@felixdotgo/aidlc-kit 0.1.0 → 0.2.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 (150) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +14 -6
  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 +1 -1
  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/manifest.schema.json +1 -1
  11. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +245 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
  30. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
  31. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  32. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  33. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  34. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
  35. package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
  36. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  37. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  38. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  39. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  40. package/dist/assets/ui/bulma.min.css +3 -0
  41. package/dist/assets/ui/dashboard.css +414 -0
  42. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  43. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  44. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  45. package/dist/services/aidlc-coordination/package.json +9 -4
  46. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
  47. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  48. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  49. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  50. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  51. package/dist/src/adapters.d.ts +0 -3
  52. package/dist/src/adapters.js +11 -6
  53. package/dist/src/cli/argv.d.ts +12 -0
  54. package/dist/src/cli/argv.js +62 -0
  55. package/dist/src/cli/commands.d.ts +8 -0
  56. package/dist/src/cli/commands.js +129 -0
  57. package/dist/src/cli/options.d.ts +25 -0
  58. package/dist/src/cli/options.js +32 -0
  59. package/dist/src/cli/prompt.d.ts +9 -0
  60. package/dist/src/cli/prompt.js +66 -0
  61. package/dist/src/cli.js +67 -217
  62. package/dist/src/context.js +2 -0
  63. package/dist/src/gate.d.ts +1 -0
  64. package/dist/src/gate.js +51 -37
  65. package/dist/src/installer.d.ts +19 -7
  66. package/dist/src/installer.js +111 -178
  67. package/dist/src/legacy.js +1 -0
  68. package/dist/src/managed-content.d.ts +7 -0
  69. package/dist/src/managed-content.js +15 -4
  70. package/dist/src/managed-planning.d.ts +65 -0
  71. package/dist/src/managed-planning.js +219 -0
  72. package/dist/src/mcp-setup.js +3 -1
  73. package/dist/src/model/config-contracts.d.ts +65 -0
  74. package/dist/src/model/config-contracts.js +1 -0
  75. package/dist/src/model/install-contracts.d.ts +29 -0
  76. package/dist/src/model/install-contracts.js +1 -0
  77. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  78. package/dist/src/model/lifecycle-enums.js +1 -0
  79. package/dist/src/model/loop.d.ts +20 -0
  80. package/dist/src/model/loop.js +1 -0
  81. package/dist/src/model/manifest.d.ts +16 -0
  82. package/dist/src/model/manifest.js +1 -0
  83. package/dist/src/model/records.d.ts +138 -0
  84. package/dist/src/model/records.js +1 -0
  85. package/dist/src/model.d.ts +11 -264
  86. package/dist/src/model.js +1 -1
  87. package/dist/src/profiles.d.ts +8 -0
  88. package/dist/src/profiles.js +63 -8
  89. package/dist/src/project-migration.d.ts +1 -0
  90. package/dist/src/project-migration.js +7 -0
  91. package/dist/src/state/archive-record.d.ts +9 -0
  92. package/dist/src/state/archive-record.js +26 -0
  93. package/dist/src/state/commands.d.ts +18 -0
  94. package/dist/src/state/commands.js +27 -0
  95. package/dist/src/state/domain.d.ts +9 -0
  96. package/dist/src/state/domain.js +9 -0
  97. package/dist/src/state/guards.d.ts +7 -0
  98. package/dist/src/state/guards.js +17 -0
  99. package/dist/src/state/io.d.ts +10 -0
  100. package/dist/src/state/io.js +35 -0
  101. package/dist/src/state/items.d.ts +24 -0
  102. package/dist/src/state/items.js +147 -0
  103. package/dist/src/state/lessons.d.ts +7 -0
  104. package/dist/src/state/lessons.js +74 -0
  105. package/dist/src/state/lifecycle.d.ts +5 -0
  106. package/dist/src/state/lifecycle.js +78 -0
  107. package/dist/src/state/lock.d.ts +7 -0
  108. package/dist/src/state/lock.js +125 -0
  109. package/dist/src/state/memory.d.ts +8 -0
  110. package/dist/src/state/memory.js +100 -0
  111. package/dist/src/state/migrate.d.ts +17 -0
  112. package/dist/src/state/migrate.js +60 -0
  113. package/dist/src/state/partition.d.ts +15 -0
  114. package/dist/src/state/partition.js +90 -0
  115. package/dist/src/state/paths.d.ts +11 -0
  116. package/dist/src/state/paths.js +12 -0
  117. package/dist/src/state/render.d.ts +26 -0
  118. package/dist/src/state/render.js +32 -0
  119. package/dist/src/state/store.d.ts +4 -0
  120. package/dist/src/state/store.js +75 -0
  121. package/dist/src/state/transitions.d.ts +21 -0
  122. package/dist/src/state/transitions.js +95 -0
  123. package/dist/src/state/validate.d.ts +4 -0
  124. package/dist/src/state/validate.js +143 -0
  125. package/dist/src/state.d.ts +20 -117
  126. package/dist/src/state.js +20 -815
  127. package/dist/src/task-ui.d.ts +54 -0
  128. package/dist/src/task-ui.js +1570 -0
  129. package/dist/src/upgrade-board.d.ts +5 -0
  130. package/dist/src/upgrade-board.js +89 -0
  131. package/dist/src/upgrade.d.ts +3 -5
  132. package/dist/src/upgrade.js +174 -255
  133. package/dist/src/workflow.js +4 -4
  134. package/docs/README.md +6 -2
  135. package/docs/architecture.md +12 -8
  136. package/docs/command-reference.md +83 -19
  137. package/docs/configuration.md +30 -7
  138. package/docs/coordination-service.md +20 -5
  139. package/docs/design-decisions.md +83 -0
  140. package/docs/development.md +41 -2
  141. package/docs/loop-local-run.md +495 -0
  142. package/docs/loop-runbook.md +235 -0
  143. package/docs/operating-workflow.md +5 -2
  144. package/docs/testing-and-release.md +11 -7
  145. package/docs/troubleshooting.md +125 -0
  146. package/package.json +5 -2
  147. package/dist/src/ledger.d.ts +0 -22
  148. package/dist/src/ledger.js +0 -119
  149. package/dist/src/tracker.d.ts +0 -29
  150. package/dist/src/tracker.js +0 -41
@@ -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;
@@ -1,22 +1,30 @@
1
+ /**
2
+ * Coding-tool adapters. Each adapter describes the managed prompt blocks, config
3
+ * files, and skill files installed for one supported coding tool (Claude, Codex,
4
+ * OpenCode). The `instruction` text is shared verbatim across adapters.
5
+ */
1
6
  import { existsSync, readFileSync } from "node:fs";
2
7
  import { join } from "node:path";
8
+ import { legacyManagedMarker, managedEndMarker, managedStartMarker } from "./managed-content.js";
3
9
  const hasManagedBlock = (root, path, owner) => {
4
10
  const target = join(root, path);
5
11
  if (!existsSync(target))
6
12
  return false;
7
13
  try {
8
14
  const content = readFileSync(target, "utf8");
9
- return content.includes(`<!-- aidlc-installer:${owner}:start -->`) || content.includes(`<!-- aidlc-installer:${owner} -->`);
15
+ return content.includes(managedStartMarker(owner)) || content.includes(legacyManagedMarker(owner));
10
16
  }
11
17
  catch {
12
18
  return false;
13
- }
19
+ } /* intentional: an unreadable prompt file is treated as not containing the block */
14
20
  };
21
+ /** Builds a managed prompt block for one adapter-owned file. */
15
22
  const adapterFile = (owner, path, body) => ({
16
23
  path,
17
24
  owner,
18
- content: `<!-- aidlc-installer:${owner}:start -->\n${body}<!-- aidlc-installer:${owner}:end -->\n`
25
+ content: `${managedStartMarker(owner)}\n${body}${managedEndMarker(owner)}\n`
19
26
  });
27
+ /** Shared AI-DLC instruction text installed into each tool's primary prompt file. */
20
28
  const instruction = (agent, adapter) => [
21
29
  `# AI-DLC for ${agent}`,
22
30
  "",
@@ -100,7 +108,4 @@ export const adapters = [
100
108
  }
101
109
  ];
102
110
  export const resolvedAdapters = () => adapters;
103
- export const resolvedAdaptersForUpgrade = () => adapters;
104
- export const installableAdapters = () => adapters;
105
111
  export const findAdapter = (id) => adapters.find((adapter) => adapter.id === id);
106
- 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,62 @@
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 start|status|intake [--task-id ID] [--title TITLE] [--root PATH] [--mode local|fleet]
14
+ npx @felixdotgo/aidlc-kit loop worker-image [--provider ${LOOP_PROVIDERS.join("|")}] [--tag TAG] [--root PATH]
15
+ npx @felixdotgo/aidlc-kit ui [path] [--port 4173] [--host 127.0.0.1] [--open] [--token-file PATH] [--reviewer-token-file PATH]
16
+ 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]
17
+ npx @felixdotgo/aidlc-kit version
18
+ Global options: --help, --version, -v`;
19
+ export const value = (args, name) => {
20
+ const index = args.indexOf(name);
21
+ if (index < 0)
22
+ return undefined;
23
+ const next = args[index + 1];
24
+ if (next === undefined || next.startsWith("--"))
25
+ throw new Error(`${name} requires a value`);
26
+ return next;
27
+ };
28
+ export const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
29
+ const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--loop-mode", "--loop-lead", "--loop-workers", "--port", "--host", "--token-file", "--reviewer-token-file"]);
30
+ const COMMAND_KEYWORDS = ["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"];
31
+ const positional = (args) => {
32
+ const result = [];
33
+ for (let index = 0; index < args.length; index += 1) {
34
+ const arg = args[index];
35
+ if (arg !== undefined && valueFlags.has(arg)) {
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (arg !== undefined && !arg.startsWith("--"))
40
+ result.push(arg);
41
+ }
42
+ return result;
43
+ };
44
+ export const rootArg = (args, fallback = ".") => positional(args).find((item) => !COMMAND_KEYWORDS.includes(item)) ?? fallback;
45
+ export const INIT_FLAGS = new Set(["--agent", "--all", "--loop-mode", "--loop-lead", "--loop-workers", "--yes", "--dry-run", "--force"]);
46
+ export const UPGRADE_FLAGS = new Set(["--loop-mode", "--loop-lead", "--loop-workers", "--dry-run"]);
47
+ export const UNINSTALL_FLAGS = new Set(["--yes", "--dry-run"]);
48
+ export const STATUS_FLAGS = new Set(["--json"]);
49
+ export const DOCTOR_FLAGS = new Set(["--strict", "--json"]);
50
+ export const MCP_FLAGS = new Set(["--dry-run", "--yes", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--enable"]);
51
+ export const UI_FLAGS = new Set(["--port", "--host", "--open", "--token-file", "--reviewer-token-file"]);
52
+ export const assertKnownOptions = (args, allowed) => {
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const arg = args[index];
55
+ if (arg === undefined || !arg.startsWith("--"))
56
+ continue;
57
+ if (!allowed.has(arg))
58
+ throw new Error(`Unknown option: ${arg}`);
59
+ if (valueFlags.has(arg))
60
+ index += 1;
61
+ }
62
+ };
@@ -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;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Command handlers for loop, MCP setup, status, doctor, and profile validate.
3
+ * Interactive prompting stays in ./prompt.ts and is invoked from here after
4
+ * argv parsing.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { doctor, doctorData, status, statusData } from "../installer.js";
10
+ import { applyMcpSetup, planMcpSetup } from "../mcp-setup.js";
11
+ import { loadProjectConfig, resolveProfiles } from "../profiles.js";
12
+ import { openBrowser, readUiTokenFile, startUiServer } from "../task-ui.js";
13
+ import { assertKnownOptions, DOCTOR_FLAGS, flags, MCP_FLAGS, rootArg, STATUS_FLAGS, UI_FLAGS, value } from "./argv.js";
14
+ import { confirmGeneral, mcpSetupSelections } from "./prompt.js";
15
+ export const runLoopCommand = async (args) => {
16
+ const action = args[0] ?? "";
17
+ if (!["start", "status", "resume", "intake", "worker-image"].includes(action))
18
+ throw new Error("Expected loop start, status, intake, or worker-image");
19
+ const parsed = new Map();
20
+ for (let index = 1; index < args.length; index += 2) {
21
+ const key = args[index];
22
+ const entry = args[index + 1];
23
+ if (key === undefined || !["--task-id", "--title", "--root", "--mode", "--provider", "--tag"].includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
24
+ throw new Error("Invalid or duplicate loop option");
25
+ parsed.set(key, entry);
26
+ }
27
+ const root = resolve(parsed.get("--root") ?? ".");
28
+ const script = join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs");
29
+ const child = spawn(process.execPath, [script, action, parsed.get("--task-id") ?? "", root, parsed.get("--mode") ?? "", parsed.get("--provider") ?? "", parsed.get("--tag") ?? "", parsed.get("--title") ?? ""], { stdio: "inherit" });
30
+ // Stay alive while the child runs so its own staged shutdown (stop after the
31
+ // current turn → cancel the turn → exit) stays reachable. A terminal Ctrl-C
32
+ // already reaches the child through the process group, so SIGINT is only
33
+ // swallowed here (forwarding it would count twice); SIGTERM sent to this
34
+ // wrapper alone (e.g. `kill <pid>`) is forwarded instead of orphaning the child.
35
+ const onInterrupt = () => { };
36
+ const onTerminate = () => { if (child.exitCode === null && child.signalCode === null)
37
+ child.kill("SIGTERM"); };
38
+ process.on("SIGINT", onInterrupt);
39
+ process.on("SIGTERM", onTerminate);
40
+ try {
41
+ const { code, signal } = await new Promise((resolveExit, rejectExit) => {
42
+ child.once("error", rejectExit);
43
+ child.once("exit", (exitCode, exitSignal) => resolveExit({ code: exitCode, signal: exitSignal }));
44
+ });
45
+ if (code !== 0)
46
+ process.exitCode = code ?? (signal === "SIGINT" ? 130 : 1);
47
+ }
48
+ finally {
49
+ process.off("SIGINT", onInterrupt);
50
+ process.off("SIGTERM", onTerminate);
51
+ }
52
+ };
53
+ export const runUi = async (args) => {
54
+ assertKnownOptions(args, UI_FLAGS);
55
+ const root = resolve(rootArg(args));
56
+ const port = Number(value(args, "--port") ?? 4173);
57
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
58
+ throw new Error("--port must be an integer between 1 and 65535");
59
+ const host = value(args, "--host") ?? "127.0.0.1";
60
+ const tokenFile = value(args, "--token-file");
61
+ const reviewerFile = value(args, "--reviewer-token-file");
62
+ const token = tokenFile ? readUiTokenFile(root, tokenFile, { create: true }) : undefined;
63
+ const reviewerToken = reviewerFile ? readUiTokenFile(root, reviewerFile, { create: false }) : undefined;
64
+ const { server, port: boundPort, token: sessionToken } = await startUiServer({ root, host, port, ...(token ? { token } : {}), ...(reviewerToken ? { reviewerToken } : {}) });
65
+ const base = `http://${host.includes(":") ? `[${host}]` : host}:${boundPort}`;
66
+ const loginUrl = `${base}/#token=${encodeURIComponent(sessionToken)}`;
67
+ console.log(`Loop (AI-DLC UI) listening on ${base} (Ctrl-C to stop)`);
68
+ console.log(`Open (signs this browser in for loop actions and gate approval): ${loginUrl}`);
69
+ if (flags(args).has("--open"))
70
+ openBrowser(loginUrl);
71
+ await new Promise((resolve) => {
72
+ const close = () => { server.close(() => resolve()); };
73
+ process.once("SIGINT", close);
74
+ process.once("SIGTERM", close);
75
+ });
76
+ };
77
+ const mcpSetupOptions = async (args) => {
78
+ const selected = await mcpSetupSelections(args);
79
+ return { root: rootArg(args), template: join(fileURLToPath(new URL("../../services/aidlc-coordination", import.meta.url))), dryRun: flags(args).has("--dry-run"), deployment: selected.deployment, storage: selected.storage, bind: selected.bind, workspace: selected.workspace, pollMs: selected.pollMs, tokenEnv: selected.tokenEnv, providers: selected.providers, enable: flags(args).has("--enable") };
80
+ };
81
+ export const runMcpSetup = async (args) => {
82
+ assertKnownOptions(args.slice(1), MCP_FLAGS);
83
+ const options = await mcpSetupOptions(args.slice(1));
84
+ const plan = planMcpSetup(options);
85
+ console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
86
+ console.log(JSON.stringify(plan, null, 2));
87
+ if (options.dryRun)
88
+ return;
89
+ if (!await confirmGeneral({ dryRun: options.dryRun, yes: flags(args.slice(1)).has("--yes") }, "Apply MCP setup"))
90
+ return;
91
+ applyMcpSetup(options);
92
+ console.log(`MCP setup written to ${plan.target}. MCP remains ${options.enable ? "enabled" : "disabled"}; start Docker explicitly with docker compose --env-file .env -f ${join(plan.target, "compose.yaml")} up -d.`);
93
+ };
94
+ export const runMcp = async (args) => { if (args[0] !== "setup")
95
+ throw new Error("Unknown command: mcp"); return runMcpSetup(args); };
96
+ export const runStatus = (args) => {
97
+ assertKnownOptions(args, STATUS_FLAGS);
98
+ const root = rootArg(args);
99
+ if (flags(args).has("--json"))
100
+ console.log(JSON.stringify(statusData(root)));
101
+ else
102
+ console.log(status(root));
103
+ };
104
+ export const runDoctor = (args) => {
105
+ assertKnownOptions(args, DOCTOR_FLAGS);
106
+ const root = rootArg(args);
107
+ const strict = flags(args).has("--strict");
108
+ if (flags(args).has("--json")) {
109
+ const data = doctorData(root, strict);
110
+ console.log(JSON.stringify(data));
111
+ if (data.level === "error")
112
+ process.exitCode = 1;
113
+ }
114
+ else {
115
+ const result = doctor(root, strict);
116
+ console.log(result);
117
+ if (result.startsWith("ERROR:"))
118
+ process.exitCode = 1;
119
+ }
120
+ };
121
+ export const runProfileValidate = (args) => {
122
+ assertKnownOptions(args.slice(1), new Set());
123
+ const root = rootArg(args.slice(1));
124
+ const config = loadProjectConfig(root);
125
+ const profiles = resolveProfiles(root, config.extends);
126
+ console.log(`OK: ${profiles.map((item) => item.id).join(" → ")}`);
127
+ };
128
+ export const runProfile = (args) => { if (args[0] !== "validate")
129
+ throw new Error("Unknown command: profile"); return runProfileValidate(args); };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure argv→options builders: init options, loop flag reads, worker mapping
3
+ * parsing, and the argv-provided part of MCP setup selections. Prompting for
4
+ * anything still missing happens in ./prompt.ts.
5
+ */
6
+ import type { InitOptions } from "../model.js";
7
+ import type { McpSetupOptions } from "../mcp-setup.js";
8
+ export declare const initOptions: (args: string[]) => InitOptions;
9
+ export declare const loopSelection: (args: string[]) => {
10
+ mode?: string;
11
+ lead?: string;
12
+ workers?: string;
13
+ };
14
+ export declare const parseWorkerMapping: (rawWorkers: string) => Record<string, string>;
15
+ /** Argv-provided MCP selections: connection choices stay optional until prompted; the rest always carry defaults. */
16
+ export type McpArgSelections = {
17
+ deployment?: McpSetupOptions["deployment"];
18
+ storage?: McpSetupOptions["storage"];
19
+ bind?: McpSetupOptions["bind"];
20
+ workspace?: string;
21
+ pollMs: number;
22
+ tokenEnv: string;
23
+ providers: string[];
24
+ };
25
+ export declare const mcpArgSelections: (args: string[]) => McpArgSelections;
@@ -0,0 +1,32 @@
1
+ import { flags, rootArg, value } from "./argv.js";
2
+ export const initOptions = (args) => ({
3
+ root: rootArg(args),
4
+ agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
5
+ all: flags(args).has("--all"),
6
+ dryRun: flags(args).has("--dry-run"),
7
+ yes: flags(args).has("--yes"),
8
+ force: flags(args).has("--force")
9
+ });
10
+ export const loopSelection = (args) => ({ mode: value(args, "--loop-mode"), lead: value(args, "--loop-lead"), workers: value(args, "--loop-workers") });
11
+ export const parseWorkerMapping = (rawWorkers) => {
12
+ const workers = Object.create(null);
13
+ for (const entry of rawWorkers.split(",")) {
14
+ const parts = entry.split("=");
15
+ if (parts.length !== 2 || !parts[0] || !parts[1] || Object.hasOwn(workers, parts[0]))
16
+ throw new Error("Invalid or duplicate loop worker mapping");
17
+ workers[parts[0]] = parts[1];
18
+ }
19
+ return workers;
20
+ };
21
+ export const mcpArgSelections = (args) => {
22
+ const selected = {
23
+ deployment: value(args, "--deployment"),
24
+ storage: value(args, "--storage"),
25
+ bind: value(args, "--bind"),
26
+ workspace: value(args, "--workspace"),
27
+ pollMs: Number(value(args, "--poll-ms") ?? 30_000),
28
+ tokenEnv: value(args, "--token-env") ?? "AIDLC_MCP_TOKEN",
29
+ providers: (value(args, "--providers") ?? "").split(",").filter(Boolean)
30
+ };
31
+ return selected;
32
+ };
@@ -0,0 +1,9 @@
1
+ import type { LoopConfig } from "../model.js";
2
+ import type { McpSetupOptions } from "../mcp-setup.js";
3
+ export declare const interactive: () => boolean;
4
+ export declare const chooseLoop: (args: string[], upgrade: boolean) => Promise<LoopConfig | undefined>;
5
+ export declare const mcpSetupSelections: (args: string[]) => Promise<Partial<Omit<McpSetupOptions, "root" | "template" | "dryRun" | "enable">>>;
6
+ export declare const confirmGeneral: (options: {
7
+ dryRun: boolean;
8
+ yes: boolean;
9
+ }, operation: string) => Promise<boolean>;