From db5794cf00ee7b89730849f9e745ec5d55630857 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 7 Sep 2026 16:00:25 +0800 Subject: [PATCH] fix: enforce lifecycle and desktop launch boundaries --- AGENTS.md | 102 +++------------ README.md | 5 + crates/core/src/engine.rs | 2 +- docs/catalog.json | 1 + .../reference/desktop-development-profiles.md | 72 +++++++++++ .../verification.md | 13 +- .../intent.md | 46 +++++++ .../2026-09-07-astra-scaffold-cleanup/plan.md | 47 +++++++ .../2026-09-07-astra-scaffold-cleanup/spec.md | 49 +++++++ .../verification.md | 68 ++++++++++ .../intent.md | 46 +++++++ .../plan.md | 47 +++++++ .../spec.md | 51 ++++++++ .../verification.md | 65 ++++++++++ docs/sdlc/development-workflow.md | 14 +- docs/sdlc/evals/ai-native-sdlc-gates.md | 22 ++-- docs/sdlc/references/artifact-contracts.md | 10 +- docs/sdlc/workflow.md | 41 ++++-- script/dev/run.sh | 113 ++++++++-------- script/dev/run.test.ts | 99 ++++++++++++++ script/devflow.test.ts | 115 ++++++++++------- script/devflow.ts | 27 ++-- script/verify/checks.test.ts | 115 ++++++++++++++++- script/verify/sdlc.ts | 45 ++----- script/verify/stage-bundle.ts | 122 ++++++++---------- 25 files changed, 995 insertions(+), 342 deletions(-) create mode 100644 docs/reference/desktop-development-profiles.md create mode 100644 docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/intent.md create mode 100644 docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/plan.md create mode 100644 docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/spec.md create mode 100644 docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/verification.md create mode 100644 docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/intent.md create mode 100644 docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/plan.md create mode 100644 docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/spec.md create mode 100644 docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/verification.md create mode 100644 script/dev/run.test.ts diff --git a/AGENTS.md b/AGENTS.md index 8ac51232..f5eedc3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,21 +10,23 @@ checkout. Artifacts, lifecycle states, Gates, verification evidence, release handoff, Incidents, and Evals. Use [`docs/sdlc/development-workflow.md`](docs/sdlc/development-workflow.md) and [`./script/devflow`](script/devflow) for daily change creation, approval recording, and validation. -Install the external [`sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) `ai-native-sdlc` skill -when Bootstrap, audit, or incident-to-improvement guidance is needed; the repository checker remains -the enforcement source. +Use the external [`sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) `ai-native-sdlc` skill only +when guidance is needed to bootstrap or audit the development lifecycle, or improve it after an +incident. Reuse an installed copy first; the repository checker remains the enforcement source. - A direct user implementation request may approve Intent. Record its source, constraints, named - approver, and observable acceptance in one change Artifact, then move it to `executing` before - repository implementation begins. An Artifact-only Intent proposal may remain `draft` or - `in-review`. + approver, and observable acceptance in one change bundle. Implementation requires accepted + Intent, Spec, and Plan; use verification `in-progress` while executing. Artifact-only proposals + may remain `draft` or `in-review`. - Reuse accepted ADRs, design documents, issues, and PRs as evidence; link them from the change Artifact instead of copying their state into another tracker. -- Run `bun test script/verify/checks.test.ts` when either repository Gate changes. Before handoff, - always run `bun script/verify/docs.ts`, `bun script/verify/sdlc.ts`, and - `bun script/verify/sdlc.ts --worktree`. A PR that - changes repository files must change or add a schema-2 canonical - `docs/sdlc/changes/-/` with schema-3 stage files (`intent.md`, `spec.md`, +- Before handing off repository file changes, run `bun script/verify/docs.ts` and + `bun script/verify/sdlc.ts --worktree`; the latter includes the full lifecycle check. Run + `bun test script/verify/checks.test.ts` for Gate or lifecycle-contract changes, and + `bun test script/devflow.test.ts` for devflow changes. Also run applicable active Evals. + Read-only audits do not require these runs. +- A PR that changes repository files must change or add a canonical + `docs/sdlc/changes/-/` bundle with schema-3 stage files (`intent.md`, `spec.md`, `plan.md`, `verification.md`); implementation differences require that bundle's `intent.md`, `spec.md`, and `plan.md` to be `accepted` and every changed path to fall under its explicit `plan.md` scope. @@ -57,9 +59,9 @@ automation state. cross-process ownership lock. - Session Git worktrees isolate code changes; they do not make shared application state safe. -Until the profile contract below is implemented and verified, assume `bun run dev` supports only -one live dev instance. A distinct `CODETWO_DATA_DIR` is a partial diagnostic workaround, not proof -of safe multi-instance development. +Until the [profile contract](docs/reference/desktop-development-profiles.md) is implemented and +verified, assume `bun run dev` supports only one live dev instance. A distinct `CODETWO_DATA_DIR` +is a partial diagnostic workaround, not proof of safe multi-instance development. ### Launch rules @@ -78,71 +80,7 @@ Before starting the desktop: If multiple windows need to show the same sessions, use one Core with multiple renderer windows. Do not solve that requirement by sharing SQLite between multiple Core processes. -### Required profile contract - -When implementing or using true multi-instance development, introduce an explicit -`CODETWO_DEV_PROFILE` and make one profile the complete isolation boundary. Preserve the existing -single-instance behavior when no profile is supplied. - -For every non-default profile, derive or require all of the following: - -- a unique absolute `CODETWO_DATA_DIR`, preferably under the current worktree's ignored - `.codex/run/instances//data` directory; -- an explicit unique `CODETWO_DEV_PORT` used by Vite with `strictPort: true`; -- a profile-specific PID/ownership-lock path, Unix socket path, logs, and temporary runtime files; -- isolated build output when two instances could build concurrently; -- on macOS, a profile-specific development application name and bundle identifier when two app - bundles will run at once, so Dock identity, TCC attribution, app capture, and UI automation do - not select the wrong instance. - -Validate profile names before using them in paths or identifiers. Accept a small slug alphabet, -reject traversal and empty values, and show the resolved profile, port, and data directory in the -startup output. - -The intended interface after implementation is: - -```bash -CODETWO_DEV_PROFILE=feature-a CODETWO_DEV_PORT=1421 bun run dev -CODETWO_DEV_PROFILE=plugin-dev CODETWO_DEV_PORT=1422 bun run dev -``` - -Do not present those commands as supported until the launcher, Vite configuration, bundle -metadata, and ownership checks have actually been implemented and exercised. - -### Same-profile ownership - -Before opening SQLite, running migrations, normalizing interrupted work, purging transient state, -or removing/rebinding the scene socket, the native Core must acquire an operating-system-backed -exclusive lock under the resolved data directory. - -- If the lock is held, fail fast with a clear "profile already running" error. Include safe owner - diagnostics such as profile, PID, and data directory when available. -- A PID file alone is not sufficient; PID reuse and stale files must not grant ownership. -- Normal process exit and crashes must release the OS lock automatically. -- Startup recovery may mark in-flight work interrupted only after ownership is acquired. -- Never unlink a live instance's socket before ownership is established. - -If shared state across independently hosted Core processes ever becomes a product requirement, -design that separately as a single Core daemon with multiple clients or as a durable lease and -fencing protocol. Do not weaken the development-profile lock ad hoc. - -### Acceptance criteria - -Do not call multi-instance development complete until an automated or agent-runnable harness proves -all of these behaviors: - -1. Profiles A and B start concurrently on different ports and use different databases, sockets, - provider child-process groups, and build/runtime directories. -2. A prompt or permission request in A cannot appear in, cancel, interrupt, or mutate B. -3. Starting a second process with profile A fails before database mutation, startup recovery, or - socket replacement, while the first A process and its active turn continue normally. -4. After the first A process exits or crashes, A can restart and only its own genuinely abandoned - in-flight work is reconciled as interrupted. -5. Stopping or rebuilding B does not stop, relaunch, or overwrite A. -6. The default no-profile launch remains backward compatible and does not move or rewrite existing - user data unexpectedly. -7. Port collisions and invalid profiles fail with actionable messages; no launcher silently falls - back to another port or shared directory. - -For validation, capture the resolved instance identities and assert the exact user-visible symptom, -not merely that two processes stayed alive. +Before implementing or changing development-instance isolation, read the +[development profile contract](docs/reference/desktop-development-profiles.md). It preserves the +required profile boundaries, OS-backed ownership lock, and complete acceptance criteria. Do not +claim profile-based launches are supported until that contract is implemented and exercised. diff --git a/README.md b/README.md index 466168f4..6e4ba710 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,11 @@ cd codeTwo ./script/dev/run.sh ``` +If this launcher's tracked instance is already running, normal launch refuses to replace it. +Use `./script/dev/run.sh --logs` or `--telemetry` to inspect it, or `--restart` to explicitly stop +and rebuild it. These modes do not provide multi-instance isolation; follow the +[desktop launch rules](AGENTS.md#desktop-development-instances). + C2 detects provider CLIs on your `PATH`. Provider-specific setup and the exact adapter commands are documented in [Providers](website/guide/providers.md). diff --git a/crates/core/src/engine.rs b/crates/core/src/engine.rs index bc7c82e4..fcae33d3 100644 --- a/crates/core/src/engine.rs +++ b/crates/core/src/engine.rs @@ -1383,7 +1383,7 @@ fn attach_host_mcp_servers( } const CODETWO_BROWSER_ROUTING_INSTRUCTIONS: &str = "[C2 desktop browser routing]\nFor any browser or website task, use the codetwo_browser MCP tools by default. Do not use node_repl, the host in-app browser, or Chrome unless the user explicitly asks for Chrome, an existing browser tab, or an existing login state. This routing rule applies even when another available skill describes a different in-app browser. Website access and sensitive actions still require the approvals requested by codetwo_browser."; -const CODEX_SITES_INSTRUCTIONS: &str = "[C2 Sites routing and safety]\nWhen the user asks to build, save, publish, deploy, manage, or inspect a hosted site, or when .openai/hosting.json exists, use the official OpenAI Sites plugin and its Sites skills. Reuse the exact project_id from .openai/hosting.json; never invent or transform Sites identifiers, and never expose or persist connector credentials. Treat saving a version and deploying it as separate stages, and remember that every Sites deployment URL is production. Immediately before any deployment, access-policy change, environment/secret change, custom-domain change, bypass-token generation, or deletion, require an explicit ACP or MCP approval even in Full Access; if the connector does not request one, stop and ask the user. A request for a local build or saved version alone never authorizes production deployment."; +const CODEX_SITES_INSTRUCTIONS: &str = "[C2 Sites routing and safety]\nWhen the user explicitly requests OpenAI Sites, or the current task operates on the Sites project identified by .openai/hosting.json, use the official OpenAI Sites plugin and its Sites skills. Reuse the exact project_id from .openai/hosting.json; never invent or transform Sites identifiers, and never expose or persist connector credentials. Treat saving a version and deploying it as separate stages, and remember that every Sites deployment URL is production. Immediately before any deployment, access-policy change, environment/secret change, custom-domain change, bypass-token generation, or deletion, require an explicit ACP or MCP approval even in Full Access; if the connector does not request one, stop and ask the user. A request for a local build or saved version alone never authorizes production deployment."; fn with_codetwo_browser_routing(prompt: String, enabled: bool) -> String { if enabled { diff --git a/docs/catalog.json b/docs/catalog.json index 1415fa1b..a774a240 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -11,6 +11,7 @@ "authority": "current", "paths": [ "docs/reference/architecture.md", + "docs/reference/desktop-development-profiles.md", "docs/design/system.md", "docs/reference/memory.md", "docs/reference/plugin-protocol.md", diff --git a/docs/reference/desktop-development-profiles.md b/docs/reference/desktop-development-profiles.md new file mode 100644 index 00000000..3f225f89 --- /dev/null +++ b/docs/reference/desktop-development-profiles.md @@ -0,0 +1,72 @@ +# Desktop Development Profile Contract + +Follow the [desktop launch rules](../../AGENTS.md#desktop-development-instances) before starting an instance. + +## Required profile contract + +When implementing or using true multi-instance development, introduce an explicit +`CODETWO_DEV_PROFILE` and make one profile the complete isolation boundary. Preserve the existing +single-instance behavior when no profile is supplied. + +For every non-default profile, derive or require all of the following: + +- a unique absolute `CODETWO_DATA_DIR`, preferably under the current worktree's ignored + `.codex/run/instances//data` directory; +- an explicit unique `CODETWO_DEV_PORT` used by Vite with `strictPort: true`; +- a profile-specific PID/ownership-lock path, Unix socket path, logs, and temporary runtime files; +- isolated build output when two instances could build concurrently; +- on macOS, a profile-specific development application name and bundle identifier when two app + bundles will run at once, so Dock identity, TCC attribution, app capture, and UI automation do + not select the wrong instance. + +Validate profile names before using them in paths or identifiers. Accept a small slug alphabet, +reject traversal and empty values, and show the resolved profile, port, and data directory in the +startup output. + +The intended interface after implementation is: + +```bash +CODETWO_DEV_PROFILE=feature-a CODETWO_DEV_PORT=1421 bun run dev +CODETWO_DEV_PROFILE=plugin-dev CODETWO_DEV_PORT=1422 bun run dev +``` + +Do not present those commands as supported until the launcher, Vite configuration, bundle +metadata, and ownership checks have actually been implemented and exercised. + +## Same-profile ownership + +Before opening SQLite, running migrations, normalizing interrupted work, purging transient state, +or removing/rebinding the scene socket, the native Core must acquire an operating-system-backed +exclusive lock under the resolved data directory. + +- If the lock is held, fail fast with a clear "profile already running" error. Include safe owner + diagnostics such as profile, PID, and data directory when available. +- A PID file alone is not sufficient; PID reuse and stale files must not grant ownership. +- Normal process exit and crashes must release the OS lock automatically. +- Startup recovery may mark in-flight work interrupted only after ownership is acquired. +- Never unlink a live instance's socket before ownership is established. + +If shared state across independently hosted Core processes ever becomes a product requirement, +design that separately as a single Core daemon with multiple clients or as a durable lease and +fencing protocol. Do not weaken the development-profile lock ad hoc. + +## Acceptance criteria + +Do not call multi-instance development complete until an automated or agent-runnable harness proves +all of these behaviors: + +1. Profiles A and B start concurrently on different ports and use different databases, sockets, + provider child-process groups, and build/runtime directories. +2. A prompt or permission request in A cannot appear in, cancel, interrupt, or mutate B. +3. Starting a second process with profile A fails before database mutation, startup recovery, or + socket replacement, while the first A process and its active turn continue normally. +4. After the first A process exits or crashes, A can restart and only its own genuinely abandoned + in-flight work is reconciled as interrupted. +5. Stopping or rebuilding B does not stop, relaunch, or overwrite A. +6. The default no-profile launch remains backward compatible and does not move or rewrite existing + user data unexpectedly. +7. Port collisions and invalid profiles fail with actionable messages; no launcher silently falls + back to another port or shared directory. + +For validation, capture the resolved instance identities and assert the exact user-visible symptom, +not merely that two processes stayed alive. diff --git a/docs/sdlc/changes/2026-09-02-four-stage-sdlc/verification.md b/docs/sdlc/changes/2026-09-02-four-stage-sdlc/verification.md index 4be0bdeb..a9debd17 100644 --- a/docs/sdlc/changes/2026-09-02-four-stage-sdlc/verification.md +++ b/docs/sdlc/changes/2026-09-02-four-stage-sdlc/verification.md @@ -18,17 +18,14 @@ release_identity: "" ## Automated checks -- AC-1: PASS — `bun test script/verify/checks.test.ts` includes schema-3 approval rejection cases. -- AC-2: PASS — `bun test script/devflow.test.ts` covers approve/design/plan flow. -- AC-3: PASS — `bun script/verify/docs.ts` passes after `change-stage` catalog classification fix. -- AC-4: PASS — `docs/sdlc/workflow.md`, `development-workflow.md`, and `artifact-contracts.md` updated. +- AC-1: PASS — `bun test script/verify/checks.test.ts` includes schema-3 approval rejection cases. `bun test script/verify/checks.test.ts` fixture rejects unaccepted intent upstream. +- AC-2: PASS — `bun test script/devflow.test.ts` covers approve/design/plan flow. `./script/devflow approve` writes `approved_by` and `approved_at` on approved stages. +- AC-3: PASS — `bun script/verify/docs.ts` passes after `change-stage` catalog classification fix. `bun script/verify/docs.ts` accepts migrated stage files as `change-stage`. +- AC-4: PASS — `docs/sdlc/workflow.md`, `development-workflow.md`, and `artifact-contracts.md` updated. [`development-workflow.md`](../../development-workflow.md) documents mandatory approval. ## Behavioral evidence -- AC-1: PASS — `bun test script/verify/checks.test.ts` fixture rejects unaccepted intent upstream. -- AC-2: PASS — `./script/devflow approve` writes `approved_by` and `approved_at` on approved stages. -- AC-3: PASS — `bun script/verify/docs.ts` accepts migrated stage files as `change-stage`. -- AC-4: PASS — [`development-workflow.md`](../../development-workflow.md) documents mandatory approval. +The acceptance mappings above retain both the automated and behavioral evidence. ## Visual evidence diff --git a/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/intent.md b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/intent.md new file mode 100644 index 00000000..0da726b3 --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/intent.md @@ -0,0 +1,46 @@ +--- +id: "2026-09-07-astra-scaffold-cleanup" +stage: intent +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +source: "user" +risk: "low" +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Intent: Astra Scaffold Cleanup + +## Problem + +Always-on instructions contain an overbroad Sites trigger, a long development-profile specification, duplicated verification commands, and an ambiguous lifecycle-skill trigger. + +## Proposed outcome + +Narrow Sites and lifecycle-skill triggers, move the profile contract into a reference document, and remove duplicate handoff checks. + +## Affected users and systems + +CodeTwo contributors and provider sessions that receive the host Sites instructions. + +## Constraints + +Preserve Ponytail unchanged, stage approvals, production and sensitive-action approvals, live Core ownership, and all multi-instance acceptance requirements. + +## Out of scope + +No profile implementation, permission changes, deployment, plugin installation, model-specific prompt forks, or unrelated cleanup. + +## Success signals + +The four approved edits are present; profile detail and safety clauses are preserved; routing regression and repository Gates pass. + +## Open questions + +None within the approved audit scope. + +## Decision + +Chen Li approved the concrete edits from the preceding audit with “除`ponytail` 立即处理” on 2026-09-07. This approval covers the stated outcome, constraints, and surgical implementation plan for findings 2–5. diff --git a/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/plan.md b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/plan.md new file mode 100644 index 00000000..a652acb8 --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/plan.md @@ -0,0 +1,47 @@ +--- +id: "2026-09-07-astra-scaffold-cleanup" +stage: plan +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +based_on: spec.md +risk: "low" +scope: AGENTS.md, docs/catalog.json, docs/reference/desktop-development-profiles.md, docs/sdlc/workflow.md, docs/sdlc/development-workflow.md, crates/core/src/engine.rs, docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Plan: Astra Scaffold Cleanup + +## Files and ownership + +Codex owns the exact paths in scope: routing sentence, instruction documentation, catalog registration, and this canonical bundle. + +## Order of work + +Preserve the existing profile-contract text; move it and add the contextual link. Narrow Sites and lifecycle-skill triggers. Align handoff commands and run the targeted routing regression plus lifecycle checks. + +## Test-first proof + +Use the existing codex_sites_routing_preserves_the_production_boundary regression. Check moved text and retained safety clauses against HEAD; no new tests that merely mirror wording. + +## Visual or integration proof + +No rendered UI changes. Inspect the emitted prompt policy through the existing routing test; do not claim a live model-selection evaluation. + +## Risks and mitigations + +Keep safety text verbatim and preserve every isolation criterion. Run the active lifecycle Eval because project instructions change. + +## Rollback + +Revert only the scoped patch and remove its new reference document and bundle; no application data or external state changes. + +## Deviations + +None. Ponytail is excluded by the user’s explicit instruction. + +## Decision + +Chen Li approved this bounded implementation through “除`ponytail` 立即处理” on 2026-09-07 following the file-scoped audit proposals. The follow-up “clean up code/docs” authorizes wording, formatting, and schema terminology cleanup within the same paths. Merge and release are not authorized. diff --git a/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/spec.md b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/spec.md new file mode 100644 index 00000000..95a64a9e --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/spec.md @@ -0,0 +1,49 @@ +--- +id: "2026-09-07-astra-scaffold-cleanup" +stage: spec +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +based_on: intent.md +risk: "low" +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Spec: Astra Scaffold Cleanup + +## Requirements + +Narrow Sites and lifecycle-skill triggers, extract the profile contract, and unify handoff checks while preserving production and live-instance safety boundaries. + +## User experience + +Generic web work must not select Sites solely because hosting.json exists. Unrelated repository tasks receive a short pointer instead of profile implementation detail. + +## Technical design + +Change only the Sites trigger sentence. Move the profile contract from AGENTS.md into a catalogued reference document. Align verification and lifecycle-skill triggers across AGENTS.md and both workflow guides. + +## Security and privacy + +Keep Sites project-id and credential rules, explicit ACP/MCP approval even in Full Access, launch ownership checks, and the full OS-lock and isolation acceptance contract. + +## Alternatives and non-goals + +No new runtime classifier, changed checker implementation, additional skills, or model-specific scaffolding. + +## Areas of concern + +Prompt wording expresses intended routing but requires a future model task evaluation to measure actual selection rates. + +## Acceptance criteria + +- [x] AC-1: Sites selection requires explicit OpenAI Sites intent or work on the project identified by hosting.json; the remaining safety text is unchanged. +- [x] AC-2: The complete profile implementation and acceptance contract is preserved in a catalogued document, with launch safeguards and a contextual pointer in AGENTS.md. +- [x] AC-3: Repository-change handoff uses docs and SDLC --worktree; Gate/devflow tests are triggered by relevant changes or active Evals; read-only audits do not require these runs. +- [x] AC-4: Lifecycle-skill triggers are domain-specific and prefer an installed copy; Ponytail and all approval boundaries remain unchanged. + +## Decision + +Accepted under Chen Li’s 2026-09-07 instruction “除`ponytail` 立即处理”, approving findings 2–5 and their concrete proposed edits. diff --git a/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/verification.md b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/verification.md new file mode 100644 index 00000000..dfa993fa --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-astra-scaffold-cleanup/verification.md @@ -0,0 +1,68 @@ +--- +id: "2026-09-07-astra-scaffold-cleanup" +stage: verification +schema: 3 +status: passed +owner: codex +created: "2026-09-07" +based_on: plan.md +commit: "" +verification_mode: owner +verified_by: "codex" +verified_at: "2026-09-07" +release_target: none +release_identity: "" +--- + +# Verification: Astra Scaffold Cleanup + +## Automated checks + +`bun test script/verify/checks.test.ts`: PASS, 5 tests and 18 assertions; reruns the active lifecycle Gate Eval. + +`cargo test -p codetwo-core --lib codex_sites_routing_preserves_the_production_boundary`: PASS, 1 test. + +`git diff --check`: PASS. + +`bun script/verify/docs.ts`: PASS, catalog, links, schemas, and assets valid. + +`bun script/verify/sdlc.ts --worktree`: PASS, contract valid including the full Artifact tree and changed-path scope. + +## Behavioral evidence + +- AC-1: PASS — compared `crates/core/src/engine.rs` with `HEAD` after substituting only the approved trigger sentence; all other bytes, including the production boundary, are identical. The existing routing test passes. + +- AC-2: PASS — compared the profile requirements against `HEAD:AGENTS.md`: text is identical after normalizing the standalone document’s section heading levels. Launch rules in `AGENTS.md` remain intact; catalog and contextual links reference the new document. + +- AC-3: PASS — inspected `AGENTS.md` and both workflow guides: handoff requires docs plus SDLC --worktree, relevant Gate/devflow tests and active Evals; read-only audit exemption is explicit. The checker implementation is unchanged. + +- AC-4: PASS — `AGENTS.md`, `docs/sdlc/workflow.md`, and `docs/sdlc/development-workflow.md` now identify lifecycle work and prefer installed copies. No global instruction file or Ponytail file was modified. Stage approvals and Sites sensitive-action approvals are unchanged. + +## Visual evidence + +Not applicable: no UI or rendered output changed. + +## Security and privacy evidence + +Source comparison confirms that Sites credential, identifier, production, and explicit ACP/MCP approval clauses are unchanged. Multi-instance ownership and isolation requirements retain their original text. + +## Deviations and residual risk + +Residual risk: natural-language routing can still be misinterpreted by a provider; this change does not implement a deterministic router or establish measured Astra selection improvements. + +## Verdict + +Verdict: verified. + +## Review and release + +Approval: Chen Li authorized audit findings 2–5 on 2026-09-07; merge and release remain unapproved. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: revert the scoped patch and its new documentation; no application state was changed. +No release: Chen Li requested PR creation on 2026-09-07; merge and deployment remain unapproved. + +## Feedback + +Follow-up cleanup corrected schema-3 bundle terminology, standalone heading levels, line wrapping, and the stale execution-state note. Runtime code is unchanged from the tested routing edit. diff --git a/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/intent.md b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/intent.md new file mode 100644 index 00000000..4c5ebc93 --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/intent.md @@ -0,0 +1,46 @@ +--- +id: "2026-09-07-enforce-lifecycle-and-launch-boundaries" +stage: intent +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +source: "user" +risk: "medium" +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Intent: Enforce Lifecycle And Launch Boundaries + +## Problem + +Launch modes can terminate an existing development owner; lifecycle checks can reuse unchanged historical approval, hide conflicting acceptance results, accept unfinished markers, and reject legitimate draft stages. + +## Proposed outcome + +Enforce the existing launch and lifecycle boundaries with targeted regressions and matching operator documentation. + +## Affected users and systems + +CodeTwo contributors, their active development sessions, and repository reviewers. + +## Constraints + +Preserve user processes, OS ownership requirements, human approval, release checks, historical evidence, and the preceding scaffold cleanup. No new dependencies. + +## Out of scope + +Complete multi-instance profiles, native Core locking, conditional provider-rule loading, deployment, merge, and Ponytail changes. + +## Success signals + +The five reproduced failures are covered by passing targeted regressions and repository Gates. + +## Open questions + +None. Exact repeated historical evidence is one logical record; differing duplicate records are rejected. + +## Decision + +Chen Li authorized the five concrete fixes proposed in the preceding investigation with “开始修复” on 2026-09-07. diff --git a/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/plan.md b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/plan.md new file mode 100644 index 00000000..3aed8a8c --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/plan.md @@ -0,0 +1,47 @@ +--- +id: "2026-09-07-enforce-lifecycle-and-launch-boundaries" +stage: plan +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +based_on: spec.md +risk: "medium" +scope: AGENTS.md, README.md, script/dev/run.sh, script/dev/run.test.ts, script/verify/stage-bundle.ts, script/verify/sdlc.ts, script/verify/checks.test.ts, script/devflow.ts, script/devflow.test.ts, docs/sdlc/workflow.md, docs/sdlc/development-workflow.md, docs/sdlc/references/artifact-contracts.md, docs/sdlc/evals/ai-native-sdlc-gates.md, docs/sdlc/changes/2026-09-02-four-stage-sdlc/verification.md, docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Plan: Enforce Lifecycle And Launch Boundaries + +## Files and ownership + +Codex owns the exact paths in scope. The single historical verification edit combines differing same-result evidence without changing outcomes or approval metadata. + +## Order of work + +Add failing regressions, fix shared validation and launch paths, align documentation, then run the bounded acceptance checks. + +## Test-first proof + +Extend existing Gate tests using disposable repositories. Add a launcher harness with mocked process/build/log commands. Cover both refusal and valid continuation. + +## Visual or integration proof + +Exercise the actual shell entrypoint in a disposable checkout. No production app or provider is started. + +## Risks and mitigations + +Keep release checks and native ownership requirements intact. Reuse parsed record equality for exact historical duplicates and retain both descriptions when combining one historical document. + +## Rollback + +Revert only this bundle’s scoped edits, preserving the preceding scaffold cleanup and all application data. + +## Deviations + +Exact duplicate historical evidence is accepted as one record to avoid rewriting 80 unrelated bundles. No conflicting result is tolerated. + +## Decision + +Chen Li’s “开始修复” on 2026-09-07 approves the proposed five fixes and scoped implementation. Merge, release, and running or stopping user instances remain outside this task. diff --git a/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/spec.md b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/spec.md new file mode 100644 index 00000000..d5c5f97f --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/spec.md @@ -0,0 +1,51 @@ +--- +id: "2026-09-07-enforce-lifecycle-and-launch-boundaries" +stage: spec +schema: 3 +status: accepted +owner: codex +created: "2026-09-07" +based_on: intent.md +risk: "medium" +approved_by: "Chen Li" +approved_at: 2026-09-07 +--- + +# Spec: Enforce Lifecycle And Launch Boundaries + +## Requirements + +Prevent implicit process replacement and close the reproduced lifecycle false positives and false negatives. + +## User experience + +Normal launch reports an existing owner; logs attach without building or stopping it. Explicit restart is the only replacement action. Valid draft stages can be saved without claiming implementation approval. + +## Technical design + +Use the existing launcher and stage checker. Restrict scope coverage to changed bundles. Validate each available stage in sequence. Deduplicate identical evidence records, reject differing duplicates, and validate unfinished markers with JavaScript syntax. + +## Security and privacy + +Release and independent-verifier requirements remain enforced. Tests use disposable repositories and mocked launch operations; no user application state is used. + +## Alternatives and non-goals + +No new workflow registry, additional approval layers, full profile implementation, or mass rewrite of historical bundles. + +## Areas of concern + +The launcher guard covers its tracked owner; it does not replace the separately specified native OS ownership lock. Historical identical evidence must remain readable. + +## Acceptance criteria + +- [x] AC-1: Run, verify, and debug refuse a live tracked owner before building; logs and telemetry attach; explicit restart verifies identity and waits for exit before rebuilding. +- [x] AC-2: Worktree and branch Gates reject new implementation covered only by an unchanged historical bundle, including deletion and rename changes. +- [x] AC-3: Conflicting or differing evidence for one acceptance id fails; byte-equivalent parsed records count once without deleting historical evidence. +- [x] AC-4: Sequential drafts and documented review/progress states validate; gaps or advancement without predecessor approval fail; implementation and release still require accepted prerequisites. +- [x] AC-5: Accepted or passed stages reject unfinished markers; complete stages and ordinary words remain valid. +- [x] AC-6: Relevant regressions and repository checks pass; documentation describes the actual states and launch modes. + +## Decision + +Accepted under Chen Li’s 2026-09-07 “开始修复” instruction for the investigated five failures. diff --git a/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/verification.md b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/verification.md new file mode 100644 index 00000000..fc836675 --- /dev/null +++ b/docs/sdlc/changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/verification.md @@ -0,0 +1,65 @@ +--- +id: "2026-09-07-enforce-lifecycle-and-launch-boundaries" +stage: verification +schema: 3 +status: passed +owner: codex +created: "2026-09-07" +based_on: plan.md +commit: "" +verification_mode: owner +verified_by: "codex" +verified_at: "2026-09-07" +release_target: none +release_identity: "" +--- + +# Verification: Enforce Lifecycle And Launch Boundaries + +## Automated checks + +`bun test script/verify/checks.test.ts script/devflow.test.ts script/dev/run.test.ts`: PASS, 14 tests and 106 assertions. The new Gate cases first failed on the original checker; the launcher cases first failed on implicit replacement and the missing explicit restart mode. + +`bash -n script/dev/run.sh`: PASS. + +`bun script/verify/docs.ts`, `bun script/verify/sdlc.ts`, and `bun script/verify/sdlc.ts --worktree`: PASS before closing this record. + +`git diff --check`: PASS. + +## Behavioral evidence + +- AC-1: PASS — `script/dev/run.test.ts` runs the shell entrypoint in disposable directories with mocked process/build/log commands: ordinary modes preserve the owner, logs and telemetry only attach, explicit restart waits, and a stubborn owner prevents rebuilding. +- AC-2: PASS — `script/verify/checks.test.ts` rejects unchanged historical approval for edits, deletions, and renames in both worktree and committed differences; updating the covering Plan restores the valid path. +- AC-3: PASS — `script/verify/checks.test.ts` rejects different-result and different-detail duplicate mappings, accepts exact repeated records, and validates genuine failed evidence. The four mappings in [the historical record](../2026-09-02-four-stage-sdlc/verification.md) each retain both original descriptions. +- AC-4: PASS — `script/devflow.test.ts` exercises creation, validation, review approval, Draft and Ready PR checks. `script/verify/checks.test.ts` accepts valid stage prefixes and blocks gaps, unapproved predecessors, implementation under a draft Plan, and release without passing verification. +- AC-5: PASS — `script/verify/checks.test.ts` rejects unfinished markers while accepting ordinary words and identifier substrings. The full live Artifact tree validates. +- AC-6: PASS — `docs/sdlc/workflow.md`, the operator guide and stage reference now describe actual stage states, current-change coverage, and evidence uniqueness. `README.md` documents explicit restart and non-mutating log inspection. + +## Visual evidence + +Not applicable: no rendered UI changes. No production desktop instance was launched. + +## Security and privacy evidence + +Approval identity, accepted prerequisites, release target/approval/rollback, and independent high-risk verification remain checked. All executable probes used temporary repositories or mock process operations; no user process was signalled. + +## Deviations and residual risk + +Residual risk: the shell guard protects the tracked launcher owner and cannot provide race-free data-directory ownership across separate launchers or worktrees; the native OS-lock/profile contract remains unimplemented. Mock process tests verify control flow, not full desktop runtime isolation. + +## Verdict + +Verdict: verified. + +## Review and release + +Approval: Chen Li authorized the five repairs with “开始修复” on 2026-09-07; merge and release are not authorized. +Release target: none. +Release identity: not applicable until released. +Smoke evidence: not applicable until released. +Rollback: revert only this repair’s scoped edits while retaining the preceding scaffold cleanup. +No release: Chen Li requested PR creation on 2026-09-07; merge and deployment remain unapproved. + +## Feedback + +Only exact repeated historical evidence is treated as one record. Differing evidence is rejected. Conditional provider-rule loading remains outside this repair. diff --git a/docs/sdlc/development-workflow.md b/docs/sdlc/development-workflow.md index b6185627..bd9b4b65 100644 --- a/docs/sdlc/development-workflow.md +++ b/docs/sdlc/development-workflow.md @@ -4,7 +4,7 @@ Status: **current operator guide**. [`workflow.md`](workflow.md) 是生命周期权威文档;本文件说明日常怎么用它。CodeTwo 采用 **schema 3 四阶段 Artifact**(`intent.md` → `spec.md` → `plan.md` → `verification.md`),对齐 doubao-work-skin:一变更一目录、证据并列、Intent/Spec/Plan 必须依次批准后才能实现与合并。 -Agent 可安装 [`IchenDEV/sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) 中的 `ai-native-sdlc` skill,用于 Bootstrap、推进变更、审计链路或处理事故改进。仓库内的 [artifact-contracts.md](references/artifact-contracts.md) 把通用契约映射到 CodeTwo 的 schema-3 字段。 +仅在需要搭建或审计研发生命周期、处理事故改进流程的指导时,使用 [`IchenDEV/sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) 中的 `ai-native-sdlc` skill;优先复用已安装版本。仓库内的 [artifact-contracts.md](references/artifact-contracts.md) 把通用契约映射到 CodeTwo 的 schema-3 字段。 ## 状态环 @@ -43,7 +43,7 @@ PR 审查 + CI ─── 人工合并决策 ./script/devflow new improve-task-board user medium ``` -这会创建 `docs/sdlc/changes/-improve-task-board/intent.md`。填写 Intent 章节后,在明确人工确认后记录批准: +这只创建 `docs/sdlc/changes/-improve-task-board/intent.md`;合法的阶段草稿可以立即运行 `devflow validate`,无需提前创建后续文件。填写 Intent 章节后,在明确人工确认后记录批准: ```bash ./script/devflow approve 2026-09-02-improve-task-board intent "product owner" @@ -100,14 +100,16 @@ Spec 已 `accepted` 后才能创建 Plan(并在 frontmatter 填写精确 `scop ## 构建与验证循环 -实现中使用最小相关循环,交付前跑完整 Gate: +实现中使用最小相关验证循环。有仓库文件变更时,交付前运行以下 Gate;`--worktree` 已包含完整生命周期校验,无需另跑普通模式。纯只读审计无需执行: ```bash -bun test script/verify/checks.test.ts script/devflow.test.ts bun script/verify/docs.ts bun script/verify/sdlc.ts --worktree ``` +Gate 或生命周期契约变更运行 `bun test script/verify/checks.test.ts`;devflow 变更运行 +`bun test script/devflow.test.ts`。同时执行满足触发条件的 active Eval。 + 桌面 UI 变更:编译成功不等于验收——需要真实渲染窗口证据(light / dark / narrow)。服务或协议变更:需要契约、集成或运行时证据。在 `verification.md` 记录实际命令与结果;截图放在同 bundle 的 `evidence/` 目录。 ## Pull Request @@ -118,7 +120,7 @@ PR 正文必须链接 canonical bundle 路径,例如: docs/sdlc/changes/2026-09-02-improve-task-board ``` -Draft PR 可在 verification 未完成时存在;Ready PR 要求 `intent.md`、`spec.md`、`plan.md` 均为 `accepted`,且 plan scope 覆盖所有改动路径。CI 的 `SDLC contract` job 会运行 `bun script/verify/sdlc.ts --base "$BASE_SHA"`。 +Draft PR 可承载合法的 Artifact-only 草稿或未完成的验证;实现差异仍必须由本次新增或更新 bundle 的 accepted Plan 覆盖。Ready PR 要求 `intent.md`、`spec.md`、`plan.md` 均为 `accepted`、verification 为 `passed`,且 scope 覆盖全部改动路径(含删除及重命名两端)。CI 的 `SDLC contract` job 会运行 `bun script/verify/sdlc.ts --base "$BASE_SHA"`。 本地 PR Gate 检查: @@ -145,7 +147,7 @@ Eval 在 fixture、oracle 和最新结果就绪前保持 `draft`;`active` 后 ## Skill 与学习循环 -- 安装:`npx skills add IchenDEV/sdlc-skill -a cursor -y`(或其他 Agent)。 +- 仅在上述生命周期任务需要且未安装时,使用 `npx skills add IchenDEV/sdlc-skill -a cursor -y`(或其他 Agent)。 - 项目级改进记录:`.agent-learning/ai-native-sdlc/`(proposal-only,不自动改 skill)。 - 生命周期回归:[`evals/ai-native-sdlc-gates.md`](evals/ai-native-sdlc-gates.md)。 diff --git a/docs/sdlc/evals/ai-native-sdlc-gates.md b/docs/sdlc/evals/ai-native-sdlc-gates.md index e7c52992..25c87587 100644 --- a/docs/sdlc/evals/ai-native-sdlc-gates.md +++ b/docs/sdlc/evals/ai-native-sdlc-gates.md @@ -5,7 +5,7 @@ status: active owner: repository maintainers approvers: user via the 2026-08-30 lifecycle migration request created: 2026-08-30 -updated: 2026-08-31 +updated: 2026-09-07 source: change-2026-08-30-ai-native-sdlc-migration inputs: isolated temporary repositories, the live Artifact tree, and the documentation catalog outputs: deterministic success and failure-path assertions @@ -40,16 +40,17 @@ deploy, or modify the user's application data. ## Observable acceptance -- Valid documentation, `executing` changes, and `verified` changes pass. +- Valid documentation, sequential draft stages, accepted plans, and passing verification records pass. +- Gaps, unapproved predecessors, unfinished markers, and differing evidence for the same acceptance id fail. - Documentation drift fails for an unclassified file, broken local link, orphan image, or legacy change schema. - Duplicate change ids, non-passing acceptance evidence, and owner-approved high-risk changes fail. - release readiness fails without approval or target; `released` fails without identity or smoke. - a resolved Incident fails without recovery, follow-up change, or regression Eval links. - an active Eval fails without linked provenance, result, or revision. -- the committed branch Gate rejects uncovered paths and a changed schema-1 Artifact. +- the committed branch Gate rejects uncovered paths, unchanged historical approval, and outdated stage schemas. - the worktree Gate sees staged and untracked files and rejects paths outside the changed Artifact - scope. + scope, including deleted paths and both sides of renames. ## Scoring and failure classes @@ -60,10 +61,9 @@ must not be reported as a lifecycle verdict. ## Last result Result: pass. -Revision: uncommitted script-organization worktree over `cdbfefe9` on 2026-08-31. Evidence: -`bun test script/verify/checks.test.ts` passed all 5 focused tests with 23 assertions. The suite -covers the valid path plus documentation drift, acceptance and authority closure, release, -Incident/Eval evidence, committed branch scope/schema, and staged/untracked worktree scope. -`bun script/verify/docs.ts`, the plain lifecycle checker, and the `--worktree` Gate passed. The -first consolidated worktree fixture did not modify its Change Artifact and was corrected before -this passing result. Isolated committed-diff evidence is recorded in the governing script change. +Revision: uncommitted lifecycle-boundary fix on 2026-09-07. Evidence: +`bun test script/verify/checks.test.ts` passed 9 tests and 49 assertions. It covers branch and +worktree approval freshness, edit/delete/rename scope, conflicting evidence, unfinished markers, +sequential drafts, review/progress states, and the existing release and incident checks. +The governing [change bundle](../changes/2026-09-07-enforce-lifecycle-and-launch-boundaries/intent.md) +records the final repository and launcher validation. diff --git a/docs/sdlc/references/artifact-contracts.md b/docs/sdlc/references/artifact-contracts.md index b4cff9be..afc15ad0 100644 --- a/docs/sdlc/references/artifact-contracts.md +++ b/docs/sdlc/references/artifact-contracts.md @@ -4,7 +4,7 @@ ## 总览 -CodeTwo 用**一个 bundle 目录**承载一项 material change,拆成四个 stage 文件: +CodeTwo 用**一个 bundle 目录**承载一项 material change,按审批顺序逐步创建四个 stage 文件;草稿允许只包含截至当前阶段的连续文件: ```text docs/sdlc/changes/-/ @@ -66,7 +66,7 @@ docs/sdlc/evals/.md 验收标准: - 使用稳定唯一的 `AC-N` 标识; -- 每条 criterion 在 `verification.md` 中有且仅有一条 `PASS` / `FAIL` / `BLOCKED` 映射。 +- 每条 criterion 在 `verification.md` 中只有一条当前 `PASS` / `FAIL` / `BLOCKED` 映射;历史文件中内容完全相同的重复记录算一条,结果或内容不同的重复记录会失败。 ### plan.md @@ -87,7 +87,8 @@ docs/sdlc/evals/.md | 状态 | 含义 | | --- | --- | -| `draft` / `in-progress` | 验证进行中 | +| `pending` | 验证尚未开始 | +| `in-progress` | 验证进行中 | | `passed` | 全部 AC 有 PASS 证据,`Verdict: verified` | | `failed` | 保留 FAIL 映射 | @@ -103,7 +104,8 @@ docs/sdlc/evals/.md ## PR Gate - 含仓库实现改动的 PR:`intent.md`、`spec.md`、`plan.md` 必须均为 `accepted`; -- 每个改动路径须落在 accepted plan 的 `scope` 内; +- 每个改动路径(含删除及重命名两端)须落在本次新增或更新 bundle 的 accepted plan `scope` 内; +- Artifact-only 草稿可以通过结构校验,但不能授权实现;Ready PR 仍要求前三阶段 accepted 和 verification passed; - 禁止新增或修改 legacy `change.md`。 创建 Intent:`./script/devflow new [source] [risk]` diff --git a/docs/sdlc/workflow.md b/docs/sdlc/workflow.md index 16ee9468..8a24e091 100644 --- a/docs/sdlc/workflow.md +++ b/docs/sdlc/workflow.md @@ -28,7 +28,9 @@ docs/sdlc/ evals/.md fixed regression cases with actual results ``` -Use four stage files inside each change bundle. This aligns with the adjacent doubao-work-skin +Create stage files progressively inside each change bundle; a draft may contain only Intent, +or a contiguous prefix through the current stage. Later files require an accepted predecessor. +A completed bundle contains four stage files. This aligns with the adjacent doubao-work-skin model: one directory per change, explicit Intent → Spec → Plan approval before implementation, and Verification as a separate evidence record. Binary or runtime evidence may live in the bundle's `evidence/` directory. Do not create global parallel `specs`, `plans`, `docs/superpowers`, @@ -54,8 +56,9 @@ plan.md accepted → implementation and verification.md ``` Pull requests with repository implementation changes require all three stages accepted in at least -one covering bundle. An Artifact-only proposal may keep later stages in `draft` until review -completes; this must not be treated as authorization to merge code outside an accepted Plan scope. +one covering bundle added or updated in that same difference. Unchanged historical approval +cannot cover new implementation. Artifact-only proposals may remain at their current draft stage; +this is not authorization to merge code outside an accepted Plan scope. ## End-to-end chain @@ -72,6 +75,11 @@ An Artifact does not advance because an Agent says work is done. ## Change states and Gates +The operational labels below are not interchangeable frontmatter values. Intent, Spec, and Plan +use `draft`, `in-review`, `accepted`, or `rejected`; Verification uses `pending`, `in-progress`, +`passed`, or `failed`. Execution is an accepted Plan with Verification `in-progress`. Record other +operational decisions in the relevant Decision or Review and release section. + | State | Required fact | Gate or next trigger | |---|---|---| | `draft` | problem, desired outcome, source, owner candidate | owner requests review | @@ -93,8 +101,8 @@ release remain human Gates unless separately authorized. A PR containing changes outside its canonical bundle must include a schema-3 bundle whose `intent.md`, `spec.md`, and `plan.md` are all `accepted`. Every changed path must fall under the -explicit `scope` in that bundle's `plan.md`. An Artifact-only proposal may keep later stages in -`draft` until review completes. Legacy `change.md` files are rejected; migrate with +explicit `scope` in an accepted bundle updated in the same difference, including deleted paths +and both sides of renames. Artifact-only proposals may remain at their current draft stage. Legacy `change.md` files are rejected; migrate with `bun script/sdlc/migrate-bundles.ts` when splitting historical records. ## Intent, Spec, Plan, and Build @@ -116,7 +124,9 @@ check supports human judgment but does not prove that a name corresponds to a re ## Verification loop Verification records actual commands, environment, results, runtime or visual evidence, failed -iterations, and residual risk. A failed attempt remains visible; correction returns the same change +iterations, and residual risk. Each acceptance id has one current evidence mapping. Exact repeated +records in historical bundles count once; differing duplicates fail. Keep prior attempts in the +evidence history rather than adding competing current mappings. A failed attempt remains visible; correction returns the same change to `executing`, then produces new evidence. - Desktop UI changes require real rendered-window evidence for applicable light, dark, and narrow @@ -177,7 +187,13 @@ Incident Eval merely because its fixture is difficult; repair its isolation or s ## Deterministic checks -Run from the repository root: +For repository file changes, run `bun script/verify/docs.ts` and +`bun script/verify/sdlc.ts --worktree` before handoff. The latter includes the full lifecycle check; +a separate plain run is unnecessary. Read-only audits do not require these runs. +Run `bun test script/verify/checks.test.ts` for Gate or lifecycle-contract changes and +`bun test script/devflow.test.ts` for devflow changes, plus applicable active Evals. + +Run the required lifecycle operation from the repository root: ```sh ./script/devflow new [source] [risk] @@ -186,17 +202,14 @@ Run from the repository root: ./script/devflow plan ./script/devflow verify ./script/devflow validate [--worktree] -bun test script/verify/checks.test.ts -bun script/verify/docs.ts -bun script/verify/sdlc.ts -bun script/verify/sdlc.ts --worktree ``` [`development-workflow.md`](development-workflow.md) is the operator guide for daily use. [`references/artifact-contracts.md`](references/artifact-contracts.md) maps generic AI-native SDLC -contracts onto schema-3 stage files. Install the external [`sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) -`ai-native-sdlc` skill for Bootstrap, audit, and incident-to-improvement modes; the repository -checker remains the enforcement source. +contracts onto schema-3 stage files. Use the external [`sdlc-skill`](https://github.com/IchenDEV/sdlc-skill) +`ai-native-sdlc` skill only when guidance is needed to bootstrap or audit the development lifecycle, +or improve it after an incident. Reuse an installed copy first; the repository checker remains +the enforcement source. The documentation check enforces `docs/catalog.json`, archive boundaries, local links, schema-3 stage history, and asset ownership. The plain lifecycle check validates the full Artifact tree. diff --git a/script/dev/run.sh b/script/dev/run.sh index 10e3adaa..9a43bf34 100755 --- a/script/dev/run.sh +++ b/script/dev/run.sh @@ -19,8 +19,6 @@ STATE_DIR="$ROOT_DIR/.codex/run" PID_FILE="$STATE_DIR/codetwo-dev.pid" APP_RUNNER_PID="" -mkdir -p "$STATE_DIR" - # Homebrew keeps versioned Zig formulae keg-only. Keep the project requirement # local to this launcher rather than modifying the user's global shell setup. if [[ -d /opt/homebrew/opt/zig@0.15/bin ]]; then @@ -34,32 +32,41 @@ require_command() { fi } -require_command bun -require_command cargo -require_command zig - -if [[ "$(zig version)" != "0.15.2" ]]; then - echo "C2 requires Zig 0.15.2; found $(zig version)." >&2 - exit 1 -fi - -stop_existing() { +# Inspect only this launcher's tracked owner. This is not a cross-process data-directory lock. +find_existing() { + EXISTING_PID="" if [[ -f "$PID_FILE" ]]; then + local previous_pid previous_command previous_pid="$(<"$PID_FILE")" - if [[ "$previous_pid" =~ ^[0-9]+$ ]] && kill -0 "$previous_pid" >/dev/null 2>&1; then + if [[ "$previous_pid" =~ ^[1-9][0-9]*$ ]] && [[ "$previous_pid" -gt 1 ]] && kill -0 "$previous_pid" >/dev/null 2>&1; then previous_command="$(ps -p "$previous_pid" -o command= 2>/dev/null || true)" - if [[ "$previous_command" == *"$DESKTOP_DIR/build/"*"/Contents/MacOS/launcher"* ]]; then - kill "$previous_pid" >/dev/null 2>&1 || true - for _ in {1..20}; do - kill -0 "$previous_pid" >/dev/null 2>&1 || break - sleep 0.1 - done + if [[ "$previous_command" == "$APP_EXECUTABLE" || "$previous_command" == "$APP_EXECUTABLE "* ]]; then + EXISTING_PID="$previous_pid" fi fi - rm -f "$PID_FILE" fi } +require_stopped() { + find_existing + if [[ -n "$EXISTING_PID" ]]; then + echo "C2 already running (pid: $EXISTING_PID). Use --logs to inspect it or --restart to replace it explicitly." >&2 + exit 1 + fi +} + +restart_existing() { + find_existing + if [[ -z "$EXISTING_PID" ]]; then return; fi + kill "$EXISTING_PID" + for _ in {1..20}; do + if ! kill -0 "$EXISTING_PID" >/dev/null 2>&1; then return; fi + sleep 0.1 + done + echo "C2 process $EXISTING_PID did not exit; refusing to rebuild or start another instance." >&2 + exit 1 +} + cleanup() { if [[ -n "$APP_RUNNER_PID" ]]; then kill "$APP_RUNNER_PID" >/dev/null 2>&1 || true @@ -72,6 +79,13 @@ cleanup() { } build_app() { + require_command bun + require_command cargo + require_command zig + if [[ "$(zig version)" != "0.15.2" ]]; then + echo "C2 requires Zig 0.15.2; found $(zig version)." >&2 + exit 1 + fi cd "$DESKTOP_DIR" if [[ ! -d node_modules ]]; then bun install --frozen-lockfile @@ -102,6 +116,7 @@ build_app() { } start_app() { + mkdir -p "$STATE_DIR" "$APP_EXECUTABLE" & APP_RUNNER_PID=$! echo "$APP_RUNNER_PID" > "$PID_FILE" @@ -120,48 +135,36 @@ wait_for_app() { } case "$MODE" in - run) - stop_existing - trap cleanup EXIT INT TERM - build_app - start_app - wait "$APP_RUNNER_PID" - ;; - --verify|verify) - stop_existing - trap cleanup EXIT INT TERM - build_app - start_app - wait_for_app - wait "$APP_RUNNER_PID" - ;; - --debug|debug) - stop_existing - export RUST_BACKTRACE=1 - export RUST_LOG="${RUST_LOG:-debug}" + run|--verify|verify|--debug|debug|--restart|restart) + if [[ "$MODE" == --restart || "$MODE" == restart ]]; then + restart_existing + else + require_stopped + fi + if [[ "$MODE" == --debug || "$MODE" == debug ]]; then + export RUST_BACKTRACE=1 + export RUST_LOG="${RUST_LOG:-debug}" + fi trap cleanup EXIT INT TERM build_app start_app + if [[ "$MODE" == --verify || "$MODE" == verify ]]; then wait_for_app; fi wait "$APP_RUNNER_PID" ;; - --logs|logs) - stop_existing - trap cleanup EXIT INT TERM - build_app - start_app - wait_for_app - /usr/bin/log stream --info --style compact --predicate "processID == $APP_RUNNER_PID" - ;; - --telemetry|telemetry) - stop_existing - trap cleanup EXIT INT TERM - build_app - start_app - wait_for_app - /usr/bin/log stream --info --style compact --predicate "subsystem == \"$BUNDLE_ID\"" + --logs|logs|--telemetry|telemetry) + find_existing + if [[ -z "$EXISTING_PID" ]]; then + echo "No tracked C2 instance is running. Start it with $0 run first." >&2 + exit 1 + fi + predicate="processID == $EXISTING_PID" + if [[ "$MODE" == --telemetry || "$MODE" == telemetry ]]; then + predicate="$predicate AND subsystem == \"$BUNDLE_ID\"" + fi + exec /usr/bin/log stream --info --style compact --predicate "$predicate" ;; *) - echo "usage: $0 [run|--verify|--debug|--logs|--telemetry]" >&2 + echo "usage: $0 [run|--verify|--debug|--restart|--logs|--telemetry]" >&2 exit 2 ;; esac diff --git a/script/dev/run.test.ts b/script/dev/run.test.ts new file mode 100644 index 00000000..327821bc --- /dev/null +++ b/script/dev/run.test.ts @@ -0,0 +1,99 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +const source = readFileSync(join(import.meta.dir, "run.sh"), "utf8"); + +function launch(mode: string, owner: "live" | "stale" | "unrelated" | "stubborn" = "live") { + const root = mkdtempSync(join(tmpdir(), "codetwo-launch-")); + const write = (path: string, text: string, executable = false) => { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, text); + if (executable) chmodSync(path, 0o755); + }; + try { + const trace = join(root, "trace"); + const pid = join(root, ".codex/run/codetwo-dev.pid"); + const script = join(root, "script/dev/run.sh"); + const bin = join(root, "bin"); + write(trace, ""); + write(pid, "424242\n"); + write(join(root, "alive"), owner === "stale" ? "no" : "yes"); + // Replace only macOS executables; all launcher control flow runs unchanged. + write(script, source.replaceAll("/usr/bin/plutil", '"$PROBE_BIN/plutil"').replaceAll("/usr/bin/log", '"$PROBE_BIN/log"')); + write(join(bin, "plutil"), '#!/bin/bash\ncase "$*" in *CFBundleIdentifier*) echo dev.codetwo.app.dev;; *) echo fixture;; esac\n', true); + write(join(bin, "log"), '#!/bin/bash\necho "log:$*" >> "$PROBE_TRACE"\n', true); + write(join(root, "apps/desktop/build/dev-macos-arm64/C2-dev.app/Contents/MacOS/launcher"), '#!/bin/bash\necho launch >> "$PROBE_TRACE"\n', true); + write(join(root, "mock.sh"), ` +uname() { echo arm64; } +bun() { echo "build:$*" >> "$PROBE_TRACE"; } +cargo() { :; } +zig() { echo 0.15.2; } +sleep() { :; } +ps() { + if [[ "$PROBE_OWNER" == unrelated ]]; then echo unrelated-process; + else echo "$PROBE_ROOT/apps/desktop/build/dev-macos-arm64/C2-dev.app/Contents/MacOS/launcher"; fi +} +kill() { + if [[ "$*" == *424242* ]]; then + if [[ "$1" == -0 ]]; then [[ "$(cat "$PROBE_ROOT/alive")" == yes ]]; return; fi + echo signal-owner >> "$PROBE_TRACE" + if [[ "$PROBE_OWNER" != stubborn ]]; then echo no > "$PROBE_ROOT/alive"; fi + return 0 + fi + builtin kill "$@" +} +`); + const result = spawnSync("bash", [script, mode], { + encoding: "utf8", + timeout: 10_000, + env: { ...process.env, BASH_ENV: join(root, "mock.sh"), PROBE_ROOT: root, PROBE_BIN: bin, + PROBE_TRACE: trace, PROBE_OWNER: owner }, + }); + if (result.error) throw result.error; + return { status: result.status, output: result.stdout + result.stderr, trace: readFileSync(trace, "utf8"), + pid: existsSync(pid) ? readFileSync(pid, "utf8").trim() : null }; + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +test("ordinary launch modes preserve the tracked live owner before building", () => { + for (const mode of ["run", "--verify", "--debug"]) { + const result = launch(mode); + expect(result.status).toBe(1); + expect(result.output).toContain("already running"); + expect(result.trace).toBe(""); + expect(result.pid).toBe("424242"); + } +}); + +test("logs attach without building, replacing, or cleaning up the owner", () => { + for (const mode of ["--logs", "--telemetry"]) { + const result = launch(mode); + expect(result.status).toBe(0); + expect(result.trace).toContain("log:"); + expect(result.trace).toContain("processID == 424242"); + expect(result.trace).not.toContain("build:"); + expect(result.trace).not.toContain("signal-owner"); + expect(result.pid).toBe("424242"); + const stale = launch(mode, "stale"); + expect(stale.status).toBe(1); + expect(stale.trace).toBe(""); + } +}); + +test("only explicit restart replaces an identified owner and waits before rebuilding", () => { + const result = launch("--restart"); + expect(result.status).toBe(0); + expect(result.trace).toContain("signal-owner\nbuild:"); + expect(result.trace).toContain("launch"); + const stubborn = launch("--restart", "stubborn"); + expect(stubborn.status).toBe(1); + expect(stubborn.trace).toBe("signal-owner\n"); + expect(stubborn.pid).toBe("424242"); + const unrelated = launch("--restart", "unrelated"); + expect(unrelated.trace).not.toContain("signal-owner"); +}); diff --git a/script/devflow.test.ts b/script/devflow.test.ts index 422425a2..4a576cd1 100644 --- a/script/devflow.test.ts +++ b/script/devflow.test.ts @@ -1,57 +1,84 @@ import { expect, test } from "bun:test"; import { spawnSync } from "node:child_process"; -import { existsSync, readFileSync, rmSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; -const REPO_ROOT = join(import.meta.dir, ".."); -const CHANGE_ID = "2026-09-02-four-stage-smoke"; +const SOURCE_ROOT = join(import.meta.dir, ".."); +const CHANGE_ID = "2026-09-07-four-stage-smoke"; -function runDevflow(args: string[], env: Record = {}): { status: number; stdout: string; stderr: string } { +function withRepository(check: (root: string) => void): void { + const root = mkdtempSync(join(tmpdir(), "codetwo-devflow-")); + try { + mkdirSync(join(root, "script"), { recursive: true }); + cpSync(join(SOURCE_ROOT, "script/devflow.ts"), join(root, "script/devflow.ts")); + cpSync(join(SOURCE_ROOT, "script/verify"), join(root, "script/verify"), { recursive: true }); + mkdirSync(join(root, "docs/sdlc/changes"), { recursive: true }); + cpSync(join(SOURCE_ROOT, "docs/sdlc/templates"), join(root, "docs/sdlc/templates"), { recursive: true }); + writeFileSync(join(root, "docs/sdlc/workflow.md"), "# Fixture workflow\n"); + check(root); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +function run(root: string, args: string[], env: Record = {}) { const result = spawnSync("bun", ["script/devflow.ts", ...args], { - cwd: REPO_ROOT, - encoding: "utf8", - env: { ...process.env, ...env }, + cwd: root, encoding: "utf8", env: { ...process.env, DEVFLOW_DATE: "2026-09-07", ...env }, }); - return { - status: result.status ?? 1, - stdout: result.stdout.trim(), - stderr: result.stderr.trim(), - }; + return { status: result.status, output: result.stdout + result.stderr }; } -test("devflow new creates only intent.md", () => { - const bundle = join(REPO_ROOT, "docs/sdlc/changes", CHANGE_ID); - try { - const created = runDevflow(["new", "four-stage-smoke", "user", "low"], { DEVFLOW_DATE: "2026-09-02" }); - expect(created.status).toBe(0); - expect(created.stdout).toContain(`${CHANGE_ID}/intent.md`); - expect(existsSync(join(bundle, "intent.md"))).toBe(true); - expect(existsSync(join(bundle, "spec.md"))).toBe(false); - const body = readFileSync(join(bundle, "intent.md"), "utf8"); - expect(body).toContain("schema: 3"); - expect(body).toContain("stage: intent"); - } finally { - rmSync(bundle, { recursive: true, force: true }); - } -}); +function fill(root: string, stage: string): void { + const path = join(root, "docs/sdlc/changes", CHANGE_ID, `${stage}.md`); + writeFileSync(path, readFileSync(path, "utf8") + .replaceAll("[fill]", "Concrete fixture requirement.") + .replace("owner: unassigned", "owner: fixture") + .replace("scope: pending", "scope: README.md") + .replace("status: draft", "status: in-review")); +} -test("devflow enforces intent before spec creation", () => { - const bundle = join(REPO_ROOT, "docs/sdlc/changes", CHANGE_ID); - try { - runDevflow(["new", "four-stage-smoke", "user", "low"], { DEVFLOW_DATE: "2026-09-02" }); - const rejected = runDevflow(["design", CHANGE_ID], { DEVFLOW_DATE: "2026-09-02" }); - expect(rejected.status).not.toBe(0); - expect(rejected.stderr).toContain("must be accepted"); - runDevflow(["approve", CHANGE_ID, "intent", "reviewer"], { DEVFLOW_DATE: "2026-09-02" }); - const created = runDevflow(["design", CHANGE_ID], { DEVFLOW_DATE: "2026-09-02" }); - expect(created.status).toBe(0); - } finally { - rmSync(bundle, { recursive: true, force: true }); - } +test("devflow creates and validates sequential drafts and records explicit review approval", () => { + withRepository(root => { + expect(run(root, ["new", "four-stage-smoke", "user", "low"]).status).toBe(0); + expect(existsSync(join(root, "docs/sdlc/changes", CHANGE_ID, "spec.md"))).toBe(false); + expect(run(root, ["validate"]).status).toBe(0); + expect(run(root, ["design", CHANGE_ID]).output).toContain("must be accepted"); + for (const [stage, next] of [["intent", "design"], ["spec", "plan"], ["plan", "verify"]]) { + fill(root, stage); + expect(run(root, ["validate"]).status).toBe(0); + expect(run(root, ["approve", CHANGE_ID, stage, "reviewer"]).status).toBe(0); + expect(run(root, [next, CHANGE_ID]).status).toBe(0); + expect(run(root, ["validate"]).status).toBe(0); + } + }); }); -test("devflow check-pr requires accepted intent spec plan", () => { - const body = `Change: docs/sdlc/changes/2026-09-02-sdlc-devflow-and-skill-integration`; - const result = runDevflow(["check-pr"], { PR_BODY: body, PR_IS_DRAFT: "true" }); - expect(result.status).toBe(0); +test("Draft PRs can carry valid proposals while Ready PRs require approval and passing evidence", () => { + withRepository(root => { + run(root, ["new", "four-stage-smoke", "user", "low"]); + const draft = { PR_BODY: `Change: docs/sdlc/changes/${CHANGE_ID}`, PR_IS_DRAFT: "true" }; + expect(run(root, ["check-pr"], draft).status).toBe(0); + expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).output).toContain("intent must be accepted"); + for (const [stage, next] of [["intent", "design"], ["spec", "plan"], ["plan", "verify"]]) { + fill(root, stage); + run(root, ["approve", CHANGE_ID, stage, "reviewer"]); + run(root, [next, CHANGE_ID]); + } + const path = join(root, "docs/sdlc/changes", CHANGE_ID, "verification.md"); + const pending = readFileSync(path, "utf8"); + writeFileSync(path, pending.replace("status: pending", "status: in-progress").replace("owner: unassigned", "owner: fixture")); + expect(run(root, ["check-pr"], draft).status).toBe(0); + expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).output).toContain("verification passed"); + writeFileSync(path, pending.replace("status: pending", "status: failed").replace("owner: unassigned", "owner: fixture")); + expect(run(root, ["check-pr"], draft).status).not.toBe(0); + writeFileSync(path, pending.replace("status: pending", "status: passed") + .replace("owner: unassigned", "owner: fixture") + .replace('verified_by: ""', 'verified_by: "fixture"').replace('verified_at: ""', 'verified_at: "2026-09-07"') + .replace("[fill]", "- AC-1: PASS — `fixture-check` passed.") + .replaceAll("[fill]", "Fixture evidence recorded above.") + .replace("Residual risk: pending.", "Residual risk: disposable fixture only.") + .replace("Verdict: pending.", "Verdict: verified.")); + expect(run(root, ["check-pr"], { ...draft, PR_IS_DRAFT: "false" }).status).toBe(0); + }); }); diff --git a/script/devflow.ts b/script/devflow.ts index b52651a5..504306dc 100644 --- a/script/devflow.ts +++ b/script/devflow.ts @@ -6,7 +6,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArtifact } from "./verify/artifact-parse"; -import { CHANGE_ID_RE, STAGE_FILES } from "./verify/stage-bundle"; +import { CHANGE_ID_RE, STAGE_FILES, validateStageBundle } from "./verify/stage-bundle"; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const SDLC_ROOT = join(REPO_ROOT, "docs", "sdlc"); @@ -139,7 +139,7 @@ function cmdApprove(args: string[]): void { if (!APPROVAL_STAGES.has(stage)) fail("approval stage must be intent, spec, or plan"); const path = requireStage(changeId, stage); const metadata = parseArtifact(path).artifact?.metadata ?? {}; - if ((metadata.status ?? "") !== "draft") fail(`${path}: approve expects draft status`); + if (!["draft", "in-review"].includes(metadata.status ?? "")) fail(`${path}: approve expects draft or in-review status`); validateApprover(path, approver); if (stage === "spec" && parseArtifact(requireStage(changeId, "intent")).artifact?.metadata.status !== "accepted") { fail("intent must be accepted before spec approval"); @@ -260,21 +260,20 @@ function cmdCheckPr(): void { const match = body.match(/docs\/sdlc\/changes\/(\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*)/); if (!match) fail("PR body must link docs/sdlc/changes/"); const changeId = match[1]; - for (const stage of ["intent", "spec", "plan"]) { - const path = requireStage(changeId, stage); - if (parseArtifact(path).artifact?.metadata.status !== "accepted") { - fail(`${path}: ${stage} must be accepted before PR gate`); - } - } - const verificationPath = requireStage(changeId, "verification"); - const verificationStatus = parseArtifact(verificationPath).artifact?.metadata.status ?? ""; + const { bundle, errors } = validateStageBundle(REPO_ROOT, changeDir(changeId)); + if (errors.length > 0 || !bundle) fail(errors.join("\n") || "invalid change bundle"); + const verificationStatus = bundle.verification?.metadata.status; if (isDraft === "true") { - if (!["pending", "passed"].includes(verificationStatus)) { - fail(`${verificationPath}: failed verification blocks Draft PRs`); - } - console.log(`devflow: Draft PR gate passed for ${changeId}`); + if (verificationStatus === "failed") fail(`${changeId}: failed verification blocks Draft PRs`); + console.log(`devflow: Draft PR bundle valid for ${changeId}; implementation scope is checked by the branch Gate`); return; } + for (const stage of ["intent", "spec", "plan"] as const) { + if (bundle[stage]?.metadata.status !== "accepted") { + fail(`${changeId}: ${stage} must be accepted before Ready PR`); + } + } + const verificationPath = join(changeDir(changeId), "verification.md"); if (verificationStatus !== "passed") fail(`${verificationPath}: Ready PR requires verification passed`); console.log(`devflow: Ready PR gate passed for ${changeId}`); } diff --git a/script/verify/checks.test.ts b/script/verify/checks.test.ts index f6e3247e..15f0937d 100644 --- a/script/verify/checks.test.ts +++ b/script/verify/checks.test.ts @@ -68,7 +68,7 @@ function stageSections(name: string): string { } return [ common("Automated checks", "- AC-1: PASS — `example-check` passed in the fixed fixture."), - common("Behavioral evidence", "- AC-1: PASS — `example-check` passed in the fixed fixture."), + common("Behavioral evidence", "The acceptance mapping is recorded under Automated checks."), common("Visual evidence", "Not applicable."), common("Security and privacy evidence", "Not applicable."), common("Deviations and residual risk", "Residual risk: the check covers only the fixture."), @@ -320,7 +320,7 @@ test("committed branch Gate requires accepted plan scope", () => { git(root, "commit", "-qm", "baseline"); const base = git(root, "rev-parse", "HEAD"); - writeStageBundle(root, { scope: "README.md" }); + writeStageBundle(root, { scope: "README.md, fixture.txt" }); write(root, "notes.txt", "uncovered change\n"); git(root, "add", "."); git(root, "commit", "-qm", "uncovered implementation"); @@ -353,7 +353,7 @@ test("worktree Gate includes staged and untracked paths", () => { git(root, "add", "."); git(root, "commit", "-qm", "baseline"); - writeStageBundle(root, { scope: "README.md" }); + writeStageBundle(root, { scope: "README.md, fixture.txt" }); write(root, "README.md", "staged change\n"); git(root, "add", "README.md"); write(root, "notes.txt", "untracked change\n"); @@ -369,3 +369,112 @@ test("worktree Gate includes staged and untracked paths", () => { rmSync(root, { recursive: true, force: true }); } }); + + +test("new changes cannot reuse an unchanged historical plan", () => { + for (const operation of ["edit", "delete", "rename"]) { + const root = sdlcRoot(); + try { + writeStageBundle(root, { scope: "README.md, moved.md" }); + write(root, "README.md", "baseline\n"); + git(root, "init", "-q"); + git(root, "config", "user.name", "SDLC Test"); + git(root, "config", "user.email", "sdlc-test@example.invalid"); + git(root, "add", "."); + git(root, "commit", "-qm", "baseline"); + const base = git(root, "rev-parse", "HEAD"); + if (operation === "edit") write(root, "README.md", "new implementation\n"); + if (operation === "delete") rmSync(join(root, "README.md")); + if (operation === "rename") git(root, "mv", "README.md", "moved.md"); + expect(validateRepository(root, undefined, undefined, true).some(e => e.includes("changed bundle"))).toBe(true); + git(root, "add", "."); + git(root, "commit", "-qm", operation); + expect(validateRepository(root, base).some(e => e.includes("changed bundle"))).toBe(true); + write(root, `${BUNDLE_DIR}/plan.md`, readFileSync(join(root, BUNDLE_DIR, "plan.md"), "utf8") + "\nReviewed this change.\n"); + git(root, "add", "."); + git(root, "commit", "-qm", "update governing plan"); + expect(validateRepository(root, base)).toEqual([]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + } +}); + +test("acceptance evidence cannot hide a conflicting or different duplicate", () => { + const root = sdlcRoot(); + try { + const path = join(root, BUNDLE_DIR, "verification.md"); + const original = readFileSync(path, "utf8"); + for (const duplicate of [ + "- AC-1: FAIL — `example-check` failed.", + "- AC-1: PASS — `different-check` passed.", + ]) { + writeFileSync(path, original.replace("The acceptance mapping is recorded under Automated checks.", duplicate)); + expect(validateRepository(root).some(e => e.includes("duplicate verification evidence AC-1"))).toBe(true); + } + writeFileSync(path, original.replace("The acceptance mapping is recorded under Automated checks.", "- AC-1: PASS — `example-check` passed in the fixed fixture.")); + expect(validateRepository(root)).toEqual([]); + writeFileSync(path, original.replace("status: passed", "status: failed")); + expect(validateRepository(root).some(e => e.includes("FAIL mapping"))).toBe(true); + writeFileSync(path, original.replace("status: passed", "status: failed") + .replace("AC-1: PASS", "AC-1: FAIL").replace("Verdict: verified.", "Verdict: failed.")); + expect(validateRepository(root)).toEqual([]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("approved stages reject unfinished markers without rejecting ordinary words", () => { + const root = sdlcRoot(); + try { + const path = join(root, BUNDLE_DIR, "intent.md"); + const original = readFileSync(path, "utf8"); + for (const marker of ["TODO", "TBD", "[fill]", "Finish later: TODO."]) { + writeFileSync(path, original.replace("Real problem and desired outcome.", marker)); + expect(validateRepository(root).some(e => e.includes("cannot contain placeholders"))).toBe(true); + } + writeFileSync(path, original.replace("Real problem and desired outcome.", "Document Todoist and TODO_LIST behavior.")); + expect(validateRepository(root)).toEqual([]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("sequential draft stages validate without granting implementation or release", () => { + const root = sdlcRoot(); + try { + const dir = join(root, BUNDLE_DIR); + const stages = ["intent", "spec", "plan", "verification"]; + const original = stages.map(name => readFileSync(join(dir, `${name}.md`), "utf8")); + for (let last = 0; last < stages.length; last += 1) { + for (let i = 0; i < stages.length; i += 1) { + const path = join(dir, `${stages[i]}.md`); + if (i > last) rmSync(path, { force: true }); + else writeFileSync(path, i === last + ? original[i].replace(/status: (accepted|passed)/, `status: ${i === 3 ? "in-progress" : "in-review"}`) + : original[i]); + } + expect(validateRepository(root)).toEqual([]); + expect(validateRepository(root, undefined, BUNDLE_ID).some(e => e.includes("verification passed"))).toBe(true); + } + // A valid proposal can be reviewed, but cannot authorize a code edit. + writeFileSync(join(dir, "verification.md"), original[3].replace("status: passed", "status: pending")); + git(root, "init", "-q"); + git(root, "config", "user.name", "SDLC Test"); + git(root, "config", "user.email", "sdlc-test@example.invalid"); + git(root, "add", "."); + git(root, "commit", "-qm", "baseline"); + writeFileSync(join(dir, "plan.md"), original[2].replace("status: accepted", "status: in-review")); + rmSync(join(dir, "verification.md")); + expect(validateRepository(root, undefined, undefined, true)).toEqual([]); + write(root, "README.md", "implementation before plan acceptance"); + expect(validateRepository(root, undefined, undefined, true).some(e => e.includes("changed bundle"))).toBe(true); + writeFileSync(join(dir, "plan.md"), original[2]); + rmSync(join(dir, "spec.md")); + expect(validateRepository(root).some(e => e.includes("requires preceding spec.md"))).toBe(true); + writeFileSync(join(dir, "spec.md"), original[1].replace("status: accepted", "status: draft")); + expect(validateRepository(root).some(e => e.includes("spec must be accepted"))).toBe(true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/script/verify/sdlc.ts b/script/verify/sdlc.ts index 8845565c..e514f81f 100644 --- a/script/verify/sdlc.ts +++ b/script/verify/sdlc.ts @@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { basename, dirname, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -132,14 +132,8 @@ function parseChangedPaths(output: string): ChangedPath[] { } function gateChangedPaths(changes: ChangedPath[]): string[] { - return changes.flatMap((change) => { - if (change.status === "D") return []; - if (/^R/.test(change.status)) { - const destination = change.paths.at(-1); - return destination ? [destination] : []; - } - return change.paths; - }); + // Deletions and both sides of renames require approval just like additions. + return changes.flatMap((change) => change.paths); } function legacyChangePathsInDiff(changes: ChangedPath[]): string[] { @@ -183,12 +177,6 @@ function changedPaths( return { changes, errors: [] }; } -function bundleForStagePath(bundles: Map, path: string): StageBundle | undefined { - const match = path.match(/^docs\/sdlc\/changes\/([^/]+)\//); - if (!match) return undefined; - return bundles.get(match[1]); -} - function validateChangedArtifactGate( root: string, base: string | undefined, @@ -211,14 +199,16 @@ function validateChangedArtifactGate( .filter(Boolean) as string[], ); - const readyBundles = Array.from(bundles.values()).filter(bundleIsImplementationReady); + const readyBundles = Array.from(bundles.values()).filter( + (bundle) => changedBundleIds.has(bundle.id) && bundleIsImplementationReady(bundle), + ); const nonStageChanges = Array.from(changed).filter( (path) => !isCanonicalStagePath(path) && !path.match(/docs\/sdlc\/changes\/[^/]+\/evidence\//), ); if (nonStageChanges.length > 0 && readyBundles.length === 0) { return [ - "repository implementation changes require intent, spec, and plan accepted in a schema-3 bundle", + "repository implementation changes require intent, spec, and plan accepted in a changed bundle", ]; } @@ -227,20 +217,6 @@ function validateChangedArtifactGate( return [`${path}: changed path is not covered by an accepted plan scope`]; } - for (const bundleId of changedBundleIds) { - const bundle = bundles.get(bundleId); - if (!bundle) continue; - if (bundle.intent.metadata.status !== "accepted") { - return [`docs/sdlc/changes/${bundleId}/intent.md: intent must be accepted before bundle changes merge`]; - } - if (bundle.spec.metadata.status !== "accepted") { - return [`docs/sdlc/changes/${bundleId}/spec.md: spec must be accepted before bundle changes merge`]; - } - if (bundle.plan.metadata.status !== "accepted") { - return [`docs/sdlc/changes/${bundleId}/plan.md: plan must be accepted before bundle changes merge`]; - } - } - return []; } @@ -248,14 +224,15 @@ function validateReleaseGate(bundles: Map, changeId: string const normalized = changeId.startsWith("change-") ? changeId.slice("change-".length) : changeId; const bundle = bundles.get(normalized); if (!bundle) return [`release change bundle not found: ${changeId}`]; - if (bundle.verification.metadata.status !== "passed") { + const verification = bundle.verification; + if (verification?.metadata.status !== "passed") { return [`release change ${changeId} requires verification passed`]; } - const target = bundle.verification.metadata.release_target ?? ""; + const target = verification.metadata.release_target ?? ""; if (!isConcrete(target) || target.toLowerCase().replace(/\.$/, "") === "none") { return [`release change ${changeId} requires a concrete release_target`]; } - const review = bundle.verification.sections["review and release"] ?? ""; + const review = verification.sections["review and release"] ?? ""; if (!isConcrete(labelValue(review, "Approval"))) { return [`release change ${changeId} requires release Approval`]; } diff --git a/script/verify/stage-bundle.ts b/script/verify/stage-bundle.ts index fc364663..a7bc81a9 100644 --- a/script/verify/stage-bundle.ts +++ b/script/verify/stage-bundle.ts @@ -3,8 +3,6 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path"; import { duplicates, - hasBlocker, - hasLinkTo, hasVerificationEvidence, isConcrete, isValidDate, @@ -28,7 +26,7 @@ export const CHANGE_ID_RE = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/; const RISK_LEVELS = new Set(["low", "medium", "high", "critical"]); const INDEPENDENT_RISK_LEVELS = new Set(["high", "critical"]); const VERIFICATION_MODES = new Set(["owner", "fresh-context", "human", "pair"]); -const PLACEHOLDER_BODY_RE = /\[fill\]|(^|[^[:alnum:]_])(TODO|TBD)([^[:alnum:]_]|$)/i; +const PLACEHOLDER_BODY_RE = /\[fill\]|(^|[^\p{L}\p{N}_])(TODO|TBD)([^\p{L}\p{N}_]|$)/iu; const INTENT_HEADINGS = [ "problem", @@ -78,9 +76,9 @@ export interface StageBundle { id: string; dir: string; intent: Artifact; - spec: Artifact; - plan: Artifact; - verification: Artifact; + spec?: Artifact; + plan?: Artifact; + verification?: Artifact; } function display(path: string): string { @@ -154,16 +152,21 @@ function validateAcceptanceCriteria(spec: Artifact): string[] { return errors; } -function validateVerificationEvidence(bundle: StageBundle): string[] { +function validateVerificationEvidence(intent: Artifact, spec: Artifact, verification: Artifact): string[] { const errors: string[] = []; - const verification = bundle.verification; const path = display(verification.path); const status = verification.metadata.status ?? ""; if (status !== "passed" && status !== "failed") return errors; - const criteria = v2Criteria(bundle.spec.sections["acceptance criteria"] ?? ""); + const criteria = v2Criteria(spec.sections["acceptance criteria"] ?? ""); const evidenceText = `${verification.sections["automated checks"] ?? ""}\n${verification.sections["behavioral evidence"] ?? ""}`; - const evidence = v2Evidence(evidenceText); + // Historical bundles repeat identical mappings in both evidence sections. + const evidence = Array.from( + new Map(v2Evidence(evidenceText).map((item) => [JSON.stringify(item), item])).values(), + ); + for (const id of duplicates(evidence.map((item) => item.id))) { + errors.push(`${path}: duplicate verification evidence ${id}`); + } const evidenceById = new Map(evidence.map((item) => [item.id, item])); const criterionIds = new Set(criteria.map((item) => item.id)); @@ -215,8 +218,8 @@ function validateVerificationEvidence(bundle: StageBundle): string[] { } if ( status === "passed" && - INDEPENDENT_RISK_LEVELS.has(bundle.intent.metadata.risk ?? "") && - normalizedActor(verification.metadata.verified_by) === normalizedActor(bundle.intent.metadata.owner) + INDEPENDENT_RISK_LEVELS.has(intent.metadata.risk ?? "") && + normalizedActor(verification.metadata.verified_by) === normalizedActor(intent.metadata.owner) ) { errors.push(`${path}: high/critical verification requires an independent verifier`); } @@ -236,70 +239,55 @@ export function validateStageBundle(root: string, bundleDir: string): { bundle: const stages: Partial> = {}; for (const fileName of STAGE_FILES) { const path = join(bundleDir, fileName); - if (!existsSync(path)) { - errors.push(`${display(path)}: missing required stage file`); - continue; - } + if (!existsSync(path)) continue; const parsed = parseArtifact(path); errors.push(...parsed.errors); if (parsed.artifact) stages[fileName] = parsed.artifact; } - if (errors.length > 0 || !stages["intent.md"] || !stages["spec.md"] || !stages["plan.md"] || !stages["verification.md"]) { - return { bundle: null, errors }; - } - const intent = stages["intent.md"]; + if (!intent) return { bundle: null, errors: [...errors, `${display(bundleDir)}: missing intent.md`] }; const spec = stages["spec.md"]; const plan = stages["plan.md"]; const verification = stages["verification.md"]; const risk = intent.metadata.risk ?? ""; - - errors.push(...validateStageCommon(intent, "intent", new Set(["draft", "accepted", "rejected"]), bundleId)); - errors.push(...validateStageCommon(spec, "spec", new Set(["draft", "accepted", "rejected"]), bundleId)); - errors.push(...validateStageCommon(plan, "plan", new Set(["draft", "accepted", "rejected"]), bundleId)); - errors.push( - ...validateStageCommon(verification, "verification", new Set(["pending", "passed", "failed"]), bundleId), - ); - - if (spec.metadata.based_on !== "intent.md") errors.push(`${display(spec.path)}: based_on must be intent.md`); - if (plan.metadata.based_on !== "spec.md") errors.push(`${display(plan.path)}: based_on must be spec.md`); - if (verification.metadata.based_on !== "plan.md") errors.push(`${display(verification.path)}: based_on must be plan.md`); - if (spec.metadata.risk !== risk) errors.push(`${display(spec.path)}: risk must match intent`); - if (plan.metadata.risk !== risk) errors.push(`${display(plan.path)}: risk must match intent`); - - for (const heading of INTENT_HEADINGS) errors.push(requireHeading(intent, heading) ?? ""); - for (const heading of SPEC_HEADINGS) errors.push(requireHeading(spec, heading) ?? ""); - for (const heading of PLAN_HEADINGS) errors.push(requireHeading(plan, heading) ?? ""); - for (const heading of VERIFICATION_HEADINGS) errors.push(requireHeading(verification, heading) ?? ""); - errors.push(...validateAcceptanceCriteria(spec)); - - const intentStatus = intent.metadata.status ?? ""; - const specStatus = spec.metadata.status ?? ""; - const planStatus = plan.metadata.status ?? ""; - const verificationStatus = verification.metadata.status ?? ""; - - if (intentStatus === "accepted") errors.push(...validateApproval(intent, risk)); - if (specStatus === "accepted") { - if (intentStatus !== "accepted") errors.push(`${display(spec.path)}: intent must be accepted before spec`); - errors.push(...validateApproval(spec, risk)); - } - if (planStatus === "accepted") { - if (specStatus !== "accepted") errors.push(`${display(plan.path)}: spec must be accepted before plan`); - errors.push(...validateApproval(plan, risk)); - if (isConcrete(plan.metadata.scope)) errors.push(...validateScope(plan.metadata.scope, display(plan.path))); - } - if ((specStatus === "accepted" || planStatus === "accepted" || verificationStatus === "passed") && intentStatus !== "accepted") { - errors.push(`${display(intent.path)}: intent must be accepted before later stages advance`); + const headings = [INTENT_HEADINGS, SPEC_HEADINGS, PLAN_HEADINGS, VERIFICATION_HEADINGS]; + + for (const [index, fileName] of STAGE_FILES.entries()) { + const stage = stages[fileName]; + if (!stage) continue; + const name = fileName.replace(".md", ""); + const statuses = index === 3 + ? new Set(["pending", "in-progress", "passed", "failed"]) + : new Set(["draft", "in-review", "accepted", "rejected"]); + errors.push(...validateStageCommon(stage, name, statuses, bundleId)); + for (const heading of headings[index]) errors.push(requireHeading(stage, heading) ?? ""); + if (index > 0) { + const previousName = STAGE_FILES[index - 1]; + const previous = stages[previousName]; + if (!previous) errors.push(`${display(stage.path)}: requires preceding ${previousName}`); + else if (previous.metadata.status !== "accepted") { + errors.push(`${display(stage.path)}: ${previousName.replace(".md", "")} must be accepted before ${name}`); + } + if (stage.metadata.based_on !== previousName) { + errors.push(`${display(stage.path)}: based_on must be ${previousName}`); + } + } + if (index < 3) { + if (stage.metadata.risk !== risk) errors.push(`${display(stage.path)}: risk must match intent`); + if (stage.metadata.status === "accepted") errors.push(...validateApproval(stage, risk)); + } } - if ((verificationStatus === "passed" || verificationStatus === "failed") && planStatus !== "accepted") { - errors.push(`${display(verification.path)}: plan must be accepted before verification verdict`); + if (spec) errors.push(...validateAcceptanceCriteria(spec)); + if (plan?.metadata.status === "accepted") { + if (!isConcrete(plan.metadata.scope)) errors.push(`${display(plan.path)}: plan requires explicit scope`); + else errors.push(...validateScope(plan.metadata.scope, display(plan.path))); } - if (verificationStatus === "passed") { - errors.push(...validateVerificationEvidence({ id: bundleId, dir: bundleDir, intent, spec, plan, verification })); - const placeholder = requireNoPlaceholders(verification.path, readFileSync(verification.path, "utf8")); - if (placeholder) errors.push(placeholder); + if (spec && verification) { + errors.push(...validateVerificationEvidence(intent, spec, verification)); + if (verification.metadata.status === "passed") { + errors.push(requireNoPlaceholders(verification.path, readFileSync(verification.path, "utf8")) ?? ""); + } } - return { bundle: { id: bundleId, dir: bundleDir, intent, spec, plan, verification }, errors: errors.filter(Boolean), @@ -320,11 +308,13 @@ export function isCanonicalStagePath(path: string): boolean { } export function planCoversPath(bundle: StageBundle, changedPath: string): boolean { - return bundle.plan.metadata.status === "accepted" && scopeCovers(bundle.plan.metadata.scope, changedPath); + return bundleIsImplementationReady(bundle) && scopeCovers(bundle.plan?.metadata.scope, changedPath); } export function bundleIsImplementationReady(bundle: StageBundle): boolean { - return bundle.plan.metadata.status === "accepted"; + return bundle.intent.metadata.status === "accepted" + && bundle.spec?.metadata.status === "accepted" + && bundle.plan?.metadata.status === "accepted"; } export function validateLocalLinks(root: string, path: string): string[] {