@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
@@ -2,12 +2,16 @@
2
2
  "name": "@felixdotgo/aidlc-coordination",
3
3
  "private": true,
4
4
  "type": "module",
5
- "bin": { "aidlc-fleet": "src/fleet-run.mjs" },
6
- "engines": { "node": ">=20" },
5
+ "bin": {
6
+ "aidlc-fleet": "src/fleet-run.mjs"
7
+ },
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
7
11
  "scripts": {
8
12
  "start": "node src/server.mjs",
9
13
  "test": "node --test test/*.test.mjs",
10
- "lint": "node --check src/repository.mjs && node --check src/coordination.mjs && node --check src/fleet.mjs && node --check src/fleet-client.mjs && node --check src/fleet-run.mjs && node --check src/server.mjs"
14
+ "lint": "node -e \"const { readdirSync } = require('node:fs'); const { execFileSync } = require('node:child_process'); for (const file of readdirSync('src').filter((name) => name.endsWith('.mjs'))) execFileSync(process.execPath, ['--check', 'src/' + file], { stdio: 'inherit' });\""
11
15
  },
12
16
  "dependencies": {
13
17
  "@modelcontextprotocol/express": "^2.0.0",
@@ -17,5 +21,6 @@
17
21
  "express": "^5.2.1",
18
22
  "pg": "^8.23.0",
19
23
  "zod": "^4.4.3"
20
- }
24
+ },
25
+ "version": "0.3.0"
21
26
  }
@@ -12,6 +12,8 @@ const phaseGate = (phase) => phase === "clarify" ? "G0_confirm" : phase === "pla
12
12
  const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
13
13
  const text = (value) => typeof value === "string" && Boolean(value.trim());
14
14
  const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
15
+ const commitPattern = /^[0-9a-f]{7,64}$/;
16
+ const commitFields = ["baseCommit", "finalCommit"];
15
17
  const digest = (content) => createHash("sha256").update(content).digest("hex");
16
18
  const archiveRecordPath = (id) => `.agents/data/state/archive/${id}.json`;
17
19
  const taskRecordContent = (task) => `${JSON.stringify(task, null, 2)}\n`;
@@ -74,6 +76,7 @@ const validateTask = (id, task) => {
74
76
  if (task.closure && (!text(task.closure.reason) || !text(task.closure.source) || !iso(task.closure.recordedAt))) throw new Error(`Invalid task closure: ${id}`);
75
77
  if ((task.status === "superseded") !== Boolean(task.successorTaskId) || (task.status === "closed" && task.successorTaskId)) throw new Error(`Invalid task successor: ${id}`);
76
78
  if (task.predecessorTaskId !== undefined && !text(task.predecessorTaskId)) throw new Error(`Invalid task predecessor: ${id}`);
79
+ for (const field of commitFields) if (task[field] !== undefined && !(typeof task[field] === "string" && commitPattern.test(task[field]))) throw new Error(`Invalid task ${field}: ${id}`);
77
80
  for (const path of Object.values(task.artifacts ?? {})) if (path) {
78
81
  const cleaned = normalize(path);
79
82
  if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${separator}`) || !cleaned.startsWith(`.agents${separator}data${separator}tasks${separator}`)) throw new Error(`Invalid artifact path for ${id}: ${path}`);
@@ -278,6 +281,14 @@ export const transitionTask = (state, id, target) => {
278
281
  task.phase = target; task.gate = phaseGate(target); task.status = target === "done" ? "done" : "active"; delete task.legacyG2Wait; task.updatedAt = new Date().toISOString(); return task;
279
282
  };
280
283
 
284
+ /** Record a git SHA on a task. Wrappers resolve the SHA (lib/repo-head.mjs); the core never runs git. */
285
+ export const recordTaskCommit = (task, field, sha) => {
286
+ if (!task || typeof task !== "object") throw new Error("recordTaskCommit requires a task");
287
+ if (!commitFields.includes(field)) throw new Error(`Unknown task commit field: ${field}`);
288
+ if (typeof sha !== "string" || !commitPattern.test(sha)) throw new Error(`Invalid ${field}: expected a lowercase hex git SHA (7-64 chars)`);
289
+ task[field] = sha; task.updatedAt = new Date().toISOString(); return task;
290
+ };
291
+
281
292
  export const readyTaskItems = (task) => {
282
293
  const byId = new Map(task.tasks.map((item) => [item.id, item]));
283
294
  if (byId.size !== task.tasks.length) throw new Error(`Duplicate execution item: ${task.id}`);
@@ -307,6 +318,60 @@ export const readyTaskItems = (task) => {
307
318
  return { ready, blocked };
308
319
  };
309
320
 
321
+ /* Parallel work partition: owned paths overlap by path segment (`a` overlaps `a/b`, not `ab`).
322
+ * Two open items can run concurrently unless one is a direct or transitive dependency of the other. */
323
+ const ownedPathKey = (path) => String(path).replace(/\/+$/, "");
324
+ export const pathOverlaps = (left, right) => { const a = ownedPathKey(left), b = ownedPathKey(right); return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`); };
325
+ export const pathsOverlap = (left = [], right = []) => left.some((a) => right.some((b) => pathOverlaps(a, b)));
326
+ const itemAncestors = (task) => {
327
+ const byId = new Map(task.tasks.map((item) => [item.id, item])); const memo = new Map();
328
+ const walk = (id, trail = new Set()) => { if (memo.has(id)) return memo.get(id); const found = new Set(); if (trail.has(id)) return found; trail.add(id); for (const dep of byId.get(id)?.dependsOn ?? []) { if (!byId.has(dep)) continue; found.add(dep); for (const deeper of walk(dep, trail)) found.add(deeper); } trail.delete(id); memo.set(id, found); return found; };
329
+ return (id) => walk(id);
330
+ };
331
+ export const concurrentItemPairs = (task) => {
332
+ const open = task.tasks.filter((item) => !["done", "deferred"].includes(item.status)); const ancestors = itemAncestors(task); const pairs = [];
333
+ for (let i = 0; i < open.length; i += 1) for (let j = i + 1; j < open.length; j += 1) { const a = open[i], b = open[j]; if (!ancestors(a.id).has(b.id) && !ancestors(b.id).has(a.id)) pairs.push([a, b]); }
334
+ return pairs;
335
+ };
336
+ /** Approve-plan partition check. `strict` (enabled loop) makes findings ERRORs; otherwise WARNs. */
337
+ export const partitionDiagnostics = (task, { strict = true } = {}) => {
338
+ const level = strict ? "ERROR" : "WARN"; const diagnostics = [];
339
+ const unscoped = task.tasks.filter((item) => !["done", "deferred"].includes(item.status) && !(Array.isArray(item.ownedPaths) && item.ownedPaths.length));
340
+ if (unscoped.length) diagnostics.push({ level, code: "PARTITION_SCOPE", message: `Build items need owned paths (--owns) for parallel workers: ${unscoped.map((item) => item.id).join(", ")}` });
341
+ for (const [a, b] of concurrentItemPairs(task)) {
342
+ const shared = (a.ownedPaths ?? []).filter((path) => (b.ownedPaths ?? []).some((other) => pathOverlaps(path, other)));
343
+ if (shared.length) diagnostics.push({ level, code: "PARTITION_OVERLAP", message: `Items ${a.id} and ${b.id} can run concurrently but both own ${shared.join(", ")}; split the paths or add --depends-on` });
344
+ }
345
+ return diagnostics;
346
+ };
347
+ /** The partition check is strict when the loop is enabled: `loop.mode` local/fleet in the project config, or
348
+ * running inside a fleet-enabled coordination service. A missing config means the default `off`; an
349
+ * unreadable or corrupt config fails closed (strict). */
350
+ export const loopPartitionStrict = (root) => {
351
+ if (process.env.MCP_FLEET_ENABLED === "1") return true;
352
+ if (root === undefined) return false;
353
+ const path = join(resolve(root), ".agents", "config.json");
354
+ if (!existsSync(path)) return false;
355
+ try { const mode = JSON.parse(readFileSync(path, "utf8"))?.loop?.mode; return mode === "local" || mode === "fleet"; }
356
+ catch { return true; }
357
+ };
358
+ /** Pick the next dispatchable batch: dependency-ready items in plan order, at most `capacity`,
359
+ * skipping items that overlap each other, `busyPaths`, or are listed in `exclude`. Never throws for overlap. */
360
+ export const selectBuildBatch = (task, { capacity = Infinity, busyPaths = [], exclude = [] } = {}) => {
361
+ const byId = new Map(task.tasks.map((item) => [item.id, item])); const selected = []; const deferred = []; const taken = [];
362
+ for (const id of readyTaskItems(task).ready) {
363
+ const owned = byId.get(id)?.ownedPaths ?? [];
364
+ if (exclude.includes(id)) { deferred.push({ id, reason: "unresolved assignment" }); continue; }
365
+ if (!owned.length) { deferred.push({ id, reason: "no owned paths" }); continue; }
366
+ if (pathsOverlap(owned, busyPaths)) { deferred.push({ id, reason: "busy path held by an active assignment" }); continue; }
367
+ const clash = taken.find((entry) => pathsOverlap(owned, entry.owned));
368
+ if (clash) { deferred.push({ id, reason: `overlaps ${clash.id}` }); continue; }
369
+ if (selected.length >= capacity) { deferred.push({ id, reason: "capacity" }); continue; }
370
+ selected.push(id); taken.push({ id, owned });
371
+ }
372
+ return { selected, deferred };
373
+ };
374
+
310
375
  export const nextAction = (task, root) => {
311
376
  const rootSuffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
312
377
  const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
@@ -357,13 +422,14 @@ export const searchLessons = (root, state, query, areas = [], limit = 5) => { co
357
422
 
358
423
  export const listTaskSummaries = (state, options = {}) => { const active = Object.values(state.tasks).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "active" })); const archived = options.includeArchive ? Object.values(state.archive ?? {}).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "archive" })) : []; const queryTokens = tokens(options.query ?? ""); const statusSet = new Set(options.statuses ?? []); const filtered = [...active, ...archived].filter((item) => (!statusSet.size || statusSet.has(item.status)) && (!queryTokens.length || queryTokens.every((token) => tokens(`${item.id} ${item.title} ${item.areas.join(" ")}`).includes(token)))).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id)); const cursor = Math.max(0, options.cursor ?? 0); const limit = Math.max(1, Math.min(options.limit ?? 20, 100)); const items = filtered.slice(cursor, cursor + limit); return { items, nextCursor: cursor + items.length < filtered.length ? String(cursor + items.length) : null, total: filtered.length }; };
359
424
 
360
- export const renderWorkplan = (task) => { const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None"; const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}`).join("\n") : "- None"; const lifecycle = [`- Status: \`${task.status}\``, task.handoff ? `- Handoff: \`${task.handoff.kind}\` — ${task.handoff.reason}` : "", task.closure ? `- Closure: ${task.closure.reason}` : "", task.predecessorTaskId ? `- Predecessor: \`${task.predecessorTaskId}\`` : "", task.successorTaskId ? `- Successor: \`${task.successorTaskId}\`` : "", task.lessonDisposition ? `- Lessons: \`${task.lessonDisposition.status}\`` : ""].filter(Boolean).join("\n"); return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (Approve plan — before implementation)\n${decisions}\n\n## 🧩 Tasks (Review implementation — work items)\n${tasks}\n`; };
425
+ const workplanScope = (item) => { const parts = [item.ownedPaths?.length ? `owns: ${item.ownedPaths.join(", ")}` : "", item.dependsOn?.length ? `after: ${item.dependsOn.join(", ")}` : ""].filter(Boolean); return parts.length ? ` — ${parts.join(" · ")}` : ""; };
426
+ export const renderWorkplan = (task) => { const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None"; const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}${workplanScope(item)}`).join("\n") : "- None"; const lifecycle = [`- Status: \`${task.status}\``, task.handoff ? `- Handoff: \`${task.handoff.kind}\` — ${task.handoff.reason}` : "", task.closure ? `- Closure: ${task.closure.reason}` : "", task.predecessorTaskId ? `- Predecessor: \`${task.predecessorTaskId}\`` : "", task.successorTaskId ? `- Successor: \`${task.successorTaskId}\`` : "", task.lessonDisposition ? `- Lessons: \`${task.lessonDisposition.status}\`` : ""].filter(Boolean).join("\n"); return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (Approve plan — before implementation)\n${decisions}\n\n## 🧩 Tasks (Review implementation — work items)\n${tasks}\n`; };
361
427
  export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => { for (const task of selected) { if (!task.artifacts.workplan) continue; assertNoSymlinkPath(root, task.artifacts.workplan); writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task)); } };
362
428
 
363
429
  const safeArtifactTarget = (root, path) => { assertNoSymlinkPath(root, path); return join(resolve(root), path); };
364
430
  const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } /* intentional: unreadable/unsafe artifact is treated as absent */ };
365
431
  const artifactDiagnostics = (root, task, names) => names.flatMap((name) => { const path = task.artifacts[name]; if (!path) return [{ level: "ERROR", code: "ARTIFACT_REFERENCE", message: `${name} artifact is not referenced` }]; try { return existsSync(safeArtifactTarget(root, path)) ? [] : [{ level: "ERROR", code: "ARTIFACT_MISSING", message: `${path} does not exist` }]; } catch (error) { return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }]; } });
366
- export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.length) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
432
+ export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); diagnostics.push(...partitionDiagnostics(task, { strict: loopPartitionStrict(root) })); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.some((item) => item.level === "ERROR")) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
367
433
  const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
368
434
  export const approveAndAdvance = (root, state, taskId, gate, source, recordedAt = new Date().toISOString()) => { const task = state.tasks[taskId]; if (!task) throw new Error(`Unknown active task: ${taskId}`); if (["closed", "superseded"].includes(task.status) || task.handoff) throw new Error(`Terminal or handed-off task cannot approve ${gate}`); const target = gateTarget[gate]; if (!target) throw new Error(`Gate ${gate} cannot be approved`); if (task.phase === target) return { task, nextAction: nextAction(task, root), idempotent: true }; if (task.gate !== gate || task.status !== "blocked_on_user") throw new Error(`Task must be blocked_on_user at ${gate}`); const errors = checkGate(root, state, taskId, gate).filter((item) => item.level === "ERROR"); if (errors.length) throw new Error(errors.map((item) => item.message).join("; ")); task.evidence.push({ kind: "approval", gate, result: "pass", source, recordedAt }); transitionTask(state, taskId, target); return { task, nextAction: nextAction(task, root), idempotent: false }; };
369
435
  export const formatDiagnostics = (diagnostics) => diagnostics.map((item) => `${item.level} ${item.code}: ${item.message}`).join("\n");
@@ -119,6 +119,19 @@ const approveSnapshot = (current, snapshot, taskId, source, integrationCommit) =
119
119
  transitionTask(state, taskId, target);
120
120
  return mutationResult(current.state, state, current.index, [taskId]);
121
121
  };
122
+ const rejectSnapshot = (current, snapshot, taskId, feedback, snapshotId) => {
123
+ if (typeof feedback !== "string" || !feedback.trim() || feedback.length > 2000) throw new Error("bounded feedback text is required");
124
+ if (!snapshot || snapshot.task_id !== taskId || !current.state.tasks?.[taskId] || snapshot.task_digest !== taskDigest(current.state.tasks[taskId])) throw new Error("stale gate snapshot");
125
+ const task = current.state.tasks?.[taskId];
126
+ if (!task || task.gate !== snapshot.gate || task.status !== "blocked_on_user" || nextAction(task).classification !== "await_user") throw new Error("gate is no longer ready");
127
+ const state = structuredClone(current.state);
128
+ const recordedAt = new Date().toISOString();
129
+ state.tasks[taskId].evidence.push({ kind: "diagnostic", result: "pass", source: `Fleet reviewer: feedback (${snapshotId})`, detail: `Cancelled gate wait at ${snapshot.gate}: ${feedback}`, recordedAt });
130
+ delete state.tasks[taskId].legacyG2Wait;
131
+ state.tasks[taskId].status = "active";
132
+ state.tasks[taskId].updatedAt = recordedAt;
133
+ return mutationResult(current.state, state, current.index, [taskId]);
134
+ };
122
135
  const verifyFence = (fleet, taskId, epoch, ownerToken, time = Date.now()) => {
123
136
  const lead = fleet?.leads?.[taskId];
124
137
  if (!lead || !Number.isInteger(epoch) || lead.epoch !== epoch || Date.parse(lead.expiresAt) <= time || (lead.ownerTokenHash && (typeof ownerToken !== "string" || createHash("sha256").update(ownerToken).digest("hex") !== lead.ownerTokenHash))) throw new Error("stale or expired lead epoch");
@@ -306,6 +319,32 @@ export class SqliteStateRepository {
306
319
  } catch(error){try{this.db.exec("ROLLBACK");}catch{} throw error;}
307
320
  }
308
321
 
322
+ async rejectGate(workspace,expectedRevision,idempotencyKey,taskId,gateSnapshotId,feedback,clientProjectRoot,principal) {
323
+ if(typeof idempotencyKey!=="string"||!idempotencyKey.trim()||typeof clientProjectRoot!=="string"||!isAbsolute(clientProjectRoot)) throw new Error("invalid rejection precondition");
324
+ this.db.exec("BEGIN IMMEDIATE");
325
+ try {
326
+ this.#ensure(workspace);
327
+ sqliteRequest(this.db,workspace,idempotencyKey,principal,"gate_reject",{expectedRevision,taskId,gateSnapshotId,feedback,clientProjectRoot});
328
+ const cached=this.db.prepare("SELECT response FROM state_idempotency WHERE workspace=? AND idempotency_key=?").get(workspace,idempotencyKey);
329
+ if(cached){this.db.exec("COMMIT");return parse(cached.response);}
330
+ const current=snapshot(this.db.prepare("SELECT * FROM state_workspaces WHERE workspace=?").get(workspace));
331
+ if(current.revision!==expectedRevision) throw new RevisionConflict(current);
332
+ const gateSnapshot=this.db.prepare("SELECT * FROM state_gate_snapshots WHERE workspace=? AND id=?").get(workspace,gateSnapshotId);
333
+ const checkpointRow=this.db.prepare("SELECT * FROM state_gate_checkpoints WHERE workspace=? AND task_id=?").get(workspace,taskId);
334
+ const fleetRow=this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='fleet_workspaces'").get() ? this.db.prepare("SELECT fleet FROM fleet_workspaces WHERE workspace=?").get(workspace) : null;
335
+ const managed=fleetRow && parse(fleetRow.fleet)?.supervisors?.[taskId];
336
+ if (managed && (!checkpointRow || checkpointRow.status !== "pending" || checkpointRow.snapshot_id !== gateSnapshotId)) throw new Error("supervised rejection requires matching pending checkpoint");
337
+ if (checkpointRow?.status === "pending" && checkpointRow.snapshot_id !== gateSnapshotId) throw new Error("gate checkpoint does not match snapshot");
338
+ const next=rejectSnapshot(current,gateSnapshot,taskId,feedback,gateSnapshotId); const revision=current.revision+1; const updatedAt=new Date().toISOString(); next.index={...next.index,sourceRevision:revision,updatedAt};
339
+ const response={workspace,revision,state:next.state,index:next.index,nextActions:{[taskId]:nextAction(next.state.tasks[taskId],resolve(clientProjectRoot))},updatedAt};
340
+ this.db.prepare("UPDATE state_workspaces SET revision=?,state=?,task_index=?,updated_at=? WHERE workspace=?").run(revision,encode(next.state),encode(next.index),updatedAt,workspace);
341
+ this.db.prepare("UPDATE state_gate_checkpoints SET status='rejected' WHERE workspace=? AND task_id=? AND snapshot_id=? AND status='pending'").run(workspace,taskId,gateSnapshotId);
342
+ this.db.prepare("INSERT INTO state_events (workspace,revision,idempotency_key,command,recorded_at) VALUES (?,?,?,?,?)").run(workspace,revision,idempotencyKey,encode({type:"gate_reject",taskId,snapshotId:gateSnapshotId}),updatedAt);
343
+ this.db.prepare("INSERT INTO state_idempotency VALUES (?,?,?)").run(workspace,idempotencyKey,encode(response));
344
+ this.db.exec("COMMIT");return response;
345
+ } catch(error){try{this.db.exec("ROLLBACK");}catch{} throw error;}
346
+ }
347
+
309
348
  close() { this.db.close(); }
310
349
  }
311
350
 
@@ -360,6 +399,33 @@ export class PostgresStateRepository {
360
399
  async putGateSnapshot(workspace,expectedRevision,idempotencyKey,taskId,gate,reviewText,leadEpoch,integrationCommit,leadOwnerToken,checkpoint=false,principal) { if(typeof idempotencyKey!=="string"||!idempotencyKey.trim()) throw new Error("idempotencyKey is required"); const c=await this.pool.connect(); try { await this.#init(c); await c.query("BEGIN"); if(leadEpoch!==undefined){const fleetRow=(await c.query("SELECT fleet FROM fleet_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0];verifyFence(fleetRow?.fleet,taskId,leadEpoch,leadOwnerToken,Number((await c.query("SELECT extract(epoch from clock_timestamp()) * 1000 AS ms")).rows[0].ms));} await c.query("INSERT INTO state_workspaces VALUES ($1,0,$2,$3,$4) ON CONFLICT DO NOTHING",[workspace,emptyState(),emptyIndex(0),new Date().toISOString()]); const row=(await c.query("SELECT * FROM state_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0]; await postgresRequest(c,workspace,idempotencyKey,principal,"gate_snapshot_put",{expectedRevision,taskId,gate,reviewText,leadEpoch,integrationCommit,leadOwnerToken,checkpoint}); const cached=(await c.query("SELECT response FROM state_idempotency WHERE workspace=$1 AND idempotency_key=$2",[workspace,idempotencyKey])).rows[0]; if(cached){await c.query("COMMIT");return cached.response;} const current={workspace,revision:Number(row.revision),state:upgradeStoredState(row.state),index:row.task_index,updatedAt:row.updated_at}; if(current.revision!==expectedRevision) throw new RevisionConflict(current); if(checkpoint){const prior=(await c.query("SELECT status FROM state_gate_checkpoints WHERE workspace=$1 AND task_id=$2 FOR UPDATE",[workspace,taskId])).rows[0];if(prior?.status==="approved") throw new Error("approved checkpoint must be reconciled before replacement");} const boundReviewText=reviewWithCommit(gate,reviewText,integrationCommit); validateGateSnapshot(current,taskId,gate,boundReviewText,integrationCommit); const item={id:snapshotId(workspace,taskId,current.revision,gate,boundReviewText,integrationCommit),workspace,taskId,gate,stateRevision:current.revision,taskDigest:taskDigest(current.state.tasks[taskId]),integrationCommit:integrationCommit??null,reviewText:boundReviewText,createdAt:new Date().toISOString()}; await c.query("INSERT INTO state_gate_snapshots VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING",[item.id,workspace,taskId,gate,current.revision,item.taskDigest,item.integrationCommit,boundReviewText,item.createdAt]); item.createdAt=(await c.query("SELECT created_at FROM state_gate_snapshots WHERE id=$1",[item.id])).rows[0].created_at; if(checkpoint) await c.query("INSERT INTO state_gate_checkpoints (workspace,task_id,snapshot_id,gate,task_digest,state_revision,integration_commit,status,approval_revision,phase) VALUES ($1,$2,$3,$4,$5,$6,$7,'pending',NULL,$8) ON CONFLICT(workspace,task_id) DO UPDATE SET snapshot_id=excluded.snapshot_id,gate=excluded.gate,task_digest=excluded.task_digest,state_revision=excluded.state_revision,integration_commit=excluded.integration_commit,status='pending',approval_revision=NULL,phase=excluded.phase",[workspace,taskId,item.id,gate,item.taskDigest,current.revision,item.integrationCommit,current.state.tasks[taskId].phase]); await c.query("INSERT INTO state_idempotency VALUES ($1,$2,$3)",[workspace,idempotencyKey,item]); await c.query("COMMIT");return item; } catch(error){try{await c.query("ROLLBACK");}catch{} throw error;} finally{c.release();} }
361
400
  async gateCheckpoint(workspace,taskId) { const c=await this.pool.connect(); try { await this.#init(c); const row=(await c.query("SELECT snapshot_id AS \"snapshotId\",gate,phase,task_digest AS \"taskDigest\",state_revision AS \"stateRevision\",integration_commit AS \"integrationCommit\",status,approval_revision AS \"approvalRevision\" FROM state_gate_checkpoints WHERE workspace=$1 AND task_id=$2",[workspace,taskId])).rows[0]; return row ? {...row,stateRevision:Number(row.stateRevision),approvalRevision:row.approvalRevision===null?null:Number(row.approvalRevision)} : null; } finally { c.release(); } }
362
401
  async approveGate(workspace,expectedRevision,idempotencyKey,taskId,gateSnapshotId,source,clientProjectRoot,integrationCommit,principal) { if(typeof idempotencyKey!=="string"||!idempotencyKey.trim()||typeof clientProjectRoot!=="string"||!isAbsolute(clientProjectRoot)) throw new Error("invalid approval precondition"); const c=await this.pool.connect(); try { await this.#init(c); await c.query("BEGIN"); const fleetTable=(await c.query("SELECT to_regclass('fleet_workspaces') AS name")).rows[0].name; const fleetRow=fleetTable ? (await c.query("SELECT fleet FROM fleet_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0] : null; await c.query("INSERT INTO state_workspaces VALUES ($1,0,$2,$3,$4) ON CONFLICT DO NOTHING",[workspace,emptyState(),emptyIndex(0),new Date().toISOString()]); const row=(await c.query("SELECT * FROM state_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0]; await postgresRequest(c,workspace,idempotencyKey,principal,"gate_approve",{expectedRevision,taskId,gateSnapshotId,source,clientProjectRoot,integrationCommit}); const cached=(await c.query("SELECT response FROM state_idempotency WHERE workspace=$1 AND idempotency_key=$2",[workspace,idempotencyKey])).rows[0]; if(cached){await c.query("COMMIT");return cached.response;} const current={workspace,revision:Number(row.revision),state:upgradeStoredState(row.state),index:row.task_index,updatedAt:row.updated_at}; if(current.revision!==expectedRevision) throw new RevisionConflict(current); const gateSnapshot=(await c.query("SELECT * FROM state_gate_snapshots WHERE workspace=$1 AND id=$2",[workspace,gateSnapshotId])).rows[0]; const checkpointRow=(await c.query("SELECT * FROM state_gate_checkpoints WHERE workspace=$1 AND task_id=$2 FOR UPDATE",[workspace,taskId])).rows[0]; if(fleetRow?.fleet?.supervisors?.[taskId] && (!checkpointRow || checkpointRow.status!=="pending" || checkpointRow.snapshot_id!==gateSnapshotId)) throw new Error("supervised approval requires matching pending checkpoint"); if(checkpointRow?.status==="pending" && checkpointRow.snapshot_id!==gateSnapshotId) throw new Error("gate checkpoint does not match snapshot"); const next=approveSnapshot(current,gateSnapshot,taskId,source,integrationCommit); const revision=current.revision+1; const updatedAt=new Date().toISOString(); next.index={...next.index,sourceRevision:revision,updatedAt}; const response={workspace,revision,state:next.state,index:next.index,nextActions:{[taskId]:nextAction(next.state.tasks[taskId],resolve(clientProjectRoot))},updatedAt}; await c.query("UPDATE state_workspaces SET revision=$1,state=$2,task_index=$3,updated_at=$4 WHERE workspace=$5",[revision,next.state,next.index,updatedAt,workspace]); await c.query("UPDATE state_gate_checkpoints SET status='approved',approval_revision=$1 WHERE workspace=$2 AND task_id=$3 AND snapshot_id=$4 AND status='pending'",[revision,workspace,taskId,gateSnapshotId]); await c.query("INSERT INTO state_events (workspace,revision,idempotency_key,command,recorded_at) VALUES ($1,$2,$3,$4,$5)",[workspace,revision,idempotencyKey,{type:"gate_approve",taskId,snapshotId:gateSnapshotId,integrationCommit:integrationCommit??null},updatedAt]); await c.query("INSERT INTO state_idempotency VALUES ($1,$2,$3)",[workspace,idempotencyKey,response]); await c.query("COMMIT");return response; } catch(error){try{await c.query("ROLLBACK");}catch{} throw error;} finally{c.release();} }
402
+ async rejectGate(workspace,expectedRevision,idempotencyKey,taskId,gateSnapshotId,feedback,clientProjectRoot,principal) {
403
+ if(typeof idempotencyKey!=="string"||!idempotencyKey.trim()||typeof clientProjectRoot!=="string"||!isAbsolute(clientProjectRoot)) throw new Error("invalid rejection precondition");
404
+ const c=await this.pool.connect();
405
+ try {
406
+ await this.#init(c); await c.query("BEGIN");
407
+ const fleetTable=(await c.query("SELECT to_regclass('fleet_workspaces') AS name")).rows[0].name;
408
+ const fleetRow=fleetTable ? (await c.query("SELECT fleet FROM fleet_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0] : null;
409
+ await c.query("INSERT INTO state_workspaces VALUES ($1,0,$2,$3,$4) ON CONFLICT DO NOTHING",[workspace,emptyState(),emptyIndex(0),new Date().toISOString()]);
410
+ const row=(await c.query("SELECT * FROM state_workspaces WHERE workspace=$1 FOR UPDATE",[workspace])).rows[0];
411
+ await postgresRequest(c,workspace,idempotencyKey,principal,"gate_reject",{expectedRevision,taskId,gateSnapshotId,feedback,clientProjectRoot});
412
+ const cached=(await c.query("SELECT response FROM state_idempotency WHERE workspace=$1 AND idempotency_key=$2",[workspace,idempotencyKey])).rows[0];
413
+ if(cached){await c.query("COMMIT");return cached.response;}
414
+ const current={workspace,revision:Number(row.revision),state:upgradeStoredState(row.state),index:row.task_index,updatedAt:row.updated_at};
415
+ if(current.revision!==expectedRevision) throw new RevisionConflict(current);
416
+ const gateSnapshot=(await c.query("SELECT * FROM state_gate_snapshots WHERE workspace=$1 AND id=$2",[workspace,gateSnapshotId])).rows[0];
417
+ const checkpointRow=(await c.query("SELECT * FROM state_gate_checkpoints WHERE workspace=$1 AND task_id=$2 FOR UPDATE",[workspace,taskId])).rows[0];
418
+ if(fleetRow?.fleet?.supervisors?.[taskId] && (!checkpointRow || checkpointRow.status!=="pending" || checkpointRow.snapshot_id!==gateSnapshotId)) throw new Error("supervised rejection requires matching pending checkpoint");
419
+ if(checkpointRow?.status==="pending" && checkpointRow.snapshot_id!==gateSnapshotId) throw new Error("gate checkpoint does not match snapshot");
420
+ const next=rejectSnapshot(current,gateSnapshot,taskId,feedback,gateSnapshotId); const revision=current.revision+1; const updatedAt=new Date().toISOString(); next.index={...next.index,sourceRevision:revision,updatedAt};
421
+ const response={workspace,revision,state:next.state,index:next.index,nextActions:{[taskId]:nextAction(next.state.tasks[taskId],resolve(clientProjectRoot))},updatedAt};
422
+ await c.query("UPDATE state_workspaces SET revision=$1,state=$2,task_index=$3,updated_at=$4 WHERE workspace=$5",[revision,next.state,next.index,updatedAt,workspace]);
423
+ await c.query("UPDATE state_gate_checkpoints SET status='rejected' WHERE workspace=$1 AND task_id=$2 AND snapshot_id=$3 AND status='pending'",[workspace,taskId,gateSnapshotId]);
424
+ await c.query("INSERT INTO state_events (workspace,revision,idempotency_key,command,recorded_at) VALUES ($1,$2,$3,$4,$5)",[workspace,revision,idempotencyKey,{type:"gate_reject",taskId,snapshotId:gateSnapshotId},updatedAt]);
425
+ await c.query("INSERT INTO state_idempotency (workspace,idempotency_key,response) VALUES ($1,$2,$3)",[workspace,idempotencyKey,response]);
426
+ await c.query("COMMIT"); return response;
427
+ } catch(error) { try { await c.query("ROLLBACK"); } catch {} throw error; } finally { c.release(); }
428
+ }
363
429
  async close() { await this.pool.end(); }
364
430
  }
365
431
 
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { timingSafeEqual } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
3
4
  import { McpServer } from "@modelcontextprotocol/server";
4
5
  import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
5
6
  import { createMcpExpressApp } from "@modelcontextprotocol/express";
@@ -10,12 +11,14 @@ import { CoordinationConflict, SqliteCoordinationRepository, createCoordinationR
10
11
  import { FleetConflict, SqliteFleetRepository, createFleetRepository, featureReady, validateCanonicalFeature } from "./fleet.mjs";
11
12
  import { nextAction } from "./lifecycle-core.mjs";
12
13
 
14
+ let serviceVersion = "0.0.0-dev";
15
+ try { const parsed = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); if (parsed?.version) serviceVersion = parsed.version; } catch {}
13
16
  const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
14
17
  const failure = (error) => ({ isError: true, content: [{ type: "text", text: JSON.stringify(error instanceof RevisionConflict ? { code: "REVISION_CONFLICT", current: error.current } : error instanceof CoordinationConflict ? { code: "COORDINATION_CONFLICT", current: error.current } : error instanceof FleetConflict ? { code: "FLEET_CONFLICT", current: error.current, message: error.message } : { code: "STATE_ERROR", message: error instanceof Error ? error.message : String(error) }) }] });
15
18
  const workspace = z.string().min(1).max(128);
16
19
 
17
20
  export const buildServer = (repository, coordination = new SqliteCoordinationRepository(), fleet = new SqliteFleetRepository(), identity = null) => {
18
- const server = new McpServer({ name: "aidlc-coordination", version: "0.1.0" });
21
+ const server = new McpServer({ name: "aidlc-coordination", version: serviceVersion });
19
22
  const requireRole = (...allowed) => { if (identity && !allowed.includes(identity.role)) throw new Error("credential role is not authorized for this operation"); };
20
23
  const actorFor = (actor) => identity ? { id: identity.id, role: { lead: "admin", worker: "commenter", reviewer: "reader" }[identity.role] } : actor;
21
24
  server.registerTool("state_get", { description: "Pull the current workflow state and derived index for a workspace.", inputSchema: z.object({ workspace }) }, async ({ workspace: id }) => { try { return result(await repository.get(id)); } catch (error) { return failure(error); } });
@@ -55,6 +58,7 @@ export const buildServer = (repository, coordination = new SqliteCoordinationRep
55
58
  server.registerTool("gate_snapshot_put", { description: "Publish a review snapshot at the current canonical task revision, binding G2 to an integration commit.", inputSchema: z.object({...precondition,taskId:z.string(),leadEpoch:z.number().int(),leadOwnerToken:z.string().min(32).max(256).optional(),checkpoint:z.boolean().optional(),gate:z.enum(["G0_confirm","G1_review","G2_codereview"]),reviewText:z.string().min(1).max(20000),integrationCommit:z.string().regex(/^[a-f0-9]{40}$/).optional()}) }, async ({workspace:id,expectedRevision,idempotencyKey,taskId,leadEpoch,leadOwnerToken,checkpoint,gate,reviewText,integrationCommit}) => { try { requireRole("lead"); return result(await repository.putGateSnapshot(id,expectedRevision,idempotencyKey,taskId,gate,reviewText,leadEpoch,integrationCommit,leadOwnerToken,checkpoint,identity.id)); } catch(error){return failure(error);} });
56
59
  server.registerTool("gate_checkpoint_get", { description: "Read durable supervisor gate checkpoint.", inputSchema: z.object({workspace,taskId:z.string()}) }, async ({workspace:id,taskId}) => { try { requireRole("lead","reviewer"); return result(await repository.gateCheckpoint(id,taskId)); } catch(error){return failure(error);} });
57
60
  server.registerTool("gate_approve", { description: "Reviewer-only atomic approval against an unchanged task revision and reviewed integration commit.", inputSchema: z.object({...precondition,taskId:z.string(),snapshotId:z.string(),source:z.string().min(1).max(2000),clientProjectRoot:z.string(),integrationCommit:z.string().regex(/^[a-f0-9]{40}$/).optional()}) }, async ({workspace:id,expectedRevision,idempotencyKey,taskId,snapshotId,source,clientProjectRoot,integrationCommit}) => { try { requireRole("reviewer"); return result(await repository.approveGate(id,expectedRevision,idempotencyKey,taskId,snapshotId,source,clientProjectRoot,integrationCommit,identity.id)); } catch(error){return failure(error);} });
61
+ server.registerTool("gate_reject", { description: "Reviewer-only feedback against an unchanged task revision; returns the task to active so the loop can revise and re-present the gate.", inputSchema: z.object({...precondition,taskId:z.string(),snapshotId:z.string(),feedback:z.string().min(1).max(2000),clientProjectRoot:z.string()}) }, async ({workspace:id,expectedRevision,idempotencyKey,taskId,snapshotId,feedback,clientProjectRoot}) => { try { requireRole("reviewer"); return result(await repository.rejectGate(id,expectedRevision,idempotencyKey,taskId,snapshotId,feedback,clientProjectRoot,identity.id)); } catch(error){return failure(error);} });
58
62
  }
59
63
  return server;
60
64
  };
@@ -77,9 +81,11 @@ if (fleetEnabled && (fleetCredentials.length < 3 || fleetCredentials.some((entry
77
81
  const equalToken = (left,right) => { const a=Buffer.from(left); const b=Buffer.from(right); return a.length===b.length && timingSafeEqual(a,b); };
78
82
  const principalFor = (authorization) => fleetCredentials.find((entry) => equalToken(authorization,`Bearer ${entry.token}`));
79
83
  const app = createMcpExpressApp({ host });
84
+ app.use((req, res, next) => { const started=Date.now(); res.on("finish", () => { const entry={ts:new Date().toISOString(),method:req.method,path:req.path,status:res.statusCode}; const principal=req.fleetPrincipal?.id; if (principal) entry.principal=principal; entry.ms=Date.now()-started; process.stderr.write(JSON.stringify(entry)+"\n"); }); next(); });
85
+ app.get("/healthz", (req, res) => res.status(200).json({ ok: true }));
80
86
  app.use((req, res, next) => {
81
87
  if (fleetEnabled) { const principal=principalFor(req.headers.authorization ?? ""); if (!principal) return res.status(401).json({ error: "unauthorized" }); req.fleetPrincipal={id:principal.id,role:principal.role}; return next(); }
82
- if (!token || req.headers.authorization === `Bearer ${token}`) return next();
88
+ if (!token || equalToken(req.headers.authorization ?? "", `Bearer ${token}`)) return next();
83
89
  res.status(401).json({ error: "unauthorized" });
84
90
  });
85
91
  app.post("/mcp", async (req, res) => {
@@ -88,6 +94,6 @@ app.post("/mcp", async (req, res) => {
88
94
  await server.connect(transport);
89
95
  await transport.handleRequest(req, res, req.body);
90
96
  });
91
- const listener = app.listen(port, host, () => console.error(`aidlc MCP state service listening at http://${host}:${port}/mcp`));
97
+ const listener = app.listen(port, host, () => { const actual = (typeof listener.address === "function" && listener.address()) ? listener.address().port : port; console.error(`aidlc MCP state service listening at http://${host}:${actual}/mcp`); });
92
98
  const shutdown = async () => { listener.close(); await repository.close(); await coordination.close(); await fleet?.close(); };
93
99
  process.once("SIGINT", shutdown); process.once("SIGTERM", shutdown);
@@ -91,6 +91,27 @@ test("review snapshots require a ready wait and approval is atomic against its r
91
91
  } finally { repo.close(); }
92
92
  });
93
93
 
94
+ test("reviewer rejection records feedback, returns the task to active, and marks the checkpoint rejected", async () => {
95
+ const repo = new SqliteStateRepository(":memory:");
96
+ try {
97
+ const first = await repo.apply("w", 0, "create", { type: "upsertTask", taskId: "T1", task }, "/workspace/project");
98
+ assert.equal(first.nextActions.T1.classification, "await_user");
99
+ const snapshot = await repo.putGateSnapshot("w", 1, "snapshot", "T1", "G0_confirm", "Review this intent", undefined, undefined, undefined, true);
100
+ assert.equal((await repo.gateCheckpoint("w", "T1")).status, "pending");
101
+ await assert.rejects(() => repo.rejectGate("w", 0, "stale", "T1", snapshot.id, "rewrite", "/workspace/project"), /revision conflict/);
102
+ const rejected = await repo.rejectGate("w", 1, "reject", "T1", snapshot.id, "Please rewrite the problem statement", "/workspace/project");
103
+ const after = rejected.state.tasks.T1;
104
+ assert.equal(after.status, "active");
105
+ assert.equal(after.phase, "clarify");
106
+ assert.equal(after.gate, "G0_confirm");
107
+ assert.equal(after.evidence.at(-1).kind, "diagnostic");
108
+ assert.match(after.evidence.at(-1).detail, /^Cancelled gate wait at G0_confirm: Please rewrite the problem statement$/);
109
+ assert.equal(rejected.nextActions.T1.classification, "run_phase");
110
+ assert.equal((await repo.gateCheckpoint("w", "T1")).status, "rejected");
111
+ await assert.rejects(() => repo.rejectGate("w", 2, "again", "T1", snapshot.id, "more feedback", "/workspace/project"), /stale gate snapshot/);
112
+ } finally { repo.close(); }
113
+ });
114
+
94
115
  test("G2 snapshot binds the reviewed integration commit through approval", async () => {
95
116
  const repo=new SqliteStateRepository(":memory:");
96
117
  try {
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { randomBytes } from "node:crypto";
9
+
10
+ const serviceRoot = join(fileURLToPath(new URL("..", import.meta.url)));
11
+
12
+ test("health endpoint is public and non-fleet auth is timing-safe", async () => {
13
+ const dir = mkdtempSync(join(tmpdir(), "aidlc-hz-"));
14
+ const db = join(dir, "state.db");
15
+ const token = randomBytes(16).toString("hex");
16
+ const env = {
17
+ ...process.env,
18
+ MCP_STATE_DATABASE: "sqlite",
19
+ MCP_STATE_SQLITE_PATH: db,
20
+ MCP_STATE_HOST: "127.0.0.1",
21
+ MCP_STATE_PORT: "0",
22
+ MCP_STATE_AUTH_TOKEN: token,
23
+ };
24
+ const child = spawn(process.execPath, [join(serviceRoot, "src", "server.mjs")], {
25
+ env,
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ });
28
+ let stderr = "";
29
+ let port;
30
+ try {
31
+ port = await new Promise((resolve, reject) => {
32
+ const timeout = setTimeout(() => reject(new Error("timeout waiting for listening: " + stderr)), 10000);
33
+ child.stderr.on("data", (chunk) => {
34
+ stderr += chunk.toString();
35
+ const match = stderr.match(/listening at http:\/\/[^:]+:(\d+)\/mcp/);
36
+ if (match) {
37
+ clearTimeout(timeout);
38
+ resolve(Number(match[1]));
39
+ }
40
+ });
41
+ child.on("exit", (code) => {
42
+ clearTimeout(timeout);
43
+ reject(new Error("server exited " + code + ": " + stderr));
44
+ });
45
+ });
46
+
47
+ const health = await fetch(`http://127.0.0.1:${port}/healthz`);
48
+ assert.equal(health.status, 200);
49
+ assert.deepEqual(await health.json(), { ok: true });
50
+
51
+ const noAuth = await fetch(`http://127.0.0.1:${port}/mcp`, { method: "POST" });
52
+ assert.equal(noAuth.status, 401);
53
+
54
+ const wrong = await fetch(`http://127.0.0.1:${port}/mcp`, { method: "POST", headers: { Authorization: "Bearer wrong-token" } });
55
+ assert.equal(wrong.status, 401);
56
+
57
+ const correct = await fetch(`http://127.0.0.1:${port}/mcp`, { method: "POST", headers: { Authorization: `Bearer ${token}` } });
58
+ assert.notEqual(correct.status, 401);
59
+
60
+ assert.match(stderr, /listening at http:/);
61
+ assert.ok(!stderr.includes(token), "stderr must not contain the token");
62
+ } finally {
63
+ child.kill();
64
+ await new Promise((resolve) => child.on("exit", resolve));
65
+ rmSync(dir, { recursive: true, force: true });
66
+ }
67
+ });
@@ -1,7 +1,4 @@
1
1
  import type { Adapter, AgentId } from "./model.js";
2
2
  export declare const adapters: readonly Adapter[];
3
3
  export declare const resolvedAdapters: () => readonly Adapter[];
4
- export declare const resolvedAdaptersForUpgrade: () => readonly Adapter[];
5
- export declare const installableAdapters: () => readonly Adapter[];
6
4
  export declare const findAdapter: (id: AgentId) => Adapter | undefined;
7
- export declare const findAdapterForUpgrade: (id: AgentId) => Adapter | undefined;
@@ -5,13 +5,14 @@
5
5
  */
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+ import { legacyManagedMarker, managedEndMarker, managedStartMarker } from "./managed-content.js";
8
9
  const hasManagedBlock = (root, path, owner) => {
9
10
  const target = join(root, path);
10
11
  if (!existsSync(target))
11
12
  return false;
12
13
  try {
13
14
  const content = readFileSync(target, "utf8");
14
- return content.includes(`<!-- aidlc-installer:${owner}:start -->`) || content.includes(`<!-- aidlc-installer:${owner} -->`);
15
+ return content.includes(managedStartMarker(owner)) || content.includes(legacyManagedMarker(owner));
15
16
  }
16
17
  catch {
17
18
  return false;
@@ -21,7 +22,7 @@ const hasManagedBlock = (root, path, owner) => {
21
22
  const adapterFile = (owner, path, body) => ({
22
23
  path,
23
24
  owner,
24
- content: `<!-- aidlc-installer:${owner}:start -->\n${body}<!-- aidlc-installer:${owner}:end -->\n`
25
+ content: `${managedStartMarker(owner)}\n${body}${managedEndMarker(owner)}\n`
25
26
  });
26
27
  /** Shared AI-DLC instruction text installed into each tool's primary prompt file. */
27
28
  const instruction = (agent, adapter) => [
@@ -107,7 +108,4 @@ export const adapters = [
107
108
  }
108
109
  ];
109
110
  export const resolvedAdapters = () => adapters;
110
- export const resolvedAdaptersForUpgrade = () => adapters;
111
- export const installableAdapters = () => adapters;
112
111
  export const findAdapter = (id) => adapters.find((adapter) => adapter.id === id);
113
- export const findAdapterForUpgrade = (id) => adapters.find((adapter) => adapter.id === id);
@@ -0,0 +1,12 @@
1
+ export declare const usage: string;
2
+ export declare const value: (args: string[], name: string) => string | undefined;
3
+ export declare const flags: (args: string[]) => Set<string>;
4
+ export declare const rootArg: (args: string[], fallback?: string) => string;
5
+ export declare const INIT_FLAGS: Set<string>;
6
+ export declare const UPGRADE_FLAGS: Set<string>;
7
+ export declare const UNINSTALL_FLAGS: Set<string>;
8
+ export declare const STATUS_FLAGS: Set<string>;
9
+ export declare const DOCTOR_FLAGS: Set<string>;
10
+ export declare const MCP_FLAGS: Set<string>;
11
+ export declare const UI_FLAGS: Set<string>;
12
+ export declare const assertKnownOptions: (args: string[], allowed: Set<string>) => void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure argv layer: usage text, value/flag parsing, and per-command option
3
+ * allowlists. No prompting, no side effects.
4
+ */
5
+ import { LOOP_PROVIDERS } from "../model.js";
6
+ export const usage = `Usage:
7
+ npx @felixdotgo/aidlc-kit init [path] [--agent <name[,name]> | --all] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--yes] [--dry-run] [--force]
8
+ npx @felixdotgo/aidlc-kit upgrade [path] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--dry-run]
9
+ npx @felixdotgo/aidlc-kit uninstall [path] [--yes] [--dry-run]
10
+ npx @felixdotgo/aidlc-kit status [path] [--json]
11
+ npx @felixdotgo/aidlc-kit doctor [path] [--strict] [--json]
12
+ npx @felixdotgo/aidlc-kit profile validate [path]
13
+ npx @felixdotgo/aidlc-kit loop up [--root PATH] [--loop-env PATH] [--port 4173] [--host 127.0.0.1] [--open] [--no-ui]
14
+ npx @felixdotgo/aidlc-kit loop down|stop [--root PATH] [--now]
15
+ npx @felixdotgo/aidlc-kit loop env [--root PATH] [--loop-env PATH] [--json]
16
+ npx @felixdotgo/aidlc-kit loop status [--root PATH] [--json] (daemon, dashboard, environment)
17
+ npx @felixdotgo/aidlc-kit loop start|status|intake [--task-id ID] [--title TITLE] [--root PATH] [--mode local|fleet] [--loop-env PATH]
18
+ npx @felixdotgo/aidlc-kit loop worker-image [--provider ${LOOP_PROVIDERS.join("|")}] [--tag TAG] [--root PATH]
19
+ npx @felixdotgo/aidlc-kit loop leases [--task-id ID] [--root PATH] [--json] [--clear-stale]
20
+ npx @felixdotgo/aidlc-kit loop migrate-data [--root PATH] [--json] [--dry-run] (move pre-runtime per-task loop files under .agents/data/tasks/<id>/runtime/)
21
+ npx @felixdotgo/aidlc-kit ui [path] [--port 4173] [--host 127.0.0.1] [--open] [--reviewer-token-file PATH]
22
+ npx @felixdotgo/aidlc-kit mcp setup [path] [--dry-run] [--yes] [--storage sqlite|postgres] [--deployment docker|remote] [--bind localhost|network] [--workspace <id>] [--poll-ms <n>] [--token-env <name>] [--providers <p,...>] [--enable]
23
+ npx @felixdotgo/aidlc-kit version
24
+ Global options: --help, --version, -v`;
25
+ export const value = (args, name) => {
26
+ const index = args.indexOf(name);
27
+ if (index < 0)
28
+ return undefined;
29
+ const next = args[index + 1];
30
+ if (next === undefined || next.startsWith("--"))
31
+ throw new Error(`${name} requires a value`);
32
+ return next;
33
+ };
34
+ export const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
35
+ const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--loop-mode", "--loop-lead", "--loop-workers", "--port", "--host", "--reviewer-token-file"]);
36
+ const COMMAND_KEYWORDS = ["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"];
37
+ const positional = (args) => {
38
+ const result = [];
39
+ for (let index = 0; index < args.length; index += 1) {
40
+ const arg = args[index];
41
+ if (arg !== undefined && valueFlags.has(arg)) {
42
+ index += 1;
43
+ continue;
44
+ }
45
+ if (arg !== undefined && !arg.startsWith("--"))
46
+ result.push(arg);
47
+ }
48
+ return result;
49
+ };
50
+ export const rootArg = (args, fallback = ".") => positional(args).find((item) => !COMMAND_KEYWORDS.includes(item)) ?? fallback;
51
+ export const INIT_FLAGS = new Set(["--agent", "--all", "--loop-mode", "--loop-lead", "--loop-workers", "--yes", "--dry-run", "--force"]);
52
+ export const UPGRADE_FLAGS = new Set(["--loop-mode", "--loop-lead", "--loop-workers", "--dry-run"]);
53
+ export const UNINSTALL_FLAGS = new Set(["--yes", "--dry-run"]);
54
+ export const STATUS_FLAGS = new Set(["--json"]);
55
+ export const DOCTOR_FLAGS = new Set(["--strict", "--json"]);
56
+ export const MCP_FLAGS = new Set(["--dry-run", "--yes", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--enable"]);
57
+ export const UI_FLAGS = new Set(["--port", "--host", "--open", "--reviewer-token-file"]);
58
+ export const assertKnownOptions = (args, allowed) => {
59
+ for (let index = 0; index < args.length; index += 1) {
60
+ const arg = args[index];
61
+ if (arg === undefined || !arg.startsWith("--"))
62
+ continue;
63
+ if (!allowed.has(arg))
64
+ throw new Error(`Unknown option: ${arg}`);
65
+ if (valueFlags.has(arg))
66
+ index += 1;
67
+ }
68
+ };
@@ -0,0 +1,8 @@
1
+ export declare const runLoopCommand: (args: string[]) => Promise<void>;
2
+ export declare const runUi: (args: string[]) => Promise<void>;
3
+ export declare const runMcpSetup: (args: string[]) => Promise<void>;
4
+ export declare const runMcp: (args: string[]) => Promise<void>;
5
+ export declare const runStatus: (args: string[]) => void;
6
+ export declare const runDoctor: (args: string[]) => void;
7
+ export declare const runProfileValidate: (args: string[]) => void;
8
+ export declare const runProfile: (args: string[]) => void;