@felixdotgo/aidlc-kit 0.1.1 → 0.3.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 (198) hide show
  1. package/DEVELOPMENT.md +14 -13
  2. package/README.md +37 -43
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +2 -2
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
  12. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
  38. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
  40. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
  41. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  42. package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
  43. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  44. package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
  45. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
  46. package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
  47. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  48. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  49. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  50. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  51. package/dist/assets/ui/bulma.min.css +3 -0
  52. package/dist/assets/ui/changes.js +83 -0
  53. package/dist/assets/ui/common.js +664 -0
  54. package/dist/assets/ui/dashboard.css +636 -0
  55. package/dist/assets/ui/dashboard.js +77 -0
  56. package/dist/assets/ui/detail.js +159 -0
  57. package/dist/assets/ui/live-log.js +332 -0
  58. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  59. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  60. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  61. package/dist/services/aidlc-coordination/package.json +9 -4
  62. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
  63. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  64. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  65. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  66. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  67. package/dist/src/adapters.d.ts +0 -3
  68. package/dist/src/adapters.js +3 -5
  69. package/dist/src/cli/argv.d.ts +12 -0
  70. package/dist/src/cli/argv.js +68 -0
  71. package/dist/src/cli/commands.d.ts +8 -0
  72. package/dist/src/cli/commands.js +159 -0
  73. package/dist/src/cli/loop-launcher.d.ts +47 -0
  74. package/dist/src/cli/loop-launcher.js +322 -0
  75. package/dist/src/cli/options.d.ts +25 -0
  76. package/dist/src/cli/options.js +32 -0
  77. package/dist/src/cli/prompt.d.ts +9 -0
  78. package/dist/src/cli/prompt.js +66 -0
  79. package/dist/src/cli.js +28 -191
  80. package/dist/src/gate.js +3 -2
  81. package/dist/src/installer.d.ts +14 -7
  82. package/dist/src/installer.js +42 -143
  83. package/dist/src/legacy.js +1 -0
  84. package/dist/src/managed-content.d.ts +7 -0
  85. package/dist/src/managed-content.js +15 -4
  86. package/dist/src/managed-planning.d.ts +65 -0
  87. package/dist/src/managed-planning.js +219 -0
  88. package/dist/src/mcp-setup.js +3 -1
  89. package/dist/src/model/config-contracts.d.ts +65 -0
  90. package/dist/src/model/config-contracts.js +1 -0
  91. package/dist/src/model/install-contracts.d.ts +29 -0
  92. package/dist/src/model/install-contracts.js +1 -0
  93. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  94. package/dist/src/model/lifecycle-enums.js +1 -0
  95. package/dist/src/model/loop.d.ts +20 -0
  96. package/dist/src/model/loop.js +1 -0
  97. package/dist/src/model/manifest.d.ts +16 -0
  98. package/dist/src/model/manifest.js +1 -0
  99. package/dist/src/model/records.d.ts +142 -0
  100. package/dist/src/model/records.js +1 -0
  101. package/dist/src/model.d.ts +11 -264
  102. package/dist/src/model.js +1 -1
  103. package/dist/src/profiles.d.ts +2 -0
  104. package/dist/src/profiles.js +53 -9
  105. package/dist/src/state/archive-record.d.ts +9 -0
  106. package/dist/src/state/archive-record.js +26 -0
  107. package/dist/src/state/commands.d.ts +18 -0
  108. package/dist/src/state/commands.js +27 -0
  109. package/dist/src/state/domain.d.ts +9 -0
  110. package/dist/src/state/domain.js +9 -0
  111. package/dist/src/state/guards.d.ts +7 -0
  112. package/dist/src/state/guards.js +17 -0
  113. package/dist/src/state/io.d.ts +10 -0
  114. package/dist/src/state/io.js +35 -0
  115. package/dist/src/state/items.d.ts +3 -4
  116. package/dist/src/state/items.js +58 -57
  117. package/dist/src/state/lessons.js +14 -7
  118. package/dist/src/state/lifecycle.js +2 -2
  119. package/dist/src/state/lock.js +7 -6
  120. package/dist/src/state/memory.js +24 -12
  121. package/dist/src/state/{archive.js → migrate.js} +7 -7
  122. package/dist/src/state/partition.d.ts +15 -0
  123. package/dist/src/state/partition.js +90 -0
  124. package/dist/src/state/paths.d.ts +11 -0
  125. package/dist/src/state/paths.js +12 -0
  126. package/dist/src/state/render.js +9 -6
  127. package/dist/src/state/store.js +9 -15
  128. package/dist/src/state/transitions.js +9 -7
  129. package/dist/src/state/validate.js +55 -7
  130. package/dist/src/state.d.ts +16 -11
  131. package/dist/src/state.js +16 -11
  132. package/dist/src/task-ui.d.ts +33 -0
  133. package/dist/src/task-ui.js +33 -0
  134. package/dist/src/ui/assets.d.ts +14 -0
  135. package/dist/src/ui/assets.js +45 -0
  136. package/dist/src/ui/changes.d.ts +83 -0
  137. package/dist/src/ui/changes.js +498 -0
  138. package/dist/src/ui/containers.d.ts +71 -0
  139. package/dist/src/ui/containers.js +214 -0
  140. package/dist/src/ui/daemon.d.ts +75 -0
  141. package/dist/src/ui/daemon.js +232 -0
  142. package/dist/src/ui/data.d.ts +42 -0
  143. package/dist/src/ui/data.js +89 -0
  144. package/dist/src/ui/docs.d.ts +13 -0
  145. package/dist/src/ui/docs.js +39 -0
  146. package/dist/src/ui/files.d.ts +3 -0
  147. package/dist/src/ui/files.js +20 -0
  148. package/dist/src/ui/http.d.ts +14 -0
  149. package/dist/src/ui/http.js +80 -0
  150. package/dist/src/ui/live.d.ts +5 -0
  151. package/dist/src/ui/live.js +43 -0
  152. package/dist/src/ui/markdown.d.ts +2 -0
  153. package/dist/src/ui/markdown.js +127 -0
  154. package/dist/src/ui/options.d.ts +48 -0
  155. package/dist/src/ui/options.js +81 -0
  156. package/dist/src/ui/security.d.ts +34 -0
  157. package/dist/src/ui/security.js +38 -0
  158. package/dist/src/ui/server.d.ts +39 -0
  159. package/dist/src/ui/server.js +347 -0
  160. package/dist/src/ui/sse.d.ts +14 -0
  161. package/dist/src/ui/sse.js +281 -0
  162. package/dist/src/ui/views/board.d.ts +7 -0
  163. package/dist/src/ui/views/board.js +131 -0
  164. package/dist/src/ui/views/changes.d.ts +19 -0
  165. package/dist/src/ui/views/changes.js +154 -0
  166. package/dist/src/ui/views/components.d.ts +56 -0
  167. package/dist/src/ui/views/components.js +113 -0
  168. package/dist/src/ui/views/docs.d.ts +15 -0
  169. package/dist/src/ui/views/docs.js +64 -0
  170. package/dist/src/ui/views/layout.d.ts +44 -0
  171. package/dist/src/ui/views/layout.js +233 -0
  172. package/dist/src/ui/views/task.d.ts +25 -0
  173. package/dist/src/ui/views/task.js +320 -0
  174. package/dist/src/upgrade-board.js +10 -10
  175. package/dist/src/upgrade.d.ts +0 -1
  176. package/dist/src/upgrade.js +24 -85
  177. package/dist/src/workflow.d.ts +4 -0
  178. package/dist/src/workflow.js +36 -4
  179. package/docs/README.md +41 -22
  180. package/docs/architecture.md +101 -37
  181. package/docs/command-reference.md +289 -52
  182. package/docs/configuration.md +70 -30
  183. package/docs/coordination-service.md +91 -30
  184. package/docs/design-decisions.md +87 -0
  185. package/docs/development.md +124 -49
  186. package/docs/loop-local-run.md +228 -0
  187. package/docs/loop-runbook.md +214 -0
  188. package/docs/operating-workflow.md +55 -42
  189. package/docs/testing-and-release.md +72 -49
  190. package/docs/troubleshooting.md +221 -0
  191. package/package.json +4 -1
  192. package/dist/src/ledger.d.ts +0 -22
  193. package/dist/src/ledger.js +0 -119
  194. package/dist/src/state/shared.d.ts +0 -29
  195. package/dist/src/state/shared.js +0 -71
  196. package/dist/src/tracker.d.ts +0 -36
  197. package/dist/src/tracker.js +0 -44
  198. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
package/DEVELOPMENT.md CHANGED
@@ -1,31 +1,32 @@
1
1
  # Contributing to aidlc-kit
2
2
 
3
- This file is the short entry point for maintainers and contributors. Detailed guidance lives in [`docs/`](./docs/README.md) so that user setup, system operation, and repository development can evolve independently.
3
+ Entry point for contributors. Details live in [`docs/`](./docs/README.md).
4
4
 
5
5
  ## Local bootstrap
6
6
 
7
- Requirements: Node.js 20 or newer and npm.
7
+ Node.js 20.17+ and npm.
8
8
 
9
9
  ```sh
10
10
  npm install
11
- npm test
12
- npm run lint
11
+ npm test # build + unit tests
12
+ npm run lint # tsc --noEmit
13
13
  ```
14
14
 
15
+ To develop with the repository's own workflow (`.agents/` is gitignored), follow [Dogfooding this repository](./docs/development.md#dogfooding-this-repository).
16
+
15
17
  ## Contributor documentation
16
18
 
17
- - [Development guide](./docs/development.md) — dependencies, repository layout, implementation boundaries, and local commands.
18
- - [Testing and release](./docs/testing-and-release.md) — focused tests, package smoke test, evaluator, and release readiness.
19
- - [Architecture](./docs/architecture.md) — module ownership, data flow, and installed-runtime parity.
20
- - [Configuration reference](./docs/configuration.md) — profiles, rules, provider permissions, and project-owned configuration.
19
+ - [Development guide](./docs/development.md) — layout, local commands, boundaries, code style, dogfooding.
20
+ - [Testing and release](./docs/testing-and-release.md) — focused tests, Docker/PostgreSQL verification, package smoke test, release checklist.
21
+ - [Architecture](./docs/architecture.md) — module ownership and source → bundle → installed flow.
22
+ - [Design decisions](./docs/design-decisions.md) — numbered decisions and how to add one.
21
23
 
22
24
  ## Documentation policy
23
25
 
24
- - Keep `README.md` focused on package requirements and setup.
25
- - Add operational and system guidance to the relevant page in `docs/`.
26
- - Update command/configuration examples and their tests in the same change as a public contract.
27
- - Do not duplicate canonical agent instructions from `assets/aidlc/` in overview documentation.
26
+ - `README.md` is the fastest path to install and use; everything else goes in `docs/` per [Adding or changing a page](./docs/README.md#adding-or-changing-a-page).
27
+ - A public contract change (command, config field, schema, installed asset) updates its guide and test in the same change.
28
+ - Never duplicate canonical agent instructions from `assets/aidlc/`; link to them.
28
29
 
29
30
  ## Delivery boundary
30
31
 
31
- Running verification does not authorise publishing, tagging, committing, or pushing. Those actions require separate human approval.
32
+ Passing verification does not authorise publishing, tagging, committing, or pushing; each needs separate human approval.
package/README.md CHANGED
@@ -1,69 +1,63 @@
1
1
  # aidlc-kit
2
2
 
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.
3
+ [![version](https://img.shields.io/badge/version-0.3.0-blue)](https://www.npmjs.com/package/@felixdotgo/aidlc-kit)
4
+ [![license](https://img.shields.io/npm/l/@felixdotgo/aidlc-kit)](https://github.com/felixdotgo/aidlc-kit/blob/main/LICENSE)
5
+ [![node](https://img.shields.io/node/v/@felixdotgo/aidlc-kit)](https://nodejs.org/)
4
6
 
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-kit loop start` — selecting a mode never starts or approves anything by itself.
7
+ Installs an AI-DLC workflow into an existing project: a shared task lifecycle, explicit human review gates, and project-owned state for Codex, Claude Code, and OpenCode. Optionally, a loop daemon runs tasks end-to-end on one machine or across a fleet.
6
8
 
7
9
  > [!WARNING]
8
- > The optional end-to-end loop and the AI-DLC Coordination service are **experimental and unfinished**: they contain known bugs and rough edges. The package is pre-1.0 (`0.x`), so their CLI flags, `loop.*`/`mcp.*` configuration fields, environment variables, and installed layouts can change incompatibly in any release before `1.0.0`.
9
-
10
- ## Requirements
11
-
12
- - Node.js 20.17 or newer.
13
- - An existing project directory.
14
- - Codex, Claude Code, or OpenCode.
10
+ > Pre-1.0 (`0.x`). The local loop is usable; fleet mode and the coordination service are still in development. See [Status and stability](./docs/loop-runbook.md#status-and-stability).
15
11
 
16
12
  ## Quick start
17
13
 
18
- Run these commands from your existing project's directory to preview the installation, then apply it:
14
+ Requires Node.js 20.17+ and Codex, Claude Code, or OpenCode (Linux/macOS; Windows is best-effort).
19
15
 
20
16
  ```sh
21
- npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit init . --agent codex --dry-run
22
- npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit init . --agent codex --yes
17
+ npx --package @felixdotgo/aidlc-kit@0.3.0 aidlc-kit init . # previews, then installs after you confirm
18
+ npx --package @felixdotgo/aidlc-kit@0.3.0 aidlc-kit doctor . --strict
23
19
  ```
24
20
 
25
- `npx` may ask permission to download the package on first use. These examples pin version `0.1.1`. After installation, the project's lifecycle scripts run locally without `npx` or a registry connection.
26
-
27
- Use the adapter that matches the coding tool:
21
+ In a terminal, `init` asks for the adapter (unless exactly one is detected or you pass `--agent codex|claude|opencode` / `--all`) and the loop mode (unless `--loop-mode …`), previews the files, then asks you to confirm; `--yes` skips the confirmation, `--dry-run` writes nothing. The installed lifecycle scripts run locally and never contact a registry.
28
22
 
29
- | Tool | `--agent` value | Installed entry point |
30
- | --- | --- | --- |
31
- | Codex | `codex` | `AGENTS.md` |
32
- | Claude Code | `claude` | `CLAUDE.md` and phase skills |
33
- | OpenCode | `opencode` | `AGENTS.md` |
23
+ Then ask your agent for a change:
34
24
 
35
- 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.
25
+ ```text
26
+ Add rate limiting to the login endpoint using the AI-DLC workflow.
27
+ ```
36
28
 
37
- Confirm the installed project locally:
29
+ The agent creates a task and stops at three review steps — Confirm requirements, Approve plan, Review implementation — each needing your explicit reply. The Codex and Claude Code adapters pre-approve only the lifecycle scripts, and OpenCode gets instructions only ([Agent permissions](./docs/configuration.md#agent-permissions)); for Codex, trust the project first.
38
30
 
39
- ```sh
40
- npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit status .
41
- npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit doctor . --strict
42
- ```
31
+ ## Run the loop (optional)
43
32
 
44
- ## Documentation
33
+ A daemon drives tasks end-to-end on a Linux host with Docker, stopping at every review step for you:
45
34
 
46
- The detailed documentation is organised by task and audience:
35
+ 1. In `.agents/config.json` update the `loop` section to match your environment, e.g.,
36
+ ```json
37
+ ...
38
+ "loop": {
39
+ "mode": "local",
40
+ "agents": {
41
+ "lead": "opencode",
42
+ "workers": {
43
+ "worker-a": "opencode"
44
+ }
45
+ }
46
+ }
47
+ ...
48
+ ```
49
+ 2. Put credentials in the git-ignored `.agents/loop.env`; keys your tools already store are detected.
50
+ 3. `./.agents/aidlc/scripts/loop.sh up` checks the environment, starts the daemon, and serves the dashboard (`--open` launches the browser); `loop.sh down` stops both.
47
51
 
48
- - [Documentation index](./docs/README.md) — choose the right guide.
49
- - [Operating the workflow](./docs/operating-workflow.md) — lifecycle, state, gates, and blocked work.
50
- - [Configuration](./docs/configuration.md) — project configuration, profiles, rules, and adapters.
51
- - [Command reference](./docs/command-reference.md) — every package CLI command and safety constraint.
52
- - [Development](./docs/development.md) — local contributor setup, source layout, and implementation rules.
53
- - [Testing and release](./docs/testing-and-release.md) — verification, evaluator, package smoke test, and release readiness.
54
- - [Architecture](./docs/architecture.md) — package boundaries and the source-to-installed flow.
55
- - [AI-DLC Coordination service](./docs/coordination-service.md) — opt-in Docker/remote coordination, security, and polling contract.
52
+ Setup: [Loop setup runbook](./docs/loop-runbook.md). Daily use: [Local loop walkthrough](./docs/loop-local-run.md).
56
53
 
57
- ## Safety note
54
+ ## Documentation
58
55
 
59
- Only a human may initiate or apply a workflow upgrade. `upgrade` rejects `--yes` and `--force`, requires an interactive terminal, and asks the human to type the target package version. See the [command reference](./docs/command-reference.md#upgrade) before changing an existing installation.
56
+ [Index](./docs/README.md) · [Operating the workflow](./docs/operating-workflow.md) · [Configuration](./docs/configuration.md) · [Command reference](./docs/command-reference.md) · [Troubleshooting](./docs/troubleshooting.md) · [Coordination service (fleet)](./docs/coordination-service.md) · [Contributing](./DEVELOPMENT.md)
60
57
 
61
- ## Recent changes
58
+ ## Safety note
62
59
 
63
- - Synced the two committed ESM lifecycle cores so `lifecycle-core-conformance` passes.
64
- - Normalized Git path scopes (trailing slashes stripped) consistently across fleet, local-worker, and snapshot flows; directory scopes now work in all three.
65
- - Hardened the state lock to reclaim partially written (corrupt) lock files instead of wedging mutations.
66
- - Made the fleet end-to-end test skip cleanly when its Docker worker image is unavailable.
60
+ Only a human may upgrade: `upgrade` rejects `--yes` and `--force`, needs an interactive terminal, and asks you to type the target version ([details](./docs/command-reference.md#upgrade)).
67
61
 
68
62
  ## License
69
63
 
@@ -0,0 +1,7 @@
1
+ # AI-DLC worker image (Claude Code). Shipped and managed by aidlc-kit — build it
2
+ # with `aidlc-kit loop worker-image --provider claude` instead of by hand.
3
+ #
4
+ # The agent CLI must be on the system PATH: workers run non-root with a read-only
5
+ # root filesystem, so a global npm install lands the binary in /usr/local/bin.
6
+ FROM node:24-bookworm-slim
7
+ RUN npm install -g @anthropic-ai/claude-code
@@ -0,0 +1,9 @@
1
+ # AI-DLC worker image (OpenAI Codex CLI). Shipped and managed by aidlc-kit —
2
+ # build it with `aidlc-kit loop worker-image --provider codex` instead of by hand.
3
+ #
4
+ # The agent CLI must be on the system PATH: workers run non-root with a read-only
5
+ # root filesystem, so a global npm install lands the binary in /usr/local/bin.
6
+ # Use the scoped `@openai/codex` package (the unscoped `codex` npm package is
7
+ # unrelated).
8
+ FROM node:24-bookworm-slim
9
+ RUN npm install -g @openai/codex
@@ -0,0 +1,8 @@
1
+ # AI-DLC worker image (OpenCode 2). Shipped and managed by aidlc-kit — build it
2
+ # with `aidlc-kit loop worker-image --provider opencode` instead of by hand.
3
+ #
4
+ # The agent CLI must be on the system PATH: workers run non-root with a read-only
5
+ # root filesystem, so a global npm install lands the binary in /usr/local/bin.
6
+ # Use `@opencode/cli` (OpenCode 2) — NOT `opencode-ai` (OpenCode 1).
7
+ FROM node:24-bookworm-slim
8
+ RUN npm install -g @opencode/cli
@@ -20,7 +20,7 @@ This file is the compact kernel contract. Project customization lives under `.ag
20
20
 
21
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
- 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.
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/tasks/<task-id>/runtime/assignments.json` 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
25
  ## Review steps
26
26
 
@@ -52,7 +52,7 @@ Economy models use the same bounded packet contract, gates, executable checks, a
52
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.
53
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
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.
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.
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. Canonical build items carry this partition as `ownedPaths`/`dependsOn`; the Approve plan gate check reports `PARTITION_OVERLAP`/`PARTITION_SCOPE` when concurrently-runnable items overlap or lack owned paths.
56
56
 
57
57
  ## Customization layers
58
58
 
@@ -7,7 +7,7 @@ When independent, read-only index or ambiguity checks exist, use the shared orch
7
7
  1. Read the compact repository/spec indexes if available; do not scan implementation code.
8
8
  2. Read `templates/model-contract.md` and apply its COSTARS clarify emphasis to the intent brief. Use CRITICS only for elevated risk, ambiguity, or model disagreement.
9
9
  3. Canonical task state already exists from turn routing (`task create` runs there, before this packet). Never create it a second time; a repeated create fails with `Task already exists`.
10
- 4. Fill the intent template: problem, affected areas, assumptions, open questions with safe defaults, and decidable in/out scope.
10
+ 4. Fill the intent template: problem, affected areas, work boundaries (independent streams and the paths each touches, or `single stream` with the reason), assumptions, open questions with safe defaults, and decidable in/out scope.
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
 
@@ -6,9 +6,9 @@ Use the shared orchestration contract in `orchestrator.md` to fan out only indep
6
6
 
7
7
  1. Retrieve only matched specs and impacted symbols. Parallelize bounded discovery when available; return summaries and exact spec anchors, not file dumps.
8
8
  2. Read `templates/model-contract.md` and apply its COSTARS plan emphasis. Use CRITICS only for elevated risk, ambiguity, or model disagreement.
9
- 3. Fill the design template with the solution per affected area, exact sources for quantified rules, contracts, risks, and reuse candidates.
9
+ 3. Fill the design template with the solution per affected area, exact sources for quantified rules, contracts, risks, reuse candidates, and the Parallel work partition table matching the canonical items.
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
- 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>`.
11
+ 5. Add implementation items with `node .agents/aidlc/scripts/state.mjs task item <task-id> <item-id> --status todo --label <label> --owns <comma-separated-paths> [--depends-on <item-ids>]` and configure narrow verification commands. Every item passes `--owns`; add `--depends-on` whenever it must touch a path that another concurrently-runnable item owns. With the loop enabled, the Approve plan gate check fails with `PARTITION_OVERLAP`/`PARTITION_SCOPE` until fixed (warning when the loop is off). Then render the workplan review artifact with `node .agents/aidlc/scripts/render.mjs <task-id>`.
12
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
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
 
@@ -13,7 +13,8 @@
13
13
  "context": { "type": "object", "required": ["maxChars"], "properties": { "maxChars": { "type": "integer", "minimum": 4000, "maximum": 128000 } } },
14
14
  "agentState": { "type": "object", "additionalProperties": { "enum": ["native", "scripted"] } },
15
15
  "tracker": { "type": "object", "additionalProperties": false, "required": ["enabled"], "properties": { "enabled": { "type": "boolean" }, "provider": { "enum": ["backlog"] }, "spaceUrl": { "type": "string", "format": "uri" }, "project": { "type": "string", "minLength": 1 }, "tokenEnv": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "workflow": { "enum": ["waterfall", "scrum"] }, "mapping": { "type": "object", "properties": { "statuses": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 1 } }, "gateFieldId": { "type": "integer", "minimum": 1 } } } } },
16
- "mcp": { "type": "object", "additionalProperties": false, "required": ["enabled"], "properties": { "enabled": { "type": "boolean" }, "endpoint": { "type": "string", "format": "uri" }, "workspace": { "type": "string", "minLength": 1 }, "tokenEnv": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "pollMs": { "type": "integer", "minimum": 1000, "maximum": 3600000 }, "providers": { "type": "array", "items": { "enum": ["jira", "trello", "github-issues"] }, "uniqueItems": true } } }
16
+ "mcp": { "type": "object", "additionalProperties": false, "required": ["enabled"], "properties": { "enabled": { "type": "boolean" }, "endpoint": { "type": "string", "format": "uri" }, "workspace": { "type": "string", "minLength": 1 }, "tokenEnv": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "pollMs": { "type": "integer", "minimum": 1000, "maximum": 3600000 }, "providers": { "type": "array", "items": { "enum": ["jira", "trello", "github-issues"] }, "uniqueItems": true } } },
17
+ "$schema": { "type": "string", "maxLength": 512 }
17
18
  },
18
19
  "$defs": {
19
20
  "command": {
@@ -35,6 +35,8 @@
35
35
  "legacyG2Wait": { "$ref": "#/$defs/legacyG2Wait" },
36
36
  "predecessorTaskId": { "type": "string", "minLength": 1 },
37
37
  "successorTaskId": { "type": "string", "minLength": 1 },
38
+ "baseCommit": { "type": "string", "pattern": "^[0-9a-f]{7,64}$" },
39
+ "finalCommit": { "type": "string", "pattern": "^[0-9a-f]{7,64}$" },
38
40
  "createdAt": { "type": "string" },
39
41
  "updatedAt": { "type": "string" }
40
42
  },
@@ -1,27 +1,31 @@
1
1
  import { randomUUID } from "node:crypto";
2
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";
3
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { acquireHolder, describeHolder } from "./loop-holder.mjs";
6
+ import { resolveTaskRuntimeRead, taskRuntimePath } from "./task-runtime.mjs";
7
+ import { migrateTaskRuntime } from "./task-runtime-migrate.mjs";
5
8
 
6
9
  const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
10
  const states = new Set(["leased", "reported", "uncertain", "integrated", "released"]);
8
11
  const safeId = (value, label) => { if (typeof value !== "string" || !idPattern.test(value)) throw new Error(`Invalid ${label}`); return value; };
9
12
  const iso = () => new Date().toISOString();
10
- const wait = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
11
13
 
14
+ /** Write location of the task's assignment records
15
+ * (`.agents/data/tasks/<task>/runtime/assignments.json`; `.lock` for the mutex),
16
+ * refusing symlinked path components. */
12
17
  export function assignmentPath(root, taskId, suffix = ".json") {
13
18
  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;
19
+ if (![".json", ".lock"].includes(suffix)) throw new Error(`Unknown assignment file suffix: ${suffix}`);
20
+ return taskRuntimePath(root, taskId, suffix === ".lock" ? "assignments.lock" : "assignments.json");
21
21
  }
22
22
 
23
+ /** Read location: the runtime file, else the pre-runtime
24
+ * `.agents/data/loop/assignments/<task>.json` until it has been migrated. */
25
+ export const assignmentReadPath = (root, taskId) => resolveTaskRuntimeRead(root, safeId(taskId, "task id"), "assignments.json");
26
+
23
27
  function read(root, taskId) {
24
- const path = assignmentPath(root, taskId);
28
+ const path = assignmentReadPath(root, taskId);
25
29
  let value;
26
30
  try { value = JSON.parse(readFileSync(path, "utf8")); }
27
31
  catch (error) {
@@ -56,44 +60,66 @@ function write(root, taskId, value) {
56
60
  }
57
61
  }
58
62
 
59
- function alive(pid) {
60
- try { process.kill(pid, 0); return true; }
61
- catch (error) { return error?.code !== "ESRCH"; }
62
- }
63
-
63
+ /** Short mutex around one read-modify-write of `assignments/<task>.json`
64
+ * (kind lock, 30s TTL, no heartbeat). Waits up to 5s for another holder. */
64
65
  function locked(root, taskId, action) {
66
+ // A legacy record moves under runtime/ before the first write, so the
67
+ // writer never has to choose between two locations.
68
+ migrateTaskRuntime(root, taskId, { names: ["assignments.json"] });
65
69
  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
- }
70
+ const handle = acquireHolder({
71
+ path, kind: "lock", scope: `assignments:${taskId}`, purpose: "assignment read-modify-write", waitMs: 5000,
72
+ busyMessage: (entry) => `Timed out waiting for assignment lock: ${taskId}: ${describeHolder(entry)}`,
73
+ });
90
74
  try { return action(); }
91
75
  finally {
92
- const current = JSON.parse(readFileSync(path, "utf8"));
93
- if (current.token === token) unlinkSync(path);
76
+ // A release failure must not mask the committed write; the lock then
77
+ // expires on its own after its 30s TTL.
78
+ try { handle.release(); } catch { /* expires via TTL */ }
94
79
  }
95
80
  }
96
81
 
82
+ /** User-facing labels for the persisted assignment (claim) states. */
83
+ export const ASSIGNMENT_STATE_LABELS = Object.freeze({
84
+ leased: "claim held",
85
+ expired: "claim expired; needs reconcile",
86
+ uncertain: "expired or outcome unknown; needs reconcile",
87
+ reported: "worker reported; awaiting integration",
88
+ integrated: "integrated",
89
+ released: "released",
90
+ });
91
+
92
+ /** Display state computed at read time: a `leased` claim past its
93
+ * leaseExpiresAt shows as `expired` (mutations persist it as `uncertain`). */
94
+ export function effectiveAssignmentState(assignment, now = Date.now()) {
95
+ return assignment.state === "leased" && Date.parse(assignment.leaseExpiresAt) <= now ? "expired" : assignment.state;
96
+ }
97
+
98
+ const reconcileCommand = (taskId) => `node .agents/aidlc/scripts/assignment.mjs reconcile ${taskId}`;
99
+ const releaseCommand = (taskId, id) => `node .agents/aidlc/scripts/assignment.mjs release ${taskId} ${id}`;
100
+
101
+ /** One-line description of a claim: id, item, worker, paths, state, expiry. */
102
+ export function describeAssignment(assignment, now = Date.now()) {
103
+ const state = effectiveAssignmentState(assignment, now);
104
+ return `assignment ${assignment.id} (item ${assignment.itemId}, worker ${assignment.workerId}, paths ${(assignment.ownedPaths ?? []).join(", ")}, state ${state}: ${ASSIGNMENT_STATE_LABELS[state] ?? state}, claim expires ${assignment.leaseExpiresAt})`;
105
+ }
106
+
107
+ /** Remedy commands for a blocking claim. */
108
+ export function assignmentRemedy(taskId, assignment, now = Date.now()) {
109
+ const release = `${releaseCommand(taskId, assignment.id)} to drop the claim (discards the worker's unintegrated result)`;
110
+ const state = effectiveAssignmentState(assignment, now);
111
+ if (state === "leased") return `wait for the worker to report or for the claim to expire; if the worker is gone, run ${release}`;
112
+ if (state === "expired" || state === "uncertain") return `the claim expired or its outcome is unknown: run ${reconcileCommand(taskId)} (marks it integrated if the item is done); if the item is not done and the worker is gone, run ${release}`;
113
+ return `run ${reconcileCommand(taskId)} once the item is done in canonical state; or run ${release}`;
114
+ }
115
+
116
+ const blockedBy = (taskId, assignment) => `${describeAssignment(assignment)}; ${assignmentRemedy(taskId, assignment)}`;
117
+
118
+ function notLeased(taskId, assignmentId, item, suffix = "") {
119
+ if (!item) return new Error(`Assignment is not leased${suffix}: assignment ${assignmentId} does not exist for ${taskId}; inspect: node .agents/aidlc/scripts/assignment.mjs show ${taskId}`);
120
+ return new Error(`Assignment is not leased${suffix}: ${describeAssignment(item)}; run ${reconcileCommand(taskId)}`);
121
+ }
122
+
97
123
  function canonical(root, taskId) {
98
124
  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
125
  const result = JSON.parse(raw);
@@ -108,8 +134,12 @@ function expire(value, now) {
108
134
  }
109
135
  }
110
136
 
137
+ /** Read-only view: each assignment gains `effectiveState` and `stateLabel`
138
+ * (an overdue `leased` claim shows as `expired`); nothing is written. */
111
139
  export function showLocalAssignments(root, taskId) {
112
- return read(root, safeId(taskId, "task id"));
140
+ const value = read(root, safeId(taskId, "task id"));
141
+ const now = Date.now();
142
+ return { ...value, assignments: value.assignments.map((assignment) => { const effectiveState = effectiveAssignmentState(assignment, now); return { ...assignment, effectiveState, stateLabel: ASSIGNMENT_STATE_LABELS[effectiveState] ?? effectiveState }; }) };
113
143
  }
114
144
 
115
145
  export function claimLocalAssignment(root, { taskId, itemId, workerId, ownedPaths, leaseMs = 300000, baseCommit = null, branch = null, sessionId = null }) {
@@ -124,9 +154,11 @@ export function claimLocalAssignment(root, { taskId, itemId, workerId, ownedPath
124
154
  return locked(root, taskId, () => {
125
155
  const value = read(root, taskId); expire(value, Date.now());
126
156
  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}`);
157
+ const leased = active.find((entry) => entry.itemId === itemId && entry.state === "leased");
158
+ if (leased) throw new Error(`Item already leased: ${itemId}: claimed by ${blockedBy(taskId, leased)}`);
159
+ const unresolved = value.assignments.find((entry) => entry.itemId === itemId && ["reported", "uncertain"].includes(entry.state));
160
+ if (unresolved) throw new Error(`Item has unresolved assignment; reconcile before retry: ${itemId}: ${blockedBy(taskId, unresolved)}`);
161
+ 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}: held by ${blockedBy(taskId, other)}`);
130
162
  const attempt = 1 + Math.max(0, ...value.assignments.filter((entry) => entry.itemId === itemId).map((entry) => entry.attempt));
131
163
  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
164
  value.assignments.push(assignment); write(root, taskId, value);
@@ -139,7 +171,7 @@ export function heartbeatLocalAssignment(root, taskId, assignmentId, leaseMs = 3
139
171
  return locked(root, taskId, () => {
140
172
  const value = read(root, taskId); expire(value, Date.now());
141
173
  const item = value.assignments.find((entry) => entry.id === assignmentId);
142
- if (!item || item.state !== "leased") throw new Error("Assignment is not leased");
174
+ if (!item || item.state !== "leased") throw notLeased(taskId, assignmentId, item);
143
175
  if (sessionId !== undefined) { if (typeof sessionId !== "string" || !sessionId || sessionId.length > 300) throw new Error("Invalid sessionId"); item.sessionId = sessionId; }
144
176
  item.leaseExpiresAt = new Date(Date.now() + leaseMs).toISOString(); item.updatedAt = iso(); write(root, taskId, value); return item;
145
177
  });
@@ -152,12 +184,26 @@ export function reportLocalAssignment(root, taskId, assignmentId, { checkpoint =
152
184
  return locked(root, taskId, () => {
153
185
  const value = read(root, taskId); expire(value, Date.now());
154
186
  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");
187
+ if (!item || item.state !== "leased") throw notLeased(taskId, assignmentId, item, "; reconcile before reporting");
156
188
  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
189
  item.state = "reported"; item.checkpoint = checkpoint; item.commit = commit; item.changedPaths = changedPaths; item.updatedAt = iso(); write(root, taskId, value); return item;
158
190
  });
159
191
  }
160
192
 
193
+ /** Record the integration-branch commit created for a reported assignment, so
194
+ * the dashboard can show exactly what the item changed. It never changes the
195
+ * assignment state; `reconcile` still marks the item integrated. */
196
+ export function recordLocalIntegration(root, taskId, assignmentId, integrationCommit) {
197
+ safeId(taskId, "task id");
198
+ if (typeof integrationCommit !== "string" || !/^[a-f0-9]{40,64}$/.test(integrationCommit)) throw new Error("Invalid integration commit");
199
+ return locked(root, taskId, () => {
200
+ const value = read(root, taskId);
201
+ const item = value.assignments.find((entry) => entry.id === assignmentId);
202
+ if (!item || !["reported", "integrated"].includes(item.state)) throw new Error("Only a reported or integrated assignment can record its integration commit");
203
+ item.integrationCommit = integrationCommit; item.updatedAt = iso(); write(root, taskId, value); return item;
204
+ });
205
+ }
206
+
161
207
  export function reconcileLocalAssignments(root, taskId) {
162
208
  const task = canonical(root, taskId);
163
209
  return locked(root, taskId, () => {
@@ -5,6 +5,7 @@
5
5
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
6
6
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
7
7
  import { loadMemoryRegistry, nextAction, selectAgenticMemory } from "./store.mjs";
8
+ import { resolveTaskRuntimeRead } from "./task-runtime.mjs";
8
9
 
9
10
  const separator = process.platform === "win32" ? "\\" : "/";
10
11
 
@@ -228,10 +229,10 @@ export const compileContext = (root, task, phase = task.phase, options = {}) =>
228
229
  const phaseContract = readFileSync(phasePath, "utf8");
229
230
  let intake = "";
230
231
  if (phase === "clarify") {
231
- const parts = [".agents", "data", "tasks", task.id, "requirement.md"];
232
- let cursor = resolve(root), safe = true;
233
- 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"); }
234
- if (safe) { const requirement = readFileSync(cursor, "utf8"); if (Buffer.byteLength(requirement) > 8192) throw new Error("Requirement exceeds intake limit"); intake = `## User requirement\n${requirement}`; }
232
+ // The intake requirement (runtime/requirement.md, or the pre-runtime task-root copy); symlinked components are refused by the resolver.
233
+ let cursor;
234
+ try { cursor = resolveTaskRuntimeRead(root, task.id, "requirement.md"); } catch { throw new Error("Unsafe requirement path"); }
235
+ if (existsSync(cursor)) { const requirement = readFileSync(cursor, "utf8"); if (Buffer.byteLength(requirement) > 8192) throw new Error("Requirement exceeds intake limit"); intake = `## User requirement\n${requirement}`; }
235
236
  }
236
237
  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");
237
238
  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");