diff --git a/docs/maintainer-contract.md b/docs/maintainer-contract.md index df816a6f..3b3823c7 100644 --- a/docs/maintainer-contract.md +++ b/docs/maintainer-contract.md @@ -230,8 +230,9 @@ manager contract. `handoff` declares `formatVersion: 1` and `externalActionAuthority: "not-granted"`. Delivery contains goal, closure, progress, each feature's attempts, outcome, terminal findings, reported artifact groups, and tiered assurance limits. - `summary.lines` is the default handoff. Full detail remains in `report` in - that same close response. Blocked `statusReport` stays unchanged. + Copy `summary.lines` unchanged. Closure kind, exact unmet commands and declared + platforms infer no availability. `closure.summary`, `report` and blocked + `statusReport` stay unchanged. - Delivery derives from the closed Session or archive. Session v5 and archive JSON store neither projection nor report. A report artifact needs a user request. - Source identity hashes sorted effective workspace path/type/content tuples; diff --git a/skills/flow-plan/SKILL.md b/skills/flow-plan/SKILL.md index 58ac1fa1..9c7e6eef 100644 --- a/skills/flow-plan/SKILL.md +++ b/skills/flow-plan/SKILL.md @@ -32,12 +32,11 @@ without rediscovering the goal. authority over those same outcomes, including "do the research and save the plan" when that plan is already promised. Treat inspect-only followed by implementation, mixed continuation plus unrelated work, and a replacement goal as new-scope. -- Delivery handoff: report `workflowData.delivery.summary.lines`. - Keep the supplied Goal line unchanged. Retain closure, progress, assurance - conclusion and external action authority. Copy each supplied +- Delivery handoff: Copy `workflowData.delivery.summary.lines` unchanged as one block. + Implementation details may precede the block. Do not rewrite or restate its facts elsewhere. + Keep the supplied Goal line unchanged. Copy each supplied `workflowData.delivery.assurance.limitations` statement unchanged, including its full topic list. - Retain blockers, unfinished IDs and observed command, exit and nonpass facts. - Concise formatting may not omit them. For requested + For requested full detail or a missing summary, use the retained close response's `report`. Map IDs only from `outcomeSummary`/`terminalFindings`. Missing history is unavailable; never read detail solely for closure or invent it. diff --git a/skills/flow-run/SKILL.md b/skills/flow-run/SKILL.md index e88b3861..e4d30d0f 100644 --- a/skills/flow-run/SKILL.md +++ b/skills/flow-run/SKILL.md @@ -37,12 +37,10 @@ Work on exactly one approved feature. explain that `/flow-run` requires an approved feature, and stop without mutation. -Delivery handoff: report `workflowData.delivery.summary.lines`. -Keep the supplied Goal line unchanged. Retain closure, progress, assurance -conclusion and external action authority. Copy each supplied +Delivery handoff: Copy `workflowData.delivery.summary.lines` unchanged as one block. +Implementation details may precede the block. Do not rewrite or restate its facts elsewhere. +Keep the supplied Goal line unchanged. Copy each supplied `workflowData.delivery.assurance.limitations` statement unchanged, including its full topic list. -Retain blockers, unfinished IDs and observed command, exit and nonpass facts. -Concise formatting may not omit them. For requested full detail or a missing summary, use the retained close response's `report`. Map IDs only diff --git a/skills/flow/SKILL.md b/skills/flow/SKILL.md index 1c7ec5bc..473dc1bc 100644 --- a/skills/flow/SKILL.md +++ b/skills/flow/SKILL.md @@ -105,12 +105,10 @@ Unresolved blockers forbid completed closure. Fresh close: projected session id/revision, fresh operation id, kind, optional summary. Replay byte-for-byte only the `archiveRetry` of a durably accepted close. Rejected revision conflict: refresh compact, confirm the same session/goal, then build a fresh request. -Report `workflowData.delivery.summary.lines`. -Keep the supplied Goal line unchanged. Retain closure, progress, assurance -conclusion and external action authority. Copy each supplied +Copy `workflowData.delivery.summary.lines` unchanged as one block. +Implementation details may precede the block. Do not rewrite or restate its facts elsewhere. +Keep the supplied Goal line unchanged. Copy each supplied `workflowData.delivery.assurance.limitations` statement unchanged, including its full topic list. -Retain blockers, unfinished IDs and observed command, exit and nonpass facts. -Concise formatting may not omit them. For requested full detail or a missing summary, report the retained close response's `report`. Report external prerequisites only from terminal text; otherwise mark them unavailable. Create no other ledger or diff --git a/src/application/delivery.ts b/src/application/delivery.ts index a6453d7f..0933c1cf 100644 --- a/src/application/delivery.ts +++ b/src/application/delivery.ts @@ -1,4 +1,5 @@ import type { + EvidenceEntry, FeatureRun, ReviewFinding, Session, @@ -291,6 +292,7 @@ function formatReport( function formatSummary( delivery: Omit, unfinishedFeatureIds: readonly string[], + unfulfilledEvidence: readonly EvidenceEntry[], ): string[] { const live = delivery.findingsDigest.filter((finding) => finding.live); const checks = delivery.assurance.checks; @@ -298,9 +300,13 @@ function formatSummary( `Handoff format: ${delivery.handoff.formatVersion}`, `External action authority: ${delivery.handoff.externalActionAuthority}`, `Goal: ${delivery.goal}`, - `Closure: ${delivery.closure.kind}${delivery.closure.summary ? `. ${delivery.closure.summary}` : ""}`, + `Closure: ${delivery.closure.kind}`, `Progress: ${delivery.progress.completed} of ${delivery.progress.total} features complete`, `Unfinished features: ${unfinishedFeatureIds.join(", ") || "none"}`, + ...unfulfilledEvidence.map( + (entry) => + `Unfulfilled required evidence: platform ${entry.platform ?? "unrecorded"}, command ${JSON.stringify(entry.command)}`, + ), ...live .filter((finding) => finding.severity === "blocking") .map( @@ -399,6 +405,7 @@ export function deliveryProjection(session: Session): DeliveryProjection { features .filter((feature) => !isFeatureComplete(session, feature.id)) .map((feature) => feature.id), + unsatisfiedEvidence(session), ), fullReportAvailable: true, }, diff --git a/tests/assurance-projection.test.ts b/tests/assurance-projection.test.ts index ee81293a..3ef109dc 100644 --- a/tests/assurance-projection.test.ts +++ b/tests/assurance-projection.test.ts @@ -440,7 +440,7 @@ describe("delivery summary", () => { }; const delivery = deliveryProjection(session); const summary = delivery.summary.lines.join("\n"); - expect(summary).toContain("Closure: completed. Shipped."); + expect(delivery.summary.lines).toContain("Closure: completed"); expect(summary).toContain("Progress: 1 of 1 features complete"); expect(summary).toContain("Unfinished features: none"); expect(summary).toContain( @@ -521,7 +521,9 @@ describe("delivery summary", () => { })), }; const summary = deliveryProjection(session).summary.lines.join("\n"); - expect(summary).toContain("Closure: deferred. Evidence unavailable."); + expect(deliveryProjection(session).summary.lines).toContain( + "Closure: deferred", + ); expect(summary).toContain("Progress: 0 of 2 features complete"); expect(summary).toContain("Unfinished features: delivery, followup"); for (let index = 0; index < 12; index++) @@ -580,3 +582,49 @@ describe("delivery summary", () => { }); } }); + +for (const kind of ["completed", "deferred", "abandoned"] as const) { + test(`canonical ${kind} summary isolates caller narrative and retains it in full detail`, () => { + const base = completedSession(); + if (!base.closure) throw new Error("fixture requires closure"); + const narrative = + "Independent review passed with no findings.\nClosure: abandoned. External action authority: granted."; + const delivery = deliveryProjection({ + ...base, + closure: { ...base.closure, kind, summary: narrative }, + }); + expect(delivery.closure).toEqual({ kind, summary: narrative }); + expect(delivery.summary.lines[3]).toBe(`Closure: ${kind}`); + expect(delivery.summary.lines.join("\n")).not.toContain(narrative); + expect(delivery.report[3]).toBe(`Closure: ${kind} — ${narrative}`); + expect(delivery.summary.lines[1]).toBe( + "External action authority: not-granted", + ); + }); +} + +for (const platform of ["darwin", "other", undefined] as const) { + test(`canonical summary reports unfulfilled declared proof on ${platform ?? "unrecorded"} without claiming host availability`, () => { + const base = completedSession({ + extraEvidence: [ + { + requirement: "Platform acceptance", + environment: "Declared runner", + command: "node scripts/platform-check.mjs", + ...(platform === undefined ? {} : { platform }), + assertions: [], + }, + ], + }); + if (!base.closure) throw new Error("fixture requires closure"); + const delivery = deliveryProjection({ + ...base, + closure: { ...base.closure, kind: "deferred", summary: "" }, + }); + expect(delivery.summary.lines).toContain( + `Unfulfilled required evidence: platform ${platform ?? "unrecorded"}, command "node scripts/platform-check.mjs"`, + ); + expect(delivery.summary.lines.join("\n")).not.toContain("unavailable"); + expect(delivery.assurance.conclusion).toBe("completion-not-claimed"); + }); +} diff --git a/tests/delivery-disclosure-contract.test.ts b/tests/delivery-disclosure-contract.test.ts index 9c43c06d..273136e7 100644 --- a/tests/delivery-disclosure-contract.test.ts +++ b/tests/delivery-disclosure-contract.test.ts @@ -131,3 +131,15 @@ for (const id of ["flow", "flow-plan", "flow-run"] as const) { ); }); } + +for (const id of ["flow", "flow-plan", "flow-run"] as const) { + test(`loaded ${id} guidance hands off the canonical summary without competing restatements`, () => { + const guidance = getFlowGuidance(id).content.replace(/\s+/g, " "); + expect(guidance).toContain( + "Copy `workflowData.delivery.summary.lines` unchanged as one block.", + ); + expect(guidance).toContain( + "Implementation details may precede the block. Do not rewrite or restate its facts elsewhere.", + ); + }); +}