From e084daa2c9d45ce2edeb8af9858b324cee9ebc38 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 20:58:20 +0200 Subject: [PATCH 01/61] config+context: Add structured plan decisions and verification scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define an optional structured plan layer for design decisions, verification scenarios, and explicit criterion/task coverage. Extend generated plan templates and contract fixtures, then document the format and its relationships in shared context so future workflow phases can consume it without changing legacy plans. Plan: plan-decisions-and-verification-scenarios (T01–T05) Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 115 +++++++ config/pkl/check-generated.sh | 3 + .../structured-plan-template-schema-check.pkl | 11 + .../renderers/generation-contract-check.pkl | 36 ++ context/context-map.md | 2 +- ...an-decisions-and-verification-scenarios.md | 325 ++++++++++++++++++ context/sce/config-generation.md | 4 +- context/sce/shared-context-plan-workflow.md | 33 ++ context/sce/workflow-vocabulary.md | 11 + 9 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl create mode 100644 context/plans/plan-decisions-and-verification-scenarios.md diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 80483fa22..b824cf7bf 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -491,6 +491,17 @@ entirely rather than writing them empty. {One or two paragraphs: what changes, where, and why. State whether this extends existing behavior, replaces it, or preserves work already in progress.} +## Design decisions + +{Optional. Include only real architectural choices this plan commits to. Remove +the section when there are none.} + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + ## Acceptance criteria How this plan is proven complete. Each criterion is observable and names the @@ -499,8 +510,19 @@ performs final validation. - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` + - Decisions: {decision IDs, e.g. D1; omit the field when no decision applies} + - Scenarios: {scenario IDs, e.g. S1, S2} - [ ] AC2: {observable outcome} - Validate: `{command, assertion, or inspection that proves AC2}` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} ### Full validation @@ -543,6 +565,7 @@ recorded. Remove the section otherwise.} - [ ] T01: `{single intent title}` (status:todo) - Task ID: T01 + - Covers: {acceptance criterion IDs, e.g. AC1} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: {task IDs, or none} - Done when: {clear acceptance for one coherent change} @@ -551,6 +574,7 @@ recorded. Remove the section otherwise.} - [ ] T02: `{single intent title}` (status:todo) - Task ID: T02 + - Covers: {acceptance criterion IDs, e.g. AC2} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: T01 - Done when: {clear acceptance for one coherent change} @@ -576,6 +600,7 @@ invent one: `None.` is the expected answer for a well-specified change.} ```markdown - [ ] T02: `Add /auth/refresh endpoint` (status:todo) - Task ID: T02 + - Covers: AC1 - Scope: In — route handler, token validation logic, response schema. Out — refresh token rotation policy (covered in T03), client-side storage changes. - Dependencies: T01 - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired or invalid token; targeted tests pass; OpenAPI spec updated. @@ -583,6 +608,47 @@ invent one: `None.` is the expected answer for a well-specified change.} - Context synchronization: pending ``` +## Filled-in structured plan example + +```markdown +## Design decisions + +- D1: The existing session store validates refresh tokens; no new token service is added. + - Boundary: `src/auth/` owns token validation; route handlers call it and never read the session store directly. + - Rationale: The session store already tracks revocation, so one owner keeps revocation consistent. + - Rejected: Stateless refresh tokens — they cannot be revoked before they expire. + - Trade-offs: Every refresh costs one session-store read. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` returns a signed JWT for a valid refresh token and 401 for an expired or revoked one. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh` with its 200 and 401 responses. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A refresh token that has not expired but is revoked in the session store. + - When: The client calls `POST /auth/refresh` with that token. + - Then: The response is 401 and no JWT is issued. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"`, created by T01. + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — route handler, session-store validation call, OpenAPI entry. Out — refresh token rotation policy. + - Dependencies: none + - Done when: The endpoint returns a JWT or 401 as AC1 states, the revoked-token test exists and passes, and the OpenAPI entry is present. + - Verify: `pnpm test src/auth/refresh.test.ts`. + - Context synchronization: pending +``` + ## Acceptance criteria rules - Acceptance criteria describe the finished system, not the work. Prefer "the @@ -600,6 +666,25 @@ invent one: `None.` is the expected answer for a well-specified change.} change request. If a criterion has no task that could satisfy it, the task stack is incomplete. +## Design decision rules + +- Record a design decision only for a real architectural choice the plan + commits to, such as an ownership boundary, a data shape, an interface, or an + alternative a reviewer would otherwise reopen. +- Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:`. +- A design decision belongs to this plan only. It does not replace an + architecture decision record. + +## Verification scenario rules + +- A scenario states one behavior that an acceptance criterion depends on, as + `Given:`, `When:`, `Then:`, and `Check:`. +- `Then:` states an exact observable result or invariant, not an intention. +- `Check:` names the planned test, command, or named inspection that would + prove `Then:`. Name a test only when it already exists or a task in the stack + creates it. + ## Task rules - Number tasks sequentially as `T01..T0N`. @@ -615,6 +700,32 @@ invent one: `None.` is the expected answer for a well-specified change.} acceptance checks. - Order tasks so each one's declared dependencies precede it. +## Reference rules + +- A plan is structured when it has a `### Verification scenarios` heading or + any task carries `Covers:`. Every other plan is a legacy plan, and the rules + in this section do not apply to it. +- IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, + `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry + starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and + `- [ ] T01:`. +- Define each ID exactly once. +- Number decisions, criteria, and scenarios sequentially in order of first + assignment. Never renumber or reuse a decision, criterion, or scenario ID; a + withdrawn entry keeps its ID and says that it is withdrawn. +- Each reference field fits on one line: `Decisions:` and `Scenarios:` on an + acceptance criterion, and `Covers:` on a task. Its value is a list of IDs + separated by a comma and a space, for example `S1, S2`. +- Every referenced ID resolves to a definition in the same plan. +- Every acceptance criterion carries `Scenarios:`, with scenario IDs or the + literal `none (inspection)` when only an inspection can prove it. Omit + `Decisions:` when no decision applies. +- Every acceptance criterion is named by at least one task's `Covers:`. +- Every scenario is referenced by at least one acceptance criterion's + `Scenarios:`. +- A planned `Check:` is not execution evidence. Only `/validate` running or + performing the check verifies a scenario. + ## No validation task - The last task in the stack is an ordinary implementation task. Do not author a @@ -656,6 +767,10 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. +- A legacy plan stays legacy. Add design decisions, verification scenarios, or + `Covers:` only when the correction explicitly asks to adopt the structured + format. +- Give each new decision, criterion, or scenario the next unused ID of its kind. """ diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 23a4ad29b..cf1c40718 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -151,6 +151,9 @@ expect_pkl_fixture_failure \ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/compact-plan-template-schema-check.pkl" \ "generated plan-template.md must use the compact task and completion schema and must not reference the removed Goal, Boundaries (in/out of scope), Verification notes, Implementation evidence, Verification evidence, or Context synchronization handoff fields" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl" \ + "generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-compact-completion-writing-check.pkl" \ "sce-next-task task-execution.md must record execution facts directly on the completed task and must not construct a separate Implementation evidence, Verification evidence, or Context synchronization handoff record" diff --git a/config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl b/config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl new file mode 100644 index 000000000..0fd0aae44 --- /dev/null +++ b/config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl @@ -0,0 +1,11 @@ +import "../generation-contract-check.pkl" as contract + +local documents = new Mapping { + ["config/.opencode/skills/sce-change-to-plan/references/plan-template.md"] = + "## Design decisions\n\n## Acceptance criteria\n\n - Validate: `check`\n\n## Task stack\n\n - Task ID: T01" +} + +result = if ( + contract.assertStructuredPlanTemplateSchema.apply(documents) +) "negative fixture unexpectedly accepted a plan template without verification scenarios or coverage fields" +else throw("generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 1e8edebf9..b7a89e547 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1001,6 +1001,34 @@ hidden assertCompactPlanTemplateSchema = (documents: Mapping) -> ) "generated plan template: compact task/completion schema present, legacy fields absent" else throw("generated plan-template.md must use the compact task and completion schema and must not reference the removed Goal, Boundaries (in/out of scope), Verification notes, Implementation evidence, Verification evidence, or Context synchronization handoff fields") +local requiredStructuredPlanTemplateTokens = new Listing { + "## Design decisions" + " - Boundary: " + " - Rationale: " + " - Rejected: " + " - Trade-offs: " + " - Decisions: " + " - Scenarios: " + "### Verification scenarios" + " - Given: " + " - When: " + " - Then: " + " - Check: " + " - Covers: " + "## Design decision rules" + "## Verification scenario rules" + "## Reference rules" +} + +hidden assertStructuredPlanTemplateSchema = (documents: Mapping) -> + if ( + documents.every((path, text) -> + !path.endsWith("/skills/sce-change-to-plan/references/plan-template.md") + || requiredStructuredPlanTemplateTokens.every((token) -> text.contains(token)) + ) + ) "generated plan template: design decisions, verification scenarios, coverage fields, and reference rules present" + else throw("generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules") + local planTemplateOwnedPolicyTokens = new Listing { "Acceptance criteria describe the finished system, not the work." "Every criterion carries a `Validate:` line." @@ -1010,6 +1038,13 @@ local planTemplateOwnedPolicyTokens = new Listing { "Split any candidate task that would require multiple independent commits" "The last task in the stack is an ordinary implementation task." "Preserve completed tasks, their `(status:done)` markers" + "Record a design decision only for a real architectural choice the plan" + "`Then:` states an exact observable result or invariant, not an intention." + "A plan is structured when it has a `### Verification scenarios` heading or" + "Never renumber or reuse a decision, criterion, or scenario ID" + "Every acceptance criterion is named by at least one task's `Covers:`." + "A planned `Check:` is not execution evidence." + "A legacy plan stays legacy." } local requiredPlanAuthoringTemplateReferences = new Listing { @@ -1331,6 +1366,7 @@ contractChecks { ["no-stale-sync-debt-text"] = assertNoStaleSyncDebtText.apply(generatedArtifacts) ["plan-review-sync-debt-recovery"] = assertPlanReviewSyncDebtRecovery.apply(workflowDocuments) ["compact-plan-template-schema"] = assertCompactPlanTemplateSchema.apply(workflowDocuments) + ["structured-plan-template-schema"] = assertStructuredPlanTemplateSchema.apply(workflowDocuments) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) ["next-task-compact-completion-writing"] = assertNextTaskCompactCompletionWriting.apply(workflowDocuments) ["plan-review-reads-completed-record"] = assertPlanReviewReadsCompletedRecord.apply(workflowDocuments) diff --git a/context/context-map.md b/context/context-map.md index 40476a1da..2952617ed 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -58,7 +58,7 @@ Feature/domain context: - `context/sce/claude-raw-hook-capture.md` (removed feature: the former `sce hooks claude-capture` raw-capture route and its supporting types, replaced by the active `diff-trace` and `conversation-trace` intakes) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with explicit config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary, logger and deliberately non-object-safe generic async telemetry boundaries with static capability dispatch and future-aware polling scope, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, managed OTLP trace/log export (credential-gated, hardcoded canonical collector, allowlisted fields, bounded fail-open lifecycle, reserved metrics endpoint), and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) -- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) +- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, structured vs legacy plan detection), clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) - `context/sce/plan-code-overlap-map.md` (overlap matrix for thin OpenCode Plan/Code routing agents and six workflow packages, including task-only context synchronization ownership) - `context/sce/dedup-ownership-table.md` (canonical owner-vs-consumer boundaries for six workflow packages, canonical phase modules, the shared synchronization skeleton, and thin OpenCode agents) diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md new file mode 100644 index 000000000..ca7439f56 --- /dev/null +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -0,0 +1,325 @@ +# Plan: plan-decisions-and-verification-scenarios + +## Change summary + +Extend the SCE plan format with structured **Design decisions** (`D1..Dn`) and +**Verification scenarios** (`S1..Sn`), and link them to the existing acceptance +criteria (`AC1..ACn`) and tasks (`T01..T0N`) through fixed one-line reference +fields. Acceptance criteria gain `Decisions:` and `Scenarios:`, and tasks gain +`Covers:`. The result is a plan that maps AC → D, AC → S, T → AC, and S → +planned check with plain line matching, so `/validate` can record +scenario-level evidence and a later PR-review workflow can find where each +criterion is implemented and proven. + +PR #305 (`context/plans/mutation-cursor-ref-reconciliation-wiring.md` on branch +`ref-recon`) showed the value of this: D1–D4 recorded the architecture, AC1–AC15 +cited scenario IDs from their `Validate:` lines, and T05's coverage map tracked +each of S1–S39 as `Verified`, `Partially verified`, or `Not verified`. That plan +built this structure by hand and spread it across `Validate:` prose, a +traceability table, and a task's evidence block. This change makes a smaller +version of it part of the format. Each concept gets one fixed home, and +scenarios are authored at planning time rather than reconstructed during a +task. + +The change extends existing behavior and does not replace it. Two PRs carry it: + +- **PR 1 (T01–T03), plan format and authoring:** canonical plan template, + plan-authoring phase, and `/next-task` plan review, gate, and execution + consumption. +- **PR 2 (T04–T05), scenario-based validation:** the `/validate` phase and both + Validation Report formats. + +Plans without these sections, which includes every plan on disk today, keep +working unchanged. Historical plans are not migrated. + +### Format at a glance + +```markdown +## Design decisions + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + +## Acceptance criteria + +- [ ] AC1: {observable outcome} + - Validate: `{check}` + - Decisions: D1 + - Scenarios: S1, S2 + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} + +## Task stack + +- [ ] T01: `{title}` (status:todo) + - Task ID: T01 + - Covers: AC1 + - ... +``` + +This plan uses the format it introduces. + +## Design decisions + +- D1: The plan's own Markdown carries the structure. Each relationship is a fixed one-line field. + - Boundary: The persisted plan schema owned by `references/plan-template.md`, generated from `changeToPlanPlanTemplate` in `config/pkl/base/workflow-change-to-plan.pkl`. + - Rationale: The plan already carries its other machine-read state (checkboxes, `(status:…)`, `Context synchronization:`) as fixed line prefixes. Reusing that convention means a regex over `^\s*- (D|AC|S)[1-9][0-9]*:`, `^\s+- T[0-9]{2}:` and `^\s+- (Decisions|Scenarios|Covers|Check|Validate): ` recovers every relationship without an LLM. It also keeps one plan representation. + - Rejected: A YAML or JSON sidecar. It creates a second source of truth that drifts from the prose. A traceability table like PR #305's. Tables are hard to edit incrementally, and they duplicate links the fields already hold. A CLI parser or linter command. That is new runtime surface, which the request rules out. + - Trade-offs: Prose contracts are enforced by workflow instructions plus generated-text contract checks. No running code parses a real plan in this change. Extractability is a property of the format, not a shipped tool. +- D2: Every AC in a structured plan carries an explicit `Scenarios:` field. Its value is a list of scenario IDs or the literal `none (inspection)`. `Decisions:` is omitted when no decision applies. + - Boundary: The acceptance-criteria shape and the relationship rules in the plan template. + - Rationale: "Behavioral" versus "structural" is a judgement call. An explicit marker turns a missing scenario into a detectable defect (the field is absent) instead of an ambiguous one (did the author forget, or is it inspection-only?). Decisions are optional by design, so a missing `Decisions:` field is not a defect. + - Rejected: Allowing `Scenarios:` to be omitted for inspection criteria. The gap could then not be detected without judgement. + - Trade-offs: Documentation-only plans gain one boilerplate line per AC. +- D3: The structured rules apply only to *structured plans*: a plan with a `### Verification scenarios` heading, or with any task carrying `Covers:`. Every other plan is a legacy plan and follows today's behavior exactly. + - Boundary: Format detection in `/next-task` plan review and in `/validate`. Existing-plan revision rules in the template. + - Rationale: This keeps the change backward compatible without migrating anything. Detection uses one deterministic textual test. Revising a legacy plan keeps it legacy unless the correction explicitly asks to adopt the structured format. + - Rejected: Requiring D/S on every plan. That would break `/validate` on every historical plan. Inferring structure from `S\d` mentions in prose. PR #305 cites scenarios inside `Validate:` text, and treating that as structure would misclassify it. + - Trade-offs: A half-converted plan (for example, `Covers:` without a scenarios section) counts as structured. Its missing pieces are reported as relationship failures rather than tolerated. +- D4: An AC is verified only by evidence `/validate` gathers in its own run. Every scenario gets one status: `Verified`, `Partially verified`, or `Not verified`. An AC is checked only when its `Validate:` check passes and every one of its scenarios is `Verified`. + - Boundary: Evidence rules in the validation phase (`references/validation.md`) and the scenario section of both Validation Report formats. + - Rationale: Task completion, a task's recorded `Verify` outcomes, and a `Check:` line describe intent or history. They are not proof of the finished system. PR #305's rule that "a related unit test never upgrades a production-path scenario" is the precedent for refusing indirect evidence. + - Rejected: Inheriting scenario status from completed tasks' `Verify` records. That is cheaper, but it lets stale or narrower evidence count. + - Trade-offs: `/validate` runs more checks on structured plans, and it fails when a planned check cannot be run or located. + +## Acceptance criteria + +How this plan is proven complete. `/validate` runs these checks; no task in the +stack performs final validation. + +- [ ] AC1: On every target, the generated `sce-change-to-plan/references/plan-template.md` defines the optional `## Design decisions` section (`Boundary:`, `Rationale:`, `Rejected:`, `Trade-offs:`), the AC fields `Decisions:` and `Scenarios:`, the `### Verification scenarios` section (`Given:`, `When:`, `Then:`, `Check:`), and the task field `Covers:`. Every existing compact task and completion field, the `Full validation` and `Context sync` sections, and the task context synchronization lifecycle section are kept unchanged. + - Validate: the planned `structured-plan-template-schema` contract check and the existing `compact-plan-template-schema` check both pass inside `nix run .#pkl-check-generated`. + - Decisions: D1, D2 + - Scenarios: S1, S2 +- [ ] AC2: The plan template states the deterministic relationship rules. These cover the ID grammar, unique definitions, sequential first-assignment numbering with no renumbering or reuse, the comma-separated reference-list grammar, resolution of every referenced ID, `Scenarios:` required on every AC of a structured plan (IDs or `none (inspection)`), every AC covered by at least one task, every scenario referenced by at least one AC, structured-plan detection, and the statement that a planned `Check:` is not execution evidence. + - Validate: inspection of the generated plan template against the rule list in this criterion, plus the planned `structured-plan-template-schema` check. + - Decisions: D1, D2, D3 + - Scenarios: S3 +- [ ] AC3: The plan-authoring phase authors decisions, then criteria, then scenarios, then tasks. It applies the template's new rules instead of restating them. It forbids manufactured decisions and invented test names, and it requires a reference-consistency pass before writing the plan. + - Validate: the planned `plan-authoring-structured-references` check and the updated plan-template ownership check pass in `nix run .#pkl-check-generated`. + - Decisions: D1 + - Scenarios: S4 +- [ ] AC4: For a structured plan, `/next-task` plan review includes the selected task's covered ACs, their scenarios, and their related decisions in the `ready` result, and the implementation gate shows them in an optional section. For a legacy plan, both outputs are unchanged. Readiness criteria, approval behavior, the `approved` flag, synchronization-debt handling, and continuation are unchanged. A dangling reference is reported as a review risk and does not block the task. + - Validate: the planned `next-task-structured-references` check passes, and the existing next-task semantic checks still pass in `nix run .#pkl-check-generated`. + - Decisions: D3 + - Scenarios: S5, S6 +- [ ] AC5: Task execution never checks an AC checkbox and never records a scenario status. Covered-AC scenarios may shape the task's own `Verify` checks. The completion record keeps its current fields; `Covers:` stays as authored, and `Files changed` stays baseline-relative. + - Validate: the planned `next-task-structured-references` check and the existing `next-task-compact-completion-writing` check pass. + - Decisions: D4 + - Scenarios: S6 +- [ ] AC6: For a structured plan, `/validate` runs or performs every scenario's `Check:` itself and gives each scenario exactly one status. It checks an AC only when its `Validate:` check passes and all of that AC's scenarios are `Verified`. Task completion, recorded task `Verify` outcomes, and the plan's own claims never count as evidence. + - Validate: the planned `validate-scenario-evidence` check passes, and the existing `validation-repair` and `validate-decision-sync-boundary` checks still pass. + - Decisions: D4 + - Scenarios: S7, S8 +- [ ] AC7: In a structured plan, a relationship-integrity defect makes validation `failed` and is listed under failed checks. The defects are a duplicate ID, a dangling reference, an AC without `Scenarios:`, an AC that no task covers, and a scenario that no AC references. Such a defect is never silently tolerated. + - Validate: the planned `validate-scenario-evidence` check asserts this rule in the generated `validation.md`. + - Decisions: D3, D4 + - Scenarios: S9 +- [ ] AC8: For structured plans, both Validation Report formats (the plan-file report and the returned report) contain a scenario-verification section. Each entry maps one scenario ID to its ACs, its evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and its status. AC evidence lines cite scenario IDs. The section is omitted for legacy plans. + - Validate: the planned `validation-report-scenario-section` check passes in `nix run .#pkl-check-generated`. + - Decisions: D4 + - Scenarios: S10 +- [ ] AC9: Existing plans stay valid and untouched. No file under `context/plans/` other than this plan changes. The generated plan-review and validation references keep today's behavior for plans without the new sections. + - Validate: `git diff --stat main...HEAD -- context/plans/` lists only this plan; the planned `next-task-structured-references` and `validate-scenario-evidence` checks assert the legacy-plan sentences. + - Decisions: D3 + - Scenarios: S11 +- [ ] AC10: Generated output stays deterministic and target-neutral. The artifact path count stays 145, and every new contract check has a checked-in negative fixture that `config/pkl/check-generated.sh` proves fails with its diagnostic. + - Validate: `nix run .#pkl-check-generated` and `nix flake check` pass. + - Scenarios: S2, S12 + +### Verification scenarios + +- S1: Structured template schema renders on all targets + - Given: T01 is implemented and generation runs from the canonical Pkl sources. + - When: `nix run .#pkl-check-generated` evaluates `generation-contract-check.pkl`. + - Then: For the OpenCode, Claude, Pi, and Codex `plan-template.md` outputs, the planned `structured-plan-template-schema` check reports success. The `compact-plan-template-schema` check still reports success. + - Check: `nix run .#pkl-check-generated` exits 0, and its output lists both checks as passing. +- S2: Missing structured-format token is rejected + - Given: A negative fixture supplies a `plan-template.md` that lacks `### Verification scenarios` or `Covers:`. + - When: `config/pkl/check-generated.sh` runs its fixture-failure step. + - Then: Evaluating the fixture fails with the exact `structured-plan-template-schema` diagnostic. + - Check: The planned fixture under `config/pkl/renderers/fixtures/` is registered with `expect_pkl_fixture_failure`, and `nix run .#pkl-check-generated` passes. +- S3: Relationships are extractable by line matching + - Given: The canonical template's filled-in structured example in `config/pkl/base/workflow-change-to-plan.pkl`. + - When: The example is matched with `nix shell nixpkgs#ripgrep -c rg -n '^\s*- ((D|AC|S)[1-9][0-9]*|T[0-9]{2}):|^\s+- (Decisions|Scenarios|Covers|Check|Validate): '`. + - Then: Every definition and every reference field is matched on its own line. Each reference value is either a comma-separated ID list or `none (inspection)`, and every referenced ID has a matching definition in the example. + - Check: Inspection of the rg output against the example block. +- S4: Plan authoring cites, and does not restate, the template rules + - Given: T02 is implemented. + - When: The plan-template ownership check and the planned `plan-authoring-structured-references` check evaluate the generated `plan-authoring.md`. + - Then: `plan-authoring.md` cites the new template rule sections by name, states the D→AC→S→T order and the pre-write consistency pass, and contains none of the template-owned rule sentences. A negative fixture that omits the citation fails. + - Check: `nix run .#pkl-check-generated` exits 0. +- S5: Structured plan surfaces coverage in review and gate + - Given: T03 is implemented, and a structured plan whose selected task has `Covers: AC1`, where AC1 lists `Decisions: D1` and `Scenarios: S1`. + - When: The generated `plan-review.md` and the `output.md` implementation gate are inspected. + - Then: The `ready` result contract names the covered ACs, their scenario IDs with `Then:` and `Check:`, and the related decisions. The gate has an optional coverage section that is omitted when the plan is legacy. No readiness bullet, approval sentence, or status was removed or added. + - Check: The planned `next-task-structured-references` check, plus `git diff` inspection of the readiness and gate sections in `config/pkl/base/workflow-next-task.pkl`. +- S6: Execution does not claim plan-level verification + - Given: T03 is implemented. + - When: The generated `task-execution.md` is evaluated. + - Then: It states that task execution never checks an AC checkbox and never records scenario status. It keeps the compact completion-writing tokens, and it keeps `Covers:` as authored. + - Check: The planned `next-task-structured-references` check and the existing `next-task-compact-completion-writing` check pass in `nix run .#pkl-check-generated`. +- S7: AC is not verified from task completion or plan claims + - Given: T04 is implemented, and a structured plan where every task is done and AC2's scenario S3 has a `Check:` that was never executed. + - When: The generated `validation.md` rules are applied. + - Then: S3 is `Not verified`, AC2 stays unchecked, and the result is `failed`, even though the covering task is done and its `Verify` record says passed. + - Check: The planned `validate-scenario-evidence` check asserts the evidence-source rule and the AC-verification rule in the generated `validation.md`. +- S8: Partial evidence does not upgrade a scenario + - Given: A scenario whose check was run only at a narrower level than its `Then:` requires. + - When: Validation classifies it. + - Then: The scenario is `Partially verified`, its AC stays unchecked, and the gap appears under failed checks with a required action. + - Check: The planned `validate-scenario-evidence` check asserts the three status names and the `Partially verified` rule. +- S9: Broken references fail validation + - Given: A structured plan with a duplicate `S2`, an AC citing an undefined `D5`, and an AC that no task's `Covers:` names. + - When: Validation runs its relationship-integrity step. + - Then: Each defect is listed under failed checks, and the status is `failed`. + - Check: The planned `validate-scenario-evidence` check asserts the integrity rule list in the generated `validation.md`. +- S10: Report maps scenarios to actual evidence + - Given: T05 is implemented. + - When: The generated `validation-report.md` and the report variants in `validation.md` are inspected. + - Then: Each format has a scenario-verification section with the fields scenario ID, ACs, evidence kind, evidence, result, and status. The section is marked omitted for legacy plans, and AC lines cite scenario IDs. + - Check: The planned `validation-report-scenario-section` check passes, and its negative fixture fails with its diagnostic. +- S11: Legacy plans keep the legacy path + - Given: `context/plans/remove-checkout-id.md` and `context/plans/agent-trace-export-readers.md`, which have no `### Verification scenarios` and no `Covers:`. + - When: The generated `plan-review.md` and `validation.md` detection rules are applied. + - Then: Both plans are classified as legacy. Review, gate, validation, and report behavior match today's. Neither plan file changes. + - Check: Inspection of the detection sentence in both generated references against these two plans, plus `git diff --stat main...HEAD -- context/plans/`. +- S12: Parity and artifact inventory hold + - Given: All tasks are implemented. + - When: `nix flake check` runs. + - Then: `pkl-generated` passes with `expectedArtifactPathCount` still 145, target-neutral reference parity passes, and every other check passes. + - Check: `nix flake check` exits 0. + +### Full validation + +- `nix run .#pkl-check-generated` +- `nix flake check` + +### Context sync + +- `context/sce/shared-context-plan-workflow.md`: the structured plan format and the authoring order. +- `context/sce/shared-context-code-workflow.md`: `/next-task` coverage consumption, `/validate` scenario evidence, and the integrity rules. +- `context/sce/config-generation.md`: the new contract checks and fixtures. +- `context/sce/workflow-vocabulary.md`: the terms *design decision*, *verification scenario*, *structured plan*, and *legacy plan*, distinguishing a plan-level decision from an ADR written by `sce-decision`. + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `config/pkl/base/workflow-change-to-plan.pkl` (`changeToPlanPlanTemplate`, `changeToPlanPlanAuthoring`), `config/pkl/base/workflow-next-task.pkl` (`nextTaskPlanReviewReference`, `nextTaskTaskExecutionReference`, `renderImplementationGate`/`nextTaskOutputReference`), `config/pkl/base/workflow-validate.pkl` (`renderValidationSkillBody`, `renderValidationResult`, `renderValidationReport`), `config/pkl/renderers/generation-contract-check.pkl`, new fixtures under `config/pkl/renderers/fixtures/`, and `config/pkl/check-generated.sh` fixture registration. +- **Out of scope:** SCE PR Review; any Rust CLI command, parser, linter, storage, or API; migrating or editing historical plans; ADR (`sce-decision`) behavior; context-sync behavior; the unrendered legacy `planAuthoringPackage`/`renderPlanTemplate` locals in `workflow-change-to-plan.pkl`. +- **Constraints:** Edit canonical Pkl only. Generated trees are never committed. Rendered references must stay identical across all targets. `/validate` stays verification-only. Task IDs, checkbox and status markers, completion fields, and the synchronization lifecycle stay unchanged apart from the new `Covers:` field. Existing contract diagnostics keep their exact wording. +- **Non-goal:** Reproducing PR #305's traceability table, review-finding mapping, authoring-time observation section, or mutation-style evidence ("did the test fail on a broken implementation?"). + +## Assumptions + +- The live plan template is `changeToPlanPlanTemplate`, rendered through `structuredComposite.referenceDocuments`. The `PLAN_TEMPLATE` in the `planAuthoringPackage` skills mapping is not rendered by any renderer, so it is left alone. +- Adding an optional coverage section to the implementation gate, omitted for legacy plans, does not change the execution gate. The approval question, the `approved` flag, readiness criteria, and statuses are unchanged. +- The contract-check names used in this plan (`structured-plan-template-schema`, `plan-authoring-structured-references`, `next-task-structured-references`, `validate-scenario-evidence`, `validation-report-scenario-section`) are planned names. They do not exist yet, and the implementing task may refine them. +- New fixtures add no generated artifact paths, so `expectedArtifactPathCount` stays 145. + +## Task stack + +- [x] T01: `Add design decisions, verification scenarios, and coverage fields to the plan template` (status:done) + - Task ID: T01 + - Covers: AC1, AC2, AC9, AC10 + - Scope: In — `changeToPlanPlanTemplate` in `config/pkl/base/workflow-change-to-plan.pkl`: + - Add the optional `## Design decisions` section after `## Change summary`. + - Add `Decisions:` and `Scenarios:` to the AC template, and add `### Verification scenarios` between the AC list and `### Full validation`. + - Add `Covers:` after `Task ID:` in the task template and in the filled-in example. + - Add new `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. The reference rules cover the ID grammar, uniqueness, sequential numbering with no renumbering or reuse (withdrawn entries keep their ID), the list grammar, resolution, coverage, structured-plan detection, and the statement that a planned check is not evidence. + - Add a short structured filled-in example: one D, one behavioral AC with a scenario, one `none (inspection)` AC, and one task. + - Extend `Updating an existing plan`: a legacy plan stays legacy unless the correction asks for the structured format, and new IDs are appended. + - Add the `structured-plan-template-schema` assertion to `generation-contract-check.pkl`, and add the new rule sentences to `planTemplateOwnedPolicyTokens`. + - Add one negative fixture and register it in `config/pkl/check-generated.sh`. + Out — plan-authoring steps, `/next-task`, `/validate`, and historical plans. + - Dependencies: none + - Done when: The generated template on every target contains the new sections, fields, and rules, with every existing compact field intact. Both schema checks pass, and the new fixture fails with its diagnostic. + - Verify: `nix run .#pkl-check-generated` — passed (145 files; `structured-plan-template-schema`, `compact-plan-template-schema`, and `plan-template-policy-ownership` all report success; the new fixture fails with its exact diagnostic). S3 rg inspection of the canonical structured example — passed: every definition (`D1`, `AC1`, `AC2`, `S1`, `T01`) and reference field matched on its own line, each value is an ID list or `none (inspection)`, and every referenced ID resolves. Deviation: S3's literal pattern `^\s*- ((D|AC|S)[1-9][0-9]*|T[0-9]{2}):` cannot match checkbox lines (`- [ ] AC1:`, `- [ ] T01:`), so the inspection used `^\s*- (\[[ x]\] )?((D|AC|S)[1-9][0-9]*|T[0-9]{2}):|^\s+- (Decisions|Scenarios|Covers|Check|Validate): `; the template's `Reference rules` state the checkbox-prefixed definition form. + - Completed: 2026-10-23 + - Files changed: config/pkl/base/workflow-change-to-plan.pkl, config/pkl/renderers/generation-contract-check.pkl, config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl, config/pkl/check-generated.sh, context/plans/plan-decisions-and-verification-scenarios.md + - Result: `changeToPlanPlanTemplate` now has an optional `## Design decisions` section, AC `Decisions:`/`Scenarios:` fields, `### Verification scenarios`, task `Covers:` (template and filled-in task example), `Design decision rules`, `Verification scenario rules`, `Reference rules`, a filled-in structured plan example, and legacy/next-ID rules under `Updating an existing plan`. Added the `structured-plan-template-schema` assertion, seven new template-owned policy tokens, and one registered negative fixture. + - Context impact: `context/sce/shared-context-plan-workflow.md` (structured plan format and reference rules), `context/sce/config-generation.md` (new contract check and fixture). + - Context synchronization: synced + +- [ ] T02: `Author decisions, scenarios, and coverage in the plan-authoring phase` (status:todo) + - Task ID: T02 + - Covers: AC3 + - Scope: In — `changeToPlanPlanAuthoring`: + - Add a step that records design decisions before the AC step. Add only real architectural choices, and omit the section when there are none. + - Make the AC step author `Decisions:` and `Scenarios:` per the template. + - Add a scenario-authoring step that covers normal, boundary, failure, cancellation, concurrency, and recovery cases only where they are relevant, with no invented test names. + - Make the task step add `Covers:`. + - Before writing, run a reference-consistency pass that applies the template's `Reference rules`. + - Update the template-rules citation sentence and `requiredPlanAuthoringTemplateReferences`. + - Add the `plan-authoring-structured-references` assertion, one negative fixture, and its registration. + Out — the template schema (T01) and the summary or output layouts. + - Dependencies: T01 + - Done when: The generated `plan-authoring.md` states the D→AC→S→T order and the consistency pass, cites the template rule sections, and restates no template-owned rule. The checks pass, and the fixture fails as expected. + - Verify: `nix run .#pkl-check-generated`. + - Context synchronization: pending + +- [ ] T03: `Surface covered criteria, scenarios, and decisions in /next-task review, gate, and execution` (status:todo) + - Task ID: T03 + - Covers: AC4, AC5, AC9 + - Scope: In — + - `nextTaskPlanReviewReference`: apply the structured-plan detection rule. For structured plans, the `ready` result also carries the task's `Covers:` ACs, their scenarios (`Then:` and `Check:`), and their decisions. A dangling reference becomes a review risk, not a blocker. The legacy-plan sentence keeps behavior unchanged. + - Implementation gate layout: add an optional `## Acceptance coverage` section before `## Verification`, omitted when the plan is legacy. + - `nextTaskTaskExecutionReference`: covered scenarios may inform the task's `Verify` checks. Execution never checks AC checkboxes or records scenario status, and `Covers:` is preserved. + - Add the `next-task-structured-references` assertion, one negative fixture, and its registration. + Out — readiness criteria, approval wording, sync-debt recovery, continuation, and context-sync references. + - Dependencies: T01 + - Done when: The generated next-task references carry the coverage contract and the legacy sentence. All existing next-task semantic checks pass unchanged, and the new fixture fails as expected. + - Verify: `nix run .#pkl-check-generated`; `git diff` review confirming that no readiness bullet, approval sentence, or status changed. + - Context synchronization: pending + +- [ ] T04: `Verify scenarios and relationship integrity in the /validate phase` (status:todo) + - Task ID: T04 + - Covers: AC6, AC7, AC9 + - Scope: In — `renderValidationSkillBody` in `config/pkl/base/workflow-validate.pkl`: + - Step 3 also collects decisions, scenarios, and `Covers:` when the plan is structured. + - Add a relationship-integrity step that applies the template's `Reference rules` and records each defect as a failed check. + - Step 4 runs or performs every scenario's `Check:` and assigns `Verified`, `Partially verified`, or `Not verified`. An AC is checked only when its `Validate:` check passes and all of its scenarios are `Verified`. Task completion, recorded `Verify` outcomes, and plan claims are excluded as evidence. + - The legacy-plan sentence keeps today's steps. + - The verification-only boundary is unchanged. + - Add the `validate-scenario-evidence` assertion, one negative fixture, and its registration. + Out — report formats (T05) and any repair or synchronization behavior. + - Dependencies: T01 + - Done when: The generated `validation.md` carries the integrity rules, the evidence rules, the three statuses, the AC rule, and the legacy sentence. The `validation-repair` and `validate-decision-sync-boundary` checks still pass, and the new fixture fails as expected. + - Verify: `nix run .#pkl-check-generated`. + - Context synchronization: pending + +- [ ] T05: `Add scenario verification to the Validation Report formats` (status:todo) + - Task ID: T05 + - Covers: AC8, AC10 + - Scope: In — + - `renderValidationReport`: add `### Scenario verification` after `### Success-criteria verification` with one entry per scenario: `S{n}` (AC IDs) → evidence kind, exact test, command, or location, actual result, and status. AC lines cite scenario IDs. The section is omitted for legacy plans. + - `renderValidationResult`: add matching sections to the validated and failed variants, and add scenario entries under `What failed` for `Partially verified` and `Not verified` scenarios. + - Add the `validation-report-scenario-section` assertion, one negative fixture, and its registration. + Out — validation execution rules (T04). + - Dependencies: T04 + - Done when: Both generated report formats carry the scenario section and the legacy-omission rule. The checks pass, the fixture fails as expected, and the artifact count is unchanged. + - Verify: `nix run .#pkl-check-generated`; `nix flake check`. + - Context synchronization: pending + +## Open questions + +- Every new check is a generated-text contract. Nothing exercises an agent actually authoring or validating a structured plan. Would a manual dry run be worth adding as a follow-up: `/validate` on a small throwaway structured plan, including one deliberately dangling reference? It is not needed to land the format. diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index fdd519f78..9e83fc936 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -10,7 +10,7 @@ Current location for canonical workflow content primitives: - `config/pkl/base/workflow-catalog.pkl` (typed six-workflow catalog owning command and skill slugs, titles, descriptions, argument hints, OpenCode routing roles, Claude allowed-tool metadata, and the per-workflow `optional` flag that defaults to `false` and is `true` only for `brownfield`) - `config/pkl/base/optional-workflow-manifest.pkl` (install-time projection of the catalog's optional records into the generated `config/optional-workflows.json` manifest — `schemaVersion` plus one entry per optional workflow carrying `id`, `title`, `description`, `commandSlug`, and `skillSlug`. Optionality never affects generation: all six workflows are still generated for all four targets, so the manifest exists solely to carry optional-workflow identity out of Pkl for install-time and doctor-time consumers) - `config/pkl/base/decision-skill.pkl` (canonical standalone `sce-decision` package outside the workflow catalog; renders its decision gate, one-record and immutable-ADR rules, active-only reuse and creation-time status semantics, deterministic written/not-qualified/skipped/blocked handoff, and `references/adr-template.md` for all four targets without creating a command or prompt) -- `config/pkl/base/workflow-change-to-plan.pkl` (canonical `/change-to-plan` package registering the target-neutral `SKILL.md` body plus `context-load.md`, `plan-authoring.md`, `plan-template.md`, and `output.md` package references; the plan template persists task synchronization lifecycle state and retains the plan format needed by existing plans) +- `config/pkl/base/workflow-change-to-plan.pkl` (canonical `/change-to-plan` package registering the target-neutral `SKILL.md` body plus `context-load.md`, `plan-authoring.md`, `plan-template.md`, and `output.md` package references; the plan template persists task synchronization lifecycle state, retains the plan format needed by existing plans, and defines the optional structured layer of design decisions, verification scenarios, and `Decisions`/`Scenarios`/`Covers` reference fields described in `shared-context-plan-workflow.md`) - `config/pkl/base/workflow-next-task.pkl` (canonical `/next-task` package registering the target-neutral `SKILL.md` body plus `plan-review.md`, `task-execution.md`, `context-sync.md`, `sync-report.md`, and `output.md` package references; review gates new tasks on synced lifecycle state, execution records pending before task synchronization, and the execution reference defines an explicit Git-baseline-relative handoff consumed by task synchronization) - `config/pkl/base/workflow-validate.pkl` (canonical `/validate` package registering the target-neutral `SKILL.md` body plus `validation.md`, `validation-report.md`, and `output.md` package references; `validation.md` carries the validation steps plus the validation result contract and keeps final validation observational by recording leftover debug/temp artifacts as failure evidence rather than deleting or repairing them; `/validate` does not invoke plan-level context synchronization, and `output.md` holds the `Completion` layout) - `config/pkl/base/workflow-commit.pkl` (canonical `/commit` package registering the target-neutral `SKILL.md` body plus `atomic-commit.md`, `commit-message-style.md`, and `output.md` package references; `atomic-commit.md` owns staged-diff procedure, internal result branching, and commit boundaries, `commit-message-style.md` owns message wording, and both regular and bypass paths read the phase reference only after their pre-phase gate) @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). Checked-in negative fixtures continue to prove the existing semantic contract failures, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 8e7285fd2..15e80d930 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -44,6 +44,39 @@ context-load phase and continues with the original request in the same session. - Durable repository context is read, not synchronized, during planning. - A ready plan ends with an exact `/next-task {plan-path} {task-id}` handoff; the workflow does not request implementation approval itself. +## Plan format + +`references/plan-template.md`, rendered from `changeToPlanPlanTemplate`, is the +only owner of the persisted plan schema and its rules. Alongside the compact +task, completion, and synchronization-lifecycle fields, it defines an optional +structured layer: + +- `## Design decisions` with `D1..Dn` entries, each carrying `Boundary:`, + `Rationale:`, `Rejected:`, and `Trade-offs:`. +- One-line reference fields: `Decisions:` and `Scenarios:` on each acceptance + criterion, and `Covers:` on each task. +- `### Verification scenarios` with `S1..Sn` entries, each carrying `Given:`, + `When:`, `Then:`, and `Check:`. + +The template's `Reference rules` make the relationships extractable by line +matching. Each ID is defined once. Decision, criterion, and scenario IDs are +never renumbered or reused. Every reference resolves. Every criterion has +`Scenarios:` (IDs or `none (inspection)`), is covered by a task, and every +scenario is referenced by a criterion. A planned `Check:` is not evidence. + +A plan is **structured** when it has a `### Verification scenarios` heading or +any task carries `Covers:`; every other plan is **legacy** and keeps the prior +format. A revision keeps a legacy plan legacy unless the correction explicitly +asks to adopt the structured format. + +```mermaid +flowchart LR + D["D{n} design decision"] -->|"Decisions:"| AC["AC{n} acceptance criterion"] + AC -->|"Scenarios:"| S["S{n} verification scenario"] + T["T{nn} task"] -->|"Covers:"| AC + S -->|"Check:"| C["planned test, command, or inspection"] +``` + ## Continuation contract Same-session waits preserve one of two explicit continuation shapes inside the diff --git a/context/sce/workflow-vocabulary.md b/context/sce/workflow-vocabulary.md index 5633322ab..d45d7075f 100644 --- a/context/sce/workflow-vocabulary.md +++ b/context/sce/workflow-vocabulary.md @@ -30,6 +30,17 @@ workflow context. Do not create a second glossary for the same concepts. - **Handover document** — the persisted session-continuation document under `context/handovers/`. +## Plan structure terms + +- **Design decision** — a plan-local `D{n}` entry recording an architectural + choice the plan commits to. It is not an ADR; ADRs are written only by + `sce-decision`. +- **Verification scenario** — a plan-local `S{n}` entry stating one behavior as + `Given:`, `When:`, `Then:`, and a planned `Check:`. +- **Structured plan** — a plan with a `### Verification scenarios` heading or + any task carrying `Covers:`; the plan template's `Reference rules` apply to it. +- **Legacy plan** — any other plan; it keeps the prior format and behavior. + ## Verbs Use verbs according to the object they operate on: From a23e0847a91fc3df828556491bfdb95fba55ce5b Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:04:14 +0200 Subject: [PATCH 02/61] config: Enforce structured plan authoring references The change-to-plan workflow now records design decisions, acceptance criteria, verification scenarios, and tasks in a fixed order, while citing template-owned rules and checking references before writing. Add generation-contract coverage and a negative fixture for the structured authoring requirements, and record the completed implementation evidence in the plan. Plan: plan-decisions-and-verification-scenarios (T02) Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 53 +++++++++++++++---- config/pkl/check-generated.sh | 3 ++ ...-authoring-structured-references-check.pkl | 11 ++++ .../renderers/generation-contract-check.pkl | 24 +++++++++ ...an-decisions-and-verification-scenarios.md | 10 ++-- context/sce/config-generation.md | 2 +- context/sce/shared-context-plan-workflow.md | 12 +++++ 7 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index b824cf7bf..7378f7783 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -274,9 +274,16 @@ This phase owns the planning process: - Write or revise exactly one `context/plans/{plan_name}.md`. `references/plan-template.md` is the sole owner of the persisted plan schema. -Read its `Acceptance criteria rules`, `Task rules` and `No validation task` rules, -and `Updating an existing plan` rules before authoring or revising. Apply those -rules rather than restating them here. +Read its `Acceptance criteria rules`, `Design decision rules`, +`Verification scenario rules`, `Reference rules`, `Task rules` and +`No validation task` rules, and `Updating an existing plan` rules before +authoring or revising. Apply those rules rather than restating them here. + +Author plan content in this order: design decisions, then acceptance criteria, +then verification scenarios, then tasks. When revising a legacy plan, skip the +design decision, verification scenario, `Covers:`, and reference-check work +unless the `Updating an existing plan` rules let the correction adopt the +structured format. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -397,31 +404,59 @@ the repository does not yet have. Do not explore the entire repository by default. -## 2.5 Author the acceptance criteria +## 2.5 Record design decisions + +Record the architectural choices this plan commits to under `## Design decisions`, +applying the `Design decision rules` in `references/plan-template.md`. Do not +manufacture a decision to fill the section. When the change commits to no such +choice, omit the section. + +## 2.6 Author the acceptance criteria Derive the plan-specific success outcomes and checks before slicing tasks, then apply the `Acceptance criteria rules` and exact section shape in `references/plan-template.md`. The template owns their generic validation semantics and placement. -## 2.6 Author the task stack +Give each criterion its `Decisions:` and `Scenarios:` fields as the +`Reference rules` define them. + +## 2.7 Author the verification scenarios + +Write the scenarios the acceptance criteria depend on under +`### Verification scenarios`, applying the `Verification scenario rules` in +`references/plan-template.md`. Cover normal, boundary, failure, cancellation, +concurrency, and recovery cases only where the change makes them relevant. +Do not invent test names: a `Check:` names an existing test, a test a task in +the stack creates, a command, or a named inspection. -Slice and order the plan-specific work after the acceptance criteria, applying the -`Task rules` and `No validation task` rules in `references/plan-template.md`. +## 2.8 Author the task stack + +Slice and order the plan-specific work after the acceptance criteria and +verification scenarios, applying the `Task rules` and `No validation task` rules in `references/plan-template.md`. Do not restate those generic task rules here. +Give each task its `Covers:` field as the `Reference rules` define it. + A finished stack always leaves at least one incomplete task, so the workflow can always hand off to `/next-task`. When the request resolves to a plan but produces no incomplete task, because the change is already implemented or already covered by completed tasks, set internal status `blocked` with category `no_actionable_work` instead of writing the plan. -## 2.7 Write the plan +## 2.9 Check references + +Before writing a structured plan, run a reference-consistency pass over the +complete draft that applies every `Reference rules` entry in +`references/plan-template.md`. Fix each defect in the draft before writing. Do +not write a plan that fails this pass. + +## 2.10 Write the plan Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -## 2.8 Return the result +## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index cf1c40718..363733881 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -154,6 +154,9 @@ expect_pkl_fixture_failure \ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl" \ "generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl" \ + "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-compact-completion-writing-check.pkl" \ "sce-next-task task-execution.md must record execution facts directly on the completed task and must not construct a separate Implementation evidence, Verification evidence, or Context synchronization handoff record" diff --git a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl new file mode 100644 index 000000000..d419a995e --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl @@ -0,0 +1,11 @@ +import "../generation-contract-check.pkl" as contract + +local documents = new Mapping { + ["config/.opencode/skills/sce-change-to-plan/references/plan-authoring.md"] = + "## 2.5 Author the acceptance criteria\n\n## 2.6 Author the task stack\n\n## 2.7 Write the plan" +} + +result = if ( + contract.assertPlanAuthoringStructuredReferences.apply(documents) +) "negative fixture unexpectedly accepted a plan-authoring phase without the structured authoring order or reference-consistency pass" +else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index b7a89e547..1abf08bbc 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1050,6 +1050,9 @@ local planTemplateOwnedPolicyTokens = new Listing { local requiredPlanAuthoringTemplateReferences = new Listing { "`references/plan-template.md` is the sole owner of the persisted plan schema" "`Acceptance criteria rules`" + "`Design decision rules`" + "`Verification scenario rules`" + "`Reference rules`" "`Task rules` and `No validation task` rules" "`Updating an existing plan` rules" } @@ -1067,6 +1070,26 @@ hidden assertPlanTemplatePolicyOwnership = (documents: Mapping) -> ) "sce-change-to-plan: plan-template owns persisted schema and generic authoring policy" else throw("sce-change-to-plan plan-template.md must own the persisted plan schema and generic acceptance/task/update rules; plan-authoring.md may reference those rules but must not restate them") +local requiredPlanAuthoringStructuredTokens = new Listing { + "Author plan content in this order: design decisions, then acceptance criteria,\nthen verification scenarios, then tasks." + "## 2.5 Record design decisions" + "Do not\nmanufacture a decision to fill the section." + "## 2.7 Author the verification scenarios" + "Do not invent test names" + "Give each task its `Covers:` field" + "## 2.9 Check references" + "run a reference-consistency pass over the\ncomplete draft that applies every `Reference rules` entry" +} + +hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> + if ( + documents.every((path, text) -> + !path.endsWith("/skills/sce-change-to-plan/references/plan-authoring.md") + || requiredPlanAuthoringStructuredTokens.every((token) -> text.contains(token)) + ) + ) "sce-change-to-plan plan-authoring: decisions, criteria, scenarios, and tasks authored in order with a reference-consistency pass" + else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") + local requiredNextTaskCompletionWritingTokens = new Listing { "Record directly on the completed task:" "`Files changed`" @@ -1368,6 +1391,7 @@ contractChecks { ["compact-plan-template-schema"] = assertCompactPlanTemplateSchema.apply(workflowDocuments) ["structured-plan-template-schema"] = assertStructuredPlanTemplateSchema.apply(workflowDocuments) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) + ["plan-authoring-structured-references"] = assertPlanAuthoringStructuredReferences.apply(workflowDocuments) ["next-task-compact-completion-writing"] = assertNextTaskCompactCompletionWriting.apply(workflowDocuments) ["plan-review-reads-completed-record"] = assertPlanReviewReadsCompletedRecord.apply(workflowDocuments) ["context-sync-validates-task-record"] = assertContextSyncValidatesTaskRecord.apply(workflowDocuments) diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md index ca7439f56..f2eba2a39 100644 --- a/context/plans/plan-decisions-and-verification-scenarios.md +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -260,7 +260,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/shared-context-plan-workflow.md` (structured plan format and reference rules), `context/sce/config-generation.md` (new contract check and fixture). - Context synchronization: synced -- [ ] T02: `Author decisions, scenarios, and coverage in the plan-authoring phase` (status:todo) +- [x] T02: `Author decisions, scenarios, and coverage in the plan-authoring phase` (status:done) - Task ID: T02 - Covers: AC3 - Scope: In — `changeToPlanPlanAuthoring`: @@ -274,8 +274,12 @@ Persist this field in every plan; this is durable plan state, not chat state: Out — the template schema (T01) and the summary or output layouts. - Dependencies: T01 - Done when: The generated `plan-authoring.md` states the D→AC→S→T order and the consistency pass, cites the template rule sections, and restates no template-owned rule. The checks pass, and the fixture fails as expected. - - Verify: `nix run .#pkl-check-generated`. - - Context synchronization: pending + - Verify: `nix run .#pkl-check-generated` — passed (145 files; `plan-template-policy-ownership` with the extended `requiredPlanAuthoringTemplateReferences` and the new `plan-authoring-structured-references` check both pass; the new fixture fails with its exact diagnostic). The generated `plan-authoring.md` is byte-identical across the OpenCode, Claude, Pi, and Codex targets. + - Completed: 2026-10-23 + - Files changed: config/pkl/base/workflow-change-to-plan.pkl, config/pkl/renderers/generation-contract-check.pkl, config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl, config/pkl/check-generated.sh, context/plans/plan-decisions-and-verification-scenarios.md + - Result: `changeToPlanPlanAuthoring` now cites `Design decision rules`, `Verification scenario rules`, and `Reference rules`. It states the decisions → criteria → scenarios → tasks order, with a legacy-revision skip, and adds steps 2.5 `Record design decisions` (no manufactured decisions), 2.7 `Author the verification scenarios` (relevant case kinds only, no invented test names), and 2.9 `Check references` (a pre-write reference-consistency pass). The AC and task steps now author `Decisions:`/`Scenarios:` and `Covers:`. The write and return steps were renumbered to 2.10 and 2.11. Added the three citations to `requiredPlanAuthoringTemplateReferences`, the `plan-authoring-structured-references` assertion, and one registered negative fixture. + - Context impact: `context/sce/shared-context-plan-workflow.md` (plan-authoring order and reference-consistency pass), `context/sce/config-generation.md` (new contract check and fixture). + - Context synchronization: synced - [ ] T03: `Surface covered criteria, scenarios, and decisions in /next-task review, gate, and execution` (status:todo) - Task ID: T03 diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 9e83fc936..b2b2fdbe6 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl` and `fixtures/plan-authoring-structured-references-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 15e80d930..6c1b5d80c 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -77,6 +77,18 @@ flowchart LR S -->|"Check:"| C["planned test, command, or inspection"] ``` +The plan-authoring phase writes this layer in a fixed order: design decisions, +then acceptance criteria with their `Decisions:` and `Scenarios:`, then +verification scenarios, then tasks with `Covers:`. It records only real +architectural choices and omits `## Design decisions` when there are none. It +writes scenarios only for the case kinds (normal, boundary, failure, +cancellation, concurrency, recovery) the change makes relevant, and a `Check:` +never names a test that neither exists nor is created by a task. Before writing +a structured plan, a reference-consistency pass applies every `Reference rules` +entry to the full draft and fixes each defect first. A legacy-plan revision +skips this structured work. The phase cites the template's rule sections by +name and does not restate them. + ## Continuation contract Same-session waits preserve one of two explicit continuation shapes inside the From 1e453e972d659b62e9c44a98af28514e794f4ad7 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:08:20 +0200 Subject: [PATCH 03/61] config: Carry structured coverage through next-task workflow The next-task workflow now detects structured plans and carries covered acceptance criteria, verification scenarios, and design decisions into readiness results while treating dangling references as review risks and preserving legacy-plan behavior. Its execution guidance keeps `Covers:` authored and prevents task execution from checking acceptance criteria or recording scenario status. Add the corresponding generation contract fixture and completion record. Plan: `plan-decisions-and-verification-scenarios` (T03) Co-authored-by: SCE --- config/pkl/base/workflow-next-task.pkl | 41 +++++++++++++++++++ config/pkl/check-generated.sh | 3 ++ .../next-task-structured-references-check.pkl | 15 +++++++ .../renderers/generation-contract-check.pkl | 22 ++++++++++ ...an-decisions-and-verification-scenarios.md | 10 +++-- context/sce/config-generation.md | 2 +- context/sce/shared-context-code-workflow.md | 3 ++ 7 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 config/pkl/renderers/fixtures/next-task-structured-references-check.pkl diff --git a/config/pkl/base/workflow-next-task.pkl b/config/pkl/base/workflow-next-task.pkl index 8f6725314..ddaee9c26 100644 --- a/config/pkl/base/workflow-next-task.pkl +++ b/config/pkl/base/workflow-next-task.pkl @@ -1436,6 +1436,13 @@ local renderImplementationGate = (mode: model.WorkflowRenderMode) -> """ - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. + ## Acceptance coverage + + - Include only when the plan is structured; omit this section for a legacy plan. + - List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. + - Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} @@ -1530,6 +1537,16 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. +A plan is structured when it has a `### Verification scenarios` heading or any +task carries `Covers:`. Every other plan is a legacy plan; review it exactly as +described here, with no coverage lookup and no coverage fields in the result. + +For a structured plan, read the selected task's `Covers:` IDs. For each covered +acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the +scenario and decision definitions they name. A covered ID or referenced ID with +no definition in the plan is a dangling reference. Record it as a review risk; +it does not make the task `blocked`. + ## 1.4 Determine readiness A task is `ready` when: @@ -1584,6 +1601,16 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. +For a structured plan, a `ready` result also identifies: + +- The covered acceptance criteria: each `Covers:` ID with its outcome and + `Validate:` check. +- Their verification scenarios: each scenario ID with its `Then:` and `Check:`. +- Their related design decisions: each decision ID with its decision text. +- Every dangling reference, as a review risk. + +A `ready` result for a legacy plan carries none of these coverage fields. + A `sync_debt` result must identify: - The resolved plan path. @@ -1744,6 +1771,12 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. +For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +the task's own `Verify` checks. Running a scenario's check during the task is +task-level evidence only. Task execution never checks an acceptance-criterion +checkbox and never records a scenario status; only `/validate` verifies +acceptance criteria and scenarios. + Verification may include: - Targeted tests. @@ -1779,6 +1812,7 @@ Only after successful implementation and task-level verification: facts above and before the synchronization phase is invoked. - Record material deviations or approved assumptions. - Preserve the plan's existing structure and terminology. +- Keep the task's `Covers:` field as authored. Do not mark the task complete when returning `declined`, `blocked`, or `incomplete`. @@ -1950,6 +1984,13 @@ Describe the smallest coherent implementation approach with enough detail for ap - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. +## Acceptance coverage + +- Include only when the plan is structured; omit this section for a legacy plan. +- List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. +- Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 363733881..f3ae778b0 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -157,6 +157,9 @@ expect_pkl_fixture_failure \ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl" \ "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/next-task-structured-references-check.pkl" \ + "generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-compact-completion-writing-check.pkl" \ "sce-next-task task-execution.md must record execution facts directly on the completed task and must not construct a separate Implementation evidence, Verification evidence, or Context synchronization handoff record" diff --git a/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl b/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl new file mode 100644 index 000000000..9da7c31a0 --- /dev/null +++ b/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl @@ -0,0 +1,15 @@ +import "../generation-contract-check.pkl" as contract + +local documents = new Mapping { + ["config/.opencode/skills/sce-next-task/references/plan-review.md"] = + "## 1.5 Return the result\n\nA `ready` result must identify:\n\n- One resolved plan." + ["config/.opencode/skills/sce-next-task/references/output.md"] = + "## Risks or trade-offs\n\n## Verification" + ["config/.opencode/skills/sce-next-task/references/task-execution.md"] = + "## 2.6 Verify the task\n\n## 2.7 Update the plan" +} + +result = if ( + contract.assertNextTaskStructuredReferences.apply(documents) +) "negative fixture unexpectedly accepted next-task references without structured-plan coverage or the legacy-plan path" +else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 1abf08bbc..5c1963e70 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1090,6 +1090,27 @@ hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> ) "sce-change-to-plan plan-authoring: decisions, criteria, scenarios, and tasks authored in order with a reference-consistency pass" else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") +local requiredNextTaskStructuredReferenceTokens = new Listing { + Pair("/skills/sce-next-task/references/plan-review.md", "A plan is structured when it has a `### Verification scenarios` heading or any\ntask carries `Covers:`.") + Pair("/skills/sce-next-task/references/plan-review.md", "review it exactly as\ndescribed here, with no coverage lookup and no coverage fields in the result.") + Pair("/skills/sce-next-task/references/plan-review.md", "Record it as a review risk;\nit does not make the task `blocked`.") + Pair("/skills/sce-next-task/references/plan-review.md", "For a structured plan, a `ready` result also identifies:") + Pair("/skills/sce-next-task/references/plan-review.md", "each scenario ID with its `Then:` and `Check:`.") + Pair("/skills/sce-next-task/references/plan-review.md", "A `ready` result for a legacy plan carries none of these coverage fields.") + Pair("/skills/sce-next-task/references/output.md", "## Acceptance coverage\n\n- Include only when the plan is structured; omit this section for a legacy plan.") + Pair("/skills/sce-next-task/references/task-execution.md", "Task execution never checks an acceptance-criterion\ncheckbox and never records a scenario status") + Pair("/skills/sce-next-task/references/task-execution.md", "- Keep the task's `Covers:` field as authored.") +} + +hidden assertNextTaskStructuredReferences = (documents: Mapping) -> + if ( + requiredNextTaskStructuredReferenceTokens.every((entry) -> + documents.any((path, _) -> path.endsWith(entry.first)) + && documents.every((path, text) -> !path.endsWith(entry.first) || text.contains(entry.second)) + ) + ) "sce-next-task structured references: review, gate, and execution consume coverage and keep the legacy path" + else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution") + local requiredNextTaskCompletionWritingTokens = new Listing { "Record directly on the completed task:" "`Files changed`" @@ -1392,6 +1413,7 @@ contractChecks { ["structured-plan-template-schema"] = assertStructuredPlanTemplateSchema.apply(workflowDocuments) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) ["plan-authoring-structured-references"] = assertPlanAuthoringStructuredReferences.apply(workflowDocuments) + ["next-task-structured-references"] = assertNextTaskStructuredReferences.apply(workflowDocuments) ["next-task-compact-completion-writing"] = assertNextTaskCompactCompletionWriting.apply(workflowDocuments) ["plan-review-reads-completed-record"] = assertPlanReviewReadsCompletedRecord.apply(workflowDocuments) ["context-sync-validates-task-record"] = assertContextSyncValidatesTaskRecord.apply(workflowDocuments) diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md index f2eba2a39..96ef2b88c 100644 --- a/context/plans/plan-decisions-and-verification-scenarios.md +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -281,7 +281,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/shared-context-plan-workflow.md` (plan-authoring order and reference-consistency pass), `context/sce/config-generation.md` (new contract check and fixture). - Context synchronization: synced -- [ ] T03: `Surface covered criteria, scenarios, and decisions in /next-task review, gate, and execution` (status:todo) +- [x] T03: `Surface covered criteria, scenarios, and decisions in /next-task review, gate, and execution` (status:done) - Task ID: T03 - Covers: AC4, AC5, AC9 - Scope: In — @@ -292,8 +292,12 @@ Persist this field in every plan; this is durable plan state, not chat state: Out — readiness criteria, approval wording, sync-debt recovery, continuation, and context-sync references. - Dependencies: T01 - Done when: The generated next-task references carry the coverage contract and the legacy sentence. All existing next-task semantic checks pass unchanged, and the new fixture fails as expected. - - Verify: `nix run .#pkl-check-generated`; `git diff` review confirming that no readiness bullet, approval sentence, or status changed. - - Context synchronization: pending + - Verify: `nix run .#pkl-check-generated` — passed (145 files; the new `next-task-structured-references` check reports success, all existing next-task semantic checks including `next-task-compact-completion-writing` and `plan-review-reads-completed-record` still pass, and the new fixture fails with its exact diagnostic). The generated `plan-review.md`, `output.md`, and `task-execution.md` are byte-identical across the OpenCode, Claude, Pi, and Codex targets. `git diff` review — passed: the diff only adds lines (0 removed), so no readiness bullet, approval sentence, status, sync-debt rule, or continuation changed. + - Completed: 2026-10-23 + - Files changed: config/pkl/base/workflow-next-task.pkl, config/pkl/renderers/generation-contract-check.pkl, config/pkl/renderers/fixtures/next-task-structured-references-check.pkl, config/pkl/check-generated.sh, context/plans/plan-decisions-and-verification-scenarios.md + - Result: `nextTaskPlanReviewReference` 1.3 now detects structured plans, reads the selected task's covered ACs with their scenarios and decisions, and treats dangling references as review risks rather than blockers. 1.5 lists the structured `ready` coverage fields, and a legacy-plan sentence leaves legacy review unchanged. Both copies of the implementation gate gained an optional `## Acceptance coverage` section before `## Verification`, omitted for legacy plans. `nextTaskTaskExecutionReference` 2.6 lets covered scenarios shape `Verify` checks and states that execution never checks an AC checkbox or records a scenario status. 2.7 keeps `Covers:` as authored. Added the `next-task-structured-references` assertion and one registered negative fixture. + - Context impact: `context/sce/shared-context-code-workflow.md` (`/next-task` coverage consumption and the execution boundary), `context/sce/config-generation.md` (new contract check and fixture). + - Context synchronization: synced - [ ] T04: `Verify scenarios and relationship integrity in the /validate phase` (status:todo) - Task ID: T04 diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index b2b2fdbe6..c707b0b04 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl` and `fixtures/plan-authoring-structured-references-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, and `fixtures/next-task-structured-references-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-code-workflow.md b/context/sce/shared-context-code-workflow.md index cbf24338e..e59e5c152 100644 --- a/context/sce/shared-context-code-workflow.md +++ b/context/sce/shared-context-code-workflow.md @@ -23,11 +23,13 @@ Phase names below identify canonical modules in `config/pkl/base/workflow-next-t - When the first debt-carrying task has no durable completed-task record (no `Files changed`, `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating that structure, or an incomplete write), it returns `blocked` directly with migration guidance rather than a reconstructed retry. - Otherwise it returns `sync_debt`, naming the debt task's ID and title and its own completed record — read directly from the plan by plan path and task ID — and, when its lifecycle is `blocked`, its persisted "Context synchronization blocker". - Once every completed task is `synced`, selects the requested task or the first incomplete task whose declared dependencies are complete. + - For a structured plan (see `shared-context-plan-workflow.md`), a `ready` result also carries the selected task's `Covers:` acceptance criteria with their `Validate:` checks, their scenarios' `Then:` and `Check:`, and their related design decisions. A dangling reference is a review risk, never a blocker. A legacy plan gets no coverage lookup and no coverage fields. - Returns `ready`, `sync_debt`, `blocked`, or `plan_complete`. - Sync-debt recovery: an explicit top-level `/next-task` branch — not plan-review behavior — reached on `sync_debt` before normal task selection resumes. It reads `references/context-sync.md`, then runs task context synchronization using the debt task's completed record read directly from the plan by plan path and task ID (and persisted blocker, when present). On `synced`/`no_context_change` it writes `synced` to the plan, clearing blocker fields, and re-invokes plan review to resume normal task selection. On a renewed `blocked` it writes the refreshed blocker, required action, and retry condition to the plan and stops, rendering the **Context synchronization blocked** layout — distinct from plan review's own **Review blocked** layout. 2. `sce-task-execution` - Receives the complete `ready` result. - Always presents the implementation gate before editing. + - For a structured plan, the gate adds an optional `## Acceptance coverage` section before `## Verification`; it is omitted for a legacy plan. - Waits for confirmation unless the user supplied `approved` to the command. - Captures a pre-edit Git baseline, then reports `changes.files_changed` by comparing post-edit state with that baseline, excluding unchanged unrelated staged, unstaged, @@ -37,6 +39,7 @@ Phase names below identify canonical modules in `config/pkl/base/workflow-next-t plan update, and context impact; stale, invalid, or contradictory handoffs block deterministically even under auto-approval. - Implements and verifies exactly one task, then records status and evidence in the plan. + - Covered scenarios may shape the task's own `Verify` checks, but execution never checks an acceptance-criterion checkbox or records a scenario status, and it keeps `Covers:` as authored; only `/validate` verifies acceptance criteria and scenarios. - Returns `declined`, `blocked`, `incomplete`, or `complete`. 3. `sce-task-context-sync` - Runs only from the complete successful execution handoff. From ef8a435f27ad163c66c44f76aeb62ac882bfe18f Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:12:55 +0200 Subject: [PATCH 04/61] config: Require scenario evidence during validation Extend generated sce-validate guidance for structured plans with reference-integrity checks and run-level scenario statuses while preserving the legacy-plan path. Add the generation-contract assertion and negative fixture, and update the plan and workflow documentation to record the completed validation behavior. Co-authored-by: SCE --- config/pkl/base/workflow-validate.pkl | 70 ++++++++++++++++--- config/pkl/check-generated.sh | 3 + .../validate-scenario-evidence-check.pkl | 11 +++ .../renderers/generation-contract-check.pkl | 32 ++++++++- ...an-decisions-and-verification-scenarios.md | 10 ++- context/sce/config-generation.md | 2 +- context/sce/shared-context-code-workflow.md | 3 + 7 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index cfd4617ea..af4f0f542 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -229,22 +229,74 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. - + + A plan is structured when it has a `### Verification scenarios` heading or any + task carries `Covers:`. Every other plan is a legacy plan; validate it exactly + as described here, with no scenario collection, no reference check, and no + scenario statuses. + + For a structured plan, also collect: + + - Every design decision. + - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. + - Every verification scenario with its `Then:` and `Check:`. + - Every task's `Covers:` field. + \(returnUpper.render.apply(mode)) `blocked` when the plan has no usable acceptance criteria, or when no validation commands can be determined from the plan or repository conventions. - + Prefer the plan's authored checks. Fall back to repository-primary test, lint, and format commands only when `Full validation` is absent, and record that fallback under notes on a `validated` or `failed` result. - - \(validationHeadings.stepHeading.apply(mode, 4, "Run full validation and acceptance checks")) - + + \(validationHeadings.stepHeading.apply(mode, 4, "Check plan references")) + + Skip this step for a legacy plan. + + For a structured plan, apply every entry of the plan template's + `Reference rules` to the plan. Each of these is a relationship-integrity + defect: + + - An ID defined more than once. + - A dangling reference: a `Decisions:`, `Scenarios:`, or `Covers:` ID with no + definition in the plan. + - An acceptance criterion without `Scenarios:`. + - An acceptance criterion that no task's `Covers:` names. + - A scenario that no acceptance criterion's `Scenarios:` references. + + Record each defect as a failed check under **Failed checks and follow-ups**, + naming the IDs involved. A relationship-integrity defect makes the result + `failed`; never tolerate one silently. Continue gathering the remaining + evidence. + + \(validationHeadings.stepHeading.apply(mode, 5, "Run full validation and acceptance checks")) + Run the plan's `Full validation` commands. - + Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. - + + For a structured plan, run or perform every scenario's `Check:` yourself in + this run, and compare the observed result with the scenario's `Then:`. Give + each scenario exactly one status: + + - `Verified` when this run's evidence proves its `Then:` in full. + - `Partially verified` when the check ran but proves `Then:` only in part, for + example at a narrower level than `Then:` requires. + - `Not verified` when the check failed, could not be run or located, or was + not performed. + + Only evidence gathered by this run counts. Task completion, a task's recorded + `Verify` outcomes, a planned `Check:` line, and the plan's own claims are never + evidence. Record every `Partially verified` or `Not verified` scenario as a + failed check with the gap and a required action. + + In a structured plan, an acceptance criterion is met only when its `Validate:` + check passes and every scenario its `Scenarios:` names is `Verified`. An + acceptance criterion marked `none (inspection)` is met by its `Validate:` check + alone. + Treat leftover debug-only flags, temporary files, intermediate artifacts, or local scaffolding as a failed validation check. Record the path and evidence under **Failed checks and follow-ups**; never delete or repair it during @@ -260,7 +312,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ Do not run task-by-task implementation work for incomplete tasks. That belongs to `/next-task`. - \(validationHeadings.stepHeading.apply(mode, 5, "Update the plan")) + \(validationHeadings.stepHeading.apply(mode, 6, "Update the plan")) For `validated` and `failed` outcomes: @@ -274,7 +326,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ For `blocked`, leave the plan file unchanged. - \(validationHeadings.stepHeading.apply(mode, 6, "Return the \(markdownResult.render.apply(mode))")) + \(validationHeadings.stepHeading.apply(mode, 7, "Return the \(markdownResult.render.apply(mode))")) \(returnExactlyOneMarkdownResult.render.apply(mode)): diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index f3ae778b0..cd3700b6d 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -184,6 +184,9 @@ expect_pkl_fixture_failure \ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validate-decision-sync-boundary-check.pkl" \ "generated sce-validate document must not contain a sce-decision reference or plan-context-sync wording" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl" \ + "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged" "$producer" "$repo_root" "$generated_input_root" generated_root="$generated_input_root/pkl-generated" diff --git a/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl new file mode 100644 index 000000000..16a07d899 --- /dev/null +++ b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl @@ -0,0 +1,11 @@ +import "../generation-contract-check.pkl" as contract + +local documents = new Mapping { + ["config/.opencode/skills/sce-validate/references/validation.md"] = + "## 1.4 Run full validation and acceptance checks\n\nRun the plan's `Full validation` commands." +} + +result = if ( + contract.assertValidateScenarioEvidence.apply(documents) +) "negative fixture unexpectedly accepted validation.md without relationship integrity or scenario evidence rules" +else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 5c1963e70..24a95c1ca 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1322,7 +1322,36 @@ hidden assertValidateExcludesDecisionAndPlanSync = (documents: Mapping) -> else throw("generated sce-validate document must not contain a sce-decision reference or plan-context-sync wording") -hidden assertWorkflowLanguageConsistency = (artifacts: Mapping) -> +local requiredValidateScenarioEvidenceTokens = new Listing { + "A plan is structured when it has a `### Verification scenarios` heading or any\ntask carries `Covers:`." + "validate it exactly\nas described here, with no scenario collection, no reference check, and no\nscenario statuses." + "Skip this step for a legacy plan." + "`Reference rules` to the plan." + "- An ID defined more than once." + "- A dangling reference:" + "- An acceptance criterion without `Scenarios:`." + "- An acceptance criterion that no task's `Covers:` names." + "- A scenario that no acceptance criterion's `Scenarios:` references." + "A relationship-integrity defect makes the result\n`failed`; never tolerate one silently." + "run or perform every scenario's `Check:` yourself" + "- `Verified` when" + "- `Partially verified` when" + "- `Not verified` when" + "Only evidence gathered by this run counts. Task completion, a task's recorded\n`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never\nevidence." + "an acceptance criterion is met only when its `Validate:`\ncheck passes and every scenario its `Scenarios:` names is `Verified`." +} + +hidden assertValidateScenarioEvidence = (documents: Mapping) -> + if ( + documents.any((path, _) -> path.endsWith("/skills/sce-validate/references/validation.md")) + && documents.every((path, text) -> + !path.endsWith("/skills/sce-validate/references/validation.md") + || requiredValidateScenarioEvidenceTokens.every((token) -> text.contains(token)) + ) + ) "sce-validate validation: structured plans get relationship integrity and scenario evidence; legacy plans keep the legacy path" + else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged") + +hidden assertWorkflowLanguageConsistency =(artifacts: Mapping) -> let (targets = generatedTargetRoots) if ( targets.every((target) -> @@ -1424,4 +1453,5 @@ contractChecks { ["validation-report-policy-ownership"] = assertValidationReportPolicyOwnership.apply(workflowDocuments) ["validation-observational"] = assertValidationIsObservational.apply(workflowDocuments) ["validate-decision-sync-boundary"] = assertValidateExcludesDecisionAndPlanSync.apply(workflowDocuments) + ["validate-scenario-evidence"] = assertValidateScenarioEvidence.apply(workflowDocuments) } diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md index 96ef2b88c..6ed3a50b2 100644 --- a/context/plans/plan-decisions-and-verification-scenarios.md +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -299,7 +299,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/shared-context-code-workflow.md` (`/next-task` coverage consumption and the execution boundary), `context/sce/config-generation.md` (new contract check and fixture). - Context synchronization: synced -- [ ] T04: `Verify scenarios and relationship integrity in the /validate phase` (status:todo) +- [x] T04: `Verify scenarios and relationship integrity in the /validate phase` (status:done) - Task ID: T04 - Covers: AC6, AC7, AC9 - Scope: In — `renderValidationSkillBody` in `config/pkl/base/workflow-validate.pkl`: @@ -312,8 +312,12 @@ Persist this field in every plan; this is durable plan state, not chat state: Out — report formats (T05) and any repair or synchronization behavior. - Dependencies: T01 - Done when: The generated `validation.md` carries the integrity rules, the evidence rules, the three statuses, the AC rule, and the legacy sentence. The `validation-repair` and `validate-decision-sync-boundary` checks still pass, and the new fixture fails as expected. - - Verify: `nix run .#pkl-check-generated`. - - Context synchronization: pending + - Verify: `nix run .#pkl-check-generated` — passed (exit 0, 145 files). Running `pkl eval` on `generation-contract-check.pkl` directly reports success for the new `validate-scenario-evidence` check and for the existing `validation-observational` (`validation-repair` fixture), `validate-decision-sync-boundary`, and `validation-report-policy-ownership` checks. The new fixture fails with its exact diagnostic. The generated `validation.md` is byte-identical across the OpenCode, Claude, Pi, and Codex targets. + - Completed: 2026-10-23 + - Files changed: config/pkl/base/workflow-validate.pkl, config/pkl/renderers/generation-contract-check.pkl, config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl, config/pkl/check-generated.sh, context/plans/plan-decisions-and-verification-scenarios.md + - Result: In `renderValidationSkillBody`, step 3 now detects structured plans with a legacy-plan sentence and collects decisions, AC `Decisions:`/`Scenarios:`, scenarios with `Then:`/`Check:`, and task `Covers:`. A new step 4 `Check plan references` applies the template's `Reference rules` and lists the five relationship-integrity defects; each is recorded as a failed check and makes the result `failed`. Step 5 runs or performs every scenario's `Check:` and assigns `Verified`, `Partially verified`, or `Not verified`. It excludes task completion, recorded `Verify` outcomes, planned `Check:` lines, and plan claims as evidence, and it meets an AC only when its `Validate:` check passes and all of its scenarios are `Verified` (`none (inspection)` ACs need only `Validate:`). The update and return steps were renumbered to 6 and 7. Added the `validate-scenario-evidence` assertion and one registered negative fixture. + - Context impact: `context/sce/shared-context-code-workflow.md` (`/validate` scenario evidence and integrity rules), `context/sce/config-generation.md` (new contract check and fixture). + - Context synchronization: synced - [ ] T05: `Add scenario verification to the Validation Report formats` (status:todo) - Task ID: T05 diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index c707b0b04..3f1630e06 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, and `fixtures/next-task-structured-references-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, and `fixtures/validate-scenario-evidence-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-code-workflow.md b/context/sce/shared-context-code-workflow.md index e59e5c152..1d147bd90 100644 --- a/context/sce/shared-context-code-workflow.md +++ b/context/sce/shared-context-code-workflow.md @@ -69,6 +69,9 @@ A context-sync blocker does not undo successful implementation: the task remains `/validate {plan-name-or-path}` 1. `sce-validation` verifies that implementation tasks are complete, runs the plan's full validation commands and acceptance checks, records leftover debug/temp artifacts as failure evidence without deleting or repairing them, and writes the Validation Report. + - For a structured plan (see `shared-context-plan-workflow.md`), it also collects decisions, scenarios, and `Covers:`, then checks plan references against the template's `Reference rules`: a duplicate ID, a dangling `Decisions:`/`Scenarios:`/`Covers:` ID, an acceptance criterion without `Scenarios:` or with no covering task, and an unreferenced scenario are each a failed check that makes the result `failed`. + - It runs or performs every scenario's `Check:` itself and gives each scenario exactly one status: `Verified`, `Partially verified`, or `Not verified`. Only evidence from this run counts; task completion, recorded task `Verify` outcomes, planned `Check:` lines, and plan claims never do. An acceptance criterion is met only when its `Validate:` check passes and every scenario it names is `Verified` (a `none (inspection)` criterion needs only `Validate:`). + - A legacy plan gets no scenario collection, reference check, or scenario statuses. 2. Failed or blocked validation ends the session without repair edits; repair occurs in a later implementation session and retry uses `/validate {plan-path}`. 3. A validated result is reported directly with the Validation Report path; `/validate` does not invoke plan-level context synchronization or persist a plan-sync lifecycle handoff. From bb2f58ea1af7c3c7f8c66c58b734fee954686861 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:28:38 +0200 Subject: [PATCH 05/61] config: Validate structured plan references outside fenced examples Add a test-only Pkl structural checker that validates plan headings, definitions, fields, and cross-references while ignoring fenced examples. Integrate it with generated-template contract checks and positive and negative fixtures, and align change-to-plan, next-task, and validate guidance with the same parsing rules while preserving legacy-plan behavior. Plan: plan-decisions-and-verification-scenarios; updated task IDs: none (the staged plan changes update D3, AC2, and S3). Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 23 +- config/pkl/base/workflow-next-task.pkl | 11 +- config/pkl/base/workflow-validate.pkl | 11 +- config/pkl/check-generated.sh | 85 +++++ .../fixtures/plan-structure/duplicate-id.md | 52 +++ .../plan-structure/fenced-example-plan.md | 71 ++++ .../plan-structure/inspection-only-plan.md | 30 ++ .../keywords-only-in-examples.md | 50 +++ ...acy-plan-with-fenced-structured-example.md | 36 ++ .../fixtures/plan-structure/legacy-plan.md | 113 ++++++ .../plan-structure/misplaced-definition.md | 46 +++ .../plan-structure/misplaced-field.md | 47 +++ .../plan-structure/missing-scenarios-field.md | 46 +++ .../fixtures/plan-structure/uncovered-ac.md | 50 +++ .../plan-structure/undefined-references.md | 47 +++ .../plan-structure/unreferenced-scenario.md | 52 +++ .../plan-structure/valid-structured-plan.md | 47 +++ .../plan-structure/wrong-heading-context.md | 47 +++ ...plan-template-dangling-reference-check.pkl | 14 + ...an-template-keywords-in-examples-check.pkl | 14 + .../plan-template-misplaced-field-check.pkl | 15 + .../renderers/generation-contract-check.pkl | 26 +- config/pkl/renderers/plan-structure-check.pkl | 10 + config/pkl/renderers/plan-structure.pkl | 337 ++++++++++++++++++ ...an-decisions-and-verification-scenarios.md | 16 +- context/sce/config-generation.md | 4 +- context/sce/shared-context-plan-workflow.md | 15 +- 27 files changed, 1293 insertions(+), 22 deletions(-) create mode 100644 config/pkl/renderers/fixtures/plan-structure/duplicate-id.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/fenced-example-plan.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/inspection-only-plan.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/keywords-only-in-examples.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/legacy-plan.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/misplaced-definition.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/misplaced-field.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/missing-scenarios-field.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/uncovered-ac.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/undefined-references.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/unreferenced-scenario.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/wrong-heading-context.md create mode 100644 config/pkl/renderers/fixtures/plan-template-dangling-reference-check.pkl create mode 100644 config/pkl/renderers/fixtures/plan-template-keywords-in-examples-check.pkl create mode 100644 config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl create mode 100644 config/pkl/renderers/plan-structure-check.pkl create mode 100644 config/pkl/renderers/plan-structure.pkl diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 7378f7783..31dcbcfb0 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -737,9 +737,28 @@ invent one: `None.` is the expected answer for a well-specified change.} ## Reference rules -- A plan is structured when it has a `### Verification scenarios` heading or - any task carries `Covers:`. Every other plan is a legacy plan, and the rules +- Read plan structure only from Markdown outside fenced code blocks. A fenced + code block runs from a line that opens a fence of three or more backticks or + tildes to its closing fence. Headings, definitions, and fields inside it are + example text, not plan structure. Keep fenced examples in place; never delete + one to make a plan read correctly. +- A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` + carries `Covers:`. Every other plan is a legacy plan, and the remaining rules in this section do not apply to it. +- A section runs from its heading to the next heading of any level outside + fenced code blocks. A definition counts only in its own section: `D` entries + under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` + entries under `### Verification scenarios`, and `T` entries under + `## Task stack`. A line shaped like a definition anywhere else, such as in a + fenced example or a validation report, defines nothing. +- A definition's fields are the `- Field:` lines indented one level under its + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and + `Check:` to a verification scenario; and `Covers:` to a task. +- To check a fenced example itself, extract the block's content and apply these + rules to it as a separate sample document. - IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and diff --git a/config/pkl/base/workflow-next-task.pkl b/config/pkl/base/workflow-next-task.pkl index ddaee9c26..969e14397 100644 --- a/config/pkl/base/workflow-next-task.pkl +++ b/config/pkl/base/workflow-next-task.pkl @@ -1537,8 +1537,15 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. -A plan is structured when it has a `### Verification scenarios` heading or any -task carries `Covers:`. Every other plan is a legacy plan; review it exactly as +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; review it exactly as described here, with no coverage lookup and no coverage fields in the result. For a structured plan, read the selected task's `Covers:` IDs. For each covered diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index af4f0f542..71474b71c 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -230,8 +230,15 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. - A plan is structured when it has a `### Verification scenarios` heading or any - task carries `Covers:`. Every other plan is a legacy plan; validate it exactly + Read plan structure only outside fenced code blocks; headings, definitions, and + fields inside a fenced example are not plan structure. A definition counts only + in its own section: `D` entries under `## Design decisions`, `AC` entries under + `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and + `T` entries under `## Task stack`. + + A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` carries + `Covers:`. Every other plan is a legacy plan; validate it exactly as described here, with no scenario collection, no reference check, and no scenario statuses. diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index cd3700b6d..e613494b9 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -188,6 +188,91 @@ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl" \ "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl" \ + "structured example: \`Covers:\` on \`AC2\` is misplaced; it belongs to a task" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/plan-template-keywords-in-examples-check.pkl" \ + "heading \`## Reference rules\` is missing outside fenced code blocks" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/plan-template-dangling-reference-check.pkl" \ + "structured example: \`Scenarios:\` on \`AC1\` references undefined \`S2\`" + +plan_structure_check="config/pkl/renderers/plan-structure-check.pkl" +plan_structure_fixtures="config/pkl/renderers/fixtures/plan-structure" + +expect_plan_structure_pass() { + local fixture_name="$1" + local expected_result="$2" + local output + + if ! output="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ + "$plan_structure_check" 2>&1)"; then + printf 'Plan structure fixture unexpectedly failed: %s\n%s\n' \ + "$fixture_name" "$output" >&2 + exit 1 + fi + + if [[ "$output" != *"$expected_result"* ]]; then + printf 'Plan structure fixture passed with the wrong classification: %s\n%s\n' \ + "$fixture_name" "$output" >&2 + exit 1 + fi +} + +expect_plan_structure_failure() { + local fixture_name="$1" + shift + local diagnostic + local expected_diagnostic + + if diagnostic="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ + "$plan_structure_check" 2>&1 >/dev/null)"; then + printf 'Negative plan structure fixture unexpectedly passed: %s\n' \ + "$fixture_name" >&2 + exit 1 + fi + + for expected_diagnostic in "$@"; do + if [[ "$diagnostic" != *"$expected_diagnostic"* ]]; then + printf 'Negative plan structure fixture failed without the expected diagnostic: %s\nexpected: %s\n%s\n' \ + "$fixture_name" "$expected_diagnostic" "$diagnostic" >&2 + exit 1 + fi + done +} + +structured_plan_result="structured plan: reference integrity holds" +legacy_plan_result="legacy plan: structured reference rules do not apply" + +expect_plan_structure_pass "valid-structured-plan.md" "$structured_plan_result" +expect_plan_structure_pass "inspection-only-plan.md" "$structured_plan_result" +expect_plan_structure_pass "fenced-example-plan.md" "$structured_plan_result" +expect_plan_structure_pass "legacy-plan.md" "$legacy_plan_result" +expect_plan_structure_pass "legacy-plan-with-fenced-structured-example.md" "$legacy_plan_result" + +expect_plan_structure_failure "duplicate-id.md" \ + "\`S1\` is defined more than once" +expect_plan_structure_failure "undefined-references.md" \ + "\`Decisions:\` on \`AC1\` references undefined \`D2\`" \ + "\`Scenarios:\` on \`AC1\` references undefined \`S9\`" \ + "\`Covers:\` on \`T01\` references undefined \`AC3\`" +expect_plan_structure_failure "unreferenced-scenario.md" \ + "\`S2\` is not referenced by any acceptance criterion's \`Scenarios:\`" +expect_plan_structure_failure "uncovered-ac.md" \ + "\`AC2\` is not covered by any task's \`Covers:\`" +expect_plan_structure_failure "missing-scenarios-field.md" \ + "\`AC1\` is missing \`Scenarios:\`" +expect_plan_structure_failure "misplaced-field.md" \ + "\`Check:\` on \`D1\` is misplaced; it belongs to a verification scenario" \ + "\`Covers:\` on \`AC1\` is misplaced; it belongs to a task" +expect_plan_structure_failure "misplaced-definition.md" \ + "\`Scenarios:\` on \`AC1\` references undefined \`S1\`; it appears only outside \`### Verification scenarios\`" +expect_plan_structure_failure "wrong-heading-context.md" \ + "heading \`### Verification scenarios\` is not inside \`## Acceptance criteria\`" +expect_plan_structure_failure "keywords-only-in-examples.md" \ + "\`Scenarios:\` on \`AC1\` references undefined \`S1\`; it appears only inside a fenced code block" + "$producer" "$repo_root" "$generated_input_root" generated_root="$generated_input_root/pkl-generated" diff --git a/config/pkl/renderers/fixtures/plan-structure/duplicate-id.md b/config/pkl/renderers/fixtures/plan-structure/duplicate-id.md new file mode 100644 index 000000000..e645e1907 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/duplicate-id.md @@ -0,0 +1,52 @@ +# Plan: duplicate-id + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` +- S1: Expired refresh token is rejected + - Given: An expired refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects an expired token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/fenced-example-plan.md b/config/pkl/renderers/fixtures/plan-structure/fenced-example-plan.md new file mode 100644 index 000000000..d10aeb1c1 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/fenced-example-plan.md @@ -0,0 +1,71 @@ +# Plan: fenced-example-plan + +## Change summary + +Add a refresh endpoint. The plan format looks like this: + +### Format at a glance + +```markdown +## Design decisions + +- D1: {decision} + - Boundary: {boundary} + +## Acceptance criteria + +- [ ] AC1: {observable outcome} + - Validate: `{check}` + - Decisions: D1 + - Scenarios: S1, S2 + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - Check: {planned check} + +## Task stack + +- [ ] T01: `{title}` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 +``` + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/inspection-only-plan.md b/config/pkl/renderers/fixtures/plan-structure/inspection-only-plan.md new file mode 100644 index 000000000..6cfcf1e29 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/inspection-only-plan.md @@ -0,0 +1,30 @@ +# Plan: inspection-only-plan + +## Change summary + +Document the refresh endpoint. + +## Acceptance criteria + +- [ ] AC1: The OpenAPI spec documents `POST /auth/refresh`. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +## Task stack + +- [x] T01: `Document /auth/refresh` (status:done) + - Task ID: T01 + - Covers: AC1 + - Scope: In — `openapi.yaml`. Out — code. + - Dependencies: none + - Done when: The entry exists. + - Verify: inspection of `openapi.yaml` — passed. + - Completed: 2026-10-23 + - Files changed: openapi.yaml + - Result: Added the entry. + - Context impact: none + - Context synchronization: synced + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/keywords-only-in-examples.md b/config/pkl/renderers/fixtures/plan-structure/keywords-only-in-examples.md new file mode 100644 index 000000000..31ffaa206 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/keywords-only-in-examples.md @@ -0,0 +1,50 @@ +# Plan: keywords-only-in-examples + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +```markdown +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +``` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md b/config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md new file mode 100644 index 000000000..b8dc85458 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md @@ -0,0 +1,36 @@ +# Plan: legacy-plan-with-fenced-structured-example + +## Change summary + +Describe the structured plan format without adopting it: + +~~~markdown +### Verification scenarios + +- S1: {short title} + - Then: {result} + +## Task stack + +- [ ] T01: `{title}` (status:todo) + - Covers: AC1 +~~~ + +## Acceptance criteria + +- [ ] AC1: The format description exists. + - Validate: inspection of this plan. + +## Task stack + +- [ ] T01: `Describe the format` (status:todo) + - Task ID: T01 + - Scope: In — this plan. Out — everything else. + - Dependencies: none + - Done when: The description exists. + - Verify: inspection. + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/legacy-plan.md b/config/pkl/renderers/fixtures/plan-structure/legacy-plan.md new file mode 100644 index 000000000..6dbc24531 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/legacy-plan.md @@ -0,0 +1,113 @@ +# Plan: remove-checkout-id + +## Change summary + +Remove the checkout identity feature: the `cli/src/services/checkout/` module (`resolve_git_dir`, `read_checkout_id`, `get_or_create_checkout_id`), the `/sce/checkout-id` file it creates/reads, and every diagnostic-only consumer of it in `agent_trace_storage`, `agent_trace_db` setup lifecycle, and `sce doctor`. Checkout ID is currently never persisted on Agent Trace rows and does not select the active database; it exists solely as a diagnostic label in setup output and `sce doctor --format json`. This replaces existing behavior by dropping that diagnostic surface, mirroring the repository's completed `remove-checkout-registry` removal of the related checkout registry feature. + +## Acceptance criteria + +- [x] AC1: `sce` no longer creates or reads `/sce/checkout-id`, and no code references the removed `cli/src/services/checkout/` module. + - Validate: `rg -n "services::checkout|get_or_create_checkout_id|resolve_git_dir|checkout_id" cli/src` returns no matches. +- [x] AC2: `sce setup` and `sce doctor` (text and JSON) no longer mention checkout identity. + - Validate: run `sce setup` in a scratch repo and confirm the output has no "Agent Trace checkout identity" line; run `sce doctor --format json` and confirm the payload has no `checkout_identity` key. +- [x] AC3: Durable context describes checkout identity as removed, with no remaining claim that current runtime creates, reads, or reports it. + - Validate: `rg -in "checkout identity|checkout_id|checkout-id" context/` shows only past-tense/removed framing, consistent with the existing `checkout registry` (removed) glossary entry. + +### Full validation + +- `nix flake check` +- `nix run .#pkl-check-generated` + +### Context sync + +- `context/cli/checkout-identity.md` +- `context/cli/agent-trace-storage.md` +- `context/sce/agent-trace-db.md` +- `context/sce/agent-trace-hook-doctor.md` +- `context/cli/cli-command-surface.md` +- `context/cli/service-lifecycle.md` +- `context/context-map.md` +- `context/glossary.md` +- `context/architecture.md` +- `context/overview.md` + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `cli/src/services/checkout/` (deletion) and its module declaration in `cli/src/services/mod.rs`; `checkout_id` field and usage in `cli/src/services/agent_trace_storage/mod.rs` (including affected tests); `checkout_id` field, `setup.checkout_id` usage, and the "Agent Trace checkout identity" message line in `cli/src/services/agent_trace_db/lifecycle.rs` (including affected tests); `CheckoutIdentityHealth`, `checkout_identity` field, `collect_checkout_identity_health`, and JSON rendering in `cli/src/services/doctor/{types,inspect,render}.rs`; the ten context files listed under Context sync. +- **Out of scope:** repository identity resolution (`repository_identity`), Agent Trace DB schema/migrations, `sce sync`, and how the repository-scoped Agent Trace DB path is selected or opened. The already-completed `checkout registry` removal (`checkout-registry.json`, `sce doctor dbs`) is unrelated prior work and stays untouched. +- **Constraints:** `nix flake check` (cli-tests, cli-clippy, cli-fmt) and `nix run .#pkl-check-generated` must keep passing; no replacement identity mechanism is introduced. +- **Non-goal:** deleting or migrating any `/sce/checkout-id` file already on disk. SCE never touches pre-existing on-disk artifacts (the same convention already applied to legacy `agent-trace-.db` files); a stray file is simply left inert. + +## Assumptions + +- "Remove checkout id" refers to the checkout identity infrastructure in `context/cli/checkout-identity.md` / `cli/src/services/checkout/`, not the separate, already-removed `checkout registry` feature (`remove-checkout-registry` plan). +- Removing `checkout_identity` from `sce doctor --format json` output is an acceptable diagnostic-surface change; no consumer depends on that field for correctness, only for human inspection. + +## Task stack + +- [x] T01: `Remove checkout identity code and its call sites` (status:done) + - Task ID: T01 + - Scope: In — delete `cli/src/services/checkout/mod.rs` and its `pub mod checkout;` declaration in `cli/src/services/mod.rs`; remove the `checkout_id` field and the `get_or_create_checkout_id`/`resolve_git_dir` calls (plus the now-invalid `use` import) from `cli/src/services/agent_trace_storage/mod.rs`, updating or removing tests that assert on `checkout_id`; remove the `checkout_id` field from `RepositoryDatabaseSetup`, the `setup.checkout_id` usage, and the "Agent Trace checkout identity: {}" segment of the setup message in `cli/src/services/agent_trace_db/lifecycle.rs`, updating its tests; remove `CheckoutIdentityHealth`, the `checkout_identity` field on `HookDoctorReport`, `collect_checkout_identity_health`, its two call sites, and the `checkout_identity` JSON key from `cli/src/services/doctor/{types,inspect,render}.rs`. Out — repository identity resolution, Agent Trace DB schema/migrations, `sce sync`, durable context files. + - Dependencies: none + - Done when: `cli/src/services/checkout/` no longer exists; `rg -n "services::checkout|get_or_create_checkout_id|resolve_git_dir|checkout_id|CheckoutIdentityHealth" cli/src` returns no matches; `nix flake check` passes. + - Verify: `nix flake check` (cli-tests, cli-clippy, cli-fmt); `rg -n "services::checkout|get_or_create_checkout_id|resolve_git_dir|checkout_id|CheckoutIdentityHealth" cli/src` returns nothing. + - Completed: 2026-09-13 + - Files changed: `cli/src/services/mod.rs`, `cli/src/services/checkout/mod.rs` (deleted), `cli/src/services/agent_trace_storage/mod.rs`, `cli/src/services/agent_trace_db/lifecycle.rs`, `cli/src/services/doctor/types.rs`, `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/render.rs` + - Result: Deleted `cli/src/services/checkout/` and its module declaration; removed `checkout_id`/`resolve_git_dir`/`get_or_create_checkout_id` from `agent_trace_storage::open_storage_with` (and the now-unused `context` parameter threaded through `open_storage`/`open_storage_for_hook_runtime`/`open_storage_with`, plus the resulting unused `anyhow::Context` import), the `checkout_id` field and setup-message segment in `agent_trace_db::lifecycle::RepositoryDatabaseSetup`, and `CheckoutIdentityHealth`/`collect_checkout_identity_health`/its two call sites/the `checkout_identity` JSON key in the doctor module. Updated four `agent_trace_storage` tests that asserted on `checkout_id`, renaming two whose names referenced "checkout_ids"/"checkout_id" since that behavior no longer exists. + - Verify (actual): `nix build .#checks.x86_64-linux.cli-tests .#checks.x86_64-linux.cli-clippy .#checks.x86_64-linux.cli-fmt` — all three passed (665 tests passed, 0 failed; clippy and fmt clean). `rg -n "services::checkout|get_or_create_checkout_id|resolve_git_dir|checkout_id|CheckoutIdentityHealth" cli/src` returns only `cli/src/services/agent_trace_db/repository.rs` (a pre-existing, out-of-scope schema-invariant test/comment documenting the unrelated absence of a `checkout_id` DB column — not the checkout identity service removed by this task). + - Context impact: `domain` (diagnostic-only feature removal, no architecture/ownership/terminology-boundary change). During this task's own context-synchronization pass, all ten durable context files listed under "Context sync", plus `context/sce/shared-turso-db.md` (found by sweep, not originally listed), were updated to stop describing checkout identity as active runtime behavior; see the Task context synchronization phase report for this task. T02 remains to do a final targeted review/verification pass against its own Done-when/Verify criteria. + - Assumptions/deviations: Per the reviewed assumption, the bare `rg ... cli/src` done-check text is read as excluding the noted out-of-scope `repository.rs` residual match, which is unrelated prior work (DB schema invariant) rather than a remnant of the removed checkout identity service. + - Context synchronization: synced + +- [x] T02: `Update durable context to reflect checkout identity removal` (status:done) + - Task ID: T02 + - Scope: In — `context/cli/checkout-identity.md`, `context/cli/agent-trace-storage.md`, `context/sce/agent-trace-db.md`, `context/sce/agent-trace-hook-doctor.md`, `context/cli/cli-command-surface.md`, `context/cli/service-lifecycle.md`, `context/context-map.md`, `context/glossary.md`, `context/architecture.md`, `context/overview.md`: rewrite or remove every claim that checkout identity is created, read, resolved, or reported by current runtime behavior, following the past-tense "removed" framing the `checkout registry` glossary entry already uses for the related prior removal. Out — code changes, plan files, decision records. + - Dependencies: T01 + - Done when: none of the ten files claim checkout identity is created, stored, resolved, or surfaced by current behavior; `rg -in "checkout identity|checkout_id|checkout-id" context/` shows only past-tense/removed framing. + - Verify: `rg -in "checkout identity|checkout_id|checkout-id" context/cli context/sce context/context-map.md context/glossary.md context/architecture.md context/overview.md`, reviewed by hand against the T01 code state. + - Completed: 2026-09-13 + - Files changed: `context/plans/remove-checkout-id.md` (this completion record only — no further edits to the ten scoped files were required) + - Result: Confirmed all ten scoped files (plus `context/sce/shared-turso-db.md`, updated by T01's own sweep) already carry only past-tense "removed"/"no longer" framing for checkout identity, written during T01's context-synchronization pass and committed in `b525f5e84`. Read every match returned by the scoped verify command by hand, including `context/cli/service-lifecycle.md` (one hit, already past-tense: "no global/checkout fallback path (removed by the `retire-legacy-agent-trace-db` plan)"). No residual live-behavior claim was found, so no rewrite was needed beyond T01's existing work. + - Verify (actual): `rg -in "checkout identity|checkout_id|checkout-id" context/cli context/sce context/context-map.md context/glossary.md context/architecture.md context/overview.md` — 20 matches across 8 files (`context/overview.md`, `context/architecture.md` x3, `context/glossary.md` x5, `context/context-map.md` x2, `context/cli/cli-command-surface.md`, `context/cli/checkout-identity.md` x3, `context/cli/repository-identity.md`, `context/cli/agent-trace-storage.md` x2, `context/sce/agent-trace-db.md` x3, `context/sce/agent-trace-hook-doctor.md`); every match reviewed by hand and confirmed past-tense/removed framing (e.g. "was removed by the `remove-checkout-id` plan", "no longer mention", "no longer reported"). + - Context impact: `domain` (durable-context-only verification pass; no code or architecture change). No new context impact beyond T01's own sync, which already covered these files. + - Assumptions/deviations: Per the reviewed assumption, T01's context-synchronization pass already satisfied this task's Done-when/AC3 criteria; T02 served as the planned final targeted review/verification pass rather than a fresh rewrite, and found no gap to close. + - Context synchronization: synced + +## Open questions + +None. The removal is fully precedented by the completed `remove-checkout-registry` plan and scoped to a diagnostic-only surface with no persisted-data implications. + +## Validation Report + +**Status:** validated +**Date:** 2026-09-13 + +### Commands run + +- `nix flake check` -> exit 0 (all checks passed; includes cli-tests, cli-clippy, cli-fmt, pkl-generated, and every other repository check) +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 141 files, inventory sha256 `39fe28cef0c034871a003e31f4342e405afd75263d692bf4e3bacbd1dc970fc8`) + +### Success-criteria verification + +- [x] AC1: `sce` no longer creates or reads `/sce/checkout-id`, and no code references the removed `cli/src/services/checkout/` module -> `cli/src/services/checkout/` does not exist; `rg -n "services::checkout|get_or_create_checkout_id|resolve_git_dir|checkout_id" cli/src` returns only pre-existing, out-of-scope matches in `cli/src/services/agent_trace_db/repository.rs` (a doc-comment and test asserting the schema has *no* `checkout_id` column — documented in T01's completion record as unrelated to the removed checkout identity service). +- [x] AC2: `sce setup` and `sce doctor` (text and JSON) no longer mention checkout identity -> built `.#sce` and ran it in a scratch Git repo with a fake `origin` remote: `sce setup --non-interactive --claude` output contains no "Agent Trace checkout identity" line; `sce doctor --format json` payload contains no `checkout_identity` key. +- [x] AC3: Durable context describes checkout identity as removed, with no remaining claim that current runtime creates, reads, or reports it -> `rg -in "checkout identity|checkout_id|checkout-id" context/` matches only past-tense/removed framing across every context file (root, `context/cli/`, `context/sce/`); the `context/plans/` and `context/decisions/` matches are historical plan/decision records, not current-state claims. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- None identified. diff --git a/config/pkl/renderers/fixtures/plan-structure/misplaced-definition.md b/config/pkl/renderers/fixtures/plan-structure/misplaced-definition.md new file mode 100644 index 000000000..f87184e04 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/misplaced-definition.md @@ -0,0 +1,46 @@ +# Plan: misplaced-definition + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Verification scenarios + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/misplaced-field.md b/config/pkl/renderers/fixtures/plan-structure/misplaced-field.md new file mode 100644 index 000000000..b6b14ecfe --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/misplaced-field.md @@ -0,0 +1,47 @@ +# Plan: misplaced-field + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Check: `pnpm test src/auth/refresh.test.ts` + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + - Covers: AC1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/missing-scenarios-field.md b/config/pkl/renderers/fixtures/plan-structure/missing-scenarios-field.md new file mode 100644 index 000000000..a8378fece --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/missing-scenarios-field.md @@ -0,0 +1,46 @@ +# Plan: missing-scenarios-field + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/uncovered-ac.md b/config/pkl/renderers/fixtures/plan-structure/uncovered-ac.md new file mode 100644 index 000000000..b81359b12 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/uncovered-ac.md @@ -0,0 +1,50 @@ +# Plan: uncovered-ac + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh`. + - Validate: inspection of `openapi.yaml`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/undefined-references.md b/config/pkl/renderers/fixtures/plan-structure/undefined-references.md new file mode 100644 index 000000000..94bf1978b --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/undefined-references.md @@ -0,0 +1,47 @@ +# Plan: undefined-references + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1, D2 + - Scenarios: S1, S9 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC3 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/unreferenced-scenario.md b/config/pkl/renderers/fixtures/plan-structure/unreferenced-scenario.md new file mode 100644 index 000000000..128d33734 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/unreferenced-scenario.md @@ -0,0 +1,52 @@ +# Plan: unreferenced-scenario + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` +- S2: Expired refresh token is rejected + - Given: An expired refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects an expired token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md b/config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md new file mode 100644 index 000000000..6b76a8c07 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md @@ -0,0 +1,47 @@ +# Plan: valid-structured-plan + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/wrong-heading-context.md b/config/pkl/renderers/fixtures/plan-structure/wrong-heading-context.md new file mode 100644 index 000000000..38f6a1d88 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/wrong-heading-context.md @@ -0,0 +1,47 @@ +# Plan: wrong-heading-context + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-template-dangling-reference-check.pkl b/config/pkl/renderers/fixtures/plan-template-dangling-reference-check.pkl new file mode 100644 index 000000000..e10bab85c --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-template-dangling-reference-check.pkl @@ -0,0 +1,14 @@ +import "../generation-contract-check.pkl" as contract +import "../../base/workflow-change-to-plan.pkl" as changeToPlan + +local template = changeToPlan.changeToPlanPlanTemplate + .replaceFirst(" - Decisions: D1\n - Scenarios: S1\n", " - Decisions: D1\n - Scenarios: S2\n") + +local documents = new Mapping { + ["config/.opencode/skills/sce-change-to-plan/references/plan-template.md"] = template +} + +result = + contract.assertStructuredPlanTemplateSchema.apply(documents) + + "; " + + contract.assertPlanTemplateStructure.apply(documents) diff --git a/config/pkl/renderers/fixtures/plan-template-keywords-in-examples-check.pkl b/config/pkl/renderers/fixtures/plan-template-keywords-in-examples-check.pkl new file mode 100644 index 000000000..0b8ffa0fb --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-template-keywords-in-examples-check.pkl @@ -0,0 +1,14 @@ +import "../generation-contract-check.pkl" as contract +import "../../base/workflow-change-to-plan.pkl" as changeToPlan + +local template = changeToPlan.changeToPlanPlanTemplate + .replaceFirst("\n## Reference rules\n", "\n```markdown\n## Reference rules\n```\n") + +local documents = new Mapping { + ["config/.opencode/skills/sce-change-to-plan/references/plan-template.md"] = template +} + +result = + contract.assertStructuredPlanTemplateSchema.apply(documents) + + "; " + + contract.assertPlanTemplateStructure.apply(documents) diff --git a/config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl b/config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl new file mode 100644 index 000000000..c49e353f9 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl @@ -0,0 +1,15 @@ +import "../generation-contract-check.pkl" as contract +import "../../base/workflow-change-to-plan.pkl" as changeToPlan + +local template = changeToPlan.changeToPlanPlanTemplate + .replaceFirst(" - Task ID: T01\n - Covers: AC1, AC2\n", " - Task ID: T01\n") + .replaceLast(" - Scenarios: none (inspection)\n", " - Scenarios: none (inspection)\n - Covers: AC1, AC2\n") + +local documents = new Mapping { + ["config/.opencode/skills/sce-change-to-plan/references/plan-template.md"] = template +} + +result = + contract.assertStructuredPlanTemplateSchema.apply(documents) + + "; " + + contract.assertPlanTemplateStructure.apply(documents) diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 24a95c1ca..6f3c53333 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -5,6 +5,7 @@ import "claude-content.pkl" as claude import "pi-content.pkl" as pi import "codex-content.pkl" as codex import "codex-metadata.pkl" as codexMetadata +import "plan-structure.pkl" as planStructure /// Build the expected inventory from each target's independently checked /// document inventory plus the retained non-workflow assets. This deliberately @@ -1029,6 +1030,17 @@ hidden assertStructuredPlanTemplateSchema = (documents: Mapping) -> ) "generated plan template: design decisions, verification scenarios, coverage fields, and reference rules present" else throw("generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules") +hidden assertPlanTemplateStructure = (documents: Mapping) -> + let (defects = documents + .fold(List(), (acc, path, text) -> + if (!path.endsWith("/skills/sce-change-to-plan/references/plan-template.md")) acc + else acc + planStructure.templateDefects(text).map((defect) -> "\(path): \(defect)"))) + if ( + defects.isEmpty + && documents.any((path, _) -> path.endsWith("/skills/sce-change-to-plan/references/plan-template.md")) + ) "generated plan template: headings, definitions, fields, and references are structurally valid outside fenced examples" + else throw("generated plan-template.md failed the structural reference-integrity check: " + defects.join("; ")) + local planTemplateOwnedPolicyTokens = new Listing { "Acceptance criteria describe the finished system, not the work." "Every criterion carries a `Validate:` line." @@ -1040,7 +1052,10 @@ local planTemplateOwnedPolicyTokens = new Listing { "Preserve completed tasks, their `(status:done)` markers" "Record a design decision only for a real architectural choice the plan" "`Then:` states an exact observable result or invariant, not an intention." - "A plan is structured when it has a `### Verification scenarios` heading or" + "Read plan structure only from Markdown outside fenced code blocks." + "A plan is structured when, outside fenced code blocks, it has a" + "A section runs from its heading to the next heading of any level outside" + "To check a fenced example itself, extract the block's content and apply these" "Never renumber or reuse a decision, criterion, or scenario ID" "Every acceptance criterion is named by at least one task's `Covers:`." "A planned `Check:` is not execution evidence." @@ -1091,7 +1106,9 @@ hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") local requiredNextTaskStructuredReferenceTokens = new Listing { - Pair("/skills/sce-next-task/references/plan-review.md", "A plan is structured when it has a `### Verification scenarios` heading or any\ntask carries `Covers:`.") + Pair("/skills/sce-next-task/references/plan-review.md", "Read plan structure only outside fenced code blocks;") + Pair("/skills/sce-next-task/references/plan-review.md", "A definition counts only\nin its own section:") + Pair("/skills/sce-next-task/references/plan-review.md", "A plan is structured when, outside fenced code blocks, it has a\n`### Verification scenarios` heading or any task under `## Task stack` carries\n`Covers:`.") Pair("/skills/sce-next-task/references/plan-review.md", "review it exactly as\ndescribed here, with no coverage lookup and no coverage fields in the result.") Pair("/skills/sce-next-task/references/plan-review.md", "Record it as a review risk;\nit does not make the task `blocked`.") Pair("/skills/sce-next-task/references/plan-review.md", "For a structured plan, a `ready` result also identifies:") @@ -1323,7 +1340,9 @@ hidden assertValidateExcludesDecisionAndPlanSync = (documents: Mapping) -> local requiredValidateScenarioEvidenceTokens = new Listing { - "A plan is structured when it has a `### Verification scenarios` heading or any\ntask carries `Covers:`." + "Read plan structure only outside fenced code blocks;" + "A definition counts only\nin its own section:" + "A plan is structured when, outside fenced code blocks, it has a\n`### Verification scenarios` heading or any task under `## Task stack` carries\n`Covers:`." "validate it exactly\nas described here, with no scenario collection, no reference check, and no\nscenario statuses." "Skip this step for a legacy plan." "`Reference rules` to the plan." @@ -1440,6 +1459,7 @@ contractChecks { ["plan-review-sync-debt-recovery"] = assertPlanReviewSyncDebtRecovery.apply(workflowDocuments) ["compact-plan-template-schema"] = assertCompactPlanTemplateSchema.apply(workflowDocuments) ["structured-plan-template-schema"] = assertStructuredPlanTemplateSchema.apply(workflowDocuments) + ["plan-template-structure"] = assertPlanTemplateStructure.apply(workflowDocuments) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) ["plan-authoring-structured-references"] = assertPlanAuthoringStructuredReferences.apply(workflowDocuments) ["next-task-structured-references"] = assertNextTaskStructuredReferences.apply(workflowDocuments) diff --git a/config/pkl/renderers/plan-structure-check.pkl b/config/pkl/renderers/plan-structure-check.pkl new file mode 100644 index 000000000..131af9ef0 --- /dev/null +++ b/config/pkl/renderers/plan-structure-check.pkl @@ -0,0 +1,10 @@ +import "plan-structure.pkl" as planStructure + +local planPath = read("prop:planPath") +local report = planStructure.check(read("file:\(planPath)").text) + +result = + if (!report.defects.isEmpty) + throw("plan failed the structural reference-integrity check: " + report.defects.join("; ")) + else if (report.structured) "structured plan: reference integrity holds" + else "legacy plan: structured reference rules do not apply" diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl new file mode 100644 index 000000000..b54a957bb --- /dev/null +++ b/config/pkl/renderers/plan-structure.pkl @@ -0,0 +1,337 @@ +class PlanLine { + text: String + fenced: Boolean + openFence: String + section: String +} + +class PlanField { + key: String + value: String +} + +class PlanEntry { + id: String + kind: String + fields: List +} + +class PlanReport { + structured: Boolean + defects: List +} + +local idPattern = #"(?:D[1-9][0-9]*|AC[1-9][0-9]*|S[1-9][0-9]*|T[0-9]{2,})"# +local fencePattern = Regex(#"\s*(`{3,}|~{3,})(.*)"#) +local headingPattern = Regex(#"#{1,6}\s+\S.*"#) +local definitionPattern = Regex(#"( *)-\s+(?:\[[ x]\]\s+)?("# + idPattern + #"):(?:\s.*)?"#) +local fieldPattern = Regex(#"( *)-\s+([A-Z][A-Za-z -]*):(?:\s+(.*))?"#) +local idListPattern = Regex(idPattern + "(?:, " + idPattern + ")*") + +local inspectionOnly = "none (inspection)" + +local authoritativeSections = Map( + "D", "## Design decisions", + "AC", "## Acceptance criteria", + "S", "### Verification scenarios", + "T", "## Task stack" +) + +local kindNames = Map( + "D", "a design decision", + "AC", "an acceptance criterion", + "S", "a verification scenario", + "T", "a task" +) + +local fieldOwners = Map( + "Boundary", "D", + "Rationale", "D", + "Rejected", "D", + "Trade-offs", "D", + "Validate", "AC", + "Decisions", "AC", + "Scenarios", "AC", + "Given", "S", + "When", "S", + "Then", "S", + "Check", "S", + "Covers", "T" +) + +local requiredFields = Map( + "D", List("Boundary", "Rationale", "Rejected", "Trade-offs"), + "AC", List("Validate", "Scenarios"), + "S", List("Given", "When", "Then", "Check"), + "T", List() +) + +local referenceKinds = Map( + "Decisions", "D", + "Scenarios", "S", + "Covers", "AC" +) + +local requiredPlanHeadings = List("## Acceptance criteria", "## Task stack") + +local orderedPlanHeadings = List( + "## Design decisions", + "## Acceptance criteria", + "### Verification scenarios", + "## Task stack" +) + +local templateSkeletonHeadings = List( + "## Change summary", + "## Design decisions", + "## Acceptance criteria", + "### Verification scenarios", + "### Full validation", + "### Context sync", + "## Task context synchronization lifecycle", + "## Constraints and non-goals", + "## Assumptions", + "## Task stack", + "## Open questions" +) + +local templateDocumentHeadings = List( + "## Template", + "## Filled-in structured plan example", + "## Design decision rules", + "## Verification scenario rules", + "## Reference rules" +) + +local function kindOf(id: String): String = if (id.startsWith("AC")) "AC" else id.take(1) + +local function indentOf(line: String): Int = line.length - line.trimStart().length + +local function closesFence(opener: String, marker: String, rest: String): Boolean = + marker.take(1) == opener.take(1) && marker.length >= opener.length && rest.isBlank + +function annotate(text: String): List = + text.split("\n").fold(List(), (acc: List, raw: String) -> + let (openMarker = acc.lastOrNull?.openFence ?? "") + let (currentSection = acc.lastOrNull?.section ?? "") + let (fence = fencePattern.matchEntire(raw)) + acc.add( + if (openMarker != "") + new PlanLine { + text = raw + fenced = true + openFence = + if (fence != null && closesFence(openMarker, fence.groups[1]!!.value, fence.groups[2]!!.value)) "" + else openMarker + section = currentSection + } + else if (fence != null) + new PlanLine { + text = raw + fenced = true + openFence = fence.groups[1]!!.value + section = currentSection + } + else + new PlanLine { + text = raw + fenced = false + openFence = "" + section = if (headingPattern.matchEntire(raw) != null) raw.trim() else currentSection + } + ) + ) + +function headingsOf(planLines: List): List = + planLines + .filter((line) -> !line.fenced && headingPattern.matchEntire(line.text) != null) + .map((line) -> line.text.trim()) + +local function endsEntry(line: PlanLine, indent: Int): Boolean = + !line.fenced && !line.text.isBlank && indentOf(line.text) <= indent + +function entries(planLines: List): List = + planLines + .mapIndexed((index, line) -> + let (match = if (line.fenced) null else definitionPattern.matchEntire(line.text)) + if (match == null) null + else + let (entryId = match.groups[2]!!.value) + let (entryKind = kindOf(entryId)) + let (indent = match.groups[1]!!.value.length) + if (line.section != authoritativeSections[entryKind]) null + else + new PlanEntry { + id = entryId + kind = entryKind + fields = planLines + .drop(index + 1) + .takeWhile((next) -> !endsEntry(next, indent)) + .filter((next) -> !next.fenced) + .map((next) -> fieldPattern.matchEntire(next.text)) + .filterNonNull() + .filter((field) -> field.groups[1]!!.value.length == indent + 2) + .map((field) -> new PlanField { + key = field.groups[2]!!.value + value = field.groups[3]?.value?.trim() ?? "" + }) + } + ) + .filterNonNull() + +local function fieldOf(entry: PlanEntry, key: String): PlanField? = + entry.fields.findOrNull((field) -> field.key == key) + +local function listedIds(value: String): List = + if (idListPattern.matchEntire(value) == null) List() else value.split(", ") + +local function orderDefects(headings: List, ordered: List): List = + let (present = ordered.filter((heading) -> headings.contains(heading))) + present + .drop(1) + .mapIndexed((index, heading) -> Pair(present[index], heading)) + .filter((pair) -> headings.indexOf(pair.first) > headings.indexOf(pair.second)) + .map((pair) -> "heading `\(pair.second)` must come after `\(pair.first)`") + +local function headingDefects(headings: List, required: List, ordered: List): List = + required.filter((heading) -> !headings.contains(heading)).map((heading) -> "heading `\(heading)` is missing") + + ordered + .filter((heading) -> headings.count((other) -> other == heading) > 1) + .map((heading) -> "heading `\(heading)` appears more than once") + + orderDefects(headings, ordered) + + ( + if (!headings.contains("### Verification scenarios")) List() + else + let (parent = headings + .take(headings.indexOf("### Verification scenarios")) + .findLastOrNull((heading) -> heading.startsWith("# ") || heading.startsWith("## "))) + if (parent == "## Acceptance criteria") List() + else List("heading `### Verification scenarios` is not inside `## Acceptance criteria`") + ) + +local function definitionDefects(defined: List): List = + let (ids = defined.map((entry) -> entry.id)) + ids + .filter((id) -> ids.count((other) -> other == id) > 1) + .distinct + .map((id) -> "`\(id)` is defined more than once") + + defined.flatMap((entry) -> + entry.fields + .filter((field) -> fieldOwners.containsKey(field.key) && fieldOwners[field.key] != entry.kind) + .map((field) -> "`\(field.key):` on `\(entry.id)` is misplaced; it belongs to \(kindNames[fieldOwners[field.key]])") + + requiredFields[entry.kind] + .filter((key) -> fieldOf(entry, key) == null) + .map((key) -> "`\(entry.id)` is missing `\(key):`") + ) + +local function undefinedHint(planLines: List, id: String): String = + let (candidates = planLines.filter((line) -> + let (match = definitionPattern.matchEntire(line.text)) + match != null && match.groups[2]!!.value == id)) + if (candidates.any((line) -> !line.fenced)) "; it appears only outside `\(authoritativeSections[kindOf(id)])`" + else if (candidates.any((line) -> line.fenced)) "; it appears only inside a fenced code block" + else "" + +local function referenceDefects(planLines: List, defined: List): List = + let (ids = defined.map((entry) -> entry.id)) + let (references = defined.flatMap((entry) -> + entry.fields + .filter((field) -> referenceKinds.containsKey(field.key) && fieldOwners[field.key] == entry.kind) + .map((field) -> Pair(entry.id, field)))) + let (coveredIds = references + .filter((reference) -> reference.second.key == "Covers") + .flatMap((reference) -> listedIds(reference.second.value))) + let (scenarioIds = references + .filter((reference) -> reference.second.key == "Scenarios") + .flatMap((reference) -> listedIds(reference.second.value))) + references.flatMap((reference) -> + let (owner = reference.first) + let (field = reference.second) + if (field.key == "Scenarios" && field.value == inspectionOnly) List() + else if (idListPattern.matchEntire(field.value) == null) + List("`\(field.key):` on `\(owner)` is not a comma-separated ID list") + else + field.value.split(", ").flatMap((id) -> + if (kindOf(id) != referenceKinds[field.key]) + List("`\(field.key):` on `\(owner)` names `\(id)`, which is not \(kindNames[referenceKinds[field.key]])") + else if (!ids.contains(id)) + List("`\(field.key):` on `\(owner)` references undefined `\(id)`\(undefinedHint(planLines, id))") + else List() + ) + ) + + defined + .filter((entry) -> entry.kind == "AC" && !coveredIds.contains(entry.id)) + .map((entry) -> "`\(entry.id)` is not covered by any task's `Covers:`") + + defined + .filter((entry) -> entry.kind == "S" && !scenarioIds.contains(entry.id)) + .map((entry) -> "`\(entry.id)` is not referenced by any acceptance criterion's `Scenarios:`") + +local function inspect(text: String, skeleton: Boolean): PlanReport = + let (planLines = annotate(text)) + let (headings = headingsOf(planLines)) + let (defined = entries(planLines)) + let (isStructured = + headings.contains("### Verification scenarios") + || defined.any((entry) -> entry.kind == "T" && fieldOf(entry, "Covers") != null)) + new PlanReport { + structured = isStructured + defects = + if (!isStructured) List() + else + headingDefects( + headings, + if (skeleton) templateSkeletonHeadings else requiredPlanHeadings, + if (skeleton) templateSkeletonHeadings else orderedPlanHeadings + ) + + definitionDefects(defined) + + (if (skeleton) List() else referenceDefects(planLines, defined)) + } + +function check(text: String): PlanReport = inspect(text, false) + +function fencedBlockUnder(text: String, heading: String): String? = + let (planLines = annotate(text)) + let (start = planLines.findIndexOrNull((line) -> line.fenced && line.section == heading)) + if (start == null) null + else + planLines + .drop(start + 1) + .takeWhile((line) -> line.openFence != "") + .map((line) -> line.text) + .join("\n") + +local function exampleShapeDefects(sample: String): List = + let (defined = entries(annotate(sample))) + List("D", "AC", "S", "T") + .filter((kind) -> !defined.any((entry) -> entry.kind == kind)) + .map((kind) -> "defines no \(authoritativeSections[kind]) entry") + + ( + if (defined.any((entry) -> entry.kind == "AC" && fieldOf(entry, "Decisions") != null)) List() + else List("no acceptance criterion carries `Decisions:`") + ) + + ( + if (defined.any((entry) -> entry.kind == "AC" && fieldOf(entry, "Scenarios")?.value == inspectionOnly)) List() + else List("no acceptance criterion carries `Scenarios: \(inspectionOnly)`") + ) + + defined + .filter((entry) -> entry.kind == "T" && fieldOf(entry, "Covers") == null) + .map((entry) -> "`\(entry.id)` is missing `Covers:`") + +local function sampleDefects(label: String, sample: String?, skeleton: Boolean): List = + ( + if (sample == null) List("no fenced example found") + else + let (report = inspect(sample, skeleton)) + (if (report.structured) List() else List("example is not detected as a structured plan")) + + report.defects + + exampleShapeDefects(sample) + ).map((defect) -> "\(label): \(defect)") + +function templateDefects(text: String): List = + let (headings = headingsOf(annotate(text))) + templateDocumentHeadings + .filter((heading) -> !headings.contains(heading)) + .map((heading) -> "heading `\(heading)` is missing outside fenced code blocks") + + orderDefects(headings, templateDocumentHeadings) + + sampleDefects("template", fencedBlockUnder(text, "## Template"), true) + + sampleDefects("structured example", fencedBlockUnder(text, "## Filled-in structured plan example"), false) diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md index 6ed3a50b2..244257b2c 100644 --- a/context/plans/plan-decisions-and-verification-scenarios.md +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -72,15 +72,15 @@ This plan uses the format it introduces. - D1: The plan's own Markdown carries the structure. Each relationship is a fixed one-line field. - Boundary: The persisted plan schema owned by `references/plan-template.md`, generated from `changeToPlanPlanTemplate` in `config/pkl/base/workflow-change-to-plan.pkl`. - - Rationale: The plan already carries its other machine-read state (checkboxes, `(status:…)`, `Context synchronization:`) as fixed line prefixes. Reusing that convention means a regex over `^\s*- (D|AC|S)[1-9][0-9]*:`, `^\s+- T[0-9]{2}:` and `^\s+- (Decisions|Scenarios|Covers|Check|Validate): ` recovers every relationship without an LLM. It also keeps one plan representation. + - Rationale: The plan already carries its other machine-read state (checkboxes, `(status:…)`, `Context synchronization:`) as fixed line prefixes. Reusing that convention means line matching finds every candidate definition and field without an LLM. The checkbox-aware pattern `^[[:space:]]*-[[:space:]]+(\[[ x]\][[:space:]]+)?(D[1-9][0-9]*|AC[1-9][0-9]*|S[1-9][0-9]*|T[0-9]{2,}):` matches definitions, including `- [ ] AC1:` and `- [ ] T01:`, and `^[[:space:]]*-[[:space:]]+(Decisions|Scenarios|Covers|Check|Validate):[[:space:]]` matches fields. A match counts only outside fenced code blocks and only inside its definition's own section: `D` under `## Design decisions`, `AC` under `## Acceptance criteria`, `S` under `### Verification scenarios`, and `T` under `## Task stack`, where a section ends at the next heading of any level. A fenced example, such as this plan's `Format at a glance`, therefore never defines anything. A field belongs to the definition it is indented under. Line matching only finds candidates; reference integrity is proven by the test-only structural check. It also keeps one plan representation. - Rejected: A YAML or JSON sidecar. It creates a second source of truth that drifts from the prose. A traceability table like PR #305's. Tables are hard to edit incrementally, and they duplicate links the fields already hold. A CLI parser or linter command. That is new runtime surface, which the request rules out. - - Trade-offs: Prose contracts are enforced by workflow instructions plus generated-text contract checks. No running code parses a real plan in this change. Extractability is a property of the format, not a shipped tool. + - Trade-offs: Prose contracts are enforced by workflow instructions plus generated-text contract checks. No production code parses a real plan in this change. The test-only checker in `config/pkl/renderers/plan-structure.pkl` parses the generated template's fenced examples and the checked-in plan fixtures during `nix run .#pkl-check-generated`. Extractability is a property of the format, not a shipped tool. - D2: Every AC in a structured plan carries an explicit `Scenarios:` field. Its value is a list of scenario IDs or the literal `none (inspection)`. `Decisions:` is omitted when no decision applies. - Boundary: The acceptance-criteria shape and the relationship rules in the plan template. - Rationale: "Behavioral" versus "structural" is a judgement call. An explicit marker turns a missing scenario into a detectable defect (the field is absent) instead of an ambiguous one (did the author forget, or is it inspection-only?). Decisions are optional by design, so a missing `Decisions:` field is not a defect. - Rejected: Allowing `Scenarios:` to be omitted for inspection criteria. The gap could then not be detected without judgement. - Trade-offs: Documentation-only plans gain one boilerplate line per AC. -- D3: The structured rules apply only to *structured plans*: a plan with a `### Verification scenarios` heading, or with any task carrying `Covers:`. Every other plan is a legacy plan and follows today's behavior exactly. +- D3: The structured rules apply only to *structured plans*: a plan with a `### Verification scenarios` heading, or with any task carrying `Covers:`, outside fenced code blocks. Every other plan is a legacy plan and follows today's behavior exactly. - Boundary: Format detection in `/next-task` plan review and in `/validate`. Existing-plan revision rules in the template. - Rationale: This keeps the change backward compatible without migrating anything. Detection uses one deterministic textual test. Revising a legacy plan keeps it legacy unless the correction explicitly asks to adopt the structured format. - Rejected: Requiring D/S on every plan. That would break `/validate` on every historical plan. Inferring structure from `S\d` mentions in prose. PR #305 cites scenarios inside `Validate:` text, and treating that as structure would misclassify it. @@ -101,7 +101,7 @@ stack performs final validation. - Decisions: D1, D2 - Scenarios: S1, S2 - [ ] AC2: The plan template states the deterministic relationship rules. These cover the ID grammar, unique definitions, sequential first-assignment numbering with no renumbering or reuse, the comma-separated reference-list grammar, resolution of every referenced ID, `Scenarios:` required on every AC of a structured plan (IDs or `none (inspection)`), every AC covered by at least one task, every scenario referenced by at least one AC, structured-plan detection, and the statement that a planned `Check:` is not execution evidence. - - Validate: inspection of the generated plan template against the rule list in this criterion, plus the planned `structured-plan-template-schema` check. + - Validate: inspection of the generated plan template against the rule list in this criterion, plus the planned `structured-plan-template-schema` check and the `plan-template-structure` reference-integrity check. - Decisions: D1, D2, D3 - Scenarios: S3 - [ ] AC3: The plan-authoring phase authors decisions, then criteria, then scenarios, then tasks. It applies the template's new rules instead of restating them. It forbids manufactured decisions and invented test names, and it requires a reference-consistency pass before writing the plan. @@ -150,9 +150,9 @@ stack performs final validation. - Check: The planned fixture under `config/pkl/renderers/fixtures/` is registered with `expect_pkl_fixture_failure`, and `nix run .#pkl-check-generated` passes. - S3: Relationships are extractable by line matching - Given: The canonical template's filled-in structured example in `config/pkl/base/workflow-change-to-plan.pkl`. - - When: The example is matched with `nix shell nixpkgs#ripgrep -c rg -n '^\s*- ((D|AC|S)[1-9][0-9]*|T[0-9]{2}):|^\s+- (Decisions|Scenarios|Covers|Check|Validate): '`. - - Then: Every definition and every reference field is matched on its own line. Each reference value is either a comma-separated ID list or `none (inspection)`, and every referenced ID has a matching definition in the example. - - Check: Inspection of the rg output against the example block. + - When: Candidate definitions and fields are extracted with `nix shell nixpkgs#ripgrep -c rg -n '^[[:space:]]*-[[:space:]]+(\[[ x]\][[:space:]]+)?(D[1-9][0-9]*|AC[1-9][0-9]*|S[1-9][0-9]*|T[0-9]{2,}):|^[[:space:]]*-[[:space:]]+(Decisions|Scenarios|Covers|Check|Validate):[[:space:]]' config/pkl/base/workflow-change-to-plan.pkl`, and the generator's structural reference-integrity check evaluates the generated template's fenced examples as separate sample documents. + - Then: Every definition, including the checkbox-prefixed `- [ ] AC1:` and `- [ ] T01:` lines, and every reference field is matched on its own line. The structural check confirms that, outside fenced code blocks and within each definition's own section, every ID is defined once, every field sits on its own kind of definition, each reference value is a comma-separated ID list or `none (inspection)`, every referenced ID resolves, every AC is covered by a task, and every scenario is referenced by an AC. + - Check: The rg command above for candidate extraction, plus `nix run .#pkl-check-generated`, whose `plan-template-structure` check and `config/pkl/renderers/fixtures/plan-structure/` fixtures prove reference integrity. An rg match alone does not prove reference integrity. - S4: Plan authoring cites, and does not restate, the template rules - Given: T02 is implemented. - When: The plan-template ownership check and the planned `plan-authoring-structured-references` check evaluate the generated `plan-authoring.md`. @@ -224,7 +224,7 @@ Persist this field in every plan; this is durable plan state, not chat state: ## Constraints and non-goals -- **In scope:** `config/pkl/base/workflow-change-to-plan.pkl` (`changeToPlanPlanTemplate`, `changeToPlanPlanAuthoring`), `config/pkl/base/workflow-next-task.pkl` (`nextTaskPlanReviewReference`, `nextTaskTaskExecutionReference`, `renderImplementationGate`/`nextTaskOutputReference`), `config/pkl/base/workflow-validate.pkl` (`renderValidationSkillBody`, `renderValidationResult`, `renderValidationReport`), `config/pkl/renderers/generation-contract-check.pkl`, new fixtures under `config/pkl/renderers/fixtures/`, and `config/pkl/check-generated.sh` fixture registration. +- **In scope:** `config/pkl/base/workflow-change-to-plan.pkl` (`changeToPlanPlanTemplate`, `changeToPlanPlanAuthoring`), `config/pkl/base/workflow-next-task.pkl` (`nextTaskPlanReviewReference`, `nextTaskTaskExecutionReference`, `renderImplementationGate`/`nextTaskOutputReference`), `config/pkl/base/workflow-validate.pkl` (`renderValidationSkillBody`, `renderValidationResult`, `renderValidationReport`), `config/pkl/renderers/generation-contract-check.pkl`, the test-only `config/pkl/renderers/plan-structure.pkl` checker and its `plan-structure-check.pkl` driver, new fixtures under `config/pkl/renderers/fixtures/`, and `config/pkl/check-generated.sh` fixture registration. - **Out of scope:** SCE PR Review; any Rust CLI command, parser, linter, storage, or API; migrating or editing historical plans; ADR (`sce-decision`) behavior; context-sync behavior; the unrendered legacy `planAuthoringPackage`/`renderPlanTemplate` locals in `workflow-change-to-plan.pkl`. - **Constraints:** Edit canonical Pkl only. Generated trees are never committed. Rendered references must stay identical across all targets. `/validate` stays verification-only. Task IDs, checkbox and status markers, completion fields, and the synchronization lifecycle stay unchanged apart from the new `Covers:` field. Existing contract diagnostics keep their exact wording. - **Non-goal:** Reproducing PR #305's traceability table, review-finding mapping, authoring-time observation section, or mutation-style evidence ("did the test fail on a broken implementation?"). diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 3f1630e06..16040c14a 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,11 +53,11 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections; `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, and `fixtures/validate-scenario-evidence-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/`, requiring the positive fixtures (minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example) to pass with their structured/legacy classification and each negative fixture (duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example) to fail with its expected defect; three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, and `fixtures/validate-scenario-evidence-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. -- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. +- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures through `plan-structure-check.pkl` with their expected classifications and defects, then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. Generated authored classes: diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 6c1b5d80c..223281fef 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -64,9 +64,18 @@ never renumbered or reused. Every reference resolves. Every criterion has `Scenarios:` (IDs or `none (inspection)`), is covered by a task, and every scenario is referenced by a criterion. A planned `Check:` is not evidence. -A plan is **structured** when it has a `### Verification scenarios` heading or -any task carries `Covers:`; every other plan is **legacy** and keeps the prior -format. A revision keeps a legacy plan legacy unless the correction explicitly +Plan structure is read only outside fenced code blocks, so a fenced example that +repeats real IDs or headings defines nothing and stays in place. A definition +counts only in its own section (`D` under `## Design decisions`, `AC` under +`## Acceptance criteria`, `S` under `### Verification scenarios`, `T` under +`## Task stack`, each ending at the next heading), and its fields are the lines +indented under it. To check a fenced example itself, its content is extracted +and checked as a separate sample document. `/change-to-plan`, `/next-task`, and +`/validate` all read plans this way. + +A plan is **structured** when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`; every other plan is **legacy** and keeps the prior format. A revision keeps a legacy plan legacy unless the correction explicitly asks to adopt the structured format. ```mermaid From 291399940fb2c51fd903b0fb50b936bb70033191 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:43:59 +0200 Subject: [PATCH 06/61] config: Add scenario verification to validation reports Extend generated validation workflows and contract checks so structured-plan validation reports record each scenario's acceptance criteria, evidence, actual result, and status while legacy plans remain unchanged. Register a negative fixture and update the shared context and plan record for the completed implementation. Plan: plan-decisions-and-verification-scenarios Task: T05 Co-authored-by: SCE --- config/pkl/base/workflow-validate.pkl | 58 ++++++++++--- config/pkl/check-generated.sh | 3 + ...lidation-report-scenario-section-check.pkl | 13 +++ .../renderers/generation-contract-check.pkl | 31 +++++++ ...an-decisions-and-verification-scenarios.md | 85 ++++++++++++++++--- context/sce/config-generation.md | 2 +- context/sce/shared-context-code-workflow.md | 3 +- 7 files changed, 168 insertions(+), 27 deletions(-) create mode 100644 config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index 71474b71c..da92fb414 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -435,9 +435,15 @@ local renderValidationReport = (mode: model.WorkflowRenderMode) -> """ ### Success-criteria verification - - [x] AC1: {criterion statement} -> {evidence} - - [ ] AC2: {criterion statement} -> {evidence of failure or not checked} - + - [x] AC1: {criterion statement} -> {evidence}; scenarios: S1 + - [ ] AC2: {criterion statement} -> {evidence of failure or not checked}; scenarios: S2, S3 + + ### Scenario verification + + {Only for a structured plan:} + + - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} + ### Failed checks and follow-ups - {check}: {problem}; evidence: {command output or inspection}; required: {decision or next action} @@ -465,7 +471,15 @@ local renderValidationReport = (mode: model.WorkflowRenderMode) -> """ run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. + not independently re-evaluate or reclassify criteria here. For a structured + plan, end each acceptance-criterion line with the scenario IDs its + `Scenarios:` field names, or `none (inspection)`. + - Include **Scenario verification** only for a structured plan, with exactly + one entry per scenario in plan order. Each entry gives the scenario ID, the + acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status already established by validation + execution. Omit the section for a legacy plan. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact @@ -508,9 +522,13 @@ local renderValidationResult = (mode: model.WorkflowRenderMode) -> """ ## Acceptance criteria - - [x] AC1: {criterion statement} — {evidence} - - [x] AC2: {criterion statement} — {evidence} - + - [x] AC1: {criterion statement} — {evidence}; scenarios: S1 + - [x] AC2: {criterion statement} — {evidence}; scenarios: none (inspection) + + ## Scenario verification + + - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified + ## Residual risks - {risk} @@ -550,12 +568,20 @@ local renderValidationResult = (mode: model.WorkflowRenderMode) -> """ - `{check or AC id}`: {problem} - Evidence: {command output, exit summary, or inspection finding} - Required action: {concrete repair or decision} - + - `{scenario id}` ({AC ids}): {Partially verified | Not verified} — {gap between the observed result and `Then:`} + - Evidence: {exact test, command, or inspected location and its actual result} + - Required action: {concrete repair or decision} + ## Acceptance criteria - - - [x] AC1: {criterion} — {evidence} - - [ ] AC2: {criterion} — {why unmet} - + + - [x] AC1: {criterion} — {evidence}; scenarios: S1 + - [ ] AC2: {criterion} — {why unmet}; scenarios: S2 + + ## Scenario verification + + - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified + - S2 (AC2): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Partially verified | Not verified} + ## Commands run - `{command}` -> {passed | failed | not_run} — {concise outcome summary} @@ -635,6 +661,14 @@ local renderValidationResult = (mode: model.WorkflowRenderMode) -> """ `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. + - For a structured plan, the validated and failed variants include **Scenario + verification** with exactly one entry per scenario in plan order: the scenario + ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status. Each acceptance-criterion line + cites its scenario IDs, or `none (inspection)`. In the failed variant, list + every `Partially verified` or `Not verified` scenario under **What failed**. + - Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index e613494b9..a64ce3c98 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -187,6 +187,9 @@ expect_pkl_fixture_failure \ expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl" \ "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged" +expect_pkl_fixture_failure \ + "config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl" \ + "generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl" \ diff --git a/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl b/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl new file mode 100644 index 000000000..c1fa16c6c --- /dev/null +++ b/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl @@ -0,0 +1,13 @@ +import "../generation-contract-check.pkl" as contract + +local documents = new Mapping { + ["config/.opencode/skills/sce-validate/references/validation-report.md"] = + "## Validation Report\n\n### Success-criteria verification\n\n- [x] AC1: {criterion statement} -> {evidence}" + ["config/.opencode/skills/sce-validate/references/validation.md"] = + "# Validation report\n\n## Acceptance criteria\n\n- [x] AC1: {criterion statement} — {evidence}" +} + +result = if ( + contract.assertValidationReportScenarioSection.apply(documents) +) "negative fixture unexpectedly accepted validation reports without a scenario verification section" +else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 6f3c53333..fae800db0 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1370,6 +1370,36 @@ hidden assertValidateScenarioEvidence = (documents: Mapping) -> ) "sce-validate validation: structured plans get relationship integrity and scenario evidence; legacy plans keep the legacy path" else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged") +local requiredValidationReportScenarioTokens = new Listing { + "### Scenario verification" + "- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified}" + "; scenarios: S1" + "- Include **Scenario verification** only for a structured plan, with exactly\n one entry per scenario in plan order." + "Omit the section for a legacy plan." +} + +local requiredValidationResultScenarioTokens = new Listing { + "## Scenario verification" + "; status: Verified" + "- `{scenario id}` ({AC ids}): {Partially verified | Not verified}" + "In the failed variant, list\n every `Partially verified` or `Not verified` scenario under **What failed**." + "- Omit **Scenario verification** and the scenario citations for a legacy plan." +} + +hidden assertValidationReportScenarioSection = (documents: Mapping) -> + if ( + documents.any((path, _) -> path.endsWith("/skills/sce-validate/references/validation-report.md")) + && documents.any((path, _) -> path.endsWith("/skills/sce-validate/references/validation.md")) + && documents.every((path, text) -> + if (path.endsWith("/skills/sce-validate/references/validation-report.md")) + requiredValidationReportScenarioTokens.every((token) -> text.contains(token)) + else if (path.endsWith("/skills/sce-validate/references/validation.md")) + requiredValidationResultScenarioTokens.every((token) -> text.contains(token)) + else true + ) + ) "sce-validate reports: structured plans get a scenario verification section; legacy plans omit it" + else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans") + hidden assertWorkflowLanguageConsistency =(artifacts: Mapping) -> let (targets = generatedTargetRoots) if ( @@ -1474,4 +1504,5 @@ contractChecks { ["validation-observational"] = assertValidationIsObservational.apply(workflowDocuments) ["validate-decision-sync-boundary"] = assertValidateExcludesDecisionAndPlanSync.apply(workflowDocuments) ["validate-scenario-evidence"] = assertValidateScenarioEvidence.apply(workflowDocuments) + ["validation-report-scenario-section"] = assertValidationReportScenarioSection.apply(workflowDocuments) } diff --git a/context/plans/plan-decisions-and-verification-scenarios.md b/context/plans/plan-decisions-and-verification-scenarios.md index 244257b2c..a10f3ee1d 100644 --- a/context/plans/plan-decisions-and-verification-scenarios.md +++ b/context/plans/plan-decisions-and-verification-scenarios.md @@ -96,43 +96,43 @@ This plan uses the format it introduces. How this plan is proven complete. `/validate` runs these checks; no task in the stack performs final validation. -- [ ] AC1: On every target, the generated `sce-change-to-plan/references/plan-template.md` defines the optional `## Design decisions` section (`Boundary:`, `Rationale:`, `Rejected:`, `Trade-offs:`), the AC fields `Decisions:` and `Scenarios:`, the `### Verification scenarios` section (`Given:`, `When:`, `Then:`, `Check:`), and the task field `Covers:`. Every existing compact task and completion field, the `Full validation` and `Context sync` sections, and the task context synchronization lifecycle section are kept unchanged. +- [x] AC1: On every target, the generated `sce-change-to-plan/references/plan-template.md` defines the optional `## Design decisions` section (`Boundary:`, `Rationale:`, `Rejected:`, `Trade-offs:`), the AC fields `Decisions:` and `Scenarios:`, the `### Verification scenarios` section (`Given:`, `When:`, `Then:`, `Check:`), and the task field `Covers:`. Every existing compact task and completion field, the `Full validation` and `Context sync` sections, and the task context synchronization lifecycle section are kept unchanged. - Validate: the planned `structured-plan-template-schema` contract check and the existing `compact-plan-template-schema` check both pass inside `nix run .#pkl-check-generated`. - Decisions: D1, D2 - Scenarios: S1, S2 -- [ ] AC2: The plan template states the deterministic relationship rules. These cover the ID grammar, unique definitions, sequential first-assignment numbering with no renumbering or reuse, the comma-separated reference-list grammar, resolution of every referenced ID, `Scenarios:` required on every AC of a structured plan (IDs or `none (inspection)`), every AC covered by at least one task, every scenario referenced by at least one AC, structured-plan detection, and the statement that a planned `Check:` is not execution evidence. +- [x] AC2: The plan template states the deterministic relationship rules. These cover the ID grammar, unique definitions, sequential first-assignment numbering with no renumbering or reuse, the comma-separated reference-list grammar, resolution of every referenced ID, `Scenarios:` required on every AC of a structured plan (IDs or `none (inspection)`), every AC covered by at least one task, every scenario referenced by at least one AC, structured-plan detection, and the statement that a planned `Check:` is not execution evidence. - Validate: inspection of the generated plan template against the rule list in this criterion, plus the planned `structured-plan-template-schema` check and the `plan-template-structure` reference-integrity check. - Decisions: D1, D2, D3 - Scenarios: S3 -- [ ] AC3: The plan-authoring phase authors decisions, then criteria, then scenarios, then tasks. It applies the template's new rules instead of restating them. It forbids manufactured decisions and invented test names, and it requires a reference-consistency pass before writing the plan. +- [x] AC3: The plan-authoring phase authors decisions, then criteria, then scenarios, then tasks. It applies the template's new rules instead of restating them. It forbids manufactured decisions and invented test names, and it requires a reference-consistency pass before writing the plan. - Validate: the planned `plan-authoring-structured-references` check and the updated plan-template ownership check pass in `nix run .#pkl-check-generated`. - Decisions: D1 - Scenarios: S4 -- [ ] AC4: For a structured plan, `/next-task` plan review includes the selected task's covered ACs, their scenarios, and their related decisions in the `ready` result, and the implementation gate shows them in an optional section. For a legacy plan, both outputs are unchanged. Readiness criteria, approval behavior, the `approved` flag, synchronization-debt handling, and continuation are unchanged. A dangling reference is reported as a review risk and does not block the task. +- [x] AC4: For a structured plan, `/next-task` plan review includes the selected task's covered ACs, their scenarios, and their related decisions in the `ready` result, and the implementation gate shows them in an optional section. For a legacy plan, both outputs are unchanged. Readiness criteria, approval behavior, the `approved` flag, synchronization-debt handling, and continuation are unchanged. A dangling reference is reported as a review risk and does not block the task. - Validate: the planned `next-task-structured-references` check passes, and the existing next-task semantic checks still pass in `nix run .#pkl-check-generated`. - Decisions: D3 - Scenarios: S5, S6 -- [ ] AC5: Task execution never checks an AC checkbox and never records a scenario status. Covered-AC scenarios may shape the task's own `Verify` checks. The completion record keeps its current fields; `Covers:` stays as authored, and `Files changed` stays baseline-relative. +- [x] AC5: Task execution never checks an AC checkbox and never records a scenario status. Covered-AC scenarios may shape the task's own `Verify` checks. The completion record keeps its current fields; `Covers:` stays as authored, and `Files changed` stays baseline-relative. - Validate: the planned `next-task-structured-references` check and the existing `next-task-compact-completion-writing` check pass. - Decisions: D4 - Scenarios: S6 -- [ ] AC6: For a structured plan, `/validate` runs or performs every scenario's `Check:` itself and gives each scenario exactly one status. It checks an AC only when its `Validate:` check passes and all of that AC's scenarios are `Verified`. Task completion, recorded task `Verify` outcomes, and the plan's own claims never count as evidence. +- [x] AC6: For a structured plan, `/validate` runs or performs every scenario's `Check:` itself and gives each scenario exactly one status. It checks an AC only when its `Validate:` check passes and all of that AC's scenarios are `Verified`. Task completion, recorded task `Verify` outcomes, and the plan's own claims never count as evidence. - Validate: the planned `validate-scenario-evidence` check passes, and the existing `validation-repair` and `validate-decision-sync-boundary` checks still pass. - Decisions: D4 - Scenarios: S7, S8 -- [ ] AC7: In a structured plan, a relationship-integrity defect makes validation `failed` and is listed under failed checks. The defects are a duplicate ID, a dangling reference, an AC without `Scenarios:`, an AC that no task covers, and a scenario that no AC references. Such a defect is never silently tolerated. +- [x] AC7: In a structured plan, a relationship-integrity defect makes validation `failed` and is listed under failed checks. The defects are a duplicate ID, a dangling reference, an AC without `Scenarios:`, an AC that no task covers, and a scenario that no AC references. Such a defect is never silently tolerated. - Validate: the planned `validate-scenario-evidence` check asserts this rule in the generated `validation.md`. - Decisions: D3, D4 - Scenarios: S9 -- [ ] AC8: For structured plans, both Validation Report formats (the plan-file report and the returned report) contain a scenario-verification section. Each entry maps one scenario ID to its ACs, its evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and its status. AC evidence lines cite scenario IDs. The section is omitted for legacy plans. +- [x] AC8: For structured plans, both Validation Report formats (the plan-file report and the returned report) contain a scenario-verification section. Each entry maps one scenario ID to its ACs, its evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and its status. AC evidence lines cite scenario IDs. The section is omitted for legacy plans. - Validate: the planned `validation-report-scenario-section` check passes in `nix run .#pkl-check-generated`. - Decisions: D4 - Scenarios: S10 -- [ ] AC9: Existing plans stay valid and untouched. No file under `context/plans/` other than this plan changes. The generated plan-review and validation references keep today's behavior for plans without the new sections. +- [x] AC9: Existing plans stay valid and untouched. No file under `context/plans/` other than this plan changes. The generated plan-review and validation references keep today's behavior for plans without the new sections. - Validate: `git diff --stat main...HEAD -- context/plans/` lists only this plan; the planned `next-task-structured-references` and `validate-scenario-evidence` checks assert the legacy-plan sentences. - Decisions: D3 - Scenarios: S11 -- [ ] AC10: Generated output stays deterministic and target-neutral. The artifact path count stays 145, and every new contract check has a checked-in negative fixture that `config/pkl/check-generated.sh` proves fails with its diagnostic. +- [x] AC10: Generated output stays deterministic and target-neutral. The artifact path count stays 145, and every new contract check has a checked-in negative fixture that `config/pkl/check-generated.sh` proves fails with its diagnostic. - Validate: `nix run .#pkl-check-generated` and `nix flake check` pass. - Scenarios: S2, S12 @@ -319,7 +319,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/shared-context-code-workflow.md` (`/validate` scenario evidence and integrity rules), `context/sce/config-generation.md` (new contract check and fixture). - Context synchronization: synced -- [ ] T05: `Add scenario verification to the Validation Report formats` (status:todo) +- [x] T05: `Add scenario verification to the Validation Report formats` (status:done) - Task ID: T05 - Covers: AC8, AC10 - Scope: In — @@ -329,9 +329,68 @@ Persist this field in every plan; this is durable plan state, not chat state: Out — validation execution rules (T04). - Dependencies: T04 - Done when: Both generated report formats carry the scenario section and the legacy-omission rule. The checks pass, the fixture fails as expected, and the artifact count is unchanged. - - Verify: `nix run .#pkl-check-generated`; `nix flake check`. - - Context synchronization: pending + - Verify: `nix run .#pkl-check-generated` — passed (exit 0, 145 files). Running `pkl eval` on `generation-contract-check.pkl` directly reports success for the new `validation-report-scenario-section` check and for the existing `validation-report-policy-ownership`, `validation-observational`, `validate-scenario-evidence`, `no-blank-line-runs`, `output-dedup`, and `target-neutral-references` checks. The new fixture fails with its exact diagnostic. The generated `validation.md` and `validation-report.md` are byte-identical across the OpenCode, Claude, and Pi targets. `nix flake check` — passed (all checks passed). It needed the new fixture marked intent-to-add (`git add -N`) because the flake source only includes tracked files. + - Completed: 2026-10-23 + - Files changed: config/pkl/base/workflow-validate.pkl, config/pkl/renderers/generation-contract-check.pkl, config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl, config/pkl/check-generated.sh, context/plans/plan-decisions-and-verification-scenarios.md + - Result: `renderValidationReport` now has a `### Scenario verification` section after `### Success-criteria verification`, with one `S{n} ({AC ids})` entry per scenario giving the evidence kind, exact evidence, actual result, and status. AC lines cite scenario IDs, and presentation rules omit the section for legacy plans and render only statuses established by validation execution. `renderValidationResult` gained matching `## Scenario verification` sections in the validated and failed variants, scenario entries under `What failed` for `Partially verified` and `Not verified` scenarios, scenario citations on AC lines, and report rules for the structured and legacy cases. Added the `validation-report-scenario-section` assertion and one registered negative fixture. + - Context impact: `context/sce/shared-context-code-workflow.md` (scenario section in both Validation Report formats), `context/sce/config-generation.md` (new contract check and fixture). + - Context synchronization: synced ## Open questions - Every new check is a generated-text contract. Nothing exercises an agent actually authoring or validating a structured plan. Would a manual dry run be worth adding as a follow-up: `/validate` on a small throwaway structured plan, including one deliberately dangling reference? It is not needed to land the format. + +## Validation Report + +**Status:** validated +**Date:** 2026-10-23 + +### Commands run + +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 145 files; every negative fixture and plan-structure fixture produced its expected result) +- `nix flake check` -> exit 0 (all checks passed; the 17 `checks.x86_64-linux` derivations, including `pkl-generated`, `cli-tests`, `cli-clippy`, and `cli-fmt`) +- `nix develop -c sh -c 'pkl eval config/pkl/renderers/generation-contract-check.pkl'` -> exit 0 (every contract check reports success, including `structured-plan-template-schema`, `compact-plan-template-schema`, `plan-template-structure`, `plan-template-policy-ownership`, `plan-authoring-structured-references`, `next-task-structured-references`, `next-task-compact-completion-writing`, `validation-observational`, `validate-decision-sync-boundary`, `validate-scenario-evidence`, `validation-report-scenario-section`, `target-neutral-references`, and `artifact-paths` = exactly 145) +- `nix shell nixpkgs#ripgrep -c rg -n '' config/pkl/base/workflow-change-to-plan.pkl` -> exit 0 (every definition, including `- [ ] AC1:` and `- [ ] T01:`, and every reference field matched on its own line) +- `pkl eval -p planPath= config/pkl/renderers/plan-structure-check.pkl` on this plan -> exit 0 (`structured plan: reference integrity holds`) +- `pkl eval -p planPath= config/pkl/renderers/plan-structure-check.pkl` on `context/plans/remove-checkout-id.md` and `context/plans/agent-trace-export-readers.md` -> exit 0 (both `legacy plan: structured reference rules do not apply`) +- `nix run .#pkl-generate -- ` -> exit 0 (generated references used for the AC2, S5, and S11 inspections) +- `git diff --stat main...HEAD -- context/plans/` -> exit 0 (only `context/plans/plan-decisions-and-verification-scenarios.md`) +- `git diff main...HEAD --numstat -- config/pkl/base/workflow-next-task.pkl` -> exit 0 (48 added, 0 removed) + +### Success-criteria verification + +- [x] AC1: Generated plan template defines decisions, AC reference fields, verification scenarios, and `Covers:` while keeping the compact schema -> `structured-plan-template-schema` and `compact-plan-template-schema` pass inside `nix run .#pkl-check-generated`; scenarios: S1, S2 +- [x] AC2: Plan template states the deterministic relationship rules -> inspection of the generated Claude `plan-template.md` `## Reference rules` section confirms the ID grammar, single definition, sequential numbering with no renumbering or reuse, comma-separated list grammar, resolution, required `Scenarios:` (IDs or `none (inspection)`), task coverage of every AC, AC reference of every scenario, structured-plan detection, and "A planned `Check:` is not execution evidence"; `structured-plan-template-schema` and `plan-template-structure` pass; scenarios: S3 +- [x] AC3: Plan authoring orders D→AC→S→T, cites rather than restates template rules, forbids manufactured decisions and invented test names, and runs a pre-write consistency pass -> `plan-authoring-structured-references` and `plan-template-policy-ownership` pass; scenarios: S4 +- [x] AC4: `/next-task` review and gate surface coverage for structured plans and keep legacy behavior -> `next-task-structured-references` and all existing next-task semantic checks pass; generated `plan-review.md` carries the detection, coverage, and dangling-reference-as-risk rules, and `output.md` has the optional `## Acceptance coverage` section; scenarios: S5, S6 +- [x] AC5: Task execution never checks an AC or records scenario status, and completion fields are unchanged -> `next-task-structured-references` and `next-task-compact-completion-writing` pass; scenarios: S6 +- [x] AC6: `/validate` runs every scenario check itself and checks an AC only on passing `Validate:` plus all scenarios `Verified` -> `validate-scenario-evidence`, `validation-observational` (`validation-repair` fixture), and `validate-decision-sync-boundary` pass; scenarios: S7, S8 +- [x] AC7: Relationship-integrity defects make validation `failed` -> `validate-scenario-evidence` asserts the defect list in the generated `validation.md`; scenarios: S9 +- [x] AC8: Both Validation Report formats carry a scenario-verification section, omitted for legacy plans -> `validation-report-scenario-section` passes and its negative fixture fails with its diagnostic; scenarios: S10 +- [x] AC9: Existing plans stay valid and untouched -> `git diff --stat main...HEAD -- context/plans/` lists only this plan; the legacy-plan sentences are asserted by `next-task-structured-references` and `validate-scenario-evidence`; scenarios: S11 +- [x] AC10: Generated output stays deterministic and target-neutral, 145 artifact paths, every new check has a failing negative fixture -> `nix run .#pkl-check-generated` and `nix flake check` pass; `artifact-paths` reports exactly 145; scenarios: S2, S12 + +### Scenario verification + +- S1 (AC1): command `nix run .#pkl-check-generated` plus direct `pkl eval config/pkl/renderers/generation-contract-check.pkl` -> exit 0; both `structured-plan-template-schema` and `compact-plan-template-schema` report success; `target-neutral-references` passes; status: Verified +- S2 (AC1, AC10): test `config/pkl/renderers/fixtures/structured-plan-template-schema-check.pkl` registered with `expect_pkl_fixture_failure` in `config/pkl/check-generated.sh` -> `nix run .#pkl-check-generated` exit 0, so the fixture failed with the exact diagnostic; status: Verified +- S3 (AC2): command the S3 `rg` extraction on `config/pkl/base/workflow-change-to-plan.pkl`, plus `plan-template-structure` and the `config/pkl/renderers/fixtures/plan-structure/` fixtures inside `nix run .#pkl-check-generated` -> every definition, including checkbox-prefixed AC and T lines, and every reference field matched on its own line; `plan-template-structure` reports success, and all five pass fixtures and every failure fixture produced their expected results; status: Verified +- S4 (AC3): command `nix run .#pkl-check-generated` -> exit 0; `plan-template-policy-ownership` and `plan-authoring-structured-references` report success, and the negative fixture `plan-authoring-structured-references-check.pkl` failed with its diagnostic; status: Verified +- S5 (AC4): test `next-task-structured-references` plus inspection of `git diff main...HEAD -- config/pkl/base/workflow-next-task.pkl` -> check passes; diff is 48 added and 0 removed lines, so no readiness bullet, approval sentence, or status was removed; generated `output.md` `## Acceptance coverage` is marked omitted for legacy plans; status: Verified +- S6 (AC4, AC5): test `next-task-structured-references` and `next-task-compact-completion-writing` in `nix run .#pkl-check-generated` -> both report success; status: Verified +- S7 (AC6): test `validate-scenario-evidence` -> passes; the generated `validation.md` states the evidence-source exclusions and the AC-verification rule; status: Verified +- S8 (AC6): test `validate-scenario-evidence` -> passes; asserts the three status names and the `Partially verified` rule; status: Verified +- S9 (AC7): test `validate-scenario-evidence` -> passes; asserts the relationship-integrity defect list; status: Verified +- S10 (AC8): test `validation-report-scenario-section` and fixture `validation-report-scenario-section-check.pkl` -> check passes and the fixture failed with its exact diagnostic; status: Verified +- S11 (AC9): inspection of the detection sentences in the generated `sce-next-task/references/plan-review.md` (line 83) and `sce-validate/references/validation.md` (line 66) against `context/plans/remove-checkout-id.md` and `context/plans/agent-trace-export-readers.md`, plus `plan-structure-check.pkl` on both and `git diff --stat main...HEAD -- context/plans/` -> neither plan has `### Verification scenarios` or `Covers:`, both are classified legacy, and neither file changed; status: Verified +- S12 (AC10): command `nix flake check` -> exit 0, all checks passed, `pkl-generated` included, `artifact-paths` exactly 145; status: Verified + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- Every new check is a generated-text contract. No check exercises an agent authoring or validating a structured plan end to end (see Open questions). +- The installed `.claude/skills/sce-validate` used for this run predates T04–T05 and has no structured-plan steps. Scenario evidence was still gathered per AC4–AC8's rules and the freshly generated `validation.md`. +- The T05 changes are uncommitted in the working tree, and the new fixture is tracked only through `git add`. `nix flake check` validated the working-tree source. diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 16040c14a..c47cc5e88 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/`, requiring the positive fixtures (minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example) to pass with their structured/legacy classification and each negative fixture (duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example) to fail with its expected defect; three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, and `fixtures/validate-scenario-evidence-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/`, requiring the positive fixtures (minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example) to pass with their structured/legacy classification and each negative fixture (duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example) to fail with its expected defect; three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-code-workflow.md b/context/sce/shared-context-code-workflow.md index 1d147bd90..13c4a3723 100644 --- a/context/sce/shared-context-code-workflow.md +++ b/context/sce/shared-context-code-workflow.md @@ -71,7 +71,8 @@ A context-sync blocker does not undo successful implementation: the task remains 1. `sce-validation` verifies that implementation tasks are complete, runs the plan's full validation commands and acceptance checks, records leftover debug/temp artifacts as failure evidence without deleting or repairing them, and writes the Validation Report. - For a structured plan (see `shared-context-plan-workflow.md`), it also collects decisions, scenarios, and `Covers:`, then checks plan references against the template's `Reference rules`: a duplicate ID, a dangling `Decisions:`/`Scenarios:`/`Covers:` ID, an acceptance criterion without `Scenarios:` or with no covering task, and an unreferenced scenario are each a failed check that makes the result `failed`. - It runs or performs every scenario's `Check:` itself and gives each scenario exactly one status: `Verified`, `Partially verified`, or `Not verified`. Only evidence from this run counts; task completion, recorded task `Verify` outcomes, planned `Check:` lines, and plan claims never do. An acceptance criterion is met only when its `Validate:` check passes and every scenario it names is `Verified` (a `none (inspection)` criterion needs only `Validate:`). - - A legacy plan gets no scenario collection, reference check, or scenario statuses. + - Both Validation Report formats (the plan-file `## Validation Report` and the returned validated/failed report) add a scenario-verification section for a structured plan. It has one entry per scenario, in plan order, mapping the scenario to its acceptance criteria, evidence kind (`test`, `command`, or `inspection`), exact evidence, actual result, and status. Acceptance-criterion lines cite their scenario IDs, and the failed report lists every `Partially verified` or `Not verified` scenario under **What failed**. + - A legacy plan gets no scenario collection, reference check, scenario statuses, or scenario-verification report section. 2. Failed or blocked validation ends the session without repair edits; repair occurs in a later implementation session and retry uses `/validate {plan-path}`. 3. A validated result is reported directly with the Validation Report path; `/validate` does not invoke plan-level context synchronization or persist a plan-sync lifecycle handoff. From ca5688ff515db9e0528a41162eae0df63f363ab6 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 21:58:30 +0200 Subject: [PATCH 07/61] language: Require task coverage in structured plans Structured plans must ensure every task explicitly covers at least one acceptance criterion, preventing orphaned implementation work from passing integrity checks. Require `Covers` for task records, add focused missing and empty fixtures, validate all repository plans during generated checks, and include plan files in the flake input set. Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 3 + config/pkl/base/workflow-validate.pkl | 1 + config/pkl/check-generated.sh | 11 ++++ .../plan-structure/task-empty-covers.md | 56 +++++++++++++++++++ .../plan-structure/task-missing-covers.md | 55 ++++++++++++++++++ .../renderers/generation-contract-check.pkl | 1 + config/pkl/renderers/plan-structure-check.pkl | 19 +++++-- config/pkl/renderers/plan-structure.pkl | 5 +- flake.nix | 1 + 9 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 config/pkl/renderers/fixtures/plan-structure/task-empty-covers.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/task-missing-covers.md diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 31dcbcfb0..da807377a 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -774,6 +774,9 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. +- Every task in a structured plan carries `Covers:` naming at least one + acceptance criterion defined in the plan. A task without `Covers:` or with an + empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. - Every scenario is referenced by at least one acceptance criterion's `Scenarios:`. diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index da92fb414..0771e96ec 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -269,6 +269,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ definition in the plan. - An acceptance criterion without `Scenarios:`. - An acceptance criterion that no task's `Covers:` names. + - A task without `Covers:` or with an empty `Covers:`. - A scenario that no acceptance criterion's `Scenarios:` references. Record each defect as a failed check under **Failed checks and follow-ups**, diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index a64ce3c98..ea4ba80a3 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -275,6 +275,17 @@ expect_plan_structure_failure "wrong-heading-context.md" \ "heading \`### Verification scenarios\` is not inside \`## Acceptance criteria\`" expect_plan_structure_failure "keywords-only-in-examples.md" \ "\`Scenarios:\` on \`AC1\` references undefined \`S1\`; it appears only inside a fenced code block" +expect_plan_structure_failure "task-missing-covers.md" \ + "\`T02\` is missing \`Covers:\`" +expect_plan_structure_failure "task-empty-covers.md" \ + "\`Covers:\` on \`T02\` is not a comma-separated ID list" + +plans_glob="$repo_root/context/plans/*.md" +if ! plans_output="$(pkl eval -p "planPath=$plans_glob" "$plan_structure_check" 2>&1)"; then + printf 'Repository plans failed the structural plan check (%s):\n%s\n' \ + "$plans_glob" "$plans_output" >&2 + exit 1 +fi "$producer" "$repo_root" "$generated_input_root" generated_root="$generated_input_root/pkl-generated" diff --git a/config/pkl/renderers/fixtures/plan-structure/task-empty-covers.md b/config/pkl/renderers/fixtures/plan-structure/task-empty-covers.md new file mode 100644 index 000000000..b56f9dc0e --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/task-empty-covers.md @@ -0,0 +1,56 @@ +# Plan: task-empty-covers + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +- [ ] T02: `Document /auth/refresh in the changelog` (status:todo) + - Task ID: T02 + - Covers: + - Scope: In — changelog entry. Out — API docs. + - Dependencies: T01 + - Done when: `CHANGELOG.md` names the refresh endpoint. + - Verify: inspection of `CHANGELOG.md`. + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/task-missing-covers.md b/config/pkl/renderers/fixtures/plan-structure/task-missing-covers.md new file mode 100644 index 000000000..4aae1446a --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/task-missing-covers.md @@ -0,0 +1,55 @@ +# Plan: task-missing-covers + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +- [ ] T02: `Document /auth/refresh in the changelog` (status:todo) + - Task ID: T02 + - Scope: In — changelog entry. Out — API docs. + - Dependencies: T01 + - Done when: `CHANGELOG.md` names the refresh endpoint. + - Verify: inspection of `CHANGELOG.md`. + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index fae800db0..2bf9c2726 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1057,6 +1057,7 @@ local planTemplateOwnedPolicyTokens = new Listing { "A section runs from its heading to the next heading of any level outside" "To check a fenced example itself, extract the block's content and apply these" "Never renumber or reuse a decision, criterion, or scenario ID" + "Every task in a structured plan carries `Covers:` naming at least one" "Every acceptance criterion is named by at least one task's `Covers:`." "A planned `Check:` is not execution evidence." "A legacy plan stays legacy." diff --git a/config/pkl/renderers/plan-structure-check.pkl b/config/pkl/renderers/plan-structure-check.pkl index 131af9ef0..ac390ad06 100644 --- a/config/pkl/renderers/plan-structure-check.pkl +++ b/config/pkl/renderers/plan-structure-check.pkl @@ -1,10 +1,21 @@ import "plan-structure.pkl" as planStructure local planPath = read("prop:planPath") -local report = planStructure.check(read("file:\(planPath)").text) +local reports = read*("file:\(planPath)").toMap().mapValues((_, plan) -> planStructure.check(plan.text)) +local failures = reports.filter((_, report) -> !report.defects.isEmpty) +local structuredCount = reports.filter((_, report) -> report.structured).length result = - if (!report.defects.isEmpty) - throw("plan failed the structural reference-integrity check: " + report.defects.join("; ")) - else if (report.structured) "structured plan: reference integrity holds" + if (reports.isEmpty) throw("no plan matched `\(planPath)`") + else if (!failures.isEmpty) + throw( + "plan failed the structural reference-integrity check: " + + failures + .map((uri, report) -> Pair(uri, "\(uri.drop("file:".length)): \(report.defects.join("; "))")) + .values + .join(" | ") + ) + else if (reports.length > 1) + "\(reports.length) plans checked: \(structuredCount) structured with reference integrity, \(reports.length - structuredCount) legacy" + else if (structuredCount == 1) "structured plan: reference integrity holds" else "legacy plan: structured reference rules do not apply" diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl index b54a957bb..246f6615a 100644 --- a/config/pkl/renderers/plan-structure.pkl +++ b/config/pkl/renderers/plan-structure.pkl @@ -63,7 +63,7 @@ local requiredFields = Map( "D", List("Boundary", "Rationale", "Rejected", "Trade-offs"), "AC", List("Validate", "Scenarios"), "S", List("Given", "When", "Then", "Check"), - "T", List() + "T", List("Covers") ) local referenceKinds = Map( @@ -313,9 +313,6 @@ local function exampleShapeDefects(sample: String): List = if (defined.any((entry) -> entry.kind == "AC" && fieldOf(entry, "Scenarios")?.value == inspectionOnly)) List() else List("no acceptance criterion carries `Scenarios: \(inspectionOnly)`") ) - + defined - .filter((entry) -> entry.kind == "T" && fieldOf(entry, "Covers") == null) - .map((entry) -> "`\(entry.id)` is missing `Covers:`") local function sampleDefects(label: String, sample: String?, skeleton: Boolean): List = ( diff --git a/flake.nix b/flake.nix index 8335a6e26..7b2014381 100644 --- a/flake.nix +++ b/flake.nix @@ -344,6 +344,7 @@ root = workspaceRoot; fileset = pkgs.lib.fileset.unions [ ./config/pkl + ./context/plans ./.version ./config/pkl/renderers/fixtures/atomic-commit-content-check.pkl ./config/pkl/renderers/fixtures/opencode-decision-gate-template-ownership-check.pkl From b9146bc93aeb7b4f1ae2a00fdc0e60ec8984d2e2 Mon Sep 17 00:00:00 2001 From: David Abram Date: Fri, 23 Oct 2026 22:22:18 +0200 Subject: [PATCH 08/61] CI: Exclude mutable plans from generation checks Keep generation and flake checks focused on authored generator inputs and fixture behavior rather than scanning context/plans. Document the split responsibility so plan authoring and validation own working-plan consistency and evidence checks, while the structural checker remains available for selected plans. Co-authored-by: SCE --- config/pkl/check-generated.sh | 7 ------- context/sce/config-generation.md | 2 +- context/sce/shared-context-plan-workflow.md | 10 ++++++++++ flake.nix | 1 - 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index ea4ba80a3..63b6ad04a 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -280,13 +280,6 @@ expect_plan_structure_failure "task-missing-covers.md" \ expect_plan_structure_failure "task-empty-covers.md" \ "\`Covers:\` on \`T02\` is not a comma-separated ID list" -plans_glob="$repo_root/context/plans/*.md" -if ! plans_output="$(pkl eval -p "planPath=$plans_glob" "$plan_structure_check" 2>&1)"; then - printf 'Repository plans failed the structural plan check (%s):\n%s\n' \ - "$plans_glob" "$plans_output" >&2 - exit 1 -fi - "$producer" "$repo_root" "$generated_input_root" generated_root="$generated_input_root/pkl-generated" diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index c47cc5e88..7935fb3f0 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -57,7 +57,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. -- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures through `plan-structure-check.pkl` with their expected classifications and defects, then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. +- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures through `plan-structure-check.pkl` with their expected classifications and defects (it does not scan `context/plans/`, and `pklGeneratedCheckSrc` excludes that directory, so mutable plan contents never affect generation checks or `nix flake check`), then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. Generated authored classes: diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 223281fef..7a8aba893 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -98,6 +98,16 @@ entry to the full draft and fixes each defect first. A legacy-plan revision skips this structured work. The phase cites the template's rule sections by name and does not restate them. +Responsibility for structural consistency is split by surface. Generation checks +(`nix run .#pkl-check-generated`, also run by `nix flake check`) verify the plan +template and the behavior of the structural checker +`config/pkl/renderers/plan-structure-check.pkl` against fixtures only; they never +read `context/plans/`, so an incomplete or malformed working plan cannot fail Nix +CI. Plan authoring produces structurally consistent plans. Plan validation checks +the selected plan's references and its actual verification evidence; the +structural checker remains available for one selected plan with +`pkl eval -p planPath= config/pkl/renderers/plan-structure-check.pkl`. + ## Continuation contract Same-session waits preserve one of two explicit continuation shapes inside the diff --git a/flake.nix b/flake.nix index 7b2014381..8335a6e26 100644 --- a/flake.nix +++ b/flake.nix @@ -344,7 +344,6 @@ root = workspaceRoot; fileset = pkgs.lib.fileset.unions [ ./config/pkl - ./context/plans ./.version ./config/pkl/renderers/fixtures/atomic-commit-content-check.pkl ./config/pkl/renderers/fixtures/opencode-decision-gate-template-ownership-check.pkl From f08219b4b9820cbe3b84a716ab61d0c5fbed6899 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 00:21:36 +0200 Subject: [PATCH 09/61] skills: Add structured plan coverage and scenario references Keep design decisions, acceptance criteria, verification scenarios, and task coverage linked across planning, execution, and validation workflows. Update the skill references and synchronized agent targets so structured plans validate relationship integrity and report scenario evidence consistently. --- .../references/plan-authoring.md | 53 +++++-- .../references/plan-template.md | 137 ++++++++++++++++++ .../skills/sce-next-task/references/output.md | 7 + .../sce-next-task/references/plan-review.md | 27 ++++ .../references/task-execution.md | 7 + .../references/validation-report.md | 20 ++- .../sce-validate/references/validation.md | 94 +++++++++++- .../references/plan-authoring.md | 53 +++++-- .../references/plan-template.md | 137 ++++++++++++++++++ .../skills/sce-next-task/references/output.md | 7 + .../sce-next-task/references/plan-review.md | 27 ++++ .../references/task-execution.md | 7 + .../references/validation-report.md | 20 ++- .../sce-validate/references/validation.md | 94 +++++++++++- .../references/plan-authoring.md | 53 +++++-- .../references/plan-template.md | 137 ++++++++++++++++++ .../skills/sce-next-task/references/output.md | 7 + .../sce-next-task/references/plan-review.md | 27 ++++ .../references/task-execution.md | 7 + .../references/validation-report.md | 20 ++- .../sce-validate/references/validation.md | 94 +++++++++++- .../references/plan-authoring.md | 53 +++++-- .../references/plan-template.md | 137 ++++++++++++++++++ .pi/skills/sce-next-task/references/output.md | 7 + .../sce-next-task/references/plan-review.md | 27 ++++ .../references/task-execution.md | 7 + .../references/validation-report.md | 20 ++- .../sce-validate/references/validation.md | 94 +++++++++++- 28 files changed, 1304 insertions(+), 76 deletions(-) diff --git a/.agents/skills/sce-change-to-plan/references/plan-authoring.md b/.agents/skills/sce-change-to-plan/references/plan-authoring.md index 5b614219b..ef084bad3 100644 --- a/.agents/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.agents/skills/sce-change-to-plan/references/plan-authoring.md @@ -28,9 +28,16 @@ This phase owns the planning process: - Write or revise exactly one `context/plans/{plan_name}.md`. `references/plan-template.md` is the sole owner of the persisted plan schema. -Read its `Acceptance criteria rules`, `Task rules` and `No validation task` rules, -and `Updating an existing plan` rules before authoring or revising. Apply those -rules rather than restating them here. +Read its `Acceptance criteria rules`, `Design decision rules`, +`Verification scenario rules`, `Reference rules`, `Task rules` and +`No validation task` rules, and `Updating an existing plan` rules before +authoring or revising. Apply those rules rather than restating them here. + +Author plan content in this order: design decisions, then acceptance criteria, +then verification scenarios, then tasks. When revising a legacy plan, skip the +design decision, verification scenario, `Covers:`, and reference-check work +unless the `Updating an existing plan` rules let the correction adopt the +structured format. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -151,31 +158,59 @@ the repository does not yet have. Do not explore the entire repository by default. -## 2.5 Author the acceptance criteria +## 2.5 Record design decisions + +Record the architectural choices this plan commits to under `## Design decisions`, +applying the `Design decision rules` in `references/plan-template.md`. Do not +manufacture a decision to fill the section. When the change commits to no such +choice, omit the section. + +## 2.6 Author the acceptance criteria Derive the plan-specific success outcomes and checks before slicing tasks, then apply the `Acceptance criteria rules` and exact section shape in `references/plan-template.md`. The template owns their generic validation semantics and placement. -## 2.6 Author the task stack +Give each criterion its `Decisions:` and `Scenarios:` fields as the +`Reference rules` define them. + +## 2.7 Author the verification scenarios + +Write the scenarios the acceptance criteria depend on under +`### Verification scenarios`, applying the `Verification scenario rules` in +`references/plan-template.md`. Cover normal, boundary, failure, cancellation, +concurrency, and recovery cases only where the change makes them relevant. +Do not invent test names: a `Check:` names an existing test, a test a task in +the stack creates, a command, or a named inspection. -Slice and order the plan-specific work after the acceptance criteria, applying the -`Task rules` and `No validation task` rules in `references/plan-template.md`. +## 2.8 Author the task stack + +Slice and order the plan-specific work after the acceptance criteria and +verification scenarios, applying the `Task rules` and `No validation task` rules in `references/plan-template.md`. Do not restate those generic task rules here. +Give each task its `Covers:` field as the `Reference rules` define it. + A finished stack always leaves at least one incomplete task, so the workflow can always hand off to `/next-task`. When the request resolves to a plan but produces no incomplete task, because the change is already implemented or already covered by completed tasks, set internal status `blocked` with category `no_actionable_work` instead of writing the plan. -## 2.7 Write the plan +## 2.9 Check references + +Before writing a structured plan, run a reference-consistency pass over the +complete draft that applies every `Reference rules` entry in +`references/plan-template.md`. Fix each defect in the draft before writing. Do +not write a plan that fails this pass. + +## 2.10 Write the plan Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -## 2.8 Return the result +## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.agents/skills/sce-change-to-plan/references/plan-template.md b/.agents/skills/sce-change-to-plan/references/plan-template.md index fe0190b94..5a5fa0fe9 100644 --- a/.agents/skills/sce-change-to-plan/references/plan-template.md +++ b/.agents/skills/sce-change-to-plan/references/plan-template.md @@ -18,6 +18,17 @@ entirely rather than writing them empty. {One or two paragraphs: what changes, where, and why. State whether this extends existing behavior, replaces it, or preserves work already in progress.} +## Design decisions + +{Optional. Include only real architectural choices this plan commits to. Remove +the section when there are none.} + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + ## Acceptance criteria How this plan is proven complete. Each criterion is observable and names the @@ -26,8 +37,19 @@ performs final validation. - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` + - Decisions: {decision IDs, e.g. D1; omit the field when no decision applies} + - Scenarios: {scenario IDs, e.g. S1, S2} - [ ] AC2: {observable outcome} - Validate: `{command, assertion, or inspection that proves AC2}` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} ### Full validation @@ -70,6 +92,7 @@ recorded. Remove the section otherwise.} - [ ] T01: `{single intent title}` (status:todo) - Task ID: T01 + - Covers: {acceptance criterion IDs, e.g. AC1} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: {task IDs, or none} - Done when: {clear acceptance for one coherent change} @@ -78,6 +101,7 @@ recorded. Remove the section otherwise.} - [ ] T02: `{single intent title}` (status:todo) - Task ID: T02 + - Covers: {acceptance criterion IDs, e.g. AC2} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: T01 - Done when: {clear acceptance for one coherent change} @@ -103,6 +127,7 @@ invent one: `None.` is the expected answer for a well-specified change.} ```markdown - [ ] T02: `Add /auth/refresh endpoint` (status:todo) - Task ID: T02 + - Covers: AC1 - Scope: In — route handler, token validation logic, response schema. Out — refresh token rotation policy (covered in T03), client-side storage changes. - Dependencies: T01 - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired or invalid token; targeted tests pass; OpenAPI spec updated. @@ -110,6 +135,47 @@ invent one: `None.` is the expected answer for a well-specified change.} - Context synchronization: pending ``` +## Filled-in structured plan example + +```markdown +## Design decisions + +- D1: The existing session store validates refresh tokens; no new token service is added. + - Boundary: `src/auth/` owns token validation; route handlers call it and never read the session store directly. + - Rationale: The session store already tracks revocation, so one owner keeps revocation consistent. + - Rejected: Stateless refresh tokens — they cannot be revoked before they expire. + - Trade-offs: Every refresh costs one session-store read. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` returns a signed JWT for a valid refresh token and 401 for an expired or revoked one. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh` with its 200 and 401 responses. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A refresh token that has not expired but is revoked in the session store. + - When: The client calls `POST /auth/refresh` with that token. + - Then: The response is 401 and no JWT is issued. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"`, created by T01. + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — route handler, session-store validation call, OpenAPI entry. Out — refresh token rotation policy. + - Dependencies: none + - Done when: The endpoint returns a JWT or 401 as AC1 states, the revoked-token test exists and passes, and the OpenAPI entry is present. + - Verify: `pnpm test src/auth/refresh.test.ts`. + - Context synchronization: pending +``` + ## Acceptance criteria rules - Acceptance criteria describe the finished system, not the work. Prefer "the @@ -127,6 +193,25 @@ invent one: `None.` is the expected answer for a well-specified change.} change request. If a criterion has no task that could satisfy it, the task stack is incomplete. +## Design decision rules + +- Record a design decision only for a real architectural choice the plan + commits to, such as an ownership boundary, a data shape, an interface, or an + alternative a reviewer would otherwise reopen. +- Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:`. +- A design decision belongs to this plan only. It does not replace an + architecture decision record. + +## Verification scenario rules + +- A scenario states one behavior that an acceptance criterion depends on, as + `Given:`, `When:`, `Then:`, and `Check:`. +- `Then:` states an exact observable result or invariant, not an intention. +- `Check:` names the planned test, command, or named inspection that would + prove `Then:`. Name a test only when it already exists or a task in the stack + creates it. + ## Task rules - Number tasks sequentially as `T01..T0N`. @@ -142,6 +227,54 @@ invent one: `None.` is the expected answer for a well-specified change.} acceptance checks. - Order tasks so each one's declared dependencies precede it. +## Reference rules + +- Read plan structure only from Markdown outside fenced code blocks. A fenced + code block runs from a line that opens a fence of three or more backticks or + tildes to its closing fence. Headings, definitions, and fields inside it are + example text, not plan structure. Keep fenced examples in place; never delete + one to make a plan read correctly. +- A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` + carries `Covers:`. Every other plan is a legacy plan, and the remaining rules + in this section do not apply to it. +- A section runs from its heading to the next heading of any level outside + fenced code blocks. A definition counts only in its own section: `D` entries + under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` + entries under `### Verification scenarios`, and `T` entries under + `## Task stack`. A line shaped like a definition anywhere else, such as in a + fenced example or a validation report, defines nothing. +- A definition's fields are the `- Field:` lines indented one level under its + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and + `Check:` to a verification scenario; and `Covers:` to a task. +- To check a fenced example itself, extract the block's content and apply these + rules to it as a separate sample document. +- IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, + `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry + starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and + `- [ ] T01:`. +- Define each ID exactly once. +- Number decisions, criteria, and scenarios sequentially in order of first + assignment. Never renumber or reuse a decision, criterion, or scenario ID; a + withdrawn entry keeps its ID and says that it is withdrawn. +- Each reference field fits on one line: `Decisions:` and `Scenarios:` on an + acceptance criterion, and `Covers:` on a task. Its value is a list of IDs + separated by a comma and a space, for example `S1, S2`. +- Every referenced ID resolves to a definition in the same plan. +- Every acceptance criterion carries `Scenarios:`, with scenario IDs or the + literal `none (inspection)` when only an inspection can prove it. Omit + `Decisions:` when no decision applies. +- Every task in a structured plan carries `Covers:` naming at least one + acceptance criterion defined in the plan. A task without `Covers:` or with an + empty `Covers:` is a defect even when other tasks cover every criterion. +- Every acceptance criterion is named by at least one task's `Covers:`. +- Every scenario is referenced by at least one acceptance criterion's + `Scenarios:`. +- A planned `Check:` is not execution evidence. Only `/validate` running or + performing the check verifies a scenario. + ## No validation task - The last task in the stack is an ordinary implementation task. Do not author a @@ -183,3 +316,7 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. +- A legacy plan stays legacy. Add design decisions, verification scenarios, or + `Covers:` only when the correction explicitly asks to adopt the structured + format. +- Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.agents/skills/sce-next-task/references/output.md b/.agents/skills/sce-next-task/references/output.md index 1ddc071ab..8ca87b94f 100644 --- a/.agents/skills/sce-next-task/references/output.md +++ b/.agents/skills/sce-next-task/references/output.md @@ -113,6 +113,13 @@ Describe the smallest coherent implementation approach with enough detail for ap - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. +## Acceptance coverage + +- Include only when the plan is structured; omit this section for a legacy plan. +- List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. +- Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} diff --git a/.agents/skills/sce-next-task/references/plan-review.md b/.agents/skills/sce-next-task/references/plan-review.md index ae6a97b66..d01d3bffe 100644 --- a/.agents/skills/sce-next-task/references/plan-review.md +++ b/.agents/skills/sce-next-task/references/plan-review.md @@ -74,6 +74,23 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; review it exactly as +described here, with no coverage lookup and no coverage fields in the result. + +For a structured plan, read the selected task's `Covers:` IDs. For each covered +acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the +scenario and decision definitions they name. A covered ID or referenced ID with +no definition in the plan is a dangling reference. Record it as a review risk; +it does not make the task `blocked`. + ## 1.4 Determine readiness A task is `ready` when: @@ -128,6 +145,16 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. +For a structured plan, a `ready` result also identifies: + +- The covered acceptance criteria: each `Covers:` ID with its outcome and + `Validate:` check. +- Their verification scenarios: each scenario ID with its `Then:` and `Check:`. +- Their related design decisions: each decision ID with its decision text. +- Every dangling reference, as a review risk. + +A `ready` result for a legacy plan carries none of these coverage fields. + A `sync_debt` result must identify: - The resolved plan path. diff --git a/.agents/skills/sce-next-task/references/task-execution.md b/.agents/skills/sce-next-task/references/task-execution.md index ae68ed4c3..ab9efcc23 100644 --- a/.agents/skills/sce-next-task/references/task-execution.md +++ b/.agents/skills/sce-next-task/references/task-execution.md @@ -132,6 +132,12 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. +For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +the task's own `Verify` checks. Running a scenario's check during the task is +task-level evidence only. Task execution never checks an acceptance-criterion +checkbox and never records a scenario status; only `/validate` verifies +acceptance criteria and scenarios. + Verification may include: - Targeted tests. @@ -167,6 +173,7 @@ Only after successful implementation and task-level verification: facts above and before the synchronization phase is invoked. - Record material deviations or approved assumptions. - Preserve the plan's existing structure and terminology. +- Keep the task's `Covers:` field as authored. Do not mark the task complete when returning `declined`, `blocked`, or `incomplete`. diff --git a/.agents/skills/sce-validate/references/validation-report.md b/.agents/skills/sce-validate/references/validation-report.md index b0fa7778b..436ec92d3 100644 --- a/.agents/skills/sce-validate/references/validation-report.md +++ b/.agents/skills/sce-validate/references/validation-report.md @@ -31,8 +31,14 @@ writes it. ### Success-criteria verification -- [x] AC1: {criterion statement} -> {evidence} -- [ ] AC2: {criterion statement} -> {evidence of failure or not checked} +- [x] AC1: {criterion statement} -> {evidence}; scenarios: S1 +- [ ] AC2: {criterion statement} -> {evidence of failure or not checked}; scenarios: S2, S3 + +### Scenario verification + +{Only for a structured plan:} + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -61,7 +67,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. + not independently re-evaluate or reclassify criteria here. For a structured + plan, end each acceptance-criterion line with the scenario IDs its + `Scenarios:` field names, or `none (inspection)`. +- Include **Scenario verification** only for a structured plan, with exactly + one entry per scenario in plan order. Each entry gives the scenario ID, the + acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status already established by validation + execution. Omit the section for a legacy plan. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.agents/skills/sce-validate/references/validation.md b/.agents/skills/sce-validate/references/validation.md index 5fdf8e6f9..1b2c50466 100644 --- a/.agents/skills/sce-validate/references/validation.md +++ b/.agents/skills/sce-validate/references/validation.md @@ -57,6 +57,25 @@ From the plan, collect: - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; validate it exactly +as described here, with no scenario collection, no reference check, and no +scenario statuses. + +For a structured plan, also collect: + +- Every design decision. +- Every acceptance criterion's `Decisions:` and `Scenarios:` fields. +- Every verification scenario with its `Then:` and `Check:`. +- Every task's `Covers:` field. + Return `blocked` when the plan has no usable acceptance criteria, or when no validation commands can be determined from the plan or repository conventions. @@ -64,7 +83,28 @@ Prefer the plan's authored checks. Fall back to repository-primary test, lint, and format commands only when `Full validation` is absent, and record that fallback under notes on a `validated` or `failed` result. -### 4. Run full validation and acceptance checks +### 4. Check plan references + +Skip this step for a legacy plan. + +For a structured plan, apply every entry of the plan template's +`Reference rules` to the plan. Each of these is a relationship-integrity +defect: + +- An ID defined more than once. +- A dangling reference: a `Decisions:`, `Scenarios:`, or `Covers:` ID with no + definition in the plan. +- An acceptance criterion without `Scenarios:`. +- An acceptance criterion that no task's `Covers:` names. +- A task without `Covers:` or with an empty `Covers:`. +- A scenario that no acceptance criterion's `Scenarios:` references. + +Record each defect as a failed check under **Failed checks and follow-ups**, +naming the IDs involved. A relationship-integrity defect makes the result +`failed`; never tolerate one silently. Continue gathering the remaining +evidence. + +### 5. Run full validation and acceptance checks Run the plan's `Full validation` commands. @@ -72,6 +112,26 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. +For a structured plan, run or perform every scenario's `Check:` yourself in +this run, and compare the observed result with the scenario's `Then:`. Give +each scenario exactly one status: + +- `Verified` when this run's evidence proves its `Then:` in full. +- `Partially verified` when the check ran but proves `Then:` only in part, for + example at a narrower level than `Then:` requires. +- `Not verified` when the check failed, could not be run or located, or was + not performed. + +Only evidence gathered by this run counts. Task completion, a task's recorded +`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never +evidence. Record every `Partially verified` or `Not verified` scenario as a +failed check with the gap and a required action. + +In a structured plan, an acceptance criterion is met only when its `Validate:` +check passes and every scenario its `Scenarios:` names is `Verified`. An +acceptance criterion marked `none (inspection)` is met by its `Validate:` check +alone. + Treat leftover debug-only flags, temporary files, intermediate artifacts, or local scaffolding as a failed validation check. Record the path and evidence under **Failed checks and follow-ups**; never delete or repair it during @@ -87,7 +147,7 @@ inspection confirmed the criterion. Do not run task-by-task implementation work for incomplete tasks. That belongs to `/next-task`. -### 5. Update the plan +### 6. Update the plan For `validated` and `failed` outcomes: @@ -101,7 +161,7 @@ Do not reopen completed tasks, rewrite task evidence, or change the task stack. For `blocked`, leave the plan file unchanged. -### 6. Return the Markdown report +### 7. Return the Markdown report Return exactly one Markdown report: @@ -176,8 +236,12 @@ invoking workflow. ## Acceptance criteria -- [x] AC1: {criterion statement} — {evidence} -- [x] AC2: {criterion statement} — {evidence} +- [x] AC1: {criterion statement} — {evidence}; scenarios: S1 +- [x] AC2: {criterion statement} — {evidence}; scenarios: none (inspection) + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified ## Residual risks @@ -218,11 +282,19 @@ a normal work session, then rerun validation. - `{check or AC id}`: {problem} - Evidence: {command output, exit summary, or inspection finding} - Required action: {concrete repair or decision} +- `{scenario id}` ({AC ids}): {Partially verified | Not verified} — {gap between the observed result and `Then:`} + - Evidence: {exact test, command, or inspected location and its actual result} + - Required action: {concrete repair or decision} ## Acceptance criteria -- [x] AC1: {criterion} — {evidence} -- [ ] AC2: {criterion} — {why unmet} +- [x] AC1: {criterion} — {evidence}; scenarios: S1 +- [ ] AC2: {criterion} — {why unmet}; scenarios: S2 + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified +- S2 (AC2): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Partially verified | Not verified} ## Commands run @@ -303,6 +375,14 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. +- For a structured plan, the validated and failed variants include **Scenario + verification** with exactly one entry per scenario in plan order: the scenario + ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status. Each acceptance-criterion line + cites its scenario IDs, or `none (inspection)`. In the failed variant, list + every `Partially verified` or `Not verified` scenario under **What failed**. +- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.claude/skills/sce-change-to-plan/references/plan-authoring.md b/.claude/skills/sce-change-to-plan/references/plan-authoring.md index 5b614219b..ef084bad3 100644 --- a/.claude/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.claude/skills/sce-change-to-plan/references/plan-authoring.md @@ -28,9 +28,16 @@ This phase owns the planning process: - Write or revise exactly one `context/plans/{plan_name}.md`. `references/plan-template.md` is the sole owner of the persisted plan schema. -Read its `Acceptance criteria rules`, `Task rules` and `No validation task` rules, -and `Updating an existing plan` rules before authoring or revising. Apply those -rules rather than restating them here. +Read its `Acceptance criteria rules`, `Design decision rules`, +`Verification scenario rules`, `Reference rules`, `Task rules` and +`No validation task` rules, and `Updating an existing plan` rules before +authoring or revising. Apply those rules rather than restating them here. + +Author plan content in this order: design decisions, then acceptance criteria, +then verification scenarios, then tasks. When revising a legacy plan, skip the +design decision, verification scenario, `Covers:`, and reference-check work +unless the `Updating an existing plan` rules let the correction adopt the +structured format. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -151,31 +158,59 @@ the repository does not yet have. Do not explore the entire repository by default. -## 2.5 Author the acceptance criteria +## 2.5 Record design decisions + +Record the architectural choices this plan commits to under `## Design decisions`, +applying the `Design decision rules` in `references/plan-template.md`. Do not +manufacture a decision to fill the section. When the change commits to no such +choice, omit the section. + +## 2.6 Author the acceptance criteria Derive the plan-specific success outcomes and checks before slicing tasks, then apply the `Acceptance criteria rules` and exact section shape in `references/plan-template.md`. The template owns their generic validation semantics and placement. -## 2.6 Author the task stack +Give each criterion its `Decisions:` and `Scenarios:` fields as the +`Reference rules` define them. + +## 2.7 Author the verification scenarios + +Write the scenarios the acceptance criteria depend on under +`### Verification scenarios`, applying the `Verification scenario rules` in +`references/plan-template.md`. Cover normal, boundary, failure, cancellation, +concurrency, and recovery cases only where the change makes them relevant. +Do not invent test names: a `Check:` names an existing test, a test a task in +the stack creates, a command, or a named inspection. -Slice and order the plan-specific work after the acceptance criteria, applying the -`Task rules` and `No validation task` rules in `references/plan-template.md`. +## 2.8 Author the task stack + +Slice and order the plan-specific work after the acceptance criteria and +verification scenarios, applying the `Task rules` and `No validation task` rules in `references/plan-template.md`. Do not restate those generic task rules here. +Give each task its `Covers:` field as the `Reference rules` define it. + A finished stack always leaves at least one incomplete task, so the workflow can always hand off to `/next-task`. When the request resolves to a plan but produces no incomplete task, because the change is already implemented or already covered by completed tasks, set internal status `blocked` with category `no_actionable_work` instead of writing the plan. -## 2.7 Write the plan +## 2.9 Check references + +Before writing a structured plan, run a reference-consistency pass over the +complete draft that applies every `Reference rules` entry in +`references/plan-template.md`. Fix each defect in the draft before writing. Do +not write a plan that fails this pass. + +## 2.10 Write the plan Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -## 2.8 Return the result +## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.claude/skills/sce-change-to-plan/references/plan-template.md b/.claude/skills/sce-change-to-plan/references/plan-template.md index fe0190b94..5a5fa0fe9 100644 --- a/.claude/skills/sce-change-to-plan/references/plan-template.md +++ b/.claude/skills/sce-change-to-plan/references/plan-template.md @@ -18,6 +18,17 @@ entirely rather than writing them empty. {One or two paragraphs: what changes, where, and why. State whether this extends existing behavior, replaces it, or preserves work already in progress.} +## Design decisions + +{Optional. Include only real architectural choices this plan commits to. Remove +the section when there are none.} + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + ## Acceptance criteria How this plan is proven complete. Each criterion is observable and names the @@ -26,8 +37,19 @@ performs final validation. - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` + - Decisions: {decision IDs, e.g. D1; omit the field when no decision applies} + - Scenarios: {scenario IDs, e.g. S1, S2} - [ ] AC2: {observable outcome} - Validate: `{command, assertion, or inspection that proves AC2}` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} ### Full validation @@ -70,6 +92,7 @@ recorded. Remove the section otherwise.} - [ ] T01: `{single intent title}` (status:todo) - Task ID: T01 + - Covers: {acceptance criterion IDs, e.g. AC1} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: {task IDs, or none} - Done when: {clear acceptance for one coherent change} @@ -78,6 +101,7 @@ recorded. Remove the section otherwise.} - [ ] T02: `{single intent title}` (status:todo) - Task ID: T02 + - Covers: {acceptance criterion IDs, e.g. AC2} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: T01 - Done when: {clear acceptance for one coherent change} @@ -103,6 +127,7 @@ invent one: `None.` is the expected answer for a well-specified change.} ```markdown - [ ] T02: `Add /auth/refresh endpoint` (status:todo) - Task ID: T02 + - Covers: AC1 - Scope: In — route handler, token validation logic, response schema. Out — refresh token rotation policy (covered in T03), client-side storage changes. - Dependencies: T01 - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired or invalid token; targeted tests pass; OpenAPI spec updated. @@ -110,6 +135,47 @@ invent one: `None.` is the expected answer for a well-specified change.} - Context synchronization: pending ``` +## Filled-in structured plan example + +```markdown +## Design decisions + +- D1: The existing session store validates refresh tokens; no new token service is added. + - Boundary: `src/auth/` owns token validation; route handlers call it and never read the session store directly. + - Rationale: The session store already tracks revocation, so one owner keeps revocation consistent. + - Rejected: Stateless refresh tokens — they cannot be revoked before they expire. + - Trade-offs: Every refresh costs one session-store read. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` returns a signed JWT for a valid refresh token and 401 for an expired or revoked one. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh` with its 200 and 401 responses. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A refresh token that has not expired but is revoked in the session store. + - When: The client calls `POST /auth/refresh` with that token. + - Then: The response is 401 and no JWT is issued. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"`, created by T01. + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — route handler, session-store validation call, OpenAPI entry. Out — refresh token rotation policy. + - Dependencies: none + - Done when: The endpoint returns a JWT or 401 as AC1 states, the revoked-token test exists and passes, and the OpenAPI entry is present. + - Verify: `pnpm test src/auth/refresh.test.ts`. + - Context synchronization: pending +``` + ## Acceptance criteria rules - Acceptance criteria describe the finished system, not the work. Prefer "the @@ -127,6 +193,25 @@ invent one: `None.` is the expected answer for a well-specified change.} change request. If a criterion has no task that could satisfy it, the task stack is incomplete. +## Design decision rules + +- Record a design decision only for a real architectural choice the plan + commits to, such as an ownership boundary, a data shape, an interface, or an + alternative a reviewer would otherwise reopen. +- Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:`. +- A design decision belongs to this plan only. It does not replace an + architecture decision record. + +## Verification scenario rules + +- A scenario states one behavior that an acceptance criterion depends on, as + `Given:`, `When:`, `Then:`, and `Check:`. +- `Then:` states an exact observable result or invariant, not an intention. +- `Check:` names the planned test, command, or named inspection that would + prove `Then:`. Name a test only when it already exists or a task in the stack + creates it. + ## Task rules - Number tasks sequentially as `T01..T0N`. @@ -142,6 +227,54 @@ invent one: `None.` is the expected answer for a well-specified change.} acceptance checks. - Order tasks so each one's declared dependencies precede it. +## Reference rules + +- Read plan structure only from Markdown outside fenced code blocks. A fenced + code block runs from a line that opens a fence of three or more backticks or + tildes to its closing fence. Headings, definitions, and fields inside it are + example text, not plan structure. Keep fenced examples in place; never delete + one to make a plan read correctly. +- A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` + carries `Covers:`. Every other plan is a legacy plan, and the remaining rules + in this section do not apply to it. +- A section runs from its heading to the next heading of any level outside + fenced code blocks. A definition counts only in its own section: `D` entries + under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` + entries under `### Verification scenarios`, and `T` entries under + `## Task stack`. A line shaped like a definition anywhere else, such as in a + fenced example or a validation report, defines nothing. +- A definition's fields are the `- Field:` lines indented one level under its + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and + `Check:` to a verification scenario; and `Covers:` to a task. +- To check a fenced example itself, extract the block's content and apply these + rules to it as a separate sample document. +- IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, + `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry + starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and + `- [ ] T01:`. +- Define each ID exactly once. +- Number decisions, criteria, and scenarios sequentially in order of first + assignment. Never renumber or reuse a decision, criterion, or scenario ID; a + withdrawn entry keeps its ID and says that it is withdrawn. +- Each reference field fits on one line: `Decisions:` and `Scenarios:` on an + acceptance criterion, and `Covers:` on a task. Its value is a list of IDs + separated by a comma and a space, for example `S1, S2`. +- Every referenced ID resolves to a definition in the same plan. +- Every acceptance criterion carries `Scenarios:`, with scenario IDs or the + literal `none (inspection)` when only an inspection can prove it. Omit + `Decisions:` when no decision applies. +- Every task in a structured plan carries `Covers:` naming at least one + acceptance criterion defined in the plan. A task without `Covers:` or with an + empty `Covers:` is a defect even when other tasks cover every criterion. +- Every acceptance criterion is named by at least one task's `Covers:`. +- Every scenario is referenced by at least one acceptance criterion's + `Scenarios:`. +- A planned `Check:` is not execution evidence. Only `/validate` running or + performing the check verifies a scenario. + ## No validation task - The last task in the stack is an ordinary implementation task. Do not author a @@ -183,3 +316,7 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. +- A legacy plan stays legacy. Add design decisions, verification scenarios, or + `Covers:` only when the correction explicitly asks to adopt the structured + format. +- Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.claude/skills/sce-next-task/references/output.md b/.claude/skills/sce-next-task/references/output.md index 1ddc071ab..8ca87b94f 100644 --- a/.claude/skills/sce-next-task/references/output.md +++ b/.claude/skills/sce-next-task/references/output.md @@ -113,6 +113,13 @@ Describe the smallest coherent implementation approach with enough detail for ap - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. +## Acceptance coverage + +- Include only when the plan is structured; omit this section for a legacy plan. +- List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. +- Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} diff --git a/.claude/skills/sce-next-task/references/plan-review.md b/.claude/skills/sce-next-task/references/plan-review.md index ae6a97b66..d01d3bffe 100644 --- a/.claude/skills/sce-next-task/references/plan-review.md +++ b/.claude/skills/sce-next-task/references/plan-review.md @@ -74,6 +74,23 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; review it exactly as +described here, with no coverage lookup and no coverage fields in the result. + +For a structured plan, read the selected task's `Covers:` IDs. For each covered +acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the +scenario and decision definitions they name. A covered ID or referenced ID with +no definition in the plan is a dangling reference. Record it as a review risk; +it does not make the task `blocked`. + ## 1.4 Determine readiness A task is `ready` when: @@ -128,6 +145,16 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. +For a structured plan, a `ready` result also identifies: + +- The covered acceptance criteria: each `Covers:` ID with its outcome and + `Validate:` check. +- Their verification scenarios: each scenario ID with its `Then:` and `Check:`. +- Their related design decisions: each decision ID with its decision text. +- Every dangling reference, as a review risk. + +A `ready` result for a legacy plan carries none of these coverage fields. + A `sync_debt` result must identify: - The resolved plan path. diff --git a/.claude/skills/sce-next-task/references/task-execution.md b/.claude/skills/sce-next-task/references/task-execution.md index ae68ed4c3..ab9efcc23 100644 --- a/.claude/skills/sce-next-task/references/task-execution.md +++ b/.claude/skills/sce-next-task/references/task-execution.md @@ -132,6 +132,12 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. +For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +the task's own `Verify` checks. Running a scenario's check during the task is +task-level evidence only. Task execution never checks an acceptance-criterion +checkbox and never records a scenario status; only `/validate` verifies +acceptance criteria and scenarios. + Verification may include: - Targeted tests. @@ -167,6 +173,7 @@ Only after successful implementation and task-level verification: facts above and before the synchronization phase is invoked. - Record material deviations or approved assumptions. - Preserve the plan's existing structure and terminology. +- Keep the task's `Covers:` field as authored. Do not mark the task complete when returning `declined`, `blocked`, or `incomplete`. diff --git a/.claude/skills/sce-validate/references/validation-report.md b/.claude/skills/sce-validate/references/validation-report.md index b0fa7778b..436ec92d3 100644 --- a/.claude/skills/sce-validate/references/validation-report.md +++ b/.claude/skills/sce-validate/references/validation-report.md @@ -31,8 +31,14 @@ writes it. ### Success-criteria verification -- [x] AC1: {criterion statement} -> {evidence} -- [ ] AC2: {criterion statement} -> {evidence of failure or not checked} +- [x] AC1: {criterion statement} -> {evidence}; scenarios: S1 +- [ ] AC2: {criterion statement} -> {evidence of failure or not checked}; scenarios: S2, S3 + +### Scenario verification + +{Only for a structured plan:} + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -61,7 +67,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. + not independently re-evaluate or reclassify criteria here. For a structured + plan, end each acceptance-criterion line with the scenario IDs its + `Scenarios:` field names, or `none (inspection)`. +- Include **Scenario verification** only for a structured plan, with exactly + one entry per scenario in plan order. Each entry gives the scenario ID, the + acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status already established by validation + execution. Omit the section for a legacy plan. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.claude/skills/sce-validate/references/validation.md b/.claude/skills/sce-validate/references/validation.md index 5fdf8e6f9..1b2c50466 100644 --- a/.claude/skills/sce-validate/references/validation.md +++ b/.claude/skills/sce-validate/references/validation.md @@ -57,6 +57,25 @@ From the plan, collect: - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; validate it exactly +as described here, with no scenario collection, no reference check, and no +scenario statuses. + +For a structured plan, also collect: + +- Every design decision. +- Every acceptance criterion's `Decisions:` and `Scenarios:` fields. +- Every verification scenario with its `Then:` and `Check:`. +- Every task's `Covers:` field. + Return `blocked` when the plan has no usable acceptance criteria, or when no validation commands can be determined from the plan or repository conventions. @@ -64,7 +83,28 @@ Prefer the plan's authored checks. Fall back to repository-primary test, lint, and format commands only when `Full validation` is absent, and record that fallback under notes on a `validated` or `failed` result. -### 4. Run full validation and acceptance checks +### 4. Check plan references + +Skip this step for a legacy plan. + +For a structured plan, apply every entry of the plan template's +`Reference rules` to the plan. Each of these is a relationship-integrity +defect: + +- An ID defined more than once. +- A dangling reference: a `Decisions:`, `Scenarios:`, or `Covers:` ID with no + definition in the plan. +- An acceptance criterion without `Scenarios:`. +- An acceptance criterion that no task's `Covers:` names. +- A task without `Covers:` or with an empty `Covers:`. +- A scenario that no acceptance criterion's `Scenarios:` references. + +Record each defect as a failed check under **Failed checks and follow-ups**, +naming the IDs involved. A relationship-integrity defect makes the result +`failed`; never tolerate one silently. Continue gathering the remaining +evidence. + +### 5. Run full validation and acceptance checks Run the plan's `Full validation` commands. @@ -72,6 +112,26 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. +For a structured plan, run or perform every scenario's `Check:` yourself in +this run, and compare the observed result with the scenario's `Then:`. Give +each scenario exactly one status: + +- `Verified` when this run's evidence proves its `Then:` in full. +- `Partially verified` when the check ran but proves `Then:` only in part, for + example at a narrower level than `Then:` requires. +- `Not verified` when the check failed, could not be run or located, or was + not performed. + +Only evidence gathered by this run counts. Task completion, a task's recorded +`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never +evidence. Record every `Partially verified` or `Not verified` scenario as a +failed check with the gap and a required action. + +In a structured plan, an acceptance criterion is met only when its `Validate:` +check passes and every scenario its `Scenarios:` names is `Verified`. An +acceptance criterion marked `none (inspection)` is met by its `Validate:` check +alone. + Treat leftover debug-only flags, temporary files, intermediate artifacts, or local scaffolding as a failed validation check. Record the path and evidence under **Failed checks and follow-ups**; never delete or repair it during @@ -87,7 +147,7 @@ inspection confirmed the criterion. Do not run task-by-task implementation work for incomplete tasks. That belongs to `/next-task`. -### 5. Update the plan +### 6. Update the plan For `validated` and `failed` outcomes: @@ -101,7 +161,7 @@ Do not reopen completed tasks, rewrite task evidence, or change the task stack. For `blocked`, leave the plan file unchanged. -### 6. Return the Markdown report +### 7. Return the Markdown report Return exactly one Markdown report: @@ -176,8 +236,12 @@ invoking workflow. ## Acceptance criteria -- [x] AC1: {criterion statement} — {evidence} -- [x] AC2: {criterion statement} — {evidence} +- [x] AC1: {criterion statement} — {evidence}; scenarios: S1 +- [x] AC2: {criterion statement} — {evidence}; scenarios: none (inspection) + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified ## Residual risks @@ -218,11 +282,19 @@ a normal work session, then rerun validation. - `{check or AC id}`: {problem} - Evidence: {command output, exit summary, or inspection finding} - Required action: {concrete repair or decision} +- `{scenario id}` ({AC ids}): {Partially verified | Not verified} — {gap between the observed result and `Then:`} + - Evidence: {exact test, command, or inspected location and its actual result} + - Required action: {concrete repair or decision} ## Acceptance criteria -- [x] AC1: {criterion} — {evidence} -- [ ] AC2: {criterion} — {why unmet} +- [x] AC1: {criterion} — {evidence}; scenarios: S1 +- [ ] AC2: {criterion} — {why unmet}; scenarios: S2 + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified +- S2 (AC2): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Partially verified | Not verified} ## Commands run @@ -303,6 +375,14 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. +- For a structured plan, the validated and failed variants include **Scenario + verification** with exactly one entry per scenario in plan order: the scenario + ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status. Each acceptance-criterion line + cites its scenario IDs, or `none (inspection)`. In the failed variant, list + every `Partially verified` or `Not verified` scenario under **What failed**. +- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md index 5b614219b..ef084bad3 100644 --- a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md @@ -28,9 +28,16 @@ This phase owns the planning process: - Write or revise exactly one `context/plans/{plan_name}.md`. `references/plan-template.md` is the sole owner of the persisted plan schema. -Read its `Acceptance criteria rules`, `Task rules` and `No validation task` rules, -and `Updating an existing plan` rules before authoring or revising. Apply those -rules rather than restating them here. +Read its `Acceptance criteria rules`, `Design decision rules`, +`Verification scenario rules`, `Reference rules`, `Task rules` and +`No validation task` rules, and `Updating an existing plan` rules before +authoring or revising. Apply those rules rather than restating them here. + +Author plan content in this order: design decisions, then acceptance criteria, +then verification scenarios, then tasks. When revising a legacy plan, skip the +design decision, verification scenario, `Covers:`, and reference-check work +unless the `Updating an existing plan` rules let the correction adopt the +structured format. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -151,31 +158,59 @@ the repository does not yet have. Do not explore the entire repository by default. -## 2.5 Author the acceptance criteria +## 2.5 Record design decisions + +Record the architectural choices this plan commits to under `## Design decisions`, +applying the `Design decision rules` in `references/plan-template.md`. Do not +manufacture a decision to fill the section. When the change commits to no such +choice, omit the section. + +## 2.6 Author the acceptance criteria Derive the plan-specific success outcomes and checks before slicing tasks, then apply the `Acceptance criteria rules` and exact section shape in `references/plan-template.md`. The template owns their generic validation semantics and placement. -## 2.6 Author the task stack +Give each criterion its `Decisions:` and `Scenarios:` fields as the +`Reference rules` define them. + +## 2.7 Author the verification scenarios + +Write the scenarios the acceptance criteria depend on under +`### Verification scenarios`, applying the `Verification scenario rules` in +`references/plan-template.md`. Cover normal, boundary, failure, cancellation, +concurrency, and recovery cases only where the change makes them relevant. +Do not invent test names: a `Check:` names an existing test, a test a task in +the stack creates, a command, or a named inspection. -Slice and order the plan-specific work after the acceptance criteria, applying the -`Task rules` and `No validation task` rules in `references/plan-template.md`. +## 2.8 Author the task stack + +Slice and order the plan-specific work after the acceptance criteria and +verification scenarios, applying the `Task rules` and `No validation task` rules in `references/plan-template.md`. Do not restate those generic task rules here. +Give each task its `Covers:` field as the `Reference rules` define it. + A finished stack always leaves at least one incomplete task, so the workflow can always hand off to `/next-task`. When the request resolves to a plan but produces no incomplete task, because the change is already implemented or already covered by completed tasks, set internal status `blocked` with category `no_actionable_work` instead of writing the plan. -## 2.7 Write the plan +## 2.9 Check references + +Before writing a structured plan, run a reference-consistency pass over the +complete draft that applies every `Reference rules` entry in +`references/plan-template.md`. Fix each defect in the draft before writing. Do +not write a plan that fails this pass. + +## 2.10 Write the plan Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -## 2.8 Return the result +## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.opencode/skills/sce-change-to-plan/references/plan-template.md b/.opencode/skills/sce-change-to-plan/references/plan-template.md index fe0190b94..5a5fa0fe9 100644 --- a/.opencode/skills/sce-change-to-plan/references/plan-template.md +++ b/.opencode/skills/sce-change-to-plan/references/plan-template.md @@ -18,6 +18,17 @@ entirely rather than writing them empty. {One or two paragraphs: what changes, where, and why. State whether this extends existing behavior, replaces it, or preserves work already in progress.} +## Design decisions + +{Optional. Include only real architectural choices this plan commits to. Remove +the section when there are none.} + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + ## Acceptance criteria How this plan is proven complete. Each criterion is observable and names the @@ -26,8 +37,19 @@ performs final validation. - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` + - Decisions: {decision IDs, e.g. D1; omit the field when no decision applies} + - Scenarios: {scenario IDs, e.g. S1, S2} - [ ] AC2: {observable outcome} - Validate: `{command, assertion, or inspection that proves AC2}` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} ### Full validation @@ -70,6 +92,7 @@ recorded. Remove the section otherwise.} - [ ] T01: `{single intent title}` (status:todo) - Task ID: T01 + - Covers: {acceptance criterion IDs, e.g. AC1} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: {task IDs, or none} - Done when: {clear acceptance for one coherent change} @@ -78,6 +101,7 @@ recorded. Remove the section otherwise.} - [ ] T02: `{single intent title}` (status:todo) - Task ID: T02 + - Covers: {acceptance criterion IDs, e.g. AC2} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: T01 - Done when: {clear acceptance for one coherent change} @@ -103,6 +127,7 @@ invent one: `None.` is the expected answer for a well-specified change.} ```markdown - [ ] T02: `Add /auth/refresh endpoint` (status:todo) - Task ID: T02 + - Covers: AC1 - Scope: In — route handler, token validation logic, response schema. Out — refresh token rotation policy (covered in T03), client-side storage changes. - Dependencies: T01 - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired or invalid token; targeted tests pass; OpenAPI spec updated. @@ -110,6 +135,47 @@ invent one: `None.` is the expected answer for a well-specified change.} - Context synchronization: pending ``` +## Filled-in structured plan example + +```markdown +## Design decisions + +- D1: The existing session store validates refresh tokens; no new token service is added. + - Boundary: `src/auth/` owns token validation; route handlers call it and never read the session store directly. + - Rationale: The session store already tracks revocation, so one owner keeps revocation consistent. + - Rejected: Stateless refresh tokens — they cannot be revoked before they expire. + - Trade-offs: Every refresh costs one session-store read. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` returns a signed JWT for a valid refresh token and 401 for an expired or revoked one. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh` with its 200 and 401 responses. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A refresh token that has not expired but is revoked in the session store. + - When: The client calls `POST /auth/refresh` with that token. + - Then: The response is 401 and no JWT is issued. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"`, created by T01. + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — route handler, session-store validation call, OpenAPI entry. Out — refresh token rotation policy. + - Dependencies: none + - Done when: The endpoint returns a JWT or 401 as AC1 states, the revoked-token test exists and passes, and the OpenAPI entry is present. + - Verify: `pnpm test src/auth/refresh.test.ts`. + - Context synchronization: pending +``` + ## Acceptance criteria rules - Acceptance criteria describe the finished system, not the work. Prefer "the @@ -127,6 +193,25 @@ invent one: `None.` is the expected answer for a well-specified change.} change request. If a criterion has no task that could satisfy it, the task stack is incomplete. +## Design decision rules + +- Record a design decision only for a real architectural choice the plan + commits to, such as an ownership boundary, a data shape, an interface, or an + alternative a reviewer would otherwise reopen. +- Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:`. +- A design decision belongs to this plan only. It does not replace an + architecture decision record. + +## Verification scenario rules + +- A scenario states one behavior that an acceptance criterion depends on, as + `Given:`, `When:`, `Then:`, and `Check:`. +- `Then:` states an exact observable result or invariant, not an intention. +- `Check:` names the planned test, command, or named inspection that would + prove `Then:`. Name a test only when it already exists or a task in the stack + creates it. + ## Task rules - Number tasks sequentially as `T01..T0N`. @@ -142,6 +227,54 @@ invent one: `None.` is the expected answer for a well-specified change.} acceptance checks. - Order tasks so each one's declared dependencies precede it. +## Reference rules + +- Read plan structure only from Markdown outside fenced code blocks. A fenced + code block runs from a line that opens a fence of three or more backticks or + tildes to its closing fence. Headings, definitions, and fields inside it are + example text, not plan structure. Keep fenced examples in place; never delete + one to make a plan read correctly. +- A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` + carries `Covers:`. Every other plan is a legacy plan, and the remaining rules + in this section do not apply to it. +- A section runs from its heading to the next heading of any level outside + fenced code blocks. A definition counts only in its own section: `D` entries + under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` + entries under `### Verification scenarios`, and `T` entries under + `## Task stack`. A line shaped like a definition anywhere else, such as in a + fenced example or a validation report, defines nothing. +- A definition's fields are the `- Field:` lines indented one level under its + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and + `Check:` to a verification scenario; and `Covers:` to a task. +- To check a fenced example itself, extract the block's content and apply these + rules to it as a separate sample document. +- IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, + `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry + starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and + `- [ ] T01:`. +- Define each ID exactly once. +- Number decisions, criteria, and scenarios sequentially in order of first + assignment. Never renumber or reuse a decision, criterion, or scenario ID; a + withdrawn entry keeps its ID and says that it is withdrawn. +- Each reference field fits on one line: `Decisions:` and `Scenarios:` on an + acceptance criterion, and `Covers:` on a task. Its value is a list of IDs + separated by a comma and a space, for example `S1, S2`. +- Every referenced ID resolves to a definition in the same plan. +- Every acceptance criterion carries `Scenarios:`, with scenario IDs or the + literal `none (inspection)` when only an inspection can prove it. Omit + `Decisions:` when no decision applies. +- Every task in a structured plan carries `Covers:` naming at least one + acceptance criterion defined in the plan. A task without `Covers:` or with an + empty `Covers:` is a defect even when other tasks cover every criterion. +- Every acceptance criterion is named by at least one task's `Covers:`. +- Every scenario is referenced by at least one acceptance criterion's + `Scenarios:`. +- A planned `Check:` is not execution evidence. Only `/validate` running or + performing the check verifies a scenario. + ## No validation task - The last task in the stack is an ordinary implementation task. Do not author a @@ -183,3 +316,7 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. +- A legacy plan stays legacy. Add design decisions, verification scenarios, or + `Covers:` only when the correction explicitly asks to adopt the structured + format. +- Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.opencode/skills/sce-next-task/references/output.md b/.opencode/skills/sce-next-task/references/output.md index 1ddc071ab..8ca87b94f 100644 --- a/.opencode/skills/sce-next-task/references/output.md +++ b/.opencode/skills/sce-next-task/references/output.md @@ -113,6 +113,13 @@ Describe the smallest coherent implementation approach with enough detail for ap - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. +## Acceptance coverage + +- Include only when the plan is structured; omit this section for a legacy plan. +- List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. +- Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} diff --git a/.opencode/skills/sce-next-task/references/plan-review.md b/.opencode/skills/sce-next-task/references/plan-review.md index ae6a97b66..d01d3bffe 100644 --- a/.opencode/skills/sce-next-task/references/plan-review.md +++ b/.opencode/skills/sce-next-task/references/plan-review.md @@ -74,6 +74,23 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; review it exactly as +described here, with no coverage lookup and no coverage fields in the result. + +For a structured plan, read the selected task's `Covers:` IDs. For each covered +acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the +scenario and decision definitions they name. A covered ID or referenced ID with +no definition in the plan is a dangling reference. Record it as a review risk; +it does not make the task `blocked`. + ## 1.4 Determine readiness A task is `ready` when: @@ -128,6 +145,16 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. +For a structured plan, a `ready` result also identifies: + +- The covered acceptance criteria: each `Covers:` ID with its outcome and + `Validate:` check. +- Their verification scenarios: each scenario ID with its `Then:` and `Check:`. +- Their related design decisions: each decision ID with its decision text. +- Every dangling reference, as a review risk. + +A `ready` result for a legacy plan carries none of these coverage fields. + A `sync_debt` result must identify: - The resolved plan path. diff --git a/.opencode/skills/sce-next-task/references/task-execution.md b/.opencode/skills/sce-next-task/references/task-execution.md index ae68ed4c3..ab9efcc23 100644 --- a/.opencode/skills/sce-next-task/references/task-execution.md +++ b/.opencode/skills/sce-next-task/references/task-execution.md @@ -132,6 +132,12 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. +For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +the task's own `Verify` checks. Running a scenario's check during the task is +task-level evidence only. Task execution never checks an acceptance-criterion +checkbox and never records a scenario status; only `/validate` verifies +acceptance criteria and scenarios. + Verification may include: - Targeted tests. @@ -167,6 +173,7 @@ Only after successful implementation and task-level verification: facts above and before the synchronization phase is invoked. - Record material deviations or approved assumptions. - Preserve the plan's existing structure and terminology. +- Keep the task's `Covers:` field as authored. Do not mark the task complete when returning `declined`, `blocked`, or `incomplete`. diff --git a/.opencode/skills/sce-validate/references/validation-report.md b/.opencode/skills/sce-validate/references/validation-report.md index b0fa7778b..436ec92d3 100644 --- a/.opencode/skills/sce-validate/references/validation-report.md +++ b/.opencode/skills/sce-validate/references/validation-report.md @@ -31,8 +31,14 @@ writes it. ### Success-criteria verification -- [x] AC1: {criterion statement} -> {evidence} -- [ ] AC2: {criterion statement} -> {evidence of failure or not checked} +- [x] AC1: {criterion statement} -> {evidence}; scenarios: S1 +- [ ] AC2: {criterion statement} -> {evidence of failure or not checked}; scenarios: S2, S3 + +### Scenario verification + +{Only for a structured plan:} + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -61,7 +67,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. + not independently re-evaluate or reclassify criteria here. For a structured + plan, end each acceptance-criterion line with the scenario IDs its + `Scenarios:` field names, or `none (inspection)`. +- Include **Scenario verification** only for a structured plan, with exactly + one entry per scenario in plan order. Each entry gives the scenario ID, the + acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status already established by validation + execution. Omit the section for a legacy plan. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.opencode/skills/sce-validate/references/validation.md b/.opencode/skills/sce-validate/references/validation.md index 5fdf8e6f9..1b2c50466 100644 --- a/.opencode/skills/sce-validate/references/validation.md +++ b/.opencode/skills/sce-validate/references/validation.md @@ -57,6 +57,25 @@ From the plan, collect: - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; validate it exactly +as described here, with no scenario collection, no reference check, and no +scenario statuses. + +For a structured plan, also collect: + +- Every design decision. +- Every acceptance criterion's `Decisions:` and `Scenarios:` fields. +- Every verification scenario with its `Then:` and `Check:`. +- Every task's `Covers:` field. + Return `blocked` when the plan has no usable acceptance criteria, or when no validation commands can be determined from the plan or repository conventions. @@ -64,7 +83,28 @@ Prefer the plan's authored checks. Fall back to repository-primary test, lint, and format commands only when `Full validation` is absent, and record that fallback under notes on a `validated` or `failed` result. -### 4. Run full validation and acceptance checks +### 4. Check plan references + +Skip this step for a legacy plan. + +For a structured plan, apply every entry of the plan template's +`Reference rules` to the plan. Each of these is a relationship-integrity +defect: + +- An ID defined more than once. +- A dangling reference: a `Decisions:`, `Scenarios:`, or `Covers:` ID with no + definition in the plan. +- An acceptance criterion without `Scenarios:`. +- An acceptance criterion that no task's `Covers:` names. +- A task without `Covers:` or with an empty `Covers:`. +- A scenario that no acceptance criterion's `Scenarios:` references. + +Record each defect as a failed check under **Failed checks and follow-ups**, +naming the IDs involved. A relationship-integrity defect makes the result +`failed`; never tolerate one silently. Continue gathering the remaining +evidence. + +### 5. Run full validation and acceptance checks Run the plan's `Full validation` commands. @@ -72,6 +112,26 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. +For a structured plan, run or perform every scenario's `Check:` yourself in +this run, and compare the observed result with the scenario's `Then:`. Give +each scenario exactly one status: + +- `Verified` when this run's evidence proves its `Then:` in full. +- `Partially verified` when the check ran but proves `Then:` only in part, for + example at a narrower level than `Then:` requires. +- `Not verified` when the check failed, could not be run or located, or was + not performed. + +Only evidence gathered by this run counts. Task completion, a task's recorded +`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never +evidence. Record every `Partially verified` or `Not verified` scenario as a +failed check with the gap and a required action. + +In a structured plan, an acceptance criterion is met only when its `Validate:` +check passes and every scenario its `Scenarios:` names is `Verified`. An +acceptance criterion marked `none (inspection)` is met by its `Validate:` check +alone. + Treat leftover debug-only flags, temporary files, intermediate artifacts, or local scaffolding as a failed validation check. Record the path and evidence under **Failed checks and follow-ups**; never delete or repair it during @@ -87,7 +147,7 @@ inspection confirmed the criterion. Do not run task-by-task implementation work for incomplete tasks. That belongs to `/next-task`. -### 5. Update the plan +### 6. Update the plan For `validated` and `failed` outcomes: @@ -101,7 +161,7 @@ Do not reopen completed tasks, rewrite task evidence, or change the task stack. For `blocked`, leave the plan file unchanged. -### 6. Return the Markdown report +### 7. Return the Markdown report Return exactly one Markdown report: @@ -176,8 +236,12 @@ invoking workflow. ## Acceptance criteria -- [x] AC1: {criterion statement} — {evidence} -- [x] AC2: {criterion statement} — {evidence} +- [x] AC1: {criterion statement} — {evidence}; scenarios: S1 +- [x] AC2: {criterion statement} — {evidence}; scenarios: none (inspection) + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified ## Residual risks @@ -218,11 +282,19 @@ a normal work session, then rerun validation. - `{check or AC id}`: {problem} - Evidence: {command output, exit summary, or inspection finding} - Required action: {concrete repair or decision} +- `{scenario id}` ({AC ids}): {Partially verified | Not verified} — {gap between the observed result and `Then:`} + - Evidence: {exact test, command, or inspected location and its actual result} + - Required action: {concrete repair or decision} ## Acceptance criteria -- [x] AC1: {criterion} — {evidence} -- [ ] AC2: {criterion} — {why unmet} +- [x] AC1: {criterion} — {evidence}; scenarios: S1 +- [ ] AC2: {criterion} — {why unmet}; scenarios: S2 + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified +- S2 (AC2): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Partially verified | Not verified} ## Commands run @@ -303,6 +375,14 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. +- For a structured plan, the validated and failed variants include **Scenario + verification** with exactly one entry per scenario in plan order: the scenario + ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status. Each acceptance-criterion line + cites its scenario IDs, or `none (inspection)`. In the failed variant, list + every `Partially verified` or `Not verified` scenario under **What failed**. +- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.pi/skills/sce-change-to-plan/references/plan-authoring.md b/.pi/skills/sce-change-to-plan/references/plan-authoring.md index 5b614219b..ef084bad3 100644 --- a/.pi/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.pi/skills/sce-change-to-plan/references/plan-authoring.md @@ -28,9 +28,16 @@ This phase owns the planning process: - Write or revise exactly one `context/plans/{plan_name}.md`. `references/plan-template.md` is the sole owner of the persisted plan schema. -Read its `Acceptance criteria rules`, `Task rules` and `No validation task` rules, -and `Updating an existing plan` rules before authoring or revising. Apply those -rules rather than restating them here. +Read its `Acceptance criteria rules`, `Design decision rules`, +`Verification scenario rules`, `Reference rules`, `Task rules` and +`No validation task` rules, and `Updating an existing plan` rules before +authoring or revising. Apply those rules rather than restating them here. + +Author plan content in this order: design decisions, then acceptance criteria, +then verification scenarios, then tasks. When revising a legacy plan, skip the +design decision, verification scenario, `Covers:`, and reference-check work +unless the `Updating an existing plan` rules let the correction adopt the +structured format. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -151,31 +158,59 @@ the repository does not yet have. Do not explore the entire repository by default. -## 2.5 Author the acceptance criteria +## 2.5 Record design decisions + +Record the architectural choices this plan commits to under `## Design decisions`, +applying the `Design decision rules` in `references/plan-template.md`. Do not +manufacture a decision to fill the section. When the change commits to no such +choice, omit the section. + +## 2.6 Author the acceptance criteria Derive the plan-specific success outcomes and checks before slicing tasks, then apply the `Acceptance criteria rules` and exact section shape in `references/plan-template.md`. The template owns their generic validation semantics and placement. -## 2.6 Author the task stack +Give each criterion its `Decisions:` and `Scenarios:` fields as the +`Reference rules` define them. + +## 2.7 Author the verification scenarios + +Write the scenarios the acceptance criteria depend on under +`### Verification scenarios`, applying the `Verification scenario rules` in +`references/plan-template.md`. Cover normal, boundary, failure, cancellation, +concurrency, and recovery cases only where the change makes them relevant. +Do not invent test names: a `Check:` names an existing test, a test a task in +the stack creates, a command, or a named inspection. -Slice and order the plan-specific work after the acceptance criteria, applying the -`Task rules` and `No validation task` rules in `references/plan-template.md`. +## 2.8 Author the task stack + +Slice and order the plan-specific work after the acceptance criteria and +verification scenarios, applying the `Task rules` and `No validation task` rules in `references/plan-template.md`. Do not restate those generic task rules here. +Give each task its `Covers:` field as the `Reference rules` define it. + A finished stack always leaves at least one incomplete task, so the workflow can always hand off to `/next-task`. When the request resolves to a plan but produces no incomplete task, because the change is already implemented or already covered by completed tasks, set internal status `blocked` with category `no_actionable_work` instead of writing the plan. -## 2.7 Write the plan +## 2.9 Check references + +Before writing a structured plan, run a reference-consistency pass over the +complete draft that applies every `Reference rules` entry in +`references/plan-template.md`. Fix each defect in the draft before writing. Do +not write a plan that fails this pass. + +## 2.10 Write the plan Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -## 2.8 Return the result +## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.pi/skills/sce-change-to-plan/references/plan-template.md b/.pi/skills/sce-change-to-plan/references/plan-template.md index fe0190b94..5a5fa0fe9 100644 --- a/.pi/skills/sce-change-to-plan/references/plan-template.md +++ b/.pi/skills/sce-change-to-plan/references/plan-template.md @@ -18,6 +18,17 @@ entirely rather than writing them empty. {One or two paragraphs: what changes, where, and why. State whether this extends existing behavior, replaces it, or preserves work already in progress.} +## Design decisions + +{Optional. Include only real architectural choices this plan commits to. Remove +the section when there are none.} + +- D1: {decision} + - Boundary: {architectural boundary the decision governs} + - Rationale: {why} + - Rejected: {alternative} — {why not} + - Trade-offs: {known limitation or cost} + ## Acceptance criteria How this plan is proven complete. Each criterion is observable and names the @@ -26,8 +37,19 @@ performs final validation. - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` + - Decisions: {decision IDs, e.g. D1; omit the field when no decision applies} + - Scenarios: {scenario IDs, e.g. S1, S2} - [ ] AC2: {observable outcome} - Validate: `{command, assertion, or inspection that proves AC2}` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: {short title} + - Given: {preconditions} + - When: {action} + - Then: {exact observable result or invariant} + - Check: {planned test, command, or named inspection} ### Full validation @@ -70,6 +92,7 @@ recorded. Remove the section otherwise.} - [ ] T01: `{single intent title}` (status:todo) - Task ID: T01 + - Covers: {acceptance criterion IDs, e.g. AC1} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: {task IDs, or none} - Done when: {clear acceptance for one coherent change} @@ -78,6 +101,7 @@ recorded. Remove the section otherwise.} - [ ] T02: `{single intent title}` (status:todo) - Task ID: T02 + - Covers: {acceptance criterion IDs, e.g. AC2} - Scope: In — {tight scope}. Out — {excluded work}. - Dependencies: T01 - Done when: {clear acceptance for one coherent change} @@ -103,6 +127,7 @@ invent one: `None.` is the expected answer for a well-specified change.} ```markdown - [ ] T02: `Add /auth/refresh endpoint` (status:todo) - Task ID: T02 + - Covers: AC1 - Scope: In — route handler, token validation logic, response schema. Out — refresh token rotation policy (covered in T03), client-side storage changes. - Dependencies: T01 - Done when: `POST /auth/refresh` returns a signed JWT on valid input and 401 on expired or invalid token; targeted tests pass; OpenAPI spec updated. @@ -110,6 +135,47 @@ invent one: `None.` is the expected answer for a well-specified change.} - Context synchronization: pending ``` +## Filled-in structured plan example + +```markdown +## Design decisions + +- D1: The existing session store validates refresh tokens; no new token service is added. + - Boundary: `src/auth/` owns token validation; route handlers call it and never read the session store directly. + - Rationale: The session store already tracks revocation, so one owner keeps revocation consistent. + - Rejected: Stateless refresh tokens — they cannot be revoked before they expire. + - Trade-offs: Every refresh costs one session-store read. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` returns a signed JWT for a valid refresh token and 401 for an expired or revoked one. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 +- [ ] AC2: The OpenAPI spec documents `POST /auth/refresh` with its 200 and 401 responses. + - Validate: inspection of `openapi.yaml` under `paths./auth/refresh`. + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A refresh token that has not expired but is revoked in the session store. + - When: The client calls `POST /auth/refresh` with that token. + - Then: The response is 401 and no JWT is issued. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"`, created by T01. + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — route handler, session-store validation call, OpenAPI entry. Out — refresh token rotation policy. + - Dependencies: none + - Done when: The endpoint returns a JWT or 401 as AC1 states, the revoked-token test exists and passes, and the OpenAPI entry is present. + - Verify: `pnpm test src/auth/refresh.test.ts`. + - Context synchronization: pending +``` + ## Acceptance criteria rules - Acceptance criteria describe the finished system, not the work. Prefer "the @@ -127,6 +193,25 @@ invent one: `None.` is the expected answer for a well-specified change.} change request. If a criterion has no task that could satisfy it, the task stack is incomplete. +## Design decision rules + +- Record a design decision only for a real architectural choice the plan + commits to, such as an ownership boundary, a data shape, an interface, or an + alternative a reviewer would otherwise reopen. +- Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:`. +- A design decision belongs to this plan only. It does not replace an + architecture decision record. + +## Verification scenario rules + +- A scenario states one behavior that an acceptance criterion depends on, as + `Given:`, `When:`, `Then:`, and `Check:`. +- `Then:` states an exact observable result or invariant, not an intention. +- `Check:` names the planned test, command, or named inspection that would + prove `Then:`. Name a test only when it already exists or a task in the stack + creates it. + ## Task rules - Number tasks sequentially as `T01..T0N`. @@ -142,6 +227,54 @@ invent one: `None.` is the expected answer for a well-specified change.} acceptance checks. - Order tasks so each one's declared dependencies precede it. +## Reference rules + +- Read plan structure only from Markdown outside fenced code blocks. A fenced + code block runs from a line that opens a fence of three or more backticks or + tildes to its closing fence. Headings, definitions, and fields inside it are + example text, not plan structure. Keep fenced examples in place; never delete + one to make a plan read correctly. +- A plan is structured when, outside fenced code blocks, it has a + `### Verification scenarios` heading or any task under `## Task stack` + carries `Covers:`. Every other plan is a legacy plan, and the remaining rules + in this section do not apply to it. +- A section runs from its heading to the next heading of any level outside + fenced code blocks. A definition counts only in its own section: `D` entries + under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` + entries under `### Verification scenarios`, and `T` entries under + `## Task stack`. A line shaped like a definition anywhere else, such as in a + fenced example or a validation report, defines nothing. +- A definition's fields are the `- Field:` lines indented one level under its + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and + `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and + `Check:` to a verification scenario; and `Covers:` to a task. +- To check a fenced example itself, extract the block's content and apply these + rules to it as a separate sample document. +- IDs are `D1..Dn` for design decisions, `AC1..ACn` for acceptance criteria, + `S1..Sn` for verification scenarios, and `T01..T0N` for tasks. Each entry + starts its own line with its ID: `- D1:`, `- [ ] AC1:`, `- S1:`, and + `- [ ] T01:`. +- Define each ID exactly once. +- Number decisions, criteria, and scenarios sequentially in order of first + assignment. Never renumber or reuse a decision, criterion, or scenario ID; a + withdrawn entry keeps its ID and says that it is withdrawn. +- Each reference field fits on one line: `Decisions:` and `Scenarios:` on an + acceptance criterion, and `Covers:` on a task. Its value is a list of IDs + separated by a comma and a space, for example `S1, S2`. +- Every referenced ID resolves to a definition in the same plan. +- Every acceptance criterion carries `Scenarios:`, with scenario IDs or the + literal `none (inspection)` when only an inspection can prove it. Omit + `Decisions:` when no decision applies. +- Every task in a structured plan carries `Covers:` naming at least one + acceptance criterion defined in the plan. A task without `Covers:` or with an + empty `Covers:` is a defect even when other tasks cover every criterion. +- Every acceptance criterion is named by at least one task's `Covers:`. +- Every scenario is referenced by at least one acceptance criterion's + `Scenarios:`. +- A planned `Check:` is not execution evidence. Only `/validate` running or + performing the check verifies a scenario. + ## No validation task - The last task in the stack is an ordinary implementation task. Do not author a @@ -183,3 +316,7 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. +- A legacy plan stays legacy. Add design decisions, verification scenarios, or + `Covers:` only when the correction explicitly asks to adopt the structured + format. +- Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.pi/skills/sce-next-task/references/output.md b/.pi/skills/sce-next-task/references/output.md index 1ddc071ab..8ca87b94f 100644 --- a/.pi/skills/sce-next-task/references/output.md +++ b/.pi/skills/sce-next-task/references/output.md @@ -113,6 +113,13 @@ Describe the smallest coherent implementation approach with enough detail for ap - Include only risks relevant to approving this task. - Omit this section when there are no meaningful risks. +## Acceptance coverage + +- Include only when the plan is structured; omit this section for a legacy plan. +- List each covered acceptance criterion `{AC id}: {outcome}` with its + scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. +- Report dangling references under **Risks or trade-offs**. + ## Verification - {task.verification} diff --git a/.pi/skills/sce-next-task/references/plan-review.md b/.pi/skills/sce-next-task/references/plan-review.md index ae6a97b66..d01d3bffe 100644 --- a/.pi/skills/sce-next-task/references/plan-review.md +++ b/.pi/skills/sce-next-task/references/plan-review.md @@ -74,6 +74,23 @@ architecture, shared terminology, or cross-domain interfaces. Do not explore the entire repository by default. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; review it exactly as +described here, with no coverage lookup and no coverage fields in the result. + +For a structured plan, read the selected task's `Covers:` IDs. For each covered +acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the +scenario and decision definitions they name. A covered ID or referenced ID with +no definition in the plan is a dangling reference. Record it as a review risk; +it does not make the task `blocked`. + ## 1.4 Determine readiness A task is `ready` when: @@ -128,6 +145,16 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. +For a structured plan, a `ready` result also identifies: + +- The covered acceptance criteria: each `Covers:` ID with its outcome and + `Validate:` check. +- Their verification scenarios: each scenario ID with its `Then:` and `Check:`. +- Their related design decisions: each decision ID with its decision text. +- Every dangling reference, as a review risk. + +A `ready` result for a legacy plan carries none of these coverage fields. + A `sync_debt` result must identify: - The resolved plan path. diff --git a/.pi/skills/sce-next-task/references/task-execution.md b/.pi/skills/sce-next-task/references/task-execution.md index ae68ed4c3..ab9efcc23 100644 --- a/.pi/skills/sce-next-task/references/task-execution.md +++ b/.pi/skills/sce-next-task/references/task-execution.md @@ -132,6 +132,12 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. +For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +the task's own `Verify` checks. Running a scenario's check during the task is +task-level evidence only. Task execution never checks an acceptance-criterion +checkbox and never records a scenario status; only `/validate` verifies +acceptance criteria and scenarios. + Verification may include: - Targeted tests. @@ -167,6 +173,7 @@ Only after successful implementation and task-level verification: facts above and before the synchronization phase is invoked. - Record material deviations or approved assumptions. - Preserve the plan's existing structure and terminology. +- Keep the task's `Covers:` field as authored. Do not mark the task complete when returning `declined`, `blocked`, or `incomplete`. diff --git a/.pi/skills/sce-validate/references/validation-report.md b/.pi/skills/sce-validate/references/validation-report.md index b0fa7778b..436ec92d3 100644 --- a/.pi/skills/sce-validate/references/validation-report.md +++ b/.pi/skills/sce-validate/references/validation-report.md @@ -31,8 +31,14 @@ writes it. ### Success-criteria verification -- [x] AC1: {criterion statement} -> {evidence} -- [ ] AC2: {criterion statement} -> {evidence of failure or not checked} +- [x] AC1: {criterion statement} -> {evidence}; scenarios: S1 +- [ ] AC2: {criterion statement} -> {evidence of failure or not checked}; scenarios: S2, S3 + +### Scenario verification + +{Only for a structured plan:} + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -61,7 +67,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. + not independently re-evaluate or reclassify criteria here. For a structured + plan, end each acceptance-criterion line with the scenario IDs its + `Scenarios:` field names, or `none (inspection)`. +- Include **Scenario verification** only for a structured plan, with exactly + one entry per scenario in plan order. Each entry gives the scenario ID, the + acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status already established by validation + execution. Omit the section for a legacy plan. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.pi/skills/sce-validate/references/validation.md b/.pi/skills/sce-validate/references/validation.md index 5fdf8e6f9..1b2c50466 100644 --- a/.pi/skills/sce-validate/references/validation.md +++ b/.pi/skills/sce-validate/references/validation.md @@ -57,6 +57,25 @@ From the plan, collect: - Every acceptance criterion and its `Validate:` check. - The `Full validation` command list. +Read plan structure only outside fenced code blocks; headings, definitions, and +fields inside a fenced example are not plan structure. A definition counts only +in its own section: `D` entries under `## Design decisions`, `AC` entries under +`## Acceptance criteria`, `S` entries under `### Verification scenarios`, and +`T` entries under `## Task stack`. + +A plan is structured when, outside fenced code blocks, it has a +`### Verification scenarios` heading or any task under `## Task stack` carries +`Covers:`. Every other plan is a legacy plan; validate it exactly +as described here, with no scenario collection, no reference check, and no +scenario statuses. + +For a structured plan, also collect: + +- Every design decision. +- Every acceptance criterion's `Decisions:` and `Scenarios:` fields. +- Every verification scenario with its `Then:` and `Check:`. +- Every task's `Covers:` field. + Return `blocked` when the plan has no usable acceptance criteria, or when no validation commands can be determined from the plan or repository conventions. @@ -64,7 +83,28 @@ Prefer the plan's authored checks. Fall back to repository-primary test, lint, and format commands only when `Full validation` is absent, and record that fallback under notes on a `validated` or `failed` result. -### 4. Run full validation and acceptance checks +### 4. Check plan references + +Skip this step for a legacy plan. + +For a structured plan, apply every entry of the plan template's +`Reference rules` to the plan. Each of these is a relationship-integrity +defect: + +- An ID defined more than once. +- A dangling reference: a `Decisions:`, `Scenarios:`, or `Covers:` ID with no + definition in the plan. +- An acceptance criterion without `Scenarios:`. +- An acceptance criterion that no task's `Covers:` names. +- A task without `Covers:` or with an empty `Covers:`. +- A scenario that no acceptance criterion's `Scenarios:` references. + +Record each defect as a failed check under **Failed checks and follow-ups**, +naming the IDs involved. A relationship-integrity defect makes the result +`failed`; never tolerate one silently. Continue gathering the remaining +evidence. + +### 5. Run full validation and acceptance checks Run the plan's `Full validation` commands. @@ -72,6 +112,26 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. +For a structured plan, run or perform every scenario's `Check:` yourself in +this run, and compare the observed result with the scenario's `Then:`. Give +each scenario exactly one status: + +- `Verified` when this run's evidence proves its `Then:` in full. +- `Partially verified` when the check ran but proves `Then:` only in part, for + example at a narrower level than `Then:` requires. +- `Not verified` when the check failed, could not be run or located, or was + not performed. + +Only evidence gathered by this run counts. Task completion, a task's recorded +`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never +evidence. Record every `Partially verified` or `Not verified` scenario as a +failed check with the gap and a required action. + +In a structured plan, an acceptance criterion is met only when its `Validate:` +check passes and every scenario its `Scenarios:` names is `Verified`. An +acceptance criterion marked `none (inspection)` is met by its `Validate:` check +alone. + Treat leftover debug-only flags, temporary files, intermediate artifacts, or local scaffolding as a failed validation check. Record the path and evidence under **Failed checks and follow-ups**; never delete or repair it during @@ -87,7 +147,7 @@ inspection confirmed the criterion. Do not run task-by-task implementation work for incomplete tasks. That belongs to `/next-task`. -### 5. Update the plan +### 6. Update the plan For `validated` and `failed` outcomes: @@ -101,7 +161,7 @@ Do not reopen completed tasks, rewrite task evidence, or change the task stack. For `blocked`, leave the plan file unchanged. -### 6. Return the Markdown report +### 7. Return the Markdown report Return exactly one Markdown report: @@ -176,8 +236,12 @@ invoking workflow. ## Acceptance criteria -- [x] AC1: {criterion statement} — {evidence} -- [x] AC2: {criterion statement} — {evidence} +- [x] AC1: {criterion statement} — {evidence}; scenarios: S1 +- [x] AC2: {criterion statement} — {evidence}; scenarios: none (inspection) + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified ## Residual risks @@ -218,11 +282,19 @@ a normal work session, then rerun validation. - `{check or AC id}`: {problem} - Evidence: {command output, exit summary, or inspection finding} - Required action: {concrete repair or decision} +- `{scenario id}` ({AC ids}): {Partially verified | Not verified} — {gap between the observed result and `Then:`} + - Evidence: {exact test, command, or inspected location and its actual result} + - Required action: {concrete repair or decision} ## Acceptance criteria -- [x] AC1: {criterion} — {evidence} -- [ ] AC2: {criterion} — {why unmet} +- [x] AC1: {criterion} — {evidence}; scenarios: S1 +- [ ] AC2: {criterion} — {why unmet}; scenarios: S2 + +## Scenario verification + +- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: Verified +- S2 (AC2): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Partially verified | Not verified} ## Commands run @@ -303,6 +375,14 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. +- For a structured plan, the validated and failed variants include **Scenario + verification** with exactly one entry per scenario in plan order: the scenario + ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, + `command`, or `inspection`), the exact test, command, or inspected location, + the actual result, and the scenario status. Each acceptance-criterion line + cites its scenario IDs, or `none (inspection)`. In the failed variant, list + every `Partially verified` or `Not verified` scenario under **What failed**. +- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. From 1c82b1f79f9c6e204d638fe938562a5ee78a3893 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 00:43:05 +0200 Subject: [PATCH 10/61] cli: Extract SCE plan structure from Markdown AST Add the initial read-only plan service backed by markdown-rs GFM mdast parsing. Model authoritative sections, definitions, fields, source positions, fenced/outside IDs, and preserve inline field values without line-scanning structure. Add focused fixture tests for fenced examples, misplaced definitions, legacy plans, and ID grammar. This establishes the extraction seam for `sce plan check`; validation and CLI wiring remain follow-up tasks. Ref: context/plans/sce-plan-check.md (T01) Co-authored-by: SCE --- cli/Cargo.lock | 16 ++ cli/Cargo.toml | 1 + cli/src/services/mod.rs | 5 + cli/src/services/plan/extract.rs | 376 +++++++++++++++++++++++++++++++ cli/src/services/plan/mod.rs | 1 + context/architecture.md | 3 +- context/cli/plan-service.md | 31 +++ context/context-map.md | 1 + context/plans/sce-plan-check.md | 224 ++++++++++++++++++ 9 files changed, 657 insertions(+), 1 deletion(-) create mode 100644 cli/src/services/plan/extract.rs create mode 100644 cli/src/services/plan/mod.rs create mode 100644 context/cli/plan-service.md create mode 100644 context/plans/sce-plan-check.md diff --git a/cli/Cargo.lock b/cli/Cargo.lock index c364cdce0..59c54b233 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -2471,6 +2471,15 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3823,6 +3832,7 @@ dependencies = [ "inquire", "jsonschema", "keyring-core", + "markdown", "murmur3", "opentelemetry", "opentelemetry-http", @@ -4869,6 +4879,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a842db904..e9014888d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -50,6 +50,7 @@ tracing = "0.1" uuid = { version = "1", features = ["v4", "v7"] } murmur3 = "0.5.2" +markdown = "1" opentelemetry-otlp = { version = "=0.33.1", default-features = false, features = ["http-proto", "trace", "logs"] } opentelemetry-http = "=0.33.1" tracing-opentelemetry = "=0.34.0" diff --git a/cli/src/services/mod.rs b/cli/src/services/mod.rs index 2612c646b..28d168dd7 100644 --- a/cli/src/services/mod.rs +++ b/cli/src/services/mod.rs @@ -29,6 +29,11 @@ pub mod observability; pub mod output_format; pub mod parse; pub mod patch; +#[cfg_attr( + not(test), + allow(dead_code, reason = "wired into the CLI by the plan check command") +)] +pub mod plan; pub mod repository_identity; pub mod resilience; pub mod security; diff --git a/cli/src/services/plan/extract.rs b/cli/src/services/plan/extract.rs new file mode 100644 index 000000000..05a4a2a6a --- /dev/null +++ b/cli/src/services/plan/extract.rs @@ -0,0 +1,376 @@ +use anyhow::{anyhow, Result}; +use markdown::mdast::Node; +use markdown::{to_mdast, ParseOptions}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PlanEntryKind { + Decision, + AcceptanceCriterion, + Scenario, + Task, +} + +impl PlanEntryKind { + fn of_id(id: &str) -> Self { + if id.starts_with("AC") { + Self::AcceptanceCriterion + } else if id.starts_with('D') { + Self::Decision + } else if id.starts_with('S') { + Self::Scenario + } else { + Self::Task + } + } + + fn of_section(heading: &PlanHeading) -> Option { + match (heading.depth, heading.text.as_str()) { + (2, "Design decisions") => Some(Self::Decision), + (2, "Acceptance criteria") => Some(Self::AcceptanceCriterion), + (3, "Verification scenarios") => Some(Self::Scenario), + (2, "Task stack") => Some(Self::Task), + _ => None, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanHeading { + pub depth: u8, + pub text: String, + pub line: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanField { + pub key: String, + pub value: String, + pub line: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanEntry { + pub id: String, + pub kind: PlanEntryKind, + pub checked: Option, + pub line: usize, + pub fields: Vec, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Plan { + pub headings: Vec, + pub entries: Vec, + pub outside_section_ids: Vec, + pub fenced_ids: Vec, +} + +impl Plan { + pub fn entries_of(&self, kind: PlanEntryKind) -> impl Iterator { + self.entries.iter().filter(move |entry| entry.kind == kind) + } +} + +pub fn extract(source: &str) -> Result { + let root = parse(source)?; + let mut plan = Plan::default(); + let mut section = None; + + for node in root.children().into_iter().flatten() { + match node { + Node::Heading(heading) => { + let heading = PlanHeading { + depth: heading.depth, + text: node.to_string(), + line: line_of(node), + }; + section = PlanEntryKind::of_section(&heading); + plan.headings.push(heading); + } + Node::List(_) => { + for item in node.children().into_iter().flatten() { + match section.and_then(|kind| definition(source, item, kind)) { + Some(entry) => plan.entries.push(entry), + None => collect_outside_section_ids(source, item, &mut plan), + } + } + } + _ => collect_outside_section_ids(source, node, &mut plan), + } + } + + Ok(plan) +} + +fn parse(source: &str) -> Result { + to_mdast(source, &ParseOptions::gfm()) + .map_err(|error| anyhow!("failed to parse Markdown: {error}")) +} + +fn definition(source: &str, item: &Node, section: PlanEntryKind) -> Option { + let Node::ListItem(list_item) = item else { + return None; + }; + let (id, _) = parse_id_token(lead_text(source, item)?)?; + let kind = PlanEntryKind::of_id(id); + if kind != section { + return None; + } + + let fields = item + .children() + .into_iter() + .flatten() + .filter(|child| matches!(child, Node::List(_))) + .flat_map(|list| list.children().into_iter().flatten()) + .filter_map(|field_item| field(source, field_item)) + .collect(); + + Some(PlanEntry { + id: id.to_string(), + kind, + checked: list_item.checked, + line: line_of(item), + fields, + }) +} + +fn field(source: &str, item: &Node) -> Option { + let (key, value) = parse_field_key(lead_text(source, item)?)?; + Some(PlanField { + key: key.to_string(), + value: value.to_string(), + line: line_of(item), + }) +} + +fn collect_outside_section_ids(source: &str, node: &Node, plan: &mut Plan) { + match node { + Node::Code(code) => collect_fenced_ids(&code.value, &mut plan.fenced_ids), + Node::ListItem(_) => { + if let Some((id, _)) = lead_text(source, node).and_then(parse_id_token) { + plan.outside_section_ids.push(id.to_string()); + } + for child in node.children().into_iter().flatten() { + collect_outside_section_ids(source, child, plan); + } + } + _ => { + for child in node.children().into_iter().flatten() { + collect_outside_section_ids(source, child, plan); + } + } + } +} + +fn collect_fenced_ids(code: &str, ids: &mut Vec) { + let Ok(root) = parse(code) else { + return; + }; + let mut nested = Plan::default(); + collect_outside_section_ids(code, &root, &mut nested); + ids.extend(nested.outside_section_ids); + ids.extend(nested.fenced_ids); +} + +fn lead_text<'a>(source: &'a str, item: &Node) -> Option<&'a str> { + let paragraph = item.children()?.first()?; + if !matches!(paragraph, Node::Paragraph(_)) { + return None; + } + let position = paragraph.position()?; + source.get(position.start.offset..position.end.offset) +} + +fn line_of(node: &Node) -> usize { + node.position().map_or(0, |position| position.start.line) +} + +fn parse_id_token(text: &str) -> Option<(&str, &str)> { + let (id, rest) = text.split_once(':')?; + if !is_id(id) || !(rest.is_empty() || rest.starts_with(char::is_whitespace)) { + return None; + } + Some((id, rest)) +} + +pub fn is_id(token: &str) -> bool { + let (digits, minimum_digits) = if let Some(digits) = token.strip_prefix("AC") { + (digits, 1) + } else if let Some(digits) = token.strip_prefix('D').or_else(|| token.strip_prefix('S')) { + (digits, 1) + } else if let Some(digits) = token.strip_prefix('T') { + return digits.len() >= 2 && digits.bytes().all(|byte| byte.is_ascii_digit()); + } else { + return false; + }; + digits.len() >= minimum_digits + && digits.bytes().all(|byte| byte.is_ascii_digit()) + && !digits.starts_with('0') +} + +fn parse_field_key(text: &str) -> Option<(&str, &str)> { + let (key, rest) = text.split_once(':')?; + let mut chars = key.chars(); + let starts_upper = chars.next().is_some_and(|first| first.is_ascii_uppercase()); + let key_chars_valid = chars.all(|ch| ch.is_ascii_alphabetic() || ch == ' ' || ch == '-'); + if !starts_upper || !key_chars_valid { + return None; + } + if !(rest.is_empty() || rest.starts_with(char::is_whitespace)) { + return None; + } + Some((key, rest.trim())) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use super::{extract, is_id, Plan, PlanEntryKind}; + + fn fixture(name: &str) -> Plan { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../config/pkl/renderers/fixtures/plan-structure") + .join(name); + let source = fs::read_to_string(&path).expect("fixture should be readable"); + extract(&source).expect("fixture should parse") + } + + fn entry_lines(plan: &Plan) -> Vec<(&str, PlanEntryKind, usize)> { + plan.entries + .iter() + .map(|entry| (entry.id.as_str(), entry.kind, entry.line)) + .collect() + } + + #[test] + fn fenced_example_plan_yields_only_out_of_fence_definitions() { + let plan = fixture("fenced-example-plan.md"); + + assert_eq!( + entry_lines(&plan), + vec![ + ("D1", PlanEntryKind::Decision, 37), + ("AC1", PlanEntryKind::AcceptanceCriterion, 45), + ("S1", PlanEntryKind::Scenario, 52), + ("T01", PlanEntryKind::Task, 60), + ] + ); + assert_eq!(plan.fenced_ids, vec!["D1", "AC1", "S1", "T01"]); + assert!(plan.outside_section_ids.is_empty()); + } + + #[test] + fn fenced_example_plan_headings_skip_fenced_headings() { + let plan = fixture("fenced-example-plan.md"); + let headings: Vec<(u8, &str, usize)> = plan + .headings + .iter() + .map(|heading| (heading.depth, heading.text.as_str(), heading.line)) + .collect(); + + assert_eq!( + headings, + vec![ + (1, "Plan: fenced-example-plan", 1), + (2, "Change summary", 3), + (3, "Format at a glance", 7), + (2, "Design decisions", 35), + (2, "Acceptance criteria", 43), + (3, "Verification scenarios", 50), + (2, "Task stack", 58), + (2, "Open questions", 69), + ] + ); + } + + #[test] + fn definition_fields_are_sliced_from_source_with_lines() { + let plan = fixture("fenced-example-plan.md"); + let task = plan + .entries_of(PlanEntryKind::Task) + .next() + .expect("task should be extracted"); + let fields: Vec<(&str, &str, usize)> = task + .fields + .iter() + .map(|field| (field.key.as_str(), field.value.as_str(), field.line)) + .collect(); + + assert_eq!(task.checked, Some(false)); + assert_eq!( + fields, + vec![ + ("Task ID", "T01", 61), + ("Covers", "AC1", 62), + ("Scope", "In — route handler. Out — rotation policy.", 63), + ("Dependencies", "none", 64), + ( + "Done when", + "The endpoint returns 401 for a revoked token.", + 65 + ), + ("Verify", "`pnpm test src/auth/refresh.test.ts`", 66), + ("Context synchronization", "pending", 67), + ] + ); + + let decision = plan + .entries_of(PlanEntryKind::Decision) + .next() + .expect("decision should be extracted"); + assert_eq!(decision.checked, None); + } + + #[test] + fn misplaced_definition_yields_no_scenario_entry() { + let plan = fixture("misplaced-definition.md"); + + assert_eq!(plan.entries_of(PlanEntryKind::Scenario).count(), 0); + assert_eq!(plan.outside_section_ids, vec!["S1"]); + let criterion = plan + .entries_of(PlanEntryKind::AcceptanceCriterion) + .next() + .expect("acceptance criterion should be extracted"); + let keys: Vec<&str> = criterion + .fields + .iter() + .map(|field| field.key.as_str()) + .collect(); + assert_eq!(keys, vec!["Validate", "Decisions", "Scenarios"]); + } + + #[test] + fn legacy_plan_with_fenced_structured_example_ignores_fenced_structure() { + let plan = fixture("legacy-plan-with-fenced-structured-example.md"); + + assert_eq!( + entry_lines(&plan), + vec![ + ("AC1", PlanEntryKind::AcceptanceCriterion, 21), + ("T01", PlanEntryKind::Task, 26), + ] + ); + assert!(!plan + .headings + .iter() + .any(|heading| heading.text == "Verification scenarios")); + assert!(plan.entries[1] + .fields + .iter() + .all(|field| field.key != "Covers")); + assert_eq!(plan.fenced_ids, vec!["S1", "T01"]); + } + + #[test] + fn id_tokens_follow_plan_id_grammar() { + for valid in ["D1", "AC12", "S3", "T01", "T100"] { + assert!(is_id(valid), "{valid} should be an ID"); + } + for invalid in ["D0", "AC", "S01", "T1", "X1", "AC1a", ""] { + assert!(!is_id(invalid), "{invalid} should not be an ID"); + } + } +} diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs new file mode 100644 index 000000000..16e7036d2 --- /dev/null +++ b/cli/src/services/plan/mod.rs @@ -0,0 +1 @@ +pub mod extract; diff --git a/context/architecture.md b/context/architecture.md index c13ca8749..c53a3d4c6 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -82,8 +82,9 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. +- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model (see `context/cli/plan-service.md`). It is not yet wired to a CLI command. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). -- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. +- `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, plan, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. - `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.95.0.default` with `rustfmt` + `clippy`, and reads package/check version from repo-root `.version`. Its `cliGeneratedInput` derivation invokes the shared generated-input producer from a declarative source containing the producer and canonical inputs, then publishes the producer-validated handoff as its store output. Native, release, test, and Clippy Cargo derivations receive that same store path through `SCE_CLI_GENERATED_INPUT_DIR`; their Cargo environments exclude Pkl and assert that it is unavailable. Repository-mode `cli/build.rs` validates the handoff before copying it into `OUT_DIR`; published crates use the validated packaging-only fallback. The build script stages SQL under `OUT_DIR/static/migrations` and writes `OUT_DIR/generated_migrations.rs` with deterministic migration constants sorted by numeric filename prefix. - Crane dependency-only derivations and `cli-fmt` intentionally do not receive `SCE_CLI_GENERATED_INPUT_DIR`, so canonical generation changes invalidate the producer and compiling derivations while preserving host/musl dependency artifacts and formatting. The root flake runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed paths and exposes `cli-generated-input` as the focused payload/inventory integrity check. It also exposes directory-scoped JS validation derivations for `npm/` and `config/lib/`, while `pkl-generated` uses a narrow canonical-input source set plus maybe-missing forbidden paths so reintroduced generated repository artifacts fail the check. diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md new file mode 100644 index 000000000..e6bb03303 --- /dev/null +++ b/context/cli/plan-service.md @@ -0,0 +1,31 @@ +# Plan Service + +`cli/src/services/plan/` owns deterministic, read-only structural analysis of one SCE plan Markdown file. It is the Rust counterpart of the test-only Pkl checker `config/pkl/renderers/plan-structure.pkl`. + +## Current scope + +- `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. +- Validation rules, the shared fixture manifest, and the `sce plan check` CLI command are not implemented yet (plan `context/plans/sce-plan-check.md`, T02–T03). The module is not reachable from the CLI. + +## Plan model + +- `Plan.headings`: one `PlanHeading { depth, text, line }` per root-level heading, where `text` is the heading's plain text. +- `Plan.entries`: one `PlanEntry { id, kind, checked, line, fields }` per definition, in source order. Use `Plan::entries_of(kind)` to select one kind. `PlanEntryKind` is one of `Decision`, `AcceptanceCriterion`, `Scenario`, or `Task`. +- `PlanField { key, value, line }`: one per directly nested list item whose first paragraph starts with `Key:`. The key is `[A-Z][A-Za-z -]*`, followed by whitespace or the end of the text. `value` is the trimmed source slice after the colon, so inline code and punctuation are kept verbatim. +- `Plan.outside_section_ids` and `Plan.fenced_ids` hold the IDs of definition-shaped list items that are not definitions. The first lists items found outside an authoritative position. The second lists items found inside `Code` blocks, which are re-parsed with `to_mdast` rather than scanned by line. They support undefined-reference hints and never create entries. +- Lines are 1-based and come from `mdast` positions. + +## Extraction rules + +- A section is the run of root children after a heading, up to the next heading of any depth. The authoritative sections are `## Design decisions` (D), `## Acceptance criteria` (AC), `### Verification scenarios` (S), and `## Task stack` (T). +- A definition is a `ListItem` of a root-level `List` in the section that matches its kind. Its first paragraph must start with an ID token (`D`, `AC`, `S` with `n` ≥ 1 and no leading zero, or `T` plus two or more digits), then `:`, then whitespace or the end of the text. The GFM `checked` value gives `checked`. +- A definition-shaped item of the wrong kind, in a non-authoritative section, or nested inside another list item is not an entry. +- `Code` nodes, both fenced and indented, never contribute headings, entries, or fields. + +## Divergences from the Pkl line scanner + +The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at exactly `entry indent + 2` and accepts definitions at any indentation. Indentation-only nesting differences, `[X]` checkboxes, and multi-line field values can therefore extract differently. Agreement is pinned by the shared fixtures under `config/pkl/renderers/fixtures/plan-structure/`. + +## Tests + +`cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, legacy plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. diff --git a/context/context-map.md b/context/context-map.md index 2952617ed..f3e0a8a46 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -18,6 +18,7 @@ Feature/domain context: - `context/cli/agent-trace-storage.md` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) +- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md new file mode 100644 index 000000000..3835a6602 --- /dev/null +++ b/context/plans/sce-plan-check.md @@ -0,0 +1,224 @@ +# Plan: sce-plan-check + +## Change summary + +Add `sce plan check `, a deterministic, read-only Rust CLI command that +validates the structure and reference integrity of one selected SCE plan. It +works in the distributed `sce` binary and needs no Pkl, Nix, LLM, or network +access. Today, structural checking exists only as the test-only Pkl checker +`config/pkl/renderers/plan-structure.pkl` (run through +`config/pkl/renderers/plan-structure-check.pkl` and the fixtures under +`config/pkl/renderers/fixtures/plan-structure/` by `config/pkl/check-generated.sh`). +The `/change-to-plan` and `/validate` workflows apply the `Reference rules` by +hand. Users of the shipped binary have no deterministic checker. + +The Rust checker parses Markdown with `markdown-rs` (`markdown` crate) into a +typed GFM `mdast` tree, extracts an SCE plan model with source positions, and +runs pure validation over that model. It keeps the Pkl checker's behavior: both +implementations run against the same fixtures, driven by one shared expectations +manifest. The canonical `/change-to-plan` and `/validate` workflow sources then +call `sce plan check` for structured plans and keep their current legacy-plan +behavior. This extends existing behavior. The Pkl checker stays in place as the +generation-time reference implementation. + +## Design decisions + +- D1: `markdown-rs` (`markdown` crate, `to_mdast` with `ParseOptions::gfm()`) is the only Markdown parser; plan structure is extracted from the typed `mdast` tree, never from line scanning. + - Boundary: `cli/src/services/plan/extract.rs` is the only code that touches `markdown::mdast::Node`. Sections are the runs of root children between `Heading` nodes, identified by depth and plain text. A definition is a `ListItem` of a root-level `List` in its authoritative section whose first paragraph starts with an ID token and `:`; GFM `checked` gives the checkbox. A definition's fields are the `ListItem`s of its directly nested `List`. Field values are sliced from the original source using node positions, so Markdown is never rewritten. `Code` nodes, both fenced and indented, are never visited for structure. Simple string parsing applies only to ID tokens and reference values after structural extraction. + - Rationale: CommonMark/GFM nesting, fences, and checkboxes are what the plan rules actually depend on. An AST gives them correctly, along with line positions for diagnostics. + - Rejected: Porting the Pkl regex/line scanner to Rust — this is the custom Markdown parser the request forbids. `comrak` / `pulldown-cmark` — the request names `markdown-rs` explicitly, and a second parser adds no value. + - Trade-offs: The AST follows CommonMark list-nesting rules, while the Pkl checker requires exactly `entry indent + 2` spaces. Inputs that only one of them treats as nested can diverge. The fixtures pin the shared behavior, and the contract document (D3) records the known divergences. +- D2: Parsing, plan representation, semantic validation, and CLI integration are separate layers in a new `cli/src/services/plan/` service with static dispatch. + - Boundary: `extract.rs` turns source into `Plan`: headings with lines, plus `PlanEntry { id, kind, checked, line, fields: Vec }` grouped by kind. `validate.rs` holds a pure `fn validate(plan: &Plan) -> PlanReport { structured, diagnostics: Vec, message }> }` with no I/O. `command.rs` reads the file, calls extract then validate, and renders the result. `cli_schema.rs`, `services/parse/command_runtime.rs`, and `services/command_registry.rs` add one `Plan`/`PlanCommand` variant each. No `dyn`, no boxed futures, no async, no persistent state. + - Rationale: Pure validation over a small owned model can be tested without the CLI. Keeping the AST out of `validate.rs` stops parser types from leaking into the rules. + - Rejected: Validating directly over `mdast` nodes — this mixes traversal with rules and makes the rules parser-shaped. A trait-based checker abstraction — there is only one implementation. + - Trade-offs: One small intermediate model to maintain. It is kept minimal: one entry type with a kind enum, not four structs per kind. +- D3: One shared fixture contract prevents Pkl/Rust drift. Both checkers run the same `config/pkl/renderers/fixtures/plan-structure/*.md` fixtures from a single manifest, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` (fixture, `structured`/`legacy`/`fail`, expected diagnostic substrings). Rust diagnostic messages reuse the Pkl defect wording verbatim. + - Boundary: `config/pkl/check-generated.sh` reads the manifest instead of hard-coding the `expect_plan_structure_pass`/`expect_plan_structure_failure` calls. A Rust test in `cli/src/services/plan/` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. That tree already reaches the Crane sandbox through `cliBuildInputFileset` (`./config/pkl`), so no flake change is needed. The behavioral contract is documented in `context/sce/plan-structure-contract.md`. + - Rationale: A single manifest makes "same fixtures, same verdicts, same defect text" mechanically true rather than a convention. + - Rejected: Separate Rust fixture copies or a Rust-only expectations table — two equivalent sets drift. Making the Rust CLI or its tests invoke Pkl — the distributed binary must not need Pkl. + - Trade-offs: Pkl defect wording becomes a cross-implementation contract, and rewording requires touching both checkers. +- D4: Outcome mapping reuses existing CLI classification and the Pkl result wording. + - Boundary: A valid plan prints `structured plan: reference integrity holds` or `legacy plan: structured reference rules do not apply` to stdout and exits `0`. A structurally invalid plan returns `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`), whose stderr message lists one diagnostic per line as `{path}:{line}: {message}`, or `{path}: {message}` for plan-level defects such as a missing heading. `{path}` is the path as given. Diagnostics are ordered by line, then message, for deterministic output. Missing or unreadable files go through the existing runtime-failure path (exit `4`) with `with_context` naming the path. + - Rationale: Exit `3` already means validation failure in `context/sce/cli-exit-code-contract.md`. Reusing the Pkl strings keeps D3's contract simple. + - Rejected: A new exit code for plan defects — it breaks the class-based contract. Printing diagnostics to stdout — stdout is reserved for command payloads. + - Trade-offs: The diagnostics share the `CliError` stderr envelope (including the class's `Try:` guidance) rather than a bespoke report format. +- D5: The workflows trust the checker only when it actually ran. A result counts only on exit `0` with one of the two D4 classification lines on stdout, or exit `3` with diagnostics. Any other outcome means the checker is unavailable, and the workflow falls back to its existing manual `Reference rules` pass and says so. + - Boundary: Canonical `config/pkl/base/workflow-change-to-plan.pkl` (step 2.9/2.10) and `config/pkl/base/workflow-validate.pkl` (step 4 "Check plan references") only. `/next-task` is untouched. + - Rationale: An older `sce` binary resolves the unknown `plan` command to help with exit `0` (per the exit-code contract), so exit status alone would report a false pass. + - Rejected: Requiring the new binary and failing without it — this would break workflows for users on an older `sce`. + - Trade-offs: The workflow prose carries one extra fallback rule. + +## Acceptance criteria + +- [ ] AC1: `sce plan check ` validates exactly the selected file, read-only. It accepts relative and absolute paths. It exits `0` and prints the structured or legacy classification line for a valid plan. It exits `3` and prints `{path}:{line}: {message}` diagnostics on stderr for a structurally invalid plan. A missing or unreadable file fails through the existing runtime-failure handling. It never executes `Check:`, `Validate:`, or `Verify:` content. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` plus `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md; echo $?` (expect `0`) and the same against `duplicate-id.md` (expect `3`). + - Decisions: D1, D2, D4 + - Scenarios: S1, S2, S3, S4 +- [ ] AC2: The Rust validator and the Pkl checker give the same verdict and defect text for every fixture under `config/pkl/renderers/fixtures/plan-structure/`, both driven by the single `expectations.tsv` manifest. The fixtures cover valid structured, inspection-only, fenced-example, legacy, and legacy-with-fenced-structured-example plans. They also cover duplicate IDs, missing and empty `Covers:`, dangling references, wrong reference kinds, uncovered AC, unreferenced scenario, missing required fields, misplaced definitions and fields, wrong heading placement, and keywords only inside fenced examples. + - Validate: `nix run .#pkl-check-generated` and `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture` + - Decisions: D1, D3 + - Scenarios: S5, S6 +- [ ] AC3: Every definition-, field-, and heading-anchored diagnostic carries the 1-based source line of the offending node, taken from `mdast` positions. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::diagnostic_lines` + - Decisions: D1, D4 + - Scenarios: S7 +- [ ] AC4: `sce plan` appears in top-level help, `sce plan check --help` documents the required `` argument, and generated bash/zsh/fish completion scripts include `plan check`. + - Validate: `nix run .#sce -- --help`, `nix run .#sce -- plan check --help`, and `nix run .#sce -- completion --shell bash | grep -n "plan"` + - Decisions: D2 + - Scenarios: S8 +- [ ] AC5: The generated `/change-to-plan` workflow runs `sce plan check {plan-path}` after writing a structured plan and corrects reported defects before returning `plan_ready`. The generated `/validate` workflow runs `sce plan check {plan-path}` before scenario verification for structured plans and records each reported defect as a failed check. Both fall back to the manual `Reference rules` pass when the checker is unavailable, keep legacy-plan behavior unchanged, and leave `/next-task` unchanged. + - Validate: `nix run .#pkl-check-generated` + - Decisions: D5 + - Scenarios: S9 +- [ ] AC6: The Pkl/Rust behavioral contract is documented: the shared rules, the shared manifest as the source of truth, the result and diagnostic wording, and the known AST-versus-line-scanner divergences. + - Validate: inspection of `context/sce/plan-structure-contract.md`, and of `context/sce/shared-context-plan-workflow.md` linking to it. + - Scenarios: none (inspection) +- [ ] AC7: The only new runtime dependency is `markdown`. No repository plan under `context/plans/` is read by generation or Nix checks, and `flake.nix` gains no `context/plans/` source dependency. + - Validate: inspection of `git diff main -- cli/Cargo.toml flake.nix config/pkl/check-generated.sh` + - Scenarios: none (inspection) + +### Verification scenarios + +- S1: Valid structured and legacy plans pass + - Given: `valid-structured-plan.md`, `inspection-only-plan.md`, and `legacy-plan.md` fixtures. + - When: `sce plan check` runs on each one, once by relative path and once by absolute path. + - Then: Exit `0`. Stdout is exactly `structured plan: reference integrity holds` for the structured fixtures and `legacy plan: structured reference rules do not apply` for the legacy fixture. Stderr is empty. + - Check: Rust command test `plan::command_valid_plans_exit_success`, created by T03, plus the AC1 `nix run .#sce` command. +- S2: Structurally invalid plan fails with validation class + - Given: The `duplicate-id.md` fixture. + - When: `sce plan check` runs on it. + - Then: Exit `3`. Stderr contains `SCE-ERR-VALIDATION` and one `{path}:{line}:` diagnostic carrying the Pkl defect text for the duplicated `S1` ID ("is defined more than once"). Stdout is empty. + - Check: Rust command test `plan::command_invalid_plan_exits_validation`, created by T03, plus the AC1 `nix run .#sce` command. +- S3: Missing file uses existing runtime error handling + - Given: A path that does not exist. + - When: `sce plan check` runs on it. + - Then: Exit `4`. Stderr names the path and carries `SCE-ERR-RUNTIME`. No diagnostics are printed. + - Check: Rust command test `plan::command_missing_file_is_runtime_failure`, created by T03. +- S4: The checker never executes plan commands + - Given: A structured fixture whose `Check:`, `Validate:`, and `Verify:` values name a command that would create a marker file. + - When: `sce plan check` runs on it. + - Then: Exit `0`, and the marker file does not exist afterwards. + - Check: Rust command test `plan::command_does_not_execute_plan_commands`, created by T03, plus named inspection that `cli/src/services/plan/` contains no `std::process` use. +- S5: Rust matches every manifest expectation + - Given: `expectations.tsv` and every fixture it lists. + - When: The Rust fixture test extracts and validates each fixture. + - Then: Each fixture classified `structured` or `legacy` has no diagnostics and that classification. Each `fail` fixture produces a diagnostic containing every expected substring. Every `.md` fixture in the directory is listed in the manifest. + - Check: Rust test `plan::fixture_expectations_match_manifest`, created by T02. +- S6: Pkl matches every manifest expectation + - Given: The same `expectations.tsv`. + - When: `config/pkl/check-generated.sh` iterates it through `plan-structure-check.pkl`. + - Then: Every pass fixture prints its classification, every fail fixture fails with each expected substring, and the run aborts on any mismatch or unlisted fixture. + - Check: `nix run .#pkl-check-generated` +- S7: Diagnostics point at the offending line + - Given: The `misplaced-field.md`, `duplicate-id.md`, and `task-empty-covers.md` fixtures. + - When: The Rust validator runs on each one. + - Then: Each diagnostic's `line` equals the 1-based line of the offending field or definition in the fixture source. A plan-level missing-heading defect has no line. + - Check: Rust test `plan::diagnostic_lines_match_source_positions`, created by T02. +- S8: Help and completion expose the command + - Given: The built `sce` binary. + - When: Top-level help, `plan check --help`, and `completion --shell {bash,zsh,fish}` run. + - Then: Top-level help lists `plan`. Command help shows the required `` argument. Each completion script contains `plan` and `check`. + - Check: Existing help/completion tests updated by T03, plus the AC4 commands. +- S9: Generated workflows invoke the checker with fallback + - Given: The canonical workflow Pkl sources after T04. + - When: Generation and the contract checks run. + - Then: Every target's generated `sce-change-to-plan/references/plan-authoring.md` and `sce-validate/references/validation.md` contain the `sce plan check` invocation, the D5 trust rule, the manual fallback, and the legacy skip. No generated `sce-next-task` document mentions `sce plan check`. + - Check: `nix run .#pkl-check-generated`, with the extended `plan-authoring-structured-references` and `validate-scenario-evidence` assertions from T04. + +### Full validation + +- `nix flake check` +- `nix run .#pkl-check-generated` + +### Context sync + +- `context/sce/plan-structure-contract.md` (new, the Pkl/Rust behavioral contract) +- `context/sce/shared-context-plan-workflow.md` (responsibility split: `sce plan check` for one selected plan, checker fallback) +- `context/sce/config-generation.md` (`check-generated.sh` drives plan-structure fixtures from `expectations.tsv`) +- `context/cli/cli-command-surface.md` and `context/overview.md` (new `plan` command and `services/plan` domain) +- `context/glossary.md` (`sce plan check`, plan-structure expectations manifest) +- `context/context-map.md` (link the new contract file) + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `cli/Cargo.toml`/`Cargo.lock` (`markdown`), new `cli/src/services/plan/` (`mod.rs`, `extract.rs`, `validate.rs`, `command.rs`), `cli/src/services/mod.rs`, `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, `cli/src/services/command_registry.rs`, the help/completion tests they affect, `config/pkl/renderers/fixtures/plan-structure/` (manifest plus new fixtures), `config/pkl/check-generated.sh`, `config/pkl/base/workflow-change-to-plan.pkl`, `config/pkl/base/workflow-validate.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, and `context/sce/plan-structure-contract.md`. +- **Out of scope:** `sce plan init`. Repository-wide plan scanning. Validating plans inside `nix flake check` or generation. Any new Nix source dependency on `context/plans/`. Executing planned tests or validation commands. Editing historical plans. YAML/JSON sidecar schemas. `/next-task` gates and task-completion semantics. Git hooks. Unrelated CLI services. +- **Constraints:** No `comrak`, `pulldown-cmark`, or custom Markdown parser, and no regex/line scanning for structure. The distributed binary never invokes Pkl. Static dispatch, with no `dyn`, boxed futures, or background tasks. Rust import grouping and `anyhow` context conventions apply. Stable diagnostic wording is shared with Pkl. All commands run through Nix. +- **Non-goal:** A general Markdown linter or auto-fixer. The checker reports defects and never rewrites the plan. + +## Assumptions + +- `markdown = "1"` is used as the request specifies. `Cargo.lock` pins the resolved version. +- The command lives under a new top-level `plan` command group shown in top-level help. It is visible rather than hidden because workflows and users invoke it directly. +- The success output reuses `plan-structure-check.pkl`'s single-plan strings exactly. There is no `--format json` in this change. +- The heading, ordering, required-field, ownership, reference, and coverage rules are exactly those in `config/pkl/renderers/plan-structure.pkl`'s `check` path. Template-skeleton mode (`templateDefects`) stays Pkl-only because it validates generated templates, not plans. +- New fixtures are added where the current set lacks a case the request names: a wrong reference kind (for example `Covers: S1`), and one fixture for the S4 no-execution check if an existing one cannot serve. +- Rust fixture tests read `config/pkl` at test runtime. That tree already reaches the Crane sandbox through `cliBuildInputFileset`, and crates.io packaging never compiles tests, so neither needs a flake change. + +## Task stack + +- [x] T01: `Extract SCE plan structure from the markdown-rs AST` (status:done) + - Task ID: T01 + - Covers: AC2, AC7 + - Scope: In — add `markdown = "1"` to `cli/Cargo.toml`. Add `cli/src/services/plan/{mod.rs,extract.rs}` with the D1/D2 `Plan`/`PlanEntry`/`PlanField` model, extracted from `to_mdast(source, &ParseOptions::gfm())`. Covered: sections by heading depth and text; definitions only in authoritative sections; GFM checkbox; nested field items; source-sliced values; 1-based lines; `Code` nodes ignored. Add focused extraction unit tests on fenced-example and legacy-with-fenced fixtures. Out — validation rules, CLI wiring, manifest. + - Dependencies: none + - Done when: Extraction of `fenced-example-plan.md` yields only the out-of-fence definitions with correct lines. Extraction of `misplaced-definition.md` yields no `S` entry. No regex or line scanner identifies structure, and no other Markdown crate is added. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::extract` + - Completed: 2026-10-24 + - Files changed: `cli/Cargo.toml`, `cli/Cargo.lock`, `cli/src/services/mod.rs`, `cli/src/services/plan/mod.rs`, `cli/src/services/plan/extract.rs` + - Result: Added `markdown = "1"` (locked `markdown 1.0.0`, plus its transitive `unicode-id 0.3.7`). `services::plan::extract::extract(&str) -> Result` parses with `to_mdast(.., &ParseOptions::gfm())`. It walks the root children, tracking the current heading (`PlanHeading { depth, text, line }`). It takes definitions only from `ListItem`s of root-level `List`s in authoritative sections whose first paragraph starts with an ID token and `:`. It gets fields from directly nested `List` items with a `Key:` prefix, slices values from source by node offsets, and records 1-based lines from `mdast` positions. `PlanEntry { id, kind: PlanEntryKind, checked, line, fields }` is stored in source order, with `Plan::entries_of(kind)`. Approved assumptions: `Plan` also records `outside_section_ids` (definition-shaped items outside authoritative positions) and `fenced_ids` (definition-shaped items found by re-parsing `Code` values with `to_mdast`, never line scanning) for T02's undefined-reference hints. `pub mod plan` carries `#[cfg_attr(not(test), allow(dead_code, ...))]` until T03 wires the command. Remove it there. + - Verify outcome: `plan::extract` passed (6 tests: fenced-example entries/lines/fenced IDs, fenced headings skipped, source-sliced fields with lines and GFM checkbox, misplaced-definition yields no `S` entry, legacy-with-fenced example ignored, ID grammar). Additionally `clippy --all-targets` (pedantic, deny warnings) and `cargo fmt` are clean. No regex, line scanner, `comrak`, or `pulldown-cmark` appears under `cli/src/services/plan/`. + - Context impact: localized — new internal `cli/src/services/plan/` domain (extraction model only, not yet reachable from the CLI) and a new `markdown` runtime dependency. No user-visible behavior change. + - Context synchronization: synced + +- [ ] T02: `Validate plan structure in pure Rust against the shared fixture manifest` (status:todo) + - Task ID: T02 + - Covers: AC2, AC3 + - Scope: In — `cli/src/services/plan/validate.rs` with a pure `validate(&Plan) -> PlanReport` that ports the `plan-structure.pkl` `check` rules and defect wording: structured detection, required, duplicate, and ordered headings; `### Verification scenarios` parent; duplicate IDs; field ownership; required fields; ID-list grammar; reference kind and resolution with the outside-section/fenced hints; AC coverage; scenario referencing. Diagnostics are sorted deterministically. Add `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` and the new wrong-reference-kind fixture. Switch `config/pkl/check-generated.sh` from hard-coded calls to iterating the manifest, and fail when a fixture is unlisted. Add `plan::fixture_expectations_match_manifest` and `plan::diagnostic_lines_match_source_positions`. Out — CLI wiring, workflow prose, template-skeleton mode. + - Dependencies: T01 + - Done when: Both the Rust fixture test and `pkl-check-generated` pass from the same manifest, and every fixture's verdict and defect text agree. The diagnostic-line test asserts exact lines. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix run .#pkl-check-generated` + - Context synchronization: pending + +- [ ] T03: `Wire sce plan check into the CLI schema and static dispatcher` (status:todo) + - Task ID: T03 + - Covers: AC1, AC4 + - Scope: In — `cli_schema.rs`: `PLAN_*` constants, a `Commands::Plan { subcommand: PlanSubcommand::Check { path: PathBuf } }` variant, and a `TOP_LEVEL_COMMANDS` entry. Conversion in `services/parse/command_runtime.rs`, a `RuntimeCommand::Plan` variant in `services/command_registry.rs`, and `services/plan/command.rs`, which reads the file with path context (runtime failure) and maps defects to `CliError::validation` with D4 rendering. Update the affected help/completion tests, and add the S1–S4 command tests. Out — workflow prose, any `plan init`. + - Dependencies: T02 + - Done when: S1–S4 tests pass. `sce plan check` on a valid fixture exits `0` and on an invalid one exits `3` with `{path}:{line}:` diagnostics. Help and completion list `plan check`. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml completion`; `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/duplicate-id.md; echo $?` + - Context synchronization: pending + +- [ ] T04: `Invoke sce plan check from the change-to-plan and validate workflows` (status:todo) + - Task ID: T04 + - Covers: AC5 + - Scope: In — `config/pkl/base/workflow-change-to-plan.pkl` steps 2.9–2.10: after writing a structured plan, run `sce plan check {plan-path}`, fix the reported defects, and re-run before `plan_ready`. `config/pkl/base/workflow-validate.pkl` step 4: run the checker before scenario verification and record each diagnostic as a failed check. Both get the D5 trust rule, the manual `Reference rules` fallback, and the legacy skip. Extend the existing `plan-authoring-structured-references` and `validate-scenario-evidence` assertions in `generation-contract-check.pkl`, and the negative fixtures they already use, so the invocation, fallback, and `/next-task` exclusion are required. Out — `/next-task` sources, Git hooks, new assertion families. + - Dependencies: T03 + - Done when: Generated plan-authoring and validation references on every target carry the invocation, trust rule, fallback, and legacy skip, and `pkl-check-generated` passes. + - Verify: `nix run .#pkl-check-generated` + - Context synchronization: pending + +- [ ] T05: `Document the Pkl/Rust plan-structure behavioral contract` (status:todo) + - Task ID: T05 + - Covers: AC6 + - Scope: In — new `context/sce/plan-structure-contract.md` covering: the shared rule set; `expectations.tsv` as the single source of truth and how to add a fixture; the D4 result and diagnostic wording; the rule that changing defect text requires both checkers; the known AST-versus-line-scanner divergences (list-nesting indentation tolerance, `[X]` checkboxes, definitions nested inside other list items, multi-line reference values); and that Pkl stays generation-only while the CLI never invokes Pkl. Link it from `context/sce/shared-context-plan-workflow.md` and `context/context-map.md`. Out — code changes. + - Dependencies: T04 + - Done when: The contract file exists with those sections and is linked from both files. + - Verify: inspection of `context/sce/plan-structure-contract.md` and its links. + - Context synchronization: pending + +## Open questions + +- Should a known AST-versus-line-scanner divergence (D1 trade-off) be fixed in the Pkl checker so the two agree beyond the fixtures, or only documented? The plan documents divergences and pins agreement through fixtures. Tightening `plan-structure.pkl` would be a follow-up. From 245ccc437778fbedb9063681abd7fb960fc2fd83 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 00:53:24 +0200 Subject: [PATCH 11/61] plan: Validate plan structure against shared fixture manifest Keep the Rust validator aligned with the Pkl structural checker and make fixture verdicts a shared contract. Add deterministic diagnostics for heading, definition, field, and reference defects, drive both Rust and Pkl checks from expectations.tsv, and cover wrong-reference-kind behavior plus diagnostic locations. Record the completed validation layer and its generation-contract documentation without changing CLI behavior. Co-authored-by: SCE --- cli/src/services/plan/extract.rs | 2 +- cli/src/services/plan/mod.rs | 136 +++++++ cli/src/services/plan/validate.rs | 332 ++++++++++++++++++ config/pkl/check-generated.sh | 90 +++-- .../fixtures/plan-structure/expectations.tsv | 18 + .../plan-structure/wrong-reference-kind.md | 47 +++ context/architecture.md | 2 +- context/cli/plan-service.md | 37 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/plans/sce-plan-check.md | 9 +- context/sce/config-generation.md | 4 +- 12 files changed, 641 insertions(+), 40 deletions(-) create mode 100644 cli/src/services/plan/validate.rs create mode 100644 config/pkl/renderers/fixtures/plan-structure/expectations.tsv create mode 100644 config/pkl/renderers/fixtures/plan-structure/wrong-reference-kind.md diff --git a/cli/src/services/plan/extract.rs b/cli/src/services/plan/extract.rs index 05a4a2a6a..3ff03625b 100644 --- a/cli/src/services/plan/extract.rs +++ b/cli/src/services/plan/extract.rs @@ -11,7 +11,7 @@ pub enum PlanEntryKind { } impl PlanEntryKind { - fn of_id(id: &str) -> Self { + pub fn of_id(id: &str) -> Self { if id.starts_with("AC") { Self::AcceptanceCriterion } else if id.starts_with('D') { diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 16e7036d2..3f79ca12d 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -1 +1,137 @@ pub mod extract; +pub mod validate; + +#[cfg(test)] +fn fixture_dir() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../config/pkl/renderers/fixtures/plan-structure") +} + +#[cfg(test)] +fn validate_source(source: &str) -> validate::PlanReport { + validate::validate(&extract::extract(source).expect("plan should parse")) +} + +#[cfg(test)] +fn validate_fixture(name: &str) -> validate::PlanReport { + let source = + std::fs::read_to_string(fixture_dir().join(name)).expect("fixture should be readable"); + validate_source(&source) +} + +#[cfg(test)] +fn diagnostic_lines(report: &validate::PlanReport) -> Vec<(Option, &str)> { + report + .diagnostics + .iter() + .map(|diagnostic| (diagnostic.line, diagnostic.message.as_str())) + .collect() +} + +#[test] +fn fixture_expectations_match_manifest() { + let manifest = std::fs::read_to_string(fixture_dir().join("expectations.tsv")) + .expect("manifest should be readable"); + let mut listed = Vec::new(); + + for row in manifest + .lines() + .filter(|row| !row.trim().is_empty() && !row.starts_with('#')) + { + let mut columns = row.split('\t'); + let fixture = columns.next().expect("row should name a fixture"); + let verdict = columns.next().expect("row should carry a verdict"); + let expected: Vec<&str> = columns.collect(); + let report = validate_fixture(fixture); + listed.push(fixture.to_string()); + + match verdict { + "structured" | "legacy" => { + assert!( + expected.is_empty(), + "{fixture}: pass rows take no substrings" + ); + assert_eq!( + report.structured, + verdict == "structured", + "{fixture}: wrong classification" + ); + assert!( + report.diagnostics.is_empty(), + "{fixture}: unexpected diagnostics {:?}", + report.diagnostics + ); + } + "fail" => { + assert!(!expected.is_empty(), "{fixture}: fail rows need substrings"); + for substring in expected { + assert!( + report + .diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains(substring)), + "{fixture}: missing `{substring}` in {:?}", + report.diagnostics + ); + } + } + other => panic!("{fixture}: unknown verdict `{other}`"), + } + } + + let mut fixtures: Vec = std::fs::read_dir(fixture_dir()) + .expect("fixture directory should be readable") + .map(|entry| { + entry + .expect("fixture entry should be readable") + .file_name() + .to_string_lossy() + .into_owned() + }) + .filter(|name| { + std::path::Path::new(name) + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("md")) + }) + .collect(); + fixtures.sort(); + listed.sort(); + assert_eq!(listed, fixtures, "manifest must list every fixture once"); +} + +#[test] +fn diagnostic_lines_match_source_positions() { + assert_eq!( + diagnostic_lines(&validate_fixture("misplaced-field.md")), + vec![ + ( + Some(14), + "`Check:` on `D1` is misplaced; it belongs to a verification scenario" + ), + (Some(18), "`AC1` is not covered by any task's `Covers:`"), + ( + Some(22), + "`Covers:` on `AC1` is misplaced; it belongs to a task" + ), + (Some(26), "`S1` is missing `Check:`"), + (Some(37), "`T01` is missing `Covers:`"), + ] + ); + assert_eq!( + diagnostic_lines(&validate_fixture("duplicate-id.md")), + vec![(Some(29), "`S1` is defined more than once")] + ); + assert_eq!( + diagnostic_lines(&validate_fixture("task-empty-covers.md")), + vec![( + Some(47), + "`Covers:` on `T02` is not a comma-separated ID list" + )] + ); + assert_eq!( + diagnostic_lines(&validate_source( + "# Plan\n\n## Acceptance criteria\n\n### Verification scenarios\n" + )), + vec![(None, "heading `## Task stack` is missing")] + ); +} diff --git a/cli/src/services/plan/validate.rs b/cli/src/services/plan/validate.rs new file mode 100644 index 000000000..3157a12ea --- /dev/null +++ b/cli/src/services/plan/validate.rs @@ -0,0 +1,332 @@ +use crate::services::plan::extract::{ + is_id, Plan, PlanEntry, PlanEntryKind, PlanField, PlanHeading, +}; + +const INSPECTION_ONLY: &str = "none (inspection)"; +const VERIFICATION_SCENARIOS_HEADING: &str = "### Verification scenarios"; +const ACCEPTANCE_CRITERIA_HEADING: &str = "## Acceptance criteria"; +const REQUIRED_PLAN_HEADINGS: [&str; 2] = [ACCEPTANCE_CRITERIA_HEADING, "## Task stack"]; +const ORDERED_PLAN_HEADINGS: [&str; 4] = [ + "## Design decisions", + ACCEPTANCE_CRITERIA_HEADING, + VERIFICATION_SCENARIOS_HEADING, + "## Task stack", +]; + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct PlanDiagnostic { + pub line: Option, + pub message: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanReport { + pub structured: bool, + pub diagnostics: Vec, +} + +impl PlanEntryKind { + fn authoritative_section(self) -> &'static str { + match self { + Self::Decision => "## Design decisions", + Self::AcceptanceCriterion => ACCEPTANCE_CRITERIA_HEADING, + Self::Scenario => VERIFICATION_SCENARIOS_HEADING, + Self::Task => "## Task stack", + } + } + + fn display_name(self) -> &'static str { + match self { + Self::Decision => "a design decision", + Self::AcceptanceCriterion => "an acceptance criterion", + Self::Scenario => "a verification scenario", + Self::Task => "a task", + } + } + + fn required_fields(self) -> &'static [&'static str] { + match self { + Self::Decision => &["Boundary", "Rationale", "Rejected", "Trade-offs"], + Self::AcceptanceCriterion => &["Validate", "Scenarios"], + Self::Scenario => &["Given", "When", "Then", "Check"], + Self::Task => &["Covers"], + } + } +} + +fn field_owner(key: &str) -> Option { + match key { + "Boundary" | "Rationale" | "Rejected" | "Trade-offs" => Some(PlanEntryKind::Decision), + "Validate" | "Decisions" | "Scenarios" => Some(PlanEntryKind::AcceptanceCriterion), + "Given" | "When" | "Then" | "Check" => Some(PlanEntryKind::Scenario), + "Covers" => Some(PlanEntryKind::Task), + _ => None, + } +} + +fn reference_kind(key: &str) -> Option { + match key { + "Decisions" => Some(PlanEntryKind::Decision), + "Scenarios" => Some(PlanEntryKind::Scenario), + "Covers" => Some(PlanEntryKind::AcceptanceCriterion), + _ => None, + } +} + +fn diagnostic(line: Option, message: String) -> PlanDiagnostic { + PlanDiagnostic { line, message } +} + +fn field_of<'a>(entry: &'a PlanEntry, key: &str) -> Option<&'a PlanField> { + entry.fields.iter().find(|field| field.key == key) +} + +fn listed_ids(value: &str) -> Option> { + let ids: Vec<&str> = value.split(", ").collect(); + ids.iter().all(|id| is_id(id)).then_some(ids) +} + +fn heading_text(heading: &PlanHeading) -> String { + format!( + "{} {}", + "#".repeat(usize::from(heading.depth)), + heading.text + ) +} + +pub fn validate(plan: &Plan) -> PlanReport { + let headings: Vec<(String, usize)> = plan + .headings + .iter() + .map(|heading| (heading_text(heading), heading.line)) + .collect(); + let structured = headings + .iter() + .any(|(text, _)| text == VERIFICATION_SCENARIOS_HEADING) + || plan + .entries_of(PlanEntryKind::Task) + .any(|entry| field_of(entry, "Covers").is_some()); + + if !structured { + return PlanReport { + structured, + diagnostics: Vec::new(), + }; + } + + let mut diagnostics = heading_diagnostics(&headings); + diagnostics.extend(definition_diagnostics(plan)); + diagnostics.extend(reference_diagnostics(plan)); + diagnostics.sort(); + + PlanReport { + structured, + diagnostics, + } +} + +fn first_line(headings: &[(String, usize)], text: &str) -> Option { + headings + .iter() + .find(|(heading, _)| heading == text) + .map(|(_, line)| *line) +} + +fn first_index(headings: &[(String, usize)], text: &str) -> Option { + headings.iter().position(|(heading, _)| heading == text) +} + +fn heading_diagnostics(headings: &[(String, usize)]) -> Vec { + let mut diagnostics: Vec = REQUIRED_PLAN_HEADINGS + .iter() + .filter(|required| first_index(headings, required).is_none()) + .map(|required| diagnostic(None, format!("heading `{required}` is missing"))) + .collect(); + + for ordered in ORDERED_PLAN_HEADINGS { + if let Some((_, line)) = headings + .iter() + .filter(|(heading, _)| heading == ordered) + .nth(1) + { + diagnostics.push(diagnostic( + Some(*line), + format!("heading `{ordered}` appears more than once"), + )); + } + } + + let present: Vec<&str> = ORDERED_PLAN_HEADINGS + .into_iter() + .filter(|ordered| first_index(headings, ordered).is_some()) + .collect(); + for pair in present.windows(2) { + let (earlier, later) = (pair[0], pair[1]); + if first_index(headings, earlier) > first_index(headings, later) { + diagnostics.push(diagnostic( + first_line(headings, later), + format!("heading `{later}` must come after `{earlier}`"), + )); + } + } + + if let Some(index) = first_index(headings, VERIFICATION_SCENARIOS_HEADING) { + let parent = headings[..index] + .iter() + .rev() + .find(|(heading, _)| heading.starts_with("# ") || heading.starts_with("## ")); + if parent.is_none_or(|(heading, _)| heading != ACCEPTANCE_CRITERIA_HEADING) { + diagnostics.push(diagnostic( + Some(headings[index].1), + format!( + "heading `{VERIFICATION_SCENARIOS_HEADING}` is not inside `{ACCEPTANCE_CRITERIA_HEADING}`" + ), + )); + } + } + + diagnostics +} + +fn definition_diagnostics(plan: &Plan) -> Vec { + let mut diagnostics = Vec::new(); + let mut seen: Vec<&str> = Vec::new(); + let mut reported: Vec<&str> = Vec::new(); + + for entry in &plan.entries { + if seen.contains(&entry.id.as_str()) { + if !reported.contains(&entry.id.as_str()) { + reported.push(&entry.id); + diagnostics.push(diagnostic( + Some(entry.line), + format!("`{}` is defined more than once", entry.id), + )); + } + } else { + seen.push(&entry.id); + } + + for field in &entry.fields { + if let Some(owner) = field_owner(&field.key).filter(|owner| *owner != entry.kind) { + diagnostics.push(diagnostic( + Some(field.line), + format!( + "`{}:` on `{}` is misplaced; it belongs to {}", + field.key, + entry.id, + owner.display_name() + ), + )); + } + } + + for key in entry.kind.required_fields() { + if field_of(entry, key).is_none() { + diagnostics.push(diagnostic( + Some(entry.line), + format!("`{}` is missing `{key}:`", entry.id), + )); + } + } + } + + diagnostics +} + +fn undefined_hint(plan: &Plan, id: &str) -> String { + if plan.outside_section_ids.iter().any(|other| other == id) { + format!( + "; it appears only outside `{}`", + PlanEntryKind::of_id(id).authoritative_section() + ) + } else if plan.fenced_ids.iter().any(|other| other == id) { + "; it appears only inside a fenced code block".to_string() + } else { + String::new() + } +} + +fn reference_diagnostics(plan: &Plan) -> Vec { + let references: Vec<(&PlanEntry, &PlanField, PlanEntryKind)> = plan + .entries + .iter() + .flat_map(|entry| { + entry.fields.iter().filter_map(move |field| { + let kind = reference_kind(&field.key)?; + (field_owner(&field.key) == Some(entry.kind)).then_some((entry, field, kind)) + }) + }) + .collect(); + let listed_by = |key: &str| -> Vec<&str> { + references + .iter() + .filter(|(_, field, _)| field.key == key) + .filter_map(|(_, field, _)| listed_ids(&field.value)) + .flatten() + .collect() + }; + let covered_ids = listed_by("Covers"); + let scenario_ids = listed_by("Scenarios"); + let mut diagnostics = Vec::new(); + + for (owner, field, kind) in &references { + if field.key == "Scenarios" && field.value == INSPECTION_ONLY { + continue; + } + let Some(ids) = listed_ids(&field.value) else { + diagnostics.push(diagnostic( + Some(field.line), + format!( + "`{}:` on `{}` is not a comma-separated ID list", + field.key, owner.id + ), + )); + continue; + }; + for id in ids { + if PlanEntryKind::of_id(id) != *kind { + diagnostics.push(diagnostic( + Some(field.line), + format!( + "`{}:` on `{}` names `{id}`, which is not {}", + field.key, + owner.id, + kind.display_name() + ), + )); + } else if !plan.entries.iter().any(|entry| entry.id == id) { + diagnostics.push(diagnostic( + Some(field.line), + format!( + "`{}:` on `{}` references undefined `{id}`{}", + field.key, + owner.id, + undefined_hint(plan, id) + ), + )); + } + } + } + + for entry in plan.entries_of(PlanEntryKind::AcceptanceCriterion) { + if !covered_ids.contains(&entry.id.as_str()) { + diagnostics.push(diagnostic( + Some(entry.line), + format!("`{}` is not covered by any task's `Covers:`", entry.id), + )); + } + } + for entry in plan.entries_of(PlanEntryKind::Scenario) { + if !scenario_ids.contains(&entry.id.as_str()) { + diagnostics.push(diagnostic( + Some(entry.line), + format!( + "`{}` is not referenced by any acceptance criterion's `Scenarios:`", + entry.id + ), + )); + } + } + + diagnostics +} diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 63b6ad04a..91262b11b 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -248,37 +248,65 @@ expect_plan_structure_failure() { structured_plan_result="structured plan: reference integrity holds" legacy_plan_result="legacy plan: structured reference rules do not apply" -expect_plan_structure_pass "valid-structured-plan.md" "$structured_plan_result" -expect_plan_structure_pass "inspection-only-plan.md" "$structured_plan_result" -expect_plan_structure_pass "fenced-example-plan.md" "$structured_plan_result" -expect_plan_structure_pass "legacy-plan.md" "$legacy_plan_result" -expect_plan_structure_pass "legacy-plan-with-fenced-structured-example.md" "$legacy_plan_result" - -expect_plan_structure_failure "duplicate-id.md" \ - "\`S1\` is defined more than once" -expect_plan_structure_failure "undefined-references.md" \ - "\`Decisions:\` on \`AC1\` references undefined \`D2\`" \ - "\`Scenarios:\` on \`AC1\` references undefined \`S9\`" \ - "\`Covers:\` on \`T01\` references undefined \`AC3\`" -expect_plan_structure_failure "unreferenced-scenario.md" \ - "\`S2\` is not referenced by any acceptance criterion's \`Scenarios:\`" -expect_plan_structure_failure "uncovered-ac.md" \ - "\`AC2\` is not covered by any task's \`Covers:\`" -expect_plan_structure_failure "missing-scenarios-field.md" \ - "\`AC1\` is missing \`Scenarios:\`" -expect_plan_structure_failure "misplaced-field.md" \ - "\`Check:\` on \`D1\` is misplaced; it belongs to a verification scenario" \ - "\`Covers:\` on \`AC1\` is misplaced; it belongs to a task" -expect_plan_structure_failure "misplaced-definition.md" \ - "\`Scenarios:\` on \`AC1\` references undefined \`S1\`; it appears only outside \`### Verification scenarios\`" -expect_plan_structure_failure "wrong-heading-context.md" \ - "heading \`### Verification scenarios\` is not inside \`## Acceptance criteria\`" -expect_plan_structure_failure "keywords-only-in-examples.md" \ - "\`Scenarios:\` on \`AC1\` references undefined \`S1\`; it appears only inside a fenced code block" -expect_plan_structure_failure "task-missing-covers.md" \ - "\`T02\` is missing \`Covers:\`" -expect_plan_structure_failure "task-empty-covers.md" \ - "\`Covers:\` on \`T02\` is not a comma-separated ID list" +plan_structure_manifest="$plan_structure_fixtures/expectations.tsv" +listed_plan_structure_fixtures=() + +while IFS=$'\t' read -r -u 3 -a manifest_columns; do + if [[ "${#manifest_columns[@]}" -eq 0 || "${manifest_columns[0]}" == \#* ]]; then + continue + fi + + fixture_name="${manifest_columns[0]}" + fixture_verdict="${manifest_columns[1]:-}" + expected_diagnostics=("${manifest_columns[@]:2}") + + if [[ ! -f "$plan_structure_fixtures/$fixture_name" ]]; then + printf 'Plan structure manifest lists a missing fixture: %s\n' "$fixture_name" >&2 + exit 1 + fi + listed_plan_structure_fixtures+=("$fixture_name") + + case "$fixture_verdict" in + structured | legacy) + if [[ "${#expected_diagnostics[@]}" -ne 0 ]]; then + printf 'Plan structure manifest pass row must not list diagnostics: %s\n' "$fixture_name" >&2 + exit 1 + fi + if [[ "$fixture_verdict" == "structured" ]]; then + expect_plan_structure_pass "$fixture_name" "$structured_plan_result" + else + expect_plan_structure_pass "$fixture_name" "$legacy_plan_result" + fi + ;; + fail) + if [[ "${#expected_diagnostics[@]}" -eq 0 ]]; then + printf 'Plan structure manifest fail row must list diagnostics: %s\n' "$fixture_name" >&2 + exit 1 + fi + expect_plan_structure_failure "$fixture_name" "${expected_diagnostics[@]}" + ;; + *) + printf 'Plan structure manifest has an unknown verdict for %s: %s\n' \ + "$fixture_name" "$fixture_verdict" >&2 + exit 1 + ;; + esac +done 3<"$plan_structure_manifest" + +for fixture_path in "$plan_structure_fixtures"/*.md; do + fixture_name="$(basename "$fixture_path")" + listed_count=0 + for listed_fixture in "${listed_plan_structure_fixtures[@]}"; do + if [[ "$listed_fixture" == "$fixture_name" ]]; then + listed_count=$((listed_count + 1)) + fi + done + if [[ "$listed_count" -ne 1 ]]; then + printf 'Plan structure fixture must be listed exactly once in %s: %s\n' \ + "$plan_structure_manifest" "$fixture_name" >&2 + exit 1 + fi +done "$producer" "$repo_root" "$generated_input_root" generated_root="$generated_input_root/pkl-generated" diff --git a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv new file mode 100644 index 000000000..e8385ba87 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv @@ -0,0 +1,18 @@ +# fixture verdict (structured | legacy | fail) expected diagnostic substrings (fail only, one per column) +duplicate-id.md fail `S1` is defined more than once +fenced-example-plan.md structured +inspection-only-plan.md structured +keywords-only-in-examples.md fail `Scenarios:` on `AC1` references undefined `S1`; it appears only inside a fenced code block +legacy-plan-with-fenced-structured-example.md legacy +legacy-plan.md legacy +misplaced-definition.md fail `Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios` +misplaced-field.md fail `Check:` on `D1` is misplaced; it belongs to a verification scenario `Covers:` on `AC1` is misplaced; it belongs to a task +missing-scenarios-field.md fail `AC1` is missing `Scenarios:` +task-empty-covers.md fail `Covers:` on `T02` is not a comma-separated ID list +task-missing-covers.md fail `T02` is missing `Covers:` +uncovered-ac.md fail `AC2` is not covered by any task's `Covers:` +undefined-references.md fail `Decisions:` on `AC1` references undefined `D2` `Scenarios:` on `AC1` references undefined `S9` `Covers:` on `T01` references undefined `AC3` +unreferenced-scenario.md fail `S2` is not referenced by any acceptance criterion's `Scenarios:` +valid-structured-plan.md structured +wrong-heading-context.md fail heading `### Verification scenarios` is not inside `## Acceptance criteria` +wrong-reference-kind.md fail `Covers:` on `T01` names `S1`, which is not an acceptance criterion `AC1` is not covered by any task's `Covers:` diff --git a/config/pkl/renderers/fixtures/plan-structure/wrong-reference-kind.md b/config/pkl/renderers/fixtures/plan-structure/wrong-reference-kind.md new file mode 100644 index 000000000..53ed3b84a --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/wrong-reference-kind.md @@ -0,0 +1,47 @@ +# Plan: wrong-reference-kind + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: S1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/context/architecture.md b/context/architecture.md index c53a3d4c6..fb8cb316e 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -82,7 +82,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model (see `context/cli/plan-service.md`). It is not yet wired to a CLI command. +- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). It is not yet wired to a CLI command. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, plan, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index e6bb03303..0105af6e5 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -5,7 +5,8 @@ ## Current scope - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. -- Validation rules, the shared fixture manifest, and the `sce plan check` CLI command are not implemented yet (plan `context/plans/sce-plan-check.md`, T02–T03). The module is not reachable from the CLI. +- `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. +- The `sce plan check` CLI command is not implemented yet (plan `context/plans/sce-plan-check.md`, T03). The module is not reachable from the CLI. ## Plan model @@ -22,6 +23,38 @@ - A definition-shaped item of the wrong kind, in a non-authoritative section, or nested inside another list item is not an entry. - `Code` nodes, both fenced and indented, never contribute headings, entries, or fields. +## Validation rules + +`validate` mirrors the `check` path of `config/pkl/renderers/plan-structure.pkl`. Template-skeleton mode stays Pkl-only. Defect messages are copied verbatim from the Pkl checker, so changing their wording means changing both checkers. + +- A plan is structured when it has a `### Verification scenarios` heading or any task carries `Covers:`. A legacy plan gets `structured: false` and no diagnostics. +- Heading rules, with headings compared as `#`×depth plus text: + - `## Acceptance criteria` and `## Task stack` are required. + - `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack` may each appear only once, in that order. + - The nearest preceding depth-1 or depth-2 heading of `### Verification scenarios` must be `## Acceptance criteria`. +- Definition rules: + - IDs are unique. + - Owned fields appear only on their owning kind: `Boundary`/`Rationale`/`Rejected`/`Trade-offs` → D, `Validate`/`Decisions`/`Scenarios` → AC, `Given`/`When`/`Then`/`Check` → S, `Covers` → T. + - Every definition carries its kind's required fields. +- Reference rules: + - `Decisions:`, `Scenarios:`, and `Covers:` on their owning kind must be an exact `ID, ID` list. `Scenarios: none (inspection)` is exempt. + - Each listed ID must name the right kind and resolve to a definition. + - An unresolved ID gets an `outside_section_ids` or `fenced_ids` hint. + - Every AC must be covered by some task's `Covers:`, and every S must be referenced by some AC's `Scenarios:`. +- `PlanReport.diagnostics` holds `PlanDiagnostic { line, message }` values, sorted by `(line, message)`. Line-less diagnostics come first, and duplicates are kept. +- Where a diagnostic's line comes from: + - Missing required headings have no line. + - Duplicate headings and duplicate IDs point at the first repeated occurrence. + - A misordered heading points at the later heading. + - The parent-rule defect points at `### Verification scenarios`. + - Misplaced and reference defects point at their field. + - Missing-field, uncovered-AC, and unreferenced-S defects point at their definition. + +## Terms + +- `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `structured`, `legacy`, or `fail`, then the expected defect substrings for `fail` rows. Every `*.md` fixture in the directory must be listed exactly once. +- `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. + ## Divergences from the Pkl line scanner The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at exactly `entry indent + 2` and accepts definitions at any indentation. Indentation-only nesting differences, `[X]` checkboxes, and multi-line field values can therefore extract differently. Agreement is pinned by the shared fixtures under `config/pkl/renderers/fixtures/plan-structure/`. @@ -29,3 +62,5 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at ## Tests `cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, legacy plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. + +Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. diff --git a/context/context-map.md b/context/context-map.md index f3e0a8a46..bcdb87688 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -18,7 +18,7 @@ Feature/domain context: - `context/cli/agent-trace-storage.md` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) -- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner) +- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, and the shared `expectations.tsv` fixture manifest) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) diff --git a/context/glossary.md b/context/glossary.md index 1bd61e54c..55001b653 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -1,5 +1,5 @@ # Glossary -- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). +- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`plan-structure expectations manifest`, `plan diagnostic`): [plan service terms](cli/plan-service.md#terms). - `pkl-check-generated`: Flake app exposed as `nix run .#pkl-check-generated`; canonical ephemeral-generation check that rejects committed target/schema/mirror outputs, evaluates exact workflow metadata, the generated artifact contract, semantic layout/path/inventory/content/parity/observational checks, and the optional-workflow manifest's content against the catalog, requires the shared helper-composition rule and SCE-scoped workflow prohibitions, enforces ordered catalog-derived OpenCode skill permissions plus explicit-permission artifact integrity, rejects stale sibling-package references or unresolved internalization tokens in workflow entrypoint `SKILL.md` documents, proves contract failures through checked-in negative fixtures, and delegates deterministic generation plus payload/input inventories to the generated-input producer while preserving its established inventory report. - `repo-level verification preference`: Current repository guidance that contributor-facing validation/check flows should prefer `nix flake check`; direct Cargo verification commands are secondary and used only when explicitly requested or for narrow targeted debugging, while `cargo fmt` remains the explicit autofix path. Required quick checks after each completed task in this repo are `nix run .#pkl-check-generated` and `nix flake check`. - disposable plan lifecycle: Policy where `context/plans/` holds active execution artifacts only; completed plans are disposable and durable outcomes must be reflected in current-state context files and/or `context/decisions/`. diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md index 3835a6602..d940da0ab 100644 --- a/context/plans/sce-plan-check.md +++ b/context/plans/sce-plan-check.md @@ -183,14 +183,19 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized — new internal `cli/src/services/plan/` domain (extraction model only, not yet reachable from the CLI) and a new `markdown` runtime dependency. No user-visible behavior change. - Context synchronization: synced -- [ ] T02: `Validate plan structure in pure Rust against the shared fixture manifest` (status:todo) +- [x] T02: `Validate plan structure in pure Rust against the shared fixture manifest` (status:done) - Task ID: T02 - Covers: AC2, AC3 - Scope: In — `cli/src/services/plan/validate.rs` with a pure `validate(&Plan) -> PlanReport` that ports the `plan-structure.pkl` `check` rules and defect wording: structured detection, required, duplicate, and ordered headings; `### Verification scenarios` parent; duplicate IDs; field ownership; required fields; ID-list grammar; reference kind and resolution with the outside-section/fenced hints; AC coverage; scenario referencing. Diagnostics are sorted deterministically. Add `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` and the new wrong-reference-kind fixture. Switch `config/pkl/check-generated.sh` from hard-coded calls to iterating the manifest, and fail when a fixture is unlisted. Add `plan::fixture_expectations_match_manifest` and `plan::diagnostic_lines_match_source_positions`. Out — CLI wiring, workflow prose, template-skeleton mode. - Dependencies: T01 - Done when: Both the Rust fixture test and `pkl-check-generated` pass from the same manifest, and every fixture's verdict and defect text agree. The diagnostic-line test asserts exact lines. - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix run .#pkl-check-generated` - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `cli/src/services/plan/validate.rs`, `cli/src/services/plan/mod.rs`, `cli/src/services/plan/extract.rs`, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, `config/pkl/renderers/fixtures/plan-structure/wrong-reference-kind.md`, `config/pkl/check-generated.sh` + - Result: `services::plan::validate::validate(&Plan) -> PlanReport { structured, diagnostics: Vec, message }> }` ports the `plan-structure.pkl` `check` rules with verbatim defect wording: structured detection; required, duplicate, and ordered headings; the `### Verification scenarios` parent rule; duplicate IDs; field ownership; required fields; ID-list grammar; reference kind and resolution with the outside-section and fenced hints from `Plan.outside_section_ids` and `Plan.fenced_ids`; AC coverage; and scenario referencing. Diagnostics are sorted by `(line, message)`, with line-less diagnostics first. `PlanEntryKind::of_id` in `extract.rs` became `pub` so it can be reused. `expectations.tsv` (tab-separated: fixture, `structured`/`legacy`/`fail`, then expected substrings; `#` comment rows) lists all 17 fixtures, including the new `wrong-reference-kind.md` (`Covers: S1`). `check-generated.sh` iterates the manifest on fd 3. It rejects missing fixtures, unknown verdicts, pass rows that list substrings, fail rows without substrings, and any `*.md` fixture not listed exactly once. Approved assumptions: plan-level missing-heading defects carry no line. Duplicate-heading and duplicate-ID defects anchor at the first repeated occurrence. Misordered-heading defects anchor at the later heading. The parent-rule defect anchors at the `### Verification scenarios` heading. Deviation: `plan::fixture_expectations_match_manifest` and `plan::diagnostic_lines_match_source_positions` are module-level `#[test]` functions in `plan/mod.rs`, not in a `tests` submodule, so the plan's `plan::fixture` and `plan::diagnostic_lines` Cargo filters match them. + - Verify outcome: `plan::` passed 8 tests (6 `extract`, plus `fixture_expectations_match_manifest` and `diagnostic_lines_match_source_positions`). `nix run .#pkl-check-generated` passed (145 files); the new files were marked intent-to-add with `git add -N` so the flake source includes them. A direct `nix develop -c ./config/pkl/check-generated.sh` also passed. A temporary unlisted fixture made it fail with `Plan structure fixture must be listed exactly once …`, and the fixture was removed afterwards. `clippy --all-targets -D warnings` is clean, and `cargo fmt` was applied. + - Context impact: localized — the internal `cli/src/services/plan/` domain gains a validation layer that is not yet reachable from the CLI. The plan-structure fixture check in `check-generated.sh` is now driven by `expectations.tsv`, which changes the documented `config-generation.md` fixture-run description. No user-visible CLI behavior change. + - Context synchronization: synced - [ ] T03: `Wire sce plan check into the CLI schema and static dispatcher` (status:todo) - Task ID: T03 diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 7935fb3f0..81b954ebf 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,11 +53,11 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/`, requiring the positive fixtures (minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example) to pass with their structured/legacy classification and each negative fixture (duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example) to fail with its expected defect; three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `structured`/`legacy`/`fail` verdict, then expected defect substrings; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `structured` and `legacy` rows must pass with that classification and no listed substrings, `fail` rows must fail with every listed substring, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example; the negative fixtures are duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. -- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures through `plan-structure-check.pkl` with their expected classifications and defects (it does not scan `context/plans/`, and `pklGeneratedCheckSrc` excludes that directory, so mutable plan contents never affect generation checks or `nix flake check`), then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. +- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures listed in `expectations.tsv` through `plan-structure-check.pkl` with their expected classifications and defects (it does not scan `context/plans/`, and `pklGeneratedCheckSrc` excludes that directory, so mutable plan contents never affect generation checks or `nix flake check`), then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. Generated authored classes: From 529e37cb0df8f63ab572a87a974b5d5af8cb3a05 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 01:21:44 +0200 Subject: [PATCH 12/61] cli: Add read-only plan check command Expose deterministic SCE plan validation through `sce plan check ` so plan structure defects can be checked from the CLI without executing plan commands. Register the command in the shared clap schema, parser/runtime dispatch, help, and completion surfaces; read plans with contextual runtime errors and render sorted validation diagnostics with stable exit codes. Update the plan service context and complete T03 of `context/plans/sce-plan-check.md`, including the command and focused tests. The change is read-only and preserves the existing plan extraction and validation contracts. Co-authored-by: SCE --- cli/src/cli_schema.rs | 24 ++++ cli/src/command_surface.rs | 7 ++ cli/src/services/command_registry.rs | 4 + cli/src/services/mod.rs | 4 - cli/src/services/parse/command_runtime.rs | 11 ++ cli/src/services/plan/command.rs | 58 +++++++++ cli/src/services/plan/mod.rs | 146 ++++++++++++++++++++++ context/architecture.md | 6 +- context/cli/cli-command-surface.md | 10 +- context/cli/plan-service.md | 16 ++- context/context-map.md | 2 +- context/glossary.md | 6 +- context/overview.md | 4 +- context/plans/sce-plan-check.md | 9 +- 14 files changed, 286 insertions(+), 21 deletions(-) create mode 100644 cli/src/services/plan/command.rs diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index c5348c670..9a32718a0 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -53,6 +53,10 @@ pub const SYNC_TOP_LEVEL_PURPOSE: &str = "Synchronize the current repository's Agent Trace database"; pub const SYNC_SHOW_IN_TOP_LEVEL_HELP: bool = true; +pub const PLAN_CLAP_ABOUT: &str = "Check the structure and reference integrity of one SCE plan"; +pub const PLAN_TOP_LEVEL_PURPOSE: &str = "Check the structure of one SCE plan"; +pub const PLAN_SHOW_IN_TOP_LEVEL_HELP: bool = true; + pub const TOP_LEVEL_COMMANDS: &[TopLevelCommandMetadata] = &[ TopLevelCommandMetadata { name: crate::services::auth_command::NAME, @@ -94,6 +98,11 @@ pub const TOP_LEVEL_COMMANDS: &[TopLevelCommandMetadata] = &[ purpose: COMPLETION_TOP_LEVEL_PURPOSE, show_in_top_level_help: COMPLETION_SHOW_IN_TOP_LEVEL_HELP, }, + TopLevelCommandMetadata { + name: crate::services::plan::NAME, + purpose: PLAN_TOP_LEVEL_PURPOSE, + show_in_top_level_help: PLAN_SHOW_IN_TOP_LEVEL_HELP, + }, TopLevelCommandMetadata { name: crate::services::sync::NAME, purpose: SYNC_TOP_LEVEL_PURPOSE, @@ -234,6 +243,12 @@ pub enum Commands { shell: CompletionShell, }, + #[command(about = PLAN_CLAP_ABOUT, hide = !PLAN_SHOW_IN_TOP_LEVEL_HELP)] + Plan { + #[command(subcommand)] + subcommand: PlanSubcommand, + }, + #[command(about = SYNC_CLAP_ABOUT, hide = !SYNC_SHOW_IN_TOP_LEVEL_HELP)] Sync { #[arg(long, value_enum, default_value_t = OutputFormat::Text)] @@ -361,6 +376,15 @@ pub enum HooksSubcommand { ExternalMutationGuard, } +#[derive(Subcommand, Debug, Clone, PartialEq, Eq)] +pub enum PlanSubcommand { + #[command(about = "Validate one plan file without executing any of its commands")] + Check { + /// Path to the plan Markdown file to check + path: PathBuf, + }, +} + #[derive(Subcommand, Debug, Clone, PartialEq, Eq)] pub enum PolicySubcommand { #[command(name = "bash", about = "Evaluate a bash policy request from STDIN")] diff --git a/cli/src/command_surface.rs b/cli/src/command_surface.rs index f47cc52eb..6affeb728 100644 --- a/cli/src/command_surface.rs +++ b/cli/src/command_surface.rs @@ -72,6 +72,13 @@ const HELP_SECTIONS: &[HelpSection] = &[ suffix: " [--format ]", }], }, + HelpSection { + title: "Plan Usage:", + body: &[HelpSectionBodyLine::Command { + cmd: " sce plan", + suffix: " check ", + }], + }, HelpSection { title: "Completion Usage:", body: &[HelpSectionBodyLine::Command { diff --git a/cli/src/services/command_registry.rs b/cli/src/services/command_registry.rs index bc8ff9023..3da1e2f8a 100644 --- a/cli/src/services/command_registry.rs +++ b/cli/src/services/command_registry.rs @@ -13,6 +13,7 @@ const DEFAULT_COMMAND_NAMES: &[&str] = &[ services::help::NAME, services::hooks::NAME, services::bash_policy::NAME, + services::plan::NAME, services::setup::NAME, services::sync::NAME, services::version::NAME, @@ -35,6 +36,7 @@ pub enum RuntimeCommand { Version(services::version::command::VersionCommand), Completion(services::completion::command::CompletionCommand), Sync(services::sync::command::SyncCommand), + Plan(services::plan::command::PlanCommand), } impl RuntimeCommand { @@ -51,6 +53,7 @@ impl RuntimeCommand { Self::Version(_) => Cow::Borrowed(services::version::NAME), Self::Completion(_) => Cow::Borrowed(services::completion::NAME), Self::Sync(_) => Cow::Borrowed(services::sync::NAME), + Self::Plan(_) => Cow::Borrowed(services::plan::NAME), } } @@ -75,6 +78,7 @@ impl RuntimeCommand { Self::Version(command) => command.execute(context), Self::Completion(command) => Ok(command.execute(context)), Self::Sync(command) => command.execute_with_stderr(context, stderr).await, + Self::Plan(command) => command.execute(), } } } diff --git a/cli/src/services/mod.rs b/cli/src/services/mod.rs index 28d168dd7..98bf2c290 100644 --- a/cli/src/services/mod.rs +++ b/cli/src/services/mod.rs @@ -29,10 +29,6 @@ pub mod observability; pub mod output_format; pub mod parse; pub mod patch; -#[cfg_attr( - not(test), - allow(dead_code, reason = "wired into the CLI by the plan check command") -)] pub mod plan; pub mod repository_identity; pub mod resilience; diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 38d9999fb..92e4ad2e9 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -323,6 +323,7 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result Ok(convert_plan_subcommand(subcommand)), cli_schema::Commands::Sync { format } => { Ok(RuntimeCommand::Sync(services::sync::command::SyncCommand { request: services::sync::SyncRequest { format }, @@ -331,6 +332,16 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result RuntimeCommand { + let request = match subcommand { + cli_schema::PlanSubcommand::Check { path } => { + services::plan::command::PlanCheckRequest { path } + } + }; + + RuntimeCommand::Plan(services::plan::command::PlanCommand { request }) +} + fn convert_doctor_command( fix: bool, format: services::output_format::OutputFormat, diff --git a/cli/src/services/plan/command.rs b/cli/src/services/plan/command.rs new file mode 100644 index 000000000..ae50f9d7c --- /dev/null +++ b/cli/src/services/plan/command.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use anyhow::Context; + +use crate::services::error::CliError; +use crate::services::plan::extract::extract; +use crate::services::plan::validate::{validate, PlanDiagnostic}; + +pub const STRUCTURED_PLAN_PASS: &str = "structured plan: reference integrity holds"; +pub const LEGACY_PLAN_PASS: &str = "legacy plan: structured reference rules do not apply"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanCheckRequest { + pub path: PathBuf, +} + +pub struct PlanCommand { + pub request: PlanCheckRequest, +} + +impl PlanCommand { + pub fn execute(&self) -> Result { + let path = &self.request.path; + let source = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read plan file '{}'", path.display())) + .map_err(CliError::runtime)?; + let plan = extract(&source) + .with_context(|| format!("Failed to parse plan file '{}'", path.display())) + .map_err(CliError::runtime)?; + let report = validate(&plan); + + if !report.diagnostics.is_empty() { + return Err(CliError::validation(render_diagnostics( + &path.display().to_string(), + &report.diagnostics, + ))); + } + + Ok(if report.structured { + STRUCTURED_PLAN_PASS + } else { + LEGACY_PLAN_PASS + } + .to_string()) + } +} + +fn render_diagnostics(path: &str, diagnostics: &[PlanDiagnostic]) -> String { + let mut lines = vec![format!("Plan structure check failed for '{path}':")]; + lines.extend(diagnostics.iter().map(|diagnostic| match diagnostic.line { + Some(line) => format!("{path}:{line}: {}", diagnostic.message), + None => format!("{path}: {}", diagnostic.message), + })); + lines.push(format!( + "Try: fix the reported plan defects, then rerun 'sce plan check {path}'." + )); + lines.join("\n") +} diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 3f79ca12d..1dc9e6a0e 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -1,6 +1,9 @@ +pub mod command; pub mod extract; pub mod validate; +pub const NAME: &str = "plan"; + #[cfg(test)] fn fixture_dir() -> std::path::PathBuf { std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -135,3 +138,146 @@ fn diagnostic_lines_match_source_positions() { vec![(None, "heading `## Task stack` is missing")] ); } + +#[cfg(test)] +struct CommandRun { + exit_code: std::process::ExitCode, + stdout: String, + stderr: String, +} + +#[cfg(test)] +fn run_plan_check(path: &str) -> CommandRun { + let command = crate::services::parse::command_runtime::parse_runtime_command( + ["sce", NAME, "check", path].map(String::from), + &crate::services::command_registry::CommandRegistry::default(), + None, + ) + .expect("plan check should parse"); + let crate::services::command_registry::RuntimeCommand::Plan(command) = command else { + panic!("expected plan command"); + }; + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let exit_code = crate::services::app_support::render_run_outcome( + crate::services::app_support::RunOutcome { + result: command.execute(), + logger: None::, + startup_diagnostic: None, + }, + &mut stdout, + &mut stderr, + ); + + CommandRun { + exit_code, + stdout: String::from_utf8(stdout).expect("stdout should be UTF-8"), + stderr: String::from_utf8(stderr).expect("stderr should be UTF-8"), + } +} + +#[test] +fn command_valid_plans_exit_success() { + let relative_dir = "../config/pkl/renderers/fixtures/plan-structure"; + for (fixture, expected) in [ + ("valid-structured-plan.md", command::STRUCTURED_PLAN_PASS), + ("inspection-only-plan.md", command::STRUCTURED_PLAN_PASS), + ("legacy-plan.md", command::LEGACY_PLAN_PASS), + ] { + let absolute = fixture_dir().join(fixture); + for path in [ + format!("{relative_dir}/{fixture}"), + absolute.display().to_string(), + ] { + let run = run_plan_check(&path); + assert_eq!(run.exit_code, std::process::ExitCode::SUCCESS, "{path}"); + assert_eq!(run.stdout, format!("{expected}\n"), "{path}"); + assert!(run.stderr.is_empty(), "{path}: {}", run.stderr); + } + } +} + +#[test] +fn command_invalid_plan_exits_validation() { + let path = "../config/pkl/renderers/fixtures/plan-structure/duplicate-id.md"; + let run = run_plan_check(path); + + assert_eq!(run.exit_code, std::process::ExitCode::from(3)); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("SCE-ERR-VALIDATION"), "{}", run.stderr); + assert_eq!( + run.stderr + .lines() + .filter(|line| line.starts_with(&format!("{path}:"))) + .collect::>(), + vec![format!("{path}:29: `S1` is defined more than once")] + ); +} + +#[test] +fn command_missing_file_is_runtime_failure() { + let directory = tempfile::tempdir().expect("tempdir should be created"); + let path = directory + .path() + .join("missing-plan.md") + .display() + .to_string(); + let run = run_plan_check(&path); + + assert_eq!(run.exit_code, std::process::ExitCode::from(4)); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("SCE-ERR-RUNTIME"), "{}", run.stderr); + assert!(run.stderr.contains(&path), "{}", run.stderr); + assert!(!run.stderr.contains(&format!("{path}:")), "{}", run.stderr); +} + +#[test] +fn command_does_not_execute_plan_commands() { + let directory = tempfile::tempdir().expect("tempdir should be created"); + let marker = directory.path().join("executed"); + let touch = format!("`touch {}`", marker.display()); + let plan_path = directory.path().join("plan.md"); + std::fs::write( + &plan_path, + format!( + "# Plan: no-execution\n\n## Acceptance criteria\n\n- [ ] AC1: Nothing runs.\n - Validate: {touch}\n - Scenarios: S1\n\n### Verification scenarios\n\n- S1: Commands stay inert\n - Given: A plan.\n - When: It is checked.\n - Then: Nothing runs.\n - Check: {touch}\n\n## Task stack\n\n- [ ] T01: `Do nothing` (status:todo)\n - Task ID: T01\n - Covers: AC1\n - Verify: {touch}\n" + ), + ) + .expect("plan should be written"); + + let run = run_plan_check(&plan_path.display().to_string()); + + assert_eq!( + run.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + run.stderr + ); + assert_eq!(run.stdout, format!("{}\n", command::STRUCTURED_PLAN_PASS)); + assert!(!marker.exists()); +} + +#[test] +fn plan_check_appears_in_help() { + assert!(crate::services::help::help_text().contains("sce plan")); + let help = crate::cli_schema::render_help_for_path(&[NAME, "check"]) + .expect("plan check help should be renderable"); + assert!(help.contains(""), "{help}"); +} + +#[test] +fn completion_scripts_include_plan_check() { + use crate::services::completion::{render_completion, CompletionRequest, CompletionShell}; + + for shell in [ + CompletionShell::Bash, + CompletionShell::Zsh, + CompletionShell::Fish, + ] { + let script = render_completion(CompletionRequest { shell }); + assert!( + script.contains("plan") && script.contains("check"), + "{shell:?}" + ); + } +} diff --git a/context/architecture.md b/context/architecture.md index fb8cb316e..61bb5ba51 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -50,12 +50,12 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/app.rs` provides the clap-based argument dispatch loop with deterministic help/setup execution, bare-command help routing for `sce auth` and `sce config`, successful unknown-command fallback to top-level or closest-parent help, centralized stream routing (`stdout` success payloads, `stderr` redacted diagnostics), stable class-based exit-code mapping (`2` parse, `3` validation, `4` runtime, `5` dependency), and stable class-based stderr diagnostic codes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) with default `Try:` remediation injection when missing. - The app runtime now moves through explicit startup phases in `cli/src/app.rs`: dependency bootstrapping (`perform_dependency_check`), startup context construction (`build_startup_context`), runtime initialization (`initialize_runtime`), directly awaited command parse/execute inside the telemetry polling scope (`run_command_lifecycle`, `parse_command_phase` plus `services::app_support::execute_command_phase`), and final output rendering through `services::app_support::render_run_outcome`. `AppRuntime` owns the concrete production logger, no-op telemetry runtime, filesystem ops, git ops, static `CommandRegistry`, and startup-diagnostic state across those phases; `RunOutcome` carries final render data with an optional generic logger implementing `services::observability::traits::Logger`, so render support can log classified errors without production-logger type coupling. If a telemetry implementation attempts to invoke the command action more than once, dispatch returns a runtime-classified error instead of panicking or reusing consumed arguments. - `AppContext` is the CLI's borrowed dependency view in `cli/src/app.rs`: it is generic over logger, telemetry, filesystem, and git capability implementations and stores references plus an optional `repo_root: Option` instead of owning `Arc` trait objects. Because it borrows from `AppRuntime`, `AppContext` is command-scoped: borrowing across directly awaited execution is safe while the owner lives, but detached work cannot retain that borrowed view. No new `Send`, `Sync`, or `'static` command bounds are required. Startup creates a context view over `AppRuntime`'s concrete production dependencies with `repo_root` set to `None`; command paths can derive repo-root-scoped context views through the `ContextWithRepoRoot` accessor trait / `AppContext::with_repo_root(...)`, which reuses the same borrowed dependencies while attaching the resolved root. Narrow accessor traits expose associated concrete capability types for logger, telemetry, fs, and git (`&Self::...`) plus repo-root access, so call sites can express capability requirements without erasing the borrowed dependencies back to trait objects; lifecycle providers consume the repo-root accessor rather than the full context type. -- Command parse-time conversion and run-time handling are separated by an internal static `RuntimeCommand` seam. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` enum with variants for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync, plus a deterministic `CommandRegistry` name catalog populated by `build_default_registry()`. `parse_command_phase` in `cli/src/app.rs` delegates clap-output conversion to `cli/src/services/parse/command_runtime.rs`, which owns clap error classification, help rendering bridges, longest-valid-parent traversal for unknown command paths, and parsed-request-to-enum conversion while returning concrete enum values. Service-owned `command.rs` modules define command payload structs and generic execution methods with narrow context requirements: context-free commands accept any context, hooks requires logger access, setup/doctor require repo-root scoping, and central dispatch requires the union of logger plus repo-root-scoping capabilities. `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute_with_stderr(...)`; the enum performs the only central dispatch match and delegates business behavior to the service-owned command structs, with sync receiving the app-owned stderr writer for format-gated progress. +- Command parse-time conversion and run-time handling are separated by an internal static `RuntimeCommand` seam. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` enum with variants for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, sync, and plan, plus a deterministic `CommandRegistry` name catalog populated by `build_default_registry()`. `parse_command_phase` in `cli/src/app.rs` delegates clap-output conversion to `cli/src/services/parse/command_runtime.rs`, which owns clap error classification, help rendering bridges, longest-valid-parent traversal for unknown command paths, and parsed-request-to-enum conversion while returning concrete enum values. Service-owned `command.rs` modules define command payload structs and generic execution methods with narrow context requirements: context-free commands accept any context, hooks requires logger access, setup/doctor require repo-root scoping, and central dispatch requires the union of logger plus repo-root-scoping capabilities. `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute_with_stderr(...)`; the enum performs the only central dispatch match and delegates business behavior to the service-owned command structs, with sync receiving the app-owned stderr writer for format-gated progress. - Startup observability bootstrapping in `cli/src/app.rs` still tolerates invalid default-discovered config files by continuing with degraded defaults plus `sce.config.invalid_config` warn-level logs, but the warning/logging work is now isolated behind the startup-context and runtime-initialization phases rather than one inline startup function. - `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format, explicit config-file/default `log_to_file`, and `log_dir` inputs with precedence `env > config file > defaults` for non-flag observability keys, stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, error-specific stderr suppression when file logging is enabled while non-error records and file-write diagnostics remain on stderr, redaction-safe emission through the shared security helper, and log-directory writes with bounded retention. Config resolution also carries a positive config-file/default-only `log_file_retention_limit` (`10` by default) into startup observability config and `sce config show`; the concrete logger stores that resolved value and threads it through primary and v2 cleanup. When `log_dir` resolves from `SCE_LOG_DIR`, config, or the `/sce/logs` default, each enabled or forced log operation selects `/sce-.log` or `/sce--.log` using the machine-local date and optional logger session context, with deterministic percent-encoding for unsafe session filename bytes; after successfully writing a newly created selected file, retention keeps the configured number of newest direct regular `*.log` files by mtime plus path/name tie-break and fails open on cleanup errors. Its `observability::traits` submodule exposes the current `Logger` API with `Option<&str>` session context plus the generic async, deliberately non-object-safe `Telemetry` capability and `NoopLogger`; the concrete observability `Logger` and `NoopTelemetry` implement those capability traits; `NoopTelemetry` installs no tracing subscriber. `services::app_support::render_run_outcome` consumes the logger through that trait boundary when logging classified errors and stdout-write failures. - `cli/src/services/managed_telemetry.rs` (+ `managed_telemetry/trace_export.rs`, `managed_telemetry/log_export.rs`) owns hardcoded managed OTLP defaults, credential-backed runtime telemetry selection from the encrypted auth DB, and bounded fail-open OTLP/HTTP protobuf export of `sce.command` spans and allowlisted `Logger` events to the pinned canonical collector (metrics endpoint reserved, not exported); see `context/sce/cli-observability-contract.md`. - `cli/src/services/observability.rs` no longer owns duplicate log enums or parsing helpers; it consumes the canonical primitive seam from `cli/src/services/config/mod.rs` and stays focused on concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary. -- `cli/src/cli_schema.rs` is now the canonical owner for top-level command metadata for the real clap-backed command set (`auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, `completion`), including the slim top-level help purpose text and per-command visibility on `sce`, `sce help`, and `sce --help`; `cli/src/command_surface.rs` remains the custom top-level help renderer and known-command classifier, adding the synthetic `help` row plus the ASCII banner while consuming that shared metadata instead of maintaining a parallel command catalog. +- `cli/src/cli_schema.rs` is now the canonical owner for top-level command metadata for the real clap-backed command set (`auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `version`, `completion`, `sync`, `plan`), including the slim top-level help purpose text and per-command visibility on `sce`, `sce help`, and `sce --help`; `cli/src/command_surface.rs` remains the custom top-level help renderer and known-command classifier, adding the synthetic `help` row plus the ASCII banner while consuming that shared metadata instead of maintaining a parallel command catalog. - `cli/src/services/default_paths.rs` is the canonical production path catalog for the CLI: it resolves config/state/cache roots with platform-aware XDG or `dirs` fallbacks through an internal `roots` seam, exposes named default paths for current persisted artifacts and database/log files (global config, auth tokens, auth DB, local DB, default observability log directory, and the sole Agent Trace DB path helper `agent_trace_db_path_for_repository` under `repos//agent-trace.db`; the former global-sentinel and per-checkout Agent Trace path helpers were removed by the `retire-legacy-agent-trace-db` plan), and owns canonical repo-relative, embedded-asset, install, hook, and context-path accessors so non-test production path definitions have one shared owner. Compile-time generated payload paths are owned by `build.rs` under `OUT_DIR`, not by the default-path catalog. Current production consumers such as config discovery, observability config resolution, doctor reporting, setup/install flows, database adapters, checkout identity, Agent Trace storage resolution, and local hook runtime path resolution consume this shared catalog rather than defining owned path literals in their own modules. - `cli/src/services/agent_trace.rs` is the Rust CLI owner for the SCE web base URL (`SCE_WEB_BASE_URL`) and exposes helpers for SCE-owned URL construction: Agent Trace conversation lookup URLs, persisted Agent Trace trace URLs, Agent Trace session URLs, and setup-created config schema URLs. Production Rust code should consume those helpers instead of repeating `sce.crocoder.dev` literals. The config resolver separately owns the `control_plane_base_url` runtime seam, whose baked `sce sync` default is `https://sce.crocoderlab.dev`; this control-plane host is not a web URL or schema owner. - `cli/src/services/config/mod.rs` is the config service facade and `sce config` orchestration surface (`show`, `validate`, `--help`), with bare `sce config` routed by `cli/src/app.rs` to the same help payload as `sce config --help`. Focused submodules own the implementation slices: `types.rs` owns shared config/runtime primitives, `schema.rs` owns generated schema embedding plus typed file parsing, `policy.rs` owns bash-policy semantic validation plus policy-specific formatting and runtime preset-catalog access for the Rust evaluator, `resolver.rs` owns deterministic config-file discovery, file-layer merging, explicit value precedence (`flags > env > config file > defaults` where flag-backed), shared auth-key resolution, observability-runtime resolution, attribution-hooks runtime gate and `agent_trace.auto_sync` resolution, database-retry config resolution and `DATABASE_RETRY_CONFIG` `OnceLock` initialization, default-discovered invalid-file degradation, strict invalid-discovered-layer errors for Agent Trace storage, and explicit-path fatal errors for `--config` / `SCE_CONFIG_FILE`, and private `render.rs` owns `sce config show` / `sce config validate` text and JSON output construction plus rendering-specific display-value helpers. The facade preserves existing `services::config` imports for startup/auth/hooks callers while delegating command execution to resolution plus rendering submodules. @@ -82,7 +82,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). It is not yet wired to a CLI command. +- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). `command.rs` exposes it as the synchronous, database-free `sce plan check ` command. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, plan, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 403828c63..75884e5d9 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -11,7 +11,7 @@ Operator onboarding currently comes from `sce --help`, command-local `--help` ou - Top-level command metadata catalog: `cli/src/cli_schema.rs` - Custom top-level help renderer and known-command classifier: `cli/src/command_surface.rs` - Turso adapters: `cli/src/services/auth_db/mod.rs`, `cli/src/services/local_db/mod.rs`, `cli/src/services/agent_trace_db/mod.rs`, and shared infrastructure in `cli/src/services/db/mod.rs` -- Service domains: `cli/src/services/{agent_trace,agent_trace_db,auth,auth_command,auth_db,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,security,setup,style,sync,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files own command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy/sync, and `cli/src/services/command_registry.rs` owns the static `RuntimeCommand` enum that dispatches them +- Service domains: `cli/src/services/{agent_trace,agent_trace_db,auth,auth_command,auth_db,completion,config,db,default_paths,hooks,local_db,observability,output_format,resilience,plan,security,setup,style,sync,token_storage,version}` plus the split doctor module at `cli/src/services/doctor/{mod,command,inspect,render,fixes,types}.rs`; service-owned `command.rs` files own command payload structs for help/version/completion/auth/config/setup/doctor/hooks/policy/sync/plan, and `cli/src/services/command_registry.rs` owns the static `RuntimeCommand` enum that dispatches them - Service lifecycle: `cli/src/services/lifecycle.rs` defines lifecycle-owned health/setup result types, the `ServiceLifecycle` trait with `diagnose`, `fix`, and `setup` methods for concrete providers, and the static `LifecycleProvider` enum used by `doctor`/`setup` to dispatch across `config`, `local_db`, `auth_db`, `agent_trace_db`, and `hooks` without boxed provider aggregation ## Application runtime dispatch @@ -28,7 +28,7 @@ awaited Turso persistence decision](../decisions/2026-10-07-directly-awaited-tur | Path | Execution and lifetime reason | | --- | --- | -| Help, HelpText, Version, Completion, Policy, Config Show/Validate | Direct synchronous execution; no database operations. | +| Help, HelpText, Version, Completion, Policy, Config Show/Validate, Plan | Direct synchronous execution; no database operations. | | Setup and Doctor | Async lifecycle-provider dispatch awaits local, auth, and repository DB operations; pure Config/Hooks work stays synchronous within async lifecycle methods. | | DB-backed Hooks and MutationScope | Static handler dispatch awaits required foreground persistence before returning. | | Sync | Async repository storage, export reads, credential persistence, and HTTP remain awaited on the application runtime; ordinary ownership retains storage across HTTP calls. | @@ -87,7 +87,7 @@ Non-interactive target setup never prompts. A missing config receives explicit ` - the banner uses a per-column right-to-left color gradient (cyan on the right, magenta on the left) when stdout color is enabled, and renders as plain ASCII when color is disabled (non-TTY or `NO_COLOR`) - the banner is the box-drawing "SCE" art from `SCE_BANNER_LINES`, rendered by `command_surface::help_text()` via `sce_banner()`, which applies `style::banner_with_gradient` and adds a `Visit sce.crocoder.dev to learn more.` line where `sce.crocoder.dev` is cyan, underlined, and an OSC 8 hyperlink to `https://sce.crocoder.dev` (`style::banner_link`); all of these render as plain text when color is disabled - the visible real-command rows are sourced from `cli_schema::TOP_LEVEL_COMMANDS`, so top-level purpose text and help visibility are defined once for both help rendering and known-command classification -- the visible command list is `help`, `auth`, `config`, `setup`, `doctor`, `sync`, `version`, and `completion` +- the visible command list is `help`, `auth`, `config`, `setup`, `doctor`, `sync`, `plan`, `version`, and `completion` - top-level help omits implemented/placeholder labels - top-level examples cover setup plus doctor/version machine-readable or repair-intent flows (`doctor --format json`, `doctor --fix`, `version --format json`) and use the shared example-command styling when stdout color is enabled - `auth` and `hooks` stay parser-valid and directly invocable; `auth` is visible in those top-level help surfaces while `hooks` remains hidden @@ -97,6 +97,8 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `config` exposes deterministic inspect/validate entrypoints (`sce config show`, `sce config validate`) with explicit precedence (`flags > env > config file > defaults`), a shared auth-runtime resolver for supported keys that declare env/config/optional baked-default inputs starting with `workos_client_id`, first-class `policies.bash` reporting for preset/custom blocked-command rules, and deterministic text/JSON output modes where `show` reports resolved values with provenance while `validate` reports pass/fail plus validation issues and warnings only. `version` exposes deterministic runtime identification output in text mode by default and JSON mode via `--format json`. `completion` exposes deterministic shell completion generation via `sce completion --shell `. + +`plan` exposes `sce plan check `, a read-only structural check of one SCE plan file; its output and exit-code contract is owned by `context/cli/plan-service.md`. `setup` defaults to an `inquire` interactive target selection (OpenCode, Claude, Pi, Codex, All) and accepts mutually-exclusive non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--codex`, `--all`); the former `--both` flag was removed in favor of `--all` (opencode+claude+pi+codex); the interactive prompt title and target labels reuse shared prompt styling helpers when stdout color is enabled. Every setup mode first validates the Git repository and effective `agent_trace.repository_remote` (default `origin`) before prompts or writes. If that configured remote is missing, the typed `NotGitRemote` diagnostic uses generic `git remote add ` guidance; the preserved technical source identifies the effective name without exposing the URL. `setup` also accepts `--bootstrap-context` as a standalone context-only mode that ensures the durable-context baseline without prompts or integration installs; every normal successful setup path ensures that baseline only after both preflights. `setup` accepts a repeatable `--workflow ` flag selecting which optional workflows to install (currently only `brownfield`). Passing it makes the listed slugs the exact selection for that run; omitting it reuses the persisted `integrations.optional_workflows`, so a repeat run preserves an earlier opt-in. Unknown slugs fail request resolution with a validation error naming the embedded catalog's available slugs and write no files, and `--workflow` is rejected alongside `--bootstrap-context` or on a hooks-only run because neither installs target assets. The resolved selection filters the installed assets and is persisted; see [config precedence contract](config-precedence-contract.md) and [setup local bootstrap](../sce/setup-repo-local-config-bootstrap.md). An interactive `setup` run instead resolves the selection through an `inquire` multi-select shown after the target prompt, titled `Select optional workflows` with one `{title} — {description}` row per optional workflow using the shared prompt styling. Rows are unchecked when nothing is persisted and pre-checked from `integrations.optional_workflows` otherwise (a supplied `--workflow` list seeds them instead); the answered prompt is the run's exact selection. It then asks `Automatically sync Agent Traces?` followed by `(Requires an SCE account. Sends Agent Traces from supported AI coding tools` and `to SCE servers so they can be stored and viewed in your account.)`, then `Record SCE involvement in Git commits?` followed by `(Adds SCE metadata to commits created or assisted by SCE.)`, as independent confirmations, both defaulting to Yes; the two answers are carried independently through setup dispatch. Cancelling either interactive prompt yields the existing `Setup cancelled. No files were changed.` outcome, a non-TTY run keeps the existing actionable guidance, and the optional-workflow prompt is skipped when the catalog has no optional workflow. @@ -141,7 +143,7 @@ An interactive `setup` run instead resolves the selection through an `inquire` m - `cli/src/services/agent_trace.rs` defines the canonical Rust SCE web base URL and helpers for Agent Trace conversation URLs, persisted Agent Trace trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. - `cli/src/services/token_storage.rs` defines WorkOS token persistence (`save_tokens`, `load_tokens`, `delete_tokens`) via the encrypted `AuthDb` `auth_credentials` table using a `OnceLock` lazy singleton with constant integer row ID `1`. `TokenStorageError` exposes `PathResolution` and `Database` variants. No JSON file I/O remains. - `cli/src/services/auth_command/mod.rs` defines the auth command orchestration surface (`AuthRequest`, `AuthSubcommand`, `run_auth_subcommand`) for `login`, `logout`, and `whoami`, including shared text/JSON rendering, resolved Control Plane telemetry-credential provisioning after WorkOS persistence on fresh/valid-stored/renewed login paths, best-effort failure handling, deterministic `telemetry_provisioning` issued/unavailable status and message rendering, login's stored-token validation and refresh path for any stored credential, device-flow fallback after absent or unsuccessfully renewed credentials, source-level typed `CliError` propagation through `run_login_with_stored_credentials` for renewal, token-save, and device-login failures, token-storage-backed logout deletion with idempotent absent-credential success, unauthenticated whoami state reporting, Control Plane `/me` profile retrieval for authenticated whoami, flat safe-field rendering with optional-name/null-value handling, exact logged-out text guidance, precedence-aware client-ID guidance sourced from the shared auth-runtime resolver instead of env-only assumptions, and directly awaited WorkOS/Control Plane HTTP on the application runtime; credential load/save/delete are async APIs directly awaited on the application runtime; `cli/src/services/auth_command/command.rs` owns the `AuthCommand` payload used by the static `RuntimeCommand` enum. -- `cli/src/app.rs` parses `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, and `completion` into service-owned runtime command handlers so runtime messages are sourced from domain modules instead of inline strings. +- `cli/src/app.rs` parses `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `plan`, `version`, and `completion` into service-owned runtime command handlers so runtime messages are sourced from domain modules instead of inline strings. ## Local and Agent Trace Turso adapter behavior diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index 0105af6e5..2bc205cbf 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -6,7 +6,7 @@ - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. - `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. -- The `sce plan check` CLI command is not implemented yet (plan `context/plans/sce-plan-check.md`, T03). The module is not reachable from the CLI. +- `command.rs` owns the `sce plan check ` command (see [Command contract](#command-contract)). ## Plan model @@ -50,9 +50,21 @@ - Misplaced and reference defects point at their field. - Missing-field, uncovered-AC, and unreferenced-S defects point at their definition. +## Command contract + +`sce plan check ` checks exactly the file at ``, relative or absolute. It is synchronous, uses no database or repository root, and never runs `Check:`, `Validate:`, or `Verify:` content. + +- Valid structured plan: stdout `structured plan: reference integrity holds`, exit `0`. +- Valid legacy plan: stdout `legacy plan: structured reference rules do not apply`, exit `0`. +- Structurally invalid plan: `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`). The stderr message is a `Plan structure check failed for '{path}':` header, one `{path}:{line}: {message}` line per diagnostic (`{path}: {message}` when the diagnostic has no line) in `validate` order, and a closing `Try:` line. `{path}` is printed as given. +- Missing or unreadable file: `CliError::runtime` (exit `4`, `SCE-ERR-RUNTIME`) with `Failed to read plan file '{path}'` context, and no diagnostics. + +`plan` is a visible top-level command and appears in `sce --help` (including a `Plan Usage:` line) and in generated Bash/Zsh/Fish completions. + ## Terms - `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `structured`, `legacy`, or `fail`, then the expected defect substrings for `fail` rows. Every `*.md` fixture in the directory must be listed exactly once. +- `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. ## Divergences from the Pkl line scanner @@ -63,4 +75,4 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at `cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, legacy plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. -Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. +Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. diff --git a/context/context-map.md b/context/context-map.md index bcdb87688..e7c09f4dd 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -18,7 +18,7 @@ Feature/domain context: - `context/cli/agent-trace-storage.md` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) -- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, and the shared `expectations.tsv` fixture manifest) +- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, the shared `expectations.tsv` fixture manifest, and the `sce plan check ` command with its exit `0`/`3`/`4` output contract) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) diff --git a/context/glossary.md b/context/glossary.md index 55001b653..57d7031c9 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -1,5 +1,5 @@ # Glossary -- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`plan-structure expectations manifest`, `plan diagnostic`): [plan service terms](cli/plan-service.md#terms). +- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`sce plan check`, `plan-structure expectations manifest`, `plan diagnostic`): [plan service terms](cli/plan-service.md#terms). - `pkl-check-generated`: Flake app exposed as `nix run .#pkl-check-generated`; canonical ephemeral-generation check that rejects committed target/schema/mirror outputs, evaluates exact workflow metadata, the generated artifact contract, semantic layout/path/inventory/content/parity/observational checks, and the optional-workflow manifest's content against the catalog, requires the shared helper-composition rule and SCE-scoped workflow prohibitions, enforces ordered catalog-derived OpenCode skill permissions plus explicit-permission artifact integrity, rejects stale sibling-package references or unresolved internalization tokens in workflow entrypoint `SKILL.md` documents, proves contract failures through checked-in negative fixtures, and delegates deterministic generation plus payload/input inventories to the generated-input producer while preserving its established inventory report. - `repo-level verification preference`: Current repository guidance that contributor-facing validation/check flows should prefer `nix flake check`; direct Cargo verification commands are secondary and used only when explicitly requested or for narrow targeted debugging, while `cargo fmt` remains the explicit autofix path. Required quick checks after each completed task in this repo are `nix run .#pkl-check-generated` and `nix flake check`. - disposable plan lifecycle: Policy where `context/plans/` holds active execution artifacts only; completed plans are disposable and durable outcomes must be reflected in current-state context files and/or `context/decisions/`. @@ -71,7 +71,7 @@ - `log_to_file`: Flat SCE config-file boolean controlling file-log emission and normal logger `stderr` routing. It defaults to `true`, is surfaced with source metadata by `sce config show`, and resolves independently from `log_dir`; enabled file logging routes normal logger records only to the file, while disabled file logging routes only error-level logger records to `stderr`. An omitted `log_dir` uses the default location, while an explicitly empty config value remains invalid. See [CLI observability contract](sce/cli-observability-contract.md). - `sce` (CLI foundation): Rust binary crate at `cli/` with implemented auth command flows (`auth login|logout|whoami`) plus auth-local bare-command guidance (`sce auth`, `sce auth --help`), Control Plane `/me`-backed whoami profile output using flat email/name/role/permissions/organization labels, exact logged-out login guidance, implemented setup installation flow including lifecycle-aggregated local DB and Agent Trace DB bootstrap, implemented attribution-only `hooks` subcommand routing/validation entrypoints, and a fully implemented top-level `sce sync` command that synchronizes the current repository's Agent Trace DB with the control plane and renders the documented text/JSON output (see `context/cli/sync-command.md`). - `auth login stored-credential renewal`: The `sce auth login` behavior that first validates every stored credential through the existing non-forced token path, preserves valid credentials, refreshes expired credentials, and falls back to device authorization when credentials are absent or renewal fails. After WorkOS persistence, every successful fresh, valid-stored, or renewed login path issues and saves one replacement telemetry credential through the resolved Control Plane client; provisioning is best effort and unavailable status retains successful WorkOS authentication. Renewal reports retain `login` labels in text and JSON; credential renewal is not exposed as a public subcommand. The auth command boundary treats missing-credential logout/whoami calls as successful state queries, classifies authenticated Control Plane failures as `UserError::NotAuthenticated`, token-storage plus `AuthError::Io`/`Storage` failures as `UserError::AuthStorageUnavailable` (`auth.storage_unavailable`), and approved user-facing rendering/prompt fallbacks as `UserError::UnexpectedFailure` (`general.unexpected_failure`); fixed catalog messages expose no storage or implementation details while technical sources remain available for structured observability, and remaining auth-command failures use `UnexpectedFailure` with preserved technical sources. -- `command surface contract`: The current top-level command/help catalog split where `cli/src/cli_schema.rs` owns the real clap-backed command metadata (top-level purpose text plus help visibility for `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, and `completion`) and `cli/src/command_surface.rs` consumes that catalog for the custom banner/help surface plus known-command classification, while still adding the synthetic `help` row. +- `command surface contract`: The current top-level command/help catalog split where `cli/src/cli_schema.rs` owns the real clap-backed command metadata (top-level purpose text plus help visibility for `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `plan`, `version`, and `completion`) and `cli/src/command_surface.rs` consumes that catalog for the custom banner/help surface plus known-command classification, while still adding the synthetic `help` row. - `top-level help visibility metadata`: Per-command `show_in_top_level_help` metadata in `cli/src/cli_schema.rs` that controls whether a known command appears in `sce`, `sce help`, and `sce --help` without affecting direct invocation; the current hidden top-level commands are `hooks` and `policy`, while `auth` is visible, and `cli/src/command_surface.rs` renders the curated top-level help list from that shared metadata. - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, `uuid`, plus target-specific keyring backends), validated through normal compile/test coverage. `[dev-dependencies]` now carries `quint-connect` (dev/test-only, for the in-progress mutation-cursor Quint Connect model-based-testing harness; see `context/plans/mutation-cursor-quint-connect.md`). - `progress reporter contract`: Library-independent, consumer-typed sync seam in `cli/src/services/sync/progress.rs` where `ProgressReporter` carries an arbitrary consumer event type, supports explicit successful finalization and closure-based collectors, and provides `NoopProgressReporter` for callers without human progress output. The sync-owned `SyncProgressEvent` in `cli/src/services/sync/sync.rs` carries lifecycle, accepted-batch, and stream-completion payloads for the three concurrent remote Agent Trace streams (`messages`, `parts`, `agent_traces`); `services::sync::progress` also owns the fixed `indicatif` terminal adapter and focused contract tests, while `sync/command.rs` selects text versus JSON behavior. No top-level `services::progress` module exists. @@ -135,7 +135,7 @@ - `SCE_LOG_DIR`: Optional runtime env key for `sce` observability log-directory configuration; when set, it overrides config-file `log_dir` and the `/sce/logs` default and must be non-empty. - `app startup phases`: Current `cli/src/app.rs` execution model that separates dependency checking, startup-context construction, runtime initialization, command parse/execute, and output rendering into named helpers while preserving the CLI's existing exit-code, stderr-diagnostic, and degraded-startup behavior; output rendering and execution-phase logging helpers live in `cli/src/services/app_support.rs`. - `RunOutcome`: Generic final render payload in `cli/src/services/app_support.rs` (`RunOutcome`) carrying a command result, optional startup diagnostic, and optional logger implementing the logger trait boundary. Production construction in `cli/src/app.rs` uses the concrete observability logger, while rendering is not hardcoded to that production type. -- `CommandRegistry`: Static command-name catalog in `cli/src/services/command_registry.rs`; populated by `build_default_registry()` and carried by `AppRuntime` during command dispatch. It exposes deterministic command-name membership for the current top-level command catalog (`help`, `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, and `completion`) while actual parsed command payloads are represented by the `RuntimeCommand` enum rather than zero-arg boxed constructors. +- `CommandRegistry`: Static command-name catalog in `cli/src/services/command_registry.rs`; populated by `build_default_registry()` and carried by `AppRuntime` during command dispatch. It exposes deterministic command-name membership for the current top-level command catalog (`help`, `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `plan`, `version`, and `completion`) while actual parsed command payloads are represented by the `RuntimeCommand` enum rather than zero-arg boxed constructors. - `ServiceLifecycle`: Compile-safe lifecycle trait seam in `cli/src/services/lifecycle.rs` with default no-op generic `diagnose`, `fix`, and `setup` methods over `C: HasRepoRoot`; it exposes lifecycle-owned health, fix, and setup result types, while doctor/setup adapt those records at orchestration boundaries before rendering command-owned output. The hooks service has `HooksLifecycle` for hook rollout diagnosis/fix/setup, the config service has `ConfigLifecycle` for global/repo-local config validation plus repo-local config bootstrap, local_db has `LocalDbLifecycle` for canonical local DB path health/bootstrap/setup, auth_db has `AuthDbLifecycle` for canonical auth DB path health/bootstrap/setup, and agent_trace_db has `AgentTraceDbLifecycle` for repository identity resolution, setup-time repository-scoped Agent Trace DB initialization, and repository Agent Trace DB path health/bootstrap, returning an actionable "requires a Git repository" diagnostic outside repository context (the former global parent fallback was removed by the `retire-legacy-agent-trace-db` plan). Doctor runtime aggregates the static provider catalog for `diagnose` and `fix`; setup command aggregates providers for `setup` in order (config → local_db → auth_db → agent_trace_db → hooks when requested). - `lifecycle provider catalog`: Shared factory in `cli/src/services/lifecycle.rs` (`lifecycle_providers(include_hooks)`) that returns static `LifecycleProvider` enum values in deterministic config → local_db → auth_db → agent_trace_db → hooks order, used by doctor with hooks included and by setup with hooks included only when requested. The enum owns concrete-provider dispatch for `id`, `diagnose`, `fix`, and `setup` without boxed provider trait objects or `&dyn HasRepoRoot` lifecycle context erasure. - `sce config command surface`: Implemented top-level CLI command routed by `cli/src/app.rs` to `cli/src/services/config/mod.rs` (with schema/file-parsing delegated to `cli/src/services/config/schema.rs`, runtime precedence resolution delegated to `cli/src/services/config/resolver.rs`, and text/JSON output construction delegated to `cli/src/services/config/render.rs`), exposing `show`, `validate`, and `--help` for deterministic runtime config inspection and validation; `show` reports resolved flat logging observability values with provenance, while `validate` reports pass/fail plus validation issues and warnings only. diff --git a/context/overview.md b/context/overview.md index ddc104775..43e6ce00e 100644 --- a/context/overview.md +++ b/context/overview.md @@ -45,7 +45,7 @@ for `/validate`. See This repository maintains shared assistant configuration for OpenCode, Claude, Pi, and Codex from a single canonical Pkl authoring source. One typed workflow catalog owns the six workflows' shared identity and target routing metadata, while canonical workflow/phase modules own behavior and migrated package-local documents, and target renderers own formatting. Generated target layouts are ephemeral: repository builds consume a pre-Cargo generated payload through `SCE_CLI_GENERATED_INPUT_DIR`, crates.io and Flatpak stage packaging-only fallbacks, and `config/.opencode`, `config/.claude`, `config/.pi`, `config/.agents`, and the generated working-tree SCE config schema are not committed; versioned SCE config schema snapshots live under `schema/v/`. The catalog also marks a workflow `optional` — currently only `brownfield` — which changes nothing about generation and is projected into a generated `config/optional-workflows.json` manifest for install-time consumers. `nix run .#pkl-check-generated` preserves its exact 145-path artifact, metadata/package, phase-reference, internal-reference, optional-workflow-manifest, workflow-orchestration, OpenCode-permission, required-path, and forbidden-path checks while delegating deterministic payload production and inventories to the shared generated-input producer; `nix flake check` runs the same contract. The target matrix contains one manual OpenCode profile plus Claude, Pi, and Codex; the former automated OpenCode profile has been removed. A fourth Pkl renderer, `config/pkl/renderers/codex-content.pkl`, also consumes the same canonical workflow composition to emit skills-only Codex output under `config/.agents/skills/**` (no per-target frontmatter, matching Pi, and no command/prompt layer). Each of the six catalog workflow skills (not `sce-decision`, which has no user-facing entrypoint on any target) additionally carries a Codex-only `agents/openai.yaml`, rendered by `config/pkl/renderers/codex-metadata.pkl` from the shared catalog's `title`/`description` plus an authored `default_prompt`, with `policy.allow_implicit_invocation: false` so these stateful lifecycle workflows activate only from explicit `$sce-` invocation or Codex's `/skills` discovery, never from conversational relevance alone. The Codex renderer also emits a Codex hook registration file (`config/.codex/hooks.json`) and its install-guidance helper (`config/.codex/hooks/run-sce-or-show-install-guidance.sh`). The existing conversation/diff registrations (`UserPromptSubmit`, `Stop`, `PreToolUse` for `Bash`, `PostToolUse` for `apply_patch`) route through `sce hooks codex` and retain their fail-open behavior where designed. The separate mutation-scope registrations route through `sce hooks codex-mutation-scope`: `PreToolUse` and `PostToolUse` use matcher `^(Bash|apply_patch)$`, while `Stop`, `Interrupt`, `SubagentStop`, and `SessionEnd` omit matcher and are unmatched. The tracked mutation `PreToolUse` bootstrap fails closed if Git-root resolution, helper or `sce` discovery, adapter startup, runtime `Start`, or Bash-policy evaluation cannot establish attribution; mutation-scope `PostToolUse` and cleanup hooks do not use this bootstrap behavior. Delegation, MCP, and unknown tools do not match these generated mutation Pre/Post registrations. `sce setup --codex` (and `--all`) now installs it as a fourth `SetupTarget`; setup merges the user-owned `.codex/hooks.json` through the shared structural Codex hook-config service, preserving unrelated valid handlers and rejecting malformed or Codex-invalid documents before the atomic swap. `sce doctor` diagnoses that file per required registration (`PresentAndCurrent`/`Missing`/`Stale`, or `Malformed` for the whole document) rather than by whole-document equality, reporting the mutation-scope registrations under distinct `#(mutation-scope)` rows separate from the `sce hooks codex` rows, and separately reports whether Codex has actually marked each structurally current registration trusted and whether its effective hook-discovery policy allows project hooks, by reading (never writing) Codex's own `$CODEX_HOME/config.toml` and policy state; `sce doctor --fix` repairs structurally unhealthy registrations through the same merge service but never touches trust or policy state (see `context/sce/doctor-human-text-contract.md`). `sce hooks codex` now exists as a typed dispatcher (`cli/src/services/hooks/codex/`): it parses the raw hook JSON into a `CodexHookEvent` and classifies `(hook_event_name, tool_name)` into `UserPromptSubmit`, `Stop`, `PreToolUse(Bash)`, `PostToolUse(apply_patch)`, or a fail-open `NoOp` fallthrough covering every other combination — including `apply_patch` under `PreToolUse`. `UserPromptSubmit` and `Stop` each capture one `messages`/`parts` row (`role="user"`/`role="assistant"`) into the repository Agent Trace DB under the idempotent `cx_` session prefix, `PreToolUse(Bash)` delegates to the existing Bash policy engine, and `PostToolUse(apply_patch)` parses Codex's own `apply_patch` text, resolves paths from the event `cwd` against the real Git root into safe repository-relative paths, normalizes Add/Update evidence into an SCE unified diff under deterministic event-scoped synthetic line identities derived from `tool_use_id`, and persists it as one `diff_traces` row when non-empty (see `context/sce/codex-integration-runtime.md`) — while invalid cwd/path resolution and malformed STDIN payloads fail open with empty stdout, reported model IDs are preserved without fabricated provider prefixes, and missing/blank apply_patch sessions produce no evidence. Delete-File operations and Bash filesystem effects do not become `diff_traces` in this existing conversation/diff pipeline, and its apply_patch evidence has separate operation limitations. Separately, the Codex mutation-scope adapter tracks `Bash` and `apply_patch` executions as `TrackedMutation` scopes; `AiExclusive` means tracked-scope exclusivity, not that Bash authored every mutation in the interval. -It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered without a module-wide dead-code allowance (only declaration-level, reasoned exceptions remain, and snapshot-ref reconciliation is wired explicitly through `sce doctor --fix`; automatic reclamation is a deferred follow-up). A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). +It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, deterministic plan structure checking (`sce plan check `), and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered without a module-wide dead-code allowance (only declaration-level, reasoned exceptions remain, and snapshot-ref reconciliation is wired explicitly through `sce doctor --fix`; automatic reclamation is a deferred follow-up). A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). The generated `/next-task` workflow persists task-level context-synchronization lifecycle state in each plan (`pending`, `synced`, or `blocked`) so unresolved task synchronization debt survives a session boundary and gates new implementation. Successful `/next-task` execution hands task synchronization an explicit, pre-edit-Git-baseline-relative changed-file list plus implementation, verification, done-check, plan-update, and context-impact evidence, recorded directly on the completed task (`Completed`, `Files changed`, `Result`, `Verify`, `Context impact`, `Context synchronization`); the five-file root context pass remains mandatory. A later-session sync-debt retry reads that same completed task record directly from the plan by plan path and task ID, with no separate persisted synchronization handoff. `/validate` is validation-only: it runs final checks, writes the Validation Report, and reports `validated`, `failed`, or `blocked` without plan-level context synchronization. @@ -61,7 +61,7 @@ The generated `/next-task` workflow persists task-level context-synchronization - **Install channels:** repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`); Homebrew deferred (see `context/sce/cli-first-install-channels-contract.md`). The CLI crate currently depends on `anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, and `uuid`, with target-specific keyring backend dependencies for Linux/FreeBSD, macOS, and Windows. `quint-connect` is the CLI's first dev-only dependency, added for the in-progress `mutation-cursor-quint-connect` model-based-testing harness. -Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with successful login paths provisioning and replacing the cached telemetry credential after WorkOS persistence on fresh, valid-stored, and renewed credentials while keeping provisioning best effort; authenticated whoami reads the Control Plane `GET /me` profile and renders flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, and the top-level `sync` command. Unknown command paths are successful help requests: top-level unknown tokens use the normal top-level help payload and nested unknown tokens use the closest valid parent's help surface, while unknown options and other parse/validation failures retain their existing errors. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. +Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with successful login paths provisioning and replacing the cached telemetry credential after WorkOS persistence on fresh, valid-stored, and renewed credentials while keeping provisioning best effort; authenticated whoami reads the Control Plane `GET /me` profile and renders flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, read-only plan structure checking (`sce plan check`), and the top-level `sync` command. Unknown command paths are successful help requests: top-level unknown tokens use the normal top-level help payload and nested unknown tokens use the closest valid parent's help surface, while unknown options and other parse/validation failures retain their existing errors. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. The current doctor presentation contract supersedes the earlier output-shape scaffolding wording above: human text uses the compact Environment/Repository/Integrations hierarchy with healthy rows collapsed and unhealthy branches expanded, while JSON retains complete path, identity, problem, and fix-result detail. See `context/sce/doctor-human-text-contract.md`. The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: `2` parse failures, `3` invocation validation failures, `4` runtime failures, and `5` dependency startup failures. Auth command orchestration routes authenticated Control Plane authentication failures, token-storage failures, and approved user-facing fallback failures through typed `CliError::User` catalog entries (`NotAuthenticated`, `AuthStorageUnavailable`, `NotGitRepository`, and `UnexpectedFailure`) while retaining technical sources for observability; missing-credential logout/whoami state queries succeed with their documented text/JSON reports, and internal auth failures remain runtime errors. The same runtime also emits stable CLI stderr diagnostics: internal failures use `SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, or `SCE-ERR-DEPENDENCY` in deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing, while expected failures emit only their redacted, unstyled catalog messages. The command boundary's former flat, string-only `ClassifiedError` has been replaced by typed `CliError` in `cli/src/services/error.rs`: `CliError::User` carries a closed catalog (`NotAuthenticated`, `NotGitRepository`, `NotGitRemote`, authentication-storage `AuthStorageUnavailable`, and general `UnexpectedFailure`) for expected, deliberately-explained failures rendered without the technical source, wrapper, styling, or automatic `Try:` guidance; the general entry renders one fixed static log-files guidance sentence without dynamic path text. `CliError::Internal` carries a live `anyhow::Error` source rendered as the real error chain with the existing styled wrapper and class-default remediation; `app_support` is the sole owner of the distinct terminal paths, and `sce sync` classifies authentication, credential-storage, and all other sync failures as cataloged user errors while preserving their technical sources for observability. Setup preflight errors preserve technical sources, including the configured remote name, while raw remote URLs remain out of user-facing diagnostics. See `context/sce/cli-error-code-taxonomy.md` for the full contract. diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md index d940da0ab..1339afe95 100644 --- a/context/plans/sce-plan-check.md +++ b/context/plans/sce-plan-check.md @@ -197,14 +197,19 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized — the internal `cli/src/services/plan/` domain gains a validation layer that is not yet reachable from the CLI. The plan-structure fixture check in `check-generated.sh` is now driven by `expectations.tsv`, which changes the documented `config-generation.md` fixture-run description. No user-visible CLI behavior change. - Context synchronization: synced -- [ ] T03: `Wire sce plan check into the CLI schema and static dispatcher` (status:todo) +- [x] T03: `Wire sce plan check into the CLI schema and static dispatcher` (status:done) - Task ID: T03 - Covers: AC1, AC4 - Scope: In — `cli_schema.rs`: `PLAN_*` constants, a `Commands::Plan { subcommand: PlanSubcommand::Check { path: PathBuf } }` variant, and a `TOP_LEVEL_COMMANDS` entry. Conversion in `services/parse/command_runtime.rs`, a `RuntimeCommand::Plan` variant in `services/command_registry.rs`, and `services/plan/command.rs`, which reads the file with path context (runtime failure) and maps defects to `CliError::validation` with D4 rendering. Update the affected help/completion tests, and add the S1–S4 command tests. Out — workflow prose, any `plan init`. - Dependencies: T02 - Done when: S1–S4 tests pass. `sce plan check` on a valid fixture exits `0` and on an invalid one exits `3` with `{path}:{line}:` diagnostics. Help and completion list `plan check`. - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml completion`; `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/duplicate-id.md; echo $?` - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `cli/src/cli_schema.rs`, `cli/src/command_surface.rs`, `cli/src/services/command_registry.rs`, `cli/src/services/mod.rs`, `cli/src/services/parse/command_runtime.rs`, `cli/src/services/plan/command.rs`, `cli/src/services/plan/mod.rs` + - Result: `cli_schema.rs` adds the `PLAN_CLAP_ABOUT`/`PLAN_TOP_LEVEL_PURPOSE`/`PLAN_SHOW_IN_TOP_LEVEL_HELP` constants, a visible `Commands::Plan { subcommand: PlanSubcommand::Check { path: PathBuf } }` with a required ``, and a `TOP_LEVEL_COMMANDS` entry. `services::plan::NAME = "plan"` is registered in `DEFAULT_COMMAND_NAMES`. `RuntimeCommand::Plan(PlanCommand)` dispatches statically to the synchronous `PlanCommand::execute()`. `command_runtime.rs` converts the clap command through `convert_plan_subcommand`. `services/plan/command.rs` reads the file with `with_context("Failed to read plan file '{path}'")` and maps read failures to `CliError::runtime` (exit `4`). It then runs `extract` and `validate` and returns `STRUCTURED_PLAN_PASS`/`LEGACY_PLAN_PASS` on stdout, or `CliError::validation` (exit `3`) for a defective plan. That error message is a header line, then one `{path}:{line}: {message}` (or `{path}: {message}`) per diagnostic, then its own `Try:` line, so the default class guidance is not appended. The T01 `dead_code` allowance on `pub mod plan` was removed. Approved assumptions: no help or completion tests existed before this task, so `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` were added. The top-level help also gains a `Plan Usage:` section (`sce plan check `). `default_runtime_command` has no `plan` default because the command requires a path. S4 uses a temporary structured plan whose `Validate:`/`Check:`/`Verify:` values are `touch `, so no new fixture or manifest row was needed. + - Verify outcome: `plan::` passed 14 tests (the 8 earlier ones, plus `command_valid_plans_exit_success`, `command_invalid_plan_exits_validation`, `command_missing_file_is_runtime_failure`, `command_does_not_execute_plan_commands`, `plan_check_appears_in_help`, and `completion_scripts_include_plan_check`). `completion` passed 2 tests. `nix run .#sce -- plan check …/duplicate-id.md` printed `…/duplicate-id.md:29: \`S1\` is defined more than once` under `SCE-ERR-VALIDATION` and exited `3`. `valid-structured-plan.md` and `legacy-plan.md` exited `0` with their classification lines. A missing file exited `4` with `SCE-ERR-RUNTIME` naming the path. `plan check --help` shows ``, top-level help lists `plan`, and bash completion contains `plan`. `clippy --all-targets -D warnings` is clean, and `cargo fmt` was applied. `cli/src/services/plan/` has no `std::process` use outside the tests' `ExitCode`. + - Context impact: significant — new user-visible top-level `sce plan check ` command (help, completion, and exit-code `0`/`3`/`4` behavior), and the `services/plan` domain is now reachable from the CLI. This affects `context/cli/cli-command-surface.md`, `context/overview.md`, `context/cli/plan-service.md`, and `context/glossary.md`. + - Context synchronization: synced - [ ] T04: `Invoke sce plan check from the change-to-plan and validate workflows` (status:todo) - Task ID: T04 From 5c3bce1f6ce7f8e83b63fa39d4505fb76e561024 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 01:33:02 +0200 Subject: [PATCH 13/61] config: Run plan checks in authoring and validation workflows Run `sce plan check` after structured plans are written and before validation scenario checks, with the documented trust rule, diagnostics handling, legacy skip, and manual Reference rules fallback. Extend generation contracts and fixtures to require the invocation and keep it out of `/next-task`, and record the completed workflow responsibility split in shared context. Plan: sce-plan-check Task: T04 Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 12 ++++++++++ config/pkl/base/workflow-validate.pkl | 12 +++++++++- config/pkl/check-generated.sh | 4 ++-- ...-authoring-structured-references-check.pkl | 2 +- .../validate-scenario-evidence-check.pkl | 2 +- .../renderers/generation-contract-check.pkl | 24 +++++++++++++++++-- context/plans/sce-plan-check.md | 9 +++++-- context/sce/config-generation.md | 2 +- context/sce/shared-context-plan-workflow.md | 24 +++++++++++++++---- 9 files changed, 77 insertions(+), 14 deletions(-) diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index da807377a..3a90b62d8 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -456,6 +456,18 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. +After writing a structured plan, run `sce plan check {plan-path}`. When it reports +diagnostics, fix each reported defect in the written plan and run it again. Do +not set internal status `plan_ready` until the checker passes. Skip the checker +for a legacy plan. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` or +`legacy plan: structured reference rules do not apply` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; apply the +manual `Reference rules` pass from step 2.9 to the written plan instead, and +record in the open questions that the checker was unavailable. + ## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index 0771e96ec..cec8cf20b 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -260,7 +260,17 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ Skip this step for a legacy plan. - For a structured plan, apply every entry of the plan template's + For a structured plan, run `sce plan check {plan-path}` before scenario + verification. Record each reported diagnostic as a failed check under + **Failed checks and follow-ups**, quoting its line and message. + + Trust the checker only when it actually ran: a result counts only on exit `0` + with `structured plan: reference integrity holds` or + `legacy plan: structured reference rules do not apply` on stdout, or on exit `3` + with diagnostics. Any other outcome means the checker is unavailable; record + that fallback under notes and apply the manual pass below instead. + + For the manual pass, apply every entry of the plan template's `Reference rules` to the plan. Each of these is a relationship-integrity defect: diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 91262b11b..41a217c26 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -156,7 +156,7 @@ expect_pkl_fixture_failure \ "generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl" \ - "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing" + "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-structured-references-check.pkl" \ "generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution" @@ -186,7 +186,7 @@ expect_pkl_fixture_failure \ "generated sce-validate document must not contain a sce-decision reference or plan-context-sync wording" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl" \ - "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged" + "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl" \ "generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans" diff --git a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl index d419a995e..d261c5f9b 100644 --- a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl +++ b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl @@ -8,4 +8,4 @@ local documents = new Mapping { result = if ( contract.assertPlanAuthoringStructuredReferences.apply(documents) ) "negative fixture unexpectedly accepted a plan-authoring phase without the structured authoring order or reference-consistency pass" -else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") +else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document") diff --git a/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl index 16a07d899..0af3e2aa5 100644 --- a/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl +++ b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl @@ -8,4 +8,4 @@ local documents = new Mapping { result = if ( contract.assertValidateScenarioEvidence.apply(documents) ) "negative fixture unexpectedly accepted validation.md without relationship integrity or scenario evidence rules" -else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged") +else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 2bf9c2726..e2c7c9e6d 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1095,16 +1095,28 @@ local requiredPlanAuthoringStructuredTokens = new Listing { "Give each task its `Covers:` field" "## 2.9 Check references" "run a reference-consistency pass over the\ncomplete draft that applies every `Reference rules` entry" + "After writing a structured plan, run `sce plan check {plan-path}`." + "Do\nnot set internal status `plan_ready` until the checker passes." + "Skip the checker\nfor a legacy plan." + "Trust the checker only when it actually ran:" + "`structured plan: reference integrity holds`" + "`legacy plan: structured reference rules do not apply`" + "Any other outcome means the checker is unavailable;" + "manual `Reference rules` pass from step 2.9" } +local noNextTaskPlanCheckReference = (documents: Mapping) -> + documents.every((path, text) -> !path.contains("/skills/sce-next-task/") || !text.contains("sce plan check")) + hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> if ( documents.every((path, text) -> !path.endsWith("/skills/sce-change-to-plan/references/plan-authoring.md") || requiredPlanAuthoringStructuredTokens.every((token) -> text.contains(token)) ) + && noNextTaskPlanCheckReference.apply(documents) ) "sce-change-to-plan plan-authoring: decisions, criteria, scenarios, and tasks authored in order with a reference-consistency pass" - else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing") + else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document") local requiredNextTaskStructuredReferenceTokens = new Listing { Pair("/skills/sce-next-task/references/plan-review.md", "Read plan structure only outside fenced code blocks;") @@ -1359,6 +1371,13 @@ local requiredValidateScenarioEvidenceTokens = new Listing { "- `Not verified` when" "Only evidence gathered by this run counts. Task completion, a task's recorded\n`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never\nevidence." "an acceptance criterion is met only when its `Validate:`\ncheck passes and every scenario its `Scenarios:` names is `Verified`." + "For a structured plan, run `sce plan check {plan-path}` before scenario\nverification." + "Record each reported diagnostic as a failed check" + "Trust the checker only when it actually ran:" + "`structured plan: reference integrity holds`" + "`legacy plan: structured reference rules do not apply`" + "Any other outcome means the checker is unavailable;" + "apply the manual pass below instead." } hidden assertValidateScenarioEvidence = (documents: Mapping) -> @@ -1368,8 +1387,9 @@ hidden assertValidateScenarioEvidence = (documents: Mapping) -> !path.endsWith("/skills/sce-validate/references/validation.md") || requiredValidateScenarioEvidenceTokens.every((token) -> text.contains(token)) ) + && noNextTaskPlanCheckReference.apply(documents) ) "sce-validate validation: structured plans get relationship integrity and scenario evidence; legacy plans keep the legacy path" - else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, and keep legacy plans unchanged") + else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged") local requiredValidationReportScenarioTokens = new Listing { "### Scenario verification" diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md index 1339afe95..cb277a757 100644 --- a/context/plans/sce-plan-check.md +++ b/context/plans/sce-plan-check.md @@ -211,14 +211,19 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: significant — new user-visible top-level `sce plan check ` command (help, completion, and exit-code `0`/`3`/`4` behavior), and the `services/plan` domain is now reachable from the CLI. This affects `context/cli/cli-command-surface.md`, `context/overview.md`, `context/cli/plan-service.md`, and `context/glossary.md`. - Context synchronization: synced -- [ ] T04: `Invoke sce plan check from the change-to-plan and validate workflows` (status:todo) +- [x] T04: `Invoke sce plan check from the change-to-plan and validate workflows` (status:done) - Task ID: T04 - Covers: AC5 - Scope: In — `config/pkl/base/workflow-change-to-plan.pkl` steps 2.9–2.10: after writing a structured plan, run `sce plan check {plan-path}`, fix the reported defects, and re-run before `plan_ready`. `config/pkl/base/workflow-validate.pkl` step 4: run the checker before scenario verification and record each diagnostic as a failed check. Both get the D5 trust rule, the manual `Reference rules` fallback, and the legacy skip. Extend the existing `plan-authoring-structured-references` and `validate-scenario-evidence` assertions in `generation-contract-check.pkl`, and the negative fixtures they already use, so the invocation, fallback, and `/next-task` exclusion are required. Out — `/next-task` sources, Git hooks, new assertion families. - Dependencies: T03 - Done when: Generated plan-authoring and validation references on every target carry the invocation, trust rule, fallback, and legacy skip, and `pkl-check-generated` passes. - Verify: `nix run .#pkl-check-generated` - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `config/pkl/base/workflow-change-to-plan.pkl`, `config/pkl/base/workflow-validate.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, `config/pkl/check-generated.sh`, `config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl`, `config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl` + - Result: Plan-authoring step 2.10 now runs `sce plan check {plan-path}` after writing a structured plan, fixes the reported defects, and re-runs until the checker passes before `plan_ready`. It skips the checker for a legacy plan. Validation step 4 runs `sce plan check {plan-path}` before scenario verification for a structured plan and records each diagnostic as a failed check. The step keeps "Skip this step for a legacy plan.", and its existing rule list is now the manual pass. Both carry the D5 trust rule: a result counts only on exit `0` with one of the two D4 classification lines, or on exit `3` with diagnostics. Any other outcome falls back to the manual `Reference rules` pass. Plan authoring records that fallback in open questions; validation records it under notes. `assertPlanAuthoringStructuredReferences` and `assertValidateScenarioEvidence` require the new invocation, trust-rule, fallback, and legacy-skip tokens. Both also apply a shared `noNextTaskPlanCheckReference` check, which rejects any `/skills/sce-next-task/` document containing `sce plan check`. Both throw messages were extended, and the matching strings in `check-generated.sh` and the two existing negative fixtures were kept in sync. Approved assumptions: the `/next-task` exclusion lives inside the two existing assertions, with no new assertion family. + - Verify outcome: `nix run .#pkl-check-generated` passed (145 files). Generated output contains `sce plan check` only in `sce-change-to-plan/references/plan-authoring.md` and `sce-validate/references/validation.md`, on all four targets (`.agents`, `.claude`, `.opencode`, `.pi`), and in no `sce-next-task` document. A throwaway Pkl evaluation in the scratchpad (since removed) confirmed two things: the generated `validation.md` passes `assertValidateScenarioEvidence`, and adding an `sce-next-task` document that mentions `sce plan check` makes it throw. + - Context impact: localized — the generated `/change-to-plan` and `/validate` workflow references now invoke `sce plan check` with a fallback. This affects `context/sce/shared-context-plan-workflow.md` (checker responsibility split and fallback). `/next-task` is unchanged. + - Context synchronization: synced - [ ] T05: `Document the Pkl/Rust plan-structure behavioral contract` (status:todo) - Task ID: T05 diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 81b954ebf..5c367d0e5 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `structured`/`legacy`/`fail` verdict, then expected defect substrings; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `structured` and `legacy` rows must pass with that classification and no listed substrings, `fail` rows must fail with every listed substring, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example; the negative fixtures are duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, and runs a reference-consistency pass before writing. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, and meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `structured`/`legacy`/`fail` verdict, then expected defect substrings; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `structured` and `legacy` rows must pass with that classification and no listed substrings, `fail` rows must fail with every listed substring, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example; the negative fixtures are duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing a structured plan with the checker trust rule, manual `Reference rules` fallback, and legacy skip. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 7a8aba893..23c7374bd 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -94,8 +94,10 @@ writes scenarios only for the case kinds (normal, boundary, failure, cancellation, concurrency, recovery) the change makes relevant, and a `Check:` never names a test that neither exists nor is created by a task. Before writing a structured plan, a reference-consistency pass applies every `Reference rules` -entry to the full draft and fixes each defect first. A legacy-plan revision -skips this structured work. The phase cites the template's rule sections by +entry to the full draft and fixes each defect first. After writing a structured +plan, it runs `sce plan check {plan-path}`, fixes every reported diagnostic, and +re-runs the checker before returning `plan_ready`. A legacy-plan revision +skips this structured work, including the checker. The phase cites the template's rule sections by name and does not restate them. Responsibility for structural consistency is split by surface. Generation checks @@ -104,10 +106,24 @@ template and the behavior of the structural checker `config/pkl/renderers/plan-structure-check.pkl` against fixtures only; they never read `context/plans/`, so an incomplete or malformed working plan cannot fail Nix CI. Plan authoring produces structurally consistent plans. Plan validation checks -the selected plan's references and its actual verification evidence; the -structural checker remains available for one selected plan with +the selected plan's references and its actual verification evidence. For one +selected plan, the shipped `sce plan check ` command (see +[plan service](../cli/plan-service.md)) is the deterministic checker; the Pkl +checker stays the generation-time reference implementation and is still +runnable with `pkl eval -p planPath= config/pkl/renderers/plan-structure-check.pkl`. +`/change-to-plan` (after writing) and `/validate` (step 4, before scenario +verification) run `sce plan check {plan-path}` on structured plans only; +`/validate` records each diagnostic as a failed check. Both trust a result only +when the checker actually ran: exit `0` with +`structured plan: reference integrity holds` or +`legacy plan: structured reference rules do not apply` on stdout, or exit `3` +with diagnostics. Any other outcome, such as an older `sce` that answers the +unknown `plan` command with help and exit `0`, means the checker is unavailable, +and the workflow applies the manual `Reference rules` pass instead and records +the fallback. `/next-task` never invokes the checker. + ## Continuation contract Same-session waits preserve one of two explicit continuation shapes inside the From 6828e7e16fcce5a0fbffb1b49d766339e67688d5 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 01:50:35 +0200 Subject: [PATCH 14/61] context: Document the plan structure checker contract Record the shared Pkl/Rust plan-structure rules, fixture manifest workflow, diagnostic contract, and known parser divergences. Link the contract from the plan workflow and context map, and mark the completed plan-check acceptance criteria and T05 task with validation evidence. Plan: sce-plan-check Tasks: T05 Acceptance criteria: AC1-AC7 Co-authored-by: SCE --- context/cli/plan-service.md | 2 +- context/context-map.md | 1 + context/plans/sce-plan-check.md | 75 ++++++++++++++++++--- context/sce/plan-structure-contract.md | 70 +++++++++++++++++++ context/sce/shared-context-plan-workflow.md | 2 + 5 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 context/sce/plan-structure-contract.md diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index 2bc205cbf..eed07d330 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -69,7 +69,7 @@ ## Divergences from the Pkl line scanner -The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at exactly `entry indent + 2` and accepts definitions at any indentation. Indentation-only nesting differences, `[X]` checkboxes, and multi-line field values can therefore extract differently. Agreement is pinned by the shared fixtures under `config/pkl/renderers/fixtures/plan-structure/`. +The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at exactly `entry indent + 2` and accepts definitions at any indentation. Indentation-only nesting differences, `[X]` checkboxes, and multi-line field values can therefore extract differently. Agreement is pinned by the shared fixtures under `config/pkl/renderers/fixtures/plan-structure/`. The full divergence list and the shared rules live in the [plan structure contract](../sce/plan-structure-contract.md). ## Tests diff --git a/context/context-map.md b/context/context-map.md index e7c09f4dd..d854762db 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -60,6 +60,7 @@ Feature/domain context: - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with explicit config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary, logger and deliberately non-object-safe generic async telemetry boundaries with static capability dispatch and future-aware polling scope, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, managed OTLP trace/log export (credential-gated, hardcoded canonical collector, allowlisted fields, bounded fail-open lifecycle, reserved metrics endpoint), and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) - `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, structured vs legacy plan detection), clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) +- [Plan structure contract](sce/plan-structure-contract.md) (shared Pkl/Rust plan-structure checker contract: shared rule set, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` as the single fixture source of truth and how to add a fixture, `sce plan check` result and diagnostic wording shared with the Pkl defect strings, the both-checkers rewording rule, known AST-versus-line-scanner divergences, and Pkl staying generation-only while the CLI never invokes Pkl) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) - `context/sce/plan-code-overlap-map.md` (overlap matrix for thin OpenCode Plan/Code routing agents and six workflow packages, including task-only context synchronization ownership) - `context/sce/dedup-ownership-table.md` (canonical owner-vs-consumer boundaries for six workflow packages, canonical phase modules, the shared synchronization skeleton, and thin OpenCode agents) diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md index cb277a757..5b07ae026 100644 --- a/context/plans/sce-plan-check.md +++ b/context/plans/sce-plan-check.md @@ -51,30 +51,30 @@ generation-time reference implementation. ## Acceptance criteria -- [ ] AC1: `sce plan check ` validates exactly the selected file, read-only. It accepts relative and absolute paths. It exits `0` and prints the structured or legacy classification line for a valid plan. It exits `3` and prints `{path}:{line}: {message}` diagnostics on stderr for a structurally invalid plan. A missing or unreadable file fails through the existing runtime-failure handling. It never executes `Check:`, `Validate:`, or `Verify:` content. +- [x] AC1: `sce plan check ` validates exactly the selected file, read-only. It accepts relative and absolute paths. It exits `0` and prints the structured or legacy classification line for a valid plan. It exits `3` and prints `{path}:{line}: {message}` diagnostics on stderr for a structurally invalid plan. A missing or unreadable file fails through the existing runtime-failure handling. It never executes `Check:`, `Validate:`, or `Verify:` content. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` plus `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md; echo $?` (expect `0`) and the same against `duplicate-id.md` (expect `3`). - Decisions: D1, D2, D4 - Scenarios: S1, S2, S3, S4 -- [ ] AC2: The Rust validator and the Pkl checker give the same verdict and defect text for every fixture under `config/pkl/renderers/fixtures/plan-structure/`, both driven by the single `expectations.tsv` manifest. The fixtures cover valid structured, inspection-only, fenced-example, legacy, and legacy-with-fenced-structured-example plans. They also cover duplicate IDs, missing and empty `Covers:`, dangling references, wrong reference kinds, uncovered AC, unreferenced scenario, missing required fields, misplaced definitions and fields, wrong heading placement, and keywords only inside fenced examples. +- [x] AC2: The Rust validator and the Pkl checker give the same verdict and defect text for every fixture under `config/pkl/renderers/fixtures/plan-structure/`, both driven by the single `expectations.tsv` manifest. The fixtures cover valid structured, inspection-only, fenced-example, legacy, and legacy-with-fenced-structured-example plans. They also cover duplicate IDs, missing and empty `Covers:`, dangling references, wrong reference kinds, uncovered AC, unreferenced scenario, missing required fields, misplaced definitions and fields, wrong heading placement, and keywords only inside fenced examples. - Validate: `nix run .#pkl-check-generated` and `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture` - Decisions: D1, D3 - Scenarios: S5, S6 -- [ ] AC3: Every definition-, field-, and heading-anchored diagnostic carries the 1-based source line of the offending node, taken from `mdast` positions. +- [x] AC3: Every definition-, field-, and heading-anchored diagnostic carries the 1-based source line of the offending node, taken from `mdast` positions. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::diagnostic_lines` - Decisions: D1, D4 - Scenarios: S7 -- [ ] AC4: `sce plan` appears in top-level help, `sce plan check --help` documents the required `` argument, and generated bash/zsh/fish completion scripts include `plan check`. +- [x] AC4: `sce plan` appears in top-level help, `sce plan check --help` documents the required `` argument, and generated bash/zsh/fish completion scripts include `plan check`. - Validate: `nix run .#sce -- --help`, `nix run .#sce -- plan check --help`, and `nix run .#sce -- completion --shell bash | grep -n "plan"` - Decisions: D2 - Scenarios: S8 -- [ ] AC5: The generated `/change-to-plan` workflow runs `sce plan check {plan-path}` after writing a structured plan and corrects reported defects before returning `plan_ready`. The generated `/validate` workflow runs `sce plan check {plan-path}` before scenario verification for structured plans and records each reported defect as a failed check. Both fall back to the manual `Reference rules` pass when the checker is unavailable, keep legacy-plan behavior unchanged, and leave `/next-task` unchanged. +- [x] AC5: The generated `/change-to-plan` workflow runs `sce plan check {plan-path}` after writing a structured plan and corrects reported defects before returning `plan_ready`. The generated `/validate` workflow runs `sce plan check {plan-path}` before scenario verification for structured plans and records each reported defect as a failed check. Both fall back to the manual `Reference rules` pass when the checker is unavailable, keep legacy-plan behavior unchanged, and leave `/next-task` unchanged. - Validate: `nix run .#pkl-check-generated` - Decisions: D5 - Scenarios: S9 -- [ ] AC6: The Pkl/Rust behavioral contract is documented: the shared rules, the shared manifest as the source of truth, the result and diagnostic wording, and the known AST-versus-line-scanner divergences. +- [x] AC6: The Pkl/Rust behavioral contract is documented: the shared rules, the shared manifest as the source of truth, the result and diagnostic wording, and the known AST-versus-line-scanner divergences. - Validate: inspection of `context/sce/plan-structure-contract.md`, and of `context/sce/shared-context-plan-workflow.md` linking to it. - Scenarios: none (inspection) -- [ ] AC7: The only new runtime dependency is `markdown`. No repository plan under `context/plans/` is read by generation or Nix checks, and `flake.nix` gains no `context/plans/` source dependency. +- [x] AC7: The only new runtime dependency is `markdown`. No repository plan under `context/plans/` is read by generation or Nix checks, and `flake.nix` gains no `context/plans/` source dependency. - Validate: inspection of `git diff main -- cli/Cargo.toml flake.nix config/pkl/check-generated.sh` - Scenarios: none (inspection) @@ -225,15 +225,72 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: localized — the generated `/change-to-plan` and `/validate` workflow references now invoke `sce plan check` with a fallback. This affects `context/sce/shared-context-plan-workflow.md` (checker responsibility split and fallback). `/next-task` is unchanged. - Context synchronization: synced -- [ ] T05: `Document the Pkl/Rust plan-structure behavioral contract` (status:todo) +- [x] T05: `Document the Pkl/Rust plan-structure behavioral contract` (status:done) - Task ID: T05 - Covers: AC6 - Scope: In — new `context/sce/plan-structure-contract.md` covering: the shared rule set; `expectations.tsv` as the single source of truth and how to add a fixture; the D4 result and diagnostic wording; the rule that changing defect text requires both checkers; the known AST-versus-line-scanner divergences (list-nesting indentation tolerance, `[X]` checkboxes, definitions nested inside other list items, multi-line reference values); and that Pkl stays generation-only while the CLI never invokes Pkl. Link it from `context/sce/shared-context-plan-workflow.md` and `context/context-map.md`. Out — code changes. - Dependencies: T04 - Done when: The contract file exists with those sections and is linked from both files. - Verify: inspection of `context/sce/plan-structure-contract.md` and its links. - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `context/sce/plan-structure-contract.md`, `context/sce/shared-context-plan-workflow.md`, `context/context-map.md` + - Result: New `context/sce/plan-structure-contract.md` covers the Pkl and Rust checker roles: Pkl runs generation-only over fixtures, and the CLI never invokes Pkl. It also covers the shared classification, heading, definition, reference, and coverage rules, with template-skeleton mode excluded. It documents `expectations.tsv` as the single source of truth: the row format, the guards in `check-generated.sh` and `plan::fixture_expectations_match_manifest`, and the steps to add a fixture. It records the D4 result and diagnostic wording, and the rule that rewording requires both checkers plus the workflow trust rule. Finally, it lists the known divergences. `shared-context-plan-workflow.md` links the contract from its responsibility-split paragraph, and `context-map.md` gains a contract entry. Approved assumptions: beyond the four divergences the task names (indentation tolerance, `[X]` checkboxes, nested definitions, multi-line values), the list adds two verified from the Pkl regexes and `extract.rs`: Pkl accepts only `-` list markers, and only Rust ignores indented code blocks. + - Verify outcome: Inspection passed. The contract file has the sections for shared rules, fixtures and manifest, wording, and divergences. `grep` confirms the links in `context/sce/shared-context-plan-workflow.md:116` and `context/context-map.md:63`. The cited defect strings match `plan-structure.pkl`, `plan-structure-check.pkl`, and `validate.rs` verbatim. + - Context impact: localized — new durable context file for the Pkl/Rust contract, plus links. No code or behavior change. + - Context synchronization: synced ## Open questions - Should a known AST-versus-line-scanner divergence (D1 trade-off) be fixed in the Pkl checker so the two agree beyond the fixtures, or only documented? The plan documents divergences and pins agreement through fixtures. Tightening `plan-structure.pkl` would be a follow-up. + +## Validation Report + +**Status:** validated +**Date:** 2026-10-24 + +### Commands run + +- `nix flake check` -> exit 1 (first run: `cli-tests` 622 passed, 2 failed — `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline` (child exited after 10.07s) and `services::hooks::commit_hooks::tests::commit_patch_id_is_none_without_warning_for_empty_post_commit`; both outside `services/plan`) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml -- ` -> exit 0 (both passed in isolation) +- `nix flake check` -> exit 0 (rerun: all checks passed) +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 145 files) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` -> exit 0 (14 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml completion` -> exit 0 (2 passed) +- `nix run .#sce -- plan check ` for `valid-structured-plan.md`, `inspection-only-plan.md`, `legacy-plan.md` (relative and absolute) -> exit 0 (classification line on stdout) +- `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/duplicate-id.md` -> exit 3 (`SCE-ERR-VALIDATION`, `…/duplicate-id.md:29: \`S1\` is defined more than once`) +- `nix run .#sce -- plan check does/not/exist.md` -> exit 4 (`SCE-ERR-RUNTIME`, names the path) +- `nix run .#sce -- plan check context/plans/sce-plan-check.md` -> exit 0 (`structured plan: reference integrity holds`) +- `nix run .#sce -- --help`, `nix run .#sce -- plan check --help` -> exit 0 (lists `plan`; shows ``) +- `nix run .#sce -- completion --shell {bash,zsh,fish}` -> exit 0 (each script contains `plan` and `check`) + +### Success-criteria verification + +- [x] AC1: `sce plan check ` validates one file read-only with 0/3/4 outcomes -> `plan::` tests passed; CLI runs gave exit 0 for valid fixtures (relative and absolute), 3 for `duplicate-id.md`, 4 for a missing file; scenarios: S1, S2, S3, S4 +- [x] AC2: Rust and Pkl agree on every fixture from `expectations.tsv` -> `plan::fixture_expectations_match_manifest` passed and `pkl-check-generated` passed from the same manifest; scenarios: S5, S6 +- [x] AC3: Diagnostics carry 1-based `mdast` source lines -> `plan::diagnostic_lines_match_source_positions` passed; scenarios: S7 +- [x] AC4: Help and bash/zsh/fish completion expose `plan check` -> top-level help line 56 `plan`, `plan check --help` shows ``, all three completion scripts contain `plan`/`check`; scenarios: S8 +- [x] AC5: Generated `/change-to-plan` and `/validate` invoke `sce plan check` with trust rule, fallback, legacy skip; `/next-task` untouched -> `nix run .#pkl-check-generated` passed with the extended assertions; scenarios: S9 +- [x] AC6: Pkl/Rust contract documented -> `context/sce/plan-structure-contract.md` has Shared rule set, Shared fixtures and manifest, Result and diagnostic wording, Known divergences; linked from `context/sce/shared-context-plan-workflow.md:116` and `context/context-map.md:63`; scenarios: none (inspection) +- [x] AC7: Only new runtime dependency is `markdown`; no `context/plans/` Nix dependency -> `git diff main -- cli/Cargo.toml` adds only `markdown = "1"` (lock adds `markdown`, transitive `unicode-id`); `flake.nix` unchanged vs `main`; no `context/plans` reference in `flake.nix` or `check-generated.sh`; scenarios: none (inspection) + +### Scenario verification + +- S1 (AC1): test `plan::command_valid_plans_exit_success` + command `nix run .#sce -- plan check` on the three fixtures (relative and absolute) -> test passed (asserts exit 0, exact stdout, empty stderr); CLI printed exact classification lines with exit 0; status: Verified +- S2 (AC1): test `plan::command_invalid_plan_exits_validation` + command `nix run .#sce -- plan check …/duplicate-id.md` -> test passed; CLI exit 3, empty stdout, `SCE-ERR-VALIDATION` and one `…/duplicate-id.md:29: \`S1\` is defined more than once` line; status: Verified +- S3 (AC1): test `plan::command_missing_file_is_runtime_failure` -> passed (exit 4, `SCE-ERR-RUNTIME`, path named, no `{path}:` diagnostics); CLI confirmed exit 4; status: Verified +- S4 (AC1): test `plan::command_does_not_execute_plan_commands` + inspection of `cli/src/services/plan/` for `std::process` -> test passed (exit 0, marker absent); only `std::process::ExitCode` in test code, no `Command` use; status: Verified +- S5 (AC2): test `plan::fixture_expectations_match_manifest` -> passed; status: Verified +- S6 (AC2): command `nix run .#pkl-check-generated` -> exit 0, manifest-driven loop in `config/pkl/check-generated.sh` with unlisted-fixture guard; status: Verified +- S7 (AC3): test `plan::diagnostic_lines_match_source_positions` -> passed; status: Verified +- S8 (AC4): test `plan::plan_check_appears_in_help`, `plan::completion_scripts_include_plan_check` + AC4 commands -> tests passed; help lists `plan`, `` shown, bash/zsh/fish scripts contain `plan` and `check`; status: Verified +- S9 (AC5): command `nix run .#pkl-check-generated` -> exit 0 with extended `plan-authoring-structured-references` and `validate-scenario-evidence` assertions (including the `sce-next-task` exclusion); status: Verified + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- The first `nix flake check` run failed two timing/environment-sensitive tests outside this plan's scope (`app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline`, `services::hooks::commit_hooks::tests::commit_patch_id_is_none_without_warning_for_empty_post_commit`); both passed in isolation and on the full rerun, which points to flakiness under load. +- `sce plan check --help` renders `Usage: check ` rather than the full `sce plan check ` path. +- AST-versus-line-scanner divergences between Rust and Pkl beyond the fixtures remain documented, not fixed (see Open questions). diff --git a/context/sce/plan-structure-contract.md b/context/sce/plan-structure-contract.md new file mode 100644 index 000000000..ff02c3453 --- /dev/null +++ b/context/sce/plan-structure-contract.md @@ -0,0 +1,70 @@ +# Plan Structure Contract (Pkl and Rust checkers) + +Two implementations check the structure and reference integrity of an SCE plan: + +- **Pkl checker** — `config/pkl/renderers/plan-structure.pkl` (`check` path), run through `config/pkl/renderers/plan-structure-check.pkl`. It is the generation-time reference implementation. It runs only inside generation checks (`nix run .#pkl-check-generated`, also part of `nix flake check`), and only against fixtures. +- **Rust checker** — `cli/src/services/plan/` (`extract` then `validate`), shipped as `sce plan check ` (see [plan service](../cli/plan-service.md)). It is the deterministic checker for one selected plan. The distributed `sce` binary and its tests never invoke Pkl, Nix, an LLM, or the network. + +This contract defines what the two must agree on, how that agreement is enforced, and where they are known to differ. + +## Shared rule set + +Both checkers apply the same rules to the plan source outside fenced code blocks: + +- **Classification.** A plan is structured when it has a `### Verification scenarios` heading or any task under `## Task stack` carries `Covers:`. Every other plan is legacy and gets no defects. +- **Headings.** + - `## Acceptance criteria` and `## Task stack` are required. + - `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack` appear at most once each, in that order. + - `### Verification scenarios` sits inside `## Acceptance criteria`. +- **Definitions.** + - `D`, `AC`, `S`, and `T` entries count only in their authoritative section: `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack`, each ending at the next heading. + - IDs are unique. + - Owned fields appear only on their owning kind. The `D` fields are `Boundary`, `Rationale`, `Rejected`, and `Trade-offs`. The `AC` fields are `Validate`, `Decisions`, and `Scenarios`. The `S` fields are `Given`, `When`, `Then`, and `Check`. The `T` field is `Covers`. + - Each definition carries its kind's required fields. +- **References.** + - `Decisions:`, `Scenarios:`, and `Covers:` hold an exact `ID, ID` list. `Scenarios: none (inspection)` is exempt. + - Each listed ID names the right kind and resolves to a definition. + - An undefined ID that appears only outside its section, or only inside a fenced block, gets a hint saying so. +- **Coverage.** Every `AC` is covered by some task's `Covers:`, and every `S` is referenced by some `AC`'s `Scenarios:`. + +Template-skeleton mode (`templateDefects` in `plan-structure.pkl`) validates generated plan templates, not plans. It stays Pkl-only and is outside this contract. + +## Shared fixtures and manifest + +The single source of truth is `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`. It covers the `*.md` fixtures in the same directory. + +- Each row is tab-separated: the fixture file name, then its verdict (`structured`, `legacy`, or `fail`). `fail` rows then list one expected defect substring per column. Rows starting with `#` are comments. +- `config/pkl/check-generated.sh` iterates the manifest through `plan-structure-check.pkl`. `structured` and `legacy` rows must print their classification line, and `fail` rows must fail with every listed substring. The run aborts on any of these: a missing fixture, an unknown verdict, a pass row with substrings, a fail row without substrings, or a `*.md` fixture not listed exactly once. +- The Rust test `plan::fixture_expectations_match_manifest` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. It requires the same verdicts and substrings, and fails on any unlisted fixture. +- To add a fixture: + 1. Add the `.md` file to the directory. + 2. Add its manifest row. + 3. Run both `nix run .#pkl-check-generated` and `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture`. + + A fixture passes only when both checkers agree with its row. Never keep a Rust-only or Pkl-only expectation table. +- Generation and Nix checks read only these fixtures. They never read `context/plans/`. + +## Result and diagnostic wording + +- Valid structured plan: `structured plan: reference integrity holds`. +- Valid legacy plan: `legacy plan: structured reference rules do not apply`. +- Defect messages are the Pkl defect strings, used verbatim by the Rust validator. Examples are ``heading `## Task stack` is missing``, `` `S1` is defined more than once``, and `` `Covers:` on `T01` names `S1`, which is not an acceptance criterion ``. +- `sce plan check` exit and output behavior: + - It exits `0` and prints the classification line on stdout. + - For a structurally invalid plan, it exits `3` (`SCE-ERR-VALIDATION`). Stderr lists one diagnostic per line as `{path}:{line}: {message}`, or `{path}: {message}` for a plan-level defect with no line. Diagnostics are ordered by line, then message. + - It exits `4` (`SCE-ERR-RUNTIME`) for a missing or unreadable file. +- The Pkl checker throws one joined `plan failed the structural reference-integrity check: …` message. Its result strings match the Rust stdout lines exactly. +- Changing a classification line or a defect message is a change to both checkers. Update `plan-structure.pkl`, `cli/src/services/plan/validate.rs` or `command.rs`, any affected manifest substrings, and the workflow trust rule in [plan workflow](shared-context-plan-workflow.md). The trust rule matches the two classification lines. + +## Known divergences + +The Rust extractor follows the CommonMark/GFM `mdast` tree from `markdown-rs`. The Pkl checker scans lines with regexes. They can disagree on inputs outside the fixture set: + +- **List-nesting indentation.** Pkl takes a field only at exactly `entry indent + 2` spaces. Rust takes any item of a list nested directly under the definition, so other indentation that CommonMark still nests is accepted. +- **Checkbox case.** Pkl matches only `[ ]` and `[x]`. With `[X]`, the line is not a definition for Pkl, while GFM accepts `[X]` as checked. +- **Nested definitions.** Pkl accepts a definition line at any indentation inside its section, including one nested in another list item. Rust takes definitions only from items of root-level lists. +- **Multi-line values.** Pkl reads a field value from its first line only. Rust slices the field item's whole first paragraph from source, so a wrapped `Covers:`, `Decisions:`, or `Scenarios:` list keeps its continuation lines. A wrapped list then fails the ID-list grammar in Rust, but may pass in Pkl. +- **List markers.** Pkl accepts only `-` bullets. Rust accepts any GFM list (`-`, `*`, `+`, or ordered). +- **Indented code blocks.** Pkl recognizes only backtick and tilde fences. Rust also ignores indented (four-space) code blocks. + +The fixtures pin the shared behavior. Do not author plans that depend on these divergences. Aligning them, for example by tightening `plan-structure.pkl`, is a possible follow-up, not part of this contract. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 23c7374bd..15e73d74d 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -112,6 +112,8 @@ selected plan, the shipped `sce plan check ` command (see checker stays the generation-time reference implementation and is still runnable with `pkl eval -p planPath= config/pkl/renderers/plan-structure-check.pkl`. +The two checkers share one rule set, wording, and fixture manifest; see the +[plan structure contract](plan-structure-contract.md). `/change-to-plan` (after writing) and `/validate` (step 4, before scenario verification) run `sce plan check {plan-path}` on structured plans only; From 0e0d756c6006a875ef1735d4ccbdaf9ae2bf9d46 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 02:35:24 +0200 Subject: [PATCH 15/61] language+runtime: Report misplaced plan definitions and complete diagnostics Detect top-level definitions placed under the wrong authoritative section and report their source lines consistently in Rust and Pkl validation. Compare exact diagnostic multisets in fixture tests and document the expanded contract. Refresh the Flatpak cargo sources for the parser dependency. Co-authored-by: SCE --- cli/src/services/plan/extract.rs | 52 ++++++++++++---- cli/src/services/plan/mod.rs | 62 +++++++++++++------ cli/src/services/plan/validate.rs | 20 +++++- config/pkl/check-generated.sh | 51 +++++++++++---- .../fixtures/plan-structure-report.pkl | 7 +++ .../fixtures/plan-structure/expectations.tsv | 10 +-- config/pkl/renderers/plan-structure.pkl | 15 ++++- context/cli/plan-service.md | 7 ++- context/sce/plan-structure-contract.md | 7 ++- nix/flatpak/cargo-sources.nix | 2 +- packaging/flatpak/cargo-sources.json | 26 ++++++++ 11 files changed, 202 insertions(+), 57 deletions(-) create mode 100644 config/pkl/renderers/fixtures/plan-structure-report.pkl diff --git a/cli/src/services/plan/extract.rs b/cli/src/services/plan/extract.rs index 3ff03625b..ec3f5e7bb 100644 --- a/cli/src/services/plan/extract.rs +++ b/cli/src/services/plan/extract.rs @@ -57,11 +57,18 @@ pub struct PlanEntry { pub fields: Vec, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OutsideDefinition { + pub id: String, + pub line: usize, + pub section: Option, +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Plan { pub headings: Vec, pub entries: Vec, - pub outside_section_ids: Vec, + pub outside_definitions: Vec, pub fenced_ids: Vec, } @@ -91,11 +98,11 @@ pub fn extract(source: &str) -> Result { for item in node.children().into_iter().flatten() { match section.and_then(|kind| definition(source, item, kind)) { Some(entry) => plan.entries.push(entry), - None => collect_outside_section_ids(source, item, &mut plan), + None => collect_outside_definitions(source, item, section, &mut plan), } } } - _ => collect_outside_section_ids(source, node, &mut plan), + _ => collect_outside_definitions(source, node, None, &mut plan), } } @@ -144,20 +151,29 @@ fn field(source: &str, item: &Node) -> Option { }) } -fn collect_outside_section_ids(source: &str, node: &Node, plan: &mut Plan) { +fn collect_outside_definitions( + source: &str, + node: &Node, + section: Option, + plan: &mut Plan, +) { match node { Node::Code(code) => collect_fenced_ids(&code.value, &mut plan.fenced_ids), Node::ListItem(_) => { if let Some((id, _)) = lead_text(source, node).and_then(parse_id_token) { - plan.outside_section_ids.push(id.to_string()); + plan.outside_definitions.push(OutsideDefinition { + id: id.to_string(), + line: line_of(node), + section, + }); } for child in node.children().into_iter().flatten() { - collect_outside_section_ids(source, child, plan); + collect_outside_definitions(source, child, None, plan); } } _ => { for child in node.children().into_iter().flatten() { - collect_outside_section_ids(source, child, plan); + collect_outside_definitions(source, child, None, plan); } } } @@ -168,8 +184,13 @@ fn collect_fenced_ids(code: &str, ids: &mut Vec) { return; }; let mut nested = Plan::default(); - collect_outside_section_ids(code, &root, &mut nested); - ids.extend(nested.outside_section_ids); + collect_outside_definitions(code, &root, None, &mut nested); + ids.extend( + nested + .outside_definitions + .into_iter() + .map(|outside| outside.id), + ); ids.extend(nested.fenced_ids); } @@ -228,7 +249,7 @@ mod tests { use std::fs; use std::path::PathBuf; - use super::{extract, is_id, Plan, PlanEntryKind}; + use super::{extract, is_id, OutsideDefinition, Plan, PlanEntryKind}; fn fixture(name: &str) -> Plan { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -259,7 +280,7 @@ mod tests { ] ); assert_eq!(plan.fenced_ids, vec!["D1", "AC1", "S1", "T01"]); - assert!(plan.outside_section_ids.is_empty()); + assert!(plan.outside_definitions.is_empty()); } #[test] @@ -329,7 +350,14 @@ mod tests { let plan = fixture("misplaced-definition.md"); assert_eq!(plan.entries_of(PlanEntryKind::Scenario).count(), 0); - assert_eq!(plan.outside_section_ids, vec!["S1"]); + assert_eq!( + plan.outside_definitions, + vec![OutsideDefinition { + id: "S1".to_string(), + line: 21, + section: Some(PlanEntryKind::AcceptanceCriterion), + }] + ); let criterion = plan .entries_of(PlanEntryKind::AcceptanceCriterion) .next() diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 1dc9e6a0e..347b29bf2 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -44,42 +44,39 @@ fn fixture_expectations_match_manifest() { let mut columns = row.split('\t'); let fixture = columns.next().expect("row should name a fixture"); let verdict = columns.next().expect("row should carry a verdict"); - let expected: Vec<&str> = columns.collect(); + let mut expected: Vec<&str> = columns.collect(); let report = validate_fixture(fixture); listed.push(fixture.to_string()); + let mut actual: Vec<&str> = report + .diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect(); + expected.sort_unstable(); + actual.sort_unstable(); match verdict { "structured" | "legacy" => { assert!( expected.is_empty(), - "{fixture}: pass rows take no substrings" + "{fixture}: pass rows take no diagnostics" ); assert_eq!( report.structured, verdict == "structured", "{fixture}: wrong classification" ); - assert!( - report.diagnostics.is_empty(), - "{fixture}: unexpected diagnostics {:?}", - report.diagnostics - ); } "fail" => { - assert!(!expected.is_empty(), "{fixture}: fail rows need substrings"); - for substring in expected { - assert!( - report - .diagnostics - .iter() - .any(|diagnostic| diagnostic.message.contains(substring)), - "{fixture}: missing `{substring}` in {:?}", - report.diagnostics - ); - } + assert!( + !expected.is_empty(), + "{fixture}: fail rows need diagnostics" + ); + assert!(report.structured, "{fixture}: wrong classification"); } other => panic!("{fixture}: unknown verdict `{other}`"), } + assert_eq!(actual, expected, "{fixture}: diagnostics differ"); } let mut fixtures: Vec = std::fs::read_dir(fixture_dir()) @@ -120,6 +117,19 @@ fn diagnostic_lines_match_source_positions() { (Some(37), "`T01` is missing `Covers:`"), ] ); + assert_eq!( + diagnostic_lines(&validate_fixture("misplaced-definition.md")), + vec![ + ( + Some(20), + "`Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios`" + ), + ( + Some(21), + "`S1` is defined under `## Acceptance criteria`; it belongs under `### Verification scenarios`" + ), + ] + ); assert_eq!( diagnostic_lines(&validate_fixture("duplicate-id.md")), vec![(Some(29), "`S1` is defined more than once")] @@ -139,6 +149,22 @@ fn diagnostic_lines_match_source_positions() { ); } +#[test] +fn misplaced_definition_ignores_prose_and_fenced_mentions() { + let source = "# Plan\n\n## Acceptance criteria\n\n- [ ] AC1: Works.\n - Validate: `true`\n - Scenarios: S1\n - S2: nested prose\n\n### Verification scenarios\n\n- S1: Works\n - Given: A.\n - When: B.\n - Then: C.\n - Check: `true`\n\n```markdown\n- T02: fenced example\n```\n\n## Task stack\n\n- [ ] T01: `Work` (status:todo)\n - Covers: AC1\n\n## Open questions\n\n- D1: incidental prose\n"; + + assert!(diagnostic_lines(&validate_source(source)).is_empty()); + assert_eq!( + diagnostic_lines(&validate_source(&source.replace(" - S2: nested prose\n", "- S2: misplaced\n"))), + vec![ + ( + Some(8), + "`S2` is defined under `## Acceptance criteria`; it belongs under `### Verification scenarios`" + ), + ] + ); +} + #[cfg(test)] struct CommandRun { exit_code: std::process::ExitCode, diff --git a/cli/src/services/plan/validate.rs b/cli/src/services/plan/validate.rs index 3157a12ea..9ca753195 100644 --- a/cli/src/services/plan/validate.rs +++ b/cli/src/services/plan/validate.rs @@ -230,11 +230,29 @@ fn definition_diagnostics(plan: &Plan) -> Vec { } } + for outside in &plan.outside_definitions { + if let Some(section) = outside.section { + diagnostics.push(diagnostic( + Some(outside.line), + format!( + "`{}` is defined under `{}`; it belongs under `{}`", + outside.id, + section.authoritative_section(), + PlanEntryKind::of_id(&outside.id).authoritative_section() + ), + )); + } + } + diagnostics } fn undefined_hint(plan: &Plan, id: &str) -> String { - if plan.outside_section_ids.iter().any(|other| other == id) { + if plan + .outside_definitions + .iter() + .any(|outside| outside.id == id) + { format!( "; it appears only outside `{}`", PlanEntryKind::of_id(id).authoritative_section() diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 41a217c26..0ed8233a6 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -202,6 +202,7 @@ expect_pkl_fixture_failure \ "structured example: \`Scenarios:\` on \`AC1\` references undefined \`S2\`" plan_structure_check="config/pkl/renderers/plan-structure-check.pkl" +plan_structure_report="config/pkl/renderers/fixtures/plan-structure-report.pkl" plan_structure_fixtures="config/pkl/renderers/fixtures/plan-structure" expect_plan_structure_pass() { @@ -225,24 +226,46 @@ expect_plan_structure_pass() { expect_plan_structure_failure() { local fixture_name="$1" - shift - local diagnostic - local expected_diagnostic - if diagnostic="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ - "$plan_structure_check" 2>&1 >/dev/null)"; then + if pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ + "$plan_structure_check" >/dev/null 2>&1; then printf 'Negative plan structure fixture unexpectedly passed: %s\n' \ "$fixture_name" >&2 exit 1 fi +} - for expected_diagnostic in "$@"; do - if [[ "$diagnostic" != *"$expected_diagnostic"* ]]; then - printf 'Negative plan structure fixture failed without the expected diagnostic: %s\nexpected: %s\n%s\n' \ - "$fixture_name" "$expected_diagnostic" "$diagnostic" >&2 - exit 1 - fi - done +expect_plan_structure_report() { + local fixture_name="$1" + local expected_classification="$2" + shift 2 + local report + local actual_classification + local actual_diagnostics + local expected_diagnostics + + if ! report="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ + "$plan_structure_report" 2>&1)"; then + printf 'Plan structure report failed to evaluate: %s\n%s\n' \ + "$fixture_name" "$report" >&2 + exit 1 + fi + + actual_classification="$(head -n 1 <<<"$report")" + actual_diagnostics="$(tail -n +2 <<<"$report" | LC_ALL=C sort)" + expected_diagnostics="$(if [[ "$#" -gt 0 ]]; then printf '%s\n' "$@"; fi | LC_ALL=C sort)" + + if [[ "$actual_classification" != "$expected_classification" ]]; then + printf 'Plan structure fixture has the wrong classification: %s\nexpected: %s\nactual: %s\n' \ + "$fixture_name" "$expected_classification" "$actual_classification" >&2 + exit 1 + fi + + if [[ "$actual_diagnostics" != "$expected_diagnostics" ]]; then + printf 'Plan structure fixture diagnostics differ from the manifest: %s\nexpected (sorted):\n%s\nactual (sorted):\n%s\n' \ + "$fixture_name" "$expected_diagnostics" "$actual_diagnostics" >&2 + exit 1 + fi } structured_plan_result="structured plan: reference integrity holds" @@ -277,13 +300,15 @@ while IFS=$'\t' read -r -u 3 -a manifest_columns; do else expect_plan_structure_pass "$fixture_name" "$legacy_plan_result" fi + expect_plan_structure_report "$fixture_name" "$fixture_verdict" ;; fail) if [[ "${#expected_diagnostics[@]}" -eq 0 ]]; then printf 'Plan structure manifest fail row must list diagnostics: %s\n' "$fixture_name" >&2 exit 1 fi - expect_plan_structure_failure "$fixture_name" "${expected_diagnostics[@]}" + expect_plan_structure_failure "$fixture_name" + expect_plan_structure_report "$fixture_name" structured "${expected_diagnostics[@]}" ;; *) printf 'Plan structure manifest has an unknown verdict for %s: %s\n' \ diff --git a/config/pkl/renderers/fixtures/plan-structure-report.pkl b/config/pkl/renderers/fixtures/plan-structure-report.pkl new file mode 100644 index 000000000..e2080fcab --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure-report.pkl @@ -0,0 +1,7 @@ +import "../plan-structure.pkl" as planStructure + +local report = planStructure.check(read("file:\(read("prop:planPath"))").text) + +output { + text = (List(if (report.structured) "structured" else "legacy") + report.defects).join("\n") + "\n" +} diff --git a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv index e8385ba87..16d1a2a46 100644 --- a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv +++ b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv @@ -1,18 +1,18 @@ -# fixture verdict (structured | legacy | fail) expected diagnostic substrings (fail only, one per column) +# fixture verdict (structured | legacy | fail) complete expected diagnostic messages (fail only, one per column, order-insensitive, duplicates counted) duplicate-id.md fail `S1` is defined more than once fenced-example-plan.md structured inspection-only-plan.md structured keywords-only-in-examples.md fail `Scenarios:` on `AC1` references undefined `S1`; it appears only inside a fenced code block legacy-plan-with-fenced-structured-example.md legacy legacy-plan.md legacy -misplaced-definition.md fail `Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios` -misplaced-field.md fail `Check:` on `D1` is misplaced; it belongs to a verification scenario `Covers:` on `AC1` is misplaced; it belongs to a task -missing-scenarios-field.md fail `AC1` is missing `Scenarios:` +misplaced-definition.md fail `S1` is defined under `## Acceptance criteria`; it belongs under `### Verification scenarios` `Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios` +misplaced-field.md fail `Check:` on `D1` is misplaced; it belongs to a verification scenario `Covers:` on `AC1` is misplaced; it belongs to a task `S1` is missing `Check:` `T01` is missing `Covers:` `AC1` is not covered by any task's `Covers:` +missing-scenarios-field.md fail `AC1` is missing `Scenarios:` `S1` is not referenced by any acceptance criterion's `Scenarios:` task-empty-covers.md fail `Covers:` on `T02` is not a comma-separated ID list task-missing-covers.md fail `T02` is missing `Covers:` uncovered-ac.md fail `AC2` is not covered by any task's `Covers:` undefined-references.md fail `Decisions:` on `AC1` references undefined `D2` `Scenarios:` on `AC1` references undefined `S9` `Covers:` on `T01` references undefined `AC3` unreferenced-scenario.md fail `S2` is not referenced by any acceptance criterion's `Scenarios:` valid-structured-plan.md structured -wrong-heading-context.md fail heading `### Verification scenarios` is not inside `## Acceptance criteria` +wrong-heading-context.md fail heading `## Task stack` must come after `### Verification scenarios` heading `### Verification scenarios` is not inside `## Acceptance criteria` wrong-reference-kind.md fail `Covers:` on `T01` names `S1`, which is not an acceptance criterion `AC1` is not covered by any task's `Covers:` diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl index 246f6615a..e036ec928 100644 --- a/config/pkl/renderers/plan-structure.pkl +++ b/config/pkl/renderers/plan-structure.pkl @@ -224,6 +224,19 @@ local function definitionDefects(defined: List): List = .map((key) -> "`\(entry.id)` is missing `\(key):`") ) +local function misplacedDefinitionDefects(planLines: List): List = + planLines + .map((line) -> if (line.fenced) null else definitionPattern.matchEntire(line.text)) + .zip(planLines) + .filter((pair) -> + pair.first != null + && pair.first.groups[1]!!.value.isEmpty + && authoritativeSections.values.contains(pair.second.section) + && pair.second.section != authoritativeSections[kindOf(pair.first.groups[2]!!.value)]) + .map((pair) -> + let (id = pair.first!!.groups[2]!!.value) + "`\(id)` is defined under `\(pair.second.section)`; it belongs under `\(authoritativeSections[kindOf(id)])`") + local function undefinedHint(planLines: List, id: String): String = let (candidates = planLines.filter((line) -> let (match = definitionPattern.matchEntire(line.text)) @@ -284,7 +297,7 @@ local function inspect(text: String, skeleton: Boolean): PlanReport = if (skeleton) templateSkeletonHeadings else orderedPlanHeadings ) + definitionDefects(defined) - + (if (skeleton) List() else referenceDefects(planLines, defined)) + + (if (skeleton) List() else misplacedDefinitionDefects(planLines) + referenceDefects(planLines, defined)) } function check(text: String): PlanReport = inspect(text, false) diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index eed07d330..a99d302b2 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -13,7 +13,7 @@ - `Plan.headings`: one `PlanHeading { depth, text, line }` per root-level heading, where `text` is the heading's plain text. - `Plan.entries`: one `PlanEntry { id, kind, checked, line, fields }` per definition, in source order. Use `Plan::entries_of(kind)` to select one kind. `PlanEntryKind` is one of `Decision`, `AcceptanceCriterion`, `Scenario`, or `Task`. - `PlanField { key, value, line }`: one per directly nested list item whose first paragraph starts with `Key:`. The key is `[A-Z][A-Za-z -]*`, followed by whitespace or the end of the text. `value` is the trimmed source slice after the colon, so inline code and punctuation are kept verbatim. -- `Plan.outside_section_ids` and `Plan.fenced_ids` hold the IDs of definition-shaped list items that are not definitions. The first lists items found outside an authoritative position. The second lists items found inside `Code` blocks, which are re-parsed with `to_mdast` rather than scanned by line. They support undefined-reference hints and never create entries. +- `Plan.outside_definitions` and `Plan.fenced_ids` hold definition-shaped list items that are not definitions. The first holds `OutsideDefinition { id, line, section }` for items found outside an authoritative position. `section` is set only for a top-level list item under an authoritative section of another kind, which marks a definition in the wrong section rather than nested or unrelated prose. The second lists items found inside `Code` blocks, which are re-parsed with `to_mdast` rather than scanned by line. They support undefined-reference hints and never create entries. - Lines are 1-based and come from `mdast` positions. ## Extraction rules @@ -39,7 +39,8 @@ - Reference rules: - `Decisions:`, `Scenarios:`, and `Covers:` on their owning kind must be an exact `ID, ID` list. `Scenarios: none (inspection)` is exempt. - Each listed ID must name the right kind and resolve to a definition. - - An unresolved ID gets an `outside_section_ids` or `fenced_ids` hint. + - An unresolved ID gets an `outside_definitions` or `fenced_ids` hint. + - An `OutsideDefinition` with a `section` is reported at its own line as defined under the wrong section. - Every AC must be covered by some task's `Covers:`, and every S must be referenced by some AC's `Scenarios:`. - `PlanReport.diagnostics` holds `PlanDiagnostic { line, message }` values, sorted by `(line, message)`. Line-less diagnostics come first, and duplicates are kept. - Where a diagnostic's line comes from: @@ -63,7 +64,7 @@ ## Terms -- `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `structured`, `legacy`, or `fail`, then the expected defect substrings for `fail` rows. Every `*.md` fixture in the directory must be listed exactly once. +- `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `structured`, `legacy`, or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. - `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. diff --git a/context/sce/plan-structure-contract.md b/context/sce/plan-structure-contract.md index ff02c3453..8a165183c 100644 --- a/context/sce/plan-structure-contract.md +++ b/context/sce/plan-structure-contract.md @@ -19,6 +19,7 @@ Both checkers apply the same rules to the plan source outside fenced code blocks - **Definitions.** - `D`, `AC`, `S`, and `T` entries count only in their authoritative section: `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack`, each ending at the next heading. - IDs are unique. + - A top-level list item that defines an ID under another authoritative section is reported at its own line as defined under the wrong section. Nested items, prose outside authoritative sections, and fenced examples are not. - Owned fields appear only on their owning kind. The `D` fields are `Boundary`, `Rationale`, `Rejected`, and `Trade-offs`. The `AC` fields are `Validate`, `Decisions`, and `Scenarios`. The `S` fields are `Given`, `When`, `Then`, and `Check`. The `T` field is `Covers`. - Each definition carries its kind's required fields. - **References.** @@ -33,9 +34,9 @@ Template-skeleton mode (`templateDefects` in `plan-structure.pkl`) validates gen The single source of truth is `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`. It covers the `*.md` fixtures in the same directory. -- Each row is tab-separated: the fixture file name, then its verdict (`structured`, `legacy`, or `fail`). `fail` rows then list one expected defect substring per column. Rows starting with `#` are comments. -- `config/pkl/check-generated.sh` iterates the manifest through `plan-structure-check.pkl`. `structured` and `legacy` rows must print their classification line, and `fail` rows must fail with every listed substring. The run aborts on any of these: a missing fixture, an unknown verdict, a pass row with substrings, a fail row without substrings, or a `*.md` fixture not listed exactly once. -- The Rust test `plan::fixture_expectations_match_manifest` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. It requires the same verdicts and substrings, and fails on any unlisted fixture. +- Each row is tab-separated: the fixture file name, then its verdict (`structured`, `legacy`, or `fail`). `fail` rows then list the complete expected defect messages, one per column. Comparison is exact on the sorted multiset of messages: missing, unexpected, or miscounted duplicate defects fail. Source lines are not part of the manifest; Rust tests assert them separately. Rows starting with `#` are comments. +- `config/pkl/check-generated.sh` iterates the manifest. `structured` and `legacy` rows must print their classification line through `plan-structure-check.pkl`, and `fail` rows must make it fail. Every row is then compared against the structured report from `config/pkl/renderers/fixtures/plan-structure-report.pkl`: classification on the first line, then one defect per line. The run aborts on any of these: a missing fixture, an unknown verdict, a pass row with diagnostics, a fail row without diagnostics, a classification or diagnostic-set mismatch, or a `*.md` fixture not listed exactly once. +- The Rust test `plan::fixture_expectations_match_manifest` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. It requires the same verdicts and the same exact sorted message multiset, and fails on any unlisted fixture. - To add a fixture: 1. Add the `.md` file to the directory. 2. Add its manifest row. diff --git a/nix/flatpak/cargo-sources.nix b/nix/flatpak/cargo-sources.nix index 4ce8be9c2..6a9d90c7d 100644 --- a/nix/flatpak/cargo-sources.nix +++ b/nix/flatpak/cargo-sources.nix @@ -33,7 +33,7 @@ let outputHashMode = "flat"; outputHashAlgo = "sha256"; - outputHash = "sha256-mbCiMc2PkUtE+9++SUOC4tGJsxFjeXX+ompwbE7vE6o="; + outputHash = "sha256-kt/cMwaeB3+MHssAy/J1B7S6qsDDxZTyhX9fwIDQ3NQ="; }; regenerateApp = pkgs.writeShellApplication { diff --git a/packaging/flatpak/cargo-sources.json b/packaging/flatpak/cargo-sources.json index 04c9ef38c..f4ffb00bb 100644 --- a/packaging/flatpak/cargo-sources.json +++ b/packaging/flatpak/cargo-sources.json @@ -3314,6 +3314,19 @@ "dest": "cargo/vendor/lz4_flex-0.13.1", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/markdown/markdown-1.0.0.crate", + "sha256": "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb", + "dest": "cargo/vendor/markdown-1.0.0" + }, + { + "type": "inline", + "contents": "{\"package\": \"a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb\", \"files\": {}}", + "dest": "cargo/vendor/markdown-1.0.0", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -6174,6 +6187,19 @@ "dest": "cargo/vendor/unicode-general-category-1.1.0", "dest-filename": ".cargo-checksum.json" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/unicode-id/unicode-id-0.3.7.crate", + "sha256": "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392", + "dest": "cargo/vendor/unicode-id-0.3.7" + }, + { + "type": "inline", + "contents": "{\"package\": \"65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392\", \"files\": {}}", + "dest": "cargo/vendor/unicode-id-0.3.7", + "dest-filename": ".cargo-checksum.json" + }, { "type": "archive", "archive-type": "tar-gzip", From 7c6dde7c6d054fc1473b8aafa2fe014e61f9cc77 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 13:43:52 +0200 Subject: [PATCH 16/61] plan: Require structured plans and remove legacy handling Make every plan use the structured reference-integrity rules, with the CLI and Pkl validators rejecting legacy-format plans and their fixtures and contracts using the pass/fail model. Update generated workflow guidance and target skill documents so scenarios and task coverage are required consistently. Co-authored-by: SCE --- .../references/plan-authoring.md | 17 +++++--- .../references/plan-template.md | 9 +--- .agents/skills/sce-next-task/SKILL.md | 2 +- .../skills/sce-next-task/references/output.md | 1 - .../sce-next-task/references/plan-review.md | 15 ++----- .../references/task-execution.md | 2 +- .../references/validation-report.md | 12 +++--- .../sce-validate/references/validation.md | 26 +++++------ .../references/plan-authoring.md | 17 +++++--- .../references/plan-template.md | 9 +--- .claude/skills/sce-next-task/SKILL.md | 2 +- .../skills/sce-next-task/references/output.md | 1 - .../sce-next-task/references/plan-review.md | 15 ++----- .../references/task-execution.md | 2 +- .../references/validation-report.md | 12 +++--- .../sce-validate/references/validation.md | 26 +++++------ .../references/plan-authoring.md | 17 +++++--- .../references/plan-template.md | 9 +--- .opencode/skills/sce-next-task/SKILL.md | 2 +- .../skills/sce-next-task/references/output.md | 1 - .../sce-next-task/references/plan-review.md | 15 ++----- .../references/task-execution.md | 2 +- .../references/validation-report.md | 12 +++--- .../sce-validate/references/validation.md | 26 +++++------ .../references/plan-authoring.md | 17 +++++--- .../references/plan-template.md | 9 +--- .pi/skills/sce-next-task/SKILL.md | 2 +- .pi/skills/sce-next-task/references/output.md | 1 - .../sce-next-task/references/plan-review.md | 15 ++----- .../references/task-execution.md | 2 +- .../references/validation-report.md | 12 +++--- .../sce-validate/references/validation.md | 26 +++++------ cli/src/services/plan/command.rs | 8 +--- cli/src/services/plan/extract.rs | 4 +- cli/src/services/plan/mod.rs | 42 ++++++++++-------- cli/src/services/plan/validate.rs | 20 +-------- config/pkl/base/workflow-change-to-plan.pkl | 24 +++-------- config/pkl/base/workflow-content.pkl | 2 +- config/pkl/base/workflow-next-task.pkl | 23 +++------- config/pkl/base/workflow-validate.pkl | 34 +++++---------- config/pkl/check-generated.sh | 39 ++++++----------- .../next-task-structured-references-check.pkl | 4 +- ...-authoring-structured-references-check.pkl | 2 +- .../fixtures/plan-structure-report.pkl | 2 +- .../fixtures/plan-structure/expectations.tsv | 12 +++--- ...ed-plan-with-fenced-structured-example.md} | 2 +- .../{legacy-plan.md => unstructured-plan.md} | 0 .../validate-scenario-evidence-check.pkl | 2 +- ...lidation-report-scenario-section-check.pkl | 2 +- .../renderers/generation-contract-check.pkl | 43 +++++++------------ config/pkl/renderers/plan-structure-check.pkl | 7 +-- config/pkl/renderers/plan-structure.pkl | 25 ++++------- context/cli/plan-service.md | 9 ++-- context/context-map.md | 2 +- context/sce/config-generation.md | 2 +- context/sce/plan-structure-contract.md | 11 +++-- context/sce/shared-context-code-workflow.md | 11 +++-- context/sce/shared-context-plan-workflow.md | 19 ++++---- context/sce/workflow-vocabulary.md | 6 +-- 59 files changed, 277 insertions(+), 416 deletions(-) rename config/pkl/renderers/fixtures/plan-structure/{legacy-plan-with-fenced-structured-example.md => unstructured-plan-with-fenced-structured-example.md} (91%) rename config/pkl/renderers/fixtures/plan-structure/{legacy-plan.md => unstructured-plan.md} (100%) diff --git a/.agents/skills/sce-change-to-plan/references/plan-authoring.md b/.agents/skills/sce-change-to-plan/references/plan-authoring.md index ef084bad3..3bd556c00 100644 --- a/.agents/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.agents/skills/sce-change-to-plan/references/plan-authoring.md @@ -34,10 +34,7 @@ Read its `Acceptance criteria rules`, `Design decision rules`, authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, -then verification scenarios, then tasks. When revising a legacy plan, skip the -design decision, verification scenario, `Covers:`, and reference-check work -unless the `Updating an existing plan` rules let the correction adopt the -structured format. +then verification scenarios, then tasks. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -200,7 +197,7 @@ by completed tasks, set internal status `blocked` with category ## 2.9 Check references -Before writing a structured plan, run a reference-consistency pass over the +Before writing the plan, run a reference-consistency pass over the complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. @@ -210,6 +207,16 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. +After writing the plan, run `sce plan check {plan-path}`. When it reports +diagnostics, fix each reported defect in the written plan and run it again. Do +not set internal status `plan_ready` until the checker passes. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; apply the +manual `Reference rules` pass from step 2.9 to the written plan instead, and +record in the open questions that the checker was unavailable. + ## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.agents/skills/sce-change-to-plan/references/plan-template.md b/.agents/skills/sce-change-to-plan/references/plan-template.md index 5a5fa0fe9..e0a7a4c06 100644 --- a/.agents/skills/sce-change-to-plan/references/plan-template.md +++ b/.agents/skills/sce-change-to-plan/references/plan-template.md @@ -234,10 +234,6 @@ invent one: `None.` is the expected answer for a well-specified change.} tildes to its closing fence. Headings, definitions, and fields inside it are example text, not plan structure. Keep fenced examples in place; never delete one to make a plan read correctly. -- A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` - carries `Covers:`. Every other plan is a legacy plan, and the remaining rules - in this section do not apply to it. - A section runs from its heading to the next heading of any level outside fenced code blocks. A definition counts only in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` @@ -266,7 +262,7 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. -- Every task in a structured plan carries `Covers:` naming at least one +- Every task carries `Covers:` naming at least one acceptance criterion defined in the plan. A task without `Covers:` or with an empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. @@ -316,7 +312,4 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. -- A legacy plan stays legacy. Add design decisions, verification scenarios, or - `Covers:` only when the correction explicitly asks to adopt the structured - format. - Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.agents/skills/sce-next-task/SKILL.md b/.agents/skills/sce-next-task/SKILL.md index b4dd35aba..20d4f1358 100644 --- a/.agents/skills/sce-next-task/SKILL.md +++ b/.agents/skills/sce-next-task/SKILL.md @@ -93,7 +93,7 @@ selected or resumed, before allowing a new implementation task to start. A missing field, or any value other than `synced`, is unresolved synchronization debt. Never infer `synced` from conversation history. When the debt-carrying task has no durable completed-task record, the **Plan review phase** returns -`blocked` directly with a legacy-migration required action; otherwise it returns +`blocked` directly with a plan-migration required action; otherwise it returns `sync_debt` with the resolved plan path, debt task ID/title, completed task record, and persisted blocker when present, for the branch above to route. diff --git a/.agents/skills/sce-next-task/references/output.md b/.agents/skills/sce-next-task/references/output.md index 8ca87b94f..bf897f69b 100644 --- a/.agents/skills/sce-next-task/references/output.md +++ b/.agents/skills/sce-next-task/references/output.md @@ -115,7 +115,6 @@ Describe the smallest coherent implementation approach with enough detail for ap ## Acceptance coverage -- Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. diff --git a/.agents/skills/sce-next-task/references/plan-review.md b/.agents/skills/sce-next-task/references/plan-review.md index d01d3bffe..6d6c4943e 100644 --- a/.agents/skills/sce-next-task/references/plan-review.md +++ b/.agents/skills/sce-next-task/references/plan-review.md @@ -29,8 +29,8 @@ any value other than `synced`, is unresolved synchronization debt. Never infer For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating - that structure, or an incomplete write), do not attempt a reconstructed retry. + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. Set internal status `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -80,12 +80,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; review it exactly as -described here, with no coverage lookup and no coverage fields in the result. - -For a structured plan, read the selected task's `Covers:` IDs. For each covered +Read the selected task's `Covers:` IDs. For each covered acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the scenario and decision definitions they name. A covered ID or referenced ID with no definition in the plan is a dangling reference. Record it as a review risk; @@ -145,7 +140,7 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. -For a structured plan, a `ready` result also identifies: +A `ready` result also identifies: - The covered acceptance criteria: each `Covers:` ID with its outcome and `Validate:` check. @@ -153,8 +148,6 @@ For a structured plan, a `ready` result also identifies: - Their related design decisions: each decision ID with its decision text. - Every dangling reference, as a review risk. -A `ready` result for a legacy plan carries none of these coverage fields. - A `sync_debt` result must identify: - The resolved plan path. diff --git a/.agents/skills/sce-next-task/references/task-execution.md b/.agents/skills/sce-next-task/references/task-execution.md index ab9efcc23..4c92e6a70 100644 --- a/.agents/skills/sce-next-task/references/task-execution.md +++ b/.agents/skills/sce-next-task/references/task-execution.md @@ -132,7 +132,7 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. -For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +The covered scenarios' `Then:` and `Check:` may shape the task's own `Verify` checks. Running a scenario's check during the task is task-level evidence only. Task execution never checks an acceptance-criterion checkbox and never records a scenario status; only `/validate` verifies diff --git a/.agents/skills/sce-validate/references/validation-report.md b/.agents/skills/sce-validate/references/validation-report.md index 436ec92d3..f3ef7379f 100644 --- a/.agents/skills/sce-validate/references/validation-report.md +++ b/.agents/skills/sce-validate/references/validation-report.md @@ -36,8 +36,6 @@ writes it. ### Scenario verification -{Only for a structured plan:} - - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -67,15 +65,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. For a structured - plan, end each acceptance-criterion line with the scenario IDs its + not independently re-evaluate or reclassify criteria here. End each + acceptance-criterion line with the scenario IDs its `Scenarios:` field names, or `none (inspection)`. -- Include **Scenario verification** only for a structured plan, with exactly - one entry per scenario in plan order. Each entry gives the scenario ID, the +- Include **Scenario verification** with exactly one entry per scenario in + plan order. Each entry gives the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status already established by validation - execution. Omit the section for a legacy plan. + execution. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.agents/skills/sce-validate/references/validation.md b/.agents/skills/sce-validate/references/validation.md index 1b2c50466..2bc27bb88 100644 --- a/.agents/skills/sce-validate/references/validation.md +++ b/.agents/skills/sce-validate/references/validation.md @@ -63,13 +63,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; validate it exactly -as described here, with no scenario collection, no reference check, and no -scenario statuses. - -For a structured plan, also collect: +Also collect: - Every design decision. - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. @@ -85,9 +79,16 @@ fallback under notes on a `validated` or `failed` result. ### 4. Check plan references -Skip this step for a legacy plan. +Run `sce plan check {plan-path}` before scenario +verification. Record each reported diagnostic as a failed check under +**Failed checks and follow-ups**, quoting its line and message. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; record +that fallback under notes and apply the manual pass below instead. -For a structured plan, apply every entry of the plan template's +For the manual pass, apply every entry of the plan template's `Reference rules` to the plan. Each of these is a relationship-integrity defect: @@ -112,7 +113,7 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. -For a structured plan, run or perform every scenario's `Check:` yourself in +Run or perform every scenario's `Check:` yourself in this run, and compare the observed result with the scenario's `Then:`. Give each scenario exactly one status: @@ -127,7 +128,7 @@ Only evidence gathered by this run counts. Task completion, a task's recorded evidence. Record every `Partially verified` or `Not verified` scenario as a failed check with the gap and a required action. -In a structured plan, an acceptance criterion is met only when its `Validate:` +An acceptance criterion is met only when its `Validate:` check passes and every scenario its `Scenarios:` names is `Verified`. An acceptance criterion marked `none (inspection)` is met by its `Validate:` check alone. @@ -375,14 +376,13 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. -- For a structured plan, the validated and failed variants include **Scenario +- The validated and failed variants include **Scenario verification** with exactly one entry per scenario in plan order: the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status. Each acceptance-criterion line cites its scenario IDs, or `none (inspection)`. In the failed variant, list every `Partially verified` or `Not verified` scenario under **What failed**. -- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.claude/skills/sce-change-to-plan/references/plan-authoring.md b/.claude/skills/sce-change-to-plan/references/plan-authoring.md index ef084bad3..3bd556c00 100644 --- a/.claude/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.claude/skills/sce-change-to-plan/references/plan-authoring.md @@ -34,10 +34,7 @@ Read its `Acceptance criteria rules`, `Design decision rules`, authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, -then verification scenarios, then tasks. When revising a legacy plan, skip the -design decision, verification scenario, `Covers:`, and reference-check work -unless the `Updating an existing plan` rules let the correction adopt the -structured format. +then verification scenarios, then tasks. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -200,7 +197,7 @@ by completed tasks, set internal status `blocked` with category ## 2.9 Check references -Before writing a structured plan, run a reference-consistency pass over the +Before writing the plan, run a reference-consistency pass over the complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. @@ -210,6 +207,16 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. +After writing the plan, run `sce plan check {plan-path}`. When it reports +diagnostics, fix each reported defect in the written plan and run it again. Do +not set internal status `plan_ready` until the checker passes. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; apply the +manual `Reference rules` pass from step 2.9 to the written plan instead, and +record in the open questions that the checker was unavailable. + ## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.claude/skills/sce-change-to-plan/references/plan-template.md b/.claude/skills/sce-change-to-plan/references/plan-template.md index 5a5fa0fe9..e0a7a4c06 100644 --- a/.claude/skills/sce-change-to-plan/references/plan-template.md +++ b/.claude/skills/sce-change-to-plan/references/plan-template.md @@ -234,10 +234,6 @@ invent one: `None.` is the expected answer for a well-specified change.} tildes to its closing fence. Headings, definitions, and fields inside it are example text, not plan structure. Keep fenced examples in place; never delete one to make a plan read correctly. -- A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` - carries `Covers:`. Every other plan is a legacy plan, and the remaining rules - in this section do not apply to it. - A section runs from its heading to the next heading of any level outside fenced code blocks. A definition counts only in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` @@ -266,7 +262,7 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. -- Every task in a structured plan carries `Covers:` naming at least one +- Every task carries `Covers:` naming at least one acceptance criterion defined in the plan. A task without `Covers:` or with an empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. @@ -316,7 +312,4 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. -- A legacy plan stays legacy. Add design decisions, verification scenarios, or - `Covers:` only when the correction explicitly asks to adopt the structured - format. - Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.claude/skills/sce-next-task/SKILL.md b/.claude/skills/sce-next-task/SKILL.md index 2806e3a51..282f228df 100644 --- a/.claude/skills/sce-next-task/SKILL.md +++ b/.claude/skills/sce-next-task/SKILL.md @@ -92,7 +92,7 @@ selected or resumed, before allowing a new implementation task to start. A missing field, or any value other than `synced`, is unresolved synchronization debt. Never infer `synced` from conversation history. When the debt-carrying task has no durable completed-task record, the **Plan review phase** returns -`blocked` directly with a legacy-migration required action; otherwise it returns +`blocked` directly with a plan-migration required action; otherwise it returns `sync_debt` with the resolved plan path, debt task ID/title, completed task record, and persisted blocker when present, for the branch above to route. diff --git a/.claude/skills/sce-next-task/references/output.md b/.claude/skills/sce-next-task/references/output.md index 8ca87b94f..bf897f69b 100644 --- a/.claude/skills/sce-next-task/references/output.md +++ b/.claude/skills/sce-next-task/references/output.md @@ -115,7 +115,6 @@ Describe the smallest coherent implementation approach with enough detail for ap ## Acceptance coverage -- Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. diff --git a/.claude/skills/sce-next-task/references/plan-review.md b/.claude/skills/sce-next-task/references/plan-review.md index d01d3bffe..6d6c4943e 100644 --- a/.claude/skills/sce-next-task/references/plan-review.md +++ b/.claude/skills/sce-next-task/references/plan-review.md @@ -29,8 +29,8 @@ any value other than `synced`, is unresolved synchronization debt. Never infer For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating - that structure, or an incomplete write), do not attempt a reconstructed retry. + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. Set internal status `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -80,12 +80,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; review it exactly as -described here, with no coverage lookup and no coverage fields in the result. - -For a structured plan, read the selected task's `Covers:` IDs. For each covered +Read the selected task's `Covers:` IDs. For each covered acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the scenario and decision definitions they name. A covered ID or referenced ID with no definition in the plan is a dangling reference. Record it as a review risk; @@ -145,7 +140,7 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. -For a structured plan, a `ready` result also identifies: +A `ready` result also identifies: - The covered acceptance criteria: each `Covers:` ID with its outcome and `Validate:` check. @@ -153,8 +148,6 @@ For a structured plan, a `ready` result also identifies: - Their related design decisions: each decision ID with its decision text. - Every dangling reference, as a review risk. -A `ready` result for a legacy plan carries none of these coverage fields. - A `sync_debt` result must identify: - The resolved plan path. diff --git a/.claude/skills/sce-next-task/references/task-execution.md b/.claude/skills/sce-next-task/references/task-execution.md index ab9efcc23..4c92e6a70 100644 --- a/.claude/skills/sce-next-task/references/task-execution.md +++ b/.claude/skills/sce-next-task/references/task-execution.md @@ -132,7 +132,7 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. -For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +The covered scenarios' `Then:` and `Check:` may shape the task's own `Verify` checks. Running a scenario's check during the task is task-level evidence only. Task execution never checks an acceptance-criterion checkbox and never records a scenario status; only `/validate` verifies diff --git a/.claude/skills/sce-validate/references/validation-report.md b/.claude/skills/sce-validate/references/validation-report.md index 436ec92d3..f3ef7379f 100644 --- a/.claude/skills/sce-validate/references/validation-report.md +++ b/.claude/skills/sce-validate/references/validation-report.md @@ -36,8 +36,6 @@ writes it. ### Scenario verification -{Only for a structured plan:} - - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -67,15 +65,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. For a structured - plan, end each acceptance-criterion line with the scenario IDs its + not independently re-evaluate or reclassify criteria here. End each + acceptance-criterion line with the scenario IDs its `Scenarios:` field names, or `none (inspection)`. -- Include **Scenario verification** only for a structured plan, with exactly - one entry per scenario in plan order. Each entry gives the scenario ID, the +- Include **Scenario verification** with exactly one entry per scenario in + plan order. Each entry gives the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status already established by validation - execution. Omit the section for a legacy plan. + execution. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.claude/skills/sce-validate/references/validation.md b/.claude/skills/sce-validate/references/validation.md index 1b2c50466..2bc27bb88 100644 --- a/.claude/skills/sce-validate/references/validation.md +++ b/.claude/skills/sce-validate/references/validation.md @@ -63,13 +63,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; validate it exactly -as described here, with no scenario collection, no reference check, and no -scenario statuses. - -For a structured plan, also collect: +Also collect: - Every design decision. - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. @@ -85,9 +79,16 @@ fallback under notes on a `validated` or `failed` result. ### 4. Check plan references -Skip this step for a legacy plan. +Run `sce plan check {plan-path}` before scenario +verification. Record each reported diagnostic as a failed check under +**Failed checks and follow-ups**, quoting its line and message. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; record +that fallback under notes and apply the manual pass below instead. -For a structured plan, apply every entry of the plan template's +For the manual pass, apply every entry of the plan template's `Reference rules` to the plan. Each of these is a relationship-integrity defect: @@ -112,7 +113,7 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. -For a structured plan, run or perform every scenario's `Check:` yourself in +Run or perform every scenario's `Check:` yourself in this run, and compare the observed result with the scenario's `Then:`. Give each scenario exactly one status: @@ -127,7 +128,7 @@ Only evidence gathered by this run counts. Task completion, a task's recorded evidence. Record every `Partially verified` or `Not verified` scenario as a failed check with the gap and a required action. -In a structured plan, an acceptance criterion is met only when its `Validate:` +An acceptance criterion is met only when its `Validate:` check passes and every scenario its `Scenarios:` names is `Verified`. An acceptance criterion marked `none (inspection)` is met by its `Validate:` check alone. @@ -375,14 +376,13 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. -- For a structured plan, the validated and failed variants include **Scenario +- The validated and failed variants include **Scenario verification** with exactly one entry per scenario in plan order: the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status. Each acceptance-criterion line cites its scenario IDs, or `none (inspection)`. In the failed variant, list every `Partially verified` or `Not verified` scenario under **What failed**. -- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md index ef084bad3..3bd556c00 100644 --- a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md @@ -34,10 +34,7 @@ Read its `Acceptance criteria rules`, `Design decision rules`, authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, -then verification scenarios, then tasks. When revising a legacy plan, skip the -design decision, verification scenario, `Covers:`, and reference-check work -unless the `Updating an existing plan` rules let the correction adopt the -structured format. +then verification scenarios, then tasks. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -200,7 +197,7 @@ by completed tasks, set internal status `blocked` with category ## 2.9 Check references -Before writing a structured plan, run a reference-consistency pass over the +Before writing the plan, run a reference-consistency pass over the complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. @@ -210,6 +207,16 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. +After writing the plan, run `sce plan check {plan-path}`. When it reports +diagnostics, fix each reported defect in the written plan and run it again. Do +not set internal status `plan_ready` until the checker passes. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; apply the +manual `Reference rules` pass from step 2.9 to the written plan instead, and +record in the open questions that the checker was unavailable. + ## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.opencode/skills/sce-change-to-plan/references/plan-template.md b/.opencode/skills/sce-change-to-plan/references/plan-template.md index 5a5fa0fe9..e0a7a4c06 100644 --- a/.opencode/skills/sce-change-to-plan/references/plan-template.md +++ b/.opencode/skills/sce-change-to-plan/references/plan-template.md @@ -234,10 +234,6 @@ invent one: `None.` is the expected answer for a well-specified change.} tildes to its closing fence. Headings, definitions, and fields inside it are example text, not plan structure. Keep fenced examples in place; never delete one to make a plan read correctly. -- A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` - carries `Covers:`. Every other plan is a legacy plan, and the remaining rules - in this section do not apply to it. - A section runs from its heading to the next heading of any level outside fenced code blocks. A definition counts only in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` @@ -266,7 +262,7 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. -- Every task in a structured plan carries `Covers:` naming at least one +- Every task carries `Covers:` naming at least one acceptance criterion defined in the plan. A task without `Covers:` or with an empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. @@ -316,7 +312,4 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. -- A legacy plan stays legacy. Add design decisions, verification scenarios, or - `Covers:` only when the correction explicitly asks to adopt the structured - format. - Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.opencode/skills/sce-next-task/SKILL.md b/.opencode/skills/sce-next-task/SKILL.md index 70b87f4f0..99d5368c8 100644 --- a/.opencode/skills/sce-next-task/SKILL.md +++ b/.opencode/skills/sce-next-task/SKILL.md @@ -92,7 +92,7 @@ selected or resumed, before allowing a new implementation task to start. A missing field, or any value other than `synced`, is unresolved synchronization debt. Never infer `synced` from conversation history. When the debt-carrying task has no durable completed-task record, the **Plan review phase** returns -`blocked` directly with a legacy-migration required action; otherwise it returns +`blocked` directly with a plan-migration required action; otherwise it returns `sync_debt` with the resolved plan path, debt task ID/title, completed task record, and persisted blocker when present, for the branch above to route. diff --git a/.opencode/skills/sce-next-task/references/output.md b/.opencode/skills/sce-next-task/references/output.md index 8ca87b94f..bf897f69b 100644 --- a/.opencode/skills/sce-next-task/references/output.md +++ b/.opencode/skills/sce-next-task/references/output.md @@ -115,7 +115,6 @@ Describe the smallest coherent implementation approach with enough detail for ap ## Acceptance coverage -- Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. diff --git a/.opencode/skills/sce-next-task/references/plan-review.md b/.opencode/skills/sce-next-task/references/plan-review.md index d01d3bffe..6d6c4943e 100644 --- a/.opencode/skills/sce-next-task/references/plan-review.md +++ b/.opencode/skills/sce-next-task/references/plan-review.md @@ -29,8 +29,8 @@ any value other than `synced`, is unresolved synchronization debt. Never infer For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating - that structure, or an incomplete write), do not attempt a reconstructed retry. + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. Set internal status `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -80,12 +80,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; review it exactly as -described here, with no coverage lookup and no coverage fields in the result. - -For a structured plan, read the selected task's `Covers:` IDs. For each covered +Read the selected task's `Covers:` IDs. For each covered acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the scenario and decision definitions they name. A covered ID or referenced ID with no definition in the plan is a dangling reference. Record it as a review risk; @@ -145,7 +140,7 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. -For a structured plan, a `ready` result also identifies: +A `ready` result also identifies: - The covered acceptance criteria: each `Covers:` ID with its outcome and `Validate:` check. @@ -153,8 +148,6 @@ For a structured plan, a `ready` result also identifies: - Their related design decisions: each decision ID with its decision text. - Every dangling reference, as a review risk. -A `ready` result for a legacy plan carries none of these coverage fields. - A `sync_debt` result must identify: - The resolved plan path. diff --git a/.opencode/skills/sce-next-task/references/task-execution.md b/.opencode/skills/sce-next-task/references/task-execution.md index ab9efcc23..4c92e6a70 100644 --- a/.opencode/skills/sce-next-task/references/task-execution.md +++ b/.opencode/skills/sce-next-task/references/task-execution.md @@ -132,7 +132,7 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. -For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +The covered scenarios' `Then:` and `Check:` may shape the task's own `Verify` checks. Running a scenario's check during the task is task-level evidence only. Task execution never checks an acceptance-criterion checkbox and never records a scenario status; only `/validate` verifies diff --git a/.opencode/skills/sce-validate/references/validation-report.md b/.opencode/skills/sce-validate/references/validation-report.md index 436ec92d3..f3ef7379f 100644 --- a/.opencode/skills/sce-validate/references/validation-report.md +++ b/.opencode/skills/sce-validate/references/validation-report.md @@ -36,8 +36,6 @@ writes it. ### Scenario verification -{Only for a structured plan:} - - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -67,15 +65,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. For a structured - plan, end each acceptance-criterion line with the scenario IDs its + not independently re-evaluate or reclassify criteria here. End each + acceptance-criterion line with the scenario IDs its `Scenarios:` field names, or `none (inspection)`. -- Include **Scenario verification** only for a structured plan, with exactly - one entry per scenario in plan order. Each entry gives the scenario ID, the +- Include **Scenario verification** with exactly one entry per scenario in + plan order. Each entry gives the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status already established by validation - execution. Omit the section for a legacy plan. + execution. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.opencode/skills/sce-validate/references/validation.md b/.opencode/skills/sce-validate/references/validation.md index 1b2c50466..2bc27bb88 100644 --- a/.opencode/skills/sce-validate/references/validation.md +++ b/.opencode/skills/sce-validate/references/validation.md @@ -63,13 +63,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; validate it exactly -as described here, with no scenario collection, no reference check, and no -scenario statuses. - -For a structured plan, also collect: +Also collect: - Every design decision. - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. @@ -85,9 +79,16 @@ fallback under notes on a `validated` or `failed` result. ### 4. Check plan references -Skip this step for a legacy plan. +Run `sce plan check {plan-path}` before scenario +verification. Record each reported diagnostic as a failed check under +**Failed checks and follow-ups**, quoting its line and message. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; record +that fallback under notes and apply the manual pass below instead. -For a structured plan, apply every entry of the plan template's +For the manual pass, apply every entry of the plan template's `Reference rules` to the plan. Each of these is a relationship-integrity defect: @@ -112,7 +113,7 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. -For a structured plan, run or perform every scenario's `Check:` yourself in +Run or perform every scenario's `Check:` yourself in this run, and compare the observed result with the scenario's `Then:`. Give each scenario exactly one status: @@ -127,7 +128,7 @@ Only evidence gathered by this run counts. Task completion, a task's recorded evidence. Record every `Partially verified` or `Not verified` scenario as a failed check with the gap and a required action. -In a structured plan, an acceptance criterion is met only when its `Validate:` +An acceptance criterion is met only when its `Validate:` check passes and every scenario its `Scenarios:` names is `Verified`. An acceptance criterion marked `none (inspection)` is met by its `Validate:` check alone. @@ -375,14 +376,13 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. -- For a structured plan, the validated and failed variants include **Scenario +- The validated and failed variants include **Scenario verification** with exactly one entry per scenario in plan order: the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status. Each acceptance-criterion line cites its scenario IDs, or `none (inspection)`. In the failed variant, list every `Partially verified` or `Not verified` scenario under **What failed**. -- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/.pi/skills/sce-change-to-plan/references/plan-authoring.md b/.pi/skills/sce-change-to-plan/references/plan-authoring.md index ef084bad3..3bd556c00 100644 --- a/.pi/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.pi/skills/sce-change-to-plan/references/plan-authoring.md @@ -34,10 +34,7 @@ Read its `Acceptance criteria rules`, `Design decision rules`, authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, -then verification scenarios, then tasks. When revising a legacy plan, skip the -design decision, verification scenario, `Covers:`, and reference-check work -unless the `Updating an existing plan` rules let the correction adopt the -structured format. +then verification scenarios, then tasks. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -200,7 +197,7 @@ by completed tasks, set internal status `blocked` with category ## 2.9 Check references -Before writing a structured plan, run a reference-consistency pass over the +Before writing the plan, run a reference-consistency pass over the complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. @@ -210,6 +207,16 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. +After writing the plan, run `sce plan check {plan-path}`. When it reports +diagnostics, fix each reported defect in the written plan and run it again. Do +not set internal status `plan_ready` until the checker passes. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; apply the +manual `Reference rules` pass from step 2.9 to the written plan instead, and +record in the open questions that the checker was unavailable. + ## 2.11 Return the result Return one internal result with one of these statuses: diff --git a/.pi/skills/sce-change-to-plan/references/plan-template.md b/.pi/skills/sce-change-to-plan/references/plan-template.md index 5a5fa0fe9..e0a7a4c06 100644 --- a/.pi/skills/sce-change-to-plan/references/plan-template.md +++ b/.pi/skills/sce-change-to-plan/references/plan-template.md @@ -234,10 +234,6 @@ invent one: `None.` is the expected answer for a well-specified change.} tildes to its closing fence. Headings, definitions, and fields inside it are example text, not plan structure. Keep fenced examples in place; never delete one to make a plan read correctly. -- A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` - carries `Covers:`. Every other plan is a legacy plan, and the remaining rules - in this section do not apply to it. - A section runs from its heading to the next heading of any level outside fenced code blocks. A definition counts only in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` @@ -266,7 +262,7 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. -- Every task in a structured plan carries `Covers:` naming at least one +- Every task carries `Covers:` naming at least one acceptance criterion defined in the plan. A task without `Covers:` or with an empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. @@ -316,7 +312,4 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. -- A legacy plan stays legacy. Add design decisions, verification scenarios, or - `Covers:` only when the correction explicitly asks to adopt the structured - format. - Give each new decision, criterion, or scenario the next unused ID of its kind. diff --git a/.pi/skills/sce-next-task/SKILL.md b/.pi/skills/sce-next-task/SKILL.md index 45a7b462f..1a59112c8 100644 --- a/.pi/skills/sce-next-task/SKILL.md +++ b/.pi/skills/sce-next-task/SKILL.md @@ -91,7 +91,7 @@ selected or resumed, before allowing a new implementation task to start. A missing field, or any value other than `synced`, is unresolved synchronization debt. Never infer `synced` from conversation history. When the debt-carrying task has no durable completed-task record, the **Plan review phase** returns -`blocked` directly with a legacy-migration required action; otherwise it returns +`blocked` directly with a plan-migration required action; otherwise it returns `sync_debt` with the resolved plan path, debt task ID/title, completed task record, and persisted blocker when present, for the branch above to route. diff --git a/.pi/skills/sce-next-task/references/output.md b/.pi/skills/sce-next-task/references/output.md index 8ca87b94f..bf897f69b 100644 --- a/.pi/skills/sce-next-task/references/output.md +++ b/.pi/skills/sce-next-task/references/output.md @@ -115,7 +115,6 @@ Describe the smallest coherent implementation approach with enough detail for ap ## Acceptance coverage -- Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. diff --git a/.pi/skills/sce-next-task/references/plan-review.md b/.pi/skills/sce-next-task/references/plan-review.md index d01d3bffe..6d6c4943e 100644 --- a/.pi/skills/sce-next-task/references/plan-review.md +++ b/.pi/skills/sce-next-task/references/plan-review.md @@ -29,8 +29,8 @@ any value other than `synced`, is unresolved synchronization debt. Never infer For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating - that structure, or an incomplete write), do not attempt a reconstructed retry. + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. Set internal status `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -80,12 +80,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; review it exactly as -described here, with no coverage lookup and no coverage fields in the result. - -For a structured plan, read the selected task's `Covers:` IDs. For each covered +Read the selected task's `Covers:` IDs. For each covered acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the scenario and decision definitions they name. A covered ID or referenced ID with no definition in the plan is a dangling reference. Record it as a review risk; @@ -145,7 +140,7 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. -For a structured plan, a `ready` result also identifies: +A `ready` result also identifies: - The covered acceptance criteria: each `Covers:` ID with its outcome and `Validate:` check. @@ -153,8 +148,6 @@ For a structured plan, a `ready` result also identifies: - Their related design decisions: each decision ID with its decision text. - Every dangling reference, as a review risk. -A `ready` result for a legacy plan carries none of these coverage fields. - A `sync_debt` result must identify: - The resolved plan path. diff --git a/.pi/skills/sce-next-task/references/task-execution.md b/.pi/skills/sce-next-task/references/task-execution.md index ab9efcc23..4c92e6a70 100644 --- a/.pi/skills/sce-next-task/references/task-execution.md +++ b/.pi/skills/sce-next-task/references/task-execution.md @@ -132,7 +132,7 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. -For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +The covered scenarios' `Then:` and `Check:` may shape the task's own `Verify` checks. Running a scenario's check during the task is task-level evidence only. Task execution never checks an acceptance-criterion checkbox and never records a scenario status; only `/validate` verifies diff --git a/.pi/skills/sce-validate/references/validation-report.md b/.pi/skills/sce-validate/references/validation-report.md index 436ec92d3..f3ef7379f 100644 --- a/.pi/skills/sce-validate/references/validation-report.md +++ b/.pi/skills/sce-validate/references/validation-report.md @@ -36,8 +36,6 @@ writes it. ### Scenario verification -{Only for a structured plan:} - - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -67,15 +65,15 @@ After repairs, rerun: run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. For a structured - plan, end each acceptance-criterion line with the scenario IDs its + not independently re-evaluate or reclassify criteria here. End each + acceptance-criterion line with the scenario IDs its `Scenarios:` field names, or `none (inspection)`. -- Include **Scenario verification** only for a structured plan, with exactly - one entry per scenario in plan order. Each entry gives the scenario ID, the +- Include **Scenario verification** with exactly one entry per scenario in + plan order. Each entry gives the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status already established by validation - execution. Omit the section for a legacy plan. + execution. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact diff --git a/.pi/skills/sce-validate/references/validation.md b/.pi/skills/sce-validate/references/validation.md index 1b2c50466..2bc27bb88 100644 --- a/.pi/skills/sce-validate/references/validation.md +++ b/.pi/skills/sce-validate/references/validation.md @@ -63,13 +63,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; validate it exactly -as described here, with no scenario collection, no reference check, and no -scenario statuses. - -For a structured plan, also collect: +Also collect: - Every design decision. - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. @@ -85,9 +79,16 @@ fallback under notes on a `validated` or `failed` result. ### 4. Check plan references -Skip this step for a legacy plan. +Run `sce plan check {plan-path}` before scenario +verification. Record each reported diagnostic as a failed check under +**Failed checks and follow-ups**, quoting its line and message. + +Trust the checker only when it actually ran: a result counts only on exit `0` +with `structured plan: reference integrity holds` on stdout, or on exit `3` +with diagnostics. Any other outcome means the checker is unavailable; record +that fallback under notes and apply the manual pass below instead. -For a structured plan, apply every entry of the plan template's +For the manual pass, apply every entry of the plan template's `Reference rules` to the plan. Each of these is a relationship-integrity defect: @@ -112,7 +113,7 @@ Then verify each acceptance criterion using its `Validate:` line. Prefer a runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. -For a structured plan, run or perform every scenario's `Check:` yourself in +Run or perform every scenario's `Check:` yourself in this run, and compare the observed result with the scenario's `Then:`. Give each scenario exactly one status: @@ -127,7 +128,7 @@ Only evidence gathered by this run counts. Task completion, a task's recorded evidence. Record every `Partially verified` or `Not verified` scenario as a failed check with the gap and a required action. -In a structured plan, an acceptance criterion is met only when its `Validate:` +An acceptance criterion is met only when its `Validate:` check passes and every scenario its `Scenarios:` names is `Verified`. An acceptance criterion marked `none (inspection)` is met by its `Validate:` check alone. @@ -375,14 +376,13 @@ a normal work session, then rerun validation. `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. -- For a structured plan, the validated and failed variants include **Scenario +- The validated and failed variants include **Scenario verification** with exactly one entry per scenario in plan order: the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status. Each acceptance-criterion line cites its scenario IDs, or `none (inspection)`. In the failed variant, list every `Partially verified` or `Not verified` scenario under **What failed**. -- Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/cli/src/services/plan/command.rs b/cli/src/services/plan/command.rs index ae50f9d7c..acc159d60 100644 --- a/cli/src/services/plan/command.rs +++ b/cli/src/services/plan/command.rs @@ -7,7 +7,6 @@ use crate::services::plan::extract::extract; use crate::services::plan::validate::{validate, PlanDiagnostic}; pub const STRUCTURED_PLAN_PASS: &str = "structured plan: reference integrity holds"; -pub const LEGACY_PLAN_PASS: &str = "legacy plan: structured reference rules do not apply"; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PlanCheckRequest { @@ -36,12 +35,7 @@ impl PlanCommand { ))); } - Ok(if report.structured { - STRUCTURED_PLAN_PASS - } else { - LEGACY_PLAN_PASS - } - .to_string()) + Ok(STRUCTURED_PLAN_PASS.to_string()) } } diff --git a/cli/src/services/plan/extract.rs b/cli/src/services/plan/extract.rs index ec3f5e7bb..4c81ed4ba 100644 --- a/cli/src/services/plan/extract.rs +++ b/cli/src/services/plan/extract.rs @@ -371,8 +371,8 @@ mod tests { } #[test] - fn legacy_plan_with_fenced_structured_example_ignores_fenced_structure() { - let plan = fixture("legacy-plan-with-fenced-structured-example.md"); + fn unstructured_plan_with_fenced_structured_example_ignores_fenced_structure() { + let plan = fixture("unstructured-plan-with-fenced-structured-example.md"); assert_eq!( entry_lines(&plan), diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 347b29bf2..590665237 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -56,24 +56,14 @@ fn fixture_expectations_match_manifest() { actual.sort_unstable(); match verdict { - "structured" | "legacy" => { - assert!( - expected.is_empty(), - "{fixture}: pass rows take no diagnostics" - ); - assert_eq!( - report.structured, - verdict == "structured", - "{fixture}: wrong classification" - ); - } - "fail" => { - assert!( - !expected.is_empty(), - "{fixture}: fail rows need diagnostics" - ); - assert!(report.structured, "{fixture}: wrong classification"); - } + "pass" => assert!( + expected.is_empty(), + "{fixture}: pass rows take no diagnostics" + ), + "fail" => assert!( + !expected.is_empty(), + "{fixture}: fail rows need diagnostics" + ), other => panic!("{fixture}: unknown verdict `{other}`"), } assert_eq!(actual, expected, "{fixture}: diagnostics differ"); @@ -208,7 +198,6 @@ fn command_valid_plans_exit_success() { for (fixture, expected) in [ ("valid-structured-plan.md", command::STRUCTURED_PLAN_PASS), ("inspection-only-plan.md", command::STRUCTURED_PLAN_PASS), - ("legacy-plan.md", command::LEGACY_PLAN_PASS), ] { let absolute = fixture_dir().join(fixture); for path in [ @@ -240,6 +229,21 @@ fn command_invalid_plan_exits_validation() { ); } +#[test] +fn command_unstructured_plan_exits_validation() { + let path = "../config/pkl/renderers/fixtures/plan-structure/unstructured-plan.md"; + let run = run_plan_check(path); + + assert_eq!(run.exit_code, std::process::ExitCode::from(3)); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!( + run.stderr + .contains(&format!("{path}:59: `T01` is missing `Covers:`")), + "{}", + run.stderr + ); +} + #[test] fn command_missing_file_is_runtime_failure() { let directory = tempfile::tempdir().expect("tempdir should be created"); diff --git a/cli/src/services/plan/validate.rs b/cli/src/services/plan/validate.rs index 9ca753195..06bce444c 100644 --- a/cli/src/services/plan/validate.rs +++ b/cli/src/services/plan/validate.rs @@ -21,7 +21,6 @@ pub struct PlanDiagnostic { #[derive(Clone, Debug, Eq, PartialEq)] pub struct PlanReport { - pub structured: bool, pub diagnostics: Vec, } @@ -100,29 +99,12 @@ pub fn validate(plan: &Plan) -> PlanReport { .iter() .map(|heading| (heading_text(heading), heading.line)) .collect(); - let structured = headings - .iter() - .any(|(text, _)| text == VERIFICATION_SCENARIOS_HEADING) - || plan - .entries_of(PlanEntryKind::Task) - .any(|entry| field_of(entry, "Covers").is_some()); - - if !structured { - return PlanReport { - structured, - diagnostics: Vec::new(), - }; - } - let mut diagnostics = heading_diagnostics(&headings); diagnostics.extend(definition_diagnostics(plan)); diagnostics.extend(reference_diagnostics(plan)); diagnostics.sort(); - PlanReport { - structured, - diagnostics, - } + PlanReport { diagnostics } } fn first_line(headings: &[(String, usize)], text: &str) -> Option { diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 3a90b62d8..f9ef97784 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -280,10 +280,7 @@ Read its `Acceptance criteria rules`, `Design decision rules`, authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, -then verification scenarios, then tasks. When revising a legacy plan, skip the -design decision, verification scenario, `Covers:`, and reference-check work -unless the `Updating an existing plan` rules let the correction adopt the -structured format. +then verification scenarios, then tasks. The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -446,7 +443,7 @@ by completed tasks, set internal status `blocked` with category ## 2.9 Check references -Before writing a structured plan, run a reference-consistency pass over the +Before writing the plan, run a reference-consistency pass over the complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. @@ -456,14 +453,12 @@ not write a plan that fails this pass. Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` exactly. For revisions, apply its `Updating an existing plan` rules. -After writing a structured plan, run `sce plan check {plan-path}`. When it reports +After writing the plan, run `sce plan check {plan-path}`. When it reports diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. Skip the checker -for a legacy plan. +not set internal status `plan_ready` until the checker passes. Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` or -`legacy plan: structured reference rules do not apply` on stdout, or on exit `3` +with `structured plan: reference integrity holds` on stdout, or on exit `3` with diagnostics. Any other outcome means the checker is unavailable; apply the manual `Reference rules` pass from step 2.9 to the written plan instead, and record in the open questions that the checker was unavailable. @@ -754,10 +749,6 @@ invent one: `None.` is the expected answer for a well-specified change.} tildes to its closing fence. Headings, definitions, and fields inside it are example text, not plan structure. Keep fenced examples in place; never delete one to make a plan read correctly. -- A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` - carries `Covers:`. Every other plan is a legacy plan, and the remaining rules - in this section do not apply to it. - A section runs from its heading to the next heading of any level outside fenced code blocks. A definition counts only in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` @@ -786,7 +777,7 @@ invent one: `None.` is the expected answer for a well-specified change.} - Every acceptance criterion carries `Scenarios:`, with scenario IDs or the literal `none (inspection)` when only an inspection can prove it. Omit `Decisions:` when no decision applies. -- Every task in a structured plan carries `Covers:` naming at least one +- Every task carries `Covers:` naming at least one acceptance criterion defined in the plan. A task without `Covers:` or with an empty `Covers:` is a defect even when other tasks cover every criterion. - Every acceptance criterion is named by at least one task's `Covers:`. @@ -836,9 +827,6 @@ Do not author either while planning. run earlier, and never renumber a completed task. - Add acceptance criteria for newly planned outcomes rather than rewriting criteria already satisfied. -- A legacy plan stays legacy. Add design decisions, verification scenarios, or - `Covers:` only when the correction explicitly asks to adopt the structured - format. - Give each new decision, criterion, or scenario the next unused ID of its kind. """ diff --git a/config/pkl/base/workflow-content.pkl b/config/pkl/base/workflow-content.pkl index 588948945..df23c6f95 100644 --- a/config/pkl/base/workflow-content.pkl +++ b/config/pkl/base/workflow-content.pkl @@ -308,7 +308,7 @@ selected or resumed, before allowing a new implementation task to start. A missing field, or any value other than `synced`, is unresolved synchronization debt. Never infer `synced` from conversation history. When the debt-carrying task has no durable completed-task record, the **Plan review phase** returns -`blocked` directly with a legacy-migration required action; otherwise it returns +`blocked` directly with a plan-migration required action; otherwise it returns `sync_debt` with the resolved plan path, debt task ID/title, completed task record, and persisted blocker when present, for the branch above to route. diff --git a/config/pkl/base/workflow-next-task.pkl b/config/pkl/base/workflow-next-task.pkl index 969e14397..1b29245e9 100644 --- a/config/pkl/base/workflow-next-task.pkl +++ b/config/pkl/base/workflow-next-task.pkl @@ -402,8 +402,8 @@ local renderReviewSkillBody = (mode: model.WorkflowRenderMode) -> """ For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan - predating that structure, or an incomplete write), do not attempt a + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. \(returnUpper.render.apply(mode)) `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -1438,7 +1438,6 @@ local renderImplementationGate = (mode: model.WorkflowRenderMode) -> """ ## Acceptance coverage - - Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. @@ -1492,8 +1491,8 @@ any value other than `synced`, is unresolved synchronization debt. Never infer For the first task carrying debt: - When the task has no durable completed-task record (no `Files changed`, - `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating - that structure, or an incomplete write), do not attempt a reconstructed retry. + `Result`, `Verify`, or `Context impact` recorded, for example after an + incomplete write), do not attempt a reconstructed retry. Set internal status `blocked` with a required action to migrate the plan (backfill the completion record, or resolve the debt manually) and a retry condition of the plan carrying that structure. Stop. @@ -1543,12 +1542,7 @@ in its own section: `D` entries under `## Design decisions`, `AC` entries under `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. -A plan is structured when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`. Every other plan is a legacy plan; review it exactly as -described here, with no coverage lookup and no coverage fields in the result. - -For a structured plan, read the selected task's `Covers:` IDs. For each covered +Read the selected task's `Covers:` IDs. For each covered acceptance criterion, read its `Scenarios:` and `Decisions:` fields and the scenario and decision definitions they name. A covered ID or referenced ID with no definition in the plan is a dangling reference. Record it as a review risk; @@ -1608,7 +1602,7 @@ A `ready` result must identify: - Relevant files and context. - Review assumptions. -For a structured plan, a `ready` result also identifies: +A `ready` result also identifies: - The covered acceptance criteria: each `Covers:` ID with its outcome and `Validate:` check. @@ -1616,8 +1610,6 @@ For a structured plan, a `ready` result also identifies: - Their related design decisions: each decision ID with its decision text. - Every dangling reference, as a review risk. -A `ready` result for a legacy plan carries none of these coverage fields. - A `sync_debt` result must identify: - The resolved plan path. @@ -1778,7 +1770,7 @@ Run the narrowest authoritative checks that demonstrate the done checks. Start with verification supplied by the readiness result. Add nearby or directly relevant checks only when needed. -For a structured plan, the covered scenarios' `Then:` and `Check:` may shape +The covered scenarios' `Then:` and `Check:` may shape the task's own `Verify` checks. Running a scenario's check during the task is task-level evidence only. Task execution never checks an acceptance-criterion checkbox and never records a scenario status; only `/validate` verifies @@ -1993,7 +1985,6 @@ Describe the smallest coherent implementation approach with enough detail for ap ## Acceptance coverage -- Include only when the plan is structured; omit this section for a legacy plan. - List each covered acceptance criterion `{AC id}: {outcome}` with its scenarios `{S id}: {Then}` (`{Check}`) and related decisions `{D id}: {decision}`. - Report dangling references under **Risks or trade-offs**. diff --git a/config/pkl/base/workflow-validate.pkl b/config/pkl/base/workflow-validate.pkl index cec8cf20b..f8998a5a1 100644 --- a/config/pkl/base/workflow-validate.pkl +++ b/config/pkl/base/workflow-validate.pkl @@ -236,13 +236,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ `## Acceptance criteria`, `S` entries under `### Verification scenarios`, and `T` entries under `## Task stack`. - A plan is structured when, outside fenced code blocks, it has a - `### Verification scenarios` heading or any task under `## Task stack` carries - `Covers:`. Every other plan is a legacy plan; validate it exactly - as described here, with no scenario collection, no reference check, and no - scenario statuses. - - For a structured plan, also collect: + Also collect: - Every design decision. - Every acceptance criterion's `Decisions:` and `Scenarios:` fields. @@ -258,15 +252,12 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ \(validationHeadings.stepHeading.apply(mode, 4, "Check plan references")) - Skip this step for a legacy plan. - - For a structured plan, run `sce plan check {plan-path}` before scenario + Run `sce plan check {plan-path}` before scenario verification. Record each reported diagnostic as a failed check under **Failed checks and follow-ups**, quoting its line and message. Trust the checker only when it actually ran: a result counts only on exit `0` - with `structured plan: reference integrity holds` or - `legacy plan: structured reference rules do not apply` on stdout, or on exit `3` + with `structured plan: reference integrity holds` on stdout, or on exit `3` with diagnostics. Any other outcome means the checker is unavailable; record that fallback under notes and apply the manual pass below instead. @@ -295,7 +286,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ runnable command. Use a named inspection only when the criterion authorizes it, and say exactly what was inspected. - For a structured plan, run or perform every scenario's `Check:` yourself in + Run or perform every scenario's `Check:` yourself in this run, and compare the observed result with the scenario's `Then:`. Give each scenario exactly one status: @@ -310,7 +301,7 @@ local renderValidationSkillBody = (mode: model.WorkflowRenderMode) -> """ evidence. Record every `Partially verified` or `Not verified` scenario as a failed check with the gap and a required action. - In a structured plan, an acceptance criterion is met only when its `Validate:` + An acceptance criterion is met only when its `Validate:` check passes and every scenario its `Scenarios:` names is `Verified`. An acceptance criterion marked `none (inspection)` is met by its `Validate:` check alone. @@ -451,8 +442,6 @@ local renderValidationReport = (mode: model.WorkflowRenderMode) -> """ ### Scenario verification - {Only for a structured plan:} - - S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified} ### Failed checks and follow-ups @@ -482,15 +471,15 @@ local renderValidationReport = (mode: model.WorkflowRenderMode) -> """ run**. Preserve its exit code and concise outcome; do not invent either. - Under **Success-criteria verification**, render the acceptance-criterion checkbox state and evidence already established by validation execution. Do - not independently re-evaluate or reclassify criteria here. For a structured - plan, end each acceptance-criterion line with the scenario IDs its + not independently re-evaluate or reclassify criteria here. End each + acceptance-criterion line with the scenario IDs its `Scenarios:` field names, or `none (inspection)`. - - Include **Scenario verification** only for a structured plan, with exactly - one entry per scenario in plan order. Each entry gives the scenario ID, the + - Include **Scenario verification** with exactly one entry per scenario in + plan order. Each entry gives the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status already established by validation - execution. Omit the section for a legacy plan. + execution. - Under **Failed checks and follow-ups**, render every failure and follow-up supplied by validation execution. Write `None.` when status is `validated`. - When status is `failed`, always include **Retry** with the exact @@ -672,14 +661,13 @@ local renderValidationResult = (mode: model.WorkflowRenderMode) -> """ `/validate {plan path}` as the final step after repairs. - The failed variant must be self-contained enough to hand to another session without the original chat. - - For a structured plan, the validated and failed variants include **Scenario + - The validated and failed variants include **Scenario verification** with exactly one entry per scenario in plan order: the scenario ID, the acceptance-criterion IDs that reference it, the evidence kind (`test`, `command`, or `inspection`), the exact test, command, or inspected location, the actual result, and the scenario status. Each acceptance-criterion line cites its scenario IDs, or `none (inspection)`. In the failed variant, list every `Partially verified` or `Not verified` scenario under **What failed**. - - Omit **Scenario verification** and the scenario citations for a legacy plan. - Do not include durable context synchronization results in this report. - Do not select or describe an unrelated next implementation task when status is `validated`. diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 0ed8233a6..f6db2606e 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -156,10 +156,10 @@ expect_pkl_fixture_failure \ "generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl" \ - "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document" + "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-structured-references-check.pkl" \ - "generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution" + "generated sce-next-task plan-review.md, output.md, and task-execution.md must carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, and never check an acceptance criterion or record a scenario status during task execution" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-compact-completion-writing-check.pkl" \ "sce-next-task task-execution.md must record execution facts directly on the completed task and must not construct a separate Implementation evidence, Verification evidence, or Context synchronization handoff record" @@ -186,10 +186,10 @@ expect_pkl_fixture_failure \ "generated sce-validate document must not contain a sce-decision reference or plan-context-sync wording" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl" \ - "generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged" + "generated sce-validate validation.md must fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl" \ - "generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans" + "generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under What failed" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-template-misplaced-field-check.pkl" \ @@ -207,7 +207,6 @@ plan_structure_fixtures="config/pkl/renderers/fixtures/plan-structure" expect_plan_structure_pass() { local fixture_name="$1" - local expected_result="$2" local output if ! output="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ @@ -217,8 +216,8 @@ expect_plan_structure_pass() { exit 1 fi - if [[ "$output" != *"$expected_result"* ]]; then - printf 'Plan structure fixture passed with the wrong classification: %s\n%s\n' \ + if [[ "$output" != *"$structured_plan_result"* ]]; then + printf 'Plan structure fixture passed with the wrong result: %s\n%s\n' \ "$fixture_name" "$output" >&2 exit 1 fi @@ -237,10 +236,8 @@ expect_plan_structure_failure() { expect_plan_structure_report() { local fixture_name="$1" - local expected_classification="$2" - shift 2 + shift local report - local actual_classification local actual_diagnostics local expected_diagnostics @@ -251,16 +248,9 @@ expect_plan_structure_report() { exit 1 fi - actual_classification="$(head -n 1 <<<"$report")" - actual_diagnostics="$(tail -n +2 <<<"$report" | LC_ALL=C sort)" + actual_diagnostics="$(LC_ALL=C sort <<<"$report")" expected_diagnostics="$(if [[ "$#" -gt 0 ]]; then printf '%s\n' "$@"; fi | LC_ALL=C sort)" - if [[ "$actual_classification" != "$expected_classification" ]]; then - printf 'Plan structure fixture has the wrong classification: %s\nexpected: %s\nactual: %s\n' \ - "$fixture_name" "$expected_classification" "$actual_classification" >&2 - exit 1 - fi - if [[ "$actual_diagnostics" != "$expected_diagnostics" ]]; then printf 'Plan structure fixture diagnostics differ from the manifest: %s\nexpected (sorted):\n%s\nactual (sorted):\n%s\n' \ "$fixture_name" "$expected_diagnostics" "$actual_diagnostics" >&2 @@ -269,7 +259,6 @@ expect_plan_structure_report() { } structured_plan_result="structured plan: reference integrity holds" -legacy_plan_result="legacy plan: structured reference rules do not apply" plan_structure_manifest="$plan_structure_fixtures/expectations.tsv" listed_plan_structure_fixtures=() @@ -290,17 +279,13 @@ while IFS=$'\t' read -r -u 3 -a manifest_columns; do listed_plan_structure_fixtures+=("$fixture_name") case "$fixture_verdict" in - structured | legacy) + pass) if [[ "${#expected_diagnostics[@]}" -ne 0 ]]; then printf 'Plan structure manifest pass row must not list diagnostics: %s\n' "$fixture_name" >&2 exit 1 fi - if [[ "$fixture_verdict" == "structured" ]]; then - expect_plan_structure_pass "$fixture_name" "$structured_plan_result" - else - expect_plan_structure_pass "$fixture_name" "$legacy_plan_result" - fi - expect_plan_structure_report "$fixture_name" "$fixture_verdict" + expect_plan_structure_pass "$fixture_name" + expect_plan_structure_report "$fixture_name" ;; fail) if [[ "${#expected_diagnostics[@]}" -eq 0 ]]; then @@ -308,7 +293,7 @@ while IFS=$'\t' read -r -u 3 -a manifest_columns; do exit 1 fi expect_plan_structure_failure "$fixture_name" - expect_plan_structure_report "$fixture_name" structured "${expected_diagnostics[@]}" + expect_plan_structure_report "$fixture_name" "${expected_diagnostics[@]}" ;; *) printf 'Plan structure manifest has an unknown verdict for %s: %s\n' \ diff --git a/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl b/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl index 9da7c31a0..e58e8b213 100644 --- a/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl +++ b/config/pkl/renderers/fixtures/next-task-structured-references-check.pkl @@ -11,5 +11,5 @@ local documents = new Mapping { result = if ( contract.assertNextTaskStructuredReferences.apply(documents) -) "negative fixture unexpectedly accepted next-task references without structured-plan coverage or the legacy-plan path" -else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution") +) "negative fixture unexpectedly accepted next-task references without acceptance coverage" +else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, and never check an acceptance criterion or record a scenario status during task execution") diff --git a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl index d261c5f9b..4ec144cc9 100644 --- a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl +++ b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl @@ -8,4 +8,4 @@ local documents = new Mapping { result = if ( contract.assertPlanAuthoringStructuredReferences.apply(documents) ) "negative fixture unexpectedly accepted a plan-authoring phase without the structured authoring order or reference-consistency pass" -else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document") +else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") diff --git a/config/pkl/renderers/fixtures/plan-structure-report.pkl b/config/pkl/renderers/fixtures/plan-structure-report.pkl index e2080fcab..bf32cd3ee 100644 --- a/config/pkl/renderers/fixtures/plan-structure-report.pkl +++ b/config/pkl/renderers/fixtures/plan-structure-report.pkl @@ -3,5 +3,5 @@ import "../plan-structure.pkl" as planStructure local report = planStructure.check(read("file:\(read("prop:planPath"))").text) output { - text = (List(if (report.structured) "structured" else "legacy") + report.defects).join("\n") + "\n" + text = report.defects.map((defect) -> defect + "\n").join("") } diff --git a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv index 16d1a2a46..72dc97121 100644 --- a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv +++ b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv @@ -1,10 +1,8 @@ -# fixture verdict (structured | legacy | fail) complete expected diagnostic messages (fail only, one per column, order-insensitive, duplicates counted) +# fixture verdict (pass | fail) complete expected diagnostic messages (fail only, one per column, order-insensitive, duplicates counted) duplicate-id.md fail `S1` is defined more than once -fenced-example-plan.md structured -inspection-only-plan.md structured +fenced-example-plan.md pass +inspection-only-plan.md pass keywords-only-in-examples.md fail `Scenarios:` on `AC1` references undefined `S1`; it appears only inside a fenced code block -legacy-plan-with-fenced-structured-example.md legacy -legacy-plan.md legacy misplaced-definition.md fail `S1` is defined under `## Acceptance criteria`; it belongs under `### Verification scenarios` `Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios` misplaced-field.md fail `Check:` on `D1` is misplaced; it belongs to a verification scenario `Covers:` on `AC1` is misplaced; it belongs to a task `S1` is missing `Check:` `T01` is missing `Covers:` `AC1` is not covered by any task's `Covers:` missing-scenarios-field.md fail `AC1` is missing `Scenarios:` `S1` is not referenced by any acceptance criterion's `Scenarios:` @@ -13,6 +11,8 @@ task-missing-covers.md fail `T02` is missing `Covers:` uncovered-ac.md fail `AC2` is not covered by any task's `Covers:` undefined-references.md fail `Decisions:` on `AC1` references undefined `D2` `Scenarios:` on `AC1` references undefined `S9` `Covers:` on `T01` references undefined `AC3` unreferenced-scenario.md fail `S2` is not referenced by any acceptance criterion's `Scenarios:` -valid-structured-plan.md structured +unstructured-plan-with-fenced-structured-example.md fail `AC1` is missing `Scenarios:` `T01` is missing `Covers:` `AC1` is not covered by any task's `Covers:` +unstructured-plan.md fail `AC1` is missing `Scenarios:` `AC2` is missing `Scenarios:` `AC3` is missing `Scenarios:` `T01` is missing `Covers:` `T02` is missing `Covers:` `AC1` is not covered by any task's `Covers:` `AC2` is not covered by any task's `Covers:` `AC3` is not covered by any task's `Covers:` +valid-structured-plan.md pass wrong-heading-context.md fail heading `## Task stack` must come after `### Verification scenarios` heading `### Verification scenarios` is not inside `## Acceptance criteria` wrong-reference-kind.md fail `Covers:` on `T01` names `S1`, which is not an acceptance criterion `AC1` is not covered by any task's `Covers:` diff --git a/config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md b/config/pkl/renderers/fixtures/plan-structure/unstructured-plan-with-fenced-structured-example.md similarity index 91% rename from config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md rename to config/pkl/renderers/fixtures/plan-structure/unstructured-plan-with-fenced-structured-example.md index b8dc85458..cc2cee670 100644 --- a/config/pkl/renderers/fixtures/plan-structure/legacy-plan-with-fenced-structured-example.md +++ b/config/pkl/renderers/fixtures/plan-structure/unstructured-plan-with-fenced-structured-example.md @@ -1,4 +1,4 @@ -# Plan: legacy-plan-with-fenced-structured-example +# Plan: unstructured-plan-with-fenced-structured-example ## Change summary diff --git a/config/pkl/renderers/fixtures/plan-structure/legacy-plan.md b/config/pkl/renderers/fixtures/plan-structure/unstructured-plan.md similarity index 100% rename from config/pkl/renderers/fixtures/plan-structure/legacy-plan.md rename to config/pkl/renderers/fixtures/plan-structure/unstructured-plan.md diff --git a/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl index 0af3e2aa5..434292d9b 100644 --- a/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl +++ b/config/pkl/renderers/fixtures/validate-scenario-evidence-check.pkl @@ -8,4 +8,4 @@ local documents = new Mapping { result = if ( contract.assertValidateScenarioEvidence.apply(documents) ) "negative fixture unexpectedly accepted validation.md without relationship integrity or scenario evidence rules" -else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged") +else throw("generated sce-validate validation.md must fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") diff --git a/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl b/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl index c1fa16c6c..adefb9dce 100644 --- a/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl +++ b/config/pkl/renderers/fixtures/validation-report-scenario-section-check.pkl @@ -10,4 +10,4 @@ local documents = new Mapping { result = if ( contract.assertValidationReportScenarioSection.apply(documents) ) "negative fixture unexpectedly accepted validation reports without a scenario verification section" -else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans") +else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under What failed") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index e2c7c9e6d..762c9260e 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1053,14 +1053,12 @@ local planTemplateOwnedPolicyTokens = new Listing { "Record a design decision only for a real architectural choice the plan" "`Then:` states an exact observable result or invariant, not an intention." "Read plan structure only from Markdown outside fenced code blocks." - "A plan is structured when, outside fenced code blocks, it has a" "A section runs from its heading to the next heading of any level outside" "To check a fenced example itself, extract the block's content and apply these" "Never renumber or reuse a decision, criterion, or scenario ID" - "Every task in a structured plan carries `Covers:` naming at least one" + "Every task carries `Covers:` naming at least one" "Every acceptance criterion is named by at least one task's `Covers:`." "A planned `Check:` is not execution evidence." - "A legacy plan stays legacy." } local requiredPlanAuthoringTemplateReferences = new Listing { @@ -1095,12 +1093,10 @@ local requiredPlanAuthoringStructuredTokens = new Listing { "Give each task its `Covers:` field" "## 2.9 Check references" "run a reference-consistency pass over the\ncomplete draft that applies every `Reference rules` entry" - "After writing a structured plan, run `sce plan check {plan-path}`." + "After writing the plan, run `sce plan check {plan-path}`." "Do\nnot set internal status `plan_ready` until the checker passes." - "Skip the checker\nfor a legacy plan." "Trust the checker only when it actually ran:" "`structured plan: reference integrity holds`" - "`legacy plan: structured reference rules do not apply`" "Any other outcome means the checker is unavailable;" "manual `Reference rules` pass from step 2.9" } @@ -1116,18 +1112,15 @@ hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> ) && noNextTaskPlanCheckReference.apply(documents) ) "sce-change-to-plan plan-authoring: decisions, criteria, scenarios, and tasks authored in order with a reference-consistency pass" - else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing a structured plan with the checker trust rule, manual Reference rules fallback, and legacy skip, and keep sce plan check out of every sce-next-task document") + else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") local requiredNextTaskStructuredReferenceTokens = new Listing { Pair("/skills/sce-next-task/references/plan-review.md", "Read plan structure only outside fenced code blocks;") Pair("/skills/sce-next-task/references/plan-review.md", "A definition counts only\nin its own section:") - Pair("/skills/sce-next-task/references/plan-review.md", "A plan is structured when, outside fenced code blocks, it has a\n`### Verification scenarios` heading or any task under `## Task stack` carries\n`Covers:`.") - Pair("/skills/sce-next-task/references/plan-review.md", "review it exactly as\ndescribed here, with no coverage lookup and no coverage fields in the result.") Pair("/skills/sce-next-task/references/plan-review.md", "Record it as a review risk;\nit does not make the task `blocked`.") - Pair("/skills/sce-next-task/references/plan-review.md", "For a structured plan, a `ready` result also identifies:") + Pair("/skills/sce-next-task/references/plan-review.md", "A `ready` result also identifies:") Pair("/skills/sce-next-task/references/plan-review.md", "each scenario ID with its `Then:` and `Check:`.") - Pair("/skills/sce-next-task/references/plan-review.md", "A `ready` result for a legacy plan carries none of these coverage fields.") - Pair("/skills/sce-next-task/references/output.md", "## Acceptance coverage\n\n- Include only when the plan is structured; omit this section for a legacy plan.") + Pair("/skills/sce-next-task/references/output.md", "## Acceptance coverage\n\n- List each covered acceptance criterion") Pair("/skills/sce-next-task/references/task-execution.md", "Task execution never checks an acceptance-criterion\ncheckbox and never records a scenario status") Pair("/skills/sce-next-task/references/task-execution.md", "- Keep the task's `Covers:` field as authored.") } @@ -1138,8 +1131,8 @@ hidden assertNextTaskStructuredReferences = (documents: Mapping) -> documents.any((path, _) -> path.endsWith(entry.first)) && documents.every((path, text) -> !path.endsWith(entry.first) || text.contains(entry.second)) ) - ) "sce-next-task structured references: review, gate, and execution consume coverage and keep the legacy path" - else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must detect structured plans, carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, keep legacy plans unchanged, and never check an acceptance criterion or record a scenario status during task execution") + ) "sce-next-task structured references: review, gate, and execution consume coverage" + else throw("generated sce-next-task plan-review.md, output.md, and task-execution.md must carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, and never check an acceptance criterion or record a scenario status during task execution") local requiredNextTaskCompletionWritingTokens = new Listing { "Record directly on the completed task:" @@ -1355,9 +1348,6 @@ hidden assertValidateExcludesDecisionAndPlanSync = (documents: Mapping) -> local requiredValidateScenarioEvidenceTokens = new Listing { "Read plan structure only outside fenced code blocks;" "A definition counts only\nin its own section:" - "A plan is structured when, outside fenced code blocks, it has a\n`### Verification scenarios` heading or any task under `## Task stack` carries\n`Covers:`." - "validate it exactly\nas described here, with no scenario collection, no reference check, and no\nscenario statuses." - "Skip this step for a legacy plan." "`Reference rules` to the plan." "- An ID defined more than once." "- A dangling reference:" @@ -1365,17 +1355,16 @@ local requiredValidateScenarioEvidenceTokens = new Listing { "- An acceptance criterion that no task's `Covers:` names." "- A scenario that no acceptance criterion's `Scenarios:` references." "A relationship-integrity defect makes the result\n`failed`; never tolerate one silently." - "run or perform every scenario's `Check:` yourself" + "Run or perform every scenario's `Check:` yourself" "- `Verified` when" "- `Partially verified` when" "- `Not verified` when" "Only evidence gathered by this run counts. Task completion, a task's recorded\n`Verify` outcomes, a planned `Check:` line, and the plan's own claims are never\nevidence." - "an acceptance criterion is met only when its `Validate:`\ncheck passes and every scenario its `Scenarios:` names is `Verified`." - "For a structured plan, run `sce plan check {plan-path}` before scenario\nverification." + "An acceptance criterion is met only when its `Validate:`\ncheck passes and every scenario its `Scenarios:` names is `Verified`." + "Run `sce plan check {plan-path}` before scenario\nverification." "Record each reported diagnostic as a failed check" "Trust the checker only when it actually ran:" "`structured plan: reference integrity holds`" - "`legacy plan: structured reference rules do not apply`" "Any other outcome means the checker is unavailable;" "apply the manual pass below instead." } @@ -1388,15 +1377,14 @@ hidden assertValidateScenarioEvidence = (documents: Mapping) -> || requiredValidateScenarioEvidenceTokens.every((token) -> text.contains(token)) ) && noNextTaskPlanCheckReference.apply(documents) - ) "sce-validate validation: structured plans get relationship integrity and scenario evidence; legacy plans keep the legacy path" - else throw("generated sce-validate validation.md must detect structured plans, fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, keep sce plan check out of every sce-next-task document, and keep legacy plans unchanged") + ) "sce-validate validation: plans get relationship integrity and scenario evidence" + else throw("generated sce-validate validation.md must fail on relationship-integrity defects, run every scenario check itself with exactly one of Verified, Partially verified, or Not verified, check an acceptance criterion only when its Validate: check passes and all its scenarios are Verified, exclude task completion and plan claims as evidence, run sce plan check before scenario verification with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") local requiredValidationReportScenarioTokens = new Listing { "### Scenario verification" "- S1 (AC1): {test | command | inspection} `{exact test, command, or inspected location}` -> {actual result}; status: {Verified | Partially verified | Not verified}" "; scenarios: S1" - "- Include **Scenario verification** only for a structured plan, with exactly\n one entry per scenario in plan order." - "Omit the section for a legacy plan." + "- Include **Scenario verification** with exactly one entry per scenario in\n plan order." } local requiredValidationResultScenarioTokens = new Listing { @@ -1404,7 +1392,6 @@ local requiredValidationResultScenarioTokens = new Listing { "; status: Verified" "- `{scenario id}` ({AC ids}): {Partially verified | Not verified}" "In the failed variant, list\n every `Partially verified` or `Not verified` scenario under **What failed**." - "- Omit **Scenario verification** and the scenario citations for a legacy plan." } hidden assertValidationReportScenarioSection = (documents: Mapping) -> @@ -1418,8 +1405,8 @@ hidden assertValidationReportScenarioSection = (documents: Mapping) -> requiredValidationResultScenarioTokens.every((token) -> text.contains(token)) else true ) - ) "sce-validate reports: structured plans get a scenario verification section; legacy plans omit it" - else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under What failed, and omit the section for legacy plans") + ) "sce-validate reports: scenario verification section present" + else throw("generated sce-validate validation-report.md and validation.md must carry a scenario verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under What failed") hidden assertWorkflowLanguageConsistency =(artifacts: Mapping) -> let (targets = generatedTargetRoots) diff --git a/config/pkl/renderers/plan-structure-check.pkl b/config/pkl/renderers/plan-structure-check.pkl index ac390ad06..c67831542 100644 --- a/config/pkl/renderers/plan-structure-check.pkl +++ b/config/pkl/renderers/plan-structure-check.pkl @@ -3,7 +3,6 @@ import "plan-structure.pkl" as planStructure local planPath = read("prop:planPath") local reports = read*("file:\(planPath)").toMap().mapValues((_, plan) -> planStructure.check(plan.text)) local failures = reports.filter((_, report) -> !report.defects.isEmpty) -local structuredCount = reports.filter((_, report) -> report.structured).length result = if (reports.isEmpty) throw("no plan matched `\(planPath)`") @@ -15,7 +14,5 @@ result = .values .join(" | ") ) - else if (reports.length > 1) - "\(reports.length) plans checked: \(structuredCount) structured with reference integrity, \(reports.length - structuredCount) legacy" - else if (structuredCount == 1) "structured plan: reference integrity holds" - else "legacy plan: structured reference rules do not apply" + else if (reports.length > 1) "\(reports.length) plans checked: reference integrity holds" + else "structured plan: reference integrity holds" diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl index e036ec928..05daf9620 100644 --- a/config/pkl/renderers/plan-structure.pkl +++ b/config/pkl/renderers/plan-structure.pkl @@ -17,7 +17,6 @@ class PlanEntry { } class PlanReport { - structured: Boolean defects: List } @@ -283,21 +282,15 @@ local function inspect(text: String, skeleton: Boolean): PlanReport = let (planLines = annotate(text)) let (headings = headingsOf(planLines)) let (defined = entries(planLines)) - let (isStructured = - headings.contains("### Verification scenarios") - || defined.any((entry) -> entry.kind == "T" && fieldOf(entry, "Covers") != null)) new PlanReport { - structured = isStructured defects = - if (!isStructured) List() - else - headingDefects( - headings, - if (skeleton) templateSkeletonHeadings else requiredPlanHeadings, - if (skeleton) templateSkeletonHeadings else orderedPlanHeadings - ) - + definitionDefects(defined) - + (if (skeleton) List() else misplacedDefinitionDefects(planLines) + referenceDefects(planLines, defined)) + headingDefects( + headings, + if (skeleton) templateSkeletonHeadings else requiredPlanHeadings, + if (skeleton) templateSkeletonHeadings else orderedPlanHeadings + ) + + definitionDefects(defined) + + (if (skeleton) List() else misplacedDefinitionDefects(planLines) + referenceDefects(planLines, defined)) } function check(text: String): PlanReport = inspect(text, false) @@ -331,9 +324,7 @@ local function sampleDefects(label: String, sample: String?, skeleton: Boolean): ( if (sample == null) List("no fenced example found") else - let (report = inspect(sample, skeleton)) - (if (report.structured) List() else List("example is not detected as a structured plan")) - + report.defects + inspect(sample, skeleton).defects + exampleShapeDefects(sample) ).map((defect) -> "\(label): \(defect)") diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index a99d302b2..46f4d467b 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -27,7 +27,7 @@ `validate` mirrors the `check` path of `config/pkl/renderers/plan-structure.pkl`. Template-skeleton mode stays Pkl-only. Defect messages are copied verbatim from the Pkl checker, so changing their wording means changing both checkers. -- A plan is structured when it has a `### Verification scenarios` heading or any task carries `Covers:`. A legacy plan gets `structured: false` and no diagnostics. +- Every plan is checked as a structured plan. There is no legacy classification: a plan in the pre-structured format fails with its missing `Scenarios:`, `Covers:`, and coverage diagnostics. - Heading rules, with headings compared as `#`×depth plus text: - `## Acceptance criteria` and `## Task stack` are required. - `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack` may each appear only once, in that order. @@ -56,7 +56,6 @@ `sce plan check ` checks exactly the file at ``, relative or absolute. It is synchronous, uses no database or repository root, and never runs `Check:`, `Validate:`, or `Verify:` content. - Valid structured plan: stdout `structured plan: reference integrity holds`, exit `0`. -- Valid legacy plan: stdout `legacy plan: structured reference rules do not apply`, exit `0`. - Structurally invalid plan: `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`). The stderr message is a `Plan structure check failed for '{path}':` header, one `{path}:{line}: {message}` line per diagnostic (`{path}: {message}` when the diagnostic has no line) in `validate` order, and a closing `Try:` line. `{path}` is printed as given. - Missing or unreadable file: `CliError::runtime` (exit `4`, `SCE-ERR-RUNTIME`) with `Failed to read plan file '{path}'` context, and no diagnostics. @@ -64,7 +63,7 @@ ## Terms -- `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `structured`, `legacy`, or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. +- `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `pass` or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. - `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. @@ -74,6 +73,6 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at ## Tests -`cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, legacy plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. +`cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, unstructured plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. -Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. +Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. diff --git a/context/context-map.md b/context/context-map.md index d854762db..a1e6d22ba 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -59,7 +59,7 @@ Feature/domain context: - `context/sce/claude-raw-hook-capture.md` (removed feature: the former `sce hooks claude-capture` raw-capture route and its supporting types, replaced by the active `diff-trace` and `conversation-trace` intakes) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with explicit config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary, logger and deliberately non-object-safe generic async telemetry boundaries with static capability dispatch and future-aware polling scope, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, managed OTLP trace/log export (credential-gated, hardcoded canonical collector, allowlisted fields, bounded fail-open lifecycle, reserved metrics endpoint), and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) -- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, structured vs legacy plan detection), clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) +- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, no legacy plan format), clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) - [Plan structure contract](sce/plan-structure-contract.md) (shared Pkl/Rust plan-structure checker contract: shared rule set, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` as the single fixture source of truth and how to add a fixture, `sce plan check` result and diagnostic wording shared with the Pkl defect strings, the both-checkers rewording rule, known AST-versus-line-scanner divergences, and Pkl staying generation-only while the CLI never invokes Pkl) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) - `context/sce/plan-code-overlap-map.md` (overlap matrix for thin OpenCode Plan/Code routing agents and six workflow packages, including task-only context synchronization ownership) diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 5c367d0e5..8eb7a5e86 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -53,7 +53,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `structured`/`legacy`/`fail` verdict, then expected defect substrings; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `structured` and `legacy` rows must pass with that classification and no listed substrings, `fail` rows must fail with every listed substring, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, fenced example repeating active IDs, a snapshot of a legacy plan, and a legacy plan with a fenced structured example; the negative fixtures are duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing a structured plan with the checker trust rule, manual `Reference rules` fallback, and legacy skip. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` detects structured plans, carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks, and states the legacy-plan path; `output.md` carries the optional `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` detects structured plans and states the legacy-plan path, lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, list unverified scenarios under **What failed**, and omit the section for legacy plans. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `pass`/`fail` verdict, then expected defect messages; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `pass` rows must pass with no listed messages, `fail` rows must fail with exactly the listed messages, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, and fenced example repeating active IDs; the negative fixtures are an unstructured plan snapshot, an unstructured plan with a fenced structured example, duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing the plan with the checker trust rule and manual `Reference rules` fallback. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks; `output.md` carries the `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under **What failed**. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/plan-structure-contract.md b/context/sce/plan-structure-contract.md index 8a165183c..6ed689570 100644 --- a/context/sce/plan-structure-contract.md +++ b/context/sce/plan-structure-contract.md @@ -11,7 +11,7 @@ This contract defines what the two must agree on, how that agreement is enforced Both checkers apply the same rules to the plan source outside fenced code blocks: -- **Classification.** A plan is structured when it has a `### Verification scenarios` heading or any task under `## Task stack` carries `Covers:`. Every other plan is legacy and gets no defects. +- **No legacy classification.** Every plan is checked against the full rule set below. A plan in the pre-structured format fails with its missing-field and coverage defects. - **Headings.** - `## Acceptance criteria` and `## Task stack` are required. - `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack` appear at most once each, in that order. @@ -34,8 +34,8 @@ Template-skeleton mode (`templateDefects` in `plan-structure.pkl`) validates gen The single source of truth is `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`. It covers the `*.md` fixtures in the same directory. -- Each row is tab-separated: the fixture file name, then its verdict (`structured`, `legacy`, or `fail`). `fail` rows then list the complete expected defect messages, one per column. Comparison is exact on the sorted multiset of messages: missing, unexpected, or miscounted duplicate defects fail. Source lines are not part of the manifest; Rust tests assert them separately. Rows starting with `#` are comments. -- `config/pkl/check-generated.sh` iterates the manifest. `structured` and `legacy` rows must print their classification line through `plan-structure-check.pkl`, and `fail` rows must make it fail. Every row is then compared against the structured report from `config/pkl/renderers/fixtures/plan-structure-report.pkl`: classification on the first line, then one defect per line. The run aborts on any of these: a missing fixture, an unknown verdict, a pass row with diagnostics, a fail row without diagnostics, a classification or diagnostic-set mismatch, or a `*.md` fixture not listed exactly once. +- Each row is tab-separated: the fixture file name, then its verdict (`pass` or `fail`). `fail` rows then list the complete expected defect messages, one per column. Comparison is exact on the sorted multiset of messages: missing, unexpected, or miscounted duplicate defects fail. Source lines are not part of the manifest; Rust tests assert them separately. Rows starting with `#` are comments. +- `config/pkl/check-generated.sh` iterates the manifest. `pass` rows must print `structured plan: reference integrity holds` through `plan-structure-check.pkl`, and `fail` rows must make it fail. Every row is then compared against the structured report from `config/pkl/renderers/fixtures/plan-structure-report.pkl`: one defect per line. The run aborts on any of these: a missing fixture, an unknown verdict, a pass row with diagnostics, a fail row without diagnostics, a wrong pass result or diagnostic-set mismatch, or a `*.md` fixture not listed exactly once. - The Rust test `plan::fixture_expectations_match_manifest` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. It requires the same verdicts and the same exact sorted message multiset, and fails on any unlisted fixture. - To add a fixture: 1. Add the `.md` file to the directory. @@ -48,14 +48,13 @@ The single source of truth is `config/pkl/renderers/fixtures/plan-structure/expe ## Result and diagnostic wording - Valid structured plan: `structured plan: reference integrity holds`. -- Valid legacy plan: `legacy plan: structured reference rules do not apply`. - Defect messages are the Pkl defect strings, used verbatim by the Rust validator. Examples are ``heading `## Task stack` is missing``, `` `S1` is defined more than once``, and `` `Covers:` on `T01` names `S1`, which is not an acceptance criterion ``. - `sce plan check` exit and output behavior: - - It exits `0` and prints the classification line on stdout. + - It exits `0` and prints `structured plan: reference integrity holds` on stdout. - For a structurally invalid plan, it exits `3` (`SCE-ERR-VALIDATION`). Stderr lists one diagnostic per line as `{path}:{line}: {message}`, or `{path}: {message}` for a plan-level defect with no line. Diagnostics are ordered by line, then message. - It exits `4` (`SCE-ERR-RUNTIME`) for a missing or unreadable file. - The Pkl checker throws one joined `plan failed the structural reference-integrity check: …` message. Its result strings match the Rust stdout lines exactly. -- Changing a classification line or a defect message is a change to both checkers. Update `plan-structure.pkl`, `cli/src/services/plan/validate.rs` or `command.rs`, any affected manifest substrings, and the workflow trust rule in [plan workflow](shared-context-plan-workflow.md). The trust rule matches the two classification lines. +- Changing the pass line or a defect message is a change to both checkers. Update `plan-structure.pkl`, `cli/src/services/plan/validate.rs` or `command.rs`, any affected manifest substrings, and the workflow trust rule in [plan workflow](shared-context-plan-workflow.md). The trust rule matches the pass line. ## Known divergences diff --git a/context/sce/shared-context-code-workflow.md b/context/sce/shared-context-code-workflow.md index 13c4a3723..511b1ba7c 100644 --- a/context/sce/shared-context-code-workflow.md +++ b/context/sce/shared-context-code-workflow.md @@ -20,16 +20,16 @@ Phase names below identify canonical modules in `config/pkl/base/workflow-next-t 1. `sce-plan-review` - Resolves exactly one plan and at most one task. - Read-only debt detector: inspects every completed task's context lifecycle in plan order, regardless of its position relative to the task being selected or resumed, before allowing any new task to start. A missing field, or any value other than `synced`, is unresolved debt. It never retries or invokes task context synchronization itself. - - When the first debt-carrying task has no durable completed-task record (no `Files changed`, `Result`, `Verify`, or `Context impact` recorded — a legacy plan predating that structure, or an incomplete write), it returns `blocked` directly with migration guidance rather than a reconstructed retry. + - When the first debt-carrying task has no durable completed-task record (no `Files changed`, `Result`, `Verify`, or `Context impact` recorded, for example after an incomplete write), it returns `blocked` directly with migration guidance rather than a reconstructed retry. - Otherwise it returns `sync_debt`, naming the debt task's ID and title and its own completed record — read directly from the plan by plan path and task ID — and, when its lifecycle is `blocked`, its persisted "Context synchronization blocker". - Once every completed task is `synced`, selects the requested task or the first incomplete task whose declared dependencies are complete. - - For a structured plan (see `shared-context-plan-workflow.md`), a `ready` result also carries the selected task's `Covers:` acceptance criteria with their `Validate:` checks, their scenarios' `Then:` and `Check:`, and their related design decisions. A dangling reference is a review risk, never a blocker. A legacy plan gets no coverage lookup and no coverage fields. + - A `ready` result (see `shared-context-plan-workflow.md`) also carries the selected task's `Covers:` acceptance criteria with their `Validate:` checks, their scenarios' `Then:` and `Check:`, and their related design decisions. A dangling reference is a review risk, never a blocker. - Returns `ready`, `sync_debt`, `blocked`, or `plan_complete`. - Sync-debt recovery: an explicit top-level `/next-task` branch — not plan-review behavior — reached on `sync_debt` before normal task selection resumes. It reads `references/context-sync.md`, then runs task context synchronization using the debt task's completed record read directly from the plan by plan path and task ID (and persisted blocker, when present). On `synced`/`no_context_change` it writes `synced` to the plan, clearing blocker fields, and re-invokes plan review to resume normal task selection. On a renewed `blocked` it writes the refreshed blocker, required action, and retry condition to the plan and stops, rendering the **Context synchronization blocked** layout — distinct from plan review's own **Review blocked** layout. 2. `sce-task-execution` - Receives the complete `ready` result. - Always presents the implementation gate before editing. - - For a structured plan, the gate adds an optional `## Acceptance coverage` section before `## Verification`; it is omitted for a legacy plan. + - The gate adds an `## Acceptance coverage` section before `## Verification`. - Waits for confirmation unless the user supplied `approved` to the command. - Captures a pre-edit Git baseline, then reports `changes.files_changed` by comparing post-edit state with that baseline, excluding unchanged unrelated staged, unstaged, @@ -69,10 +69,9 @@ A context-sync blocker does not undo successful implementation: the task remains `/validate {plan-name-or-path}` 1. `sce-validation` verifies that implementation tasks are complete, runs the plan's full validation commands and acceptance checks, records leftover debug/temp artifacts as failure evidence without deleting or repairing them, and writes the Validation Report. - - For a structured plan (see `shared-context-plan-workflow.md`), it also collects decisions, scenarios, and `Covers:`, then checks plan references against the template's `Reference rules`: a duplicate ID, a dangling `Decisions:`/`Scenarios:`/`Covers:` ID, an acceptance criterion without `Scenarios:` or with no covering task, and an unreferenced scenario are each a failed check that makes the result `failed`. + - It also collects (see `shared-context-plan-workflow.md`) decisions, scenarios, and `Covers:`, then checks plan references against the template's `Reference rules`: a duplicate ID, a dangling `Decisions:`/`Scenarios:`/`Covers:` ID, an acceptance criterion without `Scenarios:` or with no covering task, and an unreferenced scenario are each a failed check that makes the result `failed`. - It runs or performs every scenario's `Check:` itself and gives each scenario exactly one status: `Verified`, `Partially verified`, or `Not verified`. Only evidence from this run counts; task completion, recorded task `Verify` outcomes, planned `Check:` lines, and plan claims never do. An acceptance criterion is met only when its `Validate:` check passes and every scenario it names is `Verified` (a `none (inspection)` criterion needs only `Validate:`). - - Both Validation Report formats (the plan-file `## Validation Report` and the returned validated/failed report) add a scenario-verification section for a structured plan. It has one entry per scenario, in plan order, mapping the scenario to its acceptance criteria, evidence kind (`test`, `command`, or `inspection`), exact evidence, actual result, and status. Acceptance-criterion lines cite their scenario IDs, and the failed report lists every `Partially verified` or `Not verified` scenario under **What failed**. - - A legacy plan gets no scenario collection, reference check, scenario statuses, or scenario-verification report section. + - Both Validation Report formats (the plan-file `## Validation Report` and the returned validated/failed report) add a scenario-verification section. It has one entry per scenario, in plan order, mapping the scenario to its acceptance criteria, evidence kind (`test`, `command`, or `inspection`), exact evidence, actual result, and status. Acceptance-criterion lines cite their scenario IDs, and the failed report lists every `Partially verified` or `Not verified` scenario under **What failed**. 2. Failed or blocked validation ends the session without repair edits; repair occurs in a later implementation session and retry uses `/validate {plan-path}`. 3. A validated result is reported directly with the Validation Report path; `/validate` does not invoke plan-level context synchronization or persist a plan-sync lifecycle handoff. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 15e73d74d..423cd2844 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -73,10 +73,9 @@ indented under it. To check a fenced example itself, its content is extracted and checked as a separate sample document. `/change-to-plan`, `/next-task`, and `/validate` all read plans this way. -A plan is **structured** when, outside fenced code blocks, it has a -`### Verification scenarios` heading or any task under `## Task stack` carries -`Covers:`; every other plan is **legacy** and keeps the prior format. A revision keeps a legacy plan legacy unless the correction explicitly -asks to adopt the structured format. +Every plan uses this structured format. There is no legacy plan format: +`sce plan check` rejects a plan without `Scenarios:` on its acceptance criteria +and `Covers:` on its tasks. ```mermaid flowchart LR @@ -93,11 +92,10 @@ architectural choices and omits `## Design decisions` when there are none. It writes scenarios only for the case kinds (normal, boundary, failure, cancellation, concurrency, recovery) the change makes relevant, and a `Check:` never names a test that neither exists nor is created by a task. Before writing -a structured plan, a reference-consistency pass applies every `Reference rules` -entry to the full draft and fixes each defect first. After writing a structured +the plan, a reference-consistency pass applies every `Reference rules` +entry to the full draft and fixes each defect first. After writing the plan, it runs `sce plan check {plan-path}`, fixes every reported diagnostic, and -re-runs the checker before returning `plan_ready`. A legacy-plan revision -skips this structured work, including the checker. The phase cites the template's rule sections by +re-runs the checker before returning `plan_ready`. The phase cites the template's rule sections by name and does not restate them. Responsibility for structural consistency is split by surface. Generation checks @@ -116,11 +114,10 @@ The two checkers share one rule set, wording, and fixture manifest; see the [plan structure contract](plan-structure-contract.md). `/change-to-plan` (after writing) and `/validate` (step 4, before scenario -verification) run `sce plan check {plan-path}` on structured plans only; +verification) run `sce plan check {plan-path}`; `/validate` records each diagnostic as a failed check. Both trust a result only when the checker actually ran: exit `0` with -`structured plan: reference integrity holds` or -`legacy plan: structured reference rules do not apply` on stdout, or exit `3` +`structured plan: reference integrity holds` on stdout, or exit `3` with diagnostics. Any other outcome, such as an older `sce` that answers the unknown `plan` command with help and exit `0`, means the checker is unavailable, and the workflow applies the manual `Reference rules` pass instead and records diff --git a/context/sce/workflow-vocabulary.md b/context/sce/workflow-vocabulary.md index d45d7075f..e8b558d2a 100644 --- a/context/sce/workflow-vocabulary.md +++ b/context/sce/workflow-vocabulary.md @@ -37,9 +37,9 @@ workflow context. Do not create a second glossary for the same concepts. `sce-decision`. - **Verification scenario** — a plan-local `S{n}` entry stating one behavior as `Given:`, `When:`, `Then:`, and a planned `Check:`. -- **Structured plan** — a plan with a `### Verification scenarios` heading or - any task carrying `Covers:`; the plan template's `Reference rules` apply to it. -- **Legacy plan** — any other plan; it keeps the prior format and behavior. +- **Structured plan** — the only plan format: acceptance criteria carry + `Scenarios:`, tasks carry `Covers:`, and the plan template's `Reference rules` + apply. There is no legacy plan format. ## Verbs From 9d13dc3b05fe59efcb0c83758db6c98f5277245a Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 14:21:17 +0200 Subject: [PATCH 17/61] plan: Generate and embed the plan init scaffold Add a canonical Pkl-rendered scaffold for new plans by sharing the existing plan-format fragments and embedding the generated artifact in the CLI payload. Pin the scaffold's skeleton and validation defects across generation contracts, fallback assets, and plan-service tests so an untouched scaffold cannot pass as an authored plan. Plan: sce-plan-init Task: T01 Co-authored-by: SCE --- cli/build.rs | 1 + cli/src/services/plan/mod.rs | 35 +++ config/pkl/base/plan-init-scaffold.pkl | 65 +++++ config/pkl/base/workflow-change-to-plan.pkl | 42 ++- config/pkl/check-generated.sh | 2 + config/pkl/generate.pkl | 4 + .../renderers/generation-contract-check.pkl | 38 ++- config/pkl/renderers/plan-structure.pkl | 2 + context/cli/plan-service.md | 4 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/patterns.md | 2 +- context/plans/sce-plan-init.md | 270 ++++++++++++++++++ context/sce/config-generation.md | 7 +- context/sce/plan-structure-contract.md | 2 +- scripts/test-prepare-cli-generated-assets.sh | 8 +- 16 files changed, 458 insertions(+), 28 deletions(-) create mode 100644 config/pkl/base/plan-init-scaffold.pkl create mode 100644 context/plans/sce-plan-init.md diff --git a/cli/build.rs b/cli/build.rs index c20036677..033965555 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -326,6 +326,7 @@ fn validate_staged_artifacts(out_dir: &Path) -> io::Result<()> { "static/schema/agent-trace.schema.json", "pkl-generated/config/schema/sce-config.schema.json", "pkl-generated/config/optional-workflows.json", + "pkl-generated/config/plan-init-scaffold.md", ] { if !out_dir.join(relative_path).exists() { return Err(invalid_data(&format!( diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 590665237..7b709894e 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -4,6 +4,15 @@ pub mod validate; pub const NAME: &str = "plan"; +#[cfg_attr(not(test), expect(dead_code))] +pub(crate) const SCAFFOLD: &str = include_str!(concat!( + env!("OUT_DIR"), + "/pkl-generated/config/plan-init-scaffold.md" +)); + +#[cfg_attr(not(test), expect(dead_code))] +pub(crate) const SCAFFOLD_PLAN_NAME_TOKEN: &str = "{plan-name}"; + #[cfg(test)] fn fixture_dir() -> std::path::PathBuf { std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -155,6 +164,32 @@ fn misplaced_definition_ignores_prose_and_fenced_mentions() { ); } +#[test] +fn scaffold_untouched_fails_check() { + assert_eq!(SCAFFOLD.matches(SCAFFOLD_PLAN_NAME_TOKEN).count(), 1); + let source = SCAFFOLD.replacen(SCAFFOLD_PLAN_NAME_TOKEN, "untouched", 1); + assert!(source.starts_with("# Plan: untouched\n"), "{source}"); + + let plan = extract::extract(&source).expect("scaffold should parse"); + assert_eq!(plan.entries_of(extract::PlanEntryKind::Decision).count(), 0); + assert_eq!(plan.entries_of(extract::PlanEntryKind::Scenario).count(), 0); + + let report = validate::validate(&plan); + let mut actual: Vec<&str> = report + .diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect(); + let mut expected = vec![ + "`Scenarios:` on `AC1` is not a comma-separated ID list", + "`AC1` is not covered by any task's `Covers:`", + "`Covers:` on `T01` is not a comma-separated ID list", + ]; + actual.sort_unstable(); + expected.sort_unstable(); + assert_eq!(actual, expected); +} + #[cfg(test)] struct CommandRun { exit_code: std::process::ExitCode, diff --git a/config/pkl/base/plan-init-scaffold.pkl b/config/pkl/base/plan-init-scaffold.pkl new file mode 100644 index 000000000..896e97180 --- /dev/null +++ b/config/pkl/base/plan-init-scaffold.pkl @@ -0,0 +1,65 @@ +import "workflow-change-to-plan.pkl" as changeToPlan + +planNameToken = "{plan-name}" + +text = """ +# Plan: \(planNameToken) + +## Change summary + +TODO: Describe what changes, where, and why in one or two paragraphs. + +## Design decisions + +TODO: Record each real architectural choice with Boundary, Rationale, Rejected, and Trade-offs, or delete this section when there are none. + +## Acceptance criteria + +\(changeToPlan.planAcceptanceCriteriaIntro) + +- [ ] AC1: TODO: State one observable outcome. + - Validate: TODO + - Scenarios: TODO + +### Verification scenarios + +TODO: Add the scenarios that acceptance criteria reference, each with Given, When, Then, and Check. + +### Full validation + +\(changeToPlan.planFullValidationIntro) + +- TODO: List the repository-wide check commands. + +### Context sync + +- TODO: List the durable context files that must describe the change. + +\(changeToPlan.planTaskContextSynchronizationLifecycle) + +## Constraints and non-goals + +- **In scope:** TODO +- **Out of scope:** TODO +- **Constraints:** TODO +- **Non-goal:** TODO + +## Assumptions + +TODO: Record assumptions and the conventions they rest on, or delete this section when there are none. + +## Task stack + +- [ ] T01: `TODO` (status:todo) + - Task ID: T01 + - Covers: TODO + - Scope: In — TODO. Out — TODO. + - Dependencies: none + - Done when: TODO + - Verify: TODO + - Context synchronization: pending + +## Open questions + +TODO: List non-blocking questions, or write `None.` with a short justification. +""" diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index f9ef97784..97c2d961c 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -512,6 +512,30 @@ Do not: +planAcceptanceCriteriaIntro = """ +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. +""" + +planFullValidationIntro = """ +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. +""" + +planTaskContextSynchronizationLifecycle = """ +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. +""" + changeToPlanPlanTemplate = """ # Internal persisted-document format: Plan template @@ -546,9 +570,7 @@ the section when there are none.} ## Acceptance criteria -How this plan is proven complete. Each criterion is observable and names the -check that proves it. `/validate` runs these checks; no task in the stack -performs final validation. +\(planAcceptanceCriteriaIntro) - [ ] AC1: {observable outcome, stated as behavior rather than as work done} - Validate: `{command, assertion, or inspection that proves AC1}` @@ -568,8 +590,7 @@ performs final validation. ### Full validation -Repository-wide checks `/validate` runs after the last task, regardless of -which criterion they map to. +\(planFullValidationIntro) - `{full check suite command}` - `{generated-output or parity check command, when applicable}` @@ -578,16 +599,7 @@ which criterion they map to. - {Durable context files that must describe the change once implemented.} -## Task context synchronization lifecycle - -Persist this field in every plan; this is durable plan state, not chat state: - -- **Task context synchronization:** every task carries `pending | synced | blocked`. - A completed task must be `synced` before another task can start or the plan can - finish. -- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** - beside the status. Never infer `synced` from conversation history; write every - lifecycle transition to the plan file. +\(planTaskContextSynchronizationLifecycle) ## Constraints and non-goals diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index f6db2606e..35bb3fc8d 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -36,6 +36,7 @@ legacy_paths=( "config/.claude" "config/.pi" "config/schema/sce-config.schema.json" + "config/plan-init-scaffold.md" "cli/assets/generated" ) @@ -338,6 +339,7 @@ required_paths=( "config/.pi/skills" "config/.pi/extensions/sce/index.ts" "config/schema/sce-config.schema.json" + "config/plan-init-scaffold.md" ) for path in "${required_paths[@]}"; do diff --git a/config/pkl/generate.pkl b/config/pkl/generate.pkl index 84ae2ac8b..91df38dfe 100644 --- a/config/pkl/generate.pkl +++ b/config/pkl/generate.pkl @@ -6,6 +6,7 @@ import "renderers/common.pkl" as common import "base/sce-config-schema.pkl" as sce_config_schema import "base/optional-workflow-manifest.pkl" as optional_workflow_manifest import "base/bash-policy-presets.pkl" as bash_policy_presets +import "base/plan-init-scaffold.pkl" as plan_init_scaffold local bashPolicyPresetCatalogSource = bash_policy_presets.output.text local opencodeBashPolicyPluginSource = read("../lib/bash-policy-plugin/opencode-bash-policy-plugin.ts").text @@ -96,5 +97,8 @@ output { ["config/optional-workflows.json"] { text = optional_workflow_manifest.rendered } + ["config/plan-init-scaffold.md"] { + text = "\(plan_init_scaffold.text)\n" + } } } diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 762c9260e..8a93c72d2 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -54,6 +54,7 @@ hidden expectedArtifactPaths = new Mapping { ["config/schema/sce-config.schema.json"] = true ["config/optional-workflows.json"] = true + ["config/plan-init-scaffold.md"] = true } hidden generatedArtifacts = new Mapping { @@ -521,10 +522,10 @@ local forbiddenWorkflowReferenceTokens = new Listing { /// package and retained non-workflow assets, across OpenCode, Claude, Pi, and /// Codex, plus Codex's `.codex/hooks.json`, its install-guidance hook script, /// and its six per-workflow `agents/openai.yaml` files (one per catalog -/// workflow, excluding `sce-decision`). Stating the total as a literal makes -/// an unintended inventory change fail here instead of silently becoming the -/// new expectation. -local expectedArtifactPathCount = 145 +/// workflow, excluding `sce-decision`), plus the plan init scaffold. Stating +/// the total as a literal makes an unintended inventory change fail here +/// instead of silently becoming the new expectation. +local expectedArtifactPathCount = 146 local assertExactArtifactPaths = (actual: Mapping) -> if ( @@ -1041,6 +1042,34 @@ hidden assertPlanTemplateStructure = (documents: Mapping) -> ) "generated plan template: headings, definitions, fields, and references are structurally valid outside fenced examples" else throw("generated plan-template.md failed the structural reference-integrity check: " + defects.join("; ")) +local planInitScaffoldPath = "config/plan-init-scaffold.md" + +local planInitScaffoldCheckDefects = List( + "`Scenarios:` on `AC1` is not a comma-separated ID list", + "`Covers:` on `T01` is not a comma-separated ID list", + "`AC1` is not covered by any task's `Covers:`" +) + +hidden assertPlanInitScaffold = (artifacts: Mapping) -> + let (text = artifacts.getOrNull(planInitScaffoldPath) ?? "") + let (checkDefects = planStructure.check(text).defects) + let (definedKinds = planStructure.entries(planStructure.annotate(text)).map((entry) -> entry.kind)) + if ( + artifacts.containsKey(planInitScaffoldPath) + && text.startsWith("# Plan: {plan-name}\n") + && text.split("{plan-name}").length == 2 + && planStructure.skeletonDefects(text).isEmpty + && checkDefects.length == planInitScaffoldCheckDefects.length + && checkDefects.toSet() == planInitScaffoldCheckDefects.toSet() + && !definedKinds.contains("D") + && !definedKinds.contains("S") + && !text.contains("```") + && !text.contains("## Template") + && !text.contains("Filled-in") + && !text.contains(" rules") + ) "plan init scaffold: skeleton headings, single plan-name token, and exact untouched check defects pinned" + else throw("generated \(planInitScaffoldPath) must pass plan-template skeleton mode, carry `{plan-name}` exactly once, define no D or S entry, omit authoring rules and examples, and fail check with exactly: " + planInitScaffoldCheckDefects.join("; ") + "; skeleton defects: " + planStructure.skeletonDefects(text).join("; ") + "; check defects: " + checkDefects.join("; ")) + local planTemplateOwnedPolicyTokens = new Listing { "Acceptance criteria describe the finished system, not the work." "Every criterion carries a `Validate:` line." @@ -1498,6 +1527,7 @@ contractChecks { ["compact-plan-template-schema"] = assertCompactPlanTemplateSchema.apply(workflowDocuments) ["structured-plan-template-schema"] = assertStructuredPlanTemplateSchema.apply(workflowDocuments) ["plan-template-structure"] = assertPlanTemplateStructure.apply(workflowDocuments) + ["plan-init-scaffold"] = assertPlanInitScaffold.apply(generatedArtifacts) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) ["plan-authoring-structured-references"] = assertPlanAuthoringStructuredReferences.apply(workflowDocuments) ["next-task-structured-references"] = assertNextTaskStructuredReferences.apply(workflowDocuments) diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl index 05daf9620..7b262a55f 100644 --- a/config/pkl/renderers/plan-structure.pkl +++ b/config/pkl/renderers/plan-structure.pkl @@ -295,6 +295,8 @@ local function inspect(text: String, skeleton: Boolean): PlanReport = function check(text: String): PlanReport = inspect(text, false) +function skeletonDefects(text: String): List = inspect(text, true).defects + function fencedBlockUnder(text: String, heading: String): String? = let (planLines = annotate(text)) let (start = planLines.findIndexOrNull((line) -> line.fenced && line.section == heading)) diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index 46f4d467b..4d6fda7ee 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -7,6 +7,7 @@ - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. - `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. - `command.rs` owns the `sce plan check ` command (see [Command contract](#command-contract)). +- `mod.rs` embeds the generated plan init scaffold as the crate-visible `SCAFFOLD` constant (`include_str!` of `OUT_DIR/pkl-generated/config/plan-init-scaffold.md`) with its `SCAFFOLD_PLAN_NAME_TOKEN` (`{plan-name}`). `cli/build.rs` requires that staged path, so a payload or package fallback without the scaffold fails the build. The binary never renders the scaffold itself. ## Plan model @@ -66,6 +67,7 @@ - `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `pass` or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. - `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. +- `plan init scaffold`: The generated `config/plan-init-scaffold.md` plan skeleton rendered by `config/pkl/base/plan-init-scaffold.pkl` and embedded as `SCAFFOLD`. Untouched, it fails `sce plan check` with exactly its `Scenarios: TODO` and `Covers: TODO` reference defects (see the [plan structure contract](../sce/plan-structure-contract.md)). ## Divergences from the Pkl line scanner @@ -75,4 +77,4 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at `cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, unstructured plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. -Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. +Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. `plan::scaffold_untouched_fails_check` asserts the embedded scaffold has one `{plan-name}` token, defines no decision or scenario entries, and validates to exactly the three untouched-scaffold diagnostics. diff --git a/context/context-map.md b/context/context-map.md index a1e6d22ba..3b4b4610a 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -18,7 +18,7 @@ Feature/domain context: - `context/cli/agent-trace-storage.md` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) -- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, the shared `expectations.tsv` fixture manifest, and the `sce plan check ` command with its exit `0`/`3`/`4` output contract) +- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, the shared `expectations.tsv` fixture manifest, the `sce plan check ` command with its exit `0`/`3`/`4` output contract, and the embedded plan init scaffold constant) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) diff --git a/context/glossary.md b/context/glossary.md index 57d7031c9..bc891de48 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -1,5 +1,5 @@ # Glossary -- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`sce plan check`, `plan-structure expectations manifest`, `plan diagnostic`): [plan service terms](cli/plan-service.md#terms). +- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`sce plan check`, `plan-structure expectations manifest`, `plan diagnostic`, `plan init scaffold`): [plan service terms](cli/plan-service.md#terms). - `pkl-check-generated`: Flake app exposed as `nix run .#pkl-check-generated`; canonical ephemeral-generation check that rejects committed target/schema/mirror outputs, evaluates exact workflow metadata, the generated artifact contract, semantic layout/path/inventory/content/parity/observational checks, and the optional-workflow manifest's content against the catalog, requires the shared helper-composition rule and SCE-scoped workflow prohibitions, enforces ordered catalog-derived OpenCode skill permissions plus explicit-permission artifact integrity, rejects stale sibling-package references or unresolved internalization tokens in workflow entrypoint `SKILL.md` documents, proves contract failures through checked-in negative fixtures, and delegates deterministic generation plus payload/input inventories to the generated-input producer while preserving its established inventory report. - `repo-level verification preference`: Current repository guidance that contributor-facing validation/check flows should prefer `nix flake check`; direct Cargo verification commands are secondary and used only when explicitly requested or for narrow targeted debugging, while `cargo fmt` remains the explicit autofix path. Required quick checks after each completed task in this repo are `nix run .#pkl-check-generated` and `nix flake check`. - disposable plan lifecycle: Policy where `context/plans/` holds active execution artifacts only; completed plans are disposable and durable outcomes must be reflected in current-state context files and/or `context/decisions/`. diff --git a/context/patterns.md b/context/patterns.md index eb5e1a770..3660bd68b 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -102,7 +102,7 @@ - Use `config/pkl/generate.pkl` as the single generation module for authored config outputs. Flatten self-contained workflow skill documents as `{skill slug}/{package-relative path}` so nested references are emitted deterministically without sibling-package dependencies. - Use `config/pkl/README.md` as the contributor-facing runbook for prerequisites, ownership boundaries, regeneration steps, and troubleshooting. - Run multi-file generation only into an explicit temporary output root, for example `nix run .#pkl-generate -- "$(mktemp -d)"`; never evaluate with `-m .`. -- Run ephemeral generation validation through `nix run .#pkl-check-generated`; it wraps the dev-shell script, rejects committed target/schema/mirror outputs, evaluates exact metadata plus the complete 145-path artifact/reference contract, the optional-workflow manifest assertion, and its negative fixtures, requires all supported target roots, and delegates canonical input discovery, two-pass generation, and inventories to `scripts/produce-cli-generated-input.sh`. +- Run ephemeral generation validation through `nix run .#pkl-check-generated`; it wraps the dev-shell script, rejects committed target/schema/mirror outputs, evaluates exact metadata plus the complete 146-path artifact/reference contract, the optional-workflow manifest assertion, and its negative fixtures, requires all supported target roots, and delegates canonical input discovery, two-pass generation, and inventories to `scripts/produce-cli-generated-input.sh`. - Keep this contract anchored to the root `nix flake check` `pkl-generated` derivation. The separate `codex-hook-command` check executes the generated Codex command from root and nested cwd, including spaced repository paths, and verifies unchanged STDIN plus silent fail-open behavior. Removed target paths are forbidden repository artifacts even though the same path names remain valid inside temporary payload roots. - Treat `nix run .#pkl-check-generated` and `nix flake check` as the lightweight post-task verification baseline and run both after each completed task. - Keep `output.files` limited to payload-relative paths (`config/.opencode/{agent,command,skills,lib,plugins,opencode.json}`, `config/.claude/{commands,skills,hooks,settings.json}` with no Claude agents, `config/.pi/{prompts,skills,extensions}`, and the generated schema). Do not emit `config/automated/.opencode`. diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md new file mode 100644 index 000000000..77fc6770f --- /dev/null +++ b/context/plans/sce-plan-init.md @@ -0,0 +1,270 @@ +# Plan: sce-plan-init + +## Change summary + +Add `sce plan init `, a deterministic, offline scaffold generator that +creates `context/plans/.md` at the Git repository root. It extends the +existing read-only `sce plan check` service in `cli/src/services/plan/` (PR #342, +HEAD `7c6dde7c`) rather than adding a parallel service. + +The scaffold is a new compact generated artifact, `config/plan-init-scaffold.md`, +rendered by the existing Pkl pipeline from the canonical plan format in +`config/pkl/base/workflow-change-to-plan.pkl`. It travels through the generated +payload handoff into Cargo `OUT_DIR`, and the packaged fallback carries it for +Pkl-free builds. The binary embeds it with `include_str!`, so the binary never +runs Pkl, Nix, an LLM, or the network. + +The untouched scaffold fails `sce plan check`. Its `Scenarios: TODO` and +`Covers: TODO` placeholders are not valid ID lists, so it cannot pass for an +authored plan. The `sce-change-to-plan` workflow then uses `init` to create new +plans, fills the scaffold, and runs `check` before it reports `plan_ready`. +Revisions of existing plans skip `init`. Older binaries fall back to manual +template authoring. + +## Design decisions + +- D1: The scaffold has its own Pkl renderer built from fragments shared with the canonical plan template. A generation contract pins it to the template's skeleton. + - Boundary: `config/pkl/base/workflow-change-to-plan.pkl` owns the plan format. A new `config/pkl/base/plan-init-scaffold.pkl` renders only the compact scaffold, using section fragments shared with `changeToPlanPlanTemplate`, such as the task context synchronization lifecycle text. `config/pkl/renderers/plan-structure.pkl` exposes its existing skeleton mode as a public function so the contract can check the scaffold. + - Rationale: The two documents share verbatim text through one source. The contract checks the parts that must agree: the skeleton heading list (`templateSkeletonHeadings`), the required fields for each kind, and the exact `check` defects of the untouched scaffold. The scaffold can leave out the authoring rules and examples without becoming a second template. + - Rejected: A Rust string template — it would be a second hand-maintained format outside the Pkl contract. Text-transforming the fenced `## Template` block — placeholders span several lines and carry authoring prose, so a transform would be fragile and hard to read. Emitting the full `plan-template.md` — it carries rules and filled-in examples that a plan file must not contain. + - Trade-offs: Scaffold prose such as TODO wording is authored separately from template prose. Drift between them is caught only at the heading, field, and defect level, not word for word. +- D2: The scaffold ships as a required generated-payload file and is embedded with `include_str!`. It is not a setup install asset. + - Boundary: `config/pkl/generate.pkl` emits `config/plan-init-scaffold.md` into the payload. `cli/build.rs` `validate_staged_artifacts` requires `pkl-generated/config/plan-init-scaffold.md`. `cli/src/services/plan/` includes it from `OUT_DIR`. `scripts/prepare-cli-generated-assets.sh` already moves the whole `pkl-generated` tree into the packaged fallback. + - Rationale: The same payload serves repository builds, `nix build .#default`, and the crates.io and Flatpak fallback. `include_str!` makes a missing scaffold a compile-time failure. + - Rejected: Adding it to `TARGETS` / `EmbeddedAsset` — that catalog feeds `sce setup` installs and would put the scaffold into harness trees. A committed copy under `config/` — this repository forbids committed generated output. + - Trade-offs: A fallback package prepared before this change no longer builds. That is intended, and the remedy is the existing prepare script. +- D3: `init` and `check` are variants of one native Rust enum inside the existing plan command, dispatched statically. + - Boundary: `cli_schema::PlanSubcommand` gains `Init { name }`. `services::parse::command_runtime::convert_plan_subcommand` maps each subcommand to a `PlanRequest::{Check, Init}` value. `PlanCommand::execute` matches on it. Init logic lives in a new `cli/src/services/plan/init.rs`, and `RuntimeCommand::Plan` stays the only registry entry. + - Rationale: This reuses the existing `plan` command name, help, and completion wiring. It also keeps the command synchronous, with no database or repository context. + - Rejected: A new top-level `sce init`-style command — it would split plan ownership. A trait or `dyn` operation object — the repository constraints rule these out, and two variants do not need them. + - Trade-offs: `PlanCheckRequest` moves behind an enum, so the existing `plan::command_*` tests change their destructuring. +- D4: The plan name is validated before any filesystem access, and the scaffold is published with a no-clobber hard link from a fully written temporary file. + - Boundary: Names must match `[a-z0-9]+(-[a-z0-9]+)*`, at most 64 bytes, with no `.md` suffix. The repository root comes from `git rev-parse --show-toplevel`, run from the current directory. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is written to a `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`, and the temporary file is removed on every path. + - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. A successful exit only ever exposes complete content, and a crash cannot leave a half-written plan that later blocks `init`. + - Rejected: `rename` — it silently replaces an existing destination. Writing directly with `create_new` — an interrupted write leaves a partial plan at the final path. Accepting a path argument — that would invite traversal and repository escape. + - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. +- D5: `init` reports results through the existing `CliError` classes and prints exactly three lines on stdout. + - Boundary: On success, stdout is `Created plan: context/plans/.md`, `Next: complete the plan and run`, and `sce plan check context/plans/.md`, and the exit code is `0`. An invalid name or an existing destination is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, or a filesystem failure is `CliError::runtime` (exit `4`). Clap usage errors keep the existing parse class (exit `2`). + - Rationale: Automation and the workflow can branch on the class without parsing text. The printed path is always repository-relative, whichever directory `init` ran from. + - Rejected: A dedicated new exit code or error class — the existing taxonomy already covers these failures. + - Trade-offs: A destination that already exists and an invalid name share exit `3`, so callers that need to tell them apart must read stderr. +- D6: The workflow runs `init` only when writing a new plan, and it confirms the file exists rather than trusting the exit code. + - Boundary: In `changeToPlanPlanAuthoring`, step 2.10 runs `sce plan init {plan_name}` for a new plan, after the clarification gate and target resolution. Initialization counts as successful only when stdout contains `Created plan: context/plans/{plan_name}.md` and that file exists. `init` counts as unavailable when it exits `0` without that line (an older binary printing `plan` help), exits `2`, or the command is not found. Only then does the workflow fall back to writing from `references/plan-template.md`. Exit `3` or `4` returns `blocked` with the reported error and no fallback. Revisions skip `init`. + - Rationale: An older `sce` (verified with `0.4.0-pre-alpha-v11`) prints `plan` help and exits `0` for `sce plan init foo`, so the exit code alone would report success with no file written. + - Rejected: Running `init` during 2.1 target resolution — it would write a file before the clarification gate. Falling back on any non-zero exit — it would hide real filesystem failures and collisions. + - Trade-offs: The workflow now depends on exact stdout wording, so changing D5's first line is a breaking change for the workflow. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [ ] AC1: `sce plan init example`, run inside a Git repository, creates `context/plans/example.md` holding the embedded scaffold with the title `# Plan: example`, creates `context/plans/` when it is missing, prints the three-line success output, and modifies no other repository file. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` + - Decisions: D3, D4, D5 + - Scenarios: S1 +- [ ] AC2: Existing destinations, including symlinks, are never overwritten, and unsafe names or a missing repository are rejected with the documented class before any file or directory is created. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` + - Decisions: D4, D5 + - Scenarios: S3, S4, S5 +- [ ] AC3: Running `init` from a nested directory of a Git repository creates the same repository-root-relative `context/plans/.md` and prints the same root-relative paths. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` + - Decisions: D4 + - Scenarios: S2 +- [ ] AC4: The untouched scaffold fails `sce plan check` with exactly the incomplete-reference defects, and the same scaffold with its reference placeholders filled correctly passes. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold` + - Decisions: D1 + - Scenarios: S6, S7 +- [ ] AC5: The scaffold is generated from the canonical Pkl sources and pinned to the template skeleton by the generation contract. It is embedded in the binary, it is present in the packaged fallback, and it is never committed to the repository. + - Validate: `nix run .#pkl-check-generated` + - Decisions: D1, D2 + - Scenarios: S8, S9 +- [ ] AC6: The generated `sce-change-to-plan` workflow runs `sce plan init` for new plans only, after its clarification and target-resolution gates. It verifies initialization by output and file existence, falls back to manual template authoring only when `init` is unavailable, skips `init` for revisions, and runs `sce plan check` before `plan_ready`. + - Validate: `nix run .#pkl-check-generated` + - Decisions: D6 + - Scenarios: S10, S11 +- [ ] AC7: `sce --help`, `sce plan --help`, and the generated Bash, Zsh, and Fish completion scripts include `plan init`. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` + - Decisions: D3 + - Scenarios: S12 + +### Verification scenarios + +- S1: Fresh initialization at the repository root + - Given: A temporary `git init` repository with no `context/` directory, the current directory at its root. + - When: `sce plan init example` runs. + - Then: Exit `0`. Stdout is exactly the three D5 lines for `context/plans/example.md`. That file equals the embedded scaffold with its single `{plan-name}` token replaced by `example`. `git status --porcelain` lists only `context/`. + - Check: `plan::init_creates_scaffold_at_repository_root`, created by T02. +- S2: Nested invocation resolves the repository root + - Given: A temporary Git repository with the current directory at `/a/b`. + - When: `sce plan init nested-plan` runs. + - Then: `/context/plans/nested-plan.md` exists, nothing is created under `/a/b`, and stdout names `context/plans/nested-plan.md`. + - Check: `plan::init_from_nested_directory_targets_repository_root`, created by T02. +- S3: Existing entries are never overwritten + - Given: A repository where `context/plans/taken.md` exists as a regular file with known bytes, and `context/plans/linked.md` is a dangling symlink. + - When: `sce plan init taken` and `sce plan init linked` run. + - Then: Both exit `3` (`SCE-ERR-VALIDATION`). `taken.md` keeps its bytes, `linked.md` is still the same symlink, and no temporary file remains in `context/plans/`. + - Check: `plan::init_never_overwrites_existing_entry`, created by T02. +- S4: Unsafe names are rejected before any write + - Given: A repository without `context/`. + - When: `sce plan init` runs with each of `../escape`, `a/b`, `Upper`, `trailing-`, `-leading`, `double--hyphen`, `plan.md`, an empty string, and a 65-byte name. + - Then: Each exits `3`, and `context/` still does not exist afterwards. + - Check: `plan::init_rejects_unsafe_names_without_writing`, created by T02. +- S5: Outside a Git repository + - Given: A temporary directory that is not inside any Git repository. + - When: `sce plan init example` runs there. + - Then: Exit `4` (`SCE-ERR-RUNTIME`), and the directory is left unchanged. + - Check: `plan::init_outside_repository_is_runtime_failure`, created by T02. +- S6: The untouched scaffold is not a ready plan + - Given: The embedded scaffold with its title token replaced. + - When: It is extracted and validated by the plan service. + - Then: The diagnostics are exactly ``Scenarios:` on `AC1` is not a comma-separated ID list``, ``AC1` is not covered by any task's `Covers:``, and ``Covers:` on `T01` is not a comma-separated ID list``. The scaffold defines no `D` or `S` entry. + - Check: `plan::scaffold_untouched_fails_check`, created by T01. +- S7: A correctly authored scaffold passes + - Given: A scaffold created by `sce plan init authored` in which `Scenarios: TODO` is replaced by `Scenarios: none (inspection)` and `Covers: TODO` by `Covers: AC1`. + - When: `sce plan check context/plans/authored.md` runs. + - Then: Exit `0`, with stdout `structured plan: reference integrity holds`. + - Check: `plan::scaffold_authored_passes_check`, created by T02. +- S8: The generation contract pins the scaffold + - Given: The Pkl sources after T01. + - When: Generation checks run. + - Then: `output.files` contains `config/plan-init-scaffold.md`, and the artifact count is one higher than before. The scaffold passes skeleton mode, fails `check` with exactly the S6 defects, and contains `{plan-name}` exactly once. No repository-local `config/plan-init-scaffold.md` exists. + - Check: `nix run .#pkl-check-generated`, with the `plan-init-scaffold` contract assertion created by T01. +- S9: The packaged fallback carries the scaffold + - Given: A fallback staged by `scripts/prepare-cli-generated-assets.sh`. + - When: The fallback test runs, and the packaged CLI is built. + - Then: `pkl-generated/config/plan-init-scaffold.md` is in the fallback inventory, and `nix build .#default` succeeds without a runtime Pkl dependency. + - Check: `bash scripts/test-prepare-cli-generated-assets.sh`, extended by T01, and `nix build .#default`. +- S10: New-plan authoring uses `init`, with a safe fallback + - Given: The generated `sce-change-to-plan/references/plan-authoring.md`. + - When: Its write step for a new plan is inspected. + - Then: It runs `sce plan init {plan_name}` after the clarification gate. It requires both the `Created plan:` line and an existing file. It falls back to the template only for exit `0` without that line, exit `2`, or a missing command. It returns `blocked` on exit `3` or `4`. It then runs `sce plan check {plan-path}` before `plan_ready`. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion created by T03. +- S11: Revisions skip `init` + - Given: The generated plan-authoring reference. + - When: Its existing-plan revision path is inspected. + - Then: It never runs `sce plan init`. It keeps the `Updating an existing plan` rules, which preserve completed tasks and evidence. No generated `sce-next-task` or `sce-validate` document mentions `sce plan init`. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion created by T03. +- S12: Help and completions expose `plan init` + - Given: The built CLI schema. + - When: Top-level help, `plan` help, and the Bash, Zsh, and Fish completion scripts are rendered. + - Then: Each mentions `init`, and `sce plan init --help` shows the `` argument. + - Check: `plan::plan_init_appears_in_help` and `plan::completion_scripts_include_plan_init`, created by T02. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix flake check` +- `nix run .#pkl-check-generated` +- `nix build .#default` + +### Context sync + +- `context/cli/plan-service.md`: `init.rs`, the `PlanRequest` enum, the init command contract, and the new tests. +- `context/sce/plan-structure-contract.md`: the scaffold's relationship to skeleton mode and its expected `check` defects. +- `context/sce/config-generation.md`: the new generated artifact, the public skeleton function, the `plan-init-scaffold` and `plan-authoring-plan-init` contract checks, and the artifact count. +- `context/sce/shared-context-plan-workflow.md`: `init` in new-plan authoring, the older-binary fallback, and revisions skipping `init`. +- `context/cli/cli-command-surface.md` and `context/sce/cli-shell-completion-contract.md`: the `sce plan init ` surface. +- `context/glossary.md`: a `plan scaffold` term. + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** + - `cli/src/services/plan/`, `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, and `cli/build.rs`. + - `config/pkl/base/workflow-change-to-plan.pkl`, a new `config/pkl/base/plan-init-scaffold.pkl`, and `config/pkl/generate.pkl`. + - `config/pkl/renderers/plan-structure.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, `config/pkl/check-generated.sh`, and `scripts/test-prepare-cli-generated-assets.sh`. + - The durable context files listed under Context sync. +- **Out of scope:** + - Semantic plan approval, or any change to `sce plan check` rules or wording. + - `/next-task` and `/validate` workflow content. + - Committing generated target trees or the scaffold. + - Validating real plans under `context/plans/` in Nix or generation checks. +- **Constraints:** + - No new traits, `dyn`, boxed futures, async runtime, database, or crate dependency. + - No interactive prompts. + - `init` runs offline and invokes only `git`. + - Output is deterministic. + - All Cargo commands run through `scripts/run-cli-cargo.sh` under `nix develop`. +- **Non-goal:** + - `init` does not author plan content, accept arbitrary paths, take `--force` or overwrite flags, or support template variants. + +## Assumptions + +- The 64-byte name limit and the `[a-z0-9]+(-[a-z0-9]+)*` grammar are local choices. They are consistent with the existing kebab-case plan names in `context/plans/`. +- Repository root resolution reuses the existing `git rev-parse --show-toplevel` pattern, as in `cli/src/services/bash_policy.rs`, rather than adding a new Git library. +- The scaffold keeps `## Design decisions` and `## Assumptions` as headed sections, each holding one TODO line that tells the author to delete the section when it has no real entries. It defines no `D`, `S`, or placeholder reference IDs, because skeleton mode requires every heading in `templateSkeletonHeadings`. + +## Task stack + +- [x] T01: `Generate and embed the plan init scaffold from canonical Pkl` (status:done) + - Task ID: T01 + - Covers: AC4, AC5 + - Scope: In: + - Shared plan-format fragments in `workflow-change-to-plan.pkl`, with no byte change to the generated `plan-template.md`. + - The new `plan-init-scaffold.pkl` and the `generate.pkl` entry. + - A public skeleton-check function in `plan-structure.pkl`. + - The `plan-init-scaffold` assertion and the artifact count of 146 in `generation-contract-check.pkl`. + - A required output and a forbidden committed path in `check-generated.sh`. + - A required staged path in `cli/build.rs`. + - A crate-visible `include_str!` constant in `cli/src/services/plan/`, with the `plan::scaffold_untouched_fails_check` test. + - A fallback-presence assertion in `scripts/test-prepare-cli-generated-assets.sh`. + + Out: CLI command wiring and workflow prose. + - Dependencies: none + - Done when: The generated payload contains a compact scaffold with no authoring rules or examples. The contract pins its skeleton, its token, and its exact `check` defects. Both payload and fallback builds embed it, and the untouched-scaffold test passes. + - Verify: `nix run .#pkl-check-generated` — passed (146 files; `plan-init-scaffold` contract assertion evaluates to its pass message); `bash scripts/test-prepare-cli-generated-assets.sh` — passed; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold_untouched_fails_check -- --exact` — ran 0 tests because the full test name is `services::plan::scaffold_untouched_fails_check`; rerun with that exact name — passed (1 test). Also: `plan::` module tests — 17 passed; `clippy --all-targets --all-features` via `run-cli-cargo.sh` — clean; `cargo fmt` applied; a real fallback staged into a scratch directory lists `pkl-generated/config/plan-init-scaffold.md` and is byte-identical to the payload scaffold; generated output diffed against HEAD differs only by the new scaffold, so `plan-template.md` is byte-identical. + - Completed: 2026-10-24 + - Files changed: `cli/build.rs`, `cli/src/services/plan/mod.rs`, `config/pkl/base/plan-init-scaffold.pkl`, `config/pkl/base/workflow-change-to-plan.pkl`, `config/pkl/check-generated.sh`, `config/pkl/generate.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, `config/pkl/renderers/plan-structure.pkl`, `scripts/test-prepare-cli-generated-assets.sh`, `context/plans/sce-plan-init.md` + - Result: `changeToPlanPlanTemplate` now interpolates shared `planAcceptanceCriteriaIntro`, `planFullValidationIntro`, and `planTaskContextSynchronizationLifecycle` fragments. The new `plan-init-scaffold.pkl` renders `config/plan-init-scaffold.md` from them. `plan-structure.pkl` exposes `skeletonDefects`. The `plan-init-scaffold` contract pins skeleton mode, the single `{plan-name}` token, no `D`/`S` entries, no rules or examples, and the exact three S6 defects. `check-generated.sh` requires the output and forbids a committed copy, and `build.rs` requires the staged path. `SCAFFOLD` and `SCAFFOLD_PLAN_NAME_TOKEN` are crate-visible `include_str!`/token constants carrying `#[cfg_attr(not(test), expect(dead_code))]` until T02 uses them. Deviation: the plan's `--exact` filter needs the `services::` prefix; T02's verify filters without `--exact` are unaffected. `nix build .#default` was not run here: it is `/validate` full validation, and flake builds do not see the untracked new `.pkl` until it is tracked. + - Context impact: `context/sce/config-generation.md` (new generated artifact, public skeleton function, `plan-init-scaffold` contract, artifact count 146), `context/sce/plan-structure-contract.md` (scaffold and skeleton mode, expected `check` defects), `context/cli/plan-service.md` (embedded scaffold constant and test). + - Context synchronization: synced + +- [ ] T02: `Add the sce plan init command with safe repository-root creation` (status:todo) + - Task ID: T02 + - Covers: AC1, AC2, AC3, AC4, AC7 + - Scope: In: + - `PlanSubcommand::Init { name }` and its help text in `cli_schema.rs`. + - The `PlanRequest` enum and its dispatch in `command.rs` and `command_runtime.rs`. + - `cli/src/services/plan/init.rs`, covering name validation, root resolution, directory checks, and temporary-file plus hard-link publication. + - The D5 output and error contract. + - The S1–S5, S7, and S12 tests in `cli/src/services/plan/mod.rs`, plus updates to the existing `plan::command_*` tests. + + Out: Pkl sources, workflow prose, and changes to `check` behavior. + - Dependencies: T01 + - Done when: `sce plan init ` behaves as D4 and D5 state from the repository root and from nested directories. All listed tests pass. Help and completions show `init`. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix develop -c sh -c 'cd cli && cargo fmt'`. + - Context synchronization: pending + +- [ ] T03: `Use sce plan init in the change-to-plan workflow and document it` (status:todo) + - Task ID: T03 + - Covers: AC6 + - Scope: In: + - `changeToPlanPlanAuthoring` steps 2.1 and 2.10 in `workflow-change-to-plan.pkl`, adding the `init`-for-new-plans path, output-and-file verification, the narrow fallback, and the skip for revisions. + - The `plan-authoring-plan-init` assertion and its negative fixture in `generation-contract-check.pkl`. + - Any existing contract substring updates needed by the new wording. + - `context/sce/shared-context-plan-workflow.md`. + + Out: `/next-task` and `/validate` content, the plan template schema, and CLI code. + - Dependencies: T02 + - Done when: All four targets' generated `plan-authoring.md` carry the D6 behavior. The contract assertion passes on generated output and fails on its negative fixture. No `sce-next-task` or `sce-validate` document mentions `sce plan init`. + - Verify: `nix run .#pkl-check-generated`. + - Context synchronization: pending + +## Open questions + +- Should `init` refuse a symlinked `context/` or `context/plans/` directory, as D4 plans to? A repository that deliberately symlinks `context/` elsewhere would be blocked. Following the link instead would allow writes outside the repository root. +- Hard-link publication fails on filesystems without hard-link support, such as some network or FUSE mounts. Is a runtime error with no fallback acceptable for the MVP, or should `init` fall back to `create_new` with cleanup on write failure? diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 8eb7a5e86..12dbe4ac0 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -11,6 +11,7 @@ Current location for canonical workflow content primitives: - `config/pkl/base/optional-workflow-manifest.pkl` (install-time projection of the catalog's optional records into the generated `config/optional-workflows.json` manifest — `schemaVersion` plus one entry per optional workflow carrying `id`, `title`, `description`, `commandSlug`, and `skillSlug`. Optionality never affects generation: all six workflows are still generated for all four targets, so the manifest exists solely to carry optional-workflow identity out of Pkl for install-time and doctor-time consumers) - `config/pkl/base/decision-skill.pkl` (canonical standalone `sce-decision` package outside the workflow catalog; renders its decision gate, one-record and immutable-ADR rules, active-only reuse and creation-time status semantics, deterministic written/not-qualified/skipped/blocked handoff, and `references/adr-template.md` for all four targets without creating a command or prompt) - `config/pkl/base/workflow-change-to-plan.pkl` (canonical `/change-to-plan` package registering the target-neutral `SKILL.md` body plus `context-load.md`, `plan-authoring.md`, `plan-template.md`, and `output.md` package references; the plan template persists task synchronization lifecycle state, retains the plan format needed by existing plans, and defines the optional structured layer of design decisions, verification scenarios, and `Decisions`/`Scenarios`/`Covers` reference fields described in `shared-context-plan-workflow.md`) +- `config/pkl/base/plan-init-scaffold.pkl` (renders the compact `config/plan-init-scaffold.md` plan scaffold: every template-skeleton heading in order, one `AC1` with `Validate: TODO` and `Scenarios: TODO`, one `T01` with `Covers: TODO`, no `D` or `S` entries, TODO prose instead of authoring rules or examples, and a single `{plan-name}` title token; the acceptance-criteria intro, full-validation intro, and task context synchronization lifecycle section are interpolated from the `planAcceptanceCriteriaIntro`, `planFullValidationIntro`, and `planTaskContextSynchronizationLifecycle` fragments that `workflow-change-to-plan.pkl` also interpolates into `plan-template.md`) - `config/pkl/base/workflow-next-task.pkl` (canonical `/next-task` package registering the target-neutral `SKILL.md` body plus `plan-review.md`, `task-execution.md`, `context-sync.md`, `sync-report.md`, and `output.md` package references; review gates new tasks on synced lifecycle state, execution records pending before task synchronization, and the execution reference defines an explicit Git-baseline-relative handoff consumed by task synchronization) - `config/pkl/base/workflow-validate.pkl` (canonical `/validate` package registering the target-neutral `SKILL.md` body plus `validation.md`, `validation-report.md`, and `output.md` package references; `validation.md` carries the validation steps plus the validation result contract and keeps final validation observational by recording leftover debug/temp artifacts as failure evidence rather than deleting or repairing them; `/validate` does not invoke plan-level context synchronization, and `output.md` holds the `Completion` layout) - `config/pkl/base/workflow-commit.pkl` (canonical `/commit` package registering the target-neutral `SKILL.md` body plus `atomic-commit.md`, `commit-message-style.md`, and `output.md` package references; `atomic-commit.md` owns staged-diff procedure, internal result branching, and commit boundaries, `commit-message-style.md` owns message wording, and both regular and bypass paths read the phase reference only after their pre-phase gate) @@ -53,11 +54,11 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (145) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `pass`/`fail` verdict, then expected defect messages; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `pass` rows must pass with no listed messages, `fail` rows must fail with exactly the listed messages, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, and fenced example repeating active IDs; the negative fixtures are an unstructured plan snapshot, an unstructured plan with a fenced structured example, duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing the plan with the checker trust rule and manual `Reference rules` fallback. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks; `output.md` carries the `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under **What failed**. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (146) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). It also asserts `plan-init-scaffold`: the generated `config/plan-init-scaffold.md` passes skeleton mode (`skeletonDefects` in `plan-structure.pkl`), starts with `# Plan: {plan-name}` and carries that token exactly once, defines no `D` or `S` entry, contains no fenced block or template rule/example sections, and fails `check` with exactly ``Scenarios:` on `AC1` is not a comma-separated ID list``, ``Covers:` on `T01` is not a comma-separated ID list``, and ``AC1` is not covered by any task's `Covers:``. `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `pass`/`fail` verdict, then expected defect messages; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `pass` rows must pass with no listed messages, `fail` rows must fail with exactly the listed messages, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, and fenced example repeating active IDs; the negative fixtures are an unstructured plan snapshot, an unstructured plan with a fenced structured example, duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing the plan with the checker trust rule and manual `Reference rules` fallback. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks; `output.md` carries the `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under **What failed**. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). -- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; and the optional-workflow manifest at `config/optional-workflows.json`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. +- `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; the optional-workflow manifest at `config/optional-workflows.json`; and the plan init scaffold at `config/plan-init-scaffold.md`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. -- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, and `cli/assets/generated`, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures listed in `expectations.tsv` through `plan-structure-check.pkl` with their expected classifications and defects (it does not scan `context/plans/`, and `pklGeneratedCheckSrc` excludes that directory, so mutable plan contents never affect generation checks or `nix flake check`), then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. +- `config/pkl/check-generated.sh` is intentionally dev-shell scoped (`nix develop -c ...`): it requires `IN_NIX_SHELL`, rejects the repository-local `config/pkl/rendered` evaluation artifact before generation, rejects committed target trees, the generated SCE schema, a committed `config/plan-init-scaffold.md`, and `cli/assets/generated`, requires the generated scaffold among its required outputs, evaluates exact metadata and generation contracts, confirms the existing semantic negative fixtures fail with their contract diagnostics, runs the plan-structure fixtures listed in `expectations.tsv` through `plan-structure-check.pkl` with their expected classifications and defects (it does not scan `context/plans/`, and `pklGeneratedCheckSrc` excludes that directory, so mutable plan contents never affect generation checks or `nix flake check`), then delegates two-pass generation, input checks, and payload inventories to `scripts/produce-cli-generated-input.sh`. It projects the producer inventory only to preserve the established report digest path format; it does not rehash generated files. Required-path checks remain fast surface diagnostics, the Pkl contract owns exact complete-path coverage, and forbidden-output checks reject removed generator surfaces. Generated authored classes: diff --git a/context/sce/plan-structure-contract.md b/context/sce/plan-structure-contract.md index 6ed689570..f31c53818 100644 --- a/context/sce/plan-structure-contract.md +++ b/context/sce/plan-structure-contract.md @@ -28,7 +28,7 @@ Both checkers apply the same rules to the plan source outside fenced code blocks - An undefined ID that appears only outside its section, or only inside a fenced block, gets a hint saying so. - **Coverage.** Every `AC` is covered by some task's `Covers:`, and every `S` is referenced by some `AC`'s `Scenarios:`. -Template-skeleton mode (`templateDefects` in `plan-structure.pkl`) validates generated plan templates, not plans. It stays Pkl-only and is outside this contract. +Template-skeleton mode (`templateDefects` and the public `skeletonDefects` in `plan-structure.pkl`) validates generated plan templates and the generated plan init scaffold, not plans. It stays Pkl-only and is outside this contract. The scaffold (`config/plan-init-scaffold.md`) is the one generated document checked in both modes: it must pass skeleton mode, and the Pkl `check` path and the Rust `validate` path must both reject it untouched with exactly the two not-an-ID-list defects (`Scenarios:` on `AC1`, `Covers:` on `T01`) and the uncovered-`AC1` defect, so an unedited scaffold can never pass as an authored plan. ## Shared fixtures and manifest diff --git a/scripts/test-prepare-cli-generated-assets.sh b/scripts/test-prepare-cli-generated-assets.sh index 5a77acda4..53a857fe1 100755 --- a/scripts/test-prepare-cli-generated-assets.sh +++ b/scripts/test-prepare-cli-generated-assets.sh @@ -52,6 +52,7 @@ fi printf '%s\n' "$((count + 1))" > "${count_file}" mkdir -p "${destination}/config/.opencode" printf 'generated\n' > "${destination}/config/.opencode/generated.txt" +printf '# Plan: {plan-name}\n' > "${destination}/config/plan-init-scaffold.md" EOF chmod +x \ "${fake_bin}/pkl" \ @@ -81,6 +82,7 @@ if [ "$(<"${state_root}/generation-count")" -ne 4 ]; then fi test ! -e "${first_root}/INPUTS.SHA256SUMS" test -f "${first_root}/pkl-generated/config/.opencode/generated.txt" +test -f "${first_root}/pkl-generated/config/plan-init-scaffold.md" test -f "${first_root}/static/hooks/hook.sh" test -f "${first_root}/static/migrations/001.sql" test -f "${first_root}/static/schema/agent-trace.schema.json" @@ -88,7 +90,11 @@ test -f "${first_root}/static/schema/agent-trace.schema.json" cd "${first_root}" sha256sum -c SHA256SUMS >/dev/null ) -if [ "$(wc -l < "${first_root}/SHA256SUMS" | tr -d ' ')" -ne 4 ]; then +if ! grep -q ' pkl-generated/config/plan-init-scaffold.md$' "${first_root}/SHA256SUMS"; then + printf 'Package fallback inventory does not list the plan init scaffold.\n' >&2 + exit 1 +fi +if [ "$(wc -l < "${first_root}/SHA256SUMS" | tr -d ' ')" -ne 5 ]; then printf 'Combined package fallback inventory has the wrong entry count.\n' >&2 exit 1 fi From 53e7345b3ae5eee4382a2ee6ea87df24f0d2e7b6 Mon Sep 17 00:00:00 2001 From: davidabram Date: Sat, 24 Oct 2026 14:49:39 +0200 Subject: [PATCH 18/61] CI: Separate plan structure report stderr from captured output Capture `pkl eval` stderr for the plan structure report fixture into a temporary file instead of merging it into the captured report. Previously `2>&1` folded unrelated diagnostics and warnings into the report value, which could pollute the expected/actual diagnostic comparison. Report stderr is now printed separately when evaluation fails, keeping the parsed report output clean while preserving failure context. Co-authored-by: SCE --- config/pkl/check-generated.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 35bb3fc8d..047f12b7f 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -241,11 +241,12 @@ expect_plan_structure_report() { local report local actual_diagnostics local expected_diagnostics + local report_stderr="$tmp_dir/plan-structure-report.stderr" if ! report="$(pkl eval -p "planPath=$repo_root/$plan_structure_fixtures/$fixture_name" \ - "$plan_structure_report" 2>&1)"; then - printf 'Plan structure report failed to evaluate: %s\n%s\n' \ - "$fixture_name" "$report" >&2 + "$plan_structure_report" 2>"$report_stderr")"; then + printf 'Plan structure report failed to evaluate: %s\n%s\n%s\n' \ + "$fixture_name" "$report" "$(cat "$report_stderr")" >&2 exit 1 fi From bda29824b1da16192a0900b1a69c3a5b85f8c3bf Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 14:56:40 +0200 Subject: [PATCH 19/61] plans: Clarify initialization and authoring gates Document the stricter plan initialization contract and keep structural checking separate from workflow completeness. Specify safe CLI output and validation boundaries, legacy-binary fallback behavior, failure handling, and the pre-`plan_ready` authoring gate. Co-authored-by: SCE --- context/plans/sce-plan-init.md | 150 +++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 55 deletions(-) diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index 77fc6770f..de45331d7 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -16,10 +16,19 @@ runs Pkl, Nix, an LLM, or the network. The untouched scaffold fails `sce plan check`. Its `Scenarios: TODO` and `Covers: TODO` placeholders are not valid ID lists, so it cannot pass for an -authored plan. The `sce-change-to-plan` workflow then uses `init` to create new -plans, fills the scaffold, and runs `check` before it reports `plan_ready`. -Revisions of existing plans skip `init`. Older binaries fall back to manual -template authoring. +authored plan. `check` stays strictly structural, so a scaffold with only those +two placeholders fixed can pass it. Completeness is therefore an authoring gate +in the workflow, not a checker rule. The `sce-change-to-plan` workflow uses +`init` to create new plans and fills the scaffold. Before it reports +`plan_ready`, it requires that no scaffold `TODO` placeholder remains and that +`check` passes. Revisions of existing plans skip `init` but must meet the same +structured plan contract and pass the same `check`. + +Only structured plans are supported. The workflow has no fallback: when `init` +or `check` is unavailable, fails, or returns unexpected output, it returns +`blocked`. It never detects older binaries, never authors a new plan without +`init`, and never substitutes a manual reference pass for `sce plan check`. This +change removes the existing manual checker fallback from step 2.10. ## Design decisions @@ -39,20 +48,28 @@ template authoring. - Rejected: A new top-level `sce init`-style command — it would split plan ownership. A trait or `dyn` operation object — the repository constraints rule these out, and two variants do not need them. - Trade-offs: `PlanCheckRequest` moves behind an enum, so the existing `plan::command_*` tests change their destructuring. - D4: The plan name is validated before any filesystem access, and the scaffold is published with a no-clobber hard link from a fully written temporary file. - - Boundary: Names must match `[a-z0-9]+(-[a-z0-9]+)*`, at most 64 bytes, with no `.md` suffix. The repository root comes from `git rev-parse --show-toplevel`, run from the current directory. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is written to a `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`, and the temporary file is removed on every path. - - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. A successful exit only ever exposes complete content, and a crash cannot leave a half-written plan that later blocks `init`. - - Rejected: `rename` — it silently replaces an existing destination. Writing directly with `create_new` — an interrupted write leaves a partial plan at the final path. Accepting a path argument — that would invite traversal and repository escape. - - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. + - Boundary: `` is one required positional `String` argument, parsed under the existing Clap conventions with no extra parser settings. Every value Clap accepts, including an empty string and a value given after `--`, then goes through application validation: it must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes, and have no `.md` suffix. This grammar excludes path separators and traversal. Clap rejects hyphen-prefixed values such as `-leading`, unknown options, and extra positionals before validation runs. The repository root comes from `git rev-parse --show-toplevel`, run from an explicit start directory; the command passes the process current directory, so tests never change process-global state. A root that is not valid UTF-8 or that contains a newline is refused before any write, because it cannot be printed as a single shell-quoted line. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is fully written to a hidden `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`. The temporary file is then removed, both after a successful publication and on every handled error path that follows its creation. + - Guarantee: The invariant is atomic, no-clobber publication of the final plan file. The destination path either does not exist or holds the complete scaffold; it never holds partial content. An existing entry at the destination is never replaced or modified. A handled error leaves no temporary file behind. + - Limits: This is not a filesystem-wide transaction. `context/` and `context/plans/` created by a run stay in place when a later step fails; they are left empty unless other content already existed. A process crash or kill between creating and removing the temporary file can leave an orphaned temporary file, but never a file at the destination path. Crash-proof cleanup is not promised. + - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. A successful exit only ever exposes complete content, and a crash cannot leave a half-written plan that later blocks `init`. Empty directories are harmless, so preserving them avoids rollback logic. + - Rejected: `rename` — it silently replaces an existing destination. Writing directly with `create_new` — an interrupted write leaves a partial plan at the final path. Falling back to that non-atomic write when hard links are unsupported — it reintroduces partial plans. Rolling back created directories on failure — it adds removal logic that can race with concurrent writers in order to remove harmless empty directories. Accepting a path argument — that would invite traversal and repository escape. Setting `allow_hyphen_values` or similar so that `-leading` reaches name validation — it departs from the CLI's Clap conventions only to change which error class a name that is invalid anyway receives. + - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. A failed run can leave newly created empty directories, and a crashed run can leave an orphaned temporary file. - D5: `init` reports results through the existing `CliError` classes and prints exactly three lines on stdout. - - Boundary: On success, stdout is `Created plan: context/plans/.md`, `Next: complete the plan and run`, and `sce plan check context/plans/.md`, and the exit code is `0`. An invalid name or an existing destination is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, or a filesystem failure is `CliError::runtime` (exit `4`). Clap usage errors keep the existing parse class (exit `2`). - - Rationale: Automation and the workflow can branch on the class without parsing text. The printed path is always repository-relative, whichever directory `init` ran from. - - Rejected: A dedicated new exit code or error class — the existing taxonomy already covers these failures. - - Trade-offs: A destination that already exists and an invalid name share exit `3`, so callers that need to tell them apart must read stderr. -- D6: The workflow runs `init` only when writing a new plan, and it confirms the file exists rather than trusting the exit code. - - Boundary: In `changeToPlanPlanAuthoring`, step 2.10 runs `sce plan init {plan_name}` for a new plan, after the clarification gate and target resolution. Initialization counts as successful only when stdout contains `Created plan: context/plans/{plan_name}.md` and that file exists. `init` counts as unavailable when it exits `0` without that line (an older binary printing `plan` help), exits `2`, or the command is not found. Only then does the workflow fall back to writing from `references/plan-template.md`. Exit `3` or `4` returns `blocked` with the reported error and no fallback. Revisions skip `init`. - - Rationale: An older `sce` (verified with `0.4.0-pre-alpha-v11`) prints `plan` help and exits `0` for `sce plan init foo`, so the exit code alone would report success with no file written. - - Rejected: Running `init` during 2.1 target resolution — it would write a file before the clarification gate. Falling back on any non-zero exit — it would hide real filesystem failures and collisions. - - Trade-offs: The workflow now depends on exact stdout wording, so changing D5's first line is a breaking change for the workflow. + - Boundary: On success, the exit code is `0` and stdout is exactly three lines: `Created plan: context/plans/.md`, then `Next: complete the plan and run`, then `sce plan check `. `` is the absolute path `/context/plans/.md`, always wrapped in POSIX single quotes, with each `'` in it written as `'\''`. An invalid name that reaches application validation, or an existing destination, is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, an unprintable root, or a filesystem failure is `CliError::runtime` (exit `4`). Clap errors keep the existing `classify_clap_error` mapping unchanged: unknown options, hyphen-prefixed values, and extra arguments are parse errors (exit `2`), and the missing `` argument is `MissingRequiredArgument`, which that mapping already reports as `SCE-ERR-VALIDATION` (exit `3`), as `sce plan check` does today. + - Side effects by failure class: Clap errors and application name-validation errors happen before any filesystem access, so they create no directory or file and modify no existing file. A missing repository, a failed or missing `git`, and an unprintable root are detected before any write, so they also leave the filesystem unchanged. An existing destination is never overwritten or modified, and no temporary file remains. A filesystem or publication failure never publishes a partial destination file and removes its temporary file, but directories that the run created may remain, as D4 states. + - Rationale: Automation and the workflow can branch on the class without parsing text. The `Created plan:` path is always repository-relative, whichever directory `init` ran from, so the workflow can match it. `sce plan check` keeps resolving relative paths against the current directory, so the printed command uses the absolute path and works unchanged from any directory. Always quoting keeps the output deterministic and safe for roots containing spaces, apostrophes, or other shell metacharacters. + - Rejected: A dedicated new exit code or error class — the existing taxonomy already covers these failures. Printing a repository-relative `check` path — it fails with a missing-file runtime error when `init` runs from a nested directory. Making `check` resolve paths against the repository root — it would change existing `check` semantics. Quoting only when needed — it adds a second output shape for no gain. + - Trade-offs: A destination that already exists, an invalid name, and a missing argument share exit `3`, so callers that need to tell them apart must read stderr. The third line depends on the checkout location, so tests compute the expected line from the canonical repository root rather than comparing against a fixed string. +- D6: The workflow supports structured plans only. It creates every new plan with `sce plan init`, checks every written plan with `sce plan check`, and has no fallback for either. It gates `plan_ready` on a fully authored plan and a passing `check`. + - Boundary: In `changeToPlanPlanAuthoring`, step 2.10 handles each plan target as follows: + - New plan: after the clarification gate and target resolution, run `sce plan init {plan_name}`. Initialization succeeds only when all three hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, and that file exists. Every other result returns `blocked` with the exit code and stderr. This includes a missing `sce` executable, a binary without `init`, a nonzero exit, an exit `0` without the expected line, and an expected line with no created file. The agent then fills the generated scaffold with the plan content, applying the authoring rules in `references/plan-template.md`. The template is the authoring reference for filling the scaffold, never a substitute for `init`. + - Existing plan: do not run `init`. Apply the `Updating an existing plan` rules, which preserve completed tasks and their evidence. The revised plan must satisfy the same structured plan contract. + - Completeness gate, for both paths: the plan contains no scaffold `TODO` placeholder, and optional sections without real entries (`Design decisions`, `Assumptions`) are removed. The change summary, acceptance criteria, design decisions, verification scenarios, task definitions, and verify instructions are authored for this change. While any placeholder remains, the workflow keeps authoring or returns `blocked`, and never reports `plan_ready`. + - Structural check, for both paths: run `sce plan check {plan-path}`. It passes only on exit `0` with `structured plan: reference integrity holds` on stdout. On exit `3` with structural diagnostics, repair each reported defect in the plan and rerun it, or return `blocked` with the remaining diagnostics when they cannot be resolved. Every other result returns `blocked`, including a missing `sce` executable, a binary without `check`, any other exit code, and an exit `0` without the success marker. + `plan_ready` requires both a passing completeness gate and a passing `sce plan check`. The step 2.9 reference-consistency pass stays as pre-write authoring guidance only; it never substitutes for a successful `sce plan check`. The existing step 2.10 text that treats any other checker outcome as "unavailable" and applies a manual `Reference rules` pass instead is removed. + - Rationale: Structured plans are the only supported format, so any fallback could report an unchecked or hand-shaped plan as ready. Requiring an exact exit code, output line, and created file for `init`, and an exact success marker for `check`, distinguishes real success from a missing, outdated, wrapped, or broken binary without identifying versions. `check` is structural by design (S7), so completeness must be enforced where the plan is authored. + - Rejected: Detecting older `sce` binaries from their `plan` help output and falling back to manual template authoring — it adds version-sniffing complexity for a format that is no longer supported, and it can hide a broken install. Applying the manual `Reference rules` pass when `check` is unavailable — it is not equivalent to the checker and would let unchecked plans reach `plan_ready`. Running `init` during 2.1 target resolution — it would write a file before the clarification gate. Detecting TODO placeholders in the Rust checker, or adding a validator mode — it would turn `check` into semantic approval, which is out of scope. + - Trade-offs: An environment without a current `sce` that provides both `plan init` and `plan check` cannot produce or revise plans. The workflow returns `blocked` with the error instead. The workflow depends on the exact stdout wording of D5's first line and of `check`'s success marker, so changing either is a breaking change for the workflow. The completeness gate is an instruction to the authoring agent, enforced by generation contract checks on the workflow text, not by a CLI check of each plan. ## Acceptance criteria @@ -60,19 +77,19 @@ How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. -- [ ] AC1: `sce plan init example`, run inside a Git repository, creates `context/plans/example.md` holding the embedded scaffold with the title `# Plan: example`, creates `context/plans/` when it is missing, prints the three-line success output, and modifies no other repository file. +- [ ] AC1: `sce plan init example`, run inside a Git repository, creates `context/plans/example.md` holding the embedded scaffold with the title `# Plan: example`, and creates `context/plans/` when it is missing. It prints the three-line D5 success output: the repository-relative `Created plan:` path, and a `sce plan check` command with the shell-quoted absolute plan path. No temporary file remains in `context/plans/`, and no other repository file is created or modified. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - Decisions: D3, D4, D5 - Scenarios: S1 -- [ ] AC2: Existing destinations, including symlinks, are never overwritten, and unsafe names or a missing repository are rejected with the documented class before any file or directory is created. +- [ ] AC2: Existing destinations, including symlinks, are never overwritten or modified, and a conflict leaves no temporary file behind. Invalid names that reach application validation exit `3`. Inputs that Clap rejects exit `2`, and a missing `` keeps the existing Clap mapping (exit `3`). Argument and name-validation rejections create no directory or file. A missing repository exits `4` and leaves the directory unchanged. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - Decisions: D4, D5 - Scenarios: S3, S4, S5 -- [ ] AC3: Running `init` from a nested directory of a Git repository creates the same repository-root-relative `context/plans/.md` and prints the same root-relative paths. +- [ ] AC3: Running `init` from a nested directory of a Git repository, including one whose root path contains a space and an apostrophe, creates `/context/plans/.md`. It prints the same repository-relative `Created plan:` line. Its printed `sce plan check` command, run from that nested directory, checks the created file and fails structural validation with exit `3`, not a missing-file runtime error. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - - Decisions: D4 + - Decisions: D4, D5 - Scenarios: S2 -- [ ] AC4: The untouched scaffold fails `sce plan check` with exactly the incomplete-reference defects, and the same scaffold with its reference placeholders filled correctly passes. +- [ ] AC4: The untouched scaffold fails `sce plan check` with exactly the incomplete-reference defects. The same scaffold with only its reference placeholders filled passes, although its other `TODO` placeholders remain, which shows that `check` is structural and not semantic approval. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold` - Decisions: D1 - Scenarios: S6, S7 @@ -80,10 +97,10 @@ performs final validation. - Validate: `nix run .#pkl-check-generated` - Decisions: D1, D2 - Scenarios: S8, S9 -- [ ] AC6: The generated `sce-change-to-plan` workflow runs `sce plan init` for new plans only, after its clarification and target-resolution gates. It verifies initialization by output and file existence, falls back to manual template authoring only when `init` is unavailable, skips `init` for revisions, and runs `sce plan check` before `plan_ready`. +- [ ] AC6: The generated `sce-change-to-plan` workflow runs `sce plan init` for new plans only, after its clarification and target-resolution gates, and skips `init` for revisions while preserving completed tasks and evidence. It accepts initialization only on exit `0` with the success line and an existing file. It returns `blocked` when `init` is missing, unsupported, exits nonzero, or returns unexpected output. It never reports `plan_ready` while scaffold `TODO` placeholders remain. For new plans and revisions alike, it reports `plan_ready` only after `sce plan check` exits `0` with `structured plan: reference integrity holds`. It repairs and reruns on structural diagnostics, and returns `blocked` when `check` is unavailable or returns any other result. The workflow contains no legacy-binary detection, no manual initialization fallback, and no manual validation fallback. Generation contract checks enforce each of these rules and reject negative fixtures that break them. - Validate: `nix run .#pkl-check-generated` - Decisions: D6 - - Scenarios: S10, S11 + - Scenarios: S10, S11, S13, S14, S15 - [ ] AC7: `sce --help`, `sce plan --help`, and the generated Bash, Zsh, and Fish completion scripts include `plan init`. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` - Decisions: D3 @@ -94,37 +111,37 @@ performs final validation. - S1: Fresh initialization at the repository root - Given: A temporary `git init` repository with no `context/` directory, the current directory at its root. - When: `sce plan init example` runs. - - Then: Exit `0`. Stdout is exactly the three D5 lines for `context/plans/example.md`. That file equals the embedded scaffold with its single `{plan-name}` token replaced by `example`. `git status --porcelain` lists only `context/`. + - Then: Exit `0`. Stdout is exactly `Created plan: context/plans/example.md`, `Next: complete the plan and run`, and `sce plan check '/context/plans/example.md'`, where `` is the canonicalized repository root. That file equals the embedded scaffold with its single `{plan-name}` token replaced by `example`. `context/plans/` holds only `example.md`, so no temporary file remains, and `git status --porcelain` lists only `context/`. - Check: `plan::init_creates_scaffold_at_repository_root`, created by T02. -- S2: Nested invocation resolves the repository root - - Given: A temporary Git repository with the current directory at `/a/b`. - - When: `sce plan init nested-plan` runs. - - Then: `/context/plans/nested-plan.md` exists, nothing is created under `/a/b`, and stdout names `context/plans/nested-plan.md`. +- S2: Nested invocation resolves the repository root, and its printed check command works there + - Given: A temporary Git repository whose root directory name contains a space and an apostrophe (for example `repo with 'quote'`), with the start directory at `/a/b`. + - When: `sce plan init nested-plan` runs. Then its third stdout line is executed by `sh -c` with `/a/b` as the working directory and `sce` defined as a shell function that prints its decoded arguments NUL-separated. The decoded arguments are then dispatched to the in-process `plan` command. + - Then: `/context/plans/nested-plan.md` exists, nothing is created under `/a/b`, and the first stdout line is exactly `Created plan: context/plans/nested-plan.md`. The shell decodes the third line into exactly `plan`, `check`, and the absolute path of the created file. Dispatching those arguments exits `3` (`SCE-ERR-VALIDATION`) with exactly the S6 diagnostics, and stderr has no `Failed to read plan file` runtime error. - Check: `plan::init_from_nested_directory_targets_repository_root`, created by T02. - S3: Existing entries are never overwritten - Given: A repository where `context/plans/taken.md` exists as a regular file with known bytes, and `context/plans/linked.md` is a dangling symlink. - When: `sce plan init taken` and `sce plan init linked` run. - - Then: Both exit `3` (`SCE-ERR-VALIDATION`). `taken.md` keeps its bytes, `linked.md` is still the same symlink, and no temporary file remains in `context/plans/`. + - Then: Both exit `3` (`SCE-ERR-VALIDATION`). `taken.md` keeps its bytes, `linked.md` is still the same symlink with the same target, and `context/plans/` holds only those two entries, so no temporary file remains. Each conflict is detected when the hard link fails after the temporary file is written, so this case exercises the same cleanup path that a filesystem or publication failure takes. The scenario asserts no directory rollback. - Check: `plan::init_never_overwrites_existing_entry`, created by T02. -- S4: Unsafe names are rejected before any write +- S4: Unsafe names and malformed invocations are rejected before any write - Given: A repository without `context/`. - - When: `sce plan init` runs with each of `../escape`, `a/b`, `Upper`, `trailing-`, `-leading`, `double--hyphen`, `plan.md`, an empty string, and a 65-byte name. - - Then: Each exits `3`, and `context/` still does not exist afterwards. + - When: Three groups of arguments are each parsed and run through the runtime command path. Application validation: `sce plan init` followed by each of `../escape`, `a/b`, `Upper`, `trailing-`, `double--hyphen`, `plan.md`, an empty string, a 65-byte name, and `-- -leading`. Clap parse: `sce plan init -leading`, `sce plan init --force example`, and `sce plan init one two`. Missing argument: `sce plan init` with no name. + - Then: Each application-validation case exits `3` with `SCE-ERR-VALIDATION` and a plan-name error. Each Clap parse case exits `2` with `SCE-ERR-PARSE`. The missing-argument case exits `3` through the existing `MissingRequiredArgument` mapping and names ``. In every case, the repository's file listing is unchanged afterwards: `context/` does not exist, and no directory or file has been created. - Check: `plan::init_rejects_unsafe_names_without_writing`, created by T02. - S5: Outside a Git repository - Given: A temporary directory that is not inside any Git repository. - When: `sce plan init example` runs there. - - Then: Exit `4` (`SCE-ERR-RUNTIME`), and the directory is left unchanged. + - Then: Exit `4` (`SCE-ERR-RUNTIME`). The repository lookup fails before any write, so the directory's file listing is unchanged and no `context/` directory exists. - Check: `plan::init_outside_repository_is_runtime_failure`, created by T02. - S6: The untouched scaffold is not a ready plan - Given: The embedded scaffold with its title token replaced. - When: It is extracted and validated by the plan service. - Then: The diagnostics are exactly ``Scenarios:` on `AC1` is not a comma-separated ID list``, ``AC1` is not covered by any task's `Covers:``, and ``Covers:` on `T01` is not a comma-separated ID list``. The scaffold defines no `D` or `S` entry. - Check: `plan::scaffold_untouched_fails_check`, created by T01. -- S7: A correctly authored scaffold passes - - Given: A scaffold created by `sce plan init authored` in which `Scenarios: TODO` is replaced by `Scenarios: none (inspection)` and `Covers: TODO` by `Covers: AC1`. - - When: `sce plan check context/plans/authored.md` runs. - - Then: Exit `0`, with stdout `structured plan: reference integrity holds`. +- S7: Structural validation is not semantic approval + - Given: A scaffold created by `sce plan init authored` in which only `Scenarios: TODO` is replaced by `Scenarios: none (inspection)` and `Covers: TODO` by `Covers: AC1`; every other `TODO` placeholder remains. + - When: `sce plan check` runs on the created file's absolute path. + - Then: Exit `0`, with stdout `structured plan: reference integrity holds`, and the file still contains `TODO` placeholders. This passing result is structural only; S13 covers the workflow gate that keeps such a plan from being reported `plan_ready`. - Check: `plan::scaffold_authored_passes_check`, created by T02. - S8: The generation contract pins the scaffold - Given: The Pkl sources after T01. @@ -136,21 +153,36 @@ performs final validation. - When: The fallback test runs, and the packaged CLI is built. - Then: `pkl-generated/config/plan-init-scaffold.md` is in the fallback inventory, and `nix build .#default` succeeds without a runtime Pkl dependency. - Check: `bash scripts/test-prepare-cli-generated-assets.sh`, extended by T01, and `nix build .#default`. -- S10: New-plan authoring uses `init`, with a safe fallback +- S10: New-plan authoring always uses `init`, with no fallback - Given: The generated `sce-change-to-plan/references/plan-authoring.md`. - When: Its write step for a new plan is inspected. - - Then: It runs `sce plan init {plan_name}` after the clarification gate. It requires both the `Created plan:` line and an existing file. It falls back to the template only for exit `0` without that line, exit `2`, or a missing command. It returns `blocked` on exit `3` or `4`. It then runs `sce plan check {plan-path}` before `plan_ready`. + - Then: It runs `sce plan init {plan_name}` after the clarification gate. It accepts initialization only with exit `0`, the `Created plan: context/plans/{plan_name}.md` line, and an existing file. It returns `blocked`, with the exit code and stderr, when `sce` is not found, when `init` is unsupported, on any nonzero exit, on an exit `0` without the expected line, and when the line is printed but no file exists. It fills the generated scaffold and never writes a new plan from the template without `init`. It then applies the completeness gate and runs `sce plan check {plan-path}` before `plan_ready`. - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion created by T03. - S11: Revisions skip `init` - Given: The generated plan-authoring reference. - When: Its existing-plan revision path is inspected. - - Then: It never runs `sce plan init`. It keeps the `Updating an existing plan` rules, which preserve completed tasks and evidence. No generated `sce-next-task` or `sce-validate` document mentions `sce plan init`. + - Then: It never runs `sce plan init`. It keeps the `Updating an existing plan` rules, which preserve completed tasks and evidence. It applies the same completeness gate and requires the same passing `sce plan check` before `plan_ready`. No generated `sce-next-task` or `sce-validate` document mentions `sce plan init`. - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion created by T03. - S12: Help and completions expose `plan init` - Given: The built CLI schema. - When: Top-level help, `plan` help, and the Bash, Zsh, and Fish completion scripts are rendered. - Then: Each mentions `init`, and `sce plan init --help` shows the `` argument. - Check: `plan::plan_init_appears_in_help` and `plan::completion_scripts_include_plan_init`, created by T02. +- S13: `plan_ready` is impossible while scaffold placeholders remain + - Given: The generated `sce-change-to-plan/references/plan-authoring.md` for each target, plus a negative fixture of the plan-authoring reference without the completeness gate, and one that places the gate after `plan_ready` is reported. + - When: The generation contract checks run. + - Then: The generated text requires that no scaffold `TODO` placeholder remains and that unused optional sections are removed. It requires authored acceptance criteria, design decisions, scenarios, tasks, and verify instructions. It places this gate before `plan_ready`, keeps authoring or returns `blocked` while a placeholder remains, and states that a passing `sce plan check` alone is not enough. Both negative fixtures fail the assertion. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion and its negative fixtures created by T03. +- S14: Initialization failures block without a manual fallback + - Given: The generated plan-authoring reference, plus two negative fixtures. One writes a new plan from `references/plan-template.md` when `init` is unavailable or fails. The other accepts an exit `0` without the `Created plan:` line as success. + - When: The generation contract checks run. + - Then: The generated text returns `blocked` for every `init` result other than the D6 success. It contains no legacy-binary detection, such as matching `plan` help or `Usage: plan `, and no instruction to author a new plan manually when `init` does not succeed. Both negative fixtures fail the assertion. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion and its negative fixtures created by T03. +- S15: `sce plan check` is never bypassed + - Given: The generated plan-authoring reference, plus two negative fixtures. One keeps the current step 2.10 manual `Reference rules` fallback for an unavailable checker. The other reports `plan_ready` on a `check` exit `0` without `structured plan: reference integrity holds`. + - When: The generation contract checks run. + - Then: The generated text requires a `check` exit `0` with that success marker before `plan_ready`, for new plans and revisions alike. It repairs the plan and reruns `check` on structural diagnostics. It returns `blocked` when `check` is unavailable or returns any other result. It states that the step 2.9 reference-consistency pass never substitutes for `sce plan check`, and it contains neither `the checker is unavailable;` nor any manual validation fallback. Both negative fixtures fail the assertion. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion and its negative fixtures created by T03. ### Full validation @@ -166,7 +198,7 @@ which criterion they map to. - `context/cli/plan-service.md`: `init.rs`, the `PlanRequest` enum, the init command contract, and the new tests. - `context/sce/plan-structure-contract.md`: the scaffold's relationship to skeleton mode and its expected `check` defects. - `context/sce/config-generation.md`: the new generated artifact, the public skeleton function, the `plan-init-scaffold` and `plan-authoring-plan-init` contract checks, and the artifact count. -- `context/sce/shared-context-plan-workflow.md`: `init` in new-plan authoring, the older-binary fallback, and revisions skipping `init`. +- `context/sce/shared-context-plan-workflow.md`: `init` in new-plan authoring, revisions skipping `init`, the mandatory `sce plan check` gate, and the removal of the manual initialization and validation fallbacks. - `context/cli/cli-command-surface.md` and `context/sce/cli-shell-completion-contract.md`: the `sce plan init ` surface. - `context/glossary.md`: a `plan scaffold` term. @@ -189,18 +221,25 @@ Persist this field in every plan; this is durable plan state, not chat state: - `config/pkl/renderers/plan-structure.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, `config/pkl/check-generated.sh`, and `scripts/test-prepare-cli-generated-assets.sh`. - The durable context files listed under Context sync. - **Out of scope:** - - Semantic plan approval, or any change to `sce plan check` rules or wording. - - `/next-task` and `/validate` workflow content. + - Semantic plan approval, TODO detection, a separate semantic validator, or any change to `sce plan check` rules, wording, or path resolution. Plan completeness is enforced only by the workflow's authoring gate. + - Legacy or unstructured plan formats, detection of older `sce` binaries, and any compatibility behavior for them. + - Manual plan initialization when `init` does not succeed, and manual validation in place of `sce plan check`. + - Directory rollback after a failed `init`, and crash-proof temporary-file cleanup. + - Changes to Clap parser settings or to `classify_clap_error`. + - `/next-task` and `/validate` workflow content, including the checker fallback that `sce-validate` currently carries. - Committing generated target trees or the scaffold. - Validating real plans under `context/plans/` in Nix or generation checks. - **Constraints:** - No new traits, `dyn`, boxed futures, async runtime, database, or crate dependency. - No interactive prompts. - `init` runs offline and invokes only `git`. + - `init` publishes only through the temporary-file plus hard-link path, never with a non-atomic write. + - The workflow never reports `plan_ready` unless `sce plan check` actually ran and succeeded. - Output is deterministic. - All Cargo commands run through `scripts/run-cli-cargo.sh` under `nix develop`. - **Non-goal:** - `init` does not author plan content, accept arbitrary paths, take `--force` or overwrite flags, or support template variants. + - The workflow does not support environments whose `sce` lacks `plan init` or `plan check`; it returns `blocked` there. ## Assumptions @@ -239,13 +278,13 @@ Persist this field in every plan; this is durable plan state, not chat state: - Scope: In: - `PlanSubcommand::Init { name }` and its help text in `cli_schema.rs`. - The `PlanRequest` enum and its dispatch in `command.rs` and `command_runtime.rs`. - - `cli/src/services/plan/init.rs`, covering name validation, root resolution, directory checks, and temporary-file plus hard-link publication. - - The D5 output and error contract. - - The S1–S5, S7, and S12 tests in `cli/src/services/plan/mod.rs`, plus updates to the existing `plan::command_*` tests. + - `cli/src/services/plan/init.rs`, covering name validation of every value Clap accepts, root resolution from an explicit start directory, the unprintable-root refusal, directory checks, and temporary-file plus hard-link publication. The temporary file is removed after publication and on every handled error path that follows its creation. Directories created by the run are not rolled back, and there is no non-atomic fallback. + - The D5 output and error contract, including the always-single-quoted absolute path in the printed `sce plan check` line. + - The S1–S5, S7, and S12 tests in `cli/src/services/plan/mod.rs`, plus updates to the existing `plan::command_*` tests. S2 uses a root containing a space and an apostrophe and executes the printed line through `sh`. S4 covers the application-validation, Clap-parse, and missing-argument groups separately. - Out: Pkl sources, workflow prose, and changes to `check` behavior. + Out: Pkl sources, workflow prose, changes to `check` behavior or path resolution, changes to `classify_clap_error`, and Clap parser settings such as `allow_hyphen_values`. - Dependencies: T01 - - Done when: `sce plan init ` behaves as D4 and D5 state from the repository root and from nested directories. All listed tests pass. Help and completions show `init`. + - Done when: `sce plan init ` behaves as D4 and D5 state from the repository root and from nested directories. Its printed `check` command works from any directory. Clap-rejected inputs keep their existing exit classes. Argument, name, and repository failures create no file or directory. Existing destinations are never modified. No handled failure publishes a partial plan or leaves a temporary file. All listed tests pass. Help and completions show `init`. - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix develop -c sh -c 'cd cli && cargo fmt'`. - Context synchronization: pending @@ -253,18 +292,19 @@ Persist this field in every plan; this is durable plan state, not chat state: - Task ID: T03 - Covers: AC6 - Scope: In: - - `changeToPlanPlanAuthoring` steps 2.1 and 2.10 in `workflow-change-to-plan.pkl`, adding the `init`-for-new-plans path, output-and-file verification, the narrow fallback, and the skip for revisions. - - The `plan-authoring-plan-init` assertion and its negative fixture in `generation-contract-check.pkl`. - - Any existing contract substring updates needed by the new wording. + - `changeToPlanPlanAuthoring` steps 2.1 and 2.10 in `config/pkl/base/workflow-change-to-plan.pkl`. Add the `init`-for-new-plans path with the D6 success condition, `blocked` for every other `init` result, and the skip for revisions. New plans fill the generated scaffold; there is no manual template authoring when `init` does not succeed. + - Removal of the existing step 2.10 manual checker fallback: the "Trust the checker only when it actually ran" paragraph's "Any other outcome means the checker is unavailable" branch, the manual `Reference rules` pass applied in its place, and the open-question note about an unavailable checker. Replace it with the D6 `check` handling: pass only on exit `0` with the success marker, repair and rerun on structural diagnostics, and `blocked` on anything else. Step 2.9 stays as pre-write guidance and states that it never substitutes for `sce plan check`. + - The completeness gate in step 2.10, for new plans and revisions. Before `plan_ready`, every scaffold `TODO` placeholder is replaced or its optional section removed. Acceptance criteria, design decisions, scenarios, task definitions, and verify instructions are authored for the change. A passing `sce plan check` is required but not sufficient. + - The `plan-authoring-plan-init` assertion in `generation-contract-check.pkl`. It requires the D6 tokens and forbids legacy-binary detection, manual initialization, and manual validation fallback wording, so the workflow can never bypass `sce plan check`. It has negative fixtures for S13 (missing or late gate), S14 (manual template fallback; exit `0` without the success line accepted), and S15 (manual `Reference rules` fallback kept; `plan_ready` without the `check` success marker). + - Update of the existing `assertPlanAuthoringStructuredReferences` tokens and failure message, which currently require the fallback text (the `Any other outcome means the checker is unavailable;` token and the manual `Reference rules` pass token), plus any other contract substring updates needed by the new wording. - `context/sce/shared-context-plan-workflow.md`. - Out: `/next-task` and `/validate` content, the plan template schema, and CLI code. + Out: `/next-task` and `/validate` content, including the `sce-validate` checker fallback; the plan template schema; CLI code; and any TODO detection, new mode, or behavior change in `sce plan check`. - Dependencies: T02 - - Done when: All four targets' generated `plan-authoring.md` carry the D6 behavior. The contract assertion passes on generated output and fails on its negative fixture. No `sce-next-task` or `sce-validate` document mentions `sce plan init`. + - Done when: All four targets' generated `plan-authoring.md` carry the D6 behavior, including the completeness gate and the mandatory `check` gate. None of them contains legacy-binary detection, a manual initialization fallback, or a manual validation fallback. The contract assertion passes on generated output and fails on each of its negative fixtures. No `sce-next-task` or `sce-validate` document mentions `sce plan init`. - Verify: `nix run .#pkl-check-generated`. - Context synchronization: pending ## Open questions - Should `init` refuse a symlinked `context/` or `context/plans/` directory, as D4 plans to? A repository that deliberately symlinks `context/` elsewhere would be blocked. Following the link instead would allow writes outside the repository root. -- Hard-link publication fails on filesystems without hard-link support, such as some network or FUSE mounts. Is a runtime error with no fallback acceptable for the MVP, or should `init` fall back to `create_new` with cleanup on write failure? From 86037639132cacf107463fb3a5b17f37fe1bcf35 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 15:17:54 +0200 Subject: [PATCH 20/61] docs: Clarify atomic plan publication outcomes Document crash and kill outcomes for hard-link publication, handled-error cleanup, rerun behavior, and the absence of durability guarantees. Plan: sce-plan-init (D4, D5) Co-authored-by: SCE --- context/plans/sce-plan-init.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index de45331d7..d85f74e55 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -49,14 +49,20 @@ change removes the existing manual checker fallback from step 2.10. - Trade-offs: `PlanCheckRequest` moves behind an enum, so the existing `plan::command_*` tests change their destructuring. - D4: The plan name is validated before any filesystem access, and the scaffold is published with a no-clobber hard link from a fully written temporary file. - Boundary: `` is one required positional `String` argument, parsed under the existing Clap conventions with no extra parser settings. Every value Clap accepts, including an empty string and a value given after `--`, then goes through application validation: it must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes, and have no `.md` suffix. This grammar excludes path separators and traversal. Clap rejects hyphen-prefixed values such as `-leading`, unknown options, and extra positionals before validation runs. The repository root comes from `git rev-parse --show-toplevel`, run from an explicit start directory; the command passes the process current directory, so tests never change process-global state. A root that is not valid UTF-8 or that contains a newline is refused before any write, because it cannot be printed as a single shell-quoted line. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is fully written to a hidden `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`. The temporary file is then removed, both after a successful publication and on every handled error path that follows its creation. - - Guarantee: The invariant is atomic, no-clobber publication of the final plan file. The destination path either does not exist or holds the complete scaffold; it never holds partial content. An existing entry at the destination is never replaced or modified. A handled error leaves no temporary file behind. - - Limits: This is not a filesystem-wide transaction. `context/` and `context/plans/` created by a run stay in place when a later step fails; they are left empty unless other content already existed. A process crash or kill between creating and removing the temporary file can leave an orphaned temporary file, but never a file at the destination path. Crash-proof cleanup is not promised. - - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. A successful exit only ever exposes complete content, and a crash cannot leave a half-written plan that later blocks `init`. Empty directories are harmless, so preserving them avoids rollback logic. + - Guarantee: The invariant is that the destination is published atomically and never exposes partial scaffold content. The destination path either does not exist or holds the complete scaffold. An existing entry at the destination is never replaced or modified. A handled error removes the temporary file and never leaves a partially written destination. Crash recovery does not guarantee temporary-file cleanup. + - Publication lifecycle: The state a run can leave depends on where it stops. + - Before the hard link publishes the destination: a crash or kill can leave an orphaned temporary file, but this run has not created the destination. + - After the hard link and before the temporary file is removed: a crash or kill can leave both the complete destination file and the temporary file. + - After the temporary file is removed: only the complete destination remains. + - On a handled error: the temporary file is removed, an existing destination is never overwritten, and no partially written destination is left. + - On a filesystem or publication failure: directories that the run newly created may remain, empty unless other content already existed. + - Limits: This is not a filesystem-wide transaction, and it promises no durability across power loss. `context/` and `context/plans/` created by a run stay in place when a later step fails. Temporary-file cleanup after a process crash or kill is not promised, so an orphaned temporary file can remain beside no destination or beside a complete one. + - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. The destination appears only through the hard link to a fully written temporary file, so no observer and no crash ever sees a half-written plan that later blocks `init`. Empty directories and orphaned temporary files are harmless to correctness, so tolerating them avoids rollback and crash-recovery logic. - Rejected: `rename` — it silently replaces an existing destination. Writing directly with `create_new` — an interrupted write leaves a partial plan at the final path. Falling back to that non-atomic write when hard links are unsupported — it reintroduces partial plans. Rolling back created directories on failure — it adds removal logic that can race with concurrent writers in order to remove harmless empty directories. Accepting a path argument — that would invite traversal and repository escape. Setting `allow_hyphen_values` or similar so that `-leading` reaches name validation — it departs from the CLI's Clap conventions only to change which error class a name that is invalid anyway receives. - - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. A failed run can leave newly created empty directories, and a crashed run can leave an orphaned temporary file. + - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. A failed run can leave newly created empty directories. A crashed run can leave an orphaned temporary file, either alone or beside a complete destination; when the destination exists, rerunning `init` with the same name reports the existing destination, and the orphaned file must be removed by hand. - D5: `init` reports results through the existing `CliError` classes and prints exactly three lines on stdout. - Boundary: On success, the exit code is `0` and stdout is exactly three lines: `Created plan: context/plans/.md`, then `Next: complete the plan and run`, then `sce plan check `. `` is the absolute path `/context/plans/.md`, always wrapped in POSIX single quotes, with each `'` in it written as `'\''`. An invalid name that reaches application validation, or an existing destination, is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, an unprintable root, or a filesystem failure is `CliError::runtime` (exit `4`). Clap errors keep the existing `classify_clap_error` mapping unchanged: unknown options, hyphen-prefixed values, and extra arguments are parse errors (exit `2`), and the missing `` argument is `MissingRequiredArgument`, which that mapping already reports as `SCE-ERR-VALIDATION` (exit `3`), as `sce plan check` does today. - - Side effects by failure class: Clap errors and application name-validation errors happen before any filesystem access, so they create no directory or file and modify no existing file. A missing repository, a failed or missing `git`, and an unprintable root are detected before any write, so they also leave the filesystem unchanged. An existing destination is never overwritten or modified, and no temporary file remains. A filesystem or publication failure never publishes a partial destination file and removes its temporary file, but directories that the run created may remain, as D4 states. + - Side effects by failure class: Clap errors and application name-validation errors happen before any filesystem access, so they create no directory or file and modify no existing file. A missing repository, a failed or missing `git`, and an unprintable root are detected before any write, so they also leave the filesystem unchanged. An existing destination is never overwritten or modified, and no temporary file remains. A handled filesystem or publication failure never publishes a partial destination file and removes its temporary file, but directories that the run created may remain, as D4 states. These side effects cover handled errors only; after a process crash or kill, the D4 publication lifecycle applies, and an orphaned temporary file can remain. - Rationale: Automation and the workflow can branch on the class without parsing text. The `Created plan:` path is always repository-relative, whichever directory `init` ran from, so the workflow can match it. `sce plan check` keeps resolving relative paths against the current directory, so the printed command uses the absolute path and works unchanged from any directory. Always quoting keeps the output deterministic and safe for roots containing spaces, apostrophes, or other shell metacharacters. - Rejected: A dedicated new exit code or error class — the existing taxonomy already covers these failures. Printing a repository-relative `check` path — it fails with a missing-file runtime error when `init` runs from a nested directory. Making `check` resolve paths against the repository root — it would change existing `check` semantics. Quoting only when needed — it adds a second output shape for no gain. - Trade-offs: A destination that already exists, an invalid name, and a missing argument share exit `3`, so callers that need to tell them apart must read stderr. The third line depends on the checkout location, so tests compute the expected line from the canonical repository root rather than comparing against a fixed string. @@ -224,7 +230,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Semantic plan approval, TODO detection, a separate semantic validator, or any change to `sce plan check` rules, wording, or path resolution. Plan completeness is enforced only by the workflow's authoring gate. - Legacy or unstructured plan formats, detection of older `sce` binaries, and any compatibility behavior for them. - Manual plan initialization when `init` does not succeed, and manual validation in place of `sce plan check`. - - Directory rollback after a failed `init`, and crash-proof temporary-file cleanup. + - Directory rollback after a failed `init`, temporary-file cleanup after a crash or kill, `fsync` or power-loss durability, and filesystem-wide transactional guarantees. - Changes to Clap parser settings or to `classify_clap_error`. - `/next-task` and `/validate` workflow content, including the checker fallback that `sce-validate` currently carries. - Committing generated target trees or the scaffold. @@ -278,7 +284,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Scope: In: - `PlanSubcommand::Init { name }` and its help text in `cli_schema.rs`. - The `PlanRequest` enum and its dispatch in `command.rs` and `command_runtime.rs`. - - `cli/src/services/plan/init.rs`, covering name validation of every value Clap accepts, root resolution from an explicit start directory, the unprintable-root refusal, directory checks, and temporary-file plus hard-link publication. The temporary file is removed after publication and on every handled error path that follows its creation. Directories created by the run are not rolled back, and there is no non-atomic fallback. + - `cli/src/services/plan/init.rs`, covering name validation of every value Clap accepts, root resolution from an explicit start directory, the unprintable-root refusal, directory checks, and temporary-file plus hard-link publication. The temporary file is removed after publication and on every handled error path that follows its creation. Directories created by the run are not rolled back, there is no non-atomic fallback, and no crash-recovery cleanup or `fsync` is added. - The D5 output and error contract, including the always-single-quoted absolute path in the printed `sce plan check` line. - The S1–S5, S7, and S12 tests in `cli/src/services/plan/mod.rs`, plus updates to the existing `plan::command_*` tests. S2 uses a root containing a space and an apostrophe and executes the printed line through `sh`. S4 covers the application-validation, Clap-parse, and missing-argument groups separately. From 8be05e6ee43aa4284174e6fdb9c82f07312c29b6 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 15:27:46 +0200 Subject: [PATCH 21/61] plan: Add safe repository-root scaffold initialization Add `sce plan init ` with validated names, Git-root resolution, no-clobber scaffold publication, shell-safe check guidance, and help/completion coverage. Update the plan-service contracts and tests for repository-root creation and failure handling. Completes plan `sce-plan-init` task T02. Co-authored-by: SCE --- cli/src/cli_schema.rs | 13 +- cli/src/command_surface.rs | 14 +- cli/src/services/parse/command_runtime.rs | 9 +- cli/src/services/plan/command.rs | 65 ++- cli/src/services/plan/init.rs | 189 ++++++++ cli/src/services/plan/mod.rs | 430 ++++++++++++++++++- context/architecture.md | 2 +- context/cli/cli-command-surface.md | 2 +- context/cli/plan-service.md | 23 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/overview.md | 2 +- context/plans/sce-plan-init.md | 10 +- context/sce/cli-shell-completion-contract.md | 3 +- 14 files changed, 719 insertions(+), 47 deletions(-) create mode 100644 cli/src/services/plan/init.rs diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index 9a32718a0..11d3544dd 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -53,8 +53,9 @@ pub const SYNC_TOP_LEVEL_PURPOSE: &str = "Synchronize the current repository's Agent Trace database"; pub const SYNC_SHOW_IN_TOP_LEVEL_HELP: bool = true; -pub const PLAN_CLAP_ABOUT: &str = "Check the structure and reference integrity of one SCE plan"; -pub const PLAN_TOP_LEVEL_PURPOSE: &str = "Check the structure of one SCE plan"; +pub const PLAN_CLAP_ABOUT: &str = + "Create an SCE plan scaffold or check the structure and reference integrity of one SCE plan"; +pub const PLAN_TOP_LEVEL_PURPOSE: &str = "Create or check the structure of one SCE plan"; pub const PLAN_SHOW_IN_TOP_LEVEL_HELP: bool = true; pub const TOP_LEVEL_COMMANDS: &[TopLevelCommandMetadata] = &[ @@ -383,6 +384,14 @@ pub enum PlanSubcommand { /// Path to the plan Markdown file to check path: PathBuf, }, + + #[command( + about = "Create context/plans/.md at the Git repository root from the plan scaffold" + )] + Init { + /// Plan name: lowercase letters and digits separated by single hyphens, without `.md` + name: String, + }, } #[derive(Subcommand, Debug, Clone, PartialEq, Eq)] diff --git a/cli/src/command_surface.rs b/cli/src/command_surface.rs index 6affeb728..6343b68c6 100644 --- a/cli/src/command_surface.rs +++ b/cli/src/command_surface.rs @@ -74,10 +74,16 @@ const HELP_SECTIONS: &[HelpSection] = &[ }, HelpSection { title: "Plan Usage:", - body: &[HelpSectionBodyLine::Command { - cmd: " sce plan", - suffix: " check ", - }], + body: &[ + HelpSectionBodyLine::Command { + cmd: " sce plan", + suffix: " check ", + }, + HelpSectionBodyLine::Command { + cmd: " sce plan", + suffix: " init ", + }, + ], }, HelpSection { title: "Completion Usage:", diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 92e4ad2e9..0726b323d 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -335,7 +335,14 @@ fn convert_clap_command(command: cli_schema::Commands) -> Result RuntimeCommand { let request = match subcommand { cli_schema::PlanSubcommand::Check { path } => { - services::plan::command::PlanCheckRequest { path } + services::plan::command::PlanRequest::Check(services::plan::command::PlanCheckRequest { + path, + }) + } + cli_schema::PlanSubcommand::Init { name } => { + services::plan::command::PlanRequest::Init(services::plan::command::PlanInitRequest { + name, + }) } }; diff --git a/cli/src/services/plan/command.rs b/cli/src/services/plan/command.rs index acc159d60..4c62511b1 100644 --- a/cli/src/services/plan/command.rs +++ b/cli/src/services/plan/command.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::Context; use crate::services::error::CliError; use crate::services::plan::extract::extract; +use crate::services::plan::init::{create, PlanName}; use crate::services::plan::validate::{validate, PlanDiagnostic}; pub const STRUCTURED_PLAN_PASS: &str = "structured plan: reference integrity holds"; @@ -13,30 +14,60 @@ pub struct PlanCheckRequest { pub path: PathBuf, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanInitRequest { + pub name: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PlanRequest { + Check(PlanCheckRequest), + Init(PlanInitRequest), +} + pub struct PlanCommand { - pub request: PlanCheckRequest, + pub request: PlanRequest, } impl PlanCommand { pub fn execute(&self) -> Result { - let path = &self.request.path; - let source = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read plan file '{}'", path.display())) - .map_err(CliError::runtime)?; - let plan = extract(&source) - .with_context(|| format!("Failed to parse plan file '{}'", path.display())) - .map_err(CliError::runtime)?; - let report = validate(&plan); - - if !report.diagnostics.is_empty() { - return Err(CliError::validation(render_diagnostics( - &path.display().to_string(), - &report.diagnostics, - ))); + self.execute_with(std::env::current_dir) + } + + pub(crate) fn execute_with( + &self, + current_dir: impl FnOnce() -> std::io::Result, + ) -> Result { + match &self.request { + PlanRequest::Check(request) => check(&request.path), + PlanRequest::Init(request) => { + let name = PlanName::parse(&request.name)?; + let start = current_dir() + .context("Failed to read the current directory") + .map_err(CliError::runtime)?; + create(&name, &start) + } } + } +} + +fn check(path: &Path) -> Result { + let source = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read plan file '{}'", path.display())) + .map_err(CliError::runtime)?; + let plan = extract(&source) + .with_context(|| format!("Failed to parse plan file '{}'", path.display())) + .map_err(CliError::runtime)?; + let report = validate(&plan); - Ok(STRUCTURED_PLAN_PASS.to_string()) + if !report.diagnostics.is_empty() { + return Err(CliError::validation(render_diagnostics( + &path.display().to_string(), + &report.diagnostics, + ))); } + + Ok(STRUCTURED_PLAN_PASS.to_string()) } fn render_diagnostics(path: &str, diagnostics: &[PlanDiagnostic]) -> String { diff --git a/cli/src/services/plan/init.rs b/cli/src/services/plan/init.rs new file mode 100644 index 000000000..507b1a279 --- /dev/null +++ b/cli/src/services/plan/init.rs @@ -0,0 +1,189 @@ +use std::fs::OpenOptions; +use std::io::{ErrorKind, Write}; +use std::path::Path; +use std::process::Command; + +use anyhow::{anyhow, Context}; + +use crate::services::error::CliError; +use crate::services::plan::{SCAFFOLD, SCAFFOLD_PLAN_NAME_TOKEN}; + +pub const PLAN_NAME_MAX_BYTES: usize = 64; +pub const PLANS_RELATIVE_DIR: &str = "context/plans"; +const PLAN_EXTENSION: &str = ".md"; +const CREATED_PREFIX: &str = "Created plan: "; +const NEXT_LINE: &str = "Next: complete the plan and run"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanName(String); + +impl PlanName { + pub fn parse(value: &str) -> Result { + if value.ends_with(PLAN_EXTENSION) { + return Err(invalid_name(value, "drop the '.md' suffix")); + } + if value.len() > PLAN_NAME_MAX_BYTES { + return Err(invalid_name( + value, + &format!("use at most {PLAN_NAME_MAX_BYTES} bytes"), + )); + } + if !is_kebab_case(value) { + return Err(invalid_name( + value, + "use lowercase letters and digits separated by single hyphens", + )); + } + Ok(Self(value.to_string())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +fn is_kebab_case(value: &str) -> bool { + !value.is_empty() + && value.split('-').all(|segment| { + !segment.is_empty() + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) + }) +} + +fn invalid_name(value: &str, remedy: &str) -> CliError { + CliError::validation(format!( + "Invalid plan name '{value}'. Try: {remedy}; plan names match [a-z0-9]+(-[a-z0-9]+)*." + )) +} + +pub fn create(name: &PlanName, start: &Path) -> Result { + let root = resolve_repository_root(start)?; + let context_dir = Path::new(&root).join("context"); + let plans_dir = Path::new(&root).join(PLANS_RELATIVE_DIR); + ensure_directory(&context_dir)?; + ensure_directory(&plans_dir)?; + + let file_name = format!("{}{PLAN_EXTENSION}", name.as_str()); + let relative_path = format!("{PLANS_RELATIVE_DIR}/{file_name}"); + let content = SCAFFOLD.replacen(SCAFFOLD_PLAN_NAME_TOKEN, name.as_str(), 1); + publish(&plans_dir, &file_name, &relative_path, &content)?; + + let absolute_path = format!("{root}/{relative_path}"); + Ok([ + format!("{CREATED_PREFIX}{relative_path}"), + NEXT_LINE.to_string(), + format!("sce plan check {}", shell_quote(&absolute_path)), + ] + .join("\n")) +} + +fn resolve_repository_root(start: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .current_dir(start) + .output() + .context("Failed to run 'git rev-parse --show-toplevel'. Try: install Git and rerun 'sce plan init'.") + .map_err(CliError::runtime)?; + if !output.status.success() { + return Err(CliError::runtime(anyhow!( + "Not inside a Git repository: '{}' ({}). Try: run 'sce plan init' inside a Git repository.", + start.display(), + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + + let stdout = String::from_utf8(output.stdout).map_err(|_| { + CliError::runtime(anyhow!( + "Repository root is not valid UTF-8. Try: move the repository to a UTF-8 path." + )) + })?; + let root = stdout.strip_suffix('\n').unwrap_or(&stdout); + if root.is_empty() || root.contains('\n') { + return Err(CliError::runtime(anyhow!( + "Repository root cannot be printed on one line. Try: move the repository to a path without newlines." + ))); + } + Ok(root.to_string()) +} + +fn ensure_directory(path: &Path) -> Result<(), CliError> { + match std::fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() => Ok(()), + Ok(_) => Err(not_a_directory(path)), + Err(error) if error.kind() == ErrorKind::NotFound => match std::fs::create_dir(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == ErrorKind::AlreadyExists => { + match std::fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() => Ok(()), + _ => Err(not_a_directory(path)), + } + } + Err(error) => Err(CliError::runtime( + anyhow!(error).context(format!("Failed to create directory '{}'", path.display())), + )), + }, + Err(error) => Err(CliError::runtime( + anyhow!(error).context(format!("Failed to inspect '{}'", path.display())), + )), + } +} + +fn not_a_directory(path: &Path) -> CliError { + CliError::runtime(anyhow!( + "'{}' is not a directory; symlinked or non-directory entries are refused. Try: replace it with a real directory.", + path.display() + )) +} + +fn publish( + plans_dir: &Path, + file_name: &str, + relative_path: &str, + content: &str, +) -> Result<(), CliError> { + let temporary = plans_dir.join(format!(".{file_name}.{}.tmp", std::process::id())); + let destination = plans_dir.join(file_name); + + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary) + .with_context(|| format!("Failed to create temporary file '{}'", temporary.display())) + .map_err(CliError::runtime)?; + let written = file + .write_all(content.as_bytes()) + .and_then(|()| file.flush()) + .with_context(|| format!("Failed to write temporary file '{}'", temporary.display())); + drop(file); + + let published = written.map_err(CliError::runtime).and_then(|()| { + std::fs::hard_link(&temporary, &destination).map_err(|error| { + if error.kind() == ErrorKind::AlreadyExists { + CliError::validation(format!( + "Plan already exists: {relative_path}. Try: choose another plan name; existing plans are never overwritten." + )) + } else { + CliError::runtime(anyhow!(error).context(format!( + "Failed to publish '{}'", + destination.display() + ))) + } + }) + }); + let removed = remove_temporary(&temporary); + + published?; + removed +} + +fn remove_temporary(temporary: &Path) -> Result<(), CliError> { + std::fs::remove_file(temporary) + .with_context(|| format!("Failed to remove temporary file '{}'", temporary.display())) + .map_err(CliError::runtime) +} + +fn shell_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 7b709894e..f8a1da3f4 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -1,16 +1,15 @@ pub mod command; pub mod extract; +pub mod init; pub mod validate; pub const NAME: &str = "plan"; -#[cfg_attr(not(test), expect(dead_code))] pub(crate) const SCAFFOLD: &str = include_str!(concat!( env!("OUT_DIR"), "/pkl-generated/config/plan-init-scaffold.md" )); -#[cfg_attr(not(test), expect(dead_code))] pub(crate) const SCAFFOLD_PLAN_NAME_TOKEN: &str = "{plan-name}"; #[cfg(test)] @@ -198,21 +197,25 @@ struct CommandRun { } #[cfg(test)] -fn run_plan_check(path: &str) -> CommandRun { - let command = crate::services::parse::command_runtime::parse_runtime_command( - ["sce", NAME, "check", path].map(String::from), +fn run_plan_args(args: &[&str], start: &std::path::Path) -> CommandRun { + let result = crate::services::parse::command_runtime::parse_runtime_command( + std::iter::once("sce") + .chain(args.iter().copied()) + .map(String::from), &crate::services::command_registry::CommandRegistry::default(), None, ) - .expect("plan check should parse"); - let crate::services::command_registry::RuntimeCommand::Plan(command) = command else { - panic!("expected plan command"); - }; + .and_then(|command| { + let crate::services::command_registry::RuntimeCommand::Plan(command) = command else { + panic!("expected plan command"); + }; + command.execute_with(|| Ok(start.to_path_buf())) + }); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let exit_code = crate::services::app_support::render_run_outcome( crate::services::app_support::RunOutcome { - result: command.execute(), + result, logger: None::, startup_diagnostic: None, }, @@ -227,6 +230,11 @@ fn run_plan_check(path: &str) -> CommandRun { } } +#[cfg(test)] +fn run_plan_check(path: &str) -> CommandRun { + run_plan_args(&[NAME, "check", path], std::path::Path::new(".")) +} + #[test] fn command_valid_plans_exit_success() { let relative_dir = "../config/pkl/renderers/fixtures/plan-structure"; @@ -346,3 +354,405 @@ fn completion_scripts_include_plan_check() { ); } } + +#[cfg(test)] +const SCAFFOLD_CHECK_DEFECTS: [&str; 3] = [ + "`AC1` is not covered by any task's `Covers:`", + "`Covers:` on `T01` is not a comma-separated ID list", + "`Scenarios:` on `AC1` is not a comma-separated ID list", +]; + +#[cfg(test)] +fn git_init(root: &std::path::Path) { + let output = std::process::Command::new("git") + .args(["init", "--quiet"]) + .current_dir(root) + .output() + .expect("git should run"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[cfg(test)] +fn temp_repository() -> (tempfile::TempDir, std::path::PathBuf) { + let directory = tempfile::tempdir().expect("tempdir should be created"); + let root = directory + .path() + .canonicalize() + .expect("tempdir should canonicalize"); + git_init(&root); + (directory, root) +} + +#[cfg(test)] +fn tree_listing(root: &std::path::Path) -> Vec { + fn walk(root: &std::path::Path, dir: &std::path::Path, out: &mut Vec) { + for entry in std::fs::read_dir(dir).expect("directory should be readable") { + let path = entry.expect("entry should be readable").path(); + if path.file_name().is_some_and(|name| name == ".git") { + continue; + } + out.push( + path.strip_prefix(root) + .expect("path under root") + .to_path_buf(), + ); + if path.symlink_metadata().expect("metadata").is_dir() { + walk(root, &path, out); + } + } + } + let mut out = Vec::new(); + walk(root, root, &mut out); + out.sort(); + out +} + +#[cfg(test)] +fn sorted_entries(dir: &std::path::Path) -> Vec { + let mut names: Vec = std::fs::read_dir(dir) + .expect("directory should be readable") + .map(|entry| { + entry + .expect("entry should be readable") + .file_name() + .to_string_lossy() + .into_owned() + }) + .collect(); + names.sort(); + names +} + +#[cfg(test)] +fn expected_init_stdout(root: &std::path::Path, name: &str) -> String { + let absolute = format!("{}/context/plans/{name}.md", root.display()); + format!( + "Created plan: context/plans/{name}.md\nNext: complete the plan and run\nsce plan check '{}'\n", + absolute.replace('\'', "'\\''") + ) +} + +#[cfg(test)] +fn check_defects(stderr: &str, path: &str) -> Vec { + let prefix = format!("{path}:"); + let mut defects: Vec = stderr + .lines() + .filter_map(|line| line.strip_prefix(&prefix)) + .map(|rest| { + rest.split_once(": ") + .map_or(rest, |(_, message)| message) + .to_string() + }) + .collect(); + defects.sort(); + defects +} + +#[test] +fn init_creates_scaffold_at_repository_root() { + let (_directory, root) = temp_repository(); + let run = run_plan_args(&[NAME, "init", "example"], &root); + + assert_eq!( + run.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + run.stderr + ); + assert_eq!(run.stdout, expected_init_stdout(&root, "example")); + assert!(run.stderr.is_empty(), "{}", run.stderr); + + let plans = root.join("context/plans"); + assert_eq!( + std::fs::read_to_string(plans.join("example.md")).expect("plan should exist"), + SCAFFOLD.replacen(SCAFFOLD_PLAN_NAME_TOKEN, "example", 1) + ); + assert!(std::fs::read_to_string(plans.join("example.md")) + .expect("plan should exist") + .starts_with("# Plan: example\n")); + assert_eq!(sorted_entries(&plans), vec!["example.md"]); + + let status = std::process::Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(&root) + .output() + .expect("git status should run"); + assert_eq!(String::from_utf8_lossy(&status.stdout), "?? context/\n"); +} + +#[test] +fn init_from_nested_directory_targets_repository_root() { + let directory = tempfile::tempdir().expect("tempdir should be created"); + let root = directory + .path() + .canonicalize() + .expect("tempdir should canonicalize") + .join("repo with 'quote'"); + std::fs::create_dir(&root).expect("root should be created"); + git_init(&root); + let nested = root.join("a/b"); + std::fs::create_dir_all(&nested).expect("nested directory should be created"); + + let run = run_plan_args(&[NAME, "init", "nested-plan"], &nested); + + assert_eq!( + run.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + run.stderr + ); + assert_eq!(run.stdout, expected_init_stdout(&root, "nested-plan")); + let created = root.join("context/plans/nested-plan.md"); + assert!(created.is_file()); + assert!(sorted_entries(&nested).is_empty()); + + let lines: Vec<&str> = run.stdout.lines().collect(); + assert_eq!(lines[0], "Created plan: context/plans/nested-plan.md"); + let output = std::process::Command::new("sh") + .arg("-c") + .arg(format!("sce() {{ printf '%s\\0' \"$@\"; }}\n{}", lines[2])) + .current_dir(&nested) + .output() + .expect("sh should run"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let decoded: Vec = String::from_utf8(output.stdout) + .expect("decoded arguments should be UTF-8") + .split_terminator('\0') + .map(String::from) + .collect(); + let created_path = created.display().to_string(); + assert_eq!( + decoded, + vec![NAME.to_string(), "check".to_string(), created_path.clone()] + ); + + let arguments: Vec<&str> = decoded.iter().map(String::as_str).collect(); + let check = run_plan_args(&arguments, &nested); + assert_eq!( + check.exit_code, + std::process::ExitCode::from(3), + "{}", + check.stderr + ); + assert!( + check.stderr.contains("SCE-ERR-VALIDATION"), + "{}", + check.stderr + ); + assert!( + !check.stderr.contains("Failed to read plan file"), + "{}", + check.stderr + ); + assert_eq!( + check_defects(&check.stderr, &created_path), + SCAFFOLD_CHECK_DEFECTS + ); +} + +#[test] +fn init_never_overwrites_existing_entry() { + let (_directory, root) = temp_repository(); + let plans = root.join("context/plans"); + std::fs::create_dir_all(&plans).expect("plans directory should be created"); + std::fs::write(plans.join("taken.md"), b"known bytes\n").expect("taken should be written"); + std::os::unix::fs::symlink("missing-target.md", plans.join("linked.md")) + .expect("symlink should be created"); + + for name in ["taken", "linked"] { + let run = run_plan_args(&[NAME, "init", name], &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{name}: {}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{name}: {}", run.stdout); + assert!( + run.stderr.contains("SCE-ERR-VALIDATION"), + "{name}: {}", + run.stderr + ); + assert!( + run.stderr + .contains(&format!("Plan already exists: context/plans/{name}.md")), + "{name}: {}", + run.stderr + ); + } + + assert_eq!( + std::fs::read(plans.join("taken.md")).expect("taken should be readable"), + b"known bytes\n" + ); + assert_eq!( + std::fs::read_link(plans.join("linked.md")).expect("linked should stay a symlink"), + std::path::PathBuf::from("missing-target.md") + ); + assert_eq!(sorted_entries(&plans), vec!["linked.md", "taken.md"]); +} + +#[test] +fn init_rejects_unsafe_names_without_writing() { + let (_directory, root) = temp_repository(); + let before = tree_listing(&root); + let too_long = "a".repeat(init::PLAN_NAME_MAX_BYTES + 1); + + for name in [ + "../escape", + "a/b", + "Upper", + "trailing-", + "double--hyphen", + "plan.md", + "", + too_long.as_str(), + ] { + let run = run_plan_args(&[NAME, "init", name], &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{name:?}: {}", + run.stderr + ); + assert!( + run.stderr.contains("SCE-ERR-VALIDATION"), + "{name:?}: {}", + run.stderr + ); + assert!( + run.stderr.contains("Invalid plan name"), + "{name:?}: {}", + run.stderr + ); + } + let run = run_plan_args(&[NAME, "init", "--", "-leading"], &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{}", + run.stderr + ); + assert!( + run.stderr.contains("Invalid plan name '-leading'"), + "{}", + run.stderr + ); + + for arguments in [ + vec![NAME, "init", "-leading"], + vec![NAME, "init", "--force", "example"], + vec![NAME, "init", "one", "two"], + ] { + let run = run_plan_args(&arguments, &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::from(2), + "{arguments:?}: {}", + run.stderr + ); + assert!( + run.stderr.contains("SCE-ERR-PARSE"), + "{arguments:?}: {}", + run.stderr + ); + } + + let run = run_plan_args(&[NAME, "init"], &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{}", + run.stderr + ); + assert!(run.stderr.contains("SCE-ERR-VALIDATION"), "{}", run.stderr); + assert!(run.stderr.contains(""), "{}", run.stderr); + + assert!(!root.join("context").exists()); + assert_eq!(tree_listing(&root), before); +} + +#[test] +fn init_outside_repository_is_runtime_failure() { + let directory = tempfile::tempdir().expect("tempdir should be created"); + let start = directory.path(); + let run = run_plan_args(&[NAME, "init", "example"], start); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(4), + "{}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("SCE-ERR-RUNTIME"), "{}", run.stderr); + assert!(tree_listing(start).is_empty()); + assert!(!start.join("context").exists()); +} + +#[test] +fn scaffold_authored_passes_check() { + let (_directory, root) = temp_repository(); + let run = run_plan_args(&[NAME, "init", "authored"], &root); + assert_eq!( + run.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + run.stderr + ); + + let path = root.join("context/plans/authored.md"); + let source = std::fs::read_to_string(&path).expect("plan should exist"); + assert_eq!(source.matches("Scenarios: TODO").count(), 1); + assert_eq!(source.matches("Covers: TODO").count(), 1); + let authored = source + .replacen("Scenarios: TODO", "Scenarios: none (inspection)", 1) + .replacen("Covers: TODO", "Covers: AC1", 1); + std::fs::write(&path, &authored).expect("plan should be written"); + + let check = run_plan_check(&path.display().to_string()); + assert_eq!( + check.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + check.stderr + ); + assert_eq!(check.stdout, format!("{}\n", command::STRUCTURED_PLAN_PASS)); + assert!(authored.contains("TODO")); +} + +#[test] +fn plan_init_appears_in_help() { + assert!(crate::services::help::help_text().contains("sce plan init")); + let plan_help = + crate::cli_schema::render_help_for_path(&[NAME]).expect("plan help should be renderable"); + assert!(plan_help.contains("init"), "{plan_help}"); + let help = crate::cli_schema::render_help_for_path(&[NAME, "init"]) + .expect("plan init help should be renderable"); + assert!(help.contains(""), "{help}"); +} + +#[test] +fn completion_scripts_include_plan_init() { + use crate::services::completion::{render_completion, CompletionRequest, CompletionShell}; + + for shell in [ + CompletionShell::Bash, + CompletionShell::Zsh, + CompletionShell::Fish, + ] { + let script = render_completion(CompletionRequest { shell }); + assert!( + script.contains("plan") && script.contains("init"), + "{shell:?}" + ); + } +} diff --git a/context/architecture.md b/context/architecture.md index 61bb5ba51..350e1bc78 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -82,7 +82,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). `command.rs` exposes it as the synchronous, database-free `sce plan check ` command. +- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). `command.rs` exposes it as the synchronous, database-free `sce plan check ` command, and `init.rs` backs `sce plan init `, which publishes the embedded scaffold to `context/plans/.md` at the Git repository root through a no-clobber hard link. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, plan, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 75884e5d9..b252df979 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -98,7 +98,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `version` exposes deterministic runtime identification output in text mode by default and JSON mode via `--format json`. `completion` exposes deterministic shell completion generation via `sce completion --shell `. -`plan` exposes `sce plan check `, a read-only structural check of one SCE plan file; its output and exit-code contract is owned by `context/cli/plan-service.md`. +`plan` exposes `sce plan check `, a read-only structural check of one SCE plan file, and `sce plan init `, which creates `context/plans/.md` at the Git repository root from the embedded plan scaffold without overwriting an existing entry; both output and exit-code contracts are owned by `context/cli/plan-service.md`. `setup` defaults to an `inquire` interactive target selection (OpenCode, Claude, Pi, Codex, All) and accepts mutually-exclusive non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--codex`, `--all`); the former `--both` flag was removed in favor of `--all` (opencode+claude+pi+codex); the interactive prompt title and target labels reuse shared prompt styling helpers when stdout color is enabled. Every setup mode first validates the Git repository and effective `agent_trace.repository_remote` (default `origin`) before prompts or writes. If that configured remote is missing, the typed `NotGitRemote` diagnostic uses generic `git remote add ` guidance; the preserved technical source identifies the effective name without exposing the URL. `setup` also accepts `--bootstrap-context` as a standalone context-only mode that ensures the durable-context baseline without prompts or integration installs; every normal successful setup path ensures that baseline only after both preflights. `setup` accepts a repeatable `--workflow ` flag selecting which optional workflows to install (currently only `brownfield`). Passing it makes the listed slugs the exact selection for that run; omitting it reuses the persisted `integrations.optional_workflows`, so a repeat run preserves an earlier opt-in. Unknown slugs fail request resolution with a validation error naming the embedded catalog's available slugs and write no files, and `--workflow` is rejected alongside `--bootstrap-context` or on a hooks-only run because neither installs target assets. The resolved selection filters the installed assets and is persisted; see [config precedence contract](config-precedence-contract.md) and [setup local bootstrap](../sce/setup-repo-local-config-bootstrap.md). An interactive `setup` run instead resolves the selection through an `inquire` multi-select shown after the target prompt, titled `Select optional workflows` with one `{title} — {description}` row per optional workflow using the shared prompt styling. Rows are unchecked when nothing is persisted and pre-checked from `integrations.optional_workflows` otherwise (a supplied `--workflow` list seeds them instead); the answered prompt is the run's exact selection. It then asks `Automatically sync Agent Traces?` followed by `(Requires an SCE account. Sends Agent Traces from supported AI coding tools` and `to SCE servers so they can be stored and viewed in your account.)`, then `Record SCE involvement in Git commits?` followed by `(Adds SCE metadata to commits created or assisted by SCE.)`, as independent confirmations, both defaulting to Yes; the two answers are carried independently through setup dispatch. Cancelling either interactive prompt yields the existing `Setup cancelled. No files were changed.` outcome, a non-TTY run keeps the existing actionable guidance, and the optional-workflow prompt is skipped when the catalog has no optional workflow. diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index 4d6fda7ee..ea4281c35 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -1,12 +1,13 @@ # Plan Service -`cli/src/services/plan/` owns deterministic, read-only structural analysis of one SCE plan Markdown file. It is the Rust counterpart of the test-only Pkl checker `config/pkl/renderers/plan-structure.pkl`. +`cli/src/services/plan/` owns deterministic, read-only structural analysis of one SCE plan Markdown file, and the `sce plan init` scaffold generator, its only writing operation. The analysis is the Rust counterpart of the test-only Pkl checker `config/pkl/renderers/plan-structure.pkl`. ## Current scope - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. - `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. -- `command.rs` owns the `sce plan check ` command (see [Command contract](#command-contract)). +- `command.rs` owns the `plan` command. `PlanCommand` holds one `PlanRequest::{Check(PlanCheckRequest), Init(PlanInitRequest)}`, mapped from `PlanSubcommand` in `services/parse/command_runtime.rs` and dispatched by a static `match`. `execute()` passes `std::env::current_dir` to `execute_with`, which tests call with an explicit start directory instead of changing process state (see [Command contract](#command-contract) and [Init command contract](#init-command-contract)). +- `init.rs` owns `PlanName` validation and `create(&PlanName, start)`, the repository-root scaffold publication. - `mod.rs` embeds the generated plan init scaffold as the crate-visible `SCAFFOLD` constant (`include_str!` of `OUT_DIR/pkl-generated/config/plan-init-scaffold.md`) with its `SCAFFOLD_PLAN_NAME_TOKEN` (`{plan-name}`). `cli/build.rs` requires that staged path, so a payload or package fallback without the scaffold fails the build. The binary never renders the scaffold itself. ## Plan model @@ -60,11 +61,23 @@ - Structurally invalid plan: `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`). The stderr message is a `Plan structure check failed for '{path}':` header, one `{path}:{line}: {message}` line per diagnostic (`{path}: {message}` when the diagnostic has no line) in `validate` order, and a closing `Try:` line. `{path}` is printed as given. - Missing or unreadable file: `CliError::runtime` (exit `4`, `SCE-ERR-RUNTIME`) with `Failed to read plan file '{path}'` context, and no diagnostics. -`plan` is a visible top-level command and appears in `sce --help` (including a `Plan Usage:` line) and in generated Bash/Zsh/Fish completions. +## Init command contract + +`sce plan init ` creates `context/plans/.md` at the Git repository root from the embedded scaffold, with its single `{plan-name}` token replaced by ``. It is synchronous, offline, uses no database, and invokes only `git`. + +- Name: one required positional `String`. `PlanName::parse` runs before any filesystem access, including reading the current directory: the value must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes (`PLAN_NAME_MAX_BYTES`), and not end in `.md`. Clap errors keep the existing `classify_clap_error` mapping: hyphen-prefixed values, unknown options, and extra positionals are parse errors (exit `2`); a missing `` is exit `3`. A value after `--` reaches name validation. +- Root: `git rev-parse --show-toplevel` run with the start directory as its working directory. A root that is not UTF-8 or contains a newline is refused before any write. +- Directories: `context` and `context/plans` must be real directories (`symlink_metadata`, so symlinks are refused) and are created when absent. Directories created by a failed run are not rolled back. +- Publication: the content is written to a hidden `..md..tmp` file opened with `create_new` inside `context/plans/`, then published with `std::fs::hard_link`. The temporary file is removed after publication and on every handled error after its creation. The destination either does not exist or holds the complete scaffold; an existing entry, including a dangling symlink, is never replaced. There is no non-atomic fallback, no `fsync`, and no crash-recovery cleanup, so a killed run can leave an orphaned temporary file. +- Success: exit `0` and exactly three stdout lines: `Created plan: context/plans/.md`, `Next: complete the plan and run`, and `sce plan check '/context/plans/.md'`. The path is always wrapped in POSIX single quotes, each `'` written as `'\''`, so the printed command works from any directory. +- Failures: an invalid name or an existing destination (`Plan already exists: context/plans/.md`) is `CliError::validation` (exit `3`). A missing repository, a failed or missing `git`, an unprintable root, a non-directory `context` path, and any other filesystem failure are `CliError::runtime` (exit `4`). + +`plan` is a visible top-level command and appears in `sce --help` (with `sce plan check ` and `sce plan init ` lines under `Plan Usage:`) and in generated Bash/Zsh/Fish completions. ## Terms - `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `pass` or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. +- `sce plan init`: The CLI command that publishes the plan init scaffold as `context/plans/.md` at the repository root through the [init command contract](#init-command-contract). It never overwrites an existing plan. - `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. - `plan init scaffold`: The generated `config/plan-init-scaffold.md` plan skeleton rendered by `config/pkl/base/plan-init-scaffold.pkl` and embedded as `SCAFFOLD`. Untouched, it fails `sce plan check` with exactly its `Scenarios: TODO` and `Covers: TODO` reference defects (see the [plan structure contract](../sce/plan-structure-contract.md)). @@ -77,4 +90,6 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at `cli/src/services/plan/extract.rs` unit tests (`plan::extract`) read the shared plan-structure fixtures through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. They cover fenced examples, misplaced definitions, unstructured plans with fenced structured examples, source-sliced field values with lines, and the ID grammar. -Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` cover help and completion. `plan::scaffold_untouched_fails_check` asserts the embedded scaffold has one `{plan-name}` token, defines no decision or scenario entries, and validates to exactly the three untouched-scaffold diagnostics. +Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help`, `plan::completion_scripts_include_plan_check`, `plan::plan_init_appears_in_help`, and `plan::completion_scripts_include_plan_init` cover help and completion. `plan::scaffold_untouched_fails_check` asserts the embedded scaffold has one `{plan-name}` token, defines no decision or scenario entries, and validates to exactly the three untouched-scaffold diagnostics. + +The `plan::init_*` tests run `sce plan init` in temporary `git init` repositories through the parser, `execute_with`, and the renderer: creation at the root with exact stdout, file content, and `git status` (`init_creates_scaffold_at_repository_root`); a nested start directory under a root containing a space and an apostrophe, whose third stdout line is executed by `sh` and dispatched back to `plan check` (`init_from_nested_directory_targets_repository_root`); a regular file and a dangling symlink left untouched with no temporary file (`init_never_overwrites_existing_entry`); application-validation, Clap-parse, and missing-argument rejections with an unchanged tree (`init_rejects_unsafe_names_without_writing`); and exit `4` outside a repository (`init_outside_repository_is_runtime_failure`). `plan::scaffold_authored_passes_check` shows that filling only `Scenarios:` and `Covers:` makes `check` pass while `TODO` placeholders remain. diff --git a/context/context-map.md b/context/context-map.md index 3b4b4610a..a369da7fd 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -18,7 +18,7 @@ Feature/domain context: - `context/cli/agent-trace-storage.md` (async repository-scoped Agent Trace storage resolver in `cli/src/services/agent_trace_storage/`: `AgentTraceStorageContext` inputs mirroring the `agent_trace.*` config keys, shared degradation of invalid default-discovered config before identity fallback while explicit selections remain fatal, `ResolvedAgentTraceStorage` carrying repository identity + `/sce/repos//agent-trace.db` path + open `RepositoryAgentTraceDb` + typed `RepositoryMetadata`, `resolve_agent_trace_storage{,_at_state_root}` setup/lifecycle entrypoints with idempotent concurrent-safe first open via bounded fast-path-then-migrate retry plus narrow one-file schema migration-metadata repair and repository metadata validation, a separate no-migration `resolve_agent_trace_storage_for_hook_runtime{,_at_state_root}` pair that high-frequency hook callers use exclusively and that never runs migration `002` or any migration, path-unsafe repository ID rejection in `default_paths::agent_trace_db_path_for_repository{,_at}`, strict never-touch boundary for any pre-migration checkout-scoped/global DB files, and active hook/runtime plus Agent Trace lifecycle setup call-site consumption after T08) - `context/cli/checkout-identity.md` (checkout identity infrastructure, `cli/src/services/checkout/` and its `/sce/checkout-id` UUIDv7 storage, was removed by the `remove-checkout-id` plan; documents the removal plus the never-touch on-disk handling of any pre-existing checkout-id/pre-migration checkout-scoped DB files left behind) - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) -- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, the shared `expectations.tsv` fixture manifest, the `sce plan check ` command with its exit `0`/`3`/`4` output contract, and the embedded plan init scaffold constant) +- `context/cli/plan-service.md` (read-only SCE plan structure extraction in `cli/src/services/plan/`: `markdown-rs` GFM `mdast` parsing into the owned `Plan`/`PlanEntry`/`PlanField` model with authoritative sections, source-sliced field values, 1-based lines, ignored `Code` nodes, and known divergences from the Pkl line scanner; pure `validate` rules with Pkl defect wording, line-anchored sorted diagnostics, the shared `expectations.tsv` fixture manifest, the `sce plan check ` command with its exit `0`/`3`/`4` output contract, the embedded plan init scaffold constant, and the `sce plan init ` repository-root, no-clobber hard-link publication contract) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) - `context/cli/agent-trace-auto-sync.md` (setup-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: a newly created repo-local config supplies the explicit `true` opt-in while omitted runtime values resolve to `false`; the existing `sce sync` command is launched once through the current executable after local persistence, with detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) diff --git a/context/glossary.md b/context/glossary.md index bc891de48..321bf5c96 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -1,5 +1,5 @@ # Glossary -- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`sce plan check`, `plan-structure expectations manifest`, `plan diagnostic`, `plan init scaffold`): [plan service terms](cli/plan-service.md#terms). +- CLI execution terminology: [command dispatch, runtime dependencies and telemetry](cli/execution-glossary.md). Plan-structure terminology (`sce plan check`, `sce plan init`, `plan-structure expectations manifest`, `plan diagnostic`, `plan init scaffold`): [plan service terms](cli/plan-service.md#terms). - `pkl-check-generated`: Flake app exposed as `nix run .#pkl-check-generated`; canonical ephemeral-generation check that rejects committed target/schema/mirror outputs, evaluates exact workflow metadata, the generated artifact contract, semantic layout/path/inventory/content/parity/observational checks, and the optional-workflow manifest's content against the catalog, requires the shared helper-composition rule and SCE-scoped workflow prohibitions, enforces ordered catalog-derived OpenCode skill permissions plus explicit-permission artifact integrity, rejects stale sibling-package references or unresolved internalization tokens in workflow entrypoint `SKILL.md` documents, proves contract failures through checked-in negative fixtures, and delegates deterministic generation plus payload/input inventories to the generated-input producer while preserving its established inventory report. - `repo-level verification preference`: Current repository guidance that contributor-facing validation/check flows should prefer `nix flake check`; direct Cargo verification commands are secondary and used only when explicitly requested or for narrow targeted debugging, while `cargo fmt` remains the explicit autofix path. Required quick checks after each completed task in this repo are `nix run .#pkl-check-generated` and `nix flake check`. - disposable plan lifecycle: Policy where `context/plans/` holds active execution artifacts only; completed plans are disposable and durable outcomes must be reflected in current-state context files and/or `context/decisions/`. diff --git a/context/overview.md b/context/overview.md index 43e6ce00e..1d781f249 100644 --- a/context/overview.md +++ b/context/overview.md @@ -45,7 +45,7 @@ for `/validate`. See This repository maintains shared assistant configuration for OpenCode, Claude, Pi, and Codex from a single canonical Pkl authoring source. One typed workflow catalog owns the six workflows' shared identity and target routing metadata, while canonical workflow/phase modules own behavior and migrated package-local documents, and target renderers own formatting. Generated target layouts are ephemeral: repository builds consume a pre-Cargo generated payload through `SCE_CLI_GENERATED_INPUT_DIR`, crates.io and Flatpak stage packaging-only fallbacks, and `config/.opencode`, `config/.claude`, `config/.pi`, `config/.agents`, and the generated working-tree SCE config schema are not committed; versioned SCE config schema snapshots live under `schema/v/`. The catalog also marks a workflow `optional` — currently only `brownfield` — which changes nothing about generation and is projected into a generated `config/optional-workflows.json` manifest for install-time consumers. `nix run .#pkl-check-generated` preserves its exact 145-path artifact, metadata/package, phase-reference, internal-reference, optional-workflow-manifest, workflow-orchestration, OpenCode-permission, required-path, and forbidden-path checks while delegating deterministic payload production and inventories to the shared generated-input producer; `nix flake check` runs the same contract. The target matrix contains one manual OpenCode profile plus Claude, Pi, and Codex; the former automated OpenCode profile has been removed. A fourth Pkl renderer, `config/pkl/renderers/codex-content.pkl`, also consumes the same canonical workflow composition to emit skills-only Codex output under `config/.agents/skills/**` (no per-target frontmatter, matching Pi, and no command/prompt layer). Each of the six catalog workflow skills (not `sce-decision`, which has no user-facing entrypoint on any target) additionally carries a Codex-only `agents/openai.yaml`, rendered by `config/pkl/renderers/codex-metadata.pkl` from the shared catalog's `title`/`description` plus an authored `default_prompt`, with `policy.allow_implicit_invocation: false` so these stateful lifecycle workflows activate only from explicit `$sce-` invocation or Codex's `/skills` discovery, never from conversational relevance alone. The Codex renderer also emits a Codex hook registration file (`config/.codex/hooks.json`) and its install-guidance helper (`config/.codex/hooks/run-sce-or-show-install-guidance.sh`). The existing conversation/diff registrations (`UserPromptSubmit`, `Stop`, `PreToolUse` for `Bash`, `PostToolUse` for `apply_patch`) route through `sce hooks codex` and retain their fail-open behavior where designed. The separate mutation-scope registrations route through `sce hooks codex-mutation-scope`: `PreToolUse` and `PostToolUse` use matcher `^(Bash|apply_patch)$`, while `Stop`, `Interrupt`, `SubagentStop`, and `SessionEnd` omit matcher and are unmatched. The tracked mutation `PreToolUse` bootstrap fails closed if Git-root resolution, helper or `sce` discovery, adapter startup, runtime `Start`, or Bash-policy evaluation cannot establish attribution; mutation-scope `PostToolUse` and cleanup hooks do not use this bootstrap behavior. Delegation, MCP, and unknown tools do not match these generated mutation Pre/Post registrations. `sce setup --codex` (and `--all`) now installs it as a fourth `SetupTarget`; setup merges the user-owned `.codex/hooks.json` through the shared structural Codex hook-config service, preserving unrelated valid handlers and rejecting malformed or Codex-invalid documents before the atomic swap. `sce doctor` diagnoses that file per required registration (`PresentAndCurrent`/`Missing`/`Stale`, or `Malformed` for the whole document) rather than by whole-document equality, reporting the mutation-scope registrations under distinct `#(mutation-scope)` rows separate from the `sce hooks codex` rows, and separately reports whether Codex has actually marked each structurally current registration trusted and whether its effective hook-discovery policy allows project hooks, by reading (never writing) Codex's own `$CODEX_HOME/config.toml` and policy state; `sce doctor --fix` repairs structurally unhealthy registrations through the same merge service but never touches trust or policy state (see `context/sce/doctor-human-text-contract.md`). `sce hooks codex` now exists as a typed dispatcher (`cli/src/services/hooks/codex/`): it parses the raw hook JSON into a `CodexHookEvent` and classifies `(hook_event_name, tool_name)` into `UserPromptSubmit`, `Stop`, `PreToolUse(Bash)`, `PostToolUse(apply_patch)`, or a fail-open `NoOp` fallthrough covering every other combination — including `apply_patch` under `PreToolUse`. `UserPromptSubmit` and `Stop` each capture one `messages`/`parts` row (`role="user"`/`role="assistant"`) into the repository Agent Trace DB under the idempotent `cx_` session prefix, `PreToolUse(Bash)` delegates to the existing Bash policy engine, and `PostToolUse(apply_patch)` parses Codex's own `apply_patch` text, resolves paths from the event `cwd` against the real Git root into safe repository-relative paths, normalizes Add/Update evidence into an SCE unified diff under deterministic event-scoped synthetic line identities derived from `tool_use_id`, and persists it as one `diff_traces` row when non-empty (see `context/sce/codex-integration-runtime.md`) — while invalid cwd/path resolution and malformed STDIN payloads fail open with empty stdout, reported model IDs are preserved without fabricated provider prefixes, and missing/blank apply_patch sessions produce no evidence. Delete-File operations and Bash filesystem effects do not become `diff_traces` in this existing conversation/diff pipeline, and its apply_patch evidence has separate operation limitations. Separately, the Codex mutation-scope adapter tracks `Bash` and `apply_patch` executions as `TrackedMutation` scopes; `AiExclusive` means tracked-scope exclusivity, not that Bash authored every mutation in the interval. -It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, deterministic plan structure checking (`sce plan check `), and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered without a module-wide dead-code allowance (only declaration-level, reasoned exceptions remain, and snapshot-ref reconciliation is wired explicitly through `sce doctor --fix`; automatic reclamation is a deferred follow-up). A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). +It also includes a Rust CLI (`sce`) for Shared Context Engineering workflows: auth, config inspection, setup, doctor, Agent Trace hooks and synchronization, bash-policy evaluation, deterministic plan structure checking (`sce plan check `) and plan scaffold creation (`sce plan init `), and repository-scoped Agent Trace storage infrastructure. See `context/architecture.md` for module-level boundaries and `context/context-map.md` for the full domain file index. A new pure, dependency-free `cli/src/services/mutation_trace/` module now exists as a Rust refinement of the verified `spec/mutation_cursor.qnt` mutation-cursor protocol; it currently carries domain types and the full pure action set — `prepare`/`commit` transition logic, attribution/mutation-event materialization, snapshot-failure/database-failure taint actions, scope abandonment, and recovery with an explicit observed-tree input — is registered without a module-wide dead-code allowance (only declaration-level, reasoned exceptions remain, and snapshot-ref reconciliation is wired explicitly through `sce doctor --fix`; automatic reclamation is a deferred follow-up). A separate `cli/src/services/mutation_trace/store.rs` persistence layer (built out by the `mutation-cursor-store-persistence` plan) now provides a real, CAS-guarded database call site against the repository-scoped Agent Trace DB, and a `cli/src/services/mutation_trace/runtime/` coordinator layer (built out by the `mutation-cursor-runtime-coordinator` plan) adds the per-worktree advisory lock, the isolated Git snapshot/ref-pinning service, and a public `coordinate()` entrypoint that drives the protocol against a real worktree under that lock, plus a per-worktree `ref_reconciliation` maintenance pass (built out by the `mutation-cursor-ref-reconciliation` plan) that, only for the namespace of a checkout id a current worktree still derives, reclaims orphaned SCE-owned snapshot refs under that same lock while retaining every tree any durable mutation-cursor state still references (a namespace no current worktree owns — via a deleted linked worktree or checkout-id metadata loss/recreation — is out of reach and left to future repository-scoped work). That runtime now has a second protected entrypoint alongside `coordinate()`: `cli/src/services/mutation_trace/runtime/scope_runtime.rs`'s `abandon_scope()` (built out by the `mutation-scope-runtime-integration` plan) retires a scope whose final worktree boundary was never observed — a dead agent process leaves no `Close` behind — sharing `coordinate()`'s extracted `runtime/protected_worktree.rs` safety prefix but deliberately capturing no Git snapshot, so abandonment is not a `RuntimeBoundary` and needs no Quint change. Both entrypoints are now reachable crate-wide through nine `pub(crate)` re-exports in `runtime/mod.rs` while every module behind them stays private, and the lifecycle contract every current or future harness adapter must uphold is recorded in `context/cli/mutation-scope-runtime.md`. The protocol runtime entrypoints (`coordinate()` / `abandon_scope()`) are now driven by one harness-neutral CLI ingress, the hidden `sce hooks mutation-scope` command (`context/cli/mutation-scope-hook-ingress.md`), which reads a single normalized JSON lifecycle object from STDIN, strictly parses it into exactly `start`/`advance`/`close`/`flush`/`abandon`, maps it to a `RuntimeBoundary` (`start`/`advance`/`close`/`flush`) or an `abandon_scope()` call forwarding `scope_id`/`event_id`/`actor_kind` verbatim (any `worktree_id` key is refused — worktree identity is derived by the runtime from the invoking checkout), and invokes the runtime with a lazy DB provider closure so DB acquisition stays inside the protected-worktree ordering. Unlike the fail-open `diff-trace`/`conversation-trace` intakes, this ingress never discards a valid boundary: it classifies results by durable completion, returning a non-zero `CliError` for a malformed payload or a pre-completion runtime error while treating the two carried-outcome variants (`MarkerClearAfterCommit` / `MarkerClearAfterCompletion` — the durable transition succeeded and only the trailing external-taint marker cleanup failed) as empty-stdout success without retrying the transition. Every successful execution emits empty stdout and writes `mutation_trace_*` rows only. It is the generic transport seam. Concrete harness lifecycle adapters for Claude Code and Codex are wired to it in-process via a `pub(crate)` seam rather than by re-invoking the ingress command; they map each harness's lifecycle events onto one mutation `ScopeId` per mutation-capable tool execution with write-ahead fail-closed `Start`, terminal `Close`, cleanup, recovery barriers, and harness-specific unsupported-boundary handling. A Pi adapter driver (`cli/src/services/hooks/pi_mutation_scope/`) also now consumes this same `pub(crate)` in-process seam through its hidden `sce hooks pi-mutation-scope` command and canonical generated Pi extension, with a `PendingStart`→`Executed`→`Closed`/`PendingAbandon` attempt-phase lifecycle keyed on Pi's `tool_result` event rather than `tool_execution_start` (see `context/cli/pi-mutation-scope-integration.md`); ordinary Pi sessions now reach it, and its human `user_bash` path uses the separate guarded external-mutation supervisor. Codex execution identity and `ScopeId`/`EventId` derivation are owned by `context/cli/codex-mutation-scope-integration.md`. A separate **read-only** consumer of the same module's persisted mutation-event history is wired, though: the post-commit Agent Trace flow calls `mutation_trace::runtime::resolve_post_commit_mutation_ai_patch(...)` to attribute committed touched lines that direct `diff_traces` evidence does not cover, by replaying the newest 128 events for the invoking worktree (bounded also by a commit attribution cut captured under the worktree lock) oldest-to-newest as one causal per-line provenance lineage rather than matching historical patches independently, resolving the worktree's *existing* checkout identity only, creating no identity and writing no mutation-cursor state, and never inserting into `diff_traces` or `post_commit_patch_intersections` (see `context/cli/mutation-trace-agent-attribution.md` and `context/sce/agent-trace-hooks-command-routing.md`). See also `context/cli/mutation-trace-protocol.md`, `context/cli/mutation-trace-runtime-coordinator.md`, `context/cli/mutation-trace-ref-reconciliation.md`, `context/cli/mutation-trace-protected-worktree.md`, `context/cli/mutation-trace-scope-abandonment.md`, `context/cli/mutation-scope-runtime.md`, `context/cli/mutation-scope-hook-ingress.md`, `context/cli/claude-mutation-scope-integration.md`, and `context/cli/mutation-trace-external-mutation-guard.md` (the harness-neutral process supervisor that lets Pi's `user_bash` mutate a worktree, guarded by the same `WorktreeLock`, without ever creating a scope). The generated `/next-task` workflow persists task-level context-synchronization lifecycle state in each plan (`pending`, `synced`, or `blocked`) so unresolved task synchronization debt survives a session boundary and gates new implementation. Successful `/next-task` execution hands task synchronization an explicit, pre-edit-Git-baseline-relative changed-file list plus implementation, verification, done-check, plan-update, and context-impact evidence, recorded directly on the completed task (`Completed`, `Files changed`, `Result`, `Verify`, `Context impact`, `Context synchronization`); the five-file root context pass remains mandatory. A later-session sync-debt retry reads that same completed task record directly from the plan by plan path and task ID, with no separate persisted synchronization handoff. `/validate` is validation-only: it runs final checks, writes the Validation Report, and reports `validated`, `failed`, or `blocked` without plan-level context synchronization. diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index d85f74e55..a585661c6 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -278,7 +278,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/config-generation.md` (new generated artifact, public skeleton function, `plan-init-scaffold` contract, artifact count 146), `context/sce/plan-structure-contract.md` (scaffold and skeleton mode, expected `check` defects), `context/cli/plan-service.md` (embedded scaffold constant and test). - Context synchronization: synced -- [ ] T02: `Add the sce plan init command with safe repository-root creation` (status:todo) +- [x] T02: `Add the sce plan init command with safe repository-root creation` (status:done) - Task ID: T02 - Covers: AC1, AC2, AC3, AC4, AC7 - Scope: In: @@ -291,8 +291,12 @@ Persist this field in every plan; this is durable plan state, not chat state: Out: Pkl sources, workflow prose, changes to `check` behavior or path resolution, changes to `classify_clap_error`, and Clap parser settings such as `allow_hyphen_values`. - Dependencies: T01 - Done when: `sce plan init ` behaves as D4 and D5 state from the repository root and from nested directories. Its printed `check` command works from any directory. Clap-rejected inputs keep their existing exit classes. Argument, name, and repository failures create no file or directory. Existing destinations are never modified. No handled failure publishes a partial plan or leaves a temporary file. All listed tests pass. Help and completions show `init`. - - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::`; `nix develop -c sh -c 'cd cli && cargo fmt'`. - - Context synchronization: pending + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` — passed (25 tests, including the eight new S1–S5, S7, and S12 tests); `nix develop -c sh -c 'cd cli && cargo fmt'` — applied. Also: `clippy --all-targets --all-features -- -D warnings` via `run-cli-cargo.sh` — clean; full CLI test suite via `run-cli-cargo.sh test` — 635 passed. + - Completed: 2026-10-24 + - Files changed: `cli/src/cli_schema.rs`, `cli/src/command_surface.rs`, `cli/src/services/parse/command_runtime.rs`, `cli/src/services/plan/command.rs`, `cli/src/services/plan/init.rs`, `cli/src/services/plan/mod.rs`, `context/plans/sce-plan-init.md` + - Result: `PlanSubcommand::Init { name }` maps to `PlanRequest::Init(PlanInitRequest)` beside `PlanRequest::Check(PlanCheckRequest)`. `PlanCommand::execute` delegates to `execute_with(current_dir)`, which validates the name through `init::PlanName::parse` before reading the current directory, so tests pass an explicit start directory without touching process state. `init::create` resolves the root with `git rev-parse --show-toplevel` from the start directory, refuses non-UTF-8 or multi-line roots, requires real (non-symlink) `context` and `context/plans` directories or creates them, writes `..md..tmp` with `create_new`, publishes with `hard_link`, and removes the temporary file after publication and on every handled error after its creation. `AlreadyExists` on the link is `CliError::validation` (`Plan already exists: context/plans/.md`); git, root, and filesystem failures are `CliError::runtime`. Stdout is the D5 three lines with the absolute path in POSIX single quotes. The T01 `dead_code` expectations on `SCAFFOLD` and `SCAFFOLD_PLAN_NAME_TOKEN` were removed. Deviation: `cli/src/command_surface.rs` gained a `sce plan init ` line in the top-level `Plan Usage:` help section, and `PLAN_CLAP_ABOUT` / `PLAN_TOP_LEVEL_PURPOSE` now mention creation, because S12 requires top-level help to mention `init`. The symlinked-directory open question follows D4 (refused, exit `4`). + - Context impact: `context/cli/plan-service.md` (`init.rs`, `PlanRequest`, `execute_with`, the init command contract, and the new tests), `context/cli/cli-command-surface.md` and `context/sce/cli-shell-completion-contract.md` (the `sce plan init ` surface and updated plan help text), `context/architecture.md` and `context/overview.md` (plan service no longer only read-only), `context/glossary.md` (`plan scaffold`). + - Context synchronization: synced - [ ] T03: `Use sce plan init in the change-to-plan workflow and document it` (status:todo) - Task ID: T03 diff --git a/context/sce/cli-shell-completion-contract.md b/context/sce/cli-shell-completion-contract.md index 0652cab37..690ce1ceb 100644 --- a/context/sce/cli-shell-completion-contract.md +++ b/context/sce/cli-shell-completion-contract.md @@ -28,7 +28,8 @@ Defines the implemented `sce completion` contract for deterministic shell comple - Output is a shell script payload emitted on stdout for redirection/eval. - Scripts are deterministic for identical binary + input shell. - Generated scripts encode current parser-valid command/flag/subcommand surfaces for: -- top-level commands: `help`, `config`, `setup`, `doctor`, `auth`, `hooks`, `sync`, `version`, `completion` +- top-level commands: `help`, `config`, `setup`, `doctor`, `auth`, `hooks`, `sync`, `plan`, `version`, `completion` + - plan subcommands: `check`, `init` - completion-specific flags and values: `--shell` with `bash|zsh|fish` ## Implementation ownership From 3eed91332e4f396d544968e2fbc551aa3ae572a7 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 16:37:18 +0200 Subject: [PATCH 22/61] plan+tests: Enforce explicit initialization and structural validation Make the change-to-plan workflow initialize new structured plans with `sce plan init` only after clarification, enforce scaffold completeness, and require the exact `sce plan check` success marker before `plan_ready`. Block unsupported or unexpected command results while preserving existing-plan behavior. Pin the gates and their negative cases in generation contracts, update canonical workflow/context records, and complete T03 in `context/plans/sce-plan-init.md`. Strip ANSI escape sequences from plan help assertions and wait for started Claude hook transactions to release the state lock before checking quiescence. Co-authored-by: SCE --- .../hooks/claude_mutation_scope/state.rs | 15 +++- cli/src/services/plan/mod.rs | 48 ++++++++-- config/pkl/base/workflow-change-to-plan.pkl | 64 ++++++++++--- config/pkl/check-generated.sh | 2 +- ...-authoring-structured-references-check.pkl | 2 +- .../renderers/generation-contract-check.pkl | 89 +++++++++++++++++-- context/context-map.md | 2 +- context/plans/sce-plan-init.md | 78 +++++++++++++--- context/sce/config-generation.md | 2 +- context/sce/shared-context-plan-workflow.md | 52 ++++++++--- 10 files changed, 304 insertions(+), 50 deletions(-) diff --git a/cli/src/services/hooks/claude_mutation_scope/state.rs b/cli/src/services/hooks/claude_mutation_scope/state.rs index 9cf2cfd8b..1a7fee541 100644 --- a/cli/src/services/hooks/claude_mutation_scope/state.rs +++ b/cli/src/services/hooks/claude_mutation_scope/state.rs @@ -390,14 +390,15 @@ mod tests { use std::time::{Duration, Instant}; use super::{ - clear_recovery_pending_if_quiescent_inner, read_state, write_state_durably_sync, - AdapterState, ClearRecoveryOutcome, + clear_recovery_pending_if_quiescent_inner, read_state, state_dir, write_state_durably_sync, + AdapterState, ClearRecoveryOutcome, STATE_LOCK, }; const INJECTED_WRITE_DELAY: Duration = Duration::from_millis(500); const UNRELATED_TIMER: Duration = Duration::from_millis(10); const RESPONSIVENESS_BUDGET: Duration = Duration::from_millis(250); const HOOK_RELEASE_TIMEOUT: Duration = Duration::from_secs(5); + const STARTED_TRANSACTION_FINISH_TIMEOUT: Duration = Duration::from_mins(2); fn seed_quiescent_recovery_pending(git_dir: &std::path::Path) { write_state_durably_sync( @@ -471,6 +472,16 @@ mod tests { .is_cancelled()); release_tx.send(()).expect("hook should still be waiting"); + drop( + STATE_LOCK + .acquire_with_timeout_async( + &state_dir(git_dir.path()), + STARTED_TRANSACTION_FINISH_TIMEOUT, + ) + .await + .expect("started transaction should finish and release the state lock"), + ); + let outcome = clear_recovery_pending_if_quiescent_inner(git_dir.path(), |_, _| Ok(())) .await .expect("state lock should be released and state should parse"); diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index f8a1da3f4..5a6686f85 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -731,15 +731,53 @@ fn scaffold_authored_passes_check() { #[test] fn plan_init_appears_in_help() { - assert!(crate::services::help::help_text().contains("sce plan init")); - let plan_help = - crate::cli_schema::render_help_for_path(&[NAME]).expect("plan help should be renderable"); + let root_help = strip_ansi(&crate::services::help::help_text()); + assert!(root_help.contains("sce plan init"), "{root_help}"); + let plan_help = strip_ansi( + &crate::cli_schema::render_help_for_path(&[NAME]).expect("plan help should be renderable"), + ); assert!(plan_help.contains("init"), "{plan_help}"); - let help = crate::cli_schema::render_help_for_path(&[NAME, "init"]) - .expect("plan init help should be renderable"); + let help = strip_ansi( + &crate::cli_schema::render_help_for_path(&[NAME, "init"]) + .expect("plan init help should be renderable"), + ); assert!(help.contains(""), "{help}"); } +#[cfg(test)] +fn strip_ansi(text: &str) -> String { + let mut output = String::with_capacity(text.len()); + let mut chars = text.chars().peekable(); + while let Some(character) = chars.next() { + if character != '\x1b' { + output.push(character); + continue; + } + match chars.next() { + Some('[') => { + for next in chars.by_ref() { + if ('@'..='~').contains(&next) { + break; + } + } + } + Some(']') => { + while let Some(next) = chars.next() { + if next == '\x07' { + break; + } + if next == '\x1b' && chars.peek() == Some(&'\\') { + chars.next(); + break; + } + } + } + _ => {} + } + } + output +} + #[test] fn completion_scripts_include_plan_init() { use crate::services::completion::{render_completion, CompletionRequest, CompletionShell}; diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 97c2d961c..8f7f6562b 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -307,7 +307,9 @@ When multiple existing plans match and none can be selected safely, return `blocked` with the matching candidates. When the request targets a new plan, derive `plan_name` as a short kebab-case -slug of the change, and confirm it does not collide with an existing plan. +slug of the change, and confirm it does not collide with an existing plan. Do +not create the plan file here; step 2.10 initializes it after the clarification +gate. Resolve exactly one plan target per invocation. @@ -448,20 +450,60 @@ complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. +This pass is pre-write authoring guidance only. It never substitutes for a +successful `sce plan check` in step 2.10. + ## 2.10 Write the plan -Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` -exactly. For revisions, apply its `Updating an existing plan` rules. +Structured plans are the only supported format. This step has no fallback for +`sce plan init` or `sce plan check`. + +### New plan + +Run `sce plan init {plan_name}`. Initialization succeeds only when all three +hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, +and that file exists. Every other result returns `blocked` with the exit code +and stderr. This includes a missing `sce` executable, a binary without `init`, +a nonzero exit, an exit `0` without the expected line, and an expected line +with no created file. + +Fill the generated scaffold with the plan content, applying the authoring rules +in `references/plan-template.md`. The template is the authoring reference for +filling the scaffold, never a substitute for `sce plan init`. Never write a new +plan without it. + +### Existing plan + +Do not run `sce plan init`. Revise the plan by applying the +`Updating an existing plan` rules in `references/plan-template.md`, which +preserve completed tasks and their evidence. The revised plan must satisfy the +same structured plan contract. + +### Completeness gate + +For new plans and revisions alike, the plan contains no scaffold `TODO` +placeholder, and optional sections without real entries (`Design decisions`, +`Assumptions`) are removed. The change summary, acceptance criteria, design +decisions, verification scenarios, task definitions, and verify instructions +are authored for this change. + +While any placeholder remains, keep authoring or return `blocked`. A passing +`sce plan check` is required but not sufficient: it is structural and does not +detect placeholders. + +### Structural check + +For new plans and revisions alike, run `sce plan check {plan-path}`. It passes +only on exit `0` with `structured plan: reference integrity holds` on stdout. -After writing the plan, run `sce plan check {plan-path}`. When it reports -diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. +On exit `3` with structural diagnostics, fix each reported defect in the plan +and run it again, or return `blocked` with the remaining diagnostics when they +cannot be resolved. Every other result returns `blocked` with the exit code and +stderr. This includes a missing `sce` executable, a binary without `check`, any +other exit code, and an exit `0` without the success marker. -Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` on stdout, or on exit `3` -with diagnostics. Any other outcome means the checker is unavailable; apply the -manual `Reference rules` pass from step 2.9 to the written plan instead, and -record in the open questions that the checker was unavailable. +Set internal status `plan_ready` only after both the completeness gate and +`sce plan check` pass. ## 2.11 Return the result diff --git a/config/pkl/check-generated.sh b/config/pkl/check-generated.sh index 047f12b7f..7e5749054 100755 --- a/config/pkl/check-generated.sh +++ b/config/pkl/check-generated.sh @@ -157,7 +157,7 @@ expect_pkl_fixture_failure \ "generated plan-template.md must define the Design decisions section, the Decisions:, Scenarios:, and Covers: reference fields, the Verification scenarios section, and the Design decision, Verification scenario, and Reference rules" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl" \ - "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document" + "generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan, and keep sce plan check out of every sce-next-task document" expect_pkl_fixture_failure \ "config/pkl/renderers/fixtures/next-task-structured-references-check.pkl" \ "generated sce-next-task plan-review.md, output.md, and task-execution.md must carry covered acceptance criteria, scenarios, and decisions, treat dangling references as review risks, and never check an acceptance criterion or record a scenario status during task execution" diff --git a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl index 4ec144cc9..84da9c5be 100644 --- a/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl +++ b/config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl @@ -8,4 +8,4 @@ local documents = new Mapping { result = if ( contract.assertPlanAuthoringStructuredReferences.apply(documents) ) "negative fixture unexpectedly accepted a plan-authoring phase without the structured authoring order or reference-consistency pass" -else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") +else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan, and keep sce plan check out of every sce-next-task document") diff --git a/config/pkl/renderers/generation-contract-check.pkl b/config/pkl/renderers/generation-contract-check.pkl index 8a93c72d2..fc85c132b 100644 --- a/config/pkl/renderers/generation-contract-check.pkl +++ b/config/pkl/renderers/generation-contract-check.pkl @@ -1122,12 +1122,8 @@ local requiredPlanAuthoringStructuredTokens = new Listing { "Give each task its `Covers:` field" "## 2.9 Check references" "run a reference-consistency pass over the\ncomplete draft that applies every `Reference rules` entry" - "After writing the plan, run `sce plan check {plan-path}`." - "Do\nnot set internal status `plan_ready` until the checker passes." - "Trust the checker only when it actually ran:" + "run `sce plan check {plan-path}`." "`structured plan: reference integrity holds`" - "Any other outcome means the checker is unavailable;" - "manual `Reference rules` pass from step 2.9" } local noNextTaskPlanCheckReference = (documents: Mapping) -> @@ -1141,7 +1137,87 @@ hidden assertPlanAuthoringStructuredReferences = (documents: Mapping) -> ) && noNextTaskPlanCheckReference.apply(documents) ) "sce-change-to-plan plan-authoring: decisions, criteria, scenarios, and tasks authored in order with a reference-consistency pass" - else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan with the checker trust rule and manual Reference rules fallback, and keep sce plan check out of every sce-next-task document") + else throw("generated plan-authoring.md must author design decisions, acceptance criteria, verification scenarios, and tasks in that order, forbid manufactured decisions and invented test names, add Covers:, and run a reference-consistency pass before writing, run sce plan check after writing the plan, and keep sce plan check out of every sce-next-task document") + +local planInitSuccessCondition = "Initialization succeeds only when all three\nhold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`,\nand that file exists." + +local planCheckSuccessCondition = "It passes\nonly on exit `0` with `structured plan: reference integrity holds` on stdout." + +local planCompletenessGate = "### Completeness gate" + +local planReadyPermission = "Set internal status `plan_ready` only after both the completeness gate and\n`sce plan check` pass." + +local requiredPlanAuthoringPlanInitTokens = new Listing { + "Structured plans are the only supported format. This step has no fallback for\n`sce plan init` or `sce plan check`." + "### New plan\n\nRun `sce plan init {plan_name}`." + planInitSuccessCondition + "Every other result returns `blocked` with the exit code\nand stderr." + "a missing `sce` executable, a binary without `init`,\na nonzero exit, an exit `0` without the expected line, and an expected line\nwith no created file." + "never a substitute for `sce plan init`. Never write a new\nplan without it." + "### Existing plan\n\nDo not run `sce plan init`." + "`Updating an existing plan` rules in `references/plan-template.md`, which\npreserve completed tasks and their evidence." + planCompletenessGate + "the plan contains no scaffold `TODO`\nplaceholder, and optional sections without real entries (`Design decisions`,\n`Assumptions`) are removed." + "The change summary, acceptance criteria, design\ndecisions, verification scenarios, task definitions, and verify instructions\nare authored for this change." + "While any placeholder remains, keep authoring or return `blocked`." + "`sce plan check` is required but not sufficient" + "### Structural check\n\nFor new plans and revisions alike, run `sce plan check {plan-path}`." + planCheckSuccessCondition + "On exit `3` with structural diagnostics, fix each reported defect in the plan\nand run it again, or return `blocked` with the remaining diagnostics" + "a binary without `check`, any\nother exit code, and an exit `0` without the success marker." + planReadyPermission + "It never substitutes for a\nsuccessful `sce plan check` in step 2.10." +} + +local forbiddenPlanAuthoringFallbackTokens = new Listing { + "manual" + "Manual" + "checker is unavailable" + "older `sce`" + "`plan` help" + "plan --help" + "Usage: plan" +} + +local hasPlanAuthoringPlanInitContract = (text: String) -> + requiredPlanAuthoringPlanInitTokens.every((token) -> text.contains(token)) + && forbiddenPlanAuthoringFallbackTokens.every((token) -> !text.contains(token)) + && text.indexOf("## 2.3 Run the clarification gate") < text.indexOf("`sce plan init") + && text.indexOf(planCompletenessGate) < text.indexOf(planReadyPermission) + && text.indexOf(planCheckSuccessCondition) < text.indexOf(planReadyPermission) + +local noPlanInitOutsideChangeToPlan = (documents: Mapping) -> + documents.every((path, text) -> + !(path.contains("/skills/sce-next-task/") || path.contains("/skills/sce-validate/")) + || !text.contains("sce plan init") + ) + +local completenessGateSection = (text: String) -> + let (tail = text.drop(text.indexOf(planCompletenessGate))) + tail.take(tail.indexOf("### Structural check")) + +local planAuthoringPlanInitFixtures = (text: String) -> + let (gate = completenessGateSection.apply(text)) + List( + text.replaceFirst(gate, ""), + text.replaceFirst(gate, "").replaceFirst(planReadyPermission, planReadyPermission + "\n\n" + gate), + text + "\nWhen `sce plan init` is unavailable or fails, author the new plan manually from\n`references/plan-template.md`.\n", + text.replaceFirst(planInitSuccessCondition, "Initialization succeeds on exit `0`."), + text + "\nAny other outcome means the checker is unavailable; apply the\nmanual `Reference rules` pass from step 2.9 to the written plan instead.\n", + text.replaceFirst(planCheckSuccessCondition, "It passes\non exit `0`.") + ) + +hidden assertPlanAuthoringPlanInit = (documents: Mapping) -> + let (authoring = documents.toMap().filter((path, _) -> path.endsWith("/skills/sce-change-to-plan/references/plan-authoring.md"))) + if ( + authoring.length == generatedTargetRoots.length + && authoring.every((_, text) -> + hasPlanAuthoringPlanInitContract.apply(text) + && planAuthoringPlanInitFixtures.apply(text).every((fixture) -> !hasPlanAuthoringPlanInitContract.apply(fixture)) + ) + && noPlanInitOutsideChangeToPlan.apply(documents) + ) "sce-change-to-plan plan-authoring: sce plan init for new plans only, completeness gate and passing sce plan check before plan_ready, no fallback; negative fixtures rejected" + else throw("generated plan-authoring.md must run sce plan init for new plans after the clarification gate with the exact success condition, skip init for revisions, block on every other init or check result, gate plan_ready on the completeness gate and a check exit 0 with the success marker, and contain no legacy-binary detection or manual initialization or validation fallback; every negative fixture must be rejected; no sce-next-task or sce-validate document may mention sce plan init") local requiredNextTaskStructuredReferenceTokens = new Listing { Pair("/skills/sce-next-task/references/plan-review.md", "Read plan structure only outside fenced code blocks;") @@ -1530,6 +1606,7 @@ contractChecks { ["plan-init-scaffold"] = assertPlanInitScaffold.apply(generatedArtifacts) ["plan-template-policy-ownership"] = assertPlanTemplatePolicyOwnership.apply(workflowDocuments) ["plan-authoring-structured-references"] = assertPlanAuthoringStructuredReferences.apply(workflowDocuments) + ["plan-authoring-plan-init"] = assertPlanAuthoringPlanInit.apply(workflowDocuments) ["next-task-structured-references"] = assertNextTaskStructuredReferences.apply(workflowDocuments) ["next-task-compact-completion-writing"] = assertNextTaskCompactCompletionWriting.apply(workflowDocuments) ["plan-review-reads-completed-record"] = assertPlanReviewReadsCompletedRecord.apply(workflowDocuments) diff --git a/context/context-map.md b/context/context-map.md index a369da7fd..53a82c6a6 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -59,7 +59,7 @@ Feature/domain context: - `context/sce/claude-raw-hook-capture.md` (removed feature: the former `sce hooks claude-capture` raw-capture route and its supporting types, replaced by the active `diff-trace` and `conversation-trace` intakes) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with explicit config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary, logger and deliberately non-object-safe generic async telemetry boundaries with static capability dispatch and future-aware polling scope, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, managed OTLP trace/log export (credential-gated, hardcoded canonical collector, allowlisted fields, bounded fail-open lifecycle, reserved metrics endpoint), and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) -- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, no legacy plan format), clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) +- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, no legacy plan format), `sce plan init` new-plan creation with the completeness and mandatory `sce plan check` gates and no fallback, clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) - [Plan structure contract](sce/plan-structure-contract.md) (shared Pkl/Rust plan-structure checker contract: shared rule set, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` as the single fixture source of truth and how to add a fixture, `sce plan check` result and diagnostic wording shared with the Pkl defect strings, the both-checkers rewording rule, known AST-versus-line-scanner divergences, and Pkl staying generation-only while the CLI never invokes Pkl) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) - `context/sce/plan-code-overlap-map.md` (overlap matrix for thin OpenCode Plan/Code routing agents and six workflow packages, including task-only context synchronization ownership) diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index a585661c6..1ec72cf8a 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -83,31 +83,31 @@ How this plan is proven complete. Each criterion is observable and names the check that proves it. `/validate` runs these checks; no task in the stack performs final validation. -- [ ] AC1: `sce plan init example`, run inside a Git repository, creates `context/plans/example.md` holding the embedded scaffold with the title `# Plan: example`, and creates `context/plans/` when it is missing. It prints the three-line D5 success output: the repository-relative `Created plan:` path, and a `sce plan check` command with the shell-quoted absolute plan path. No temporary file remains in `context/plans/`, and no other repository file is created or modified. +- [x] AC1: `sce plan init example`, run inside a Git repository, creates `context/plans/example.md` holding the embedded scaffold with the title `# Plan: example`, and creates `context/plans/` when it is missing. It prints the three-line D5 success output: the repository-relative `Created plan:` path, and a `sce plan check` command with the shell-quoted absolute plan path. No temporary file remains in `context/plans/`, and no other repository file is created or modified. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - Decisions: D3, D4, D5 - Scenarios: S1 -- [ ] AC2: Existing destinations, including symlinks, are never overwritten or modified, and a conflict leaves no temporary file behind. Invalid names that reach application validation exit `3`. Inputs that Clap rejects exit `2`, and a missing `` keeps the existing Clap mapping (exit `3`). Argument and name-validation rejections create no directory or file. A missing repository exits `4` and leaves the directory unchanged. +- [x] AC2: Existing destinations, including symlinks, are never overwritten or modified, and a conflict leaves no temporary file behind. Invalid names that reach application validation exit `3`. Inputs that Clap rejects exit `2`, and a missing `` keeps the existing Clap mapping (exit `3`). Argument and name-validation rejections create no directory or file. A missing repository exits `4` and leaves the directory unchanged. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - Decisions: D4, D5 - Scenarios: S3, S4, S5 -- [ ] AC3: Running `init` from a nested directory of a Git repository, including one whose root path contains a space and an apostrophe, creates `/context/plans/.md`. It prints the same repository-relative `Created plan:` line. Its printed `sce plan check` command, run from that nested directory, checks the created file and fails structural validation with exit `3`, not a missing-file runtime error. +- [x] AC3: Running `init` from a nested directory of a Git repository, including one whose root path contains a space and an apostrophe, creates `/context/plans/.md`. It prints the same repository-relative `Created plan:` line. Its printed `sce plan check` command, run from that nested directory, checks the created file and fails structural validation with exit `3`, not a missing-file runtime error. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` - Decisions: D4, D5 - Scenarios: S2 -- [ ] AC4: The untouched scaffold fails `sce plan check` with exactly the incomplete-reference defects. The same scaffold with only its reference placeholders filled passes, although its other `TODO` placeholders remain, which shows that `check` is structural and not semantic approval. +- [x] AC4: The untouched scaffold fails `sce plan check` with exactly the incomplete-reference defects. The same scaffold with only its reference placeholders filled passes, although its other `TODO` placeholders remain, which shows that `check` is structural and not semantic approval. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold` - Decisions: D1 - Scenarios: S6, S7 -- [ ] AC5: The scaffold is generated from the canonical Pkl sources and pinned to the template skeleton by the generation contract. It is embedded in the binary, it is present in the packaged fallback, and it is never committed to the repository. +- [x] AC5: The scaffold is generated from the canonical Pkl sources and pinned to the template skeleton by the generation contract. It is embedded in the binary, it is present in the packaged fallback, and it is never committed to the repository. - Validate: `nix run .#pkl-check-generated` - Decisions: D1, D2 - Scenarios: S8, S9 -- [ ] AC6: The generated `sce-change-to-plan` workflow runs `sce plan init` for new plans only, after its clarification and target-resolution gates, and skips `init` for revisions while preserving completed tasks and evidence. It accepts initialization only on exit `0` with the success line and an existing file. It returns `blocked` when `init` is missing, unsupported, exits nonzero, or returns unexpected output. It never reports `plan_ready` while scaffold `TODO` placeholders remain. For new plans and revisions alike, it reports `plan_ready` only after `sce plan check` exits `0` with `structured plan: reference integrity holds`. It repairs and reruns on structural diagnostics, and returns `blocked` when `check` is unavailable or returns any other result. The workflow contains no legacy-binary detection, no manual initialization fallback, and no manual validation fallback. Generation contract checks enforce each of these rules and reject negative fixtures that break them. +- [x] AC6: The generated `sce-change-to-plan` workflow runs `sce plan init` for new plans only, after its clarification and target-resolution gates, and skips `init` for revisions while preserving completed tasks and evidence. It accepts initialization only on exit `0` with the success line and an existing file. It returns `blocked` when `init` is missing, unsupported, exits nonzero, or returns unexpected output. It never reports `plan_ready` while scaffold `TODO` placeholders remain. For new plans and revisions alike, it reports `plan_ready` only after `sce plan check` exits `0` with `structured plan: reference integrity holds`. It repairs and reruns on structural diagnostics, and returns `blocked` when `check` is unavailable or returns any other result. The workflow contains no legacy-binary detection, no manual initialization fallback, and no manual validation fallback. Generation contract checks enforce each of these rules and reject negative fixtures that break them. - Validate: `nix run .#pkl-check-generated` - Decisions: D6 - Scenarios: S10, S11, S13, S14, S15 -- [ ] AC7: `sce --help`, `sce plan --help`, and the generated Bash, Zsh, and Fish completion scripts include `plan init`. +- [x] AC7: `sce --help`, `sce plan --help`, and the generated Bash, Zsh, and Fish completion scripts include `plan init`. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` - Decisions: D3 - Scenarios: S12 @@ -298,7 +298,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/cli/plan-service.md` (`init.rs`, `PlanRequest`, `execute_with`, the init command contract, and the new tests), `context/cli/cli-command-surface.md` and `context/sce/cli-shell-completion-contract.md` (the `sce plan init ` surface and updated plan help text), `context/architecture.md` and `context/overview.md` (plan service no longer only read-only), `context/glossary.md` (`plan scaffold`). - Context synchronization: synced -- [ ] T03: `Use sce plan init in the change-to-plan workflow and document it` (status:todo) +- [x] T03: `Use sce plan init in the change-to-plan workflow and document it` (status:done) - Task ID: T03 - Covers: AC6 - Scope: In: @@ -312,9 +312,67 @@ Persist this field in every plan; this is durable plan state, not chat state: Out: `/next-task` and `/validate` content, including the `sce-validate` checker fallback; the plan template schema; CLI code; and any TODO detection, new mode, or behavior change in `sce plan check`. - Dependencies: T02 - Done when: All four targets' generated `plan-authoring.md` carry the D6 behavior, including the completeness gate and the mandatory `check` gate. None of them contains legacy-binary detection, a manual initialization fallback, or a manual validation fallback. The contract assertion passes on generated output and fails on each of its negative fixtures. No `sce-next-task` or `sce-validate` document mentions `sce plan init`. - - Verify: `nix run .#pkl-check-generated`. - - Context synchronization: pending + - Verify: `nix run .#pkl-check-generated` — passed (146 files; the new `plan-authoring-plan-init` contract check evaluates to its pass message, which requires all four targets' `plan-authoring.md` to satisfy it and all six negative fixtures to be rejected; the updated `plan-authoring-structured-references` check and its existing negative fixture still pass). + - Completed: 2026-10-24 + - Files changed: `config/pkl/base/workflow-change-to-plan.pkl`, `config/pkl/check-generated.sh`, `config/pkl/renderers/fixtures/plan-authoring-structured-references-check.pkl`, `config/pkl/renderers/generation-contract-check.pkl`, `context/sce/shared-context-plan-workflow.md`, `context/plans/sce-plan-init.md` + - Result: Step 2.1 no longer writes, and says step 2.10 initializes a new plan after the clarification gate. Step 2.9 is pre-write guidance that never substitutes for `sce plan check`. Step 2.10 now has `New plan` (`sce plan init {plan_name}` with the exact three-part D6 success condition, `blocked` otherwise, then fill the scaffold), `Existing plan` (no `init`, `Updating an existing plan` rules), `Completeness gate`, and `Structural check` (exit `0` plus success marker, repair and rerun on exit `3`, `blocked` otherwise) subsections, and ends with the `plan_ready` permission. The manual `Reference rules` fallback and its open-question note were removed. `assertPlanAuthoringPlanInit` requires the D6 tokens, forbids `manual`, `checker is unavailable`, and legacy-help wording, orders the clarification gate before `sce plan init` and the gate and check marker before the `plan_ready` permission, and keeps `sce plan init` out of `sce-next-task` and `sce-validate` documents. Its six negative fixtures are derived in-contract from each real generated document: gate removed, gate moved after `plan_ready`, manual template fallback appended, init success reduced to exit `0`, the old checker fallback appended, and check success reduced to exit `0`. `requiredPlanAuthoringStructuredTokens` dropped the fallback tokens, and its failure message (mirrored in the fixture and `check-generated.sh`) dropped "with the checker trust rule and manual Reference rules fallback". Deviation: the negative fixtures live in `generation-contract-check.pkl`, following `assertCompactExecutionContractFixtures`, rather than as separate fixture files. + - Context impact: `context/sce/shared-context-plan-workflow.md` (updated in this task: step 2.10 init, completeness, and check gates, and the change-to-plan versus validate checker split); `context/sce/config-generation.md` (the `plan-authoring-plan-init` contract check). + - Context synchronization: synced ## Open questions - Should `init` refuse a symlinked `context/` or `context/plans/` directory, as D4 plans to? A repository that deliberately symlinks `context/` elsewhere would be blocked. Following the link instead would allow writes outside the repository root. + +## Validation Report + +**Status:** validated +**Date:** 2026-10-24 + +### Commands run + +- `nix run .#sce -- plan check context/plans/sce-plan-init.md` -> exit 0 (`structured plan: reference integrity holds`) +- `nix flake check` -> exit 0 (all checks passed) +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 146 files, inventory sha256 49dfdb6b…0392) +- `nix build .#default --no-link` -> exit 0 (packaged CLI built) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` -> exit 0 (5 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold` -> exit 0 (2 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` -> exit 0 (1 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::completion_scripts_include_plan_init` -> exit 0 (1 passed) +- `bash scripts/test-prepare-cli-generated-assets.sh` -> exit 0 (delegation tests passed) + +### Success-criteria verification + +- [x] AC1: `sce plan init example` creates the scaffold at the repository root with the D5 output -> `plan::init` filter passed (5 tests); scenarios: S1 +- [x] AC2: Existing entries never overwritten; validation, Clap, and missing-repository failure classes with no writes -> `plan::init` filter passed; scenarios: S3, S4, S5 +- [x] AC3: Nested invocation targets the root and its printed check command works there -> `plan::init` filter passed; scenarios: S2 +- [x] AC4: Untouched scaffold fails `check` with exactly the reference defects; reference-filled scaffold passes -> `plan::scaffold` filter passed (2 tests); scenarios: S6, S7 +- [x] AC5: Scaffold generated, contract-pinned, embedded, in the fallback, not committed -> `nix run .#pkl-check-generated` passed, `nix build .#default` passed, fallback test passed, no `config/plan-init-scaffold.md` in the worktree or `git ls-files`; scenarios: S8, S9 +- [x] AC6: Workflow uses `init` for new plans only, blocks on any other result, gates `plan_ready` on completeness and `check`, has no fallbacks -> `nix run .#pkl-check-generated` passed, including the `plan-authoring-plan-init` contract and its negative fixtures; scenarios: S10, S11, S13, S14, S15 +- [x] AC7: Help and completions include `plan init` -> `plan::plan_init` filter passed, and `cli-tests` passed inside `nix flake check`; scenarios: S12 + +### Scenario verification + +- S1 (AC1): test `services::plan::init_creates_scaffold_at_repository_root` -> ok; status: Verified +- S2 (AC3): test `services::plan::init_from_nested_directory_targets_repository_root` -> ok; status: Verified +- S3 (AC2): test `services::plan::init_never_overwrites_existing_entry` -> ok; status: Verified +- S4 (AC2): test `services::plan::init_rejects_unsafe_names_without_writing` -> ok; status: Verified +- S5 (AC2): test `services::plan::init_outside_repository_is_runtime_failure` -> ok; status: Verified +- S6 (AC4): test `services::plan::scaffold_untouched_fails_check` -> ok; status: Verified +- S7 (AC4): test `services::plan::scaffold_authored_passes_check` -> ok; status: Verified +- S8 (AC5): command `nix run .#pkl-check-generated` (`plan-init-scaffold` contract) -> passed, 146 files; no repository-local `config/plan-init-scaffold.md`; status: Verified +- S9 (AC5): command `bash scripts/test-prepare-cli-generated-assets.sh` and `nix build .#default --no-link` -> both exit 0; status: Verified +- S10 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract) -> passed; status: Verified +- S11 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract) -> passed; status: Verified +- S12 (AC7): test `services::plan::plan_init_appears_in_help` and `services::plan::completion_scripts_include_plan_init` -> both ok; `cli-tests` passed in `nix flake check`; status: Verified +- S13 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified +- S14 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified +- S15 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- `nix flake check` does not build the release package, so a non-test-only compile failure is caught only by `nix build .#default`. +- The worktree carries an out-of-scope test change in `cli/src/services/hooks/claude_mutation_scope/state.rs`; it is not part of this plan. diff --git a/context/sce/config-generation.md b/context/sce/config-generation.md index 12dbe4ac0..09b2eb584 100644 --- a/context/sce/config-generation.md +++ b/context/sce/config-generation.md @@ -54,7 +54,7 @@ Renderer modules apply target-specific metadata/frontmatter rules while reusing - The canonical OpenCode plugin-registration source for generated SCE plugins lives in `config/pkl/base/opencode.pkl`; `config/pkl/renderers/common.pkl` re-exports the shared plugin list and JSON-ready paths for OpenCode renderers, and the current generated registration scope is limited to SCE-managed plugins emitted by this repo (`sce-bash-policy` and `sce-agent-trace`). - Target renderers remain responsible for formatting target-supported metadata. OpenCode metadata owns thin-agent presentation and compatibility while deriving the ordered permission blocks — non-SCE wildcard allow, `sce-*` wildcard deny, then catalog-owned workflow allows — from catalog role assignments; OpenCode command routing derives the same role and skill identity from the catalog. Claude metadata derives command tools from catalog records. Pi has no metadata module because it adds no target-specific frontmatter. - `config/pkl/renderers/metadata-coverage-check.pkl` derives commands and exact package-relative workflow-document expectations from the typed catalog and the workflow-document inventories, adds the unchanged phase-free and decision-package expectations, verifies every command's one-to-one workflow-skill route for OpenCode/Claude/Pi, asserts the shared exact skill-document inventory plus Codex's own inventory (the shared inventory extended with the six catalog workflows' `agents/openai.yaml`, no command-route check since Codex has no commands), and forces every rendered document and target metadata lookup to evaluate. -- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (146) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). It also asserts `plan-init-scaffold`: the generated `config/plan-init-scaffold.md` passes skeleton mode (`skeletonDefects` in `plan-structure.pkl`), starts with `# Plan: {plan-name}` and carries that token exactly once, defines no `D` or `S` entry, contains no fenced block or template rule/example sections, and fails `check` with exactly ``Scenarios:` on `AC1` is not a comma-separated ID list``, ``Covers:` on `T01` is not a comma-separated ID list``, and ``AC1` is not covered by any task's `Covers:``. `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `pass`/`fail` verdict, then expected defect messages; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `pass` rows must pass with no listed messages, `fail` rows must fail with exactly the listed messages, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, and fenced example repeating active IDs; the negative fixtures are an unstructured plan snapshot, an unstructured plan with a fenced structured example, duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing the plan with the checker trust rule and manual `Reference rules` fallback. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks; `output.md` carries the `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under **What failed**. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. +- `config/pkl/renderers/generation-contract-check.pkl` independently derives the complete expected artifact paths from those target document inventories plus explicitly retained non-workflow assets, compares them with `generate.pkl`'s `output.files`, and requires the exact path count declared by the current generation contract — stated as a literal `expectedArtifactPathCount` (146) inside the same assertion so an unintended inventory change fails rather than redefining the expectation. It asserts (`codex-skill-metadata`) that each of the six generated `agents/openai.yaml` files contains `policy.allow_implicit_invocation: false` plus catalog-derived `interface.{display_name,short_description,default_prompt}` values sourced from `codex-metadata.pkl`, and that no such file exists under `sce-decision`. It asserts the generated `config/optional-workflows.json` against the catalog (`optional-workflow-manifest`): every optional workflow appears with its catalog title and both slugs, no core workflow id appears, and `schemaVersion` is present. It also checks that Codex's four hook registrations share the root-aware, quoted, no-`eval`, fail-open invocation contract. It also verifies that every required phase reference exists and is cited by its owning `SKILL.md`, scans generated workflow entrypoint `SKILL.md` documents for stale phase-skill slugs and unresolved package-local reference tokens while allowing package-local reference prose to mention its own persisted-format history, asserts the shared non-SCE helper-composition rule and SCE-scoped workflow prohibitions on every generated workflow skill, asserts the exact cross-target `sce-decision` paths plus its required gate, status, immutability, handoff, and ADR-template content, permits `sce-decision` references only in `sce-next-task`, verifies the exact catalog-derived OpenCode skill permission order and Code-only OpenCode decision permission, asserts every explicit `sce-*` allow names an emitted OpenCode skill artifact, asserts the generated `sce-handover` `SKILL.md` covers both writer- and loader-mode content on all four targets, asserts the generated `sce-brownfield` `SKILL.md` still carries the bootstrap gate, documentation-discovery sweep, no-network rule, sub-`50` blocking threshold, always-disclosed contradiction contract, and additive-vs-`rebuild` write rule on all four targets, rejects two or more consecutive blank lines in generated workflow entrypoint `SKILL.md` documents (`no-blank-line-runs`), and rejects any generated `SKILL.md` that reproduces one of its sibling `references/output.md` fenced layouts verbatim (`output-dedup`, matched fence markers included), plus twenty semantic checks for layout-heading resolution, package-local path existence, forbidden validate/commit files, consolidated atomic-commit content, next-task report ownership, cross-target reference parity, stale synchronization wording, observational validation, OpenCode permission-artifact integrity, plan-review sync-debt-recovery reference wording (`plan-review-sync-debt-recovery`, asserting the generated `sce-next-task/references/plan-review.md` states both the sync-debt recovery and legacy-migration-failure behavior), the compact completed-task record model (four checks replacing the removed `handoff-identity-fields` persisted-handoff check: `compact-plan-template-schema`, asserting the generated plan-template's new-task and completion examples use the compact `Scope`/`Done when`/`Verify`/`Result`/`Files changed`/`Context impact`/`Context synchronization` fields and name none of the removed `Goal`/`Boundaries (in/out of scope)`/`Verification notes`/`Implementation evidence`/`Verification evidence`/`Context synchronization handoff` fields; `next-task-compact-completion-writing`, asserting `task-execution.md` records execution facts directly on the completed task with no separate handoff/evidence construction; `plan-review-reads-completed-record`, asserting `plan-review.md`'s sync-debt recovery reads the completed task record directly by plan path and task ID rather than a persisted handoff; and `context-sync-validates-task-record`, asserting `context-sync.md` validates the completed task record rather than a persisted handoff), the `/next-task` sync-debt-recovery branch's reference-before-invocation ordering (`sync-debt-recovery-branch`, asserting its citation of `references/context-sync.md` precedes any instruction to run the Task context synchronization phase), the synchronization-debt scan's all-completed-task scope (`plan-review-all-tasks-scope`, asserting `plan-review.md` covers every completed task with no surviving position-relative wording), the sync-debt-recovery branch's blocked-outcome layout routing (`sync-debt-blocked-routing`, asserting its `blocked` branch cites the **Context synchronization blocked** layout rather than **Review blocked**), and the `sce-validate` decision/plan-sync exclusion (`validate-decision-sync-boundary`, asserting no generated `sce-validate` document contains a `sce-decision` reference or plan-context-sync wording). It also asserts `structured-plan-template-schema`: every generated `sce-change-to-plan/references/plan-template.md` defines the `## Design decisions` section and its `Boundary`/`Rationale`/`Rejected`/`Trade-offs` fields, the `Decisions:`, `Scenarios:`, and `Covers:` reference fields, the `### Verification scenarios` section with `Given`/`When`/`Then`/`Check`, and the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections. It also asserts `plan-template-structure`, a test-only structural reference-integrity check implemented by `config/pkl/renderers/plan-structure.pkl` that reads Markdown only outside fenced code blocks and recognizes `D`/`AC`/`S`/`T` definitions only in their own sections: the generated template's top-level rule headings must exist outside fences, the fenced `## Template` skeleton is extracted as a sample document and must keep its plan headings in order with every required field on the right kind of definition, and the fenced `## Filled-in structured plan example` is extracted and must pass the full checks (unique IDs, resolved `Decisions:`/`Scenarios:`/`Covers:` references, `Scenarios:` on every acceptance criterion, every criterion covered, every scenario referenced). It also asserts `plan-init-scaffold`: the generated `config/plan-init-scaffold.md` passes skeleton mode (`skeletonDefects` in `plan-structure.pkl`), starts with `# Plan: {plan-name}` and carries that token exactly once, defines no `D` or `S` entry, contains no fenced block or template rule/example sections, and fails `check` with exactly ``Scenarios:` on `AC1` is not a comma-separated ID list``, ``Covers:` on `T01` is not a comma-separated ID list``, and ``AC1` is not covered by any task's `Covers:``. `config/pkl/renderers/plan-structure-check.pkl` applies the same checker to one plan file given as `-p planPath=…`; `check-generated.sh` runs it over `config/pkl/renderers/fixtures/plan-structure/` as directed by the shared manifest `expectations.tsv` (tab-separated fixture, `pass`/`fail` verdict, then expected defect messages; `#` rows are comments), which the Rust plan validator also consumes (see `context/cli/plan-service.md`): `pass` rows must pass with no listed messages, `fail` rows must fail with exactly the listed messages, and the run aborts on a missing fixture, an unknown verdict, or any `*.md` fixture not listed exactly once. The positive fixtures are minimal structured, inspection-only, and fenced example repeating active IDs; the negative fixtures are an unstructured plan snapshot, an unstructured plan with a fenced structured example, duplicate ID, undefined references, unreferenced scenario, uncovered criterion, missing `Scenarios:`, misplaced fields, misplaced definition, wrong heading context, keywords only inside a fenced example, task missing `Covers:`, task with empty `Covers:`, and a wrong reference kind (`Covers:` naming a scenario); three template fixtures mutate the canonical template so the token-only `structured-plan-template-schema` check still passes while `plan-template-structure` rejects it. `plan-template-policy-ownership` also keeps the new structured-plan rule sentences out of `plan-authoring.md` and requires it to cite the `Design decision rules`, `Verification scenario rules`, and `Reference rules` sections by name. It also asserts `plan-authoring-structured-references`: the generated `plan-authoring.md` states the decisions → criteria → scenarios → tasks authoring order, forbids manufactured decisions and invented test names, adds `Covers:` to tasks, runs a reference-consistency pass before writing, and runs `sce plan check {plan-path}` after writing the plan. It also asserts `plan-authoring-plan-init`: each of the four generated `plan-authoring.md` documents runs `sce plan init {plan_name}` for new plans only, after the clarification gate, with the exact exit-`0`/`Created plan:` line/existing-file success condition. It skips `init` for revisions, blocks on every other `init` or `check` result, and places the completeness gate and the `check` success marker before the `plan_ready` permission. It contains no `manual`, `checker is unavailable`, or legacy `plan` help wording. The check also rejects six negative fixtures derived in-contract from each real document (gate removed, gate moved after `plan_ready`, manual template fallback, `init` accepted on exit `0` alone, the old checker fallback, and `check` accepted on exit `0` alone), and it keeps `sce plan init` out of every `sce-next-task` and `sce-validate` document. It also asserts `next-task-structured-references`: the generated `sce-next-task` `plan-review.md` carries covered acceptance criteria, scenarios, and decisions in the `ready` result, treats dangling references as review risks; `output.md` carries the `## Acceptance coverage` gate section; and `task-execution.md` never checks an acceptance criterion or records a scenario status and keeps `Covers:` as authored. It also asserts `validate-scenario-evidence`: the generated `sce-validate/references/validation.md` lists the relationship-integrity defects that make validation `failed`, runs every scenario's `Check:` itself with exactly one of `Verified`, `Partially verified`, or `Not verified`, excludes task completion, recorded `Verify` outcomes, planned checks, and plan claims as evidence, meets an acceptance criterion only when its `Validate:` check passes and all its scenarios are `Verified`, and runs `sce plan check {plan-path}` before scenario verification with the checker trust rule and manual `Reference rules` fallback. Both `plan-authoring-structured-references` and `validate-scenario-evidence` also reject any generated `sce-next-task` document that mentions `sce plan check`. It also asserts `validation-report-scenario-section`: the generated `sce-validate/references/validation-report.md` and the report variants in `validation.md` carry a scenario-verification section mapping each scenario to its acceptance criteria, evidence kind, exact evidence, actual result, and status, cite scenario IDs on acceptance-criterion lines, and list unverified scenarios under **What failed**. Checked-in negative fixtures continue to prove the existing semantic contract failures, including `fixtures/structured-plan-template-schema-check.pkl`, `fixtures/plan-authoring-structured-references-check.pkl`, `fixtures/next-task-structured-references-check.pkl`, `fixtures/validate-scenario-evidence-check.pkl`, and `fixtures/validation-report-scenario-section-check.pkl`, while the Codex invocation assertion is exercised by the dedicated generated command check. - OpenCode, Claude, Pi, and Codex renderers expose flattened `{skill slug}/{package-relative path}` skill documents consumed by `config/pkl/generate.pkl` (OpenCode, Claude, and Pi also expose command documents; Codex exposes none); every target's flattened skill-document inventory contains `SKILL.md` and `references/output.md` for each workflow slug plus `sce-decision/SKILL.md` and `sce-decision/references/adr-template.md`. Codex's inventory additionally contains `{slug}/agents/openai.yaml` for each of the six catalog workflow slugs (not `sce-decision`). - `config/pkl/generate.pkl` emits deterministic `output.files` mappings for all authored generated targets: OpenCode's six workflow commands, four phase-based workflow packages with package-local phase and supporting references, and two phase-free workflow packages (handover also has its persisted-format template), standalone two-file decision package, and two thin routing agents; Claude's six thin commands, the same workflow-package inventories, and standalone decision package with no agents; Claude project settings and hook helper; shared bash-policy preset assets; OpenCode plugin entrypoints (`sce-bash-policy.ts` and `sce-agent-trace.ts`); generated OpenCode `opencode.json`; the Pi target tree (six thin workflow prompts, the same four phase-based packages with package-local references and two phase-free workflow packages, with handover's persisted-format template, the standalone two-file decision package, and the extension emitted verbatim from `config/lib/pi-plugin/sce-pi-extension.ts`); the Codex target tree under `config/.agents/skills/` (the same workflow-package inventories as Pi, bodies matching Pi's byte-for-byte except for the arguments-reference token and each skill's trailing `$sce-{slug}` invocation-example paragraph, plus a per-catalog-workflow `agents/openai.yaml` with no Pi/OpenCode/Claude equivalent, no commands, no agents, no settings/plugin manifest) plus its separate `.codex/hooks.json` and `.codex/hooks/run-sce-or-show-install-guidance.sh` hook-registration outputs; the generated `sce/config.json` schema artifact; the optional-workflow manifest at `config/optional-workflows.json`; and the plan init scaffold at `config/plan-init-scaffold.md`. The removed `config/automated/.opencode` profile has no generator ownership or output mappings. - Generated-file warning markers are not injected by the generator: Markdown outputs render deterministic frontmatter + body, and shared library outputs are emitted without a leading generated warning header. diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index 423cd2844..a9ec1d6d3 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -93,10 +93,37 @@ writes scenarios only for the case kinds (normal, boundary, failure, cancellation, concurrency, recovery) the change makes relevant, and a `Check:` never names a test that neither exists nor is created by a task. Before writing the plan, a reference-consistency pass applies every `Reference rules` -entry to the full draft and fixes each defect first. After writing the -plan, it runs `sce plan check {plan-path}`, fixes every reported diagnostic, and -re-runs the checker before returning `plan_ready`. The phase cites the template's rule sections by -name and does not restate them. +entry to the full draft and fixes each defect first. That pass is pre-write +guidance only and never substitutes for `sce plan check`. The phase cites the +template's rule sections by name and does not restate them. + +Step 2.10 writes the plan with no fallback, because structured plans are the +only supported format: + +- **New plan:** after the clarification gate, it runs `sce plan init {plan_name}` + (see [plan service](../cli/plan-service.md)). Initialization succeeds only on + exit `0`, a `Created plan: context/plans/{plan_name}.md` stdout line, and an + existing file. Every other result, including a missing `sce` or a binary + without `init`, returns `blocked` with the exit code and stderr. The agent + then fills the generated plan scaffold using `references/plan-template.md`. + The template never substitutes for `init`. +- **Existing plan:** it skips `init` and applies the `Updating an existing plan` + rules, which preserve completed tasks and their evidence. +- **Completeness gate (both paths):** no scaffold `TODO` placeholder remains, + and unused optional sections (`Design decisions`, `Assumptions`) are removed. + `sce plan check` is structural and does not detect placeholders, so the gate + is enforced here. +- **Structural check (both paths):** `sce plan check {plan-path}` passes only on + exit `0` with `structured plan: reference integrity holds`. On exit `3` + diagnostics the phase repairs the plan and reruns the check, or returns + `blocked`. Every other result returns `blocked`. + +`plan_ready` requires both the completeness gate and a passing `sce plan check`. +The `plan-authoring-plan-init` generation contract check pins these rules. It +rejects negative fixtures that drop or reorder the gate, add a manual template +or `Reference rules` fallback, or accept `init` or `check` without their exact +success output. It also keeps `sce plan init` out of every `sce-next-task` and +`sce-validate` document. Responsibility for structural consistency is split by surface. Generation checks (`nix run .#pkl-check-generated`, also run by `nix flake check`) verify the plan @@ -113,15 +140,16 @@ runnable with The two checkers share one rule set, wording, and fixture manifest; see the [plan structure contract](plan-structure-contract.md). -`/change-to-plan` (after writing) and `/validate` (step 4, before scenario -verification) run `sce plan check {plan-path}`; -`/validate` records each diagnostic as a failed check. Both trust a result only -when the checker actually ran: exit `0` with +`/change-to-plan` (step 2.10, as above) and `/validate` (step 4, before scenario +verification) run `sce plan check {plan-path}`. `/change-to-plan` has no +fallback when the checker is missing or returns anything else. `/validate` +records each diagnostic as a failed check, and it trusts a result only when the +checker actually ran: exit `0` with `structured plan: reference integrity holds` on stdout, or exit `3` -with diagnostics. Any other outcome, such as an older `sce` that answers the -unknown `plan` command with help and exit `0`, means the checker is unavailable, -and the workflow applies the manual `Reference rules` pass instead and records -the fallback. `/next-task` never invokes the checker. +with diagnostics. For `/validate` only, any other outcome, such as an older +`sce` that answers the unknown `plan` command with help and exit `0`, means the +checker is unavailable. The workflow then applies the manual `Reference rules` +pass instead and records the fallback. `/next-task` never invokes the checker. ## Continuation contract From 3bc73a54223dd95db5936f0b3dcbee565d58c348 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 17:47:29 +0200 Subject: [PATCH 23/61] runtime+workflow: Preserve plan publication outcomes and constrain prose Treat successful hard-link publication as the commit point so cleanup failures warn without masking created plans, while preserving original errors when publication fails. Thread stderr through plan commands and add deterministic failure-injection tests. Add controlled-English rules to plan authoring and context-sync workflows, and update the plan-service contract. Update plan `sce-plan-init` with AC8, S16, and completed T04 evidence. Co-authored-by: SCE --- cli/src/services/command_registry.rs | 2 +- cli/src/services/plan/command.rs | 8 +- cli/src/services/plan/init.rs | 71 +++++++--- cli/src/services/plan/mod.rs | 142 +++++++++++++++++++- config/pkl/base/controlled-english.pkl | 17 +++ config/pkl/base/workflow-change-to-plan.pkl | 8 ++ config/pkl/base/workflow-context-sync.pkl | 5 + context/cli/plan-service.md | 11 +- context/plans/sce-plan-init.md | 44 +++++- 9 files changed, 274 insertions(+), 34 deletions(-) create mode 100644 config/pkl/base/controlled-english.pkl diff --git a/cli/src/services/command_registry.rs b/cli/src/services/command_registry.rs index 3da1e2f8a..60d89e967 100644 --- a/cli/src/services/command_registry.rs +++ b/cli/src/services/command_registry.rs @@ -78,7 +78,7 @@ impl RuntimeCommand { Self::Version(command) => command.execute(context), Self::Completion(command) => Ok(command.execute(context)), Self::Sync(command) => command.execute_with_stderr(context, stderr).await, - Self::Plan(command) => command.execute(), + Self::Plan(command) => command.execute_with_stderr(stderr), } } } diff --git a/cli/src/services/plan/command.rs b/cli/src/services/plan/command.rs index 4c62511b1..76931f373 100644 --- a/cli/src/services/plan/command.rs +++ b/cli/src/services/plan/command.rs @@ -1,3 +1,4 @@ +use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::Context; @@ -30,13 +31,14 @@ pub struct PlanCommand { } impl PlanCommand { - pub fn execute(&self) -> Result { - self.execute_with(std::env::current_dir) + pub fn execute_with_stderr(&self, stderr: &mut impl Write) -> Result { + self.execute_with(std::env::current_dir, stderr) } pub(crate) fn execute_with( &self, current_dir: impl FnOnce() -> std::io::Result, + stderr: &mut impl Write, ) -> Result { match &self.request { PlanRequest::Check(request) => check(&request.path), @@ -45,7 +47,7 @@ impl PlanCommand { let start = current_dir() .context("Failed to read the current directory") .map_err(CliError::runtime)?; - create(&name, &start) + create(&name, &start, stderr) } } } diff --git a/cli/src/services/plan/init.rs b/cli/src/services/plan/init.rs index 507b1a279..1be6a3747 100644 --- a/cli/src/services/plan/init.rs +++ b/cli/src/services/plan/init.rs @@ -1,6 +1,6 @@ use std::fs::OpenOptions; use std::io::{ErrorKind, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{anyhow, Context}; @@ -58,7 +58,23 @@ fn invalid_name(value: &str, remedy: &str) -> CliError { )) } -pub fn create(name: &PlanName, start: &Path) -> Result { +pub fn create(name: &PlanName, start: &Path, stderr: &mut impl Write) -> Result { + create_with( + name, + start, + stderr, + |temporary, destination| std::fs::hard_link(temporary, destination), + |temporary| std::fs::remove_file(temporary), + ) +} + +pub(super) fn create_with( + name: &PlanName, + start: &Path, + stderr: &mut impl Write, + link: impl FnOnce(&Path, &Path) -> std::io::Result<()>, + remove: impl FnOnce(&Path) -> std::io::Result<()>, +) -> Result { let root = resolve_repository_root(start)?; let context_dir = Path::new(&root).join("context"); let plans_dir = Path::new(&root).join(PLANS_RELATIVE_DIR); @@ -68,7 +84,16 @@ pub fn create(name: &PlanName, start: &Path) -> Result { let file_name = format!("{}{PLAN_EXTENSION}", name.as_str()); let relative_path = format!("{PLANS_RELATIVE_DIR}/{file_name}"); let content = SCAFFOLD.replacen(SCAFFOLD_PLAN_NAME_TOKEN, name.as_str(), 1); - publish(&plans_dir, &file_name, &relative_path, &content)?; + if let Some(warning) = publish( + &plans_dir, + &file_name, + &relative_path, + &content, + link, + remove, + )? { + let _ = writeln!(stderr, "{warning}"); + } let absolute_path = format!("{root}/{relative_path}"); Ok([ @@ -137,13 +162,19 @@ fn not_a_directory(path: &Path) -> CliError { )) } +pub(super) fn temporary_path(plans_dir: &Path, file_name: &str) -> PathBuf { + plans_dir.join(format!(".{file_name}.{}.tmp", std::process::id())) +} + fn publish( plans_dir: &Path, file_name: &str, relative_path: &str, content: &str, -) -> Result<(), CliError> { - let temporary = plans_dir.join(format!(".{file_name}.{}.tmp", std::process::id())); + link: impl FnOnce(&Path, &Path) -> std::io::Result<()>, + remove: impl FnOnce(&Path) -> std::io::Result<()>, +) -> Result, CliError> { + let temporary = temporary_path(plans_dir, file_name); let destination = plans_dir.join(file_name); let mut file = OpenOptions::new() @@ -159,7 +190,7 @@ fn publish( drop(file); let published = written.map_err(CliError::runtime).and_then(|()| { - std::fs::hard_link(&temporary, &destination).map_err(|error| { + link(&temporary, &destination).map_err(|error| { if error.kind() == ErrorKind::AlreadyExists { CliError::validation(format!( "Plan already exists: {relative_path}. Try: choose another plan name; existing plans are never overwritten." @@ -172,16 +203,24 @@ fn publish( } }) }); - let removed = remove_temporary(&temporary); - - published?; - removed -} - -fn remove_temporary(temporary: &Path) -> Result<(), CliError> { - std::fs::remove_file(temporary) - .with_context(|| format!("Failed to remove temporary file '{}'", temporary.display())) - .map_err(CliError::runtime) + let removed = remove(&temporary); + + match (published, removed) { + (Ok(()), Ok(())) => Ok(None), + (Ok(()), Err(error)) => Ok(Some(format!( + "Warning: Created plan {relative_path}, but failed to remove temporary file '{}': {error}. Try: remove '{}' manually; the plan was created and does not need to be initialized again.", + temporary.display(), + temporary.display() + ))), + (Err(error), Ok(())) => Err(error), + (Err(error), Err(cleanup)) => Err(CliError::internal( + error.class(), + anyhow!( + "{error}\nCleanup also failed: could not remove temporary file '{}': {cleanup}. Try: remove it manually.", + temporary.display() + ), + )), + } } fn shell_quote(value: &str) -> String { diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 5a6686f85..585235454 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -198,6 +198,7 @@ struct CommandRun { #[cfg(test)] fn run_plan_args(args: &[&str], start: &std::path::Path) -> CommandRun { + let mut stderr = Vec::new(); let result = crate::services::parse::command_runtime::parse_runtime_command( std::iter::once("sce") .chain(args.iter().copied()) @@ -209,10 +210,17 @@ fn run_plan_args(args: &[&str], start: &std::path::Path) -> CommandRun { let crate::services::command_registry::RuntimeCommand::Plan(command) = command else { panic!("expected plan command"); }; - command.execute_with(|| Ok(start.to_path_buf())) + command.execute_with(|| Ok(start.to_path_buf()), &mut stderr) }); + render_plan_result(result, stderr) +} + +#[cfg(test)] +fn render_plan_result( + result: Result, + mut stderr: Vec, +) -> CommandRun { let mut stdout = Vec::new(); - let mut stderr = Vec::new(); let exit_code = crate::services::app_support::render_run_outcome( crate::services::app_support::RunOutcome { result, @@ -600,6 +608,136 @@ fn init_never_overwrites_existing_entry() { assert_eq!(sorted_entries(&plans), vec!["linked.md", "taken.md"]); } +#[cfg(test)] +fn run_init_with( + root: &std::path::Path, + name: &str, + link: impl FnOnce(&std::path::Path, &std::path::Path) -> std::io::Result<()>, + remove: impl FnOnce(&std::path::Path) -> std::io::Result<()>, +) -> CommandRun { + let mut stderr = Vec::new(); + let plan_name = init::PlanName::parse(name).expect("name should be valid"); + let result = init::create_with(&plan_name, root, &mut stderr, link, remove); + render_plan_result(result, stderr) +} + +#[cfg(test)] +fn injected(message: &str) -> std::io::Error { + std::io::Error::other(message.to_string()) +} + +#[test] +fn init_cleanup_failure_after_publication_warns_and_succeeds() { + let (_directory, root) = temp_repository(); + let plans = root.join("context/plans"); + let temporary = init::temporary_path(&plans, "published.md"); + + let run = run_init_with( + &root, + "published", + |temporary, destination| std::fs::hard_link(temporary, destination), + |_| Err(injected("injected cleanup failure")), + ); + + assert_eq!( + run.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + run.stderr + ); + assert_eq!(run.stdout, expected_init_stdout(&root, "published")); + assert_eq!( + run.stderr, + format!( + "Warning: Created plan context/plans/published.md, but failed to remove temporary file '{0}': injected cleanup failure. Try: remove '{0}' manually; the plan was created and does not need to be initialized again.\n", + temporary.display() + ) + ); + assert_eq!( + std::fs::read_to_string(plans.join("published.md")).expect("plan should exist"), + SCAFFOLD.replacen(SCAFFOLD_PLAN_NAME_TOKEN, "published", 1) + ); + assert!(temporary.is_file()); +} + +#[test] +fn init_publication_failure_removes_temporary_and_keeps_error() { + let (_directory, root) = temp_repository(); + let plans = root.join("context/plans"); + + let run = run_init_with( + &root, + "unpublished", + |_, _| Err(injected("injected link failure")), + |temporary| std::fs::remove_file(temporary), + ); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(4), + "{}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("SCE-ERR-RUNTIME"), "{}", run.stderr); + assert!(run.stderr.contains("Failed to publish"), "{}", run.stderr); + assert!( + run.stderr.contains("injected link failure"), + "{}", + run.stderr + ); + assert!( + !run.stderr.contains("Cleanup also failed"), + "{}", + run.stderr + ); + assert!(sorted_entries(&plans).is_empty()); +} + +#[test] +fn init_publication_and_cleanup_failure_keeps_original_error_and_destination() { + let (_directory, root) = temp_repository(); + let plans = root.join("context/plans"); + std::fs::create_dir_all(&plans).expect("plans directory should be created"); + std::fs::write(plans.join("taken.md"), b"known bytes\n").expect("taken should be written"); + let temporary = init::temporary_path(&plans, "taken.md"); + + let run = run_init_with( + &root, + "taken", + |temporary, destination| std::fs::hard_link(temporary, destination), + |_| Err(injected("injected cleanup failure")), + ); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("SCE-ERR-VALIDATION"), "{}", run.stderr); + assert!( + run.stderr + .contains("Plan already exists: context/plans/taken.md"), + "{}", + run.stderr + ); + assert!( + run.stderr.contains(&format!( + "Cleanup also failed: could not remove temporary file '{}': injected cleanup failure", + temporary.display() + )), + "{}", + run.stderr + ); + assert!(!run.stderr.contains("Warning:"), "{}", run.stderr); + assert_eq!( + std::fs::read(plans.join("taken.md")).expect("taken should be readable"), + b"known bytes\n" + ); +} + #[test] fn init_rejects_unsafe_names_without_writing() { let (_directory, root) = temp_repository(); diff --git a/config/pkl/base/controlled-english.pkl b/config/pkl/base/controlled-english.pkl new file mode 100644 index 000000000..6834a79a0 --- /dev/null +++ b/config/pkl/base/controlled-english.pkl @@ -0,0 +1,17 @@ +title = "Controlled English for Plans and Context" + +rules = """ +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. +""" + +section = """ +## \(title) + +\(rules) +""" diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 8f7f6562b..3baf839e1 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -1,4 +1,5 @@ import "workflow-content.pkl" as model +import "controlled-english.pkl" as controlledEnglish changeToPlanSkillBody = (argumentsReference: String, invocationExample: String) -> """ # SCE Change to Plan @@ -282,6 +283,11 @@ authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, then verification scenarios, then tasks. +Apply the **\(controlledEnglish.title)** rules +below to new or revised plan prose, including design decisions, acceptance +criteria, verification scenarios, and tasks, in initial authoring and in every +revision. + The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -529,6 +535,8 @@ Write user-facing questions and open questions directly and specifically. - Do not invent concerns, add praise or reassurance, or repeat the same concern in several forms. - When the user overrules a concern, record the decision and continue. Do not reintroduce it as a constraint, non-goal, or task. +\(controlledEnglish.section) + ## Plan authoring boundaries Do not: diff --git a/config/pkl/base/workflow-context-sync.pkl b/config/pkl/base/workflow-context-sync.pkl index 415636c20..4a497767f 100644 --- a/config/pkl/base/workflow-context-sync.pkl +++ b/config/pkl/base/workflow-context-sync.pkl @@ -1,4 +1,5 @@ import "workflow-content.pkl" as workflow +import "controlled-english.pkl" as controlledEnglish local taskExecution = workflow.semanticReference.apply("`sce-task-execution`", "the **Task execution phase**") local taskExecutionName = workflow.semanticReference.apply("sce-task-execution", "Task execution phase") @@ -1336,6 +1337,8 @@ When editing context: - Update cross-references when files are added, moved, renamed, or superseded. - Keep one authoritative statement for each durable fact. - Avoid copying the execution result verbatim into context files. +- Write new or modified prose under the **\(controlledEnglish.title)** + rules below. - Do not change application code, tests, or plan state. Create a new context file only when: @@ -1433,6 +1436,8 @@ Record only the Markdown report. Do not add explanatory prose before or after it Do not determine whether the plan is complete. The `/next-task` workflow owns that decision after context synchronization. +\(controlledEnglish.section) + ## Task context synchronization boundaries Do not: diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index ea4281c35..ffc2aee47 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -6,7 +6,7 @@ - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. - `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. -- `command.rs` owns the `plan` command. `PlanCommand` holds one `PlanRequest::{Check(PlanCheckRequest), Init(PlanInitRequest)}`, mapped from `PlanSubcommand` in `services/parse/command_runtime.rs` and dispatched by a static `match`. `execute()` passes `std::env::current_dir` to `execute_with`, which tests call with an explicit start directory instead of changing process state (see [Command contract](#command-contract) and [Init command contract](#init-command-contract)). +- `command.rs` owns the `plan` command. `PlanCommand` holds one `PlanRequest::{Check(PlanCheckRequest), Init(PlanInitRequest)}`, mapped from `PlanSubcommand` in `services/parse/command_runtime.rs` and dispatched by a static `match`. `execute_with_stderr(stderr)`, called from `RuntimeCommand::execute_with_stderr`, passes `std::env::current_dir` and the command stderr writer to `execute_with`, which tests call with an explicit start directory and a captured stderr buffer instead of changing process state (see [Command contract](#command-contract) and [Init command contract](#init-command-contract)). - `init.rs` owns `PlanName` validation and `create(&PlanName, start)`, the repository-root scaffold publication. - `mod.rs` embeds the generated plan init scaffold as the crate-visible `SCAFFOLD` constant (`include_str!` of `OUT_DIR/pkl-generated/config/plan-init-scaffold.md`) with its `SCAFFOLD_PLAN_NAME_TOKEN` (`{plan-name}`). `cli/build.rs` requires that staged path, so a payload or package fallback without the scaffold fails the build. The binary never renders the scaffold itself. @@ -67,10 +67,11 @@ - Name: one required positional `String`. `PlanName::parse` runs before any filesystem access, including reading the current directory: the value must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes (`PLAN_NAME_MAX_BYTES`), and not end in `.md`. Clap errors keep the existing `classify_clap_error` mapping: hyphen-prefixed values, unknown options, and extra positionals are parse errors (exit `2`); a missing `` is exit `3`. A value after `--` reaches name validation. - Root: `git rev-parse --show-toplevel` run with the start directory as its working directory. A root that is not UTF-8 or contains a newline is refused before any write. -- Directories: `context` and `context/plans` must be real directories (`symlink_metadata`, so symlinks are refused) and are created when absent. Directories created by a failed run are not rolled back. -- Publication: the content is written to a hidden `..md..tmp` file opened with `create_new` inside `context/plans/`, then published with `std::fs::hard_link`. The temporary file is removed after publication and on every handled error after its creation. The destination either does not exist or holds the complete scaffold; an existing entry, including a dangling symlink, is never replaced. There is no non-atomic fallback, no `fsync`, and no crash-recovery cleanup, so a killed run can leave an orphaned temporary file. +- Directories: `context` and `context/plans` must be real directories (`symlink_metadata`, so symlinks are refused) and are created when absent. Symlinked directories are refused even when a repository links `context/` elsewhere on purpose, because following the link would allow writes outside the repository root. Directories created by a failed run are not rolled back. +- Publication: the content is written to a hidden `..md..tmp` file opened with `create_new` inside `context/plans/`, then published with `std::fs::hard_link`. A successful hard link is the commit point: from then on the run succeeds. The temporary file is removed after publication and on every handled error after its creation. `init::create` delegates to `init::create_with`, which takes the link and remove operations as generic closures so tests inject failures deterministically. The destination either does not exist or holds the complete scaffold; an existing entry, including a dangling symlink, is never replaced. There is no non-atomic fallback, no `fsync`, and no crash-recovery cleanup, so a killed run can leave an orphaned temporary file. - Success: exit `0` and exactly three stdout lines: `Created plan: context/plans/.md`, `Next: complete the plan and run`, and `sce plan check '/context/plans/.md'`. The path is always wrapped in POSIX single quotes, each `'` written as `'\''`, so the printed command works from any directory. -- Failures: an invalid name or an existing destination (`Plan already exists: context/plans/.md`) is `CliError::validation` (exit `3`). A missing repository, a failed or missing `git`, an unprintable root, a non-directory `context` path, and any other filesystem failure are `CliError::runtime` (exit `4`). +- Cleanup warning: when the hard link succeeds but removing the temporary file fails, the run still exits `0` with the same three stdout lines, and writes one stderr line before them: `Warning: Created plan context/plans/.md, but failed to remove temporary file '': . Try: remove '' manually; the plan was created and does not need to be initialized again.` The published destination is never removed. +- Failures: an invalid name or an existing destination (`Plan already exists: context/plans/.md`) is `CliError::validation` (exit `3`). A missing repository, a failed or missing `git`, an unprintable root, a non-directory `context` path, and any other filesystem failure are `CliError::runtime` (exit `4`). When writing or publishing fails, the temporary file is removed and the original error and class are returned. When that removal also fails, the class is kept and the rendered message is the original error followed by a line `Cleanup also failed: could not remove temporary file '': . Try: remove it manually.` `plan` is a visible top-level command and appears in `sce --help` (with `sce plan check ` and `sce plan init ` lines under `Plan Usage:`) and in generated Bash/Zsh/Fish completions. @@ -92,4 +93,4 @@ The extractor follows CommonMark/GFM nesting. The Pkl checker requires fields at Module-level tests in `cli/src/services/plan/mod.rs` make the shared contract mechanical. `plan::fixture_expectations_match_manifest` runs every manifest row through `extract` and `validate` and requires that every fixture is listed. `plan::diagnostic_lines_match_source_positions` asserts exact diagnostic lines, and checks that a missing heading has no line. The `plan::command_*` tests drive `sce plan check` through the parser and stdout/stderr renderer for valid plans (by relative and absolute path), an invalid plan, an unstructured plan, a missing file, and a plan whose commands would create a marker file. `plan::plan_check_appears_in_help`, `plan::completion_scripts_include_plan_check`, `plan::plan_init_appears_in_help`, and `plan::completion_scripts_include_plan_init` cover help and completion. `plan::scaffold_untouched_fails_check` asserts the embedded scaffold has one `{plan-name}` token, defines no decision or scenario entries, and validates to exactly the three untouched-scaffold diagnostics. -The `plan::init_*` tests run `sce plan init` in temporary `git init` repositories through the parser, `execute_with`, and the renderer: creation at the root with exact stdout, file content, and `git status` (`init_creates_scaffold_at_repository_root`); a nested start directory under a root containing a space and an apostrophe, whose third stdout line is executed by `sh` and dispatched back to `plan check` (`init_from_nested_directory_targets_repository_root`); a regular file and a dangling symlink left untouched with no temporary file (`init_never_overwrites_existing_entry`); application-validation, Clap-parse, and missing-argument rejections with an unchanged tree (`init_rejects_unsafe_names_without_writing`); and exit `4` outside a repository (`init_outside_repository_is_runtime_failure`). `plan::scaffold_authored_passes_check` shows that filling only `Scenarios:` and `Covers:` makes `check` pass while `TODO` placeholders remain. +The `plan::init_*` tests run `sce plan init` in temporary `git init` repositories through the parser, `execute_with`, and the renderer: creation at the root with exact stdout, file content, and `git status` (`init_creates_scaffold_at_repository_root`); a nested start directory under a root containing a space and an apostrophe, whose third stdout line is executed by `sh` and dispatched back to `plan check` (`init_from_nested_directory_targets_repository_root`); a regular file and a dangling symlink left untouched with no temporary file (`init_never_overwrites_existing_entry`); application-validation, Clap-parse, and missing-argument rejections with an unchanged tree (`init_rejects_unsafe_names_without_writing`); and exit `4` outside a repository (`init_outside_repository_is_runtime_failure`). Three tests call `init::create_with` with injected operations: a cleanup failure after a real hard link exits `0` with the exact stdout, the exact warning line, the complete destination, and the temporary file left in place (`init_cleanup_failure_after_publication_warns_and_succeeds`); an injected link failure exits `4` with the original publish error and an empty `context/plans/` (`init_publication_failure_removes_temporary_and_keeps_error`); and a real conflict on an existing file with an injected cleanup failure exits `3` with `Plan already exists` plus the `Cleanup also failed` line, leaving the existing bytes unchanged (`init_publication_and_cleanup_failure_keeps_original_error_and_destination`). `plan::scaffold_authored_passes_check` shows that filling only `Scenarios:` and `Covers:` makes `check` pass while `TODO` placeholders remain. diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index 1ec72cf8a..f4b762082 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -48,20 +48,21 @@ change removes the existing manual checker fallback from step 2.10. - Rejected: A new top-level `sce init`-style command — it would split plan ownership. A trait or `dyn` operation object — the repository constraints rule these out, and two variants do not need them. - Trade-offs: `PlanCheckRequest` moves behind an enum, so the existing `plan::command_*` tests change their destructuring. - D4: The plan name is validated before any filesystem access, and the scaffold is published with a no-clobber hard link from a fully written temporary file. - - Boundary: `` is one required positional `String` argument, parsed under the existing Clap conventions with no extra parser settings. Every value Clap accepts, including an empty string and a value given after `--`, then goes through application validation: it must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes, and have no `.md` suffix. This grammar excludes path separators and traversal. Clap rejects hyphen-prefixed values such as `-leading`, unknown options, and extra positionals before validation runs. The repository root comes from `git rev-parse --show-toplevel`, run from an explicit start directory; the command passes the process current directory, so tests never change process-global state. A root that is not valid UTF-8 or that contains a newline is refused before any write, because it cannot be printed as a single shell-quoted line. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is fully written to a hidden `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`. The temporary file is then removed, both after a successful publication and on every handled error path that follows its creation. - - Guarantee: The invariant is that the destination is published atomically and never exposes partial scaffold content. The destination path either does not exist or holds the complete scaffold. An existing entry at the destination is never replaced or modified. A handled error removes the temporary file and never leaves a partially written destination. Crash recovery does not guarantee temporary-file cleanup. + - Boundary: `` is one required positional `String` argument, parsed under the existing Clap conventions with no extra parser settings. Every value Clap accepts, including an empty string and a value given after `--`, then goes through application validation: it must match `[a-z0-9]+(-[a-z0-9]+)*`, be at most 64 bytes, and have no `.md` suffix. This grammar excludes path separators and traversal. Clap rejects hyphen-prefixed values such as `-leading`, unknown options, and extra positionals before validation runs. The repository root comes from `git rev-parse --show-toplevel`, run from an explicit start directory; the command passes the process current directory, so tests never change process-global state. A root that is not valid UTF-8 or that contains a newline is refused before any write, because it cannot be printed as a single shell-quoted line. `context` and `context/plans` must be real directories when they exist; they are created when absent. The content is fully written to a hidden `create_new` temporary file inside `context/plans/`, then `std::fs::hard_link` publishes it to `.md`. A successful hard link is the commit point. The temporary file is then removed, both after a successful publication and on every handled error path that follows its creation. A removal failure after a successful link is a nonfatal stderr warning; a removal failure after a failed write or link is appended to the original error. + - Guarantee: The invariant is that the destination is published atomically and never exposes partial scaffold content. The destination path either does not exist or holds the complete scaffold. An existing entry at the destination is never replaced or modified. A handled error attempts to remove the temporary file and never leaves a partially written destination. The published destination is never removed. Crash recovery does not guarantee temporary-file cleanup. - Publication lifecycle: The state a run can leave depends on where it stops. - Before the hard link publishes the destination: a crash or kill can leave an orphaned temporary file, but this run has not created the destination. - After the hard link and before the temporary file is removed: a crash or kill can leave both the complete destination file and the temporary file. - After the temporary file is removed: only the complete destination remains. - - On a handled error: the temporary file is removed, an existing destination is never overwritten, and no partially written destination is left. + - After the hard link when removing the temporary file fails: the run succeeds with exit `0`, the D5 stdout, and a stderr warning naming the temporary file, which remains for manual removal. + - On a handled error: the temporary file is removed, an existing destination is never overwritten, and no partially written destination is left. When that removal also fails, the original error and class are returned with the cleanup failure appended, and the temporary file can remain. - On a filesystem or publication failure: directories that the run newly created may remain, empty unless other content already existed. - Limits: This is not a filesystem-wide transaction, and it promises no durability across power loss. `context/` and `context/plans/` created by a run stay in place when a later step fails. Temporary-file cleanup after a process crash or kill is not promised, so an orphaned temporary file can remain beside no destination or beside a complete one. - Rationale: `link` fails on any existing entry, including a dangling or conflicting symlink, so a plan is never overwritten. The destination appears only through the hard link to a fully written temporary file, so no observer and no crash ever sees a half-written plan that later blocks `init`. Empty directories and orphaned temporary files are harmless to correctness, so tolerating them avoids rollback and crash-recovery logic. - Rejected: `rename` — it silently replaces an existing destination. Writing directly with `create_new` — an interrupted write leaves a partial plan at the final path. Falling back to that non-atomic write when hard links are unsupported — it reintroduces partial plans. Rolling back created directories on failure — it adds removal logic that can race with concurrent writers in order to remove harmless empty directories. Accepting a path argument — that would invite traversal and repository escape. Setting `allow_hyphen_values` or similar so that `-leading` reaches name validation — it departs from the CLI's Clap conventions only to change which error class a name that is invalid anyway receives. - - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused. A failed run can leave newly created empty directories. A crashed run can leave an orphaned temporary file, either alone or beside a complete destination; when the destination exists, rerunning `init` with the same name reports the existing destination, and the orphaned file must be removed by hand. + - Trade-offs: This requires a filesystem that supports hard links inside `context/plans/`. On one that does not, `init` fails with a runtime error and has no non-atomic fallback. Symlinked `context` or `context/plans` directories are refused, even in a repository that deliberately symlinks `context/` elsewhere, because following the link would allow writes outside the repository root. This is resolved and implemented in T02; it is no longer an open question. A failed run can leave newly created empty directories. A crashed run can leave an orphaned temporary file, either alone or beside a complete destination; when the destination exists, rerunning `init` with the same name reports the existing destination, and the orphaned file must be removed by hand. - D5: `init` reports results through the existing `CliError` classes and prints exactly three lines on stdout. - - Boundary: On success, the exit code is `0` and stdout is exactly three lines: `Created plan: context/plans/.md`, then `Next: complete the plan and run`, then `sce plan check `. `` is the absolute path `/context/plans/.md`, always wrapped in POSIX single quotes, with each `'` in it written as `'\''`. An invalid name that reaches application validation, or an existing destination, is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, an unprintable root, or a filesystem failure is `CliError::runtime` (exit `4`). Clap errors keep the existing `classify_clap_error` mapping unchanged: unknown options, hyphen-prefixed values, and extra arguments are parse errors (exit `2`), and the missing `` argument is `MissingRequiredArgument`, which that mapping already reports as `SCE-ERR-VALIDATION` (exit `3`), as `sce plan check` does today. + - Boundary: On success, the exit code is `0` and stdout is exactly three lines: `Created plan: context/plans/.md`, then `Next: complete the plan and run`, then `sce plan check `. `` is the absolute path `/context/plans/.md`, always wrapped in POSIX single quotes, with each `'` in it written as `'\''`. When the hard link succeeds but temporary-file removal fails, the exit code and stdout are unchanged, and stderr carries one `Warning: Created plan context/plans/.md, but failed to remove temporary file '': ...` line that asks for manual removal and states that the plan does not need to be initialized again. An invalid name that reaches application validation, or an existing destination, is `CliError::validation` (exit `3`). Running outside a Git repository, a failed or missing `git`, an unprintable root, or a filesystem failure is `CliError::runtime` (exit `4`). Clap errors keep the existing `classify_clap_error` mapping unchanged: unknown options, hyphen-prefixed values, and extra arguments are parse errors (exit `2`), and the missing `` argument is `MissingRequiredArgument`, which that mapping already reports as `SCE-ERR-VALIDATION` (exit `3`), as `sce plan check` does today. - Side effects by failure class: Clap errors and application name-validation errors happen before any filesystem access, so they create no directory or file and modify no existing file. A missing repository, a failed or missing `git`, and an unprintable root are detected before any write, so they also leave the filesystem unchanged. An existing destination is never overwritten or modified, and no temporary file remains. A handled filesystem or publication failure never publishes a partial destination file and removes its temporary file, but directories that the run created may remain, as D4 states. These side effects cover handled errors only; after a process crash or kill, the D4 publication lifecycle applies, and an orphaned temporary file can remain. - Rationale: Automation and the workflow can branch on the class without parsing text. The `Created plan:` path is always repository-relative, whichever directory `init` ran from, so the workflow can match it. `sce plan check` keeps resolving relative paths against the current directory, so the printed command uses the absolute path and works unchanged from any directory. Always quoting keeps the output deterministic and safe for roots containing spaces, apostrophes, or other shell metacharacters. - Rejected: A dedicated new exit code or error class — the existing taxonomy already covers these failures. Printing a repository-relative `check` path — it fails with a missing-file runtime error when `init` runs from a nested directory. Making `check` resolve paths against the repository root — it would change existing `check` semantics. Quoting only when needed — it adds a second output shape for no gain. @@ -111,6 +112,10 @@ performs final validation. - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` - Decisions: D3 - Scenarios: S12 +- [x] AC8: A successful hard link is the commit point. When removing the temporary file then fails, `init` exits `0` with the D5 stdout, leaves the complete destination in place, and writes a stderr warning that names the temporary file. When writing or publishing fails, the original error and exit class are kept, the temporary file is removed, and a failed removal is reported as additional context. An existing destination is never overwritten. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` + - Decisions: D4, D5 + - Scenarios: S16 ### Verification scenarios @@ -189,6 +194,11 @@ performs final validation. - When: The generation contract checks run. - Then: The generated text requires a `check` exit `0` with that success marker before `plan_ready`, for new plans and revisions alike. It repairs the plan and reruns `check` on structural diagnostics. It returns `blocked` when `check` is unavailable or returns any other result. It states that the step 2.9 reference-consistency pass never substitutes for `sce plan check`, and it contains neither `the checker is unavailable;` nor any manual validation fallback. Both negative fixtures fail the assertion. - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-plan-init` contract assertion and its negative fixtures created by T03. +- S16: Publication is the commit point, and cleanup failures never hide or invent an outcome + - Given: Temporary Git repositories, with `init::create_with` called using deterministic injected link and remove operations. + - When: (a) A real hard link succeeds and the injected removal fails. (b) The injected link fails and the real removal succeeds. (c) The destination already exists as a regular file with known bytes, the real hard link fails with `AlreadyExists`, and the injected removal fails. + - Then: (a) Exit `0`, the exact D5 stdout, the exact cleanup warning on stderr naming the temporary file, the complete scaffold at the destination, and the temporary file still present. (b) Exit `4` with the original `Failed to publish` error, no `Cleanup also failed` line, and an empty `context/plans/`. (c) Exit `3` with `Plan already exists: context/plans/taken.md` followed by the `Cleanup also failed` line naming the temporary file, no warning, and `taken.md` keeping its bytes. + - Check: `plan::init_cleanup_failure_after_publication_warns_and_succeeds`, `plan::init_publication_failure_removes_temporary_and_keeps_error`, and `plan::init_publication_and_cleanup_failure_keeps_original_error_and_destination`, created by T04. ### Full validation @@ -319,9 +329,28 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: `context/sce/shared-context-plan-workflow.md` (updated in this task: step 2.10 init, completeness, and check gates, and the change-to-plan versus validate checker split); `context/sce/config-generation.md` (the `plan-authoring-plan-init` contract check). - Context synchronization: synced +- [x] T04: `Treat hard-link publication as the init commit point and resolve the symlink question` (status:done) + - Task ID: T04 + - Covers: AC8 + - Scope: In: + - `publish` in `cli/src/services/plan/init.rs`: a successful hard link is the commit point; a later cleanup failure becomes a stderr warning with exit `0`, and a cleanup failure after a failed write or link is appended to the original error with its class kept. + - `init::create_with` with generic link and remove closures for deterministic failure injection, and stderr threaded through `PlanCommand::execute_with_stderr` from `RuntimeCommand::execute_with_stderr`, as `sync` already does. + - The three S16 tests in `cli/src/services/plan/mod.rs`, and `run_plan_args` capturing command-written stderr. + - D4 and D5 wording, the resolved symlinked-directory decision, and `context/cli/plan-service.md`. + + Out: changes to `sce plan check`, the workflow, Pkl sources, directory rollback, crash recovery, and `fsync`. + - Dependencies: T02 + - Done when: A cleanup failure after a successful hard link exits `0` with the D5 stdout and an actionable stderr warning. Write and publication failures keep their original error and class, and report a failed cleanup as extra context. Existing destinations are never overwritten. The open question is resolved in D4. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` — passed (28 tests, including the three new S16 tests); `nix develop -c ./scripts/run-cli-cargo.sh clippy --manifest-path cli/Cargo.toml --all-targets --all-features -- -D warnings` — clean; `nix develop -c sh -c 'cd cli && cargo fmt'` — applied; `nix flake check` — passed (`cli-tests`, `cli-clippy`, `cli-fmt`); `sce plan check context/plans/sce-plan-init.md` — passed. + - Completed: 2026-10-24 + - Files changed: `cli/src/services/command_registry.rs`, `cli/src/services/plan/command.rs`, `cli/src/services/plan/init.rs`, `cli/src/services/plan/mod.rs`, `context/cli/plan-service.md`, `context/plans/sce-plan-init.md` + - Result: `publish` now matches on the publication and cleanup results: both succeed returns no warning; publication succeeds and cleanup fails returns the `Warning: Created plan ...` line, which `create_with` writes to stderr before the unchanged stdout; publication fails returns the original `CliError`; both fail returns `CliError::internal` with the original class and the original message followed by a `Cleanup also failed:` line. `PlanCommand::execute` became `execute_with_stderr`. The symlinked-directory open question is recorded as resolved in D4 (refused, because following the link would allow writes outside the repository root) and Open questions is `None.`. + - Context impact: `context/cli/plan-service.md` (publication commit point, cleanup warning, failure behavior, `create_with`, `execute_with_stderr`, and the S16 tests). + - Context synchronization: synced + ## Open questions -- Should `init` refuse a symlinked `context/` or `context/plans/` directory, as D4 plans to? A repository that deliberately symlinks `context/` elsewhere would be blocked. Following the link instead would allow writes outside the repository root. +None. ## Validation Report @@ -370,7 +399,8 @@ Persist this field in every plan; this is durable plan state, not chat state: ### Failed checks and follow-ups -- None. +- None at validation time. +- Post-validation review follow-up (PR #342), recorded as T04 without changing the results above: temporary-file cleanup after a successful hard link no longer fails `init` (AC8, S16), and the resolved symlinked-directory question was removed from Open questions and recorded in D4. ### Residual risks From 98365ff3432125f310504d5b1ca0c530f546862e Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 17:49:26 +0200 Subject: [PATCH 24/61] skills: Enforce controlled English in plan guidance Keep plan and context prose consistent across agent workflows. Apply the new writing rules to authoring and synchronization references for all supported agent targets. --- .../references/plan-authoring.md | 79 ++++++++++++++++--- .../sce-next-task/references/context-sync.md | 12 +++ .../references/plan-authoring.md | 79 ++++++++++++++++--- .../sce-next-task/references/context-sync.md | 12 +++ .../references/plan-authoring.md | 79 ++++++++++++++++--- .../sce-next-task/references/context-sync.md | 12 +++ .../references/plan-authoring.md | 79 ++++++++++++++++--- .../sce-next-task/references/context-sync.md | 12 +++ 8 files changed, 320 insertions(+), 44 deletions(-) diff --git a/.agents/skills/sce-change-to-plan/references/plan-authoring.md b/.agents/skills/sce-change-to-plan/references/plan-authoring.md index 3bd556c00..d0dfaebb2 100644 --- a/.agents/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.agents/skills/sce-change-to-plan/references/plan-authoring.md @@ -36,6 +36,11 @@ authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, then verification scenarios, then tasks. +Apply the **Controlled English for Plans and Context** rules +below to new or revised plan prose, including design decisions, acceptance +criteria, verification scenarios, and tasks, in initial authoring and in every +revision. + The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -61,7 +66,9 @@ When multiple existing plans match and none can be selected safely, return `blocked` with the matching candidates. When the request targets a new plan, derive `plan_name` as a short kebab-case -slug of the change, and confirm it does not collide with an existing plan. +slug of the change, and confirm it does not collide with an existing plan. Do +not create the plan file here; step 2.10 initializes it after the clarification +gate. Resolve exactly one plan target per invocation. @@ -202,20 +209,60 @@ complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. +This pass is pre-write authoring guidance only. It never substitutes for a +successful `sce plan check` in step 2.10. + ## 2.10 Write the plan -Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` -exactly. For revisions, apply its `Updating an existing plan` rules. +Structured plans are the only supported format. This step has no fallback for +`sce plan init` or `sce plan check`. + +### New plan + +Run `sce plan init {plan_name}`. Initialization succeeds only when all three +hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, +and that file exists. Every other result returns `blocked` with the exit code +and stderr. This includes a missing `sce` executable, a binary without `init`, +a nonzero exit, an exit `0` without the expected line, and an expected line +with no created file. + +Fill the generated scaffold with the plan content, applying the authoring rules +in `references/plan-template.md`. The template is the authoring reference for +filling the scaffold, never a substitute for `sce plan init`. Never write a new +plan without it. + +### Existing plan + +Do not run `sce plan init`. Revise the plan by applying the +`Updating an existing plan` rules in `references/plan-template.md`, which +preserve completed tasks and their evidence. The revised plan must satisfy the +same structured plan contract. + +### Completeness gate + +For new plans and revisions alike, the plan contains no scaffold `TODO` +placeholder, and optional sections without real entries (`Design decisions`, +`Assumptions`) are removed. The change summary, acceptance criteria, design +decisions, verification scenarios, task definitions, and verify instructions +are authored for this change. -After writing the plan, run `sce plan check {plan-path}`. When it reports -diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. +While any placeholder remains, keep authoring or return `blocked`. A passing +`sce plan check` is required but not sufficient: it is structural and does not +detect placeholders. -Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` on stdout, or on exit `3` -with diagnostics. Any other outcome means the checker is unavailable; apply the -manual `Reference rules` pass from step 2.9 to the written plan instead, and -record in the open questions that the checker was unavailable. +### Structural check + +For new plans and revisions alike, run `sce plan check {plan-path}`. It passes +only on exit `0` with `structured plan: reference integrity holds` on stdout. + +On exit `3` with structural diagnostics, fix each reported defect in the plan +and run it again, or return `blocked` with the remaining diagnostics when they +cannot be resolved. Every other result returns `blocked` with the exit code and +stderr. This includes a missing `sce` executable, a binary without `check`, any +other exit code, and an exit `0` without the success marker. + +Set internal status `plan_ready` only after both the completeness gate and +`sce plan check` pass. ## 2.11 Return the result @@ -241,6 +288,16 @@ Write user-facing questions and open questions directly and specifically. - Do not invent concerns, add praise or reassurance, or repeat the same concern in several forms. - When the user overrules a concern, record the decision and continue. Do not reintroduce it as a constraint, non-goal, or task. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Plan authoring boundaries Do not: diff --git a/.agents/skills/sce-next-task/references/context-sync.md b/.agents/skills/sce-next-task/references/context-sync.md index 807b83ffa..86cd8a166 100644 --- a/.agents/skills/sce-next-task/references/context-sync.md +++ b/.agents/skills/sce-next-task/references/context-sync.md @@ -214,6 +214,8 @@ When editing context: - Update cross-references when files are added, moved, renamed, or superseded. - Keep one authoritative statement for each durable fact. - Avoid copying the execution result verbatim into context files. +- Write new or modified prose under the **Controlled English for Plans and Context** + rules below. - Do not change application code, tests, or plan state. Create a new context file only when: @@ -311,6 +313,16 @@ Record only the Markdown report. Do not add explanatory prose before or after it Do not determine whether the plan is complete. The `/next-task` workflow owns that decision after context synchronization. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Task context synchronization boundaries Do not: diff --git a/.claude/skills/sce-change-to-plan/references/plan-authoring.md b/.claude/skills/sce-change-to-plan/references/plan-authoring.md index 3bd556c00..d0dfaebb2 100644 --- a/.claude/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.claude/skills/sce-change-to-plan/references/plan-authoring.md @@ -36,6 +36,11 @@ authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, then verification scenarios, then tasks. +Apply the **Controlled English for Plans and Context** rules +below to new or revised plan prose, including design decisions, acceptance +criteria, verification scenarios, and tasks, in initial authoring and in every +revision. + The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -61,7 +66,9 @@ When multiple existing plans match and none can be selected safely, return `blocked` with the matching candidates. When the request targets a new plan, derive `plan_name` as a short kebab-case -slug of the change, and confirm it does not collide with an existing plan. +slug of the change, and confirm it does not collide with an existing plan. Do +not create the plan file here; step 2.10 initializes it after the clarification +gate. Resolve exactly one plan target per invocation. @@ -202,20 +209,60 @@ complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. +This pass is pre-write authoring guidance only. It never substitutes for a +successful `sce plan check` in step 2.10. + ## 2.10 Write the plan -Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` -exactly. For revisions, apply its `Updating an existing plan` rules. +Structured plans are the only supported format. This step has no fallback for +`sce plan init` or `sce plan check`. + +### New plan + +Run `sce plan init {plan_name}`. Initialization succeeds only when all three +hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, +and that file exists. Every other result returns `blocked` with the exit code +and stderr. This includes a missing `sce` executable, a binary without `init`, +a nonzero exit, an exit `0` without the expected line, and an expected line +with no created file. + +Fill the generated scaffold with the plan content, applying the authoring rules +in `references/plan-template.md`. The template is the authoring reference for +filling the scaffold, never a substitute for `sce plan init`. Never write a new +plan without it. + +### Existing plan + +Do not run `sce plan init`. Revise the plan by applying the +`Updating an existing plan` rules in `references/plan-template.md`, which +preserve completed tasks and their evidence. The revised plan must satisfy the +same structured plan contract. + +### Completeness gate + +For new plans and revisions alike, the plan contains no scaffold `TODO` +placeholder, and optional sections without real entries (`Design decisions`, +`Assumptions`) are removed. The change summary, acceptance criteria, design +decisions, verification scenarios, task definitions, and verify instructions +are authored for this change. -After writing the plan, run `sce plan check {plan-path}`. When it reports -diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. +While any placeholder remains, keep authoring or return `blocked`. A passing +`sce plan check` is required but not sufficient: it is structural and does not +detect placeholders. -Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` on stdout, or on exit `3` -with diagnostics. Any other outcome means the checker is unavailable; apply the -manual `Reference rules` pass from step 2.9 to the written plan instead, and -record in the open questions that the checker was unavailable. +### Structural check + +For new plans and revisions alike, run `sce plan check {plan-path}`. It passes +only on exit `0` with `structured plan: reference integrity holds` on stdout. + +On exit `3` with structural diagnostics, fix each reported defect in the plan +and run it again, or return `blocked` with the remaining diagnostics when they +cannot be resolved. Every other result returns `blocked` with the exit code and +stderr. This includes a missing `sce` executable, a binary without `check`, any +other exit code, and an exit `0` without the success marker. + +Set internal status `plan_ready` only after both the completeness gate and +`sce plan check` pass. ## 2.11 Return the result @@ -241,6 +288,16 @@ Write user-facing questions and open questions directly and specifically. - Do not invent concerns, add praise or reassurance, or repeat the same concern in several forms. - When the user overrules a concern, record the decision and continue. Do not reintroduce it as a constraint, non-goal, or task. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Plan authoring boundaries Do not: diff --git a/.claude/skills/sce-next-task/references/context-sync.md b/.claude/skills/sce-next-task/references/context-sync.md index 807b83ffa..86cd8a166 100644 --- a/.claude/skills/sce-next-task/references/context-sync.md +++ b/.claude/skills/sce-next-task/references/context-sync.md @@ -214,6 +214,8 @@ When editing context: - Update cross-references when files are added, moved, renamed, or superseded. - Keep one authoritative statement for each durable fact. - Avoid copying the execution result verbatim into context files. +- Write new or modified prose under the **Controlled English for Plans and Context** + rules below. - Do not change application code, tests, or plan state. Create a new context file only when: @@ -311,6 +313,16 @@ Record only the Markdown report. Do not add explanatory prose before or after it Do not determine whether the plan is complete. The `/next-task` workflow owns that decision after context synchronization. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Task context synchronization boundaries Do not: diff --git a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md index 3bd556c00..d0dfaebb2 100644 --- a/.opencode/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.opencode/skills/sce-change-to-plan/references/plan-authoring.md @@ -36,6 +36,11 @@ authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, then verification scenarios, then tasks. +Apply the **Controlled English for Plans and Context** rules +below to new or revised plan prose, including design decisions, acceptance +criteria, verification scenarios, and tasks, in initial authoring and in every +revision. + The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -61,7 +66,9 @@ When multiple existing plans match and none can be selected safely, return `blocked` with the matching candidates. When the request targets a new plan, derive `plan_name` as a short kebab-case -slug of the change, and confirm it does not collide with an existing plan. +slug of the change, and confirm it does not collide with an existing plan. Do +not create the plan file here; step 2.10 initializes it after the clarification +gate. Resolve exactly one plan target per invocation. @@ -202,20 +209,60 @@ complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. +This pass is pre-write authoring guidance only. It never substitutes for a +successful `sce plan check` in step 2.10. + ## 2.10 Write the plan -Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` -exactly. For revisions, apply its `Updating an existing plan` rules. +Structured plans are the only supported format. This step has no fallback for +`sce plan init` or `sce plan check`. + +### New plan + +Run `sce plan init {plan_name}`. Initialization succeeds only when all three +hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, +and that file exists. Every other result returns `blocked` with the exit code +and stderr. This includes a missing `sce` executable, a binary without `init`, +a nonzero exit, an exit `0` without the expected line, and an expected line +with no created file. + +Fill the generated scaffold with the plan content, applying the authoring rules +in `references/plan-template.md`. The template is the authoring reference for +filling the scaffold, never a substitute for `sce plan init`. Never write a new +plan without it. + +### Existing plan + +Do not run `sce plan init`. Revise the plan by applying the +`Updating an existing plan` rules in `references/plan-template.md`, which +preserve completed tasks and their evidence. The revised plan must satisfy the +same structured plan contract. + +### Completeness gate + +For new plans and revisions alike, the plan contains no scaffold `TODO` +placeholder, and optional sections without real entries (`Design decisions`, +`Assumptions`) are removed. The change summary, acceptance criteria, design +decisions, verification scenarios, task definitions, and verify instructions +are authored for this change. -After writing the plan, run `sce plan check {plan-path}`. When it reports -diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. +While any placeholder remains, keep authoring or return `blocked`. A passing +`sce plan check` is required but not sufficient: it is structural and does not +detect placeholders. -Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` on stdout, or on exit `3` -with diagnostics. Any other outcome means the checker is unavailable; apply the -manual `Reference rules` pass from step 2.9 to the written plan instead, and -record in the open questions that the checker was unavailable. +### Structural check + +For new plans and revisions alike, run `sce plan check {plan-path}`. It passes +only on exit `0` with `structured plan: reference integrity holds` on stdout. + +On exit `3` with structural diagnostics, fix each reported defect in the plan +and run it again, or return `blocked` with the remaining diagnostics when they +cannot be resolved. Every other result returns `blocked` with the exit code and +stderr. This includes a missing `sce` executable, a binary without `check`, any +other exit code, and an exit `0` without the success marker. + +Set internal status `plan_ready` only after both the completeness gate and +`sce plan check` pass. ## 2.11 Return the result @@ -241,6 +288,16 @@ Write user-facing questions and open questions directly and specifically. - Do not invent concerns, add praise or reassurance, or repeat the same concern in several forms. - When the user overrules a concern, record the decision and continue. Do not reintroduce it as a constraint, non-goal, or task. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Plan authoring boundaries Do not: diff --git a/.opencode/skills/sce-next-task/references/context-sync.md b/.opencode/skills/sce-next-task/references/context-sync.md index 807b83ffa..86cd8a166 100644 --- a/.opencode/skills/sce-next-task/references/context-sync.md +++ b/.opencode/skills/sce-next-task/references/context-sync.md @@ -214,6 +214,8 @@ When editing context: - Update cross-references when files are added, moved, renamed, or superseded. - Keep one authoritative statement for each durable fact. - Avoid copying the execution result verbatim into context files. +- Write new or modified prose under the **Controlled English for Plans and Context** + rules below. - Do not change application code, tests, or plan state. Create a new context file only when: @@ -311,6 +313,16 @@ Record only the Markdown report. Do not add explanatory prose before or after it Do not determine whether the plan is complete. The `/next-task` workflow owns that decision after context synchronization. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Task context synchronization boundaries Do not: diff --git a/.pi/skills/sce-change-to-plan/references/plan-authoring.md b/.pi/skills/sce-change-to-plan/references/plan-authoring.md index 3bd556c00..d0dfaebb2 100644 --- a/.pi/skills/sce-change-to-plan/references/plan-authoring.md +++ b/.pi/skills/sce-change-to-plan/references/plan-authoring.md @@ -36,6 +36,11 @@ authoring or revising. Apply those rules rather than restating them here. Author plan content in this order: design decisions, then acceptance criteria, then verification scenarios, then tasks. +Apply the **Controlled English for Plans and Context** rules +below to new or revised plan prose, including design decisions, acceptance +criteria, verification scenarios, and tasks, in initial authoring and in every +revision. + The workflow renders this phase's result as the summary defined in `references/output.md`. @@ -61,7 +66,9 @@ When multiple existing plans match and none can be selected safely, return `blocked` with the matching candidates. When the request targets a new plan, derive `plan_name` as a short kebab-case -slug of the change, and confirm it does not collide with an existing plan. +slug of the change, and confirm it does not collide with an existing plan. Do +not create the plan file here; step 2.10 initializes it after the clarification +gate. Resolve exactly one plan target per invocation. @@ -202,20 +209,60 @@ complete draft that applies every `Reference rules` entry in `references/plan-template.md`. Fix each defect in the draft before writing. Do not write a plan that fails this pass. +This pass is pre-write authoring guidance only. It never substitutes for a +successful `sce plan check` in step 2.10. + ## 2.10 Write the plan -Write `context/plans/{plan_name}.md` by applying `references/plan-template.md` -exactly. For revisions, apply its `Updating an existing plan` rules. +Structured plans are the only supported format. This step has no fallback for +`sce plan init` or `sce plan check`. + +### New plan + +Run `sce plan init {plan_name}`. Initialization succeeds only when all three +hold: exit `0`, stdout contains `Created plan: context/plans/{plan_name}.md`, +and that file exists. Every other result returns `blocked` with the exit code +and stderr. This includes a missing `sce` executable, a binary without `init`, +a nonzero exit, an exit `0` without the expected line, and an expected line +with no created file. + +Fill the generated scaffold with the plan content, applying the authoring rules +in `references/plan-template.md`. The template is the authoring reference for +filling the scaffold, never a substitute for `sce plan init`. Never write a new +plan without it. + +### Existing plan + +Do not run `sce plan init`. Revise the plan by applying the +`Updating an existing plan` rules in `references/plan-template.md`, which +preserve completed tasks and their evidence. The revised plan must satisfy the +same structured plan contract. + +### Completeness gate + +For new plans and revisions alike, the plan contains no scaffold `TODO` +placeholder, and optional sections without real entries (`Design decisions`, +`Assumptions`) are removed. The change summary, acceptance criteria, design +decisions, verification scenarios, task definitions, and verify instructions +are authored for this change. -After writing the plan, run `sce plan check {plan-path}`. When it reports -diagnostics, fix each reported defect in the written plan and run it again. Do -not set internal status `plan_ready` until the checker passes. +While any placeholder remains, keep authoring or return `blocked`. A passing +`sce plan check` is required but not sufficient: it is structural and does not +detect placeholders. -Trust the checker only when it actually ran: a result counts only on exit `0` -with `structured plan: reference integrity holds` on stdout, or on exit `3` -with diagnostics. Any other outcome means the checker is unavailable; apply the -manual `Reference rules` pass from step 2.9 to the written plan instead, and -record in the open questions that the checker was unavailable. +### Structural check + +For new plans and revisions alike, run `sce plan check {plan-path}`. It passes +only on exit `0` with `structured plan: reference integrity holds` on stdout. + +On exit `3` with structural diagnostics, fix each reported defect in the plan +and run it again, or return `blocked` with the remaining diagnostics when they +cannot be resolved. Every other result returns `blocked` with the exit code and +stderr. This includes a missing `sce` executable, a binary without `check`, any +other exit code, and an exit `0` without the success marker. + +Set internal status `plan_ready` only after both the completeness gate and +`sce plan check` pass. ## 2.11 Return the result @@ -241,6 +288,16 @@ Write user-facing questions and open questions directly and specifically. - Do not invent concerns, add praise or reassurance, or repeat the same concern in several forms. - When the user overrules a concern, record the decision and continue. Do not reintroduce it as a constraint, non-goal, or task. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Plan authoring boundaries Do not: diff --git a/.pi/skills/sce-next-task/references/context-sync.md b/.pi/skills/sce-next-task/references/context-sync.md index 807b83ffa..86cd8a166 100644 --- a/.pi/skills/sce-next-task/references/context-sync.md +++ b/.pi/skills/sce-next-task/references/context-sync.md @@ -214,6 +214,8 @@ When editing context: - Update cross-references when files are added, moved, renamed, or superseded. - Keep one authoritative statement for each durable fact. - Avoid copying the execution result verbatim into context files. +- Write new or modified prose under the **Controlled English for Plans and Context** + rules below. - Do not change application code, tests, or plan state. Create a new context file only when: @@ -311,6 +313,16 @@ Record only the Markdown report. Do not add explanatory prose before or after it Do not determine whether the plan is complete. The `/next-task` workflow owns that decision after context synchronization. +## Controlled English for Plans and Context + +Apply these rules to new or modified prose in SCE plans and durable context. Preserve Markdown structure, code, commands, identifiers, technical terminology, factual meaning, and existing task evidence. Do not rewrite unrelated content solely for style. + +1. Sentence structure: Use a maximum of 20 words per procedural sentence and 25 words per descriptive sentence, counted according to STE rules. Give one instruction per sentence, using imperative form and placing prerequisites first. Limit ordinary multi-word nouns to three words. Keep one topic and at most six sentences per descriptive paragraph. Do not use contractions or semicolons. Use active voice, except when the actor is unknown in descriptive text. + +2. Vocabulary: Prefer STE-approved words and established domain terminology. Use terms consistently and preserve their technical meanings. + +3. Verbs: Use simple present, simple past, simple future, and imperative forms. Avoid perfect and progressive constructions. Use "can" for possibility, ability, or permission, "must" for obligation, and "will" for future events. Replace "may", "should", "shall", and "would" with appropriate constructions. Use "could" only for past ability or permission. + ## Task context synchronization boundaries Do not: From 69e617c03aa80eebff49c896cbf521672b01ef55 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 18:09:56 +0200 Subject: [PATCH 25/61] workflow: Strengthen test-design guidance for task execution Require task execution workflows to design discriminating tests before implementation, independently verify high-risk expectations afterward, and use meaningful property-based or structured-random testing when feasible. Apply the guidance consistently across canonical and generated workflow references. Co-authored-by: SCE --- .../references/task-execution.md | 15 +++++++++ .../references/task-execution.md | 15 +++++++++ .../references/task-execution.md | 15 +++++++++ .../references/task-execution.md | 15 +++++++++ config/pkl/base/workflow-next-task.pkl | 32 ++++++++++++++++++- 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/.agents/skills/sce-next-task/references/task-execution.md b/.agents/skills/sce-next-task/references/task-execution.md index 4c92e6a70..6d0a09998 100644 --- a/.agents/skills/sce-next-task/references/task-execution.md +++ b/.agents/skills/sce-next-task/references/task-execution.md @@ -113,6 +113,21 @@ checks. Use judgment for ordinary, reversible local implementation choices. +When writing or modifying tests: + +1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + +2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + +3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. diff --git a/.claude/skills/sce-next-task/references/task-execution.md b/.claude/skills/sce-next-task/references/task-execution.md index 4c92e6a70..6d0a09998 100644 --- a/.claude/skills/sce-next-task/references/task-execution.md +++ b/.claude/skills/sce-next-task/references/task-execution.md @@ -113,6 +113,21 @@ checks. Use judgment for ordinary, reversible local implementation choices. +When writing or modifying tests: + +1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + +2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + +3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. diff --git a/.opencode/skills/sce-next-task/references/task-execution.md b/.opencode/skills/sce-next-task/references/task-execution.md index 4c92e6a70..6d0a09998 100644 --- a/.opencode/skills/sce-next-task/references/task-execution.md +++ b/.opencode/skills/sce-next-task/references/task-execution.md @@ -113,6 +113,21 @@ checks. Use judgment for ordinary, reversible local implementation choices. +When writing or modifying tests: + +1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + +2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + +3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. diff --git a/.pi/skills/sce-next-task/references/task-execution.md b/.pi/skills/sce-next-task/references/task-execution.md index 4c92e6a70..6d0a09998 100644 --- a/.pi/skills/sce-next-task/references/task-execution.md +++ b/.pi/skills/sce-next-task/references/task-execution.md @@ -113,6 +113,21 @@ checks. Use judgment for ordinary, reversible local implementation choices. +When writing or modifying tests: + +1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + +2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + +3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. diff --git a/config/pkl/base/workflow-next-task.pkl b/config/pkl/base/workflow-next-task.pkl index 1b29245e9..c2194001a 100644 --- a/config/pkl/base/workflow-next-task.pkl +++ b/config/pkl/base/workflow-next-task.pkl @@ -936,7 +936,22 @@ local renderExecutionSkillBody = (mode: model.WorkflowRenderMode) -> """ checks. Use judgment for ordinary, reversible local implementation choices. - + + When writing or modifying tests: + + 1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + + 2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + + 3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. @@ -1751,6 +1766,21 @@ checks. Use judgment for ordinary, reversible local implementation choices. +When writing or modifying tests: + +1. Before implementing, identify likely subtle bugs, mistakes, and + alternative interpretations. Design tests that distinguish them, + preferring asymmetric examples and boundary cases on both sides. + +2. After implementing, independently derive expected results for high-risk + cases from the requirements, without using production code or helpers. + Compare these expectations with actual results, using fresh context when + available. + +3. When feasible, use property-based tests or structured random inputs to + explore meaningful states and code paths. Avoid random tests that only + check for crashes or repeatedly exercise the same error path. + Stop when implementation requires: - Material scope expansion. From 61f88b3b32377326b2aeb39e71678c78ad0ffed6 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 18:35:47 +0200 Subject: [PATCH 26/61] plans: Refresh validation evidence Update the sce-plan-check and sce-plan-init records to reflect the revised plan-check behavior and current validation evidence. Document the single structured-plan success result, unstructured-plan diagnostics, the changed workflow fallback rules, and the post-T04 publication-cleanup verification while preserving superseded validation as historical evidence. Plans: sce-plan-check, sce-plan-init Co-authored-by: SCE --- context/plans/sce-plan-check.md | 169 ++++++++++++++++++++++++++------ context/plans/sce-plan-init.md | 92 ++++++++++++++++- 2 files changed, 226 insertions(+), 35 deletions(-) diff --git a/context/plans/sce-plan-check.md b/context/plans/sce-plan-check.md index 5b07ae026..b0279b4d0 100644 --- a/context/plans/sce-plan-check.md +++ b/context/plans/sce-plan-check.md @@ -17,9 +17,30 @@ typed GFM `mdast` tree, extracts an SCE plan model with source positions, and runs pure validation over that model. It keeps the Pkl checker's behavior: both implementations run against the same fixtures, driven by one shared expectations manifest. The canonical `/change-to-plan` and `/validate` workflow sources then -call `sce plan check` for structured plans and keep their current legacy-plan -behavior. This extends existing behavior. The Pkl checker stays in place as the -generation-time reference implementation. +call `sce plan check` on every plan they write or validate. This extends +existing behavior. The Pkl checker stays in place as the generation-time +reference implementation. + +Current behavior: this plan first specified a legacy-plan classification. A +legacy plan passed with exit `0`, the workflows skipped the checker for it, and +both workflows fell back to a manual `Reference rules` pass. Later commits on the +same branch (PR #342) superseded these requirements: + +- Commit `0e0d756c` changed the manifest from expected substrings to the + complete set of expected messages. +- Commit `7c6dde7c` removed the legacy classification. Every plan is now checked + against the structured-plan rules. An unstructured plan fails with validation + diagnostics and exit `3`. A valid structured plan passes with exactly + `structured plan: reference integrity holds`. No legacy-plan success + classification exists. +- `context/plans/sce-plan-init.md` T03 (its D6) removed the `/change-to-plan` + checker fallback. `/validate` keeps its fallback. + +The design decisions, acceptance criteria, and verification scenarios below +describe the current behavior. Each revised entry carries a `Revised:` field. +The task records and the original validation results are kept unchanged as +historical evidence. They describe what was built and verified before those +revisions. ## Design decisions @@ -29,33 +50,39 @@ generation-time reference implementation. - Rejected: Porting the Pkl regex/line scanner to Rust — this is the custom Markdown parser the request forbids. `comrak` / `pulldown-cmark` — the request names `markdown-rs` explicitly, and a second parser adds no value. - Trade-offs: The AST follows CommonMark list-nesting rules, while the Pkl checker requires exactly `entry indent + 2` spaces. Inputs that only one of them treats as nested can diverge. The fixtures pin the shared behavior, and the contract document (D3) records the known divergences. - D2: Parsing, plan representation, semantic validation, and CLI integration are separate layers in a new `cli/src/services/plan/` service with static dispatch. - - Boundary: `extract.rs` turns source into `Plan`: headings with lines, plus `PlanEntry { id, kind, checked, line, fields: Vec }` grouped by kind. `validate.rs` holds a pure `fn validate(plan: &Plan) -> PlanReport { structured, diagnostics: Vec, message }> }` with no I/O. `command.rs` reads the file, calls extract then validate, and renders the result. `cli_schema.rs`, `services/parse/command_runtime.rs`, and `services/command_registry.rs` add one `Plan`/`PlanCommand` variant each. No `dyn`, no boxed futures, no async, no persistent state. + - Boundary: `extract.rs` turns source into `Plan`: headings with lines, plus `PlanEntry { id, kind, checked, line, fields: Vec }` grouped by kind. `validate.rs` holds a pure `fn validate(plan: &Plan) -> PlanReport { diagnostics: Vec, message }> }` with no I/O. `command.rs` reads the file, calls extract then validate, and renders the result. `cli_schema.rs`, `services/parse/command_runtime.rs`, and `services/command_registry.rs` add one `Plan`/`PlanCommand` variant each. No `dyn`, no boxed futures, no async, no persistent state. - Rationale: Pure validation over a small owned model can be tested without the CLI. Keeping the AST out of `validate.rs` stops parser types from leaking into the rules. - Rejected: Validating directly over `mdast` nodes — this mixes traversal with rules and makes the rules parser-shaped. A trait-based checker abstraction — there is only one implementation. - Trade-offs: One small intermediate model to maintain. It is kept minimal: one entry type with a kind enum, not four structs per kind. -- D3: One shared fixture contract prevents Pkl/Rust drift. Both checkers run the same `config/pkl/renderers/fixtures/plan-structure/*.md` fixtures from a single manifest, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` (fixture, `structured`/`legacy`/`fail`, expected diagnostic substrings). Rust diagnostic messages reuse the Pkl defect wording verbatim. + - Revised: The original `PlanReport` also carried a `structured` flag for the legacy classification. Commit `7c6dde7c` removed it. +- D3: One shared fixture contract prevents Pkl/Rust drift. Both checkers run the same `config/pkl/renderers/fixtures/plan-structure/*.md` fixtures from a single manifest, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` (fixture, `pass`/`fail`, then for `fail` rows the complete expected defect messages, compared as an exact sorted multiset). Rust diagnostic messages reuse the Pkl defect wording verbatim. + - Revised: The original manifest had `structured`/`legacy`/`fail` verdicts and expected diagnostic substrings. Commit `0e0d756c` changed it to complete messages, and commit `7c6dde7c` replaced the verdicts with `pass`/`fail`. - Boundary: `config/pkl/check-generated.sh` reads the manifest instead of hard-coding the `expect_plan_structure_pass`/`expect_plan_structure_failure` calls. A Rust test in `cli/src/services/plan/` reads the same manifest and fixtures at test runtime through `CARGO_MANIFEST_DIR/../config/pkl/renderers/fixtures/plan-structure`. That tree already reaches the Crane sandbox through `cliBuildInputFileset` (`./config/pkl`), so no flake change is needed. The behavioral contract is documented in `context/sce/plan-structure-contract.md`. - Rationale: A single manifest makes "same fixtures, same verdicts, same defect text" mechanically true rather than a convention. - Rejected: Separate Rust fixture copies or a Rust-only expectations table — two equivalent sets drift. Making the Rust CLI or its tests invoke Pkl — the distributed binary must not need Pkl. - Trade-offs: Pkl defect wording becomes a cross-implementation contract, and rewording requires touching both checkers. - D4: Outcome mapping reuses existing CLI classification and the Pkl result wording. - - Boundary: A valid plan prints `structured plan: reference integrity holds` or `legacy plan: structured reference rules do not apply` to stdout and exits `0`. A structurally invalid plan returns `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`), whose stderr message lists one diagnostic per line as `{path}:{line}: {message}`, or `{path}: {message}` for plan-level defects such as a missing heading. `{path}` is the path as given. Diagnostics are ordered by line, then message, for deterministic output. Missing or unreadable files go through the existing runtime-failure path (exit `4`) with `with_context` naming the path. + - Revised: The original boundary also gave a legacy plan exit `0` with `legacy plan: structured reference rules do not apply`. Commit `7c6dde7c` removed that classification. + - Boundary: Every plan is checked against the structured-plan rules. A valid structured plan prints exactly `structured plan: reference integrity holds` to stdout and exits `0`. No other success line exists. A structurally invalid plan, including an unstructured plan, returns `CliError::validation` (exit `3`, `SCE-ERR-VALIDATION`), whose stderr message lists one diagnostic per line as `{path}:{line}: {message}`, or `{path}: {message}` for plan-level defects such as a missing heading. `{path}` is the path as given. Diagnostics are ordered by line, then message, for deterministic output. Missing or unreadable files go through the existing runtime-failure path (exit `4`) with `with_context` naming the path. - Rationale: Exit `3` already means validation failure in `context/sce/cli-exit-code-contract.md`. Reusing the Pkl strings keeps D3's contract simple. - Rejected: A new exit code for plan defects — it breaks the class-based contract. Printing diagnostics to stdout — stdout is reserved for command payloads. - Trade-offs: The diagnostics share the `CliError` stderr envelope (including the class's `Try:` guidance) rather than a bespoke report format. -- D5: The workflows trust the checker only when it actually ran. A result counts only on exit `0` with one of the two D4 classification lines on stdout, or exit `3` with diagnostics. Any other outcome means the checker is unavailable, and the workflow falls back to its existing manual `Reference rules` pass and says so. - - Boundary: Canonical `config/pkl/base/workflow-change-to-plan.pkl` (step 2.9/2.10) and `config/pkl/base/workflow-validate.pkl` (step 4 "Check plan references") only. `/next-task` is untouched. +- D5: The workflows trust the checker only when it actually ran. A result counts only on exit `0` with `structured plan: reference integrity holds` on stdout, or exit `3` with diagnostics. `/validate` treats any other outcome as an unavailable checker, applies its manual `Reference rules` pass, and records that fallback under notes. `/change-to-plan` has no fallback: any other outcome returns `blocked`. + - Revised: Originally both workflows accepted either D4 classification line, skipped the checker for a legacy plan, and fell back to the manual pass. Commit `7c6dde7c` removed the legacy line and the legacy skip. `context/plans/sce-plan-init.md` D6 and T03 removed the `/change-to-plan` fallback. + - Boundary: Canonical `config/pkl/base/workflow-change-to-plan.pkl` (step 2.10 "Structural check") and `config/pkl/base/workflow-validate.pkl` (step 4 "Check plan references") only. `/next-task` never mentions `sce plan check`. - Rationale: An older `sce` binary resolves the unknown `plan` command to help with exit `0` (per the exit-code contract), so exit status alone would report a false pass. - - Rejected: Requiring the new binary and failing without it — this would break workflows for users on an older `sce`. - - Trade-offs: The workflow prose carries one extra fallback rule. + - Rejected: For `/validate`, requiring the new binary and failing without it, because validation still yields a report from the manual pass. For `/change-to-plan`, a manual fallback, because it could report an unchecked plan as `plan_ready` (`context/plans/sce-plan-init.md` D6). + - Trade-offs: The `/validate` prose carries one extra fallback rule. `/change-to-plan` cannot author plans without a current `sce`. ## Acceptance criteria -- [x] AC1: `sce plan check ` validates exactly the selected file, read-only. It accepts relative and absolute paths. It exits `0` and prints the structured or legacy classification line for a valid plan. It exits `3` and prints `{path}:{line}: {message}` diagnostics on stderr for a structurally invalid plan. A missing or unreadable file fails through the existing runtime-failure handling. It never executes `Check:`, `Validate:`, or `Verify:` content. - - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` plus `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md; echo $?` (expect `0`) and the same against `duplicate-id.md` (expect `3`). +- [x] AC1: `sce plan check ` validates exactly the selected file, read-only. It accepts relative and absolute paths. It checks every plan against the structured-plan rules. It exits `0` and prints exactly `structured plan: reference integrity holds` for a valid structured plan. It exits `3` and prints `{path}:{line}: {message}` diagnostics on stderr for a structurally invalid plan, including an unstructured plan. No legacy-plan success classification exists. A missing or unreadable file fails through the existing runtime-failure handling. It never executes `Check:`, `Validate:`, or `Verify:` content. + - Revised: Originally a valid legacy plan exited `0` with a legacy classification line. Commit `7c6dde7c` removed that classification and added S10. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` plus `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/valid-structured-plan.md; echo $?` (expect `0`), and the same against `duplicate-id.md` and `unstructured-plan.md` (expect `3`). - Decisions: D1, D2, D4 - - Scenarios: S1, S2, S3, S4 -- [x] AC2: The Rust validator and the Pkl checker give the same verdict and defect text for every fixture under `config/pkl/renderers/fixtures/plan-structure/`, both driven by the single `expectations.tsv` manifest. The fixtures cover valid structured, inspection-only, fenced-example, legacy, and legacy-with-fenced-structured-example plans. They also cover duplicate IDs, missing and empty `Covers:`, dangling references, wrong reference kinds, uncovered AC, unreferenced scenario, missing required fields, misplaced definitions and fields, wrong heading placement, and keywords only inside fenced examples. + - Scenarios: S1, S2, S3, S4, S10 +- [x] AC2: The Rust validator and the Pkl checker give the same verdict and defect text for every fixture under `config/pkl/renderers/fixtures/plan-structure/`, both driven by the single `expectations.tsv` manifest. The passing fixtures are valid structured, inspection-only, and fenced-example plans. The unstructured and unstructured-with-fenced-structured-example plans fail. The fixtures also cover duplicate IDs, missing and empty `Covers:`, dangling references, wrong reference kinds, uncovered AC, unreferenced scenario, missing required fields, misplaced definitions and fields, wrong heading placement, and keywords only inside fenced examples. + - Revised: The original legacy and legacy-with-fenced-structured-example fixtures passed as legacy plans. Commit `7c6dde7c` renamed them to `unstructured-plan.md` and `unstructured-plan-with-fenced-structured-example.md`, which now fail. - Validate: `nix run .#pkl-check-generated` and `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture` - Decisions: D1, D3 - Scenarios: S5, S6 @@ -67,7 +94,8 @@ generation-time reference implementation. - Validate: `nix run .#sce -- --help`, `nix run .#sce -- plan check --help`, and `nix run .#sce -- completion --shell bash | grep -n "plan"` - Decisions: D2 - Scenarios: S8 -- [x] AC5: The generated `/change-to-plan` workflow runs `sce plan check {plan-path}` after writing a structured plan and corrects reported defects before returning `plan_ready`. The generated `/validate` workflow runs `sce plan check {plan-path}` before scenario verification for structured plans and records each reported defect as a failed check. Both fall back to the manual `Reference rules` pass when the checker is unavailable, keep legacy-plan behavior unchanged, and leave `/next-task` unchanged. +- [x] AC5: The generated `/change-to-plan` workflow runs `sce plan check {plan-path}` after writing every new or revised plan. It repairs reported defects and reruns the checker. It reports `plan_ready` only on exit `0` with `structured plan: reference integrity holds`, and returns `blocked` on every other result. The generated `/validate` workflow runs `sce plan check {plan-path}` before scenario verification for every plan and records each reported defect as a failed check. It applies the D5 trust rule and falls back to the manual `Reference rules` pass when the checker is unavailable. Neither workflow skips the checker for a legacy plan. No generated `/next-task` document mentions `sce plan check`. + - Revised: Originally both workflows fell back to the manual pass and skipped the checker for legacy plans. Commit `7c6dde7c` removed the legacy skip. `context/plans/sce-plan-init.md` T03 removed the `/change-to-plan` fallback. - Validate: `nix run .#pkl-check-generated` - Decisions: D5 - Scenarios: S9 @@ -80,11 +108,12 @@ generation-time reference implementation. ### Verification scenarios -- S1: Valid structured and legacy plans pass - - Given: `valid-structured-plan.md`, `inspection-only-plan.md`, and `legacy-plan.md` fixtures. +- S1: Valid structured plans pass + - Given: The `valid-structured-plan.md` and `inspection-only-plan.md` fixtures. - When: `sce plan check` runs on each one, once by relative path and once by absolute path. - - Then: Exit `0`. Stdout is exactly `structured plan: reference integrity holds` for the structured fixtures and `legacy plan: structured reference rules do not apply` for the legacy fixture. Stderr is empty. + - Then: Exit `0`. Stdout is exactly `structured plan: reference integrity holds`. Stderr is empty. - Check: Rust command test `plan::command_valid_plans_exit_success`, created by T03, plus the AC1 `nix run .#sce` command. + - Revised: The original scenario also expected `legacy-plan.md` to exit `0` with the legacy classification line. Commit `7c6dde7c` removed that case, and S10 covers unstructured plans. - S2: Structurally invalid plan fails with validation class - Given: The `duplicate-id.md` fixture. - When: `sce plan check` runs on it. @@ -99,17 +128,20 @@ generation-time reference implementation. - Given: A structured fixture whose `Check:`, `Validate:`, and `Verify:` values name a command that would create a marker file. - When: `sce plan check` runs on it. - Then: Exit `0`, and the marker file does not exist afterwards. - - Check: Rust command test `plan::command_does_not_execute_plan_commands`, created by T03, plus named inspection that `cli/src/services/plan/` contains no `std::process` use. + - Check: Rust command test `plan::command_does_not_execute_plan_commands`, created by T03, plus named inspection that the check path (`extract.rs`, `validate.rs`, and `check` in `command.rs`) contains no `std::process` use. + - Revised: The original inspection covered all of `cli/src/services/plan/`. `context/plans/sce-plan-init.md` T02 later added `init.rs`, which runs `git rev-parse --show-toplevel` for `sce plan init` only. - S5: Rust matches every manifest expectation - Given: `expectations.tsv` and every fixture it lists. - When: The Rust fixture test extracts and validates each fixture. - - Then: Each fixture classified `structured` or `legacy` has no diagnostics and that classification. Each `fail` fixture produces a diagnostic containing every expected substring. Every `.md` fixture in the directory is listed in the manifest. + - Then: Each `pass` fixture produces no diagnostics. Each `fail` fixture produces exactly its expected messages, compared as a sorted multiset. Every `.md` fixture in the directory is listed in the manifest. - Check: Rust test `plan::fixture_expectations_match_manifest`, created by T02. + - Revised: The original scenario expected `structured`/`legacy` classifications and substring matches. Commits `0e0d756c` and `7c6dde7c` changed these to exact messages and `pass`/`fail` verdicts. - S6: Pkl matches every manifest expectation - Given: The same `expectations.tsv`. - When: `config/pkl/check-generated.sh` iterates it through `plan-structure-check.pkl`. - - Then: Every pass fixture prints its classification, every fail fixture fails with each expected substring, and the run aborts on any mismatch or unlisted fixture. + - Then: Every `pass` fixture prints `structured plan: reference integrity holds`. Every `fail` fixture fails, and its structured report equals its expected message set. The run aborts on any mismatch or unlisted fixture. - Check: `nix run .#pkl-check-generated` + - Revised: The original scenario expected per-fixture classifications and substring matches. Commits `0e0d756c` and `7c6dde7c` changed these to the single pass line and exact message sets. - S7: Diagnostics point at the offending line - Given: The `misplaced-field.md`, `duplicate-id.md`, and `task-empty-covers.md` fixtures. - When: The Rust validator runs on each one. @@ -120,11 +152,17 @@ generation-time reference implementation. - When: Top-level help, `plan check --help`, and `completion --shell {bash,zsh,fish}` run. - Then: Top-level help lists `plan`. Command help shows the required `` argument. Each completion script contains `plan` and `check`. - Check: Existing help/completion tests updated by T03, plus the AC4 commands. -- S9: Generated workflows invoke the checker with fallback +- S9: Generated workflows invoke the checker, and only `/validate` has a fallback - Given: The canonical workflow Pkl sources after T04. - When: Generation and the contract checks run. - - Then: Every target's generated `sce-change-to-plan/references/plan-authoring.md` and `sce-validate/references/validation.md` contain the `sce plan check` invocation, the D5 trust rule, the manual fallback, and the legacy skip. No generated `sce-next-task` document mentions `sce plan check`. - - Check: `nix run .#pkl-check-generated`, with the extended `plan-authoring-structured-references` and `validate-scenario-evidence` assertions from T04. + - Then: Every target's generated `sce-change-to-plan/references/plan-authoring.md` contains the `sce plan check {plan-path}` invocation and the exact success marker, and has no checker fallback. Every target's generated `sce-validate/references/validation.md` contains the invocation, the D5 trust rule, and the manual `Reference rules` fallback. Neither document has a legacy skip. No generated `sce-next-task` document mentions `sce plan check`. + - Check: `nix run .#pkl-check-generated`, with the `plan-authoring-structured-references` and `validate-scenario-evidence` assertions from T04 and the `plan-authoring-plan-init` assertion from `context/plans/sce-plan-init.md` T03. + - Revised: The original scenario required the manual fallback and the legacy skip in both documents. Commit `7c6dde7c` removed the legacy skip. `context/plans/sce-plan-init.md` T03 removed the plan-authoring fallback. +- S10: Unstructured plans fail with validation diagnostics + - Given: The `unstructured-plan.md` fixture, which uses the pre-structured plan format. + - When: `sce plan check` runs on it. + - Then: Exit `3`. Stdout is empty. Stderr carries `{path}:{line}:` diagnostics, including ``{path}:59: `T01` is missing `Covers:` ``. No legacy classification line is printed. + - Check: Rust command test `plan::command_unstructured_plan_exits_validation`, added by commit `7c6dde7c`, plus the AC1 `nix run .#sce` command. ### Full validation @@ -134,7 +172,7 @@ generation-time reference implementation. ### Context sync - `context/sce/plan-structure-contract.md` (new, the Pkl/Rust behavioral contract) -- `context/sce/shared-context-plan-workflow.md` (responsibility split: `sce plan check` for one selected plan, checker fallback) +- `context/sce/shared-context-plan-workflow.md` (responsibility split: `sce plan check` for one selected plan, the `/validate` checker fallback) - `context/sce/config-generation.md` (`check-generated.sh` drives plan-structure fixtures from `expectations.tsv`) - `context/cli/cli-command-surface.md` and `context/overview.md` (new `plan` command and `services/plan` domain) - `context/glossary.md` (`sce plan check`, plan-structure expectations manifest) @@ -248,7 +286,78 @@ Persist this field in every plan; this is durable plan state, not chat state: **Status:** validated **Date:** 2026-10-24 -### Commands run +The current status comes from the revalidation below, which verifies the +revised requirements at PR #342 head `69e617c0`. The original validation is +kept as a historical record. It verified the original requirements, including +the legacy classification and the `/change-to-plan` fallback that are now +superseded, and it is not evidence for the revised requirements. + +### Revalidation of the revised requirements + +Run on 2026-10-24 at HEAD `69e617c03aa80eebff49c896cbf521672b01ef55`. The tree +was clean apart from edits to `context/plans/`, which Nix and generation checks +do not read. + +#### Commands run + +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` -> exit 0 (28 passed, 610 filtered out) +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 146 files, inventory sha256 `087ce623…ef16`) +- `nix flake check` -> exit 1 (first run, concurrent with other Cargo test runs: `cli-tests` 637 passed, 1 failed — `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline`, "child exited after 8.3714708s", a wall-clock bound outside `services/plan`) +- `nix flake check` -> exit 0 (rerun with no other load: `cli-tests` 638 passed, 0 failed; all checks passed) +- `nix build .#default --no-link` -> exit 0 +- `nix run .#sce -- plan check config/pkl/renderers/fixtures/plan-structure/{valid-structured-plan,inspection-only-plan}.md`, by relative and by absolute path -> exit 0, stdout exactly `structured plan: reference integrity holds` +- `nix run .#sce -- plan check …/duplicate-id.md`, by relative and by absolute path -> exit 3, empty stdout, `SCE-ERR-VALIDATION`, one ``…/duplicate-id.md:29: `S1` is defined more than once`` diagnostic +- `nix run .#sce -- plan check …/unstructured-plan.md`, by relative and by absolute path -> exit 3, empty stdout, `SCE-ERR-VALIDATION`, eight `{path}:{line}:` diagnostics (lines 9, 11, 13, 59, 73), including ``…/unstructured-plan.md:59: `T01` is missing `Covers:` ``; no legacy line +- `nix run .#sce -- plan check does/not/exist.md` -> exit 4 (`SCE-ERR-RUNTIME`, `Failed to read plan file 'does/not/exist.md'`) +- `nix run .#sce -- --help` -> lists `plan` and the `Plan Usage:` lines `sce plan check ` and `sce plan init `; `nix run .#sce -- plan check --help` -> `Usage: check ` +- `nix run .#sce -- completion --shell {bash,zsh,fish}` -> each script contains `plan` and `check` +- Inspection: `grep -n 'process\|Command::new' cli/src/services/plan/*.rs` -> no match in `extract.rs`, `validate.rs`, or `command.rs`; matches only in `init.rs` (`git rev-parse`, temporary-file PID) and test helpers in `mod.rs` +- Inspection of the committed generated trees (`.agents`, `.claude`, `.opencode`, `.pi`): each `sce-change-to-plan/references/plan-authoring.md` has the `sce plan check {plan-path}` invocation and the success marker, no `checker is unavailable`, and no `legacy`; each `sce-validate/references/validation.md` has the invocation, the trust rule, and `checker is unavailable`, and no `legacy`; no `sce-next-task` document mentions `sce plan check` +- Inspection: `git diff main -- cli/Cargo.toml` adds only `markdown = "1"`; the lock adds `markdown` and `unicode-id`; `flake.nix` is unchanged versus `main`; `flake.nix` and `config/pkl/check-generated.sh` contain no `context/plans` reference +- `nix run .#sce -- plan check context/plans/sce-plan-check.md` -> exit 0 (`structured plan: reference integrity holds`) + +#### Success-criteria verification + +- [x] AC1: Every plan is checked against the structured rules; valid structured plans exit 0 with the exact marker; invalid and unstructured plans exit 3 with diagnostics; missing file exits 4; no plan command runs -> `plan::` tests passed and CLI runs matched; scenarios: S1, S2, S3, S4, S10 +- [x] AC2: Rust and Pkl agree on every fixture from the `pass`/`fail` manifest with exact message sets -> `plan::fixture_expectations_match_manifest` and `nix run .#pkl-check-generated` passed; scenarios: S5, S6 +- [x] AC3: Diagnostics carry 1-based `mdast` source lines -> `plan::diagnostic_lines_match_source_positions` passed; scenarios: S7 +- [x] AC4: Help and bash/zsh/fish completion expose `plan check` -> help, `plan check --help`, and completion output as listed; `plan::plan_check_appears_in_help` and `plan::completion_scripts_include_plan_check` passed; scenarios: S8 +- [x] AC5: `/change-to-plan` requires the checker with no fallback; `/validate` runs it with the trust rule and manual fallback; no legacy skip; `/next-task` never mentions it -> `nix run .#pkl-check-generated` passed with the `plan-authoring-structured-references`, `plan-authoring-plan-init`, and `validate-scenario-evidence` assertions, and the generated-tree inspection matched; scenarios: S9 +- [x] AC6: Pkl/Rust contract documented -> `context/sce/plan-structure-contract.md` states "No legacy classification" and the single pass line, and is linked from `context/sce/shared-context-plan-workflow.md:141` and `context/context-map.md:63`; scenarios: none (inspection) +- [x] AC7: Only new runtime dependency is `markdown`; no `context/plans/` Nix dependency -> inspection as listed; scenarios: none (inspection) + +#### Scenario verification + +- S1 (AC1): test `services::plan::command_valid_plans_exit_success` + CLI runs on both fixtures by relative and absolute path -> passed; exit 0, exact marker; status: Verified +- S2 (AC1): test `services::plan::command_invalid_plan_exits_validation` + CLI run on `duplicate-id.md` -> passed; exit 3, empty stdout, one line-29 diagnostic; status: Verified +- S3 (AC1): test `services::plan::command_missing_file_is_runtime_failure` + CLI run on a missing path -> passed; exit 4, `SCE-ERR-RUNTIME`, path named; status: Verified +- S4 (AC1): test `services::plan::command_does_not_execute_plan_commands` + check-path `std::process` inspection -> passed; no process use on the check path; status: Verified +- S5 (AC2): test `services::plan::fixture_expectations_match_manifest` -> passed; status: Verified +- S6 (AC2): command `nix run .#pkl-check-generated` -> exit 0; status: Verified +- S7 (AC3): test `services::plan::diagnostic_lines_match_source_positions` -> passed; status: Verified +- S8 (AC4): tests `services::plan::plan_check_appears_in_help` and `services::plan::completion_scripts_include_plan_check` + AC4 commands -> passed; status: Verified +- S9 (AC5): command `nix run .#pkl-check-generated` + generated-tree inspection -> exit 0, inspection matched; status: Verified +- S10 (AC1): test `services::plan::command_unstructured_plan_exits_validation` + CLI run on `unstructured-plan.md` -> passed; exit 3, empty stdout, line-59 `T01` diagnostic present, no legacy line; status: Verified + +#### Failed checks and follow-ups + +- None. The first `nix flake check` failure is recorded under residual risks. + +#### Residual risks + +- `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline` failed again under concurrent load and passed on the quiet rerun. It asserts a wall-clock bound on a child process and stays flaky under load, as the original validation also observed. +- `sce plan check --help` still renders `Usage: check ` rather than `sce plan check `. +- AST-versus-line-scanner divergences beyond the fixtures remain documented, not fixed (see Open questions). + +### Original validation (historical) + +Recorded on 2026-10-24, before commit `7c6dde7c`. These results verified the +original requirements. The S1 legacy pass, the `legacy-plan.md` runs, and the +AC5/S9 fallback and legacy-skip results describe behavior that has since been +removed. They are kept unchanged as historical evidence and are not evidence +for current behavior. + +#### Commands run - `nix flake check` -> exit 1 (first run: `cli-tests` 622 passed, 2 failed — `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline` (child exited after 10.07s) and `services::hooks::commit_hooks::tests::commit_patch_id_is_none_without_warning_for_empty_post_commit`; both outside `services/plan`) - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml -- ` -> exit 0 (both passed in isolation) @@ -263,7 +372,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - `nix run .#sce -- --help`, `nix run .#sce -- plan check --help` -> exit 0 (lists `plan`; shows ``) - `nix run .#sce -- completion --shell {bash,zsh,fish}` -> exit 0 (each script contains `plan` and `check`) -### Success-criteria verification +#### Success-criteria verification - [x] AC1: `sce plan check ` validates one file read-only with 0/3/4 outcomes -> `plan::` tests passed; CLI runs gave exit 0 for valid fixtures (relative and absolute), 3 for `duplicate-id.md`, 4 for a missing file; scenarios: S1, S2, S3, S4 - [x] AC2: Rust and Pkl agree on every fixture from `expectations.tsv` -> `plan::fixture_expectations_match_manifest` passed and `pkl-check-generated` passed from the same manifest; scenarios: S5, S6 @@ -273,7 +382,7 @@ Persist this field in every plan; this is durable plan state, not chat state: - [x] AC6: Pkl/Rust contract documented -> `context/sce/plan-structure-contract.md` has Shared rule set, Shared fixtures and manifest, Result and diagnostic wording, Known divergences; linked from `context/sce/shared-context-plan-workflow.md:116` and `context/context-map.md:63`; scenarios: none (inspection) - [x] AC7: Only new runtime dependency is `markdown`; no `context/plans/` Nix dependency -> `git diff main -- cli/Cargo.toml` adds only `markdown = "1"` (lock adds `markdown`, transitive `unicode-id`); `flake.nix` unchanged vs `main`; no `context/plans` reference in `flake.nix` or `check-generated.sh`; scenarios: none (inspection) -### Scenario verification +#### Scenario verification - S1 (AC1): test `plan::command_valid_plans_exit_success` + command `nix run .#sce -- plan check` on the three fixtures (relative and absolute) -> test passed (asserts exit 0, exact stdout, empty stderr); CLI printed exact classification lines with exit 0; status: Verified - S2 (AC1): test `plan::command_invalid_plan_exits_validation` + command `nix run .#sce -- plan check …/duplicate-id.md` -> test passed; CLI exit 3, empty stdout, `SCE-ERR-VALIDATION` and one `…/duplicate-id.md:29: \`S1\` is defined more than once` line; status: Verified @@ -285,11 +394,11 @@ Persist this field in every plan; this is durable plan state, not chat state: - S8 (AC4): test `plan::plan_check_appears_in_help`, `plan::completion_scripts_include_plan_check` + AC4 commands -> tests passed; help lists `plan`, `` shown, bash/zsh/fish scripts contain `plan` and `check`; status: Verified - S9 (AC5): command `nix run .#pkl-check-generated` -> exit 0 with extended `plan-authoring-structured-references` and `validate-scenario-evidence` assertions (including the `sce-next-task` exclusion); status: Verified -### Failed checks and follow-ups +#### Failed checks and follow-ups - None. -### Residual risks +#### Residual risks - The first `nix flake check` run failed two timing/environment-sensitive tests outside this plan's scope (`app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline`, `services::hooks::commit_hooks::tests::commit_patch_id_is_none_without_warning_for_empty_post_commit`); both passed in isolation and on the full rerun, which points to flakiness under load. - `sce plan check --help` renders `Usage: check ` rather than the full `sce plan check ` path. diff --git a/context/plans/sce-plan-init.md b/context/plans/sce-plan-init.md index f4b762082..f0421dfcd 100644 --- a/context/plans/sce-plan-init.md +++ b/context/plans/sce-plan-init.md @@ -357,7 +357,89 @@ None. **Status:** validated **Date:** 2026-10-24 -### Commands run +The current status comes from the revalidation below. It covers AC1–AC8 and +S1–S16, including AC8 and S16, which T04 added after the original validation. +The original validation covered only AC1–AC7 and S1–S15. It is kept as a +historical record and is not evidence for AC8 or S16. + +### Revalidation after T04 (AC1–AC8, S1–S16) + +Run on 2026-10-24 at PR #342 head `69e617c03aa80eebff49c896cbf521672b01ef55`. +The tree was clean apart from edits to `context/plans/`, which Nix and +generation checks do not read. + +#### Commands run + +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` -> exit 0 (28 passed, 610 filtered out) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::init` -> exit 0 (8 passed: the five S1–S5 tests and the three S16 tests) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::scaffold` -> exit 0 (2 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_init` -> exit 0 (1 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::completion_scripts_include_plan_init` -> exit 0 (1 passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::plan:: -- --exact`, once for each S16 test (`init_cleanup_failure_after_publication_warns_and_succeeds`, `init_publication_failure_removes_temporary_and_keeps_error`, and `init_publication_and_cleanup_failure_keeps_original_error_and_destination`) -> exit 0 each (1 passed) +- Observation run: the three S16 tests ran with `-- --nocapture --test-threads=1` and a temporary `eprintln!` of exit code, stdout, stderr, and `context/plans/` entries. The instrumentation was reverted with `git checkout -- cli/src/services/plan/mod.rs`, and `git status` was clean afterwards. Observed results: + - (a) Exit `0`. Stdout is the three D5 lines. Stderr is exactly one `Warning: Created plan context/plans/published.md, but failed to remove temporary file '/context/plans/.published.md..tmp': injected cleanup failure. Try: remove '…' manually; the plan was created and does not need to be initialized again.` line. Entries are `.published.md..tmp` and `published.md`. + - (b) Exit `4`. Stdout is empty. Stderr is `Error [SCE-ERR-RUNTIME]: Failed to publish '/context/plans/unpublished.md': injected link failure …`, with no `Cleanup also failed` line. Entries are empty. + - (c) Exit `3`. Stdout is empty. Stderr is `Error [SCE-ERR-VALIDATION]: Plan already exists: context/plans/taken.md. …`, and the next line is `Cleanup also failed: could not remove temporary file '/context/plans/.taken.md..tmp': injected cleanup failure. Try: remove it manually.` No `Warning:` appears. Entries are `.taken.md..tmp` (the injected removal left it) and `taken.md`, and `taken.md` keeps `known bytes\n`. +- `nix flake check` -> exit 1 (first run, concurrent with other Cargo test runs: `cli-tests` 637 passed, 1 failed — `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline`, "child exited after 8.3714708s", a wall-clock bound outside `services/plan`) +- `nix flake check` -> exit 0 (rerun with no other load: `cli-tests` 638 passed, 0 failed; all checks passed) +- `nix run .#pkl-check-generated` -> exit 0 (Ephemeral Pkl generation passed: 146 files, inventory sha256 `087ce623…ef16`) +- `nix build .#default --no-link` -> exit 0 +- `bash scripts/test-prepare-cli-generated-assets.sh` -> exit 0 (delegation tests passed) +- Inspection: `config/plan-init-scaffold.md` is absent from the worktree, and `git ls-files` lists no `plan-init-scaffold.md` +- `nix run .#sce -- --help` -> `Plan Usage:` lists `sce plan init `; `nix run .#sce -- plan --help` -> lists `init`; `nix run .#sce -- completion --shell {bash,zsh,fish}` -> each script contains `init` +- `nix run .#sce -- plan check context/plans/sce-plan-init.md` -> exit 0 (`structured plan: reference integrity holds`) + +#### Success-criteria verification + +- [x] AC1: `sce plan init example` creates the scaffold at the repository root with the D5 output -> `plan::init` passed; scenarios: S1 +- [x] AC2: Existing entries never overwritten; validation, Clap, and missing-repository failure classes with no writes -> `plan::init` passed; scenarios: S3, S4, S5 +- [x] AC3: Nested invocation targets the root, and its printed check command works there -> `plan::init` passed; scenarios: S2 +- [x] AC4: Untouched scaffold fails `check` with exactly the reference defects; reference-filled scaffold passes -> `plan::scaffold` passed (2 tests); scenarios: S6, S7 +- [x] AC5: Scaffold generated, contract-pinned, embedded, in the fallback, not committed -> `nix run .#pkl-check-generated`, `nix build .#default`, and the fallback test passed; no committed scaffold; scenarios: S8, S9 +- [x] AC6: Workflow uses `init` for new plans only, blocks on any other result, gates `plan_ready` on completeness and `check`, has no fallbacks -> `nix run .#pkl-check-generated` passed, including the `plan-authoring-plan-init` contract and its negative fixtures; scenarios: S10, S11, S13, S14, S15 +- [x] AC7: Help and completions include `plan init` -> `plan::plan_init` and `plan::completion_scripts_include_plan_init` passed; CLI help and completion output as listed; scenarios: S12 +- [x] AC8: Hard-link publication is the commit point; a cleanup failure after it is a warning with exit `0`; write and publication failures keep their error and class and append a failed cleanup; existing destinations are never overwritten -> the three S16 tests passed in `plan::init`, by exact name, and inside `nix flake check`; the observation run matched each S16 case; scenarios: S16 + +#### Scenario verification + +- S1 (AC1): test `services::plan::init_creates_scaffold_at_repository_root` -> ok; status: Verified +- S2 (AC3): test `services::plan::init_from_nested_directory_targets_repository_root` -> ok; status: Verified +- S3 (AC2): test `services::plan::init_never_overwrites_existing_entry` -> ok; status: Verified +- S4 (AC2): test `services::plan::init_rejects_unsafe_names_without_writing` -> ok; status: Verified +- S5 (AC2): test `services::plan::init_outside_repository_is_runtime_failure` -> ok; status: Verified +- S6 (AC4): test `services::plan::scaffold_untouched_fails_check` -> ok; status: Verified +- S7 (AC4): test `services::plan::scaffold_authored_passes_check` -> ok; status: Verified +- S8 (AC5): command `nix run .#pkl-check-generated` (`plan-init-scaffold` contract) -> passed, 146 files; no committed scaffold; status: Verified +- S9 (AC5): commands `bash scripts/test-prepare-cli-generated-assets.sh` and `nix build .#default --no-link` -> both exit 0; status: Verified +- S10 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract) -> passed; status: Verified +- S11 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract) -> passed; status: Verified +- S12 (AC7): tests `services::plan::plan_init_appears_in_help` and `services::plan::completion_scripts_include_plan_init` -> both ok; status: Verified +- S13 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified +- S14 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified +- S15 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified +- S16 (AC8): + - (a) test `services::plan::init_cleanup_failure_after_publication_warns_and_succeeds` -> ok. It asserts exit `0`, exact D5 stdout, the exact warning naming the temporary file, the complete scaffold at the destination, and the temporary file still present. + - (b) test `services::plan::init_publication_failure_removes_temporary_and_keeps_error` -> ok. It asserts exit `4`, empty stdout, `SCE-ERR-RUNTIME`, `Failed to publish`, the injected cause, no `Cleanup also failed`, and an empty `context/plans/`. + - (c) test `services::plan::init_publication_and_cleanup_failure_keeps_original_error_and_destination` -> ok. It asserts exit `3`, empty stdout, `SCE-ERR-VALIDATION`, `Plan already exists: context/plans/taken.md`, the `Cleanup also failed` line naming the temporary file, no `Warning:`, and unchanged `taken.md` bytes. + - The observation run confirmed all three cases. + - status: Verified + +#### Failed checks and follow-ups + +- None. The first `nix flake check` failure is recorded under residual risks. + +#### Residual risks + +- `app::tests::stalled_telemetry_headers_lookup_fails_closed_within_deadline` failed under concurrent load and passed on the quiet rerun. It asserts a wall-clock bound on a child process and is flaky under load. +- S16 (c) says the `Cleanup also failed` line follows the `Plan already exists` error. The test asserts that both lines are present, but not their order. The order is fixed by the `"{error}\nCleanup also failed: …"` format in `cli/src/services/plan/init.rs` and was confirmed only in the observation run. A future reordering would not fail the test. +- `nix flake check` does not build the release package, so a non-test-only compile failure is caught only by `nix build .#default`. + +### Original validation (historical, AC1–AC7 and S1–S15) + +Recorded on 2026-10-24, before T04. It did not cover AC8 or S16. Its +`plan::init` count of five tests predates the three S16 tests. + +#### Commands run - `nix run .#sce -- plan check context/plans/sce-plan-init.md` -> exit 0 (`structured plan: reference integrity holds`) - `nix flake check` -> exit 0 (all checks passed) @@ -369,7 +451,7 @@ None. - `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::completion_scripts_include_plan_init` -> exit 0 (1 passed) - `bash scripts/test-prepare-cli-generated-assets.sh` -> exit 0 (delegation tests passed) -### Success-criteria verification +#### Success-criteria verification - [x] AC1: `sce plan init example` creates the scaffold at the repository root with the D5 output -> `plan::init` filter passed (5 tests); scenarios: S1 - [x] AC2: Existing entries never overwritten; validation, Clap, and missing-repository failure classes with no writes -> `plan::init` filter passed; scenarios: S3, S4, S5 @@ -379,7 +461,7 @@ None. - [x] AC6: Workflow uses `init` for new plans only, blocks on any other result, gates `plan_ready` on completeness and `check`, has no fallbacks -> `nix run .#pkl-check-generated` passed, including the `plan-authoring-plan-init` contract and its negative fixtures; scenarios: S10, S11, S13, S14, S15 - [x] AC7: Help and completions include `plan init` -> `plan::plan_init` filter passed, and `cli-tests` passed inside `nix flake check`; scenarios: S12 -### Scenario verification +#### Scenario verification - S1 (AC1): test `services::plan::init_creates_scaffold_at_repository_root` -> ok; status: Verified - S2 (AC3): test `services::plan::init_from_nested_directory_targets_repository_root` -> ok; status: Verified @@ -397,12 +479,12 @@ None. - S14 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified - S15 (AC6): command `nix run .#pkl-check-generated` (`plan-authoring-plan-init` contract and negative fixtures) -> passed; status: Verified -### Failed checks and follow-ups +#### Failed checks and follow-ups - None at validation time. - Post-validation review follow-up (PR #342), recorded as T04 without changing the results above: temporary-file cleanup after a successful hard link no longer fails `init` (AC8, S16), and the resolved symlinked-directory question was removed from Open questions and recorded in D4. -### Residual risks +#### Residual risks - `nix flake check` does not build the release package, so a non-test-only compile failure is caught only by `nix build .#default`. - The worktree carries an out-of-scope test change in `cli/src/services/hooks/claude_mutation_scope/state.rs`; it is not part of this plan. From a735ba66887dab23fc4d297c0e40db60c1465a45 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 19:39:59 +0200 Subject: [PATCH 27/61] plans: Add PR description workflow plan Define deterministic intent-review brief generation, review-field validation, strict CLI input grammar, and the implementation task stack for the new plan command. Plan: plan-pr-description Tasks: T01, T02, T03, T04 Co-authored-by: SCE --- context/plans/plan-pr-description.md | 235 +++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 context/plans/plan-pr-description.md diff --git a/context/plans/plan-pr-description.md b/context/plans/plan-pr-description.md new file mode 100644 index 000000000..cc4065dd2 --- /dev/null +++ b/context/plans/plan-pr-description.md @@ -0,0 +1,235 @@ +# Plan: plan-pr-description + +## Change summary + +This plan adds a deterministic reviewer brief for GitHub pull requests. It extends the structured plan format from PR #342 with two optional, paired design-decision fields: `Review: required` and `Review question: {one question}`. Both structural checkers, `cli/src/services/plan/validate.rs` and `config/pkl/renderers/plan-structure.pkl`, validate the pair with identical diagnostics. The canonical plan template and authoring rules tell the planning agent to mark only decisions that need senior engineering judgment. + +A new `sce plan pr-description --repo {owner/name} --head {sha} {plan-path}...` command validates each plan with the existing extractor and validator. It then prints a compact Markdown brief to stdout. The brief lists the input plans and the explicitly marked review questions, with links to the exact decision lines at the supplied commit. The brief uses no AI, heuristics, risk scoring, summarization, network access, or file writes. The plan dogfoods the format on `context/plans/sce-plan-init.md` D4 and D6. This change extends existing behavior and keeps every current plan valid. + +## Design decisions + +- D1: `Review:` and `Review question:` are optional fields owned by design decisions, and both checkers validate them as one pair. + - Boundary: `validate.rs` and `plan-structure.pkl` own the pair rules. `extract.rs` stays unchanged because its field grammar already accepts `Review question`. + - Rationale: A field owned by the decision kind reuses the existing misplaced-field rule, the line-anchored diagnostics, and the shared fixture manifest. + - Rejected: A separate review section that lists decision IDs — it splits one decision across two places and needs new reference rules. + - Trade-offs: Each new defect message is a change to both checkers, and the Pkl line scanner needs explicit continuation-line detection for the single-line rule. +- D2: The brief renderer is a pure function in a new `cli/src/services/plan/pr_description.rs` module, reached through a `PlanRequest::PrDescription` variant. + - Boundary: `pr_description.rs` owns argument grammar, collection of marked decisions, the question limit, escaping, and rendering. `command.rs` owns file reads, repository-root resolution, and error classes. + - Rationale: A pure renderer over extracted `Plan` values makes golden and determinism tests independent of the filesystem. + - Rejected: Rendering inside `command.rs` — it mixes I/O with output formatting and makes golden tests depend on temporary repositories. + - Trade-offs: The command adds a third `PlanRequest` variant and one more help and completion surface. +- D3: The command accepts only strict input grammar, so every URL is built without percent-encoding. + - Boundary: `--repo` matches `{owner}/{name}` with owner `[A-Za-z0-9-]+` and name `[A-Za-z0-9._-]+`, excluding `.` and `..`. `--head` is exactly 40 lowercase hexadecimal characters. Each plan path is `context/plans/{file}.md`, where `{file}` uses only `[A-Za-z0-9._-]` and is not `.` or `..`. + - Rationale: Strict grammar rejects absolute paths, `..` traversal, nested paths, and characters that need URL encoding before any file access. + - Rejected: Percent-encoding arbitrary paths — it adds an encoding contract and accepts paths that SCE never creates. + - Trade-offs: Abbreviated or uppercase SHAs and plans in subdirectories of `context/plans/` are rejected. +- D4: The renderer escapes a fixed set of Markdown punctuation in every interpolated text with a backslash. + - Boundary: The escaped set is `` \ ` * _ [ ] < > ! | ~ & `` and applies to review questions and plan link labels. URLs are never escaped because D3 restricts their characters. + - Rationale: CommonMark renders a backslash-escaped ASCII punctuation character as that literal character, so the rendered question keeps its exact wording. + - Rejected: Wrapping questions in code spans — it changes the presentation and fails on questions that contain backticks. + - Trade-offs: Inline code and emphasis in a question render as literal punctuation, not as formatting. +- D5: The command reads each plan from the working tree at the Git repository root, not from the Git object at `--head`. + - Boundary: `command.rs` resolves the root with `git rev-parse --show-toplevel`, as `sce plan init` does, and reads `{root}/{plan-path}`. The command never reads Git objects and never checks that the working tree matches `--head`. + - Rationale: A preview works before the plan edits are committed, and the command stays independent of local object availability. + - Rejected: `git show {sha}:{path}` — it fails for an uncommitted preview and for a SHA that is not available locally. + - Trade-offs: When the working tree differs from `--head`, a decision link can point at a wrong line. The caller must pass the SHA that contains the plan files as read. + - Review: required + - Review question: Is reading plans from the working tree instead of the `--head` commit acceptable for stable links? +- D6: More than three marked questions across all input plans is a validation error, never a truncation. + - Boundary: The limit applies after every input plan passes the structural check. The error names the count and every marked decision as `{plan-label} {decision-id}`. + - Rationale: The brief targets a two-to-three-minute review, and a silent truncation hides a decision that the author marked. + - Rejected: Rendering the first three questions with a warning — the output then omits marked decisions without a failing exit code. + - Trade-offs: A pull request with four or more marked decisions must remove marks or split the plan set before it can generate a brief. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [ ] AC1: The Rust and Pkl checkers accept decisions with and without a complete review pair, and both report identical diagnostics for a missing pair, an invalid `Review:` value, an empty question, a multi-line question, a duplicate review field, and a misplaced review field. + - Validate: `nix run .#pkl-check-generated` and `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` pass with the new review-field fixtures listed in `expectations.tsv`. + - Decisions: D1 + - Scenarios: S1, S2, S3 +- [ ] AC2: Every structured plan that passed `sce plan check` before this change still passes it without edits. + - Validate: `sce plan check` built from this branch passes on `context/plans/plan-decisions-and-verification-scenarios.md` and `context/plans/sce-plan-check.md`, and every existing `pass` fixture keeps its `pass` row. + - Decisions: D1 + - Scenarios: S4 +- [ ] AC3: The canonical plan template and authoring rules document the optional review pair, and tell the planning agent to mark only decisions that need senior engineering judgment. + - Validate: inspection of the `Design decision rules`, the decision template entry, and the `Reference rules` field list in `config/pkl/base/workflow-change-to-plan.pkl`, plus a passing `nix run .#pkl-check-generated`. + - Decisions: D1 + - Scenarios: none (inspection) +- [ ] AC4: `sce plan pr-description` prints the brief between the `` and `` markers, with sorted plan links, marked questions in plan order, escaped question text, exact decision-line links, and byte-identical output on repeated runs. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::pr_description` + - Decisions: D2, D4 + - Scenarios: S5, S6, S7, S8 +- [ ] AC5: `sce plan pr-description` rejects invalid repository, SHA, and plan-path arguments, duplicate plan paths, structurally invalid plans, and more than three marked questions, and it writes nothing to stdout in each case. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::pr_description` + - Decisions: D3, D5, D6 + - Scenarios: S9, S10, S11 +- [ ] AC6: The command runs offline and adds no dependency, trait, `dyn` dispatch, async runtime, database access, GitHub API call, network call, or file write. + - Validate: inspection of the branch diff for `cli/Cargo.toml`, `cli/Cargo.lock`, and `cli/src/services/plan/`. + - Decisions: D2, D5 + - Scenarios: none (inspection) +- [ ] AC7: The PR #342 preview over its three plans contains exactly the two questions marked on `sce-plan-init` D4 and D6, links to the D4 and D6 entry lines, and links to all three plans. + - Validate: `sce plan pr-description --repo crocoder-dev/shared-context-engineering --head "$(git rev-parse HEAD)" context/plans/plan-decisions-and-verification-scenarios.md context/plans/sce-plan-check.md context/plans/sce-plan-init.md`, built from this branch, prints the brief that S12 describes. + - Decisions: D5 + - Scenarios: S12 +- [ ] AC8: `sce --help`, `sce plan --help`, and the generated Bash, Zsh, and Fish completions list `pr-description`. + - Validate: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_pr_description_appears_in_help` + - Decisions: D2 + - Scenarios: S13 + +### Verification scenarios + +- S1: Decisions with and without the review pair are valid + - Given: A structured plan in which one decision carries `Review: required` and a one-line `Review question:`, and another decision carries neither field. + - When: Both checkers check the plan. + - Then: Both report `structured plan: reference integrity holds`. + - Check: fixture `review-fields-valid.md` with a `pass` row in `expectations.tsv`, created by T01, run by `plan::fixture_expectations_match_manifest` and `nix run .#pkl-check-generated`. +- S2: Missing, malformed, and duplicate review fields fail with identical messages + - Given: Decisions that carry only `Review:`, only `Review question:`, `Review: optional`, an empty `Review question:`, a question with a continuation line, and a repeated `Review:` or `Review question:`. + - When: Both checkers check the fixtures. + - Then: Both report exactly the messages in the fixture rows: `` `D1` has `Review:` without `Review question:` ``, `` `D1` has `Review question:` without `Review:` ``, `` `Review:` on `D1` must be `required` ``, `` `Review question:` on `D1` is empty ``, `` `Review question:` on `D1` must be a single line ``, and `` `Review:` appears more than once on `D1` ``. + - Check: fixtures `review-fields-missing-pair.md`, `review-fields-malformed.md`, and `review-fields-duplicate.md` with `fail` rows, created by T01, run by `plan::fixture_expectations_match_manifest` and `nix run .#pkl-check-generated`. +- S3: Misplaced review fields and diagnostic lines + - Given: An acceptance criterion and a task that carry `Review:` and `Review question:`, and the defect fixtures from S2. + - When: The Rust validator checks them. + - Then: Each misplaced field reports `` `{key}:` on `{id}` is misplaced; it belongs to a design decision `` at the field line. Pair defects point at the decision line. Value, empty, single-line, and duplicate defects point at the offending field line. + - Check: fixture `review-fields-misplaced.md` with a `fail` row, plus extended assertions in `plan::diagnostic_lines_match_source_positions`, created by T01. +- S4: Existing structured plans stay valid + - Given: The existing `pass` fixtures and the unchanged PR #342 plans `plan-decisions-and-verification-scenarios.md` and `sce-plan-check.md`. + - When: Both checkers run after T01. + - Then: Every existing `pass` row still passes, and `sce plan check` exits `0` on both plans. + - Check: `nix run .#pkl-check-generated`, `plan::fixture_expectations_match_manifest`, and `nix run .#sce -- plan check` on both plans. +- S5: Golden brief for multiple plans + - Given: Two valid in-memory plans supplied in reverse lexicographic path order, with marked decisions on the second and first decision of one plan and one marked decision in the other. + - When: The renderer runs with a fixed repository and SHA. + - Then: The output equals the golden string byte for byte: start marker, `## Intent review`, a `**Plans:**` line with links in sorted path order, `**Decisions requiring review**`, numbered questions in sorted plan order and source decision order, each with a `blob/{sha}/{path}#L{line}` link, then the end marker and one trailing newline. + - Check: `plan::pr_description_golden_multiple_plans`, created by T03. +- S6: No marked decisions + - Given: Valid plans with no `Review: required` decision. + - When: The renderer runs. + - Then: The output contains the markers, the heading, the `**Plans:**` line, and the line `No decisions explicitly flagged for review.`, and it contains no `**Decisions requiring review**` line. + - Check: `plan::pr_description_no_flagged_decisions`, created by T03. +- S7: Markdown escaping keeps the question text + - Given: A review question that contains `` ` ``, `*`, `_`, `[`, `]`, `<`, `>`, `|`, `~`, `&`, and `\`. + - When: The renderer runs. + - Then: Each character of the D4 set is preceded by one backslash, every other character is copied verbatim, and removing those backslashes gives the original question. + - Check: `plan::pr_description_escapes_markdown`, created by T03. +- S8: Repeated generation is byte-identical + - Given: The same repository, SHA, and plan files in a temporary Git repository. + - When: `sce plan pr-description` runs twice through the parser and `execute_with`, once with the paths in reverse order. + - Then: Both stdout strings are byte-identical. + - Check: `plan::pr_description_repeated_runs_are_identical`, created by T03. +- S9: Invalid arguments are rejected before file access + - Given: Repository values `owner`, `owner/`, `/name`, `a/b/c`, and `owner/..`, SHA values with 39 characters, 41 characters, uppercase hexadecimal, and a non-hexadecimal character, plan paths that are absolute, outside `context/plans/`, nested, without `.md`, or with `..`, and one plan path given twice. + - When: `sce plan pr-description` runs with each input. + - Then: Each run exits `3` (`SCE-ERR-VALIDATION`), names the rejected argument, and writes nothing to stdout. + - Check: `plan::pr_description_rejects_invalid_arguments`, created by T03. +- S10: More than three marked questions is an error + - Given: Two valid plans that mark four decisions in total. + - When: `sce plan pr-description` runs with both plans. + - Then: The run exits `3`, the error states the count `4`, lists every marked decision as `{plan-label} {decision-id}`, and stdout is empty. + - Check: `plan::pr_description_rejects_more_than_three_questions`, created by T03. +- S11: Invalid or missing plans fail + - Given: One structurally invalid plan, and separately one valid path that names no file. + - When: `sce plan pr-description` runs with each input. + - Then: The invalid plan exits `3` with the same diagnostic lines that `sce plan check` prints for it. The missing file exits `4` (`SCE-ERR-RUNTIME`). Stdout is empty in both cases. + - Check: `plan::pr_description_rejects_invalid_plan`, created by T03. +- S12: PR #342 dogfood preview + - Given: `sce-plan-init.md` D4 and D6 carry the review pair with the questions from AC7, and the other two PR #342 plans carry no review fields. + - When: The AC7 command runs at the repository root. + - Then: Stdout has three plan links, exactly two numbered questions, `Are hard-link-only publication and its filesystem limitations acceptable?` linked to the D4 entry line and `Is workflow-only completeness enforcement acceptable?` linked to the D6 entry line. + - Check: the AC7 command, with each `#L{line}` compared against `grep -n -E '^- D(4|6):' context/plans/sce-plan-init.md`. +- S13: Help and completions list the command + - Given: The built CLI. + - When: `sce --help`, `sce plan --help`, and `sce completion` for Bash, Zsh, and Fish run. + - Then: The help output contains a `sce plan pr-description` usage line, and each completion script contains `pr-description`. + - Check: `plan::plan_pr_description_appears_in_help` and `plan::completion_scripts_include_plan_pr_description`, created by T03. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix run .#pkl-check-generated` +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::` +- `nix flake check` +- `nix build .#default --no-link` + +### Context sync + +- `context/cli/plan-service.md`: the review pair rules, the `pr_description.rs` module, the `PrDescription` request, and the `sce plan pr-description` command contract. +- `context/sce/plan-structure-contract.md`: the review pair in the shared rule set, the new defect messages, and the new fixtures. +- `context/sce/shared-context-plan-workflow.md`: the optional review pair in the plan format and the marking rule for planning agents. +- `context/cli/cli-command-surface.md` and `context/glossary.md`: the new command and the terms `review question` and `intent review brief`. + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. + A completed task must be `synced` before another task can start or the plan can + finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** + beside the status. Never infer `synced` from conversation history; write every + lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** `cli/src/services/plan/` (`validate.rs`, `command.rs`, `mod.rs`, new `pr_description.rs`), `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, help text, `config/pkl/renderers/plan-structure.pkl`, `config/pkl/renderers/fixtures/plan-structure/`, `config/pkl/base/workflow-change-to-plan.pkl`, `context/plans/sce-plan-init.md` D4 and D6, and the durable context files listed under `Context sync`. +- **Out of scope:** creating or updating a pull request, pushing commits, editing any GitHub pull request description, replacing an existing description section, and changes to `/next-task` or `/validate`. +- **Constraints:** Reuse the existing `markdown-rs` extractor and validator. Add no dependency, trait, `dyn` dispatch, async runtime, database, GitHub API, or network call. Keep exact Rust and Pkl diagnostic parity through `expectations.tsv`. Preserve completed tasks, validation evidence, and historical records in `sce-plan-init.md`. +- **Non-goal:** Automatic selection, ranking, summarization, or risk scoring of decisions. Acceptance-criteria tables, task logs, validation reports, and CI status in the brief. + +## Assumptions + +- The `Review question:` value is not required to end with `?`. The rule requires only a nonempty single-line value, as the change request states. +- In the no-marked-decisions case, the brief omits the `**Decisions requiring review**` line and prints `No decisions explicitly flagged for review.` in its place. +- A plan link label is the plan file name without `.md`, for example `sce-plan-init`. Multiple plan links are joined with `, ` on the `**Plans:**` line. +- Numbered questions are separated by one blank line, as in the requested example, and the output ends with the end marker and one newline. +- Argument and limit errors use `CliError::validation` (exit `3`), and an unreadable plan file uses `CliError::runtime` (exit `4`), as `sce plan check` does. +- The plan template's decision example shows the review pair as optional lines, and the generated plan init scaffold does not include them, because an unmarked decision is the default. + +## Task stack + +- [ ] T01: `Validate optional review fields in both plan checkers` (status:todo) + - Task ID: T01 + - Covers: AC1, AC2 + - Scope: In — `Review` and `Review question` as decision-owned fields in `validate.rs` and `plan-structure.pkl`, the pair, value, empty, single-line, and duplicate rules with the S2 messages, the five `review-fields-*.md` fixtures and their `expectations.tsv` rows, and extended line assertions in `plan::diagnostic_lines_match_source_positions`. Out — template text, the new command, and edits to existing plans. + - Dependencies: none + - Done when: Both checkers agree on every manifest row, the new fixtures cover S1, S2, and S3, and every existing `pass` fixture still passes. + - Verify: `nix run .#pkl-check-generated`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::diagnostic_lines_match_source_positions`. + - Context synchronization: pending + +- [ ] T02: `Document the review pair in the plan template and authoring rules` (status:todo) + - Task ID: T02 + - Covers: AC3 + - Scope: In — the decision template entry, `Design decision rules`, and `Reference rules` field list in `config/pkl/base/workflow-change-to-plan.pkl`, with a rule to mark only decisions that need senior engineering judgment and to add no placeholder value. Out — the plan init scaffold content, `/next-task`, and `/validate`. + - Dependencies: T01 + - Done when: The generated `plan-template.md` shows the optional pair and the marking rule, and the skeleton and generation contract checks still pass. + - Verify: `nix run .#pkl-check-generated`. + - Context synchronization: pending + +- [ ] T03: `Add the sce plan pr-description command` (status:todo) + - Task ID: T03 + - Covers: AC4, AC5, AC6, AC8 + - Scope: In — `PlanSubcommand::PrDescription` in `cli_schema.rs`, its mapping in `command_runtime.rs`, `PlanRequest::PrDescription` in `command.rs`, the pure renderer in `pr_description.rs`, help text, and the S5 to S11 and S13 tests. Out — any GitHub API, network access, file writes, and edits to plan files. + - Dependencies: T01 + - Done when: The command renders the golden brief, rejects every S9, S10, and S11 input with the stated exit code and empty stdout, appears in help and completions, and adds no dependency. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::pr_description`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::plan_pr_description_appears_in_help`; `git diff --stat main -- cli/Cargo.toml cli/Cargo.lock` shows no change from this task. + - Context synchronization: pending + +- [ ] T04: `Mark PR #342 review decisions and record the preview` (status:todo) + - Task ID: T04 + - Covers: AC7 + - Scope: In — the review pair on D4 and D6 in `context/plans/sce-plan-init.md` with the exact AC7 questions, and the generated three-plan preview recorded in this task's completion evidence. Out — other decisions, completed tasks, validation reports, historical records, and the real pull request description. + - Dependencies: T03 + - Done when: `sce plan check` passes on `sce-plan-init.md`, and the AC7 command prints exactly the two questions with links to the D4 and D6 entry lines and to the three plans. + - Verify: `nix run .#sce -- plan check context/plans/sce-plan-init.md`; the AC7 command through `nix run .#sce --`; `git diff context/plans/sce-plan-init.md` shows only the four added field lines. + - Context synchronization: pending + +## Open questions + +None. The change request specifies the format, rules, and limits. The one material trade-off, reading plans from the working tree, is marked for review on D5. From 43ffd92aaf574fff9636aa8ab275a62657254068 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 19:51:47 +0200 Subject: [PATCH 28/61] plan: Validate decision review fields in both checkers Plans can now mark design decisions for senior engineering review, but the two plan checkers must reject malformed review metadata consistently. Add decision-owned `Review` and `Review question` validation to the Rust and Pkl checkers, including pair, value, duplicate, and multiline rules, with fixtures and contract coverage for matching diagnostics. Update the completed plan task with the implementation evidence and verification results. Plan: `plan-pr-description`, T01. Co-authored-by: SCE --- cli/src/services/plan/mod.rs | 47 +++++++++++ cli/src/services/plan/validate.rs | 79 ++++++++++++++++++- .../fixtures/plan-structure/expectations.tsv | 5 ++ .../plan-structure/review-fields-duplicate.md | 58 ++++++++++++++ .../plan-structure/review-fields-malformed.md | 72 +++++++++++++++++ .../plan-structure/review-fields-misplaced.md | 51 ++++++++++++ .../review-fields-missing-pair.md | 54 +++++++++++++ .../plan-structure/review-fields-valid.md | 55 +++++++++++++ config/pkl/renderers/plan-structure.pkl | 70 +++++++++++++--- context/cli/plan-service.md | 9 ++- context/plans/plan-pr-description.md | 10 ++- context/sce/plan-structure-contract.md | 10 ++- 12 files changed, 503 insertions(+), 17 deletions(-) create mode 100644 config/pkl/renderers/fixtures/plan-structure/review-fields-duplicate.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/review-fields-malformed.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/review-fields-misplaced.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/review-fields-missing-pair.md create mode 100644 config/pkl/renderers/fixtures/plan-structure/review-fields-valid.md diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index 585235454..b4bf737bf 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -139,6 +139,53 @@ fn diagnostic_lines_match_source_positions() { "`Covers:` on `T02` is not a comma-separated ID list" )] ); + assert_eq!( + diagnostic_lines(&validate_fixture("review-fields-missing-pair.md")), + vec![ + (Some(9), "`D1` has `Review:` without `Review question:`"), + (Some(15), "`D2` has `Review question:` without `Review:`"), + ] + ); + assert_eq!( + diagnostic_lines(&validate_fixture("review-fields-malformed.md")), + vec![ + (Some(14), "`Review:` on `D1` must be `required`"), + (Some(22), "`Review question:` on `D2` is empty"), + (Some(29), "`Review question:` on `D3` must be a single line"), + (Some(36), "`Review:` on `D4` must be `required`"), + ] + ); + assert_eq!( + diagnostic_lines(&validate_fixture("review-fields-duplicate.md")), + vec![ + (Some(16), "`Review:` appears more than once on `D1`"), + ( + Some(24), + "`Review question:` appears more than once on `D2`" + ), + ] + ); + assert_eq!( + diagnostic_lines(&validate_fixture("review-fields-misplaced.md")), + vec![ + ( + Some(21), + "`Review:` on `AC1` is misplaced; it belongs to a design decision" + ), + ( + Some(22), + "`Review question:` on `AC1` is misplaced; it belongs to a design decision" + ), + ( + Some(45), + "`Review:` on `T01` is misplaced; it belongs to a design decision" + ), + ( + Some(46), + "`Review question:` on `T01` is misplaced; it belongs to a design decision" + ), + ] + ); assert_eq!( diagnostic_lines(&validate_source( "# Plan\n\n## Acceptance criteria\n\n### Verification scenarios\n" diff --git a/cli/src/services/plan/validate.rs b/cli/src/services/plan/validate.rs index 06bce444c..0b01244ad 100644 --- a/cli/src/services/plan/validate.rs +++ b/cli/src/services/plan/validate.rs @@ -5,6 +5,9 @@ use crate::services::plan::extract::{ const INSPECTION_ONLY: &str = "none (inspection)"; const VERIFICATION_SCENARIOS_HEADING: &str = "### Verification scenarios"; const ACCEPTANCE_CRITERIA_HEADING: &str = "## Acceptance criteria"; +const REVIEW_KEY: &str = "Review"; +const REVIEW_QUESTION_KEY: &str = "Review question"; +const REVIEW_REQUIRED: &str = "required"; const REQUIRED_PLAN_HEADINGS: [&str; 2] = [ACCEPTANCE_CRITERIA_HEADING, "## Task stack"]; const ORDERED_PLAN_HEADINGS: [&str; 4] = [ "## Design decisions", @@ -55,7 +58,9 @@ impl PlanEntryKind { fn field_owner(key: &str) -> Option { match key { - "Boundary" | "Rationale" | "Rejected" | "Trade-offs" => Some(PlanEntryKind::Decision), + "Boundary" | "Rationale" | "Rejected" | "Trade-offs" | REVIEW_KEY | REVIEW_QUESTION_KEY => { + Some(PlanEntryKind::Decision) + } "Validate" | "Decisions" | "Scenarios" => Some(PlanEntryKind::AcceptanceCriterion), "Given" | "When" | "Then" | "Check" => Some(PlanEntryKind::Scenario), "Covers" => Some(PlanEntryKind::Task), @@ -101,6 +106,7 @@ pub fn validate(plan: &Plan) -> PlanReport { .collect(); let mut diagnostics = heading_diagnostics(&headings); diagnostics.extend(definition_diagnostics(plan)); + diagnostics.extend(review_diagnostics(plan)); diagnostics.extend(reference_diagnostics(plan)); diagnostics.sort(); @@ -229,6 +235,77 @@ fn definition_diagnostics(plan: &Plan) -> Vec { diagnostics } +fn review_diagnostics(plan: &Plan) -> Vec { + let mut diagnostics = Vec::new(); + + for entry in plan.entries_of(PlanEntryKind::Decision) { + let fields_of = |key: &str| -> Vec<&PlanField> { + entry + .fields + .iter() + .filter(|field| field.key == key) + .collect() + }; + let reviews = fields_of(REVIEW_KEY); + let questions = fields_of(REVIEW_QUESTION_KEY); + + for (key, fields) in [(REVIEW_KEY, &reviews), (REVIEW_QUESTION_KEY, &questions)] { + if let Some(repeated) = fields.get(1) { + diagnostics.push(diagnostic( + Some(repeated.line), + format!("`{key}:` appears more than once on `{}`", entry.id), + )); + } + } + + for (present_key, present, missing_key, missing) in [ + (REVIEW_KEY, &reviews, REVIEW_QUESTION_KEY, &questions), + (REVIEW_QUESTION_KEY, &questions, REVIEW_KEY, &reviews), + ] { + if !present.is_empty() && missing.is_empty() { + diagnostics.push(diagnostic( + Some(entry.line), + format!( + "`{}` has `{present_key}:` without `{missing_key}:`", + entry.id + ), + )); + } + } + + for review in &reviews { + if review.value != REVIEW_REQUIRED { + diagnostics.push(diagnostic( + Some(review.line), + format!( + "`{REVIEW_KEY}:` on `{}` must be `{REVIEW_REQUIRED}`", + entry.id + ), + )); + } + } + + for question in &questions { + if question.value.is_empty() { + diagnostics.push(diagnostic( + Some(question.line), + format!("`{REVIEW_QUESTION_KEY}:` on `{}` is empty", entry.id), + )); + } else if question.value.contains('\n') { + diagnostics.push(diagnostic( + Some(question.line), + format!( + "`{REVIEW_QUESTION_KEY}:` on `{}` must be a single line", + entry.id + ), + )); + } + } + } + + diagnostics +} + fn undefined_hint(plan: &Plan, id: &str) -> String { if plan .outside_definitions diff --git a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv index 72dc97121..0e1092a57 100644 --- a/config/pkl/renderers/fixtures/plan-structure/expectations.tsv +++ b/config/pkl/renderers/fixtures/plan-structure/expectations.tsv @@ -6,6 +6,11 @@ keywords-only-in-examples.md fail `Scenarios:` on `AC1` references undefined `S1 misplaced-definition.md fail `S1` is defined under `## Acceptance criteria`; it belongs under `### Verification scenarios` `Scenarios:` on `AC1` references undefined `S1`; it appears only outside `### Verification scenarios` misplaced-field.md fail `Check:` on `D1` is misplaced; it belongs to a verification scenario `Covers:` on `AC1` is misplaced; it belongs to a task `S1` is missing `Check:` `T01` is missing `Covers:` `AC1` is not covered by any task's `Covers:` missing-scenarios-field.md fail `AC1` is missing `Scenarios:` `S1` is not referenced by any acceptance criterion's `Scenarios:` +review-fields-duplicate.md fail `Review:` appears more than once on `D1` `Review question:` appears more than once on `D2` +review-fields-malformed.md fail `Review:` on `D1` must be `required` `Review question:` on `D2` is empty `Review question:` on `D3` must be a single line `Review:` on `D4` must be `required` +review-fields-misplaced.md fail `Review:` on `AC1` is misplaced; it belongs to a design decision `Review question:` on `AC1` is misplaced; it belongs to a design decision `Review:` on `T01` is misplaced; it belongs to a design decision `Review question:` on `T01` is misplaced; it belongs to a design decision +review-fields-missing-pair.md fail `D1` has `Review:` without `Review question:` `D2` has `Review question:` without `Review:` +review-fields-valid.md pass task-empty-covers.md fail `Covers:` on `T02` is not a comma-separated ID list task-missing-covers.md fail `T02` is missing `Covers:` uncovered-ac.md fail `AC2` is not covered by any task's `Covers:` diff --git a/config/pkl/renderers/fixtures/plan-structure/review-fields-duplicate.md b/config/pkl/renderers/fixtures/plan-structure/review-fields-duplicate.md new file mode 100644 index 000000000..507247078 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/review-fields-duplicate.md @@ -0,0 +1,58 @@ +# Plan: review-fields-duplicate + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + - Review question: Is one store read per refresh acceptable? + - Review: required +- D2: Revoked tokens return 401. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + - Review question: Is a 401 status acceptable for a revoked token? + - Review question: Is a 401 status acceptable for every client? + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1, D2 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/review-fields-malformed.md b/config/pkl/renderers/fixtures/plan-structure/review-fields-malformed.md new file mode 100644 index 000000000..89bb316de --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/review-fields-malformed.md @@ -0,0 +1,72 @@ +# Plan: review-fields-malformed + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: optional + - Review question: Is one store read per refresh acceptable? +- D2: Revoked tokens return 401. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + - Review question: +- D3: Refresh responses are not cached. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + - Review question: Is disabling the cache acceptable + for every client? +- D4: Refresh tokens are opaque. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + and urgent + - Review question: Are opaque tokens acceptable? + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1, D2, D3, D4 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/review-fields-misplaced.md b/config/pkl/renderers/fixtures/plan-structure/review-fields-misplaced.md new file mode 100644 index 000000000..538a1d1ec --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/review-fields-misplaced.md @@ -0,0 +1,51 @@ +# Plan: review-fields-misplaced + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1 + - Scenarios: S1 + - Review: required + - Review question: Is a 401 status acceptable for a revoked token? + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Review: required + - Review question: Is the endpoint scope acceptable? + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/review-fields-missing-pair.md b/config/pkl/renderers/fixtures/plan-structure/review-fields-missing-pair.md new file mode 100644 index 000000000..ea3c3f791 --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/review-fields-missing-pair.md @@ -0,0 +1,54 @@ +# Plan: review-fields-missing-pair + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required +- D2: Revoked tokens return 401. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review question: Is a 401 status acceptable for a revoked token? + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1, D2 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/fixtures/plan-structure/review-fields-valid.md b/config/pkl/renderers/fixtures/plan-structure/review-fields-valid.md new file mode 100644 index 000000000..5abb0fcba --- /dev/null +++ b/config/pkl/renderers/fixtures/plan-structure/review-fields-valid.md @@ -0,0 +1,55 @@ +# Plan: review-fields-valid + +## Change summary + +Add a refresh endpoint. + +## Design decisions + +- D1: The session store validates refresh tokens. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + - Review: required + - Review question: Is one store read per refresh acceptable? + - Note that the store is shared with login. +- D2: Revoked tokens return 401. + - Boundary: `src/auth/` owns token validation. + - Rationale: The store already tracks revocation. + - Rejected: Stateless tokens — they cannot be revoked. + - Trade-offs: One store read per refresh. + +## Acceptance criteria + +- [ ] AC1: `POST /auth/refresh` rejects a revoked token with 401. + - Validate: `pnpm test src/auth/refresh.test.ts` + - Decisions: D1, D2 + - Scenarios: S1 + +### Verification scenarios + +- S1: Revoked refresh token is rejected + - Given: A revoked refresh token. + - When: The client calls `POST /auth/refresh`. + - Then: The response is 401. + - Check: `pnpm test src/auth/refresh.test.ts -t "rejects a revoked token"` + +### Full validation + +- `pnpm test` + +## Task stack + +- [ ] T01: `Add /auth/refresh endpoint` (status:todo) + - Task ID: T01 + - Covers: AC1 + - Scope: In — route handler. Out — rotation policy. + - Dependencies: none + - Done when: The endpoint returns 401 for a revoked token. + - Verify: `pnpm test src/auth/refresh.test.ts` + - Context synchronization: pending + +## Open questions + +None. diff --git a/config/pkl/renderers/plan-structure.pkl b/config/pkl/renderers/plan-structure.pkl index 7b262a55f..080c37f12 100644 --- a/config/pkl/renderers/plan-structure.pkl +++ b/config/pkl/renderers/plan-structure.pkl @@ -8,6 +8,7 @@ class PlanLine { class PlanField { key: String value: String + continued: List } class PlanEntry { @@ -26,9 +27,14 @@ local headingPattern = Regex(#"#{1,6}\s+\S.*"#) local definitionPattern = Regex(#"( *)-\s+(?:\[[ x]\]\s+)?("# + idPattern + #"):(?:\s.*)?"#) local fieldPattern = Regex(#"( *)-\s+([A-Z][A-Za-z -]*):(?:\s+(.*))?"#) local idListPattern = Regex(idPattern + "(?:, " + idPattern + ")*") +local blockStartPattern = Regex(#"\s*(?:[-*+](?:\s.*)?|[0-9]+[.)](?:\s.*)?|>.*|#{1,6}(?:\s.*)?)"#) local inspectionOnly = "none (inspection)" +local reviewKey = "Review" +local reviewQuestionKey = "Review question" +local reviewRequired = "required" + local authoritativeSections = Map( "D", "## Design decisions", "AC", "## Acceptance criteria", @@ -48,6 +54,8 @@ local fieldOwners = Map( "Rationale", "D", "Rejected", "D", "Trade-offs", "D", + "Review", "D", + "Review question", "D", "Validate", "AC", "Decisions", "AC", "Scenarios", "AC", @@ -149,6 +157,12 @@ function headingsOf(planLines: List): List = local function endsEntry(line: PlanLine, indent: Int): Boolean = !line.fenced && !line.text.isBlank && indentOf(line.text) <= indent +local function continuationOf(following: List): List = + following + .takeWhile((next) -> + !next.fenced && !next.text.isBlank && blockStartPattern.matchEntire(next.text) == null) + .map((next) -> next.text.trim()) + function entries(planLines: List): List = planLines .mapIndexed((index, line) -> @@ -163,17 +177,21 @@ function entries(planLines: List): List = new PlanEntry { id = entryId kind = entryKind - fields = planLines - .drop(index + 1) - .takeWhile((next) -> !endsEntry(next, indent)) - .filter((next) -> !next.fenced) - .map((next) -> fieldPattern.matchEntire(next.text)) - .filterNonNull() - .filter((field) -> field.groups[1]!!.value.length == indent + 2) - .map((field) -> new PlanField { - key = field.groups[2]!!.value - value = field.groups[3]?.value?.trim() ?? "" - }) + fields = + let (body = planLines.drop(index + 1)) + body + .takeWhile((next) -> !endsEntry(next, indent)) + .mapIndexed((offset, next) -> + let (field = if (next.fenced) null else fieldPattern.matchEntire(next.text)) + if (field == null || field.groups[1]!!.value.length != indent + 2) null + else + new PlanField { + key = field.groups[2]!!.value + value = field.groups[3]?.value?.trim() ?? "" + continued = continuationOf(body.drop(offset + 1)) + } + ) + .filterNonNull() } ) .filterNonNull() @@ -181,6 +199,35 @@ function entries(planLines: List): List = local function fieldOf(entry: PlanEntry, key: String): PlanField? = entry.fields.findOrNull((field) -> field.key == key) +local function fieldText(field: PlanField): String = + (List(field.value) + field.continued).join("\n").trim() + +local function reviewDefects(entry: PlanEntry): List = + let (reviews = entry.fields.filter((field) -> field.key == reviewKey)) + let (questions = entry.fields.filter((field) -> field.key == reviewQuestionKey)) + List(Pair(reviewKey, reviews), Pair(reviewQuestionKey, questions)) + .filter((pair) -> pair.second.length > 1) + .map((pair) -> "`\(pair.first):` appears more than once on `\(entry.id)`") + + ( + if (!reviews.isEmpty && questions.isEmpty) + List("`\(entry.id)` has `\(reviewKey):` without `\(reviewQuestionKey):`") + else List() + ) + + ( + if (!questions.isEmpty && reviews.isEmpty) + List("`\(entry.id)` has `\(reviewQuestionKey):` without `\(reviewKey):`") + else List() + ) + + reviews + .filter((field) -> fieldText(field) != reviewRequired) + .map((field) -> "`\(reviewKey):` on `\(entry.id)` must be `\(reviewRequired)`") + + questions + .map((field) -> fieldText(field)) + .filter((text) -> text.isEmpty || text.contains("\n")) + .map((text) -> + if (text.isEmpty) "`\(reviewQuestionKey):` on `\(entry.id)` is empty" + else "`\(reviewQuestionKey):` on `\(entry.id)` must be a single line") + local function listedIds(value: String): List = if (idListPattern.matchEntire(value) == null) List() else value.split(", ") @@ -221,6 +268,7 @@ local function definitionDefects(defined: List): List = + requiredFields[entry.kind] .filter((key) -> fieldOf(entry, key) == null) .map((key) -> "`\(entry.id)` is missing `\(key):`") + + (if (entry.kind == "D") reviewDefects(entry) else List()) ) local function misplacedDefinitionDefects(planLines: List): List = diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index ffc2aee47..5b5c17bbd 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -36,8 +36,13 @@ - The nearest preceding depth-1 or depth-2 heading of `### Verification scenarios` must be `## Acceptance criteria`. - Definition rules: - IDs are unique. - - Owned fields appear only on their owning kind: `Boundary`/`Rationale`/`Rejected`/`Trade-offs` → D, `Validate`/`Decisions`/`Scenarios` → AC, `Given`/`When`/`Then`/`Check` → S, `Covers` → T. + - Owned fields appear only on their owning kind: `Boundary`/`Rationale`/`Rejected`/`Trade-offs`/`Review`/`Review question` → D, `Validate`/`Decisions`/`Scenarios` → AC, `Given`/`When`/`Then`/`Check` → S, `Covers` → T. - Every definition carries its kind's required fields. +- Review pair rules, per decision: + - `Review:` and `Review question:` are optional, but each requires the other. + - Every `Review:` value must be `required`. + - Every `Review question:` value must be nonempty and must not contain a newline. The source-sliced value keeps lazy continuation lines, so a wrapped question fails. + - A repeated `Review:` or `Review question:` key is reported once. - Reference rules: - `Decisions:`, `Scenarios:`, and `Covers:` on their owning kind must be an exact `ID, ID` list. `Scenarios: none (inspection)` is exempt. - Each listed ID must name the right kind and resolve to a definition. @@ -52,6 +57,7 @@ - The parent-rule defect points at `### Verification scenarios`. - Misplaced and reference defects point at their field. - Missing-field, uncovered-AC, and unreferenced-S defects point at their definition. + - A missing review-pair half points at its decision. A review value, empty, or single-line defect points at its field. A repeated review key points at its second occurrence. ## Command contract @@ -80,6 +86,7 @@ - `plan-structure expectations manifest`: `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, the single source of fixture verdicts for both the Pkl checker (driven by `config/pkl/check-generated.sh`) and the Rust validator. Each row is tab-separated: fixture file, `pass` or `fail`, then the complete expected defect messages for `fail` rows, compared as an exact order-insensitive multiset. Every `*.md` fixture in the directory must be listed exactly once. - `sce plan init`: The CLI command that publishes the plan init scaffold as `context/plans/.md` at the repository root through the [init command contract](#init-command-contract). It never overwrites an existing plan. - `sce plan check`: The read-only CLI command that runs `extract` then `validate` on one plan file and reports through the exit `0`/`3`/`4` contract above. +- `review pair`: The optional `Review: required` and one-line `Review question:` fields on a design decision. They mark a decision that needs senior engineering judgment. - `plan diagnostic`: One `PlanDiagnostic` from `validate`, with Pkl defect wording and an optional 1-based source line. - `plan init scaffold`: The generated `config/plan-init-scaffold.md` plan skeleton rendered by `config/pkl/base/plan-init-scaffold.pkl` and embedded as `SCAFFOLD`. Untouched, it fails `sce plan check` with exactly its `Scenarios: TODO` and `Covers: TODO` reference defects (see the [plan structure contract](../sce/plan-structure-contract.md)). diff --git a/context/plans/plan-pr-description.md b/context/plans/plan-pr-description.md index cc4065dd2..e95fec228 100644 --- a/context/plans/plan-pr-description.md +++ b/context/plans/plan-pr-description.md @@ -194,14 +194,20 @@ Persist this field in every plan; this is durable plan state, not chat state: ## Task stack -- [ ] T01: `Validate optional review fields in both plan checkers` (status:todo) +- [x] T01: `Validate optional review fields in both plan checkers` (status:done) - Task ID: T01 - Covers: AC1, AC2 - Scope: In — `Review` and `Review question` as decision-owned fields in `validate.rs` and `plan-structure.pkl`, the pair, value, empty, single-line, and duplicate rules with the S2 messages, the five `review-fields-*.md` fixtures and their `expectations.tsv` rows, and extended line assertions in `plan::diagnostic_lines_match_source_positions`. Out — template text, the new command, and edits to existing plans. - Dependencies: none - Done when: Both checkers agree on every manifest row, the new fixtures cover S1, S2, and S3, and every existing `pass` fixture still passes. - Verify: `nix run .#pkl-check-generated`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::fixture`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml plan::diagnostic_lines_match_source_positions`. - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `cli/src/services/plan/validate.rs`, `cli/src/services/plan/mod.rs`, `config/pkl/renderers/plan-structure.pkl`, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv`, `config/pkl/renderers/fixtures/plan-structure/review-fields-valid.md`, `config/pkl/renderers/fixtures/plan-structure/review-fields-missing-pair.md`, `config/pkl/renderers/fixtures/plan-structure/review-fields-malformed.md`, `config/pkl/renderers/fixtures/plan-structure/review-fields-duplicate.md`, `config/pkl/renderers/fixtures/plan-structure/review-fields-misplaced.md` + - Result: Both checkers treat `Review` and `Review question` as decision-owned fields, so the existing misplaced rule covers acceptance criteria and tasks. A new per-decision review rule set reports the duplicate key once at its second occurrence, a missing pair half at the decision line, and a non-`required` value, an empty question, and a multi-line question at the field line. The value, empty, and single-line rules run on every occurrence. Rust detects multi-line values through the newline that the extracted lead paragraph keeps. Pkl records each field's lazy continuation lines (until a blank, fenced, heading, list-item, or block-quote line), joins them with the field value, and applies the same three predicates. `extract.rs` is unchanged. Five new fixtures have manifest rows: `review-fields-valid.md` passes (it includes a nested list under a question, which is not a continuation). `review-fields-malformed.md` also covers a multi-line `Review:` value. + - Verify outcomes: `nix run .#pkl-check-generated` passed (146 files; every manifest row matched for Pkl). `plan::fixture` passed (`fixture_expectations_match_manifest`). `plan::diagnostic_lines_match_source_positions` passed. Extra checks: all 28 `plan::` tests passed, `run-cli-cargo.sh clippy --all-targets -- -D warnings` was clean, and `nix run .#sce -- plan check` exited `0` on `plan-decisions-and-verification-scenarios.md`, `sce-plan-check.md`, and `plan-pr-description.md` (S4). + - Deviations: The S2 messages are written with `D1`. The missing-pair and duplicate fixtures need two decisions, so their second messages name `D2`. + - Context impact: durable — the plan structure contract gains the review pair rules, six new defect messages, and five fixtures (`context/sce/plan-structure-contract.md`, `context/cli/plan-service.md`). + - Context synchronization: synced - [ ] T02: `Document the review pair in the plan template and authoring rules` (status:todo) - Task ID: T02 diff --git a/context/sce/plan-structure-contract.md b/context/sce/plan-structure-contract.md index f31c53818..39a6e2147 100644 --- a/context/sce/plan-structure-contract.md +++ b/context/sce/plan-structure-contract.md @@ -20,8 +20,14 @@ Both checkers apply the same rules to the plan source outside fenced code blocks - `D`, `AC`, `S`, and `T` entries count only in their authoritative section: `## Design decisions`, `## Acceptance criteria`, `### Verification scenarios`, and `## Task stack`, each ending at the next heading. - IDs are unique. - A top-level list item that defines an ID under another authoritative section is reported at its own line as defined under the wrong section. Nested items, prose outside authoritative sections, and fenced examples are not. - - Owned fields appear only on their owning kind. The `D` fields are `Boundary`, `Rationale`, `Rejected`, and `Trade-offs`. The `AC` fields are `Validate`, `Decisions`, and `Scenarios`. The `S` fields are `Given`, `When`, `Then`, and `Check`. The `T` field is `Covers`. + - Owned fields appear only on their owning kind. The `D` fields are `Boundary`, `Rationale`, `Rejected`, `Trade-offs`, `Review`, and `Review question`. The `AC` fields are `Validate`, `Decisions`, and `Scenarios`. The `S` fields are `Given`, `When`, `Then`, and `Check`. The `T` field is `Covers`. - Each definition carries its kind's required fields. +- **Review pair.** `Review:` and `Review question:` are optional on a `D` entry, but they appear together or not at all. + - `Review:` must be exactly `required`. + - `Review question:` must hold one nonempty line of text. + - A key that appears more than once on one decision is reported once. + - The defects are `` `D1` has `Review:` without `Review question:` ``, `` `D1` has `Review question:` without `Review:` ``, `` `Review:` on `D1` must be `required` ``, `` `Review question:` on `D1` is empty ``, `` `Review question:` on `D1` must be a single line ``, and `` `Review:` appears more than once on `D1` ``. + - The `review-fields-*.md` fixtures pin these rules. A misplaced review field uses the ordinary misplaced-field defect. - **References.** - `Decisions:`, `Scenarios:`, and `Covers:` hold an exact `ID, ID` list. `Scenarios: none (inspection)` is exempt. - Each listed ID names the right kind and resolves to a definition. @@ -63,7 +69,7 @@ The Rust extractor follows the CommonMark/GFM `mdast` tree from `markdown-rs`. T - **List-nesting indentation.** Pkl takes a field only at exactly `entry indent + 2` spaces. Rust takes any item of a list nested directly under the definition, so other indentation that CommonMark still nests is accepted. - **Checkbox case.** Pkl matches only `[ ]` and `[x]`. With `[X]`, the line is not a definition for Pkl, while GFM accepts `[X]` as checked. - **Nested definitions.** Pkl accepts a definition line at any indentation inside its section, including one nested in another list item. Rust takes definitions only from items of root-level lists. -- **Multi-line values.** Pkl reads a field value from its first line only. Rust slices the field item's whole first paragraph from source, so a wrapped `Covers:`, `Decisions:`, or `Scenarios:` list keeps its continuation lines. A wrapped list then fails the ID-list grammar in Rust, but may pass in Pkl. +- **Multi-line values.** Rust slices the field item's whole first paragraph from source, so a wrapped value keeps its continuation lines. Pkl reads a reference value from its first line only. A wrapped `Covers:`, `Decisions:`, or `Scenarios:` list then fails the ID-list grammar in Rust, but can pass in Pkl. The review-pair rules are aligned: Pkl joins a review field's lazy continuation lines, which end at a blank, fenced, heading, list-item, or block-quote line. A setext underline or other block start that CommonMark recognizes can still differ. - **List markers.** Pkl accepts only `-` bullets. Rust accepts any GFM list (`-`, `*`, `+`, or ordered). - **Indented code blocks.** Pkl recognizes only backtick and tilde fences. Rust also ignores indented (four-space) code blocks. From 1dde751969cdb296d238215e80db9ef9ac751e97 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 19:56:25 +0200 Subject: [PATCH 29/61] plans: Document optional review pair in plan authoring rules Document the optional senior-engineering review pair in the generated plan template and authoring rules, and mirror it in the shared workflow context. Marked decisions require `Review: required` plus one nonempty question; unmarked decisions omit both fields. Plan: `plan-pr-description`, T02. Co-authored-by: SCE --- config/pkl/base/workflow-change-to-plan.pkl | 16 ++++++++++++++-- context/context-map.md | 2 +- context/plans/plan-pr-description.md | 9 +++++++-- context/sce/shared-context-plan-workflow.md | 5 ++++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/config/pkl/base/workflow-change-to-plan.pkl b/config/pkl/base/workflow-change-to-plan.pkl index 3baf839e1..05cd97729 100644 --- a/config/pkl/base/workflow-change-to-plan.pkl +++ b/config/pkl/base/workflow-change-to-plan.pkl @@ -617,6 +617,8 @@ the section when there are none.} - Rationale: {why} - Rejected: {alternative} — {why not} - Trade-offs: {known limitation or cost} + - Review: required + - Review question: {optional; one question for senior engineering judgment; remove both review lines when none is needed} ## Acceptance criteria @@ -777,6 +779,15 @@ invent one: `None.` is the expected answer for a well-specified change.} alternative a reviewer would otherwise reopen. - Every decision carries `Boundary:`, `Rationale:`, `Rejected:`, and `Trade-offs:`. +- A decision may also carry the optional review pair `Review: required` and + `Review question: {one question}`. Mark only a decision that needs senior + engineering judgment before the change merges, such as a trade-off a reviewer + must accept. Leave every other decision unmarked. +- The review pair is all or nothing: a decision carries both fields or + neither. `Review:` is exactly `required`, and `Review question:` is one + nonempty line that states one question. +- An unmarked decision carries no review field. Do not add a placeholder value + such as `Review: none` or an empty `Review question:`. - A design decision belongs to this plan only. It does not replace an architecture decision record. @@ -818,8 +829,9 @@ invent one: `None.` is the expected answer for a well-specified change.} `## Task stack`. A line shaped like a definition anywhere else, such as in a fenced example or a validation report, defines nothing. - A definition's fields are the `- Field:` lines indented one level under its - entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, and - `Trade-offs:` belong to a design decision; `Validate:`, `Decisions:`, and + entry line, up to the next entry. `Boundary:`, `Rationale:`, `Rejected:`, + `Trade-offs:`, and the optional `Review:` and `Review question:` belong to a + design decision; `Validate:`, `Decisions:`, and `Scenarios:` to an acceptance criterion; `Given:`, `When:`, `Then:`, and `Check:` to a verification scenario; and `Covers:` to a task. - To check a fenced example itself, extract the block's content and apply these diff --git a/context/context-map.md b/context/context-map.md index 53a82c6a6..4756bb13f 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -59,7 +59,7 @@ Feature/domain context: - `context/sce/claude-raw-hook-capture.md` (removed feature: the former `sce hooks claude-capture` raw-capture route and its supporting types, replaced by the active `diff-trace` and `conversation-trace` intakes) - `context/sce/cli-observability-contract.md` (implemented config-backed runtime observability contract for the flat logging config-file shape with explicit config-file/default `log_to_file`, `log_dir` / `SCE_LOG_DIR` env-over-config-over-`/sce/logs` fallback, append-only local-date/session log file routing with a one-time complete-record `-v2.log` fallback on primary open/append/flush failure, creation-triggered retention of direct regular `*.log` files to 10 entries, reliable producer-native diff-trace/conversation-trace session routing and hook-specific non-duplicated Agent Trace DB-open error events, deterministic session filename sanitization, concrete `Logger` behavior plus the `NoopTelemetry`-backed telemetry capability boundary, logger and deliberately non-object-safe generic async telemetry boundaries with static capability dispatch and future-aware polling scope, AppContext observability wiring, generic `RunOutcome` final rendering, runtime-classified repeated telemetry action protection, operator-facing `sce config show` observability reporting, managed OTLP trace/log export (credential-gated, hardcoded canonical collector, allowlisted fields, bounded fail-open lifecycle, reserved metrics endpoint), and the trimmed `sce config validate` status-only validation surface) - `context/sce/shared-context-code-workflow.md` (canonical `/next-task` task-synchronization lifecycle and validation-only `/validate` lifecycle, package-local phase references with single-skill control flow, and the task-synchronization-scoped `sce-decision` sibling invocation with ADR reuse/blocker propagation) -- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions, verification scenarios, coverage reference fields, no legacy plan format), `sce plan init` new-plan creation with the completeness and mandatory `sce plan check` gates and no fallback, clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) +- `context/sce/shared-context-plan-workflow.md` (canonical `/change-to-plan` workflow, package-local context-load/plan-authoring/template references, the structured plan format (design decisions with the optional review pair, verification scenarios, coverage reference fields, no legacy plan format), `sce plan init` new-plan creation with the completeness and mandatory `sce plan check` gates and no fallback, clarification/readiness gate contract, and one-task/one-atomic-commit task slicing) - [Plan structure contract](sce/plan-structure-contract.md) (shared Pkl/Rust plan-structure checker contract: shared rule set, `config/pkl/renderers/fixtures/plan-structure/expectations.tsv` as the single fixture source of truth and how to add a fixture, `sce plan check` result and diagnostic wording shared with the Pkl defect strings, the both-checkers rewording rule, known AST-versus-line-scanner divergences, and Pkl staying generation-only while the CLI never invokes Pkl) - [Context workflow rules](sce/context-workflow-rules.md) (canonical bootstrap, ongoing context maintenance, task synchronization, hygiene, discoverability, and feature-existence rules) - `context/sce/plan-code-overlap-map.md` (overlap matrix for thin OpenCode Plan/Code routing agents and six workflow packages, including task-only context synchronization ownership) diff --git a/context/plans/plan-pr-description.md b/context/plans/plan-pr-description.md index e95fec228..b26f43974 100644 --- a/context/plans/plan-pr-description.md +++ b/context/plans/plan-pr-description.md @@ -209,14 +209,19 @@ Persist this field in every plan; this is durable plan state, not chat state: - Context impact: durable — the plan structure contract gains the review pair rules, six new defect messages, and five fixtures (`context/sce/plan-structure-contract.md`, `context/cli/plan-service.md`). - Context synchronization: synced -- [ ] T02: `Document the review pair in the plan template and authoring rules` (status:todo) +- [x] T02: `Document the review pair in the plan template and authoring rules` (status:done) - Task ID: T02 - Covers: AC3 - Scope: In — the decision template entry, `Design decision rules`, and `Reference rules` field list in `config/pkl/base/workflow-change-to-plan.pkl`, with a rule to mark only decisions that need senior engineering judgment and to add no placeholder value. Out — the plan init scaffold content, `/next-task`, and `/validate`. - Dependencies: T01 - Done when: The generated `plan-template.md` shows the optional pair and the marking rule, and the skeleton and generation contract checks still pass. - Verify: `nix run .#pkl-check-generated`. - - Context synchronization: pending + - Completed: 2026-10-24 + - Files changed: `config/pkl/base/workflow-change-to-plan.pkl` + - Result: The structured plan template (`changeToPlanPlanTemplate`) has two changes. First, its D1 entry shows the optional lines `- Review: required` and a single-line `- Review question:` placeholder, which says to remove both lines when no review is needed. Second, the `Design decision rules` gain three bullets: mark only a decision that needs senior engineering judgment; the pair is all or nothing, with `Review:` exactly `required` and a nonempty one-line question; and an unmarked decision carries no review field and no placeholder value such as `Review: none`. The `Reference rules` field list now names `Review:` and `Review question:` as fields that belong to a design decision. The compact package template and the plan init scaffold are unchanged. + - Verify outcomes: `nix run .#pkl-check-generated` passed (146 files). This run includes the plan-template structure, structured schema, and plan-init scaffold contract checks. An ephemeral `pkl-generate` into the scratchpad confirmed that the generated `sce-change-to-plan/references/plan-template.md` contains the review pair and the marking rules. + - Context impact: durable — the plan workflow's authoring rules now include the optional review pair and its marking rule (`context/sce/shared-context-plan-workflow.md`). + - Context synchronization: synced - [ ] T03: `Add the sce plan pr-description command` (status:todo) - Task ID: T03 diff --git a/context/sce/shared-context-plan-workflow.md b/context/sce/shared-context-plan-workflow.md index a9ec1d6d3..50e1cd6f6 100644 --- a/context/sce/shared-context-plan-workflow.md +++ b/context/sce/shared-context-plan-workflow.md @@ -52,7 +52,10 @@ task, completion, and synchronization-lifecycle fields, it defines an optional structured layer: - `## Design decisions` with `D1..Dn` entries, each carrying `Boundary:`, - `Rationale:`, `Rejected:`, and `Trade-offs:`. + `Rationale:`, `Rejected:`, and `Trade-offs:`. A decision can also carry the + optional review pair `Review: required` and a one-line `Review question:`. + The planning agent marks only a decision that needs senior engineering + judgment. An unmarked decision carries neither field and no placeholder value. - One-line reference fields: `Decisions:` and `Scenarios:` on each acceptance criterion, and `Covers:` on each task. - `### Verification scenarios` with `S1..Sn` entries, each carrying `Given:`, From cc8a30f565ca580d65d32e83897131ea0d09a199 Mon Sep 17 00:00:00 2001 From: David Abram Date: Sat, 24 Oct 2026 20:10:00 +0200 Subject: [PATCH 30/61] plan: Add offline PR intent review briefs Add `sce plan pr-description` as a synchronous, offline command that validates plans, collects marked review decisions, and renders linked Markdown briefs with strict argument validation and escaping. Share repository-root and plan validation paths with the existing plan commands, and document the command contract and completed implementation task. Plan: `plan-pr-description`, T03. Co-authored-by: SCE --- cli/src/cli_schema.rs | 37 +- cli/src/command_surface.rs | 4 + cli/src/services/parse/command_runtime.rs | 5 + cli/src/services/plan/command.rs | 46 ++- cli/src/services/plan/init.rs | 9 +- cli/src/services/plan/mod.rs | 409 ++++++++++++++++++++++ cli/src/services/plan/pr_description.rs | 221 ++++++++++++ context/architecture.md | 2 +- context/cli/cli-command-surface.md | 2 +- context/cli/plan-service.md | 29 +- context/context-map.md | 2 +- context/glossary.md | 2 +- context/overview.md | 2 +- context/plans/plan-pr-description.md | 10 +- 14 files changed, 756 insertions(+), 24 deletions(-) create mode 100644 cli/src/services/plan/pr_description.rs diff --git a/cli/src/cli_schema.rs b/cli/src/cli_schema.rs index 11d3544dd..6b56c2045 100644 --- a/cli/src/cli_schema.rs +++ b/cli/src/cli_schema.rs @@ -54,8 +54,9 @@ pub const SYNC_TOP_LEVEL_PURPOSE: &str = pub const SYNC_SHOW_IN_TOP_LEVEL_HELP: bool = true; pub const PLAN_CLAP_ABOUT: &str = - "Create an SCE plan scaffold or check the structure and reference integrity of one SCE plan"; -pub const PLAN_TOP_LEVEL_PURPOSE: &str = "Create or check the structure of one SCE plan"; + "Create an SCE plan scaffold, check the structure and reference integrity of one SCE plan, or print an intent-review brief"; +pub const PLAN_TOP_LEVEL_PURPOSE: &str = + "Create or check the structure of one SCE plan, or print an intent-review brief"; pub const PLAN_SHOW_IN_TOP_LEVEL_HELP: bool = true; pub const TOP_LEVEL_COMMANDS: &[TopLevelCommandMetadata] = &[ @@ -381,7 +382,7 @@ pub enum HooksSubcommand { pub enum PlanSubcommand { #[command(about = "Validate one plan file without executing any of its commands")] Check { - /// Path to the plan Markdown file to check + #[arg(help = "Path to the plan Markdown file to check")] path: PathBuf, }, @@ -389,9 +390,37 @@ pub enum PlanSubcommand { about = "Create context/plans/.md at the Git repository root from the plan scaffold" )] Init { - /// Plan name: lowercase letters and digits separated by single hyphens, without `.md` + #[arg( + help = "Plan name: lowercase letters and digits separated by single hyphens, without `.md`" + )] name: String, }, + + #[command( + name = "pr-description", + about = "Print a Markdown intent-review brief for the given plans, offline and without writing files" + )] + PrDescription { + #[arg( + long, + value_name = "OWNER/NAME", + help = "GitHub repository as /" + )] + repo: String, + + #[arg( + long, + value_name = "SHA", + help = "Full 40-character lowercase commit SHA that contains the plan files" + )] + head: String, + + #[arg( + required = true, + help = "Plan paths relative to the Git repository root, each context/plans/.md" + )] + paths: Vec, + }, } #[derive(Subcommand, Debug, Clone, PartialEq, Eq)] diff --git a/cli/src/command_surface.rs b/cli/src/command_surface.rs index 6343b68c6..6396cc9e2 100644 --- a/cli/src/command_surface.rs +++ b/cli/src/command_surface.rs @@ -83,6 +83,10 @@ const HELP_SECTIONS: &[HelpSection] = &[ cmd: " sce plan", suffix: " init ", }, + HelpSectionBodyLine::Command { + cmd: " sce plan", + suffix: " pr-description --repo --head ...", + }, ], }, HelpSection { diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 0726b323d..834a22e18 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -344,6 +344,11 @@ fn convert_plan_subcommand(subcommand: cli_schema::PlanSubcommand) -> RuntimeCom name, }) } + cli_schema::PlanSubcommand::PrDescription { repo, head, paths } => { + services::plan::command::PlanRequest::PrDescription( + services::plan::command::PlanPrDescriptionRequest { repo, head, paths }, + ) + } }; RuntimeCommand::Plan(services::plan::command::PlanCommand { request }) diff --git a/cli/src/services/plan/command.rs b/cli/src/services/plan/command.rs index 76931f373..ab27acd0e 100644 --- a/cli/src/services/plan/command.rs +++ b/cli/src/services/plan/command.rs @@ -4,11 +4,13 @@ use std::path::{Path, PathBuf}; use anyhow::Context; use crate::services::error::CliError; -use crate::services::plan::extract::extract; -use crate::services::plan::init::{create, PlanName}; +use crate::services::plan::extract::{extract, Plan}; +use crate::services::plan::init::{create, resolve_repository_root, PlanName}; +use crate::services::plan::pr_description::{parse_arguments, render, PlanInput}; use crate::services::plan::validate::{validate, PlanDiagnostic}; pub const STRUCTURED_PLAN_PASS: &str = "structured plan: reference integrity holds"; +const PR_DESCRIPTION_COMMAND: &str = "sce plan pr-description"; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PlanCheckRequest { @@ -20,10 +22,18 @@ pub struct PlanInitRequest { pub name: String, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanPrDescriptionRequest { + pub repo: String, + pub head: String, + pub paths: Vec, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum PlanRequest { Check(PlanCheckRequest), Init(PlanInitRequest), + PrDescription(PlanPrDescriptionRequest), } pub struct PlanCommand { @@ -49,11 +59,39 @@ impl PlanCommand { .map_err(CliError::runtime)?; create(&name, &start, stderr) } + PlanRequest::PrDescription(request) => pr_description(request, current_dir), } } } fn check(path: &Path) -> Result { + load_valid_plan(path, &path.display().to_string())?; + Ok(STRUCTURED_PLAN_PASS.to_string()) +} + +fn pr_description( + request: &PlanPrDescriptionRequest, + current_dir: impl FnOnce() -> std::io::Result, +) -> Result { + let arguments = parse_arguments(&request.repo, &request.head, &request.paths)?; + let start = current_dir() + .context("Failed to read the current directory") + .map_err(CliError::runtime)?; + let root = resolve_repository_root(&start, PR_DESCRIPTION_COMMAND)?; + let plans = arguments + .paths + .iter() + .map(|path| { + Ok(PlanInput { + path: path.clone(), + plan: load_valid_plan(&Path::new(&root).join(path), path)?, + }) + }) + .collect::, CliError>>()?; + render(&arguments, &plans) +} + +fn load_valid_plan(path: &Path, label: &str) -> Result { let source = std::fs::read_to_string(path) .with_context(|| format!("Failed to read plan file '{}'", path.display())) .map_err(CliError::runtime)?; @@ -64,12 +102,12 @@ fn check(path: &Path) -> Result { if !report.diagnostics.is_empty() { return Err(CliError::validation(render_diagnostics( - &path.display().to_string(), + label, &report.diagnostics, ))); } - Ok(STRUCTURED_PLAN_PASS.to_string()) + Ok(plan) } fn render_diagnostics(path: &str, diagnostics: &[PlanDiagnostic]) -> String { diff --git a/cli/src/services/plan/init.rs b/cli/src/services/plan/init.rs index 1be6a3747..15ba2b0d1 100644 --- a/cli/src/services/plan/init.rs +++ b/cli/src/services/plan/init.rs @@ -13,6 +13,7 @@ pub const PLANS_RELATIVE_DIR: &str = "context/plans"; const PLAN_EXTENSION: &str = ".md"; const CREATED_PREFIX: &str = "Created plan: "; const NEXT_LINE: &str = "Next: complete the plan and run"; +const INIT_COMMAND: &str = "sce plan init"; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PlanName(String); @@ -75,7 +76,7 @@ pub(super) fn create_with( link: impl FnOnce(&Path, &Path) -> std::io::Result<()>, remove: impl FnOnce(&Path) -> std::io::Result<()>, ) -> Result { - let root = resolve_repository_root(start)?; + let root = resolve_repository_root(start, INIT_COMMAND)?; let context_dir = Path::new(&root).join("context"); let plans_dir = Path::new(&root).join(PLANS_RELATIVE_DIR); ensure_directory(&context_dir)?; @@ -104,16 +105,16 @@ pub(super) fn create_with( .join("\n")) } -fn resolve_repository_root(start: &Path) -> Result { +pub(super) fn resolve_repository_root(start: &Path, command: &str) -> Result { let output = Command::new("git") .args(["rev-parse", "--show-toplevel"]) .current_dir(start) .output() - .context("Failed to run 'git rev-parse --show-toplevel'. Try: install Git and rerun 'sce plan init'.") + .with_context(|| format!("Failed to run 'git rev-parse --show-toplevel'. Try: install Git and rerun '{command}'.")) .map_err(CliError::runtime)?; if !output.status.success() { return Err(CliError::runtime(anyhow!( - "Not inside a Git repository: '{}' ({}). Try: run 'sce plan init' inside a Git repository.", + "Not inside a Git repository: '{}' ({}). Try: run '{command}' inside a Git repository.", start.display(), String::from_utf8_lossy(&output.stderr).trim() ))); diff --git a/cli/src/services/plan/mod.rs b/cli/src/services/plan/mod.rs index b4bf737bf..14cb273f0 100644 --- a/cli/src/services/plan/mod.rs +++ b/cli/src/services/plan/mod.rs @@ -1,6 +1,7 @@ pub mod command; pub mod extract; pub mod init; +pub mod pr_description; pub mod validate; pub const NAME: &str = "plan"; @@ -979,3 +980,411 @@ fn completion_scripts_include_plan_init() { ); } } + +#[cfg(test)] +const PR_REPO: &str = "crocoder-dev/shared-context-engineering"; + +#[cfg(test)] +const PR_HEAD: &str = "0123456789abcdef0123456789abcdef01234567"; + +#[cfg(test)] +fn review_plan_source(name: &str, questions: [Option<&str>; 3]) -> String { + use std::fmt::Write; + + let mut source = format!("# Plan: {name}\n\n## Design decisions\n\n"); + for (index, question) in questions.iter().enumerate() { + write!( + source, + "- D{}: Decision {}.\n - Boundary: One module.\n - Rationale: It is simple.\n - Rejected: Two modules — they split ownership.\n - Trade-offs: None.\n", + index + 1, + index + 1 + ) + .expect("writing to String should not fail"); + if let Some(question) = question { + write!( + source, + " - Review: required\n - Review question: {question}\n" + ) + .expect("writing to String should not fail"); + } + } + source.push_str( + "\n## Acceptance criteria\n\n- [ ] AC1: It works.\n - Validate: `true`\n - Scenarios: none (inspection)\n\n## Task stack\n\n- [ ] T01: `Do it` (status:todo)\n - Task ID: T01\n - Covers: AC1\n", + ); + source +} + +#[cfg(test)] +fn pr_plan_input(path: &str, source: &str) -> pr_description::PlanInput { + let report = validate_source(source); + assert!(report.diagnostics.is_empty(), "{:?}", report.diagnostics); + pr_description::PlanInput { + path: path.to_string(), + plan: extract::extract(source).expect("plan should parse"), + } +} + +#[cfg(test)] +fn pr_arguments(paths: &[&str]) -> pr_description::PrDescriptionArguments { + pr_description::PrDescriptionArguments { + repo: PR_REPO.to_string(), + head: PR_HEAD.to_string(), + paths: paths.iter().map(ToString::to_string).collect(), + } +} + +#[cfg(test)] +fn write_repository_plan(root: &std::path::Path, path: &str, source: &str) { + let destination = root.join(path); + std::fs::create_dir_all( + destination + .parent() + .expect("plan path should have a parent"), + ) + .expect("plans directory should be created"); + std::fs::write(destination, source).expect("plan should be written"); +} + +#[cfg(test)] +fn run_pr_description(root: &std::path::Path, paths: &[&str]) -> CommandRun { + let mut args = vec![NAME, "pr-description", "--repo", PR_REPO, "--head", PR_HEAD]; + args.extend_from_slice(paths); + run_plan_args(&args, root) +} + +#[test] +fn pr_description_golden_multiple_plans() { + let zeta = pr_plan_input( + "context/plans/zeta_notes.md", + &review_plan_source( + "zeta_notes", + [None, Some("Is the zeta store shared?"), None], + ), + ); + let alpha = pr_plan_input( + "context/plans/alpha-plan.md", + &review_plan_source( + "alpha-plan", + [Some("Is A acceptable?"), Some("Is B acceptable?"), None], + ), + ); + let arguments = pr_arguments(&["context/plans/zeta_notes.md", "context/plans/alpha-plan.md"]); + + let output = pr_description::render(&arguments, &[zeta, alpha]).expect("brief should render"); + + let blob = "https://github.com/crocoder-dev/shared-context-engineering/blob/0123456789abcdef0123456789abcdef01234567"; + let expected = format!( + "\n\ +## Intent review\n\ +\n\ +**Plans:** [alpha-plan]({blob}/context/plans/alpha-plan.md), [zeta\\_notes]({blob}/context/plans/zeta_notes.md)\n\ +\n\ +**Decisions requiring review**\n\ +\n\ +1. Is A acceptable? — [alpha-plan D1]({blob}/context/plans/alpha-plan.md#L5)\n\ +\n\ +2. Is B acceptable? — [alpha-plan D2]({blob}/context/plans/alpha-plan.md#L12)\n\ +\n\ +3. Is the zeta store shared? — [zeta\\_notes D2]({blob}/context/plans/zeta_notes.md#L10)\n\ +\n\ +" + ); + assert_eq!(output, expected); +} + +#[test] +fn pr_description_no_flagged_decisions() { + let plan = pr_plan_input( + "context/plans/quiet.md", + &review_plan_source("quiet", [None, None, None]), + ); + + let output = pr_description::render(&pr_arguments(&["context/plans/quiet.md"]), &[plan]) + .expect("brief should render"); + + assert_eq!( + output, + "\n## Intent review\n\n**Plans:** [quiet](https://github.com/crocoder-dev/shared-context-engineering/blob/0123456789abcdef0123456789abcdef01234567/context/plans/quiet.md)\n\nNo decisions explicitly flagged for review.\n\n" + ); + assert!(!output.contains("**Decisions requiring review**")); +} + +#[test] +fn pr_description_escapes_markdown() { + let question = r"Keep `code` *bold* _it_ [l] !b |p| ~s & c:\path # (ok) — fine?"; + let expected = + r"Keep \`code\` \*bold\* \_it\_ \[l\] \ \!b \|p\| \~s \& c:\\path # (ok) — fine?"; + + assert_eq!(pr_description::escape_markdown(question), expected); + + let mut unescaped = String::new(); + let mut characters = expected.chars().peekable(); + while let Some(character) = characters.next() { + let escapes_next = character == '\\' + && characters + .peek() + .is_some_and(|next| r"\`*_[]<>!|~&".contains(*next)); + if !escapes_next { + unescaped.push(character); + } else if let Some(next) = characters.next() { + unescaped.push(next); + } + } + assert_eq!(unescaped, question); + + let plan = pr_plan_input( + "context/plans/escape.md", + &review_plan_source("escape", [Some(question), None, None]), + ); + let output = pr_description::render(&pr_arguments(&["context/plans/escape.md"]), &[plan]) + .expect("brief should render"); + assert!( + output.contains(&format!("\n1. {expected} — [escape D1](")), + "{output}" + ); +} + +#[test] +fn pr_description_repeated_runs_are_identical() { + let (_directory, root) = temp_repository(); + write_repository_plan( + &root, + "context/plans/beta.md", + &review_plan_source("beta", [None, None, Some("Is beta safe?")]), + ); + write_repository_plan( + &root, + "context/plans/alpha.md", + &review_plan_source("alpha", [Some("Is alpha safe?"), None, None]), + ); + + let forward = run_pr_description(&root, &["context/plans/alpha.md", "context/plans/beta.md"]); + let reverse = run_pr_description(&root, &["context/plans/beta.md", "context/plans/alpha.md"]); + + assert_eq!( + forward.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + forward.stderr + ); + assert_eq!( + reverse.exit_code, + std::process::ExitCode::SUCCESS, + "{}", + reverse.stderr + ); + assert_eq!(forward.stdout, reverse.stdout); + assert!(forward + .stdout + .starts_with("\n")); + assert!( + forward + .stdout + .ends_with("#L15)\n\n\n"), + "{:?}", + forward.stdout + ); + assert!(forward.stdout.contains("1. Is alpha safe? — [alpha D1](")); + assert!(forward.stdout.contains("2. Is beta safe? — [beta D3](")); +} + +#[test] +fn pr_description_rejects_invalid_arguments() { + let (_directory, root) = temp_repository(); + let valid_path = "context/plans/missing.md"; + let mut cases: Vec<(Vec<&str>, &str)> = Vec::new(); + for repo in [ + "owner", + "owner/", + "/name", + "a/b/c", + "owner/..", + "owner/.", + "own_er/name", + "owner/na me", + ] { + cases.push((vec!["--repo", repo, "--head", PR_HEAD, valid_path], repo)); + } + let short = &PR_HEAD[..39]; + let long = format!("{PR_HEAD}0"); + let upper = PR_HEAD.to_uppercase(); + let non_hex = format!("{}g", &PR_HEAD[..39]); + for head in [short, long.as_str(), upper.as_str(), non_hex.as_str()] { + cases.push((vec!["--repo", PR_REPO, "--head", head, valid_path], head)); + } + let absolute = format!("{}/context/plans/plan.md", root.display()); + for path in [ + absolute.as_str(), + "docs/plan.md", + "context/plans/nested/plan.md", + "context/plans/plan", + "context/plans/../plan.md", + "context/plans/..md", + "context/plans/.md", + "context/plans/pl@n.md", + ] { + cases.push((vec!["--repo", PR_REPO, "--head", PR_HEAD, path], path)); + } + cases.push(( + vec!["--repo", PR_REPO, "--head", PR_HEAD, valid_path, valid_path], + valid_path, + )); + + for (arguments, rejected) in cases { + let mut args = vec![NAME, "pr-description"]; + args.extend(arguments); + let run = run_plan_args(&args, &root); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{args:?}: {}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{args:?}: {}", run.stdout); + assert!( + run.stderr.contains("SCE-ERR-VALIDATION"), + "{args:?}: {}", + run.stderr + ); + assert!( + run.stderr.contains(&format!("'{rejected}'")), + "{args:?}: {}", + run.stderr + ); + } + + let accepted = run_plan_args( + &[ + NAME, + "pr-description", + "--repo", + "a-1/x.y_z-1", + "--head", + PR_HEAD, + "context/plans/a.b_c-1.md", + ], + &root, + ); + assert_eq!( + accepted.exit_code, + std::process::ExitCode::from(4), + "{}", + accepted.stderr + ); +} + +#[test] +fn pr_description_rejects_more_than_three_questions() { + let (_directory, root) = temp_repository(); + write_repository_plan( + &root, + "context/plans/beta.md", + &review_plan_source("beta", [Some("One?"), Some("Two?"), None]), + ); + write_repository_plan( + &root, + "context/plans/alpha.md", + &review_plan_source("alpha", [Some("Three?"), None, Some("Four?")]), + ); + + let run = run_pr_description(&root, &["context/plans/beta.md", "context/plans/alpha.md"]); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{}", run.stdout); + assert!(run.stderr.contains("Found 4 decisions"), "{}", run.stderr); + assert!( + run.stderr.contains("alpha D1, alpha D3, beta D1, beta D2."), + "{}", + run.stderr + ); +} + +#[test] +fn pr_description_rejects_invalid_plan() { + let (_directory, root) = temp_repository(); + let path = "context/plans/duplicate-id.md"; + let source = std::fs::read_to_string(fixture_dir().join("duplicate-id.md")) + .expect("fixture should be readable"); + write_repository_plan(&root, path, &source); + + let run = run_pr_description(&root, &[path]); + let absolute = root.join(path).display().to_string(); + let check = run_plan_check(&absolute); + + assert_eq!( + run.exit_code, + std::process::ExitCode::from(3), + "{}", + run.stderr + ); + assert!(run.stdout.is_empty(), "{}", run.stdout); + let defects = |stderr: &str, label: &str| -> Vec { + stderr + .lines() + .filter_map(|line| line.strip_prefix(&format!("{label}:"))) + .map(str::to_string) + .collect() + }; + assert_eq!( + defects(&run.stderr, path), + vec!["29: `S1` is defined more than once".to_string()] + ); + assert_eq!( + defects(&run.stderr, path), + defects(&check.stderr, &absolute) + ); + + let missing = run_pr_description(&root, &["context/plans/absent.md"]); + assert_eq!( + missing.exit_code, + std::process::ExitCode::from(4), + "{}", + missing.stderr + ); + assert!(missing.stdout.is_empty(), "{}", missing.stdout); + assert!( + missing.stderr.contains("SCE-ERR-RUNTIME"), + "{}", + missing.stderr + ); +} + +#[test] +fn plan_pr_description_appears_in_help() { + let root_help = strip_ansi(&crate::services::help::help_text()); + assert!( + root_help.contains("sce plan pr-description --repo --head ..."), + "{root_help}" + ); + let plan_help = strip_ansi( + &crate::cli_schema::render_help_for_path(&[NAME]).expect("plan help should be renderable"), + ); + assert!(plan_help.contains("pr-description"), "{plan_help}"); + let help = strip_ansi( + &crate::cli_schema::render_help_for_path(&[NAME, "pr-description"]) + .expect("plan pr-description help should be renderable"), + ); + assert!(help.contains("--repo "), "{help}"); + assert!(help.contains("--head "), "{help}"); + assert!(help.contains("..."), "{help}"); +} + +#[test] +fn completion_scripts_include_plan_pr_description() { + use crate::services::completion::{render_completion, CompletionRequest, CompletionShell}; + + for shell in [ + CompletionShell::Bash, + CompletionShell::Zsh, + CompletionShell::Fish, + ] { + let script = render_completion(CompletionRequest { shell }); + assert!(script.contains("pr-description"), "{shell:?}"); + } +} diff --git a/cli/src/services/plan/pr_description.rs b/cli/src/services/plan/pr_description.rs new file mode 100644 index 000000000..3b29b7b98 --- /dev/null +++ b/cli/src/services/plan/pr_description.rs @@ -0,0 +1,221 @@ +use std::collections::BTreeSet; + +use crate::services::error::CliError; +use crate::services::plan::extract::{Plan, PlanEntryKind}; + +pub const START_MARKER: &str = ""; +pub const END_MARKER: &str = ""; +pub const HEADING: &str = "## Intent review"; +pub const PLANS_LABEL: &str = "**Plans:**"; +pub const DECISIONS_HEADING: &str = "**Decisions requiring review**"; +pub const NO_FLAGGED_DECISIONS: &str = "No decisions explicitly flagged for review."; +pub const MAX_REVIEW_QUESTIONS: usize = 3; +pub const SHA_LENGTH: usize = 40; +pub const PLAN_PATH_PREFIX: &str = "context/plans/"; +pub const PLAN_PATH_SUFFIX: &str = ".md"; +const GITHUB_URL: &str = "https://github.com"; +const REVIEW_KEY: &str = "Review"; +const REVIEW_REQUIRED: &str = "required"; +const REVIEW_QUESTION_KEY: &str = "Review question"; +const ESCAPED_CHARACTERS: [char; 12] = + ['\\', '`', '*', '_', '[', ']', '<', '>', '!', '|', '~', '&']; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrDescriptionArguments { + pub repo: String, + pub head: String, + pub paths: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanInput { + pub path: String, + pub plan: Plan, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ReviewQuestion<'a> { + path: &'a str, + decision_id: &'a str, + line: usize, + question: &'a str, +} + +pub fn parse_arguments( + repo: &str, + head: &str, + paths: &[String], +) -> Result { + if !is_repository(repo) { + return Err(CliError::validation(format!( + "Invalid --repo value '{repo}'. Try: pass /, where OWNER uses [A-Za-z0-9-] and NAME uses [A-Za-z0-9._-] and is not '.' or '..'." + ))); + } + if !is_full_sha(head) { + return Err(CliError::validation(format!( + "Invalid --head value '{head}'. Try: pass the full {SHA_LENGTH}-character lowercase hexadecimal commit SHA, for example from 'git rev-parse HEAD'." + ))); + } + + let mut seen = BTreeSet::new(); + for path in paths { + if !is_plan_path(path) { + return Err(CliError::validation(format!( + "Invalid plan path '{path}'. Try: pass a path relative to the repository root as {PLAN_PATH_PREFIX}{PLAN_PATH_SUFFIX}, where FILE uses [A-Za-z0-9._-] and is not '.' or '..'." + ))); + } + if !seen.insert(path.as_str()) { + return Err(CliError::validation(format!( + "Plan path '{path}' is given more than once. Try: pass each plan path once." + ))); + } + } + + Ok(PrDescriptionArguments { + repo: repo.to_string(), + head: head.to_string(), + paths: paths.to_vec(), + }) +} + +fn is_repository(value: &str) -> bool { + let Some((owner, name)) = value.split_once('/') else { + return false; + }; + !owner.is_empty() + && owner + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + && is_safe_segment(name) +} + +fn is_full_sha(value: &str) -> bool { + value.len() == SHA_LENGTH + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn is_plan_path(value: &str) -> bool { + value + .strip_prefix(PLAN_PATH_PREFIX) + .and_then(|file| file.strip_suffix(PLAN_PATH_SUFFIX)) + .is_some_and(is_safe_segment) +} + +fn is_safe_segment(value: &str) -> bool { + !value.is_empty() + && value != "." + && value != ".." + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) +} + +pub fn render(arguments: &PrDescriptionArguments, plans: &[PlanInput]) -> Result { + let mut sorted: Vec<&PlanInput> = plans.iter().collect(); + sorted.sort_by(|left, right| left.path.cmp(&right.path)); + + let questions: Vec = sorted + .iter() + .flat_map(|input| review_questions(input)) + .collect(); + if questions.len() > MAX_REVIEW_QUESTIONS { + let marked: Vec = questions + .iter() + .map(|question| format!("{} {}", plan_label(question.path), question.decision_id)) + .collect(); + return Err(CliError::validation(format!( + "Found {} decisions marked 'Review: required'; at most {MAX_REVIEW_QUESTIONS} are allowed: {}. Try: remove 'Review:' and 'Review question:' from the decisions that do not need senior review, or split the plans into separate pull requests.", + questions.len(), + marked.join(", ") + ))); + } + + let plan_links: Vec = sorted + .iter() + .map(|input| { + format!( + "[{}]({})", + escape_markdown(plan_label(&input.path)), + blob_url(arguments, &input.path) + ) + }) + .collect(); + + let mut lines = vec![ + START_MARKER.to_string(), + HEADING.to_string(), + String::new(), + format!("{PLANS_LABEL} {}", plan_links.join(", ")), + String::new(), + ]; + if questions.is_empty() { + lines.push(NO_FLAGGED_DECISIONS.to_string()); + } else { + lines.push(DECISIONS_HEADING.to_string()); + for (index, question) in questions.iter().enumerate() { + lines.push(String::new()); + lines.push(format!( + "{}. {} — [{} {}]({}#L{})", + index + 1, + escape_markdown(question.question), + escape_markdown(plan_label(question.path)), + question.decision_id, + blob_url(arguments, question.path), + question.line + )); + } + } + lines.push(String::new()); + lines.push(END_MARKER.to_string()); + + Ok(lines.join("\n")) +} + +fn review_questions(input: &PlanInput) -> Vec> { + input + .plan + .entries_of(PlanEntryKind::Decision) + .filter_map(|decision| { + let marked = decision + .fields + .iter() + .any(|field| field.key == REVIEW_KEY && field.value == REVIEW_REQUIRED); + let question = decision + .fields + .iter() + .find(|field| field.key == REVIEW_QUESTION_KEY)?; + marked.then_some(ReviewQuestion { + path: &input.path, + decision_id: &decision.id, + line: decision.line, + question: &question.value, + }) + }) + .collect() +} + +fn plan_label(path: &str) -> &str { + path.strip_prefix(PLAN_PATH_PREFIX) + .and_then(|file| file.strip_suffix(PLAN_PATH_SUFFIX)) + .unwrap_or(path) +} + +fn blob_url(arguments: &PrDescriptionArguments, path: &str) -> String { + format!( + "{GITHUB_URL}/{}/blob/{}/{path}", + arguments.repo, arguments.head + ) +} + +pub fn escape_markdown(text: &str) -> String { + let mut escaped = String::with_capacity(text.len()); + for character in text.chars() { + if ESCAPED_CHARACTERS.contains(&character) { + escaped.push('\\'); + } + escaped.push(character); + } + escaped +} diff --git a/context/architecture.md b/context/architecture.md index 350e1bc78..c9068bc03 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -82,7 +82,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its payload semantics for the three remotely synchronized streams (`messages`, `parts`, `agent_traces`; `diff_traces` is captured locally but not synchronized), while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the three remote `messages`/`parts`/`agent_traces` stream state machines concurrently (`diff_traces` remains a local-only compatibility report) via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helper (`load_patch_from_json` for string input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. -- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). `command.rs` exposes it as the synchronous, database-free `sce plan check ` command, and `init.rs` backs `sce plan init `, which publishes the embedded scaffold to `context/plans/.md` at the Git repository root through a no-clobber hard link. +- `cli/src/services/plan/` owns read-only SCE plan structure analysis; `extract.rs` is its only `markdown::mdast` consumer and turns `markdown-rs` GFM output into an owned plan model, and `validate.rs` applies the Pkl-parity structural rules to that model as a pure function (see `context/cli/plan-service.md`). `command.rs` exposes it as the synchronous, database-free `sce plan check ` command, and `init.rs` backs `sce plan init `, which publishes the embedded scaffold to `context/plans/.md` at the Git repository root through a no-clobber hard link. `pr_description.rs` is the pure renderer behind `sce plan pr-description`, which validates plans read from the working tree and prints an offline intent review brief. - `cli/src/services/structured_patch.rs` defines the synchronous structured editor-hook derivation seam. It derives Claude `PostToolUse` `Write` structured-update hunks, `Write` `tool_input.content` create fallback, and `Edit` structured-patch payloads into canonical `ParsedPatch` values plus Claude session/tool metadata, returning deterministic skip reasons for unsupported events/tools/payload shapes. The module is pure and side-effect-free. It is wired into `sce hooks diff-trace` for Claude payload classification at intake (T04) and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing dispatch at read time (T05). - `cli/src/services/` contains module boundaries for command_registry, lifecycle, auth_command, config, setup, doctor, hooks, bash_policy, version, completion, help, patch, plan, sync, SCE web URL helpers, shared database infrastructure, local DB adapters, encrypted auth DB adapters, and Agent Trace DB adapters with explicit trait seams for future implementations. The former `cli/src/services/checkout/` checkout-identity module was removed by the `remove-checkout-id` plan (see `context/cli/checkout-identity.md`); there is no per-clone identity, worktrees are identified by Git topology (`resolve_worktree_id`), active setup/hooks use `agent_trace_storage` to initialize/open the repository-scoped DB, and `sce doctor` surfaces credential-safe repository Agent Trace DB metadata. There is no checkout-scoped discovery or former trace inspection surface; any pre-migration `agent-trace-*.db` files on disk are never touched and no longer inspectable via the CLI. `cli/src/services/bash_policy.rs` owns both the CLI-agnostic evaluator logic and the hidden `sce policy bash` command adapter used by OpenCode, Claude, and Pi callers; Codex delegates in-process to the same evaluator for its native `PreToolUse(Bash)` response. `cli/src/services/command_registry.rs` defines the static `RuntimeCommand` enum, deterministic `CommandRegistry` name catalog, and `build_default_registry()` function for command dispatch metadata. Service-owned command modules own the runtime command payload structs for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index b252df979..655d07b24 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -98,7 +98,7 @@ Deferred or gated command surfaces currently avoid claiming unimplemented behavi `version` exposes deterministic runtime identification output in text mode by default and JSON mode via `--format json`. `completion` exposes deterministic shell completion generation via `sce completion --shell `. -`plan` exposes `sce plan check `, a read-only structural check of one SCE plan file, and `sce plan init `, which creates `context/plans/.md` at the Git repository root from the embedded plan scaffold without overwriting an existing entry; both output and exit-code contracts are owned by `context/cli/plan-service.md`. +`plan` exposes `sce plan check `, a read-only structural check of one SCE plan file, `sce plan init `, which creates `context/plans/.md` at the Git repository root from the embedded plan scaffold without overwriting an existing entry, and `sce plan pr-description --repo --head ...`, which prints an offline intent review brief for pull requests. All three output and exit-code contracts are owned by `context/cli/plan-service.md`. `setup` defaults to an `inquire` interactive target selection (OpenCode, Claude, Pi, Codex, All) and accepts mutually-exclusive non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--codex`, `--all`); the former `--both` flag was removed in favor of `--all` (opencode+claude+pi+codex); the interactive prompt title and target labels reuse shared prompt styling helpers when stdout color is enabled. Every setup mode first validates the Git repository and effective `agent_trace.repository_remote` (default `origin`) before prompts or writes. If that configured remote is missing, the typed `NotGitRemote` diagnostic uses generic `git remote add ` guidance; the preserved technical source identifies the effective name without exposing the URL. `setup` also accepts `--bootstrap-context` as a standalone context-only mode that ensures the durable-context baseline without prompts or integration installs; every normal successful setup path ensures that baseline only after both preflights. `setup` accepts a repeatable `--workflow ` flag selecting which optional workflows to install (currently only `brownfield`). Passing it makes the listed slugs the exact selection for that run; omitting it reuses the persisted `integrations.optional_workflows`, so a repeat run preserves an earlier opt-in. Unknown slugs fail request resolution with a validation error naming the embedded catalog's available slugs and write no files, and `--workflow` is rejected alongside `--bootstrap-context` or on a hooks-only run because neither installs target assets. The resolved selection filters the installed assets and is persisted; see [config precedence contract](config-precedence-contract.md) and [setup local bootstrap](../sce/setup-repo-local-config-bootstrap.md). An interactive `setup` run instead resolves the selection through an `inquire` multi-select shown after the target prompt, titled `Select optional workflows` with one `{title} — {description}` row per optional workflow using the shared prompt styling. Rows are unchecked when nothing is persisted and pre-checked from `integrations.optional_workflows` otherwise (a supplied `--workflow` list seeds them instead); the answered prompt is the run's exact selection. It then asks `Automatically sync Agent Traces?` followed by `(Requires an SCE account. Sends Agent Traces from supported AI coding tools` and `to SCE servers so they can be stored and viewed in your account.)`, then `Record SCE involvement in Git commits?` followed by `(Adds SCE metadata to commits created or assisted by SCE.)`, as independent confirmations, both defaulting to Yes; the two answers are carried independently through setup dispatch. Cancelling either interactive prompt yields the existing `Setup cancelled. No files were changed.` outcome, a non-TTY run keeps the existing actionable guidance, and the optional-workflow prompt is skipped when the catalog has no optional workflow. diff --git a/context/cli/plan-service.md b/context/cli/plan-service.md index 5b5c17bbd..478d201ec 100644 --- a/context/cli/plan-service.md +++ b/context/cli/plan-service.md @@ -1,13 +1,14 @@ # Plan Service -`cli/src/services/plan/` owns deterministic, read-only structural analysis of one SCE plan Markdown file, and the `sce plan init` scaffold generator, its only writing operation. The analysis is the Rust counterpart of the test-only Pkl checker `config/pkl/renderers/plan-structure.pkl`. +`cli/src/services/plan/` owns deterministic, read-only structural analysis of one SCE plan Markdown file, the `sce plan pr-description` intent review brief, and the `sce plan init` scaffold generator, its only writing operation. The analysis is the Rust counterpart of the test-only Pkl checker `config/pkl/renderers/plan-structure.pkl`. ## Current scope - `extract.rs` is the only plan-service code that touches `markdown::mdast::Node`. It parses the source with `markdown-rs` (`markdown` crate, `to_mdast(source, &ParseOptions::gfm())`) and returns an owned `Plan` model. It never scans lines to find structure. - `validate.rs` holds the pure `validate(&Plan) -> PlanReport` rules. It has no I/O and never touches `mdast` types. -- `command.rs` owns the `plan` command. `PlanCommand` holds one `PlanRequest::{Check(PlanCheckRequest), Init(PlanInitRequest)}`, mapped from `PlanSubcommand` in `services/parse/command_runtime.rs` and dispatched by a static `match`. `execute_with_stderr(stderr)`, called from `RuntimeCommand::execute_with_stderr`, passes `std::env::current_dir` and the command stderr writer to `execute_with`, which tests call with an explicit start directory and a captured stderr buffer instead of changing process state (see [Command contract](#command-contract) and [Init command contract](#init-command-contract)). -- `init.rs` owns `PlanName` validation and `create(&PlanName, start)`, the repository-root scaffold publication. +- `command.rs` owns the `plan` command. `PlanCommand` holds one `PlanRequest::{Check(PlanCheckRequest), Init(PlanInitRequest), PrDescription(PlanPrDescriptionRequest)}`, mapped from `PlanSubcommand` in `services/parse/command_runtime.rs` and dispatched by a static `match`. `execute_with_stderr(stderr)`, called from `RuntimeCommand::execute_with_stderr`, passes `std::env::current_dir` and the command stderr writer to `execute_with`, which tests call with an explicit start directory and a captured stderr buffer instead of changing process state (see [Command contract](#command-contract), [Init command contract](#init-command-contract), and [PR description command contract](#pr-description-command-contract)). `check` and `pr-description` share one load path: read, `extract`, `validate`, and the same diagnostic rendering. +- `init.rs` owns `PlanName` validation and `create(&PlanName, start)`, the repository-root scaffold publication. Its `resolve_repository_root(start, command)` is shared with `pr-description` and names the calling command in its remediation text. +- `pr_description.rs` is a pure module with no I/O. It owns the argument grammar (`parse_arguments`), the collection of marked decisions, the question limit, Markdown escaping (`escape_markdown`), and `render(&PrDescriptionArguments, &[PlanInput])`. - `mod.rs` embeds the generated plan init scaffold as the crate-visible `SCAFFOLD` constant (`include_str!` of `OUT_DIR/pkl-generated/config/plan-init-scaffold.md`) with its `SCAFFOLD_PLAN_NAME_TOKEN` (`{plan-name}`). `cli/build.rs` requires that staged path, so a payload or package fallback without the scaffold fails the build. The binary never renders the scaffold itself. ## Plan model @@ -79,14 +80,30 @@ - Cleanup warning: when the hard link succeeds but removing the temporary file fails, the run still exits `0` with the same three stdout lines, and writes one stderr line before them: `Warning: Created plan context/plans/.md, but failed to remove temporary file '': . Try: remove '' manually; the plan was created and does not need to be initialized again.` The published destination is never removed. - Failures: an invalid name or an existing destination (`Plan already exists: context/plans/.md`) is `CliError::validation` (exit `3`). A missing repository, a failed or missing `git`, an unprintable root, a non-directory `context` path, and any other filesystem failure are `CliError::runtime` (exit `4`). When writing or publishing fails, the temporary file is removed and the original error and class are returned. When that removal also fails, the class is kept and the rendered message is the original error followed by a line `Cleanup also failed: could not remove temporary file '': . Try: remove it manually.` -`plan` is a visible top-level command and appears in `sce --help` (with `sce plan check ` and `sce plan init ` lines under `Plan Usage:`) and in generated Bash/Zsh/Fish completions. +## PR description command contract + +`sce plan pr-description --repo --head ...` prints an intent review brief for one or more plans. It is synchronous and offline. It uses no database, no network, no AI, and no heuristics, and it never writes files. + +- Arguments are checked before any file access. `--repo` is `/`, with owner `[A-Za-z0-9-]+` and name `[A-Za-z0-9._-]+` that is not `.` or `..`. `--head` is exactly 40 lowercase hexadecimal characters. Each path is `context/plans/.md`, with `` in `[A-Za-z0-9._-]+` and not `.` or `..`. A path given twice is rejected. Because of this grammar, URLs need no percent-encoding. +- Plans are read from the working tree at `{root}/{path}`, where the root comes from `git rev-parse --show-toplevel`. The command never reads Git objects and never checks that the working tree matches `--head`. The caller must pass the SHA that contains the plan files as read. +- Every plan must pass the same `extract` and `validate` checks as `sce plan check`. A failing plan reports the same diagnostic lines, labeled with its relative path. +- A marked decision carries `Review: required` and its `Review question:`. More than three marked decisions across all plans is an error, never a truncation. The error states the count and lists each one as `