@felixdotgo/aidlc-kit 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/DEVELOPMENT.md +1 -1
  2. package/README.md +5 -4
  3. package/dist/assets/.agents/aidlc/conventions.md +8 -8
  4. package/dist/assets/.agents/aidlc/orchestrator.md +15 -10
  5. package/dist/assets/.agents/aidlc/phase-build.md +7 -7
  6. package/dist/assets/.agents/aidlc/phase-clarify.md +3 -3
  7. package/dist/assets/.agents/aidlc/phase-plan.md +6 -6
  8. package/dist/assets/.agents/aidlc/phase-wrap.md +1 -1
  9. package/dist/assets/.agents/aidlc/scripts/assignment.mjs +36 -0
  10. package/dist/assets/.agents/aidlc/scripts/context.mjs +2 -0
  11. package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +2 -0
  12. package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +2 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +181 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/authority.mjs +3 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +8 -1
  16. package/dist/assets/.agents/aidlc/scripts/lib/feature-store.mjs +70 -0
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +215 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +183 -0
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +100 -0
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +58 -0
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +314 -0
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +279 -0
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +51 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-artifacts.mjs +63 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-broker.mjs +53 -0
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-local-broker.mjs +44 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-sandbox.mjs +88 -0
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +89 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-gate-receipt.mjs +32 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +161 -0
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-planner.mjs +13 -0
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +254 -0
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +131 -0
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +55 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +101 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +32 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +65 -0
  38. package/dist/assets/.agents/aidlc/scripts/lib/remote-state-client.mjs +112 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +39 -4
  40. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +90 -0
  41. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +58 -0
  42. package/dist/assets/.agents/aidlc/scripts/loop.mjs +36 -0
  43. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +309 -0
  44. package/dist/assets/.agents/aidlc/scripts/render.mjs +2 -0
  45. package/dist/assets/.agents/aidlc/scripts/state.mjs +67 -11
  46. package/dist/assets/.agents/aidlc/scripts/task-next.mjs +2 -0
  47. package/dist/assets/.agents/aidlc/templates/model-contract.md +3 -3
  48. package/dist/assets/.agents/aidlc/templates/workplan.md +2 -2
  49. package/dist/assets/.agents/skills/aidlc-build/SKILL.md +2 -2
  50. package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +2 -2
  51. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +2 -2
  52. package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +1 -1
  53. package/dist/services/aidlc-coordination/.env.fleet.example +11 -0
  54. package/dist/services/aidlc-coordination/compose.yaml +23 -2
  55. package/dist/services/aidlc-coordination/package-lock.json +1 -0
  56. package/dist/services/aidlc-coordination/package.json +2 -1
  57. package/dist/services/aidlc-coordination/src/coordination.mjs +1 -1
  58. package/dist/services/aidlc-coordination/src/fleet-client.mjs +36 -0
  59. package/dist/services/aidlc-coordination/src/fleet-run.mjs +18 -0
  60. package/dist/services/aidlc-coordination/src/fleet.mjs +233 -0
  61. package/dist/services/aidlc-coordination/src/git-gateway.mjs +50 -0
  62. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +39 -4
  63. package/dist/services/aidlc-coordination/src/repository.mjs +167 -4
  64. package/dist/services/aidlc-coordination/src/server.mjs +53 -9
  65. package/dist/services/aidlc-coordination/test/fleet.test.mjs +351 -0
  66. package/dist/services/aidlc-coordination/test/git-gateway.test.mjs +75 -0
  67. package/dist/services/aidlc-coordination/test/supervisor-fencing.test.mjs +59 -0
  68. package/dist/src/adapters.js +25 -5
  69. package/dist/src/cli.js +66 -6
  70. package/dist/src/gate.js +3 -3
  71. package/dist/src/installer.d.ts +5 -1
  72. package/dist/src/installer.js +114 -30
  73. package/dist/src/managed-content.js +1 -1
  74. package/dist/src/model.d.ts +15 -0
  75. package/dist/src/model.js +1 -1
  76. package/dist/src/profiles.d.ts +3 -1
  77. package/dist/src/profiles.js +26 -3
  78. package/dist/src/state.d.ts +7 -0
  79. package/dist/src/state.js +54 -3
  80. package/dist/src/upgrade.d.ts +2 -2
  81. package/dist/src/upgrade.js +39 -4
  82. package/dist/src/workflow.d.ts +1 -0
  83. package/dist/src/workflow.js +2 -1
  84. package/docs/README.md +2 -2
  85. package/docs/architecture.md +6 -1
  86. package/docs/command-reference.md +63 -5
  87. package/docs/configuration.md +20 -1
  88. package/docs/coordination-service.md +54 -1
  89. package/docs/development.md +1 -1
  90. package/docs/operating-workflow.md +50 -14
  91. package/docs/testing-and-release.md +47 -4
  92. package/package.json +2 -2
package/DEVELOPMENT.md CHANGED
@@ -17,7 +17,7 @@ npm run lint
17
17
  - [Development guide](./docs/development.md) — dependencies, repository layout, implementation boundaries, and local commands.
18
18
  - [Testing and release](./docs/testing-and-release.md) — focused tests, package smoke test, evaluator, and release readiness.
19
19
  - [Architecture](./docs/architecture.md) — module ownership, data flow, and installed-runtime parity.
20
- - [Configuration reference](./docs/configuration.md) — profiles, rules, Codex/Claude permissions, and project-owned configuration.
20
+ - [Configuration reference](./docs/configuration.md) — profiles, rules, provider permissions, and project-owned configuration.
21
21
 
22
22
  ## Documentation policy
23
23
 
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # aidlc-kit
2
2
 
3
- `@felixdotgo/aidlc-kit` installs a local AI-DLC workflow into an existing project. The workflow gives coding agents a shared task lifecycle, explicit human review gates, and project-owned state and rules.
3
+ `@felixdotgo/aidlc-kit` installs an AI-DLC workflow into an existing project. The workflow gives coding agents a shared task lifecycle, dependency-aware work, explicit human review gates, and project-owned state and rules. The normal lead runs in your Codex, Claude Code, or OpenCode conversation; an optional coordination service supports workers on multiple machines.
4
4
 
5
- It is deliberately local-only: workflow assets are bundled with the package, `remoteUpdates` is `false`, and normal status checks never contact a registry.
5
+ Workflow assets are bundled with the package, `remoteUpdates` is `false`, and normal status checks never contact a registry. Fleet execution is opt-in and uses a separately deployed service. The optional end-to-end loop is also opt-in: select a mode during `init` or a human-run `upgrade`, then start it explicitly with `aidlc-workflow loop start` — selecting a mode never starts or approves anything by itself.
6
6
 
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 20.17 or newer.
10
10
  - An existing project directory.
11
- - Codex or Claude Code.
11
+ - Codex, Claude Code, or OpenCode.
12
12
 
13
13
  ## Quick start
14
14
 
@@ -27,8 +27,9 @@ Use the adapter that matches the coding tool:
27
27
  | --- | --- | --- |
28
28
  | Codex | `codex` | `AGENTS.md` |
29
29
  | Claude Code | `claude` | `CLAUDE.md` and phase skills |
30
+ | OpenCode | `opencode` | `AGENTS.md` |
30
31
 
31
- Use `--all` to install both supported adapters. Codex installs workspace-write sandboxing with on-request approvals and a project execpolicy that permits only AI-DLC lifecycle scripts; trust the project before starting Codex so that it loads the project configuration. Claude Code installs a local permission allowlist for the same lifecycle-script command prefix without granting general Bash access. Use `--force --yes` only for an initial installation when replacing an unmanaged conflicting file is intentional.
32
+ Use `--all` to install every supported adapter. Codex installs workspace-write sandboxing with on-request approvals and a project execpolicy that permits only AI-DLC lifecycle scripts; trust the project before starting Codex so that it loads the project configuration. Claude Code installs a local permission allowlist for the same lifecycle-script command prefix without granting general Bash access. OpenCode installs an owner-scoped block in `AGENTS.md` and uses `.opencode/` only for detection; it writes no OpenCode configuration, MCP, or permission file, so your own settings are not overridden. When Codex and OpenCode are installed together, their blocks merge in the same `AGENTS.md`. Use `--force --yes` only for an initial installation when replacing an unmanaged conflicting file is intentional.
32
33
 
33
34
  Confirm the installed project locally:
34
35
 
@@ -10,14 +10,14 @@ Never edit review artifacts to change state. Use `node .agents/aidlc/scripts/sta
10
10
 
11
11
  ```text
12
12
  clarify/G0_confirm → plan/G1_review → build/G2_codereview → wrap/none → done/none
13
- ↘ paused/handoff → reopened plan, closed, or superseded → fresh successor G0
13
+ ↘ paused/handoff → reopened plan, closed, or superseded → fresh successor at Confirm requirements
14
14
  ```
15
15
 
16
- Each transition requires the preceding approval evidence. Build additionally requires every decision to be `approved`, `changed`, or `dropped`. The latest passing G1 approval starts the current build boundary. Wrap requires current-build verification, current-build review, and current-build G2 approval; evidence from an older build boundary cannot advance a reopened task.
16
+ Each transition requires the preceding approval evidence. Build additionally requires every decision to be `approved`, `changed`, or `dropped`. The latest passing approval at Approve plan starts the current build boundary. Wrap requires current-build verification, current-build review, and current-build approval at Review implementation; evidence from an older build boundary cannot advance a reopened task.
17
17
 
18
18
  Verification is evaluated independently for each affected area and each `test` or `lint` kind recorded after the current build boundary. At least one verification kind must exist per area, and the latest record of every kind present must pass. A lint pass cannot hide a test failure, or vice versa.
19
19
 
20
- `blocked_on_user` is reserved for a gate that is structurally ready and waiting for explicit human approval; wrap has no human gate, so it is rejected there. Every route that leaves a human gate wait, including `paused`, requires the audited form `task status <id> --status <active|paused> --mode audited --reason <reason> --source <source>`, which records a gate-bound diagnostic evidence entry, or a sourced closure/handoff command that leaves its own terminal trail. Entering `paused` from `active` requires the same audited form and records a `Paused while active` diagnostic; resuming `paused → active` needs no audit metadata. `task archive <id> --reason <reason> --source <source>` is the sourced completion alias of `task transition --to done`: it applies the same transition checks and records an `Archive to done` diagnostic. Exactly two compatibility paths exist and neither is a human approval: a migration-stamped legacy G2 wait (the `legacyG2Wait` marker is written only by schema migration and only exempts a still-ready wait from the repair bound; runtime commands consume and clear it) and the command-driven recovery of an invalid `wrap`/`none` wait back to `active`. A real blocker or exhausted repair bound uses `task handoff` with `--kind` exactly one of `repair_exhausted`, `review_exhausted`, `g2_failed`, `release_failed`, `structural_change`, or `other`. Every durable handoff exposes `create_successor`, `supersede`, and `close`; build-phase handoffs additionally expose `reopen_g1`. Reopening G1 resets every non-deferred build item to `todo`. `closed` and `superseded` are terminal non-success outcomes that preserve the phase where work stopped; only `done` is successful completion. A successor is created as a fresh G0 task, then linked atomically with `task supersede`; it never inherits approvals or evidence.
20
+ `blocked_on_user` is reserved for a gate that is structurally ready and waiting for explicit human approval; wrap has no human gate, so it is rejected there. Every route that leaves a human gate wait, including `paused`, requires the audited form `task status <id> --status <active|paused> --mode audited --reason <reason> --source <source>`, which records a gate-bound diagnostic evidence entry, or a sourced closure/handoff command that leaves its own terminal trail. Entering `paused` from `active` requires the same audited form and records a `Paused while active` diagnostic; resuming `paused → active` needs no audit metadata. `task archive <id> --reason <reason> --source <source>` is the sourced completion alias of `task transition --to done`: it applies the same transition checks and records an `Archive to done` diagnostic. Exactly two compatibility paths exist and neither is a human approval: a migration-stamped legacy Review implementation wait (the `legacyG2Wait` marker is written only by schema migration and only exempts a still-ready wait from the repair bound; runtime commands consume and clear it) and the command-driven recovery of an invalid `wrap`/`none` wait back to `active`. A real blocker or exhausted repair bound uses `task handoff` with `--kind` exactly one of `repair_exhausted`, `review_exhausted`, `g2_failed`, `release_failed`, `structural_change`, or `other`. Every durable handoff exposes `create_successor`, `supersede`, and `close`; build-phase handoffs additionally expose `reopen_g1`. Reopening the Approve plan step resets every non-deferred build item to `todo`. `closed` and `superseded` are terminal non-success outcomes that preserve the phase where work stopped; only `done` is successful completion. A successor is created as a fresh task at Confirm requirements, then linked atomically with `task supersede`; it never inherits approvals or evidence.
21
21
 
22
22
  Lifecycle commands that end or redirect work require an explicit reason and source:
23
23
 
@@ -47,7 +47,7 @@ Every `state.mjs` and `task-next.mjs` command prints one JSON envelope: `{ok: tr
47
47
 
48
48
  Gate presentation is executable, not model-authored. After a passing gate check, set `blocked_on_user`, render the phase artifact when required, then run `node .agents/aidlc/scripts/gate-view.mjs <task-id>` and relay its output verbatim. The default Markdown uses a portable `[!IMPORTANT]` blockquote with explicit task state, artifact links, decision/execution counts, evidence, and exactly one `ACTION REQUIRED` line. Use `--format plain` when the tool does not render Markdown and `--format json` for integrations. Renderer-specific color is enhancement only; approval semantics live in canonical state.
49
49
 
50
- G0, G1, and G2 never auto-pass. Gate approval and phase transition are atomic; after a non-terminal transition or item/evidence mutation, continue until the next human gate, real blocker, or completion. Item completion is never a turn-level handoff. The local CLI validates gate readiness and records `--source` as provenance, but without a host-attested channel it cannot prove who authored free-form text; adapters must invoke approval only in a later turn containing explicit user assent.
50
+ The Confirm requirements, Approve plan, and Review implementation steps never auto-pass. Gate approval and phase transition are atomic; after a non-terminal transition or item/evidence mutation, continue until the next human gate, real blocker, or completion. Item completion is never a turn-level handoff. The local CLI validates gate readiness and records `--source` as provenance, but without a host-attested channel it cannot prove who authored free-form text; adapters must invoke approval only in a later turn containing explicit user assent.
51
51
 
52
52
  ## Context budget
53
53
 
@@ -57,12 +57,12 @@ The default phase packet budget is 16,000 characters. Optional rules and diagnos
57
57
 
58
58
  Repair bounds are machine-enforced from current-build evidence; the model does not have to count.
59
59
 
60
- - Verification: at most three failed verify records (`test`/`lint`) per affected area. The third failure flips `nextAction` to `blocked` and adds a `REPAIR_BOUND` error to the G2 gate check.
60
+ - Verification: at most three failed verify records (`test`/`lint`) per affected area. The third failure flips `nextAction` to `blocked` and adds a `REPAIR_BOUND` error to the Review implementation gate check.
61
61
  - Review: at most two failed review records; the second failure does the same with `--kind review_exhausted`.
62
- - Security, migration, contract, or where-logic-lives changes reopen G1 immediately.
62
+ - Security, migration, contract, or where-logic-lives changes reopen the Approve plan step immediately.
63
63
  - A bound hit produces a diagnosis and human escalation; it never silently passes. Follow the handoff command returned by `nextAction` instead of retrying.
64
- - Reopening G1 moves the build boundary, which resets both counters for the new cycle.
64
+ - Reopening the Approve plan step moves the build boundary, which resets both counters for the new cycle.
65
65
 
66
66
  ## Language and scaffold
67
67
 
68
- Technical tokens, paths, commands, enum values, and structural headings remain English. Prose follows the task language. Stable emoji headings are: `📋 Problem`, `🗺️ Affected areas`, `💭 Assumptions`, `❓ Open questions`, `🎯 Scope` (with an inline `**🚫 Out:**` list inside it), `🧩 Solution per affected area`, `📌 Spec traceability`, `🔗 Cross-service contracts`, `⚠️ Risks / edge cases`, `🧩 Decisions`, and `🧩 Tasks`. The intent headings and the design headings are enforced verbatim by `gate-check.mjs` at G0 and G1.
68
+ Technical tokens, paths, commands, enum values, and structural headings remain English. Prose follows the task language. Stable emoji headings are: `📋 Problem`, `🗺️ Affected areas`, `💭 Assumptions`, `❓ Open questions`, `🎯 Scope` (with an inline `**🚫 Out:**` list inside it), `🧩 Solution per affected area`, `📌 Spec traceability`, `🔗 Cross-service contracts`, `⚠️ Risks / edge cases`, `🧩 Decisions`, and `🧩 Tasks`. The intent headings and the design headings are enforced verbatim by `gate-check.mjs` at Confirm requirements and Approve plan.
@@ -9,7 +9,7 @@ This file is the compact kernel contract. Project customization lives under `.ag
9
9
  3. Classify the request as `NEW`, `RESUME`, `SWITCH`, or `OFF-WORKFLOW`.
10
10
  - A requested feature, bug fix, or project change is `NEW` unless canonical state already contains the matching unfinished task; a read-only explanation or inspection is `OFF-WORKFLOW`.
11
11
  - Bootstrap `NEW` with the exact user-supplied canonical ID when present: run `state.mjs task create`, then execute the clarify phase. `task next` applies only after canonical state exists. When the user supplies no ID, derive a short kebab-case slug from the title (lowercase letters, digits, hyphens; it must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`, e.g. `fix-login-retry`) and reuse that exact ID for the whole lifecycle.
12
- - For `SWITCH`, preserve the current task. If it has a durable handoff, ask the user to reopen G1, close it, or create a fresh G0 successor and then supersede it; never mark failed work `done` or inherit approvals into the successor.
12
+ - For `SWITCH`, preserve the current task. If it has a durable handoff, ask the user to reopen the Approve plan step, close it, or create a fresh successor at Confirm requirements and then supersede it; never mark failed work `done` or inherit approvals into the successor.
13
13
  - `task create` fails while another task is actionable (`nextAction` `run_phase`) and returns the actionable task(s) plus the exact `--switch-from <task-id>` acknowledgement. Pass that flag only with explicit user direction to switch; it records a resume-marker diagnostic on each acknowledged task and never pauses or mutates its lifecycle — the acknowledged task remains the authoritative resume target. This guard is local-CLI enforcement; the optional MCP state service does not enforce it.
14
14
  4. For a task, request only its current phase packet with `node .agents/aidlc/scripts/context.mjs <task-id> --phase <phase>`. Do not load every workflow file.
15
15
  5. Use `node .agents/aidlc/scripts/state.mjs` for lifecycle mutations when the active adapter policy is `scripted`. Gate approval always uses the atomic `gate approve` command when scripts are available. Native fallback must apply approval and transition as one logical transaction after the same checks.
@@ -18,17 +18,21 @@ This file is the compact kernel contract. Project customization lives under `.ag
18
18
  Immediately before any final response for a task, run `node .agents/aidlc/scripts/task-next.mjs <task-id> --require-stop` and parse its `continuation` object. Exit code 2 (`continuation.code = CONTINUATION_REQUIRED`) is a hard local guard: execute `continuation.command` instead of replying, and never convert the guard output into a final response or summary. This cannot technically intercept a message already emitted by a model host; if a premature final response slipped out, recover in the very next turn by running `state.mjs task next <task-id>` and executing its returned command — never re-approve a gate to catch up.
19
19
  7. Use `node .agents/aidlc/scripts/render.mjs <task-id>` for the selected task review artifact. `--all` is explicit maintenance only. Never persist a second lifecycle-state projection such as `BOARD.md`.
20
20
 
21
- Read-only questions remain off-workflow and do not create state. Multiple tasks may exist, but a session builds one task at a time.
21
+ Read-only questions remain off-workflow and do not create state. The current Codex, Claude Code, or OpenCode conversation is the interactive lead for local mode. It keeps following fresh `nextAction` in the same conversation, uses native host delegation for independent items, and presents gates here. A later explicit user reply approves a gate. No nested Codex/Claude/OpenCode CLI process is needed for interactive mode.
22
22
 
23
- ## Gates
23
+ On `resume <task-id>` in a reopened conversation, first read `task next` and `task show`, then run `assignment.mjs reconcile <task-id>` and inspect each `reported` or `uncertain` assignment against canonical item status, owned files, and worker output. Resume only work still required by canonical state. A stale assignment never authorizes replaying a lifecycle mutation or gate approval. Local assignment records under `.agents/data/loop/assignments/` are scheduling metadata, not lifecycle authority. Fleet mode uses an explicitly selected remote authority and fenced service claims; never silently switch between local and remote state.
24
24
 
25
- - `G0_confirm`: the user confirms intent and scope.
26
- - `G1_review`: the user resolves every design decision and approves the plan.
27
- - `G2_codereview`: verification and adversarial review have run; the user reviews the code.
25
+ ## Review steps
28
26
 
29
- Before presenting a gate, run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate <gate>`, set the validated wait with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`, render the task artifact when the phase requires it, then run `node .agents/aidlc/scripts/gate-view.mjs <task-id>` and relay that output verbatim. Structural errors must be fixed. If G2 cannot pass or a repair bound is exhausted, record diagnostic evidence and use `task handoff`; do not set `blocked_on_user` or suggest gate approval. Semantic fidelity remains an LLM/human responsibility. Never mark work complete without executable evidence.
27
+ - **Confirm requirements** (`G0_confirm`): the user confirms requirements and scope.
28
+ - **Approve plan** (`G1_review`): the user resolves every design decision and approves the plan.
29
+ - **Review implementation** (`G2_codereview`): verification and adversarial review have run; the user reviews the implementation.
30
30
 
31
- Transitions are enforced by the state machine. A gate may not be skipped, and unresolved decisions block build. G2 always requires human approval.
31
+ Use these English names in user-facing communication and keep gate IDs for commands and structured state. The review-step names do not depend on the task language.
32
+
33
+ Before presenting a gate, run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate <gate>`, set the validated wait with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`, render the task artifact when the phase requires it, then run `node .agents/aidlc/scripts/gate-view.mjs <task-id>` and relay that output verbatim. Structural errors must be fixed. If Review implementation cannot pass or a repair bound is exhausted, record diagnostic evidence and use `task handoff`; do not set `blocked_on_user` or suggest gate approval. Semantic fidelity remains an LLM/human responsibility. Never mark work complete without executable evidence.
34
+
35
+ Transitions are enforced by the state machine. A gate may not be skipped, and unresolved decisions block build. Review implementation always requires human approval.
32
36
 
33
37
  ## Quality contract
34
38
 
@@ -37,8 +41,8 @@ Transitions are enforced by the state machine. A gate may not be skipped, and un
37
41
  - Trace quantified rules, enumerations, and contracts to an exact source.
38
42
  - Run the narrowest meaningful verification for every affected area.
39
43
  - Review only the task diff against its approved decisions and spec anchors.
40
- - Repair bounds (three verify cycles per area, two review passes) are machine-enforced from evidence: the bound hit flips `nextAction` to `blocked` and closes G2 with a `REPAIR_BOUND` error. Follow the returned handoff command and escalate with evidence.
41
- - Structural changes discovered during build reopen G1.
44
+ - Repair bounds (three verify cycles per area, two review passes) are machine-enforced from evidence: the bound hit flips `nextAction` to `blocked` and closes Review implementation with a `REPAIR_BOUND` error. Follow the returned handoff command and escalate with evidence.
45
+ - Structural changes discovered during build reopen the Approve plan step.
42
46
 
43
47
  Economy models use the same bounded packet contract, gates, executable checks, and quality rules. Escalate to a stronger model or human for security, migrations, cross-service contracts, ambiguous specs, or exhausted repair bounds.
44
48
 
@@ -47,6 +51,7 @@ Economy models use the same bounded packet contract, gates, executable checks, a
47
51
  - At every phase entry and dependency boundary, the lead must triage whether independent, valuable work can proceed in parallel. Fan out only such work, and use at most the available collaboration slots minus the lead; keep dependent or low-value work local.
48
52
  - The lead is the single writer for lifecycle state, gate actions, and task review artifacts. Workers never approve or present gates, mutate lifecycle records, or make canonical artifact decisions; every human-gate and state-machine invariant remains in force.
49
53
  - Each worker assignment must name a bounded objective, owned paths or read-only scope, dependencies, expected verification, and required return output (summary, changed paths, evidence, and concerns). The lead integrates and verifies worker results before recording evidence or advancing the task.
54
+ - In local interactive mode, before dispatching a canonical build item, run `assignment.mjs claim <task-id> <item-id> --worker <worker-id> --paths <comma-separated-owned-paths>`. Pass the returned assignment ID to the native worker. Use `assignment.mjs heartbeat` for long work and `assignment.mjs report` when it returns. After integration and canonical item completion, run `assignment.mjs reconcile`. An expired claim becomes `uncertain`; inspect its files and report before `assignment.mjs release` and any retry. Do not use assignments to store approvals or mark canonical items done.
50
55
  - Assign one writer per path. Partition work so concurrent changes cannot overlap; workers may inspect shared files, but must report a needed cross-owner change to the lead rather than edit it. The lead resolves conflicts and performs final integration.
51
56
 
52
57
  ## Customization layers
@@ -1,20 +1,20 @@
1
- # AI-DLC · Build (G2)
1
+ # AI-DLC · Build (Review implementation)
2
2
 
3
- Precondition: G1 approval evidence exists and every decision is resolved.
3
+ Precondition: approval evidence for Approve plan exists and every decision is resolved.
4
4
 
5
- Use the shared orchestration contract in `orchestrator.md`. The lead may assign a batch of independent canonical items to workers only after giving each one exclusive file/area ownership; shared/public surfaces, overlapping paths, lifecycle actions, gates, final integration, and task-wide review remain lead-owned. The lead integrates and inspects each result before marking items or recording any lifecycle evidence/state mutation.
5
+ Use the shared orchestration contract in `orchestrator.md`. The current client conversation is the interactive lead. The lead may assign a batch of independent canonical items to native host workers only after giving each one exclusive file/area ownership and recording its local assignment claim; shared/public surfaces, overlapping paths, lifecycle actions, gates, final integration, and task-wide review remain lead-owned. The lead integrates and inspects each result before marking items or recording any lifecycle evidence/state mutation. On a reopened conversation, reconcile claims and canonical state before dispatch.
6
6
 
7
7
  1. Read the build phase packet; re-open cited spec lines immediately before implementing dependent behavior.
8
8
  2. Read `templates/model-contract.md` and apply its COSTARS build emphasis to the implementation context.
9
- 3. Loop over canonical task items, one item and one affected area at a time, or a safe batch of such independent items under the shared orchestration contract. Prefer an `in_progress` item, otherwise select the next `todo` item; inspect callers before shared/public changes and preserve unrelated work. For a batch, wait for all assigned results, integrate and narrowly verify each owned area, then have the lead record progress and mark each item `done` or explicitly `deferred` with `node .agents/aidlc/scripts/state.mjs task item <task-id> <item-id> --status done|deferred`. Parse every returned `nextAction` immediately and stop for a `blocked` or `terminal` action; otherwise, after the batch use only the final/current authoritative `nextAction` to continue. An item or area completion may be reported only as commentary/progress, never as a final response. Continue the loop while `nextAction.classification` is `run_phase`; when no actionable item remains, continue directly into aggregate verification, adversarial review, and G2 preparation.
10
- 4. A new structural or spec conflict requires `node .agents/aidlc/scripts/state.mjs task handoff <task-id> --kind structural_change --reason <reason> --source <source>`; after explicit user direction, `task reopen <task-id> --to plan --reason <reason> --source <user-message>` invalidates the prior G1 approval. Do not spend repair cycles on an unapproved workaround.
11
- 5. Run the narrowest configured test/lint per area. Record every result with `node .agents/aidlc/scripts/state.mjs evidence add <task-id> --kind test|lint --area <area> --result pass|fail|skip --source <command-or-tool> --detail <summary>`; a skip must state residual risk in `--detail`. The repair bound is machine-enforced: after three failed verify records in one area (current build), `nextAction` turns `blocked` and G2 is structurally closed — record the failed diagnostic and follow the returned handoff action instead of retrying.
9
+ 3. Loop over canonical task items, one item and one affected area at a time, or a safe batch of such independent items under the shared orchestration contract. Prefer an `in_progress` item, otherwise select the next `todo` item; inspect callers before shared/public changes and preserve unrelated work. For a batch, wait for all assigned results, integrate and narrowly verify each owned area, then have the lead record progress and mark each item `done` or explicitly `deferred` with `node .agents/aidlc/scripts/state.mjs task item <task-id> <item-id> --status done|deferred`. Parse every returned `nextAction` immediately and stop for a `blocked` or `terminal` action; otherwise, after the batch use only the final/current authoritative `nextAction` to continue. An item or area completion may be reported only as commentary/progress, never as a final response. Continue the loop while `nextAction.classification` is `run_phase`; when no actionable item remains, continue directly into aggregate verification, adversarial review, and Review implementation preparation.
10
+ 4. A new structural or spec conflict requires `node .agents/aidlc/scripts/state.mjs task handoff <task-id> --kind structural_change --reason <reason> --source <source>`; after explicit user direction, `task reopen <task-id> --to plan --reason <reason> --source <user-message>` invalidates the prior Approve plan approval. Do not spend repair cycles on an unapproved workaround.
11
+ 5. Run the narrowest configured test/lint per area. Record every result with `node .agents/aidlc/scripts/state.mjs evidence add <task-id> --kind test|lint --area <area> --result pass|fail|skip --source <command-or-tool> --detail <summary>`; a skip must state residual risk in `--detail`. The repair bound is machine-enforced: after three failed verify records in one area (current build), `nextAction` turns `blocked` and Review implementation is structurally closed — record the failed diagnostic and follow the returned handoff action instead of retrying.
12
12
  6. Perform an adversarial review of only the task diff against decisions and spec anchors, using every CRITICS section from `templates/model-contract.md`. Record findings and the outcome with `evidence add <task-id> --kind review --result pass|fail --source <reviewer> --detail <findings>`. The review bound is machine-enforced after two failed review records; the returned handoff action uses `--kind review_exhausted`.
13
13
  7. Handoff kinds are exactly: `repair_exhausted`, `review_exhausted`, `g2_failed`, `release_failed`, `structural_change`, `other`.
14
14
  8. Mark items and record per-area verify evidence, render the workplan, and run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate G2_codereview`. Only after the check passes, set task status to `blocked_on_user` with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`.
15
15
  9. Run `node .agents/aidlc/scripts/gate-view.mjs <task-id>`, relay it verbatim, and stop for human review. Presenting the gate ends this turn.
16
16
 
17
- ## ONLY in a later turn, after the user explicitly approves G2
17
+ ## ONLY in a later turn, after the user explicitly approves the result at the Review implementation step
18
18
 
19
19
  Explicit approval is a user message that says yes to this gate. Then — and only then — run `node .agents/aidlc/scripts/state.mjs gate approve <task-id> --gate G2_codereview --source <the-user-approval-message>`, parse the returned `nextAction`, and execute its `run_phase` command before emitting any final response. Continue through `wrap` to `complete` unless delivery authority or a real blocker is missing; never invent approval. If the user instead requests fixes, record the rejection as review evidence (`--kind review --result fail`), cancel the wait with `task status <task-id> --status active --mode audited --reason <requested-changes> --source <the-user-message>`, and continue the build loop.
20
20
 
@@ -1,4 +1,4 @@
1
- # AI-DLC · Clarify (G0)
1
+ # AI-DLC · Clarify (Confirm requirements)
2
2
 
3
3
  Goal: confirm intent before discovery cost.
4
4
 
@@ -11,9 +11,9 @@ When independent, read-only index or ambiguity checks exist, use the shared orch
11
11
  5. Run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate G0_confirm` and fix every structural error. Only after it passes, set the gate wait with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`, then render only this task with `node .agents/aidlc/scripts/render.mjs <task-id>`.
12
12
  6. Run `node .agents/aidlc/scripts/gate-view.mjs <task-id>`, relay the output verbatim, and stop. Presenting the gate ends this turn.
13
13
 
14
- ## ONLY in a later turn, after the user explicitly approves G0
14
+ ## ONLY in a later turn, after the user explicitly confirms the requirements at the Confirm requirements step
15
15
 
16
- Explicit approval is a user message that says yes to this gate (for example "ok", "approve", "duyệt"). Resolving questions or requesting changes is NOT approval. Then — and only then — run `node .agents/aidlc/scripts/state.mjs gate approve <task-id> --gate G0_confirm --source <the-user-approval-message>`, parse the returned `nextAction`, and execute its `run_phase` command before emitting any final response. Continue into `plan` until G1 or a real blocker; never invent approval. If the user instead requests changes, revise the intent while the task waits, or cancel the wait explicitly with `task status <task-id> --status active --mode audited --reason <what-changed> --source <the-user-message>`.
16
+ Explicit approval is a user message that says yes to this gate (for example "ok", "approve"). Resolving questions or requesting changes is NOT approval. Then — and only then — run `node .agents/aidlc/scripts/state.mjs gate approve <task-id> --gate G0_confirm --source <the-user-approval-message>`, parse the returned `nextAction`, and execute its `run_phase` command before emitting any final response. Continue into `plan` until Approve plan or a real blocker; never invent approval. If the user instead requests changes, revise the intent while the task waits, or cancel the wait explicitly with `task status <task-id> --status active --mode audited --reason <what-changed> --source <the-user-message>`.
17
17
 
18
18
  Read-only questions remain off-workflow. The task language is set here and retained throughout the lifecycle.
19
19
 
@@ -1,6 +1,6 @@
1
- # AI-DLC · Plan (G1)
1
+ # AI-DLC · Plan (Approve plan)
2
2
 
3
- Precondition: canonical state contains passing G0 approval evidence.
3
+ Precondition: canonical state contains passing approval evidence for Confirm requirements.
4
4
 
5
5
  Use the shared orchestration contract in `orchestrator.md` to fan out only independent, bounded discovery streams. Workers return summaries and exact anchors; the lead reconciles conflicts, writes design/workplan content, and is the sole lifecycle and gate writer.
6
6
 
@@ -9,12 +9,12 @@ Use the shared orchestration contract in `orchestrator.md` to fan out only indep
9
9
  3. Fill the design template with the solution per affected area, exact sources for quantified rules, contracts, risks, and reuse candidates.
10
10
  4. Add every real judgement call with `node .agents/aidlc/scripts/state.mjs decision set <task-id> <decision-id> --status unresolved --label <label> --resolution <proposed-default>`. Code/spec conflicts and ambiguous enumerations are always decisions.
11
11
  5. Add implementation items with `node .agents/aidlc/scripts/state.mjs task item <task-id> <item-id> --status todo --label <label>` and configure narrow verification commands; render the workplan review artifact with `node .agents/aidlc/scripts/render.mjs <task-id>`.
12
- 6. Unresolved decisions block the G1 gate structurally. Present them to the user and apply each answer with `decision set <task-id> <decision-id> --status approved|changed|dropped --resolution <user-resolution>`. Do not treat a decision answer as gate approval.
13
- 7. When every decision is resolved, run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate G1_review` and fix every structural error. Only after it passes, set the gate wait with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`, run `node .agents/aidlc/scripts/gate-view.mjs <task-id>`, relay it verbatim, and stop. G1 always requires explicit human approval.
12
+ 6. Unresolved decisions block the Approve plan gate structurally. Present them to the user and apply each answer with `decision set <task-id> <decision-id> --status approved|changed|dropped --resolution <user-resolution>`. Do not treat a decision answer as gate approval.
13
+ 7. When every decision is resolved, run `node .agents/aidlc/scripts/gate-check.mjs <task-id> --gate G1_review` and fix every structural error. Only after it passes, set the gate wait with `node .agents/aidlc/scripts/state.mjs task status <task-id> --status blocked_on_user`, run `node .agents/aidlc/scripts/gate-view.mjs <task-id>`, relay it verbatim, and stop. Approve plan always requires explicit human approval.
14
14
 
15
- ## ONLY in a later turn, after the user explicitly approves G1
15
+ ## ONLY in a later turn, after the user explicitly approves the plan at the Approve plan step
16
16
 
17
- Explicit approval is a user message that says yes to this gate (for example "approve", "ok", "duyệt"). Resolving decisions is NOT approval — never run gate approve just because no decision remains unresolved. Then — and only then — run `node .agents/aidlc/scripts/state.mjs gate approve <task-id> --gate G1_review --source <the-user-approval-message>`, parse the returned `nextAction`, and execute its `run_phase` command before emitting any final response. Continue into `build` until G2 or a real blocker; never invent approval. If the user instead requests plan changes, revise the design/workplan, or cancel the wait explicitly with `task status <task-id> --status active --mode audited --reason <what-changed> --source <the-user-message>`.
17
+ Explicit approval is a user message that says yes to this gate (for example "approve", "ok"). Resolving decisions is NOT approval — never run gate approve just because no decision remains unresolved. Then — and only then — run `node .agents/aidlc/scripts/state.mjs gate approve <task-id> --gate G1_review --source <the-user-approval-message>`, parse the returned `nextAction`, and execute its `run_phase` command before emitting any final response. Continue into `build` until Review implementation or a real blocker; never invent approval. If the user instead requests plan changes, revise the design/workplan, or cancel the wait explicitly with `task status <task-id> --status active --mode audited --reason <what-changed> --source <the-user-message>`.
18
18
 
19
19
  Post-condition: the plan names concrete affected areas/paths, traces every relied-upon value to a source, exposes conflicts, and needs no implementation judgement beyond mechanical details.
20
20
 
@@ -1,6 +1,6 @@
1
1
  # AI-DLC · Wrap
2
2
 
3
- Precondition: the user approved G2 and canonical state contains passing G2 approval evidence.
3
+ Precondition: the user approved the implementation at Review implementation and canonical state contains passing approval evidence for that step.
4
4
 
5
5
  Wrap is success-only and has NO human gate (`gate: none`): never set `blocked_on_user` here — the CLI rejects it. `closed` and `superseded` tasks are terminal at the phase where they stopped and never enter wrap or imply release success.
6
6
 
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { claimLocalAssignment, heartbeatLocalAssignment, reconcileLocalAssignments, releaseLocalAssignment, reportLocalAssignment, showLocalAssignments } from "./lib/assignment-store.mjs";
4
+ import { requireLocalAuthority } from "./lib/authority.mjs";
5
+
6
+ try {
7
+ requireLocalAuthority();
8
+ const args = process.argv.slice(2);
9
+ const optionAt = args.findIndex((value) => value.startsWith("--"));
10
+ const [command, taskId, assignmentOrItem] = args.slice(0, optionAt < 0 ? undefined : optionAt);
11
+ const tail = optionAt < 0 ? [] : args.slice(optionAt);
12
+ const options = new Map();
13
+ for (let i = 0; i < tail.length; i += 2) {
14
+ const name = tail[i];
15
+ if (!name?.startsWith("--") || !tail[i + 1] || options.has(name)) throw new Error(`Invalid option: ${name}`);
16
+ options.set(name, tail[i + 1]);
17
+ }
18
+ const allowed = new Set(["--root", "--worker", "--paths", "--lease-ms", "--base-commit", "--branch", "--session-id", "--checkpoint", "--commit", "--changed-paths"]);
19
+ for (const name of options.keys()) if (!allowed.has(name)) throw new Error(`Unknown option: ${name}`);
20
+ const root = resolve(options.get("--root") ?? process.cwd());
21
+ const number = (name, fallback) => options.has(name) ? Number(options.get(name)) : fallback;
22
+ let result;
23
+ if (command === "show" && taskId && !assignmentOrItem) result = showLocalAssignments(root, taskId);
24
+ else if (command === "reconcile" && taskId && !assignmentOrItem) result = reconcileLocalAssignments(root, taskId);
25
+ else if (command === "claim" && taskId && assignmentOrItem) result = claimLocalAssignment(root, {
26
+ taskId, itemId: assignmentOrItem, workerId: options.get("--worker") ?? "", ownedPaths: (options.get("--paths") ?? "").split(","), leaseMs: number("--lease-ms", 300000), baseCommit: options.get("--base-commit") ?? null, branch: options.get("--branch") ?? null, sessionId: options.get("--session-id") ?? null
27
+ });
28
+ else if (command === "heartbeat" && taskId && assignmentOrItem) result = heartbeatLocalAssignment(root, taskId, assignmentOrItem, number("--lease-ms", 300000), options.get("--session-id"));
29
+ else if (command === "report" && taskId && assignmentOrItem) result = reportLocalAssignment(root, taskId, assignmentOrItem, { checkpoint: options.get("--checkpoint") ?? null, commit: options.get("--commit") ?? null, changedPaths: options.has("--changed-paths") ? options.get("--changed-paths").split(",") : [] });
30
+ else if (command === "release" && taskId && assignmentOrItem) result = releaseLocalAssignment(root, taskId, assignmentOrItem);
31
+ else throw new Error("Usage: assignment.mjs show|reconcile <task-id> | claim <task-id> <item-id> --worker <id> --paths <comma-separated-paths> | heartbeat|report|release <task-id> <assignment-id>");
32
+ console.log(JSON.stringify({ ok: true, result }, null, 2));
33
+ } catch (error) {
34
+ console.error(error instanceof Error ? error.message : String(error));
35
+ process.exitCode = 1;
36
+ }
@@ -1,6 +1,8 @@
1
1
  import { compileContext, failEntry, loadState, loadTask, option, parseArguments, rootOption, validateArguments } from "./lib/runtime.mjs";
2
+ import { requireLocalAuthority } from "./lib/authority.mjs";
2
3
 
3
4
  try {
5
+ requireLocalAuthority();
4
6
  const raw = process.argv.slice(2);
5
7
  const parsed = parseArguments(raw, { valueOptions: ["--root", "--phase", "--mode", "--item", "--format"] });
6
8
  validateArguments(parsed, { minPositionals: 1, valueOptions: ["--phase", "--mode", "--item", "--format"], usage: "Usage: context.mjs <task-id> [--phase <phase>] [--mode <mode>] [--item <item-id>] [--format <format>] [--root <path>]" });
@@ -1,6 +1,8 @@
1
1
  import { checkGate, failEntry, formatDiagnostics, loadState, option, parseArguments, rootOption, validateArguments } from "./lib/runtime.mjs";
2
+ import { requireLocalAuthority } from "./lib/authority.mjs";
2
3
 
3
4
  try {
5
+ requireLocalAuthority();
4
6
  const raw = process.argv.slice(2);
5
7
  const parsed = parseArguments(raw, { valueOptions: ["--root", "--gate"] });
6
8
  validateArguments(parsed, { minPositionals: 1, valueOptions: ["--gate"], usage: "Usage: gate-check.mjs <task-id> [--gate <gate>] [--root <path>]" });
@@ -1,6 +1,8 @@
1
1
  import { checkGate, failEntry, formatGateView, loadState, option, parseArguments, rootOption, validateArguments } from "./lib/runtime.mjs";
2
+ import { requireLocalAuthority } from "./lib/authority.mjs";
2
3
 
3
4
  try {
5
+ requireLocalAuthority();
4
6
  const raw = process.argv.slice(2);
5
7
  const parsed = parseArguments(raw, { valueOptions: ["--root", "--format"] });
6
8
  validateArguments(parsed, { minPositionals: 1, valueOptions: ["--format"], usage: "Usage: gate-view.mjs <task-id> [--format <format>] [--root <path>]" });
@@ -0,0 +1,181 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+
6
+ const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
+ const states = new Set(["leased", "reported", "uncertain", "integrated", "released"]);
8
+ const safeId = (value, label) => { if (typeof value !== "string" || !idPattern.test(value)) throw new Error(`Invalid ${label}`); return value; };
9
+ const iso = () => new Date().toISOString();
10
+ const wait = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
11
+
12
+ export function assignmentPath(root, taskId, suffix = ".json") {
13
+ safeId(taskId, "task id");
14
+ let path = resolve(root);
15
+ for (const part of [".agents", "data", "loop", "assignments", `${taskId}${suffix}`]) {
16
+ path = join(path, part);
17
+ try { if (lstatSync(path).isSymbolicLink()) throw new Error(`Assignment path crosses symlink: ${path}`); }
18
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
19
+ }
20
+ return path;
21
+ }
22
+
23
+ function read(root, taskId) {
24
+ const path = assignmentPath(root, taskId);
25
+ let value;
26
+ try { value = JSON.parse(readFileSync(path, "utf8")); }
27
+ catch (error) {
28
+ if (error?.code === "ENOENT") return { schemaVersion: 1, taskId, assignments: [] };
29
+ throw new Error(`Corrupt local assignments for ${taskId}: ${error.message}`);
30
+ }
31
+ if (value?.schemaVersion !== 1 || value.taskId !== taskId || !Array.isArray(value.assignments)) throw new Error(`Corrupt local assignments for ${taskId}`);
32
+ for (const item of value.assignments) {
33
+ if (!item || typeof item.id !== "string" || !item.id || !idPattern.test(item.itemId) || !idPattern.test(item.workerId) || !states.has(item.state) || !Number.isSafeInteger(item.attempt) || item.attempt < 1 || !Array.isArray(item.ownedPaths) || item.ownedPaths.length > 64 || !item.ownedPaths.every(safeOwnedPath) || typeof item.leaseExpiresAt !== "string" || Number.isNaN(Date.parse(item.leaseExpiresAt))) throw new Error(`Corrupt local assignments for ${taskId}`);
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function safeOwnedPath(path) {
39
+ return typeof path === "string" && path.length > 0 && path.length <= 300 && path !== "." && !path.startsWith("/") && !path.split("/").includes("..") && !path.includes("\\") && !path.includes("\0");
40
+ }
41
+
42
+ function write(root, taskId, value) {
43
+ const path = assignmentPath(root, taskId);
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
46
+ let fd;
47
+ try {
48
+ fd = openSync(temporary, "wx", 0o600);
49
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
50
+ fsyncSync(fd); closeSync(fd); fd = undefined;
51
+ renameSync(temporary, path);
52
+ const dir = openSync(dirname(path), "r"); try { fsyncSync(dir); } finally { closeSync(dir); }
53
+ } finally {
54
+ if (fd !== undefined) closeSync(fd);
55
+ try { unlinkSync(temporary); } catch (error) { if (error?.code !== "ENOENT") throw error; }
56
+ }
57
+ }
58
+
59
+ function alive(pid) {
60
+ try { process.kill(pid, 0); return true; }
61
+ catch (error) { return error?.code !== "ESRCH"; }
62
+ }
63
+
64
+ function locked(root, taskId, action) {
65
+ const path = assignmentPath(root, taskId, ".lock");
66
+ mkdirSync(dirname(path), { recursive: true });
67
+ const token = randomUUID();
68
+ const deadline = Date.now() + 5000;
69
+ for (;;) {
70
+ try {
71
+ const fd = openSync(path, "wx", 0o600);
72
+ try { writeFileSync(fd, JSON.stringify({ pid: process.pid, token })); fsyncSync(fd); }
73
+ finally { closeSync(fd); }
74
+ break;
75
+ } catch (error) {
76
+ if (error?.code !== "EEXIST") throw error;
77
+ let owner;
78
+ try { owner = JSON.parse(readFileSync(path, "utf8")); }
79
+ catch { throw new Error(`Corrupt assignment lock for ${taskId}; inspect before retry`); }
80
+ if (!Number.isSafeInteger(owner.pid) || typeof owner.token !== "string") throw new Error(`Corrupt assignment lock for ${taskId}`);
81
+ if (!alive(owner.pid)) {
82
+ const current = JSON.parse(readFileSync(path, "utf8"));
83
+ if (current.token === owner.token) unlinkSync(path);
84
+ continue;
85
+ }
86
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for assignment lock: ${taskId}`);
87
+ wait(10);
88
+ }
89
+ }
90
+ try { return action(); }
91
+ finally {
92
+ const current = JSON.parse(readFileSync(path, "utf8"));
93
+ if (current.token === token) unlinkSync(path);
94
+ }
95
+ }
96
+
97
+ function canonical(root, taskId) {
98
+ const raw = execFileSync(process.execPath, [join(root, ".agents/aidlc/scripts/state.mjs"), "task", "show", taskId, "--root", root], { cwd: root, encoding: "utf8", timeout: 30000, maxBuffer: 2 * 1024 * 1024 });
99
+ const result = JSON.parse(raw);
100
+ if (result?.ok !== true || result.result?.id !== taskId) throw new Error(`Unknown canonical task: ${taskId}`);
101
+ return result.result;
102
+ }
103
+
104
+ function expire(value, now) {
105
+ for (const assignment of value.assignments) if (assignment.state === "leased" && Date.parse(assignment.leaseExpiresAt) <= now) {
106
+ assignment.state = "uncertain";
107
+ assignment.updatedAt = iso();
108
+ }
109
+ }
110
+
111
+ export function showLocalAssignments(root, taskId) {
112
+ return read(root, safeId(taskId, "task id"));
113
+ }
114
+
115
+ export function claimLocalAssignment(root, { taskId, itemId, workerId, ownedPaths, leaseMs = 300000, baseCommit = null, branch = null, sessionId = null }) {
116
+ safeId(taskId, "task id"); safeId(itemId, "item id"); safeId(workerId, "worker id");
117
+ if (!Array.isArray(ownedPaths) || ownedPaths.length < 1 || ownedPaths.length > 64 || !ownedPaths.every(safeOwnedPath)) throw new Error("ownedPaths must contain safe relative paths");
118
+ if (!Number.isSafeInteger(leaseMs) || leaseMs < 1000 || leaseMs > 3600000) throw new Error("leaseMs must be 1000..3600000");
119
+ if (sessionId !== null && (typeof sessionId !== "string" || !sessionId || sessionId.length > 300)) throw new Error("Invalid sessionId");
120
+ const task = canonical(root, taskId);
121
+ const item = task.tasks?.find((entry) => entry.id === itemId);
122
+ if (!item || !["todo", "in_progress"].includes(item.status) || task.phase !== "build" || task.status !== "active") throw new Error(`Canonical item is not dispatchable: ${taskId}/${itemId}`);
123
+ for (const prerequisite of item.dependsOn ?? []) if (task.tasks.find((entry) => entry.id === prerequisite)?.status !== "done") throw new Error(`Item prerequisite is not done: ${prerequisite}`);
124
+ return locked(root, taskId, () => {
125
+ const value = read(root, taskId); expire(value, Date.now());
126
+ const active = value.assignments.filter((entry) => ["leased", "reported", "uncertain"].includes(entry.state));
127
+ if (active.some((entry) => entry.itemId === itemId && entry.state === "leased")) throw new Error(`Item already leased: ${itemId}`);
128
+ if (value.assignments.some((entry) => entry.itemId === itemId && ["reported", "uncertain"].includes(entry.state))) throw new Error(`Item has unresolved assignment; reconcile before retry: ${itemId}`);
129
+ for (const other of active) for (const path of ownedPaths) for (const prior of other.ownedPaths) if (path === prior || path.startsWith(`${prior}/`) || prior.startsWith(`${path}/`)) throw new Error(`Owned path overlaps active assignment: ${path}`);
130
+ const attempt = 1 + Math.max(0, ...value.assignments.filter((entry) => entry.itemId === itemId).map((entry) => entry.attempt));
131
+ const assignment = { id: randomUUID(), taskId, itemId, workerId, ownedPaths, attempt, baseCommit, branch, sessionId, checkpoint: null, state: "leased", leaseExpiresAt: new Date(Date.now() + leaseMs).toISOString(), idempotencyKey: randomUUID(), createdAt: iso(), updatedAt: iso() };
132
+ value.assignments.push(assignment); write(root, taskId, value);
133
+ return assignment;
134
+ });
135
+ }
136
+
137
+ export function heartbeatLocalAssignment(root, taskId, assignmentId, leaseMs = 300000, sessionId) {
138
+ if (!Number.isSafeInteger(leaseMs) || leaseMs < 1000 || leaseMs > 3600000) throw new Error("leaseMs must be 1000..3600000");
139
+ return locked(root, taskId, () => {
140
+ const value = read(root, taskId); expire(value, Date.now());
141
+ const item = value.assignments.find((entry) => entry.id === assignmentId);
142
+ if (!item || item.state !== "leased") throw new Error("Assignment is not leased");
143
+ if (sessionId !== undefined) { if (typeof sessionId !== "string" || !sessionId || sessionId.length > 300) throw new Error("Invalid sessionId"); item.sessionId = sessionId; }
144
+ item.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString(); item.updatedAt = iso(); write(root, taskId, value); return item;
145
+ });
146
+ }
147
+
148
+ export function reportLocalAssignment(root, taskId, assignmentId, { checkpoint = null, commit = null, changedPaths = [] } = {}) {
149
+ if (checkpoint !== null && (typeof checkpoint !== "string" || checkpoint.length > 2000)) throw new Error("Invalid checkpoint");
150
+ if (commit !== null && (typeof commit !== "string" || !/^[a-f0-9]{40,64}$/.test(commit))) throw new Error("Invalid commit");
151
+ if (!Array.isArray(changedPaths) || changedPaths.length > 128 || !changedPaths.every(safeOwnedPath)) throw new Error("Invalid changedPaths");
152
+ return locked(root, taskId, () => {
153
+ const value = read(root, taskId); expire(value, Date.now());
154
+ const item = value.assignments.find((entry) => entry.id === assignmentId);
155
+ if (!item || item.state !== "leased") throw new Error("Assignment is not leased; reconcile before reporting");
156
+ for (const path of changedPaths) if (!item.ownedPaths.some((owned) => path === owned || path.startsWith(`${owned}/`))) throw new Error(`Changed path is outside assignment scope: ${path}`);
157
+ item.state = "reported"; item.checkpoint = checkpoint; item.commit = commit; item.changedPaths = changedPaths; item.updatedAt = iso(); write(root, taskId, value); return item;
158
+ });
159
+ }
160
+
161
+ export function reconcileLocalAssignments(root, taskId) {
162
+ const task = canonical(root, taskId);
163
+ return locked(root, taskId, () => {
164
+ const value = read(root, taskId); expire(value, Date.now());
165
+ for (const item of value.assignments) {
166
+ const canonicalItem = task.tasks?.find((entry) => entry.id === item.itemId);
167
+ if (canonicalItem?.status === "done" && item.state !== "integrated") { item.state = "integrated"; item.updatedAt = iso(); }
168
+ else if (!canonicalItem && item.state === "leased") { item.state = "uncertain"; item.updatedAt = iso(); }
169
+ }
170
+ write(root, taskId, value);
171
+ return value;
172
+ });
173
+ }
174
+
175
+ export function releaseLocalAssignment(root, taskId, assignmentId) {
176
+ return locked(root, taskId, () => {
177
+ const value = read(root, taskId); const item = value.assignments.find((entry) => entry.id === assignmentId);
178
+ if (!item || !["leased", "reported", "uncertain"].includes(item.state)) throw new Error("Assignment cannot be released");
179
+ item.state = "released"; item.updatedAt = iso(); write(root, taskId, value); return item;
180
+ });
181
+ }
@@ -0,0 +1,3 @@
1
+ export const requireLocalAuthority = () => {
2
+ if (process.env.AIDLC_FLEET_URL) throw new Error("Fleet authority is configured; use remote-state.mjs instead of local lifecycle scripts");
3
+ };
@@ -221,7 +221,14 @@ export const compileContext = (root, task, phase = task.phase, options = {}) =>
221
221
  const invariants = [options.itemId ? `- Focused item context does not narrow the workplan: actionable items are ${actionableItems.join(", ") || "none"}; after this item, execute nextAction immediately.` : "", "- Continue after a non-terminal transition or item/evidence mutation; yield only at a human gate, real blocker, or completion.", "- Item completion is progress for commentary, never a final response while nextAction is run_phase.", "- Never omit approved decisions, spec anchors, safety constraints, or applicable verification evidence.", "- Agents may run configured build/test/lint commands, but workflow installation and upgrades remain human-only npm/npx operations."].filter(Boolean).join("\n");
222
222
  const phasePath = join(resolve(root), ".agents/aidlc", phase === "done" ? "phase-wrap.md" : `phase-${phase}.md`);
223
223
  const phaseContract = readFileSync(phasePath, "utf8");
224
- let content = [`# AI-DLC phase packet — ${phase}`, `mode: ${options.mode ?? "standard"}${options.itemId ? ` · item: ${options.itemId}` : ""}`, "## Next action / stop contract", JSON.stringify(nextAction(task, root), null, 2), "## Phase contract", phaseContract, "## Canonical task state", compact, "## Resolved profiles", profiles.map((item) => JSON.stringify({ id: item.id, topology: item.topology, discovery: item.discovery, specs: item.specs, commands: item.commands })).join("\n"), "## Effective project configuration", JSON.stringify({ discovery: effective.discovery, specs: effective.specs, commands: effective.commands, risk: effective.risk, context: effective.context }, null, 2), "## Invariants", invariants].join("\n\n");
224
+ let intake = "";
225
+ if (phase === "clarify") {
226
+ const parts = [".agents", "data", "tasks", task.id, "requirement.md"];
227
+ let cursor = resolve(root), safe = true;
228
+ for (const part of parts) { cursor = join(cursor, part); if (!existsSync(cursor)) { safe = false; break; } if (lstatSync(cursor).isSymbolicLink()) throw new Error("Unsafe requirement path"); }
229
+ if (safe) { const requirement = readFileSync(cursor, "utf8"); if (Buffer.byteLength(requirement) > 8192) throw new Error("Requirement exceeds intake limit"); intake = `## User requirement\n${requirement}`; }
230
+ }
231
+ let content = [`# AI-DLC phase packet — ${phase}`, `mode: ${options.mode ?? "standard"}${options.itemId ? ` · item: ${options.itemId}` : ""}`, "## Next action / stop contract", JSON.stringify(nextAction(task, root), null, 2), "## Phase contract", phaseContract, ...(intake ? [intake] : []), "## Canonical task state", compact, "## Resolved profiles", profiles.map((item) => JSON.stringify({ id: item.id, topology: item.topology, discovery: item.discovery, specs: item.specs, commands: item.commands })).join("\n"), "## Effective project configuration", JSON.stringify({ discovery: effective.discovery, specs: effective.specs, commands: effective.commands, risk: effective.risk, context: effective.context }, null, 2), "## Invariants", invariants].join("\n\n");
225
232
  if (content.length > config.context.maxChars) throw new Error("Context budget is too small for the full phase contract, next action, canonical task state, and mandatory invariants");
226
233
  const omittedRules = [];
227
234
  for (const path of rules) {