@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
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ // Fleet lead lifecycle entry point. It never reads or writes local lifecycle JSON.
3
+ import { execFile } from "node:child_process";
4
+ import { readFile } from "node:fs/promises";
5
+ import { promisify } from "node:util";
6
+ import { createRemoteReviewerClient, createRemoteStateClient, featureConfig, remoteConfig, reviewerConfig } from "./lib/remote-state-client.mjs";
7
+ import { compileContext } from "./lib/context-runtime.mjs";
8
+ import { checkGate, closeTask, handoffTask, nextAction, readyTaskItems, recordLesson, recordNoLessons, reopenTask, transitionTask, validateState } from "./lib/store.mjs";
9
+
10
+ const parse = (argv) => {
11
+ const positionals = []; const options = new Map();
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const value = argv[i];
14
+ if (!value.startsWith("--")) { positionals.push(value); continue; }
15
+ if (i + 1 >= argv.length || argv[i + 1].startsWith("--")) throw new Error(`Missing value for ${value}`);
16
+ if (options.has(value)) throw new Error(`Duplicate option: ${value}`);
17
+ options.set(value, argv[++i]);
18
+ }
19
+ return { positionals, options, option: (name) => options.get(name) };
20
+ };
21
+
22
+ const required = (value, name) => { if (typeof value !== "string" || !value.trim()) throw new Error(`${name} is required`); return value; };
23
+ const statuses = new Set(["active", "blocked_on_user", "paused"]);
24
+ const itemStatuses = new Set(["todo", "in_progress", "done", "deferred"]);
25
+ const evidenceKinds = new Set(["spec", "test", "lint", "review", "diagnostic"]);
26
+ const results = new Set(["pass", "fail", "skip"]);
27
+ const gateNames = new Set(["G0_confirm", "G1_review", "G2_codereview"]);
28
+ const now = () => new Date().toISOString();
29
+ const execFileAsync = promisify(execFile);
30
+ export const readIntegrationCommit = async ({ projectRoot, integrationBranch, gitRemote = "origin" }, run = execFileAsync) => {
31
+ if (!projectRoot?.startsWith("/") || !integrationBranch || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(integrationBranch) || integrationBranch.includes("..") || integrationBranch.endsWith("/") || !gitRemote || gitRemote.startsWith("-")) throw new Error("Valid project root, integration branch, and Git remote are required for G2 approval");
32
+ const ref = `refs/heads/${integrationBranch}`;
33
+ let stdout;
34
+ try { ({ stdout } = await run("git", ["ls-remote", "--exit-code", "--heads", gitRemote, ref], { cwd: projectRoot, timeout: 20000, maxBuffer: 65536 })); }
35
+ catch (error) { throw new Error(`Cannot verify integration branch remote ref: ${error.message}`); }
36
+ const rows = String(stdout).trim().split(/\r?\n/).filter(Boolean).map((line) => line.split("\t"));
37
+ if (rows.length !== 1 || rows[0][1] !== ref || !/^[a-f0-9]{40}$/i.test(rows[0][0])) throw new Error("Integration branch remote ref is missing or ambiguous");
38
+ return rows[0][0].toLowerCase();
39
+ };
40
+ const remoteCommand = (value) => typeof value === "string" ? value
41
+ .replace(/node (?:"([^"]*\/)context\.mjs"|([^\s"]*\/)context\.mjs)/g, (_match, quoted, bare) => quoted ? `node "${quoted}remote-state.mjs" phase context` : `node ${bare}remote-state.mjs phase context`)
42
+ .replace(/node (?:"([^"]*\/)state\.mjs"|([^\s"]*\/)state\.mjs)/g, (_match, quoted, bare) => quoted ? `node "${quoted}remote-state.mjs"` : `node ${bare}remote-state.mjs`)
43
+ .replace(/node (?:"([^"]*\/)gate-check\.mjs"|([^\s"]*\/)gate-check\.mjs)/g, (_match, quoted, bare) => quoted ? `node "${quoted}remote-state.mjs" gate check` : `node ${bare}remote-state.mjs gate check`)
44
+ .replace(/ --root (?:"[^"]*"|[^\s]+)/g, "") : value;
45
+ const remoteAction = (action) => action && typeof action === "object" ? { ...action, ...(action.command ? { command: remoteCommand(action.command) } : {}) } : action;
46
+
47
+ export const runRemoteState = async (argv, client, root, git = {}) => {
48
+ const { positionals: args, options, option } = parse(argv);
49
+ const [group, action, id, extra] = args;
50
+ const route = `${group ?? ""} ${action ?? ""}`;
51
+ const readOnly = new Set(["task show", "task next", "task ready", "gate check", "phase context"]);
52
+ const mutations = new Set(["task create", "task status", "task item", "task transition", "task handoff", "task close", "task reopen", "decision set", "evidence add", "lesson record", "lesson none", "gate snapshot"]);
53
+ if (!readOnly.has(route) && !mutations.has(route)) throw new Error(`Unsupported remote lifecycle command: ${route}. Gate approval is reviewer-only.`);
54
+ required(id, "task id");
55
+ const allowed = {
56
+ "task show": [], "task next": [], "task ready": [], "gate check": ["--gate"], "phase context": ["--phase", "--mode", "--item"],
57
+ "task create": ["--title", "--type", "--language", "--risk", "--area", "--feature"],
58
+ "task status": ["--status", "--mode", "--reason", "--source"], "task item": ["--status", "--label", "--depends-on", "--owns"],
59
+ "task transition": ["--to", "--mode", "--reason", "--source"],
60
+ "task handoff": ["--kind", "--reason", "--source"], "task close": ["--reason", "--source"], "task reopen": ["--to", "--reason", "--source"],
61
+ "decision set": ["--status", "--label", "--resolution"],
62
+ "evidence add": ["--kind", "--result", "--gate", "--area", "--source", "--detail"],
63
+ "lesson record": ["--area", "--summary", "--prevention", "--example", "--promotion", "--source"],
64
+ "lesson none": ["--reason", "--source"],
65
+ "gate snapshot": ["--gate", "--review-file"]
66
+ }[route];
67
+ for (const key of options.keys()) if (!allowed.includes(key)) throw new Error(`Unsupported option for ${route}: ${key}`);
68
+ if (extra && !["task item", "decision set", "lesson record"].includes(route)) throw new Error(`Unexpected argument: ${extra}`);
69
+ if (!extra && ["task item", "decision set", "lesson record"].includes(route)) throw new Error(`${route} requires an item/decision/lesson id`);
70
+
71
+ const current = await client.get();
72
+ if (!Number.isSafeInteger(current?.revision) || !current?.state?.tasks) throw new Error("Remote coordination returned invalid canonical state");
73
+ const state = current.state;
74
+ const previous = state.tasks[id];
75
+ if (route === "task show") return { ok: true, result: previous ?? null };
76
+ if (route === "task next") { if (!previous) throw new Error(`Unknown task: ${id}`); return { ok: true, result: { id, phase: previous.phase, gate: previous.gate, status: previous.status }, nextAction: remoteAction(nextAction(previous, root)) }; }
77
+ if (route === "task ready") { if (!previous) throw new Error(`Unknown task: ${id}`); return { ok: true, result: { id, ...readyTaskItems(previous) }, nextAction: remoteAction(nextAction(previous, root)) }; }
78
+ if (route === "phase context") {
79
+ if (!previous) throw new Error(`Unknown task: ${id}`);
80
+ const packet = compileContext(root, previous, option("--phase") ?? previous.phase, { mode: option("--mode") ?? "standard", itemId: option("--item") });
81
+ const warning = "Fleet lifecycle authority: use remote-state.mjs for task, evidence, decision, lesson, and gate commands. Lead credentials cannot approve gates; only a reviewer using AIDLC_FLEET_REVIEWER_TOKEN may run gate approve after explicit user assent.\n\n";
82
+ const content = warning + remoteCommand(packet.content);
83
+ return { ok: true, result: { ...packet, content, chars: content.length, estimatedTokens: Math.ceil(content.length / 4), canonicalRevision: current.revision } };
84
+ }
85
+ if (route === "gate check") {
86
+ if (!previous) throw new Error(`Unknown task: ${id}`);
87
+ const gate = option("--gate") ?? previous.gate;
88
+ if (!gateNames.has(gate)) throw new Error("Gate must be G0_confirm, G1_review, or G2_codereview");
89
+ const diagnostics = checkGate(root, state, id, gate);
90
+ return { ok: !diagnostics.some((item) => item.level === "ERROR"), result: { taskId: id, gate, diagnostics } };
91
+ }
92
+ if (route === "gate snapshot") {
93
+ if (!previous) throw new Error(`Unknown task: ${id}`);
94
+ const gate = required(option("--gate"), "--gate");
95
+ if (!gateNames.has(gate) || previous.gate !== gate || previous.status !== "blocked_on_user") throw new Error("Task is not waiting at the requested gate");
96
+ const reviewFile = required(option("--review-file"), "--review-file");
97
+ const reviewText = await readFile(reviewFile, "utf8");
98
+ if (!reviewText.trim() || reviewText.length > 20000) throw new Error("Review text must contain 1–20000 characters");
99
+ const integrationCommit = gate === "G2_codereview" ? await (git.readRemoteRef ?? readIntegrationCommit)({ projectRoot: root, integrationBranch: git.integrationBranch, gitRemote: git.gitRemote }) : undefined;
100
+ return { ok: true, result: await client.snapshot(current.revision, id, gate, reviewText, integrationCommit) };
101
+ }
102
+
103
+ let task;
104
+ if (route === "task create") {
105
+ if (previous || state.archive?.[id]) throw new Error(`Task already exists: ${id}`);
106
+ if (Object.keys({ ...state.tasks, ...state.archive }).some((key) => key.toLowerCase() === id.toLowerCase())) throw new Error("Task id collides case-insensitively");
107
+ const title = required(option("--title"), "--title");
108
+ const featureId = option("--feature");
109
+ if (featureId) {
110
+ const manifest = await client.featureGet(featureId);
111
+ if (manifest?.feature && !manifest.feature.tasks.some((member) => member.id === id)) throw new Error(`Task ${id} is not declared in feature ${featureId}; run feature link first`);
112
+ }
113
+ const recordedAt = now();
114
+ task = { id, title, ...(featureId ? { featureId } : {}), type: option("--type") ?? "infra", phase: "clarify", gate: "G0_confirm", status: "active",
115
+ language: option("--language") === "en" ? "en" : "vi", risk: option("--risk") ?? "normal",
116
+ areas: (option("--area") ?? "root").split(",").map((item) => item.trim()).filter(Boolean), branch: "—",
117
+ artifacts: { intent: `.agents/data/tasks/${id}/intent.md`, design: `.agents/data/tasks/${id}/design.md`, workplan: `.agents/data/tasks/${id}/workplan.md` },
118
+ decisions: [], tasks: [], evidence: [], createdAt: recordedAt, updatedAt: recordedAt };
119
+ } else {
120
+ if (!previous) throw new Error(`Unknown task: ${id}`);
121
+ task = structuredClone(previous);
122
+ if (task.status === "blocked_on_user" && !["evidence add", "task status", "task handoff", "task close"].includes(route)) throw new Error("Task is frozen at a human gate");
123
+ if (route === "task status") {
124
+ const status = required(option("--status"), "--status");
125
+ if (!statuses.has(status)) throw new Error("Invalid task status");
126
+ if (task.status === "blocked_on_user" && status !== "blocked_on_user") {
127
+ if (option("--mode") !== "audited") throw new Error("Cancelling a human-gate wait requires --mode audited");
128
+ const reason = required(option("--reason"), "--reason"); const source = required(option("--source"), "--source");
129
+ task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Cancelled gate wait at ${task.gate}: ${reason}`, recordedAt: now() });
130
+ }
131
+ if (task.status === "active" && status === "paused") {
132
+ if (option("--mode") !== "audited") throw new Error("Pausing active work requires --mode audited");
133
+ const reason = required(option("--reason"), "--reason"); const source = required(option("--source"), "--source");
134
+ task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Paused while active: ${reason}`, recordedAt: now() });
135
+ }
136
+ if (status === "blocked_on_user") {
137
+ const diagnostics = checkGate(root, state, id, task.gate).filter((item) => item.level === "ERROR");
138
+ if (diagnostics.length) throw new Error(`Gate is not ready: ${diagnostics.map((item) => item.code).join(", ")}`);
139
+ }
140
+ task.status = status;
141
+ } else if (route === "task handoff") {
142
+ const clone = structuredClone(state); clone.tasks[id] = task;
143
+ task = handoffTask(clone, id, required(option("--kind"), "--kind"), required(option("--reason"), "--reason"), required(option("--source"), "--source"));
144
+ } else if (route === "task close") {
145
+ const clone = structuredClone(state); clone.tasks[id] = task;
146
+ task = closeTask(clone, id, required(option("--reason"), "--reason"), required(option("--source"), "--source"));
147
+ } else if (route === "task reopen") {
148
+ const clone = structuredClone(state); clone.tasks[id] = task;
149
+ task = reopenTask(clone, id, required(option("--to"), "--to"), required(option("--reason"), "--reason"), required(option("--source"), "--source"));
150
+ } else if (route === "task item") {
151
+ const status = required(option("--status"), "--status");
152
+ if (!itemStatuses.has(status)) throw new Error("Invalid item status");
153
+ const item = task.tasks.find((entry) => entry.id === extra);
154
+ const split = (name) => option(name)?.split(",").map((entry) => entry.trim()).filter(Boolean);
155
+ if (item) { item.status = status; if (options.has("--label")) item.label = option("--label"); if (options.has("--depends-on")) item.dependsOn = split("--depends-on"); if (options.has("--owns")) item.ownedPaths = split("--owns"); }
156
+ else task.tasks.push({ id: extra, label: option("--label") ?? extra, status, ...(split("--depends-on") ? { dependsOn: split("--depends-on") } : {}), ...(split("--owns") ? { ownedPaths: split("--owns") } : {}) });
157
+ readyTaskItems(task);
158
+ } else if (route === "task transition") {
159
+ if (option("--mode") !== "audited") throw new Error("Transition requires --mode audited");
160
+ const reason = required(option("--reason"), "--reason"); const source = required(option("--source"), "--source");
161
+ const target = required(option("--to"), "--to");
162
+ if (target === "done" && task.featureId) {
163
+ const manifest = await client.featureGet(task.featureId);
164
+ const member = manifest?.feature?.tasks?.find((entry) => entry.id === id);
165
+ if (!member) throw new Error(`Task ${id} is missing from feature ${task.featureId}`);
166
+ const waitingOn = member.dependsOn.filter((dependency) => (state.tasks[dependency] ?? state.archive?.[dependency])?.status !== "done");
167
+ if (waitingOn.length) throw new Error(`Feature prerequisites incomplete for ${id}: ${waitingOn.join(", ")}`);
168
+ }
169
+ const clone = structuredClone(state); clone.tasks[id] = task;
170
+ transitionTask(clone, id, target);
171
+ task = clone.tasks[id];
172
+ task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Audited transition ${previous.phase} → ${target}: ${reason}`, recordedAt: now() });
173
+ } else if (route === "decision set") {
174
+ const status = required(option("--status"), "--status");
175
+ if (!["unresolved", "approved", "changed", "dropped"].includes(status)) throw new Error("Invalid decision status");
176
+ const decision = task.decisions.find((entry) => entry.id === extra);
177
+ if (decision) { decision.status = status; if (options.has("--label")) decision.label = option("--label"); if (options.has("--resolution")) decision.resolution = option("--resolution"); }
178
+ else task.decisions.push({ id: extra, label: option("--label") ?? extra, status, resolution: option("--resolution") });
179
+ } else if (route === "evidence add") {
180
+ const kind = required(option("--kind"), "--kind"); const result = required(option("--result"), "--result");
181
+ if (!evidenceKinds.has(kind) || !results.has(result)) throw new Error("Invalid evidence kind or result");
182
+ const area = option("--area");
183
+ if (area && !task.areas.includes(area)) throw new Error("Evidence area must belong to task");
184
+ if (["test", "lint"].includes(kind) && task.areas.length > 1 && !area) throw new Error("Multi-area verification requires --area");
185
+ task.evidence.push({ kind, gate: option("--gate"), area, result, source: option("--source") ?? "fleet lead", detail: option("--detail"), recordedAt: now() });
186
+ } else if (route === "lesson record") {
187
+ recordLesson(task, { id: extra, areas: (option("--area") ?? task.areas.join(",")).split(",").map((entry) => entry.trim()).filter(Boolean), summary: option("--summary"), prevention: option("--prevention"), example: option("--example"), promotion: option("--promotion") ?? "not promoted", source: required(option("--source"), "--source") });
188
+ } else if (route === "lesson none") {
189
+ recordNoLessons(task, required(option("--reason"), "--reason"), required(option("--source"), "--source"));
190
+ }
191
+ task.updatedAt = now();
192
+ }
193
+ const candidate = structuredClone(state); candidate.tasks[id] = task; validateState(candidate);
194
+ const saved = await client.apply(current.revision, id, task);
195
+ const canonical = saved?.state?.tasks?.[id];
196
+ if (!Number.isSafeInteger(saved?.revision) || !canonical) throw new Error("Remote coordination did not confirm the mutation");
197
+ return { ok: true, result: { task: canonical, revision: saved.revision }, nextAction: remoteAction(saved.nextActions?.[id] ?? nextAction(canonical, root)) };
198
+ };
199
+
200
+ export const runRemoteReview = async (argv, client, git = {}) => {
201
+ const { positionals: args, options, option } = parse(argv);
202
+ const [group, action, taskId] = args;
203
+ if (group !== "gate" || !["inbox", "approve"].includes(action)) throw new Error("Reviewer commands are gate inbox and gate approve");
204
+ if (action === "inbox") {
205
+ if (taskId || options.size) throw new Error("gate inbox takes no arguments");
206
+ return { ok: true, result: await client.inbox() };
207
+ }
208
+ required(taskId, "task id");
209
+ if (args.length !== 3 || [...options.keys()].some((key) => !["--snapshot", "--source"].includes(key))) throw new Error("Usage: gate approve <task-id> --snapshot <id> --source <explicit user message>");
210
+ const snapshotId = required(option("--snapshot"), "--snapshot");
211
+ const source = required(option("--source"), "--source");
212
+ const current = await client.get();
213
+ if (!Number.isSafeInteger(current?.revision) || !current.state?.tasks?.[taskId]) throw new Error("Task is absent from canonical remote state");
214
+ const inbox = await client.inbox();
215
+ if (!Array.isArray(inbox)) throw new Error("Remote gate inbox returned invalid data");
216
+ const snapshot = inbox.find((entry) => entry.id === snapshotId && entry.taskId === taskId);
217
+ if (!snapshot || current.state.tasks[taskId].gate !== snapshot.gate || current.state.tasks[taskId].status !== "blocked_on_user") throw new Error("Gate snapshot is stale or does not match the waiting task");
218
+ let integrationCommit;
219
+ if (snapshot.gate === "G2_codereview") {
220
+ if (!/^[a-f0-9]{40}$/i.test(snapshot.integrationCommit ?? "")) throw new Error("G2 snapshot has no valid integration commit");
221
+ integrationCommit = await (git.readRemoteRef ?? readIntegrationCommit)({ projectRoot: git.projectRoot, integrationBranch: git.integrationBranch, gitRemote: git.gitRemote });
222
+ if (integrationCommit !== snapshot.integrationCommit.toLowerCase()) throw new Error("Integration branch remote ref changed since G2 snapshot; review a fresh snapshot");
223
+ }
224
+ const saved = await client.approve(current.revision, taskId, snapshotId, source, integrationCommit);
225
+ const task = saved?.state?.tasks?.[taskId];
226
+ if (!Number.isSafeInteger(saved?.revision) || !task) throw new Error("Remote coordination did not confirm gate approval");
227
+ return { ok: true, result: { task, revision: saved.revision }, nextAction: remoteAction(saved.nextActions?.[taskId]) };
228
+ };
229
+
230
+ export const runRemoteFeature = async (argv, client) => {
231
+ const { positionals: args, options, option } = parse(argv);
232
+ const [group, action, featureId, taskId] = args;
233
+ if (group !== "feature" || !["create", "show", "ready", "link"].includes(action)) throw new Error("Unsupported remote feature command");
234
+ required(featureId, "feature id");
235
+ if (taskId && action !== "link") throw new Error(`Unexpected task id for feature ${action}`);
236
+ if (action === "link") required(taskId, "task id");
237
+ const allowed = { create: ["--branch", "--tasks"], show: [], ready: [], link: ["--depends-on"] }[action];
238
+ for (const key of options.keys()) if (!allowed.includes(key)) throw new Error(`Unsupported option: ${key}`);
239
+ const current = await client.featureGet(featureId);
240
+ if (!Number.isSafeInteger(current?.revision)) throw new Error("Remote feature service returned invalid revision");
241
+ const feature = current.feature;
242
+ if (action === "show") return { ok: true, result: feature };
243
+ if (action === "ready") {
244
+ if (!feature) throw new Error(`Unknown feature: ${featureId}`);
245
+ const ready = await client.featureReady(featureId);
246
+ if (!Number.isSafeInteger(ready?.stateRevision) || !Array.isArray(ready.items)) throw new Error("Remote feature readiness unavailable");
247
+ return { ok: true, result: { feature, ...ready } };
248
+ }
249
+ let next;
250
+ if (action === "create") {
251
+ if (feature) throw new Error(`Feature already exists: ${featureId}`);
252
+ const tasks = required(option("--tasks"), "--tasks").split(",").map((entry) => entry.trim()).filter(Boolean);
253
+ if (!tasks.length || new Set(tasks).size !== tasks.length) throw new Error("Feature requires unique initial tasks");
254
+ const canonical = await client.get();
255
+ if (!canonical?.state?.tasks) throw new Error("Remote lifecycle state unavailable");
256
+ for (const id of tasks) if (canonical.state.tasks[id]?.featureId !== featureId) throw new Error(`Create canonical task ${id} with --feature ${featureId} before feature create`);
257
+ next = { schemaVersion: 1, id: featureId, integrationBranch: required(option("--branch"), "--branch"), tasks: tasks.map((id) => ({ id, dependsOn: [] })) };
258
+ } else {
259
+ if (!feature) throw new Error(`Unknown feature: ${featureId}`);
260
+ const canonical = await client.get();
261
+ if (!canonical?.state?.tasks) throw new Error("Remote lifecycle state unavailable");
262
+ if (canonical.state.tasks[taskId] && canonical.state.tasks[taskId].featureId !== featureId) throw new Error(`Declare feature membership before task creation: ${taskId}`);
263
+ next = structuredClone(feature);
264
+ const dependsOn = (option("--depends-on") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
265
+ const member = next.tasks.find((entry) => entry.id === taskId);
266
+ if (member) member.dependsOn = dependsOn;
267
+ else next.tasks.push({ id: taskId, dependsOn });
268
+ }
269
+ const saved = await client.featureApply(current.revision, next);
270
+ const canonical = saved?.fleet?.features?.[featureId];
271
+ if (!canonical) throw new Error("Remote feature service did not confirm manifest mutation");
272
+ return { ok: true, result: { feature: canonical, revision: saved.revision } };
273
+ };
274
+
275
+ export const runRemoteLease = async (argv, client) => {
276
+ const { positionals: args, options, option } = parse(argv);
277
+ const [group, action, taskId] = args;
278
+ if (group !== "run" || !["claim", "release"].includes(action) || args.length !== 3) throw new Error("Usage: run claim <task-id> [--lease-ms <ms>] | run release <task-id> --epoch <n>");
279
+ required(taskId, "task id");
280
+ const allowed = action === "claim" ? ["--lease-ms"] : ["--epoch"];
281
+ for (const key of options.keys()) if (!allowed.includes(key)) throw new Error(`Unsupported option: ${key}`);
282
+ const current = await client.fleetGet();
283
+ if (!Number.isSafeInteger(current?.revision) || !current.fleet?.leads) throw new Error("Remote fleet state unavailable");
284
+ if (action === "claim") {
285
+ const leaseMs = option("--lease-ms") === undefined ? 300000 : Number(option("--lease-ms"));
286
+ if (!Number.isSafeInteger(leaseMs) || leaseMs < 60000 || leaseMs > 3600000) throw new Error("--lease-ms must be 60000..3600000");
287
+ const saved = await client.runClaim(current.revision, taskId, leaseMs);
288
+ const lease = saved?.fleet?.leads?.[taskId];
289
+ if (!Number.isSafeInteger(lease?.epoch) || !lease.expiresAt) throw new Error("Remote fleet service did not confirm lead claim");
290
+ return { ok: true, result: { taskId, epoch: lease.epoch, expiresAt: lease.expiresAt, fleetRevision: saved.revision } };
291
+ }
292
+ const epoch = Number(required(option("--epoch"), "--epoch"));
293
+ if (!Number.isSafeInteger(epoch) || epoch < 1) throw new Error("--epoch must be a positive integer");
294
+ const saved = await client.runRelease(current.revision, taskId, epoch);
295
+ if (!Number.isSafeInteger(saved?.revision)) throw new Error("Remote fleet service did not confirm lead release");
296
+ return { ok: true, result: { taskId, epoch, fleetRevision: saved.revision, released: true } };
297
+ };
298
+
299
+ const isEntry = process.argv[1] && new URL(`file://${process.argv[1]}`).href === import.meta.url;
300
+ if (isEntry) {
301
+ try {
302
+ const argv = process.argv.slice(2);
303
+ const isReview = argv[0] === "gate" && ["inbox", "approve"].includes(argv[1]);
304
+ const config = isReview ? reviewerConfig() : ["feature", "run"].includes(argv[0]) ? featureConfig() : remoteConfig();
305
+ const result = isReview ? await runRemoteReview(argv, createRemoteReviewerClient(config), config) : argv[0] === "feature" ? await runRemoteFeature(argv, createRemoteStateClient(config)) : argv[0] === "run" ? await runRemoteLease(argv, createRemoteStateClient(config)) : await runRemoteState(argv, createRemoteStateClient(config), config.projectRoot, config);
306
+ console.log(JSON.stringify(result, null, 2));
307
+ if (!result.ok) process.exitCode = 1;
308
+ } catch (error) { console.error(JSON.stringify({ ok: false, error: error.message, code: error.code, currentRevision: error.current?.revision }, null, 2)); process.exitCode = 1; }
309
+ }
@@ -1,6 +1,8 @@
1
1
  import { acquireStateLock, failEntry, loadState, loadTask, parseArguments, renderViews, 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"], booleanOptions: ["--all"] });
6
8
  validateArguments(parsed, { minPositionals: 0, maxPositionals: 1, booleanOptions: ["--all"], usage: "Usage: render.mjs <task-id> [--root <path>] | --all [--root <path>]" });
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { acquireStateLock, approveAndAdvance, checkGate, closeTask, failEntry, handoffTask, listTaskSummaries, loadMemoryRegistry, loadState, loadTask, migrateState, nextAction, option, parseArguments, promoteAgenticMemory, rebuildLessonIndex, recordLesson, recordNoLessons, renderViews, reopenTask, retireAgenticMemory, rootOption, saveState, searchLessons, supersedeTask, transitionTask, validateArguments } from "./lib/runtime.mjs";
3
+ import { readLiveLease } from "./lib/loop-lease.mjs";
4
+ import { listFeatures, loadFeature, readyFeatureTasks, saveFeature } from "./lib/feature-store.mjs";
5
+ import { readyTaskItems } from "./lib/store.mjs";
6
+ import { requireLocalAuthority } from "./lib/authority.mjs";
3
7
 
4
8
  try {
9
+ requireLocalAuthority();
5
10
  const raw = process.argv.slice(2);
6
- const valueOptions = ["--root", "--status", "--limit", "--cursor", "--query", "--title", "--type", "--language", "--risk", "--area", "--mode", "--reason", "--source", "--to", "--kind", "--successor", "--label", "--resolution", "--result", "--gate", "--detail", "--summary", "--prevention", "--example", "--promotion", "--guidance", "--phase", "--priority", "--source-task", "--source-lesson", "--approved-by", "--switch-from"];
11
+ const valueOptions = ["--root", "--status", "--limit", "--cursor", "--query", "--title", "--type", "--language", "--risk", "--area", "--mode", "--reason", "--source", "--to", "--kind", "--successor", "--label", "--resolution", "--result", "--gate", "--detail", "--summary", "--prevention", "--example", "--promotion", "--guidance", "--phase", "--priority", "--source-task", "--source-lesson", "--approved-by", "--switch-from", "--feature", "--depends-on", "--branch", "--owns"];
7
12
  const parsed = parseArguments(raw, { valueOptions, booleanOptions: ["--include-archive"] });
8
13
  const args = parsed.positionals;
9
14
  const [group, action, rawId] = args;
@@ -14,7 +19,8 @@ const schemas = {
14
19
  "task list": { minPositionals: 2, valueOptions: ["--status", "--limit", "--cursor"], booleanOptions: ["--include-archive"] },
15
20
  "task find": { minPositionals: 2, valueOptions: ["--query", "--status", "--limit", "--cursor"], booleanOptions: ["--include-archive"], requiredOptions: ["--query"] },
16
21
  "task next": { minPositionals: 3 },
17
- "task create": { minPositionals: 3, valueOptions: ["--title", "--type", "--language", "--risk", "--area", "--switch-from"], requiredOptions: ["--title"] },
22
+ "task ready": { minPositionals: 3 },
23
+ "task create": { minPositionals: 3, valueOptions: ["--title", "--type", "--language", "--risk", "--area", "--switch-from", "--feature", "--depends-on"], requiredOptions: ["--title"] },
18
24
  "task transition": { minPositionals: 3, valueOptions: ["--mode", "--reason", "--source", "--to"], requiredOptions: ["--mode", "--reason", "--source", "--to"], usage: "task transition requires --mode audited, --reason, --source, and --to" },
19
25
  "task archive": { minPositionals: 3, valueOptions: ["--reason", "--source"], requiredOptions: ["--reason", "--source"] },
20
26
  "task handoff": { minPositionals: 3, valueOptions: ["--kind", "--reason", "--source"], requiredOptions: ["--kind", "--reason", "--source"] },
@@ -22,7 +28,11 @@ const schemas = {
22
28
  "task supersede": { minPositionals: 3, valueOptions: ["--successor", "--reason", "--source"], requiredOptions: ["--successor", "--reason", "--source"] },
23
29
  "task reopen": { minPositionals: 3, valueOptions: ["--to", "--reason", "--source"], requiredOptions: ["--to", "--reason", "--source"] },
24
30
  "task status": { minPositionals: 3, valueOptions: ["--status", "--mode", "--reason", "--source"], requiredOptions: ["--status"] },
25
- "task item": { minPositionals: 4, valueOptions: ["--status", "--label"], requiredOptions: ["--status"] },
31
+ "task item": { minPositionals: 4, valueOptions: ["--status", "--label", "--depends-on", "--owns"], requiredOptions: ["--status"] },
32
+ "feature create": { minPositionals: 3, valueOptions: ["--branch"], requiredOptions: ["--branch"] },
33
+ "feature show": { minPositionals: 3 },
34
+ "feature ready": { minPositionals: 3 },
35
+ "feature link": { minPositionals: 4, valueOptions: ["--depends-on"] },
26
36
  "decision set": { minPositionals: 4, valueOptions: ["--status", "--label", "--resolution"], requiredOptions: ["--status"] },
27
37
  "evidence add": { minPositionals: 3, valueOptions: ["--kind", "--result", "--gate", "--area", "--source", "--detail"], requiredOptions: ["--kind", "--result"] },
28
38
  "lesson record": { minPositionals: 4, valueOptions: ["--area", "--summary", "--prevention", "--example", "--promotion", "--source"] },
@@ -34,7 +44,7 @@ const schemas = {
34
44
  "memory retire": { minPositionals: 3, valueOptions: ["--reason", "--approved-by"], requiredOptions: ["--reason", "--approved-by"] },
35
45
  "gate approve": { minPositionals: 3, valueOptions: ["--gate", "--source"], requiredOptions: ["--gate", "--source"] }
36
46
  };
37
- const usage = "Usage: state.mjs state migrate | task create|show|list|find|next|status|item|transition|archive|handoff|close|supersede|reopen | decision set | evidence add | lesson record|none|search|rebuild | memory list|promote|retire | gate approve";
47
+ const usage = "Usage: state.mjs state migrate | task create|show|list|find|next|ready|status|item|transition|archive|handoff|close|supersede|reopen | feature create|show|ready|link | decision set | evidence add | lesson record|none|search|rebuild | memory list|promote|retire | gate approve";
38
48
  const schema = schemas[`${group ?? ""} ${action ?? ""}`];
39
49
  if (!schema) throw new Error(usage);
40
50
  validateArguments(parsed, { ...schema, maxPositionals: schema.maxPositionals ?? schema.minPositionals, usage: schema.usage ?? usage });
@@ -69,6 +79,15 @@ const persist = (tasks, rebuildLessons = false) => {
69
79
  };
70
80
  const emit = (result, task) => console.log(JSON.stringify({ ok: true, ...(result === undefined ? {} : { result }), ...(warnings.length ? { warnings } : {}), ...(task ? { nextAction: nextAction(task, root) } : {}) }, null, 2));
71
81
  const respond = (task, result = {}) => emit({ task, ...result }, task);
82
+ const requireFeaturePrerequisites = (task) => {
83
+ if (!task) throw new Error("Unknown active task");
84
+ if (!task.featureId) return;
85
+ const feature = loadFeature(root, task.featureId, state);
86
+ const member = feature?.tasks.find((entry) => entry.id === task.id);
87
+ if (!member) throw new Error(`Task is missing from feature manifest: ${task.id}`);
88
+ const waitingOn = member.dependsOn.filter((dependency) => (state.tasks[dependency] ?? state.archive?.[dependency])?.status !== "done");
89
+ if (waitingOn.length) throw new Error(`Feature prerequisites incomplete for ${task.id}: ${waitingOn.join(", ")}`);
90
+ };
72
91
 
73
92
  if (group === "state" && action === "migrate") {
74
93
  emit(migrateState(root, state));
@@ -85,6 +104,31 @@ if (group === "state" && action === "migrate") {
85
104
  const task = loadTask(root, id, state);
86
105
  if (!task) throw new Error(`Unknown task: ${id}`);
87
106
  emit({ id: task.id, phase: task.phase, gate: task.gate, status: task.status }, task);
107
+ } else if (group === "task" && action === "ready") {
108
+ const task = loadTask(root, id, state);
109
+ if (!task) throw new Error(`Unknown task: ${id}`);
110
+ emit({ id, ...readyTaskItems(task) }, task);
111
+ } else if (group === "feature" && action === "create") {
112
+ if (loadFeature(root, id, state)) throw new Error(`Feature already exists: ${id}`);
113
+ emit(saveFeature(root, { schemaVersion: 1, id, integrationBranch: option(parsed, "--branch"), tasks: [] }, state));
114
+ } else if (group === "feature" && action === "show") {
115
+ const feature = loadFeature(root, id, state);
116
+ if (!feature) throw new Error(`Unknown feature: ${id}`);
117
+ emit(feature);
118
+ } else if (group === "feature" && action === "ready") {
119
+ const feature = loadFeature(root, id, state);
120
+ if (!feature) throw new Error(`Unknown feature: ${id}`);
121
+ emit(readyFeatureTasks(feature, state));
122
+ } else if (group === "feature" && action === "link") {
123
+ const taskId = args[3]; const feature = loadFeature(root, id, state); const task = state.tasks[taskId];
124
+ if (!feature || !task) throw new Error(`Unknown feature or active task: ${id}/${taskId}`);
125
+ if (task.featureId && task.featureId !== id) throw new Error(`Task belongs to another feature: ${taskId}`);
126
+ if (listFeatures(root, state).some((other) => other.id !== id && other.tasks.some((entry) => entry.id === taskId))) throw new Error(`Task belongs to another feature: ${taskId}`);
127
+ const dependsOn = (option(parsed, "--depends-on") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
128
+ const entry = feature.tasks.find((item) => item.id === taskId);
129
+ if (entry) entry.dependsOn = dependsOn; else feature.tasks.push({ id: taskId, dependsOn });
130
+ saveFeature(root, feature, state);
131
+ task.featureId = id; task.updatedAt = now(); persist([task]); respond(task, { feature });
88
132
  } else if (group === "task" && action === "create") {
89
133
  const title = option(parsed, "--title");
90
134
  if (!id || !title) throw new Error("task create requires <task-id> and --title");
@@ -97,11 +141,14 @@ if (group === "state" && action === "migrate") {
97
141
  const unknownAcknowledgement = acknowledgedTokens.filter((ack) => !actionableByLowercase.has(ack.toLowerCase()));
98
142
  if (unknownAcknowledgement.length) throw new Error(`--switch-from must name only actionable task(s); not actionable: ${unknownAcknowledgement.join(", ")}`);
99
143
  const acknowledged = [...new Set(acknowledgedTokens.map((ack) => actionableByLowercase.get(ack.toLowerCase())))];
100
- const stranded = actionable.filter((existing) => !acknowledged.includes(existing));
144
+ const featureId = option(parsed, "--feature");
145
+ const feature = featureId ? loadFeature(root, featureId, state) : undefined;
146
+ if (featureId && !feature) throw new Error(`Unknown feature: ${featureId}`);
147
+ const stranded = actionable.filter((existing) => !acknowledged.includes(existing) && (!feature || state.tasks[existing].featureId !== featureId));
101
148
  if (stranded.length) throw new Error(`Task create is blocked while other work is actionable: ${stranded.join(", ")}. With explicit user direction to switch, rerun with --switch-from ${actionable.join(",")}; acknowledged tasks keep their state and remain the resume target`);
102
149
  const recordedAt = now();
103
150
  const task = state.tasks[id] = {
104
- id, title, type: option(parsed, "--type") ?? "infra", phase: "clarify", gate: "G0_confirm", status: "active",
151
+ id, title, featureId, type: option(parsed, "--type") ?? "infra", phase: "clarify", gate: "G0_confirm", status: "active",
105
152
  language: option(parsed, "--language") === "en" ? "en" : "vi", risk: option(parsed, "--risk") ?? "normal",
106
153
  areas: (option(parsed, "--area") ?? "root").split(",").map((area) => area.trim()).filter(Boolean), branch: "—",
107
154
  artifacts: { intent: `.agents/data/tasks/${id}/intent.md`, design: `.agents/data/tasks/${id}/design.md`, workplan: `.agents/data/tasks/${id}/workplan.md` },
@@ -112,17 +159,22 @@ if (group === "state" && action === "migrate") {
112
159
  prior.evidence.push({ kind: "diagnostic", result: "pass", source: `task create ${id}`, detail: `Switch acknowledged: created ${id} while ${prior.id} was actionable; ${prior.id} keeps its state — resume with task next ${prior.id}`, recordedAt });
113
160
  prior.updatedAt = recordedAt;
114
161
  }
162
+ if (feature) {
163
+ const dependsOn = (option(parsed, "--depends-on") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
164
+ feature.tasks.push({ id, dependsOn });
165
+ saveFeature(root, feature, state);
166
+ }
115
167
  persist([task, ...acknowledgedTasks]); respond(task);
116
168
  } else if (group === "task" && action === "transition") {
117
169
  const mode = option(parsed, "--mode"); const reason = option(parsed, "--reason"); const source = option(parsed, "--source");
118
170
  if (mode !== "audited" || !reason?.trim() || !source?.trim()) throw new Error("task transition requires --mode audited, --reason, and --source");
119
- const from = state.tasks[id]?.phase; const target = option(parsed, "--to"); const task = transitionTask(state, id, target);
171
+ const from = state.tasks[id]?.phase; const target = option(parsed, "--to"); if (target === "done") requireFeaturePrerequisites(state.tasks[id]); const task = transitionTask(state, id, target);
120
172
  task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Audited transition ${from} → ${target}: ${reason}`, recordedAt: now() });
121
173
  persist([task]); respond(task);
122
174
  } else if (group === "task" && action === "archive") {
123
175
  const reason = option(parsed, "--reason"); const source = option(parsed, "--source");
124
176
  if (!reason?.trim() || !source?.trim()) throw new Error("task archive requires --reason and --source");
125
- const task = transitionTask(state, id, "done");
177
+ requireFeaturePrerequisites(state.tasks[id]); const task = transitionTask(state, id, "done");
126
178
  task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Archive to done: ${reason}`, recordedAt: now() });
127
179
  persist([task], true); respond(task);
128
180
  } else if (group === "task" && action === "handoff") {
@@ -164,8 +216,11 @@ if (group === "state" && action === "migrate") {
164
216
  const itemId = args[3]; const task = state.tasks[id]; const status = option(parsed, "--status");
165
217
  if (!task || !itemId || !["todo", "in_progress", "done", "deferred"].includes(status)) throw new Error("task item requires active task id, item id, and valid --status");
166
218
  const item = task.tasks.find((entry) => entry.id === itemId);
167
- if (item) { item.status = status; if (parsed.present.has("--label")) item.label = option(parsed, "--label"); }
168
- else task.tasks.push({ id: itemId, label: option(parsed, "--label") ?? itemId, status });
219
+ const dependsOn = parsed.present.has("--depends-on") ? (option(parsed, "--depends-on") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean) : undefined;
220
+ const ownedPaths = parsed.present.has("--owns") ? (option(parsed, "--owns") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean) : undefined;
221
+ if (item) { item.status = status; if (parsed.present.has("--label")) item.label = option(parsed, "--label"); if (dependsOn) item.dependsOn = dependsOn; if (ownedPaths) item.ownedPaths = ownedPaths; }
222
+ else task.tasks.push({ id: itemId, label: option(parsed, "--label") ?? itemId, status, ...(dependsOn ? { dependsOn } : {}), ...(ownedPaths ? { ownedPaths } : {}) });
223
+ readyTaskItems(task);
169
224
  task.updatedAt = now(); persist([task]); respond(task);
170
225
  } else if (group === "decision" && action === "set") {
171
226
  const decisionId = args[3]; const task = state.tasks[id]; const status = option(parsed, "--status");
@@ -211,9 +266,10 @@ if (group === "state" && action === "migrate") {
211
266
  } else if (group === "gate" && action === "approve") {
212
267
  const gate = option(parsed, "--gate"); const source = option(parsed, "--source");
213
268
  if (!id || !gate || !source) throw new Error("gate approve requires active task id, --gate, and explicit --source");
269
+ if (readLiveLease(root, id)) throw new Error(`Gate approval is unavailable while loop runner owns ${id}; wait for the runner to stop at the human gate`);
214
270
  const outcome = approveAndAdvance(root, state, id, gate, source); persist([outcome.task]); emit({ task: outcome.task, idempotent: outcome.idempotent }, outcome.task);
215
271
  } else {
216
- throw new Error("Usage: state.mjs state migrate | task create|show|list|find|next|status|item|transition|archive|handoff|close|supersede|reopen | decision set | evidence add | lesson record|none|search|rebuild | memory list|promote|retire | gate approve");
272
+ throw new Error(usage);
217
273
  }
218
274
  } finally {
219
275
  process.off("exit", releaseOnce);
@@ -1,6 +1,8 @@
1
1
  import { failEntry, loadState, loadTask, nextAction, 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"], booleanOptions: ["--require-stop"] });
6
8
  validateArguments(parsed, { minPositionals: 1, booleanOptions: ["--require-stop"], usage: "Usage: task-next.mjs <task-id> [--root <path>] [--require-stop]" });
@@ -28,7 +28,7 @@ Use CRITICS during clarify or plan only when risk is elevated, requirements are
28
28
 
29
29
  ## CRITICS — adversarial review
30
30
 
31
- Before recording passing review evidence for G2, report these sections against the approved decisions, cited anchors, and task diff:
31
+ Before recording passing review evidence for Review implementation, report these sections against the approved decisions, cited anchors, and task diff:
32
32
 
33
33
  1. **Criteria** — acceptance criteria, invariants, and exact spec anchors checked.
34
34
  2. **Risks** — regression, safety, compatibility, and unverified assumptions.
@@ -36,6 +36,6 @@ Before recording passing review evidence for G2, report these sections against t
36
36
  4. **Tests / evidence** — commands or inspections run, results, and residual gaps.
37
37
  5. **Improvements** — required fixes or explicitly deferred follow-ups, each tied to an issue.
38
38
  6. **Conclusion** — `pass`, `needs changes`, or `escalate`; a pass requires no unresolved critical/high issue.
39
- 7. **Stop / escalate** — reopen G1 for a structural/spec conflict; otherwise defer to the human gate when authority is needed.
39
+ 7. **Stop / escalate** — reopen the Approve plan step for a structural/spec conflict; otherwise defer to the human gate when authority is needed.
40
40
 
41
- CRITICS is a review record, not approval. Only the state machine, recorded evidence, and human G2 approval advance the task.
41
+ CRITICS is a review record, not approval. Only the state machine, recorded evidence, and human Review implementation approval advance the task.
@@ -2,8 +2,8 @@
2
2
 
3
3
  > Review artifact derived from `.agents/data/state/aidlc-state.json`. Resolve decisions and update progress with `node .agents/aidlc/scripts/state.mjs`, then run `node .agents/aidlc/scripts/render.mjs`.
4
4
 
5
- ## 🧩 Decisions (Gate G1 — approve before build)
5
+ ## 🧩 Decisions (Approve plan — before implementation)
6
6
  - <generated decision state>
7
7
 
8
- ## 🧩 Tasks (Gate G2 — build execution)
8
+ ## 🧩 Tasks (Review implementation — work items)
9
9
  - <generated task state>
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: aidlc-build
3
- description: "AI-DLC phase 3 (Gate G2): after explicit G1 plan approval, implement the canonical work items, run the narrowest tests/lint, then run code-review. Triggers when the AI-DLC Orchestrator routes an approved post-G1 task to build. Ends at G2 for explicit user code review."
3
+ description: "AI-DLC phase 3 (Review implementation): after explicit plan approval, implement the canonical work items, run the narrowest tests/lint, then run code-review. Triggers when the AI-DLC Orchestrator routes a task after plan approval to build. Ends at Review implementation for explicit user code review."
4
4
  ---
5
5
 
6
- # AI-DLC · Build (Gate G2) — adapter
6
+ # AI-DLC · Build (Review implementation) — adapter
7
7
 
8
8
  This skill is a thin Claude Code adapter. The canonical playbook is tool-agnostic and lives in the repo:
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: aidlc-clarify
3
- description: "AI-DLC phase 1 (Gate G0): turn a raw request into a confirmed intent brief — restate the problem, map affected submodules + specs, list assumptions and open questions, fix scope. Cheap: reads only the AI-DLC index, no deep code dive. Triggers: a NEW feature/bug/change request when the AI-DLC Orchestrator routes to clarify. Ends by STOPPING for user confirmation (G0)."
3
+ description: "AI-DLC phase 1 (Confirm requirements): turn a raw request into a confirmed intent brief — restate the problem, map affected submodules + specs, list assumptions and open questions, fix scope. Cheap: reads only the AI-DLC index, no deep code dive. Triggers: a NEW feature/bug/change request when the AI-DLC Orchestrator routes to clarify. Ends by STOPPING for user confirmation (Confirm requirements)."
4
4
  ---
5
5
 
6
- # AI-DLC · Clarify (Gate G0) — adapter
6
+ # AI-DLC · Clarify (Confirm requirements) — adapter
7
7
 
8
8
  This skill is a thin Claude Code adapter. The canonical playbook is tool-agnostic and lives in the repo:
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: aidlc-plan
3
- description: "AI-DLC phase 2 (Gate G1): after intent is confirmed, run bounded discovery, then produce a detailed design, canonical decisions, and a per-submodule workplan. Triggers when the AI-DLC Orchestrator routes a confirmed post-G0 task to planning. Ends at the executable G1 plan review for explicit user approval."
3
+ description: "AI-DLC phase 2 (Approve plan): after intent is confirmed, run bounded discovery, then produce a detailed design, canonical decisions, and a per-submodule workplan. Triggers when the AI-DLC Orchestrator routes a task after requirements confirmation to planning. Ends at the executable Approve plan review for explicit user approval."
4
4
  ---
5
5
 
6
- # AI-DLC · Plan (Gate G1) — adapter
6
+ # AI-DLC · Plan (Approve plan) — adapter
7
7
 
8
8
  This skill is a thin Claude Code adapter. The canonical playbook is tool-agnostic and lives in the repo:
9
9
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: aidlc-wrap
3
- description: "AI-DLC phase 4 (post-G2): after the user approves the code, follow project delivery policy, capture lessons and residual risk, then close canonical task state."
3
+ description: "AI-DLC phase 4 (after implementation approval): after the user approves the code, follow project delivery policy, capture lessons and residual risk, then close canonical task state."
4
4
  ---
5
5
 
6
6
  # AI-DLC · Wrap — adapter
@@ -0,0 +1,11 @@
1
+ # Copy to .env, set distinct random credentials, then run:
2
+ # docker compose --profile fleet up -d fleet-coordination
3
+ # Bind through a TLS reverse proxy before exposing this service to other machines.
4
+ MCP_POSTGRES_PASSWORD=replace-with-a-strong-postgres-password
5
+ # Existing local SQLite service in compose.yaml uses this only when started.
6
+ MCP_STATE_AUTH_TOKEN=replace-with-a-distinct-local-service-token
7
+ MCP_FLEET_LEAD_TOKEN=replace-with-a-distinct-lead-token-at-least-24-characters
8
+ MCP_FLEET_REVIEWER_TOKEN=replace-with-a-distinct-reviewer-token-at-least-24-characters
9
+ MCP_FLEET_WORKER_TOKENS={"worker1":"replace-with-a-distinct-worker-token-at-least-24-characters"}
10
+ MCP_FLEET_BIND_ADDRESS=127.0.0.1
11
+ MCP_FLEET_PORT=8788