@felixdotgo/aidlc-kit 0.0.1 → 0.1.1
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.
- package/DEVELOPMENT.md +1 -1
- package/README.md +20 -9
- package/dist/assets/.agents/aidlc/conventions.md +8 -8
- package/dist/assets/.agents/aidlc/orchestrator.md +15 -10
- package/dist/assets/.agents/aidlc/phase-build.md +7 -7
- package/dist/assets/.agents/aidlc/phase-clarify.md +3 -3
- package/dist/assets/.agents/aidlc/phase-plan.md +6 -6
- package/dist/assets/.agents/aidlc/phase-wrap.md +1 -1
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
- package/dist/assets/.agents/aidlc/scripts/assignment.mjs +36 -0
- package/dist/assets/.agents/aidlc/scripts/context.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +181 -0
- package/dist/assets/.agents/aidlc/scripts/lib/authority.mjs +3 -0
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +13 -1
- package/dist/assets/.agents/aidlc/scripts/lib/feature-store.mjs +70 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +220 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +183 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +100 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +58 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +320 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +284 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +51 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-artifacts.mjs +63 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-broker.mjs +53 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-local-broker.mjs +44 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-sandbox.mjs +88 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +89 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-gate-receipt.mjs +32 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +162 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-planner.mjs +13 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +260 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +131 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +55 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +101 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +32 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +66 -0
- package/dist/assets/.agents/aidlc/scripts/lib/remote-state-client.mjs +112 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +41 -6
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +90 -0
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +58 -0
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +36 -0
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +311 -0
- package/dist/assets/.agents/aidlc/scripts/render.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +71 -11
- package/dist/assets/.agents/aidlc/scripts/task-next.mjs +2 -0
- package/dist/assets/.agents/aidlc/templates/model-contract.md +3 -3
- package/dist/assets/.agents/aidlc/templates/workplan.md +2 -2
- package/dist/assets/.agents/skills/aidlc-build/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +1 -1
- package/dist/services/aidlc-coordination/.env.fleet.example +11 -0
- package/dist/services/aidlc-coordination/compose.yaml +23 -2
- package/dist/services/aidlc-coordination/package-lock.json +1 -0
- package/dist/services/aidlc-coordination/package.json +2 -1
- package/dist/services/aidlc-coordination/src/coordination.mjs +1 -1
- package/dist/services/aidlc-coordination/src/fleet-client.mjs +36 -0
- package/dist/services/aidlc-coordination/src/fleet-run.mjs +18 -0
- package/dist/services/aidlc-coordination/src/fleet.mjs +233 -0
- package/dist/services/aidlc-coordination/src/git-gateway.mjs +50 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +41 -6
- package/dist/services/aidlc-coordination/src/repository.mjs +167 -4
- package/dist/services/aidlc-coordination/src/server.mjs +53 -9
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +351 -0
- package/dist/services/aidlc-coordination/test/git-gateway.test.mjs +75 -0
- package/dist/services/aidlc-coordination/test/supervisor-fencing.test.mjs +59 -0
- package/dist/src/adapters.js +32 -5
- package/dist/src/cli.js +146 -70
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +51 -38
- package/dist/src/installer.d.ts +10 -1
- package/dist/src/installer.js +157 -37
- package/dist/src/managed-content.js +1 -1
- package/dist/src/model.d.ts +16 -1
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +9 -1
- package/dist/src/profiles.js +37 -3
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive.d.ts +17 -0
- package/dist/src/state/archive.js +60 -0
- package/dist/src/state/items.d.ts +25 -0
- package/dist/src/state/items.js +146 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +67 -0
- package/dist/src/state/lifecycle.d.ts +5 -0
- package/dist/src/state/lifecycle.js +78 -0
- package/dist/src/state/lock.d.ts +7 -0
- package/dist/src/state/lock.js +124 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +88 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +29 -0
- package/dist/src/state/shared.d.ts +29 -0
- package/dist/src/state/shared.js +71 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +81 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +93 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +100 -0
- package/dist/src/state.d.ts +15 -110
- package/dist/src/state.js +15 -764
- package/dist/src/tracker.d.ts +7 -0
- package/dist/src/tracker.js +3 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +5 -6
- package/dist/src/upgrade.js +211 -193
- package/dist/src/workflow.d.ts +1 -0
- package/dist/src/workflow.js +3 -2
- package/docs/README.md +4 -4
- package/docs/architecture.md +10 -2
- package/docs/command-reference.md +72 -11
- package/docs/configuration.md +26 -4
- package/docs/coordination-service.md +59 -3
- package/docs/development.md +16 -2
- package/docs/operating-workflow.md +53 -14
- package/docs/testing-and-release.md +53 -10
- package/package.json +3 -3
|
@@ -29,7 +29,7 @@ const lockRecord = (path) => {
|
|
|
29
29
|
try {
|
|
30
30
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
31
31
|
return typeof value.token === "string" && Number.isInteger(value.pid) && value.pid > 0 && Number.isFinite(value.createdAt) ? value : undefined;
|
|
32
|
-
} catch { return undefined; }
|
|
32
|
+
} catch { return undefined; } /* intentional: unreadable record is treated as absent */
|
|
33
33
|
};
|
|
34
34
|
const processAlive = (pid) => {
|
|
35
35
|
try { process.kill(pid, 0); return true; }
|
|
@@ -61,7 +61,9 @@ const validateTask = (id, task) => {
|
|
|
61
61
|
if (!task.title || !["feature", "bug", "refactor", "infra"].includes(task.type) || !statuses.includes(task.status) || (task.phase === "done") !== (task.status === "done")) throw new Error(`Invalid task fields: ${id}`);
|
|
62
62
|
if (!["vi", "en"].includes(task.language) || !["low", "normal", "high", "regulated"].includes(task.risk) || !Array.isArray(task.areas) || !task.areas.length || task.areas.some((area) => !text(area))) throw new Error(`Invalid task language, risk, or areas: ${id}`);
|
|
63
63
|
if (task.decisions.some((item) => !item.id || !item.label || !["unresolved", "approved", "changed", "dropped"].includes(item.status) || (item.status === "changed" && !item.resolution))) throw new Error(`Invalid decisions: ${id}`);
|
|
64
|
+
if (task.featureId !== undefined && !idPattern.test(task.featureId)) throw new Error(`Invalid feature id: ${id}`);
|
|
64
65
|
if (task.tasks.some((item) => !item.id || !item.label || !["todo", "in_progress", "done", "deferred"].includes(item.status))) throw new Error(`Invalid execution tasks: ${id}`);
|
|
66
|
+
readyTaskItems(task);
|
|
65
67
|
if (task.evidence.some((item) => !["approval", "spec", "test", "lint", "review", "diagnostic"].includes(item.kind) || !["pass", "fail", "skip"].includes(item.result) || !item.source || !iso(item.recordedAt) || (item.area !== undefined && !text(item.area)) || (item.gate !== undefined && !gates.includes(item.gate)) || (item.kind === "approval" && (!item.gate || item.gate === "none")))) throw new Error(`Invalid evidence: ${id}`);
|
|
66
68
|
const lessonIds = new Set();
|
|
67
69
|
for (const lesson of task.lessons ?? []) { validateLesson(id, lesson); if (lessonIds.has(lesson.id)) throw new Error(`Duplicate lesson id: ${id}/${lesson.id}`); lessonIds.add(lesson.id); }
|
|
@@ -276,6 +278,35 @@ export const transitionTask = (state, id, target) => {
|
|
|
276
278
|
task.phase = target; task.gate = phaseGate(target); task.status = target === "done" ? "done" : "active"; delete task.legacyG2Wait; task.updatedAt = new Date().toISOString(); return task;
|
|
277
279
|
};
|
|
278
280
|
|
|
281
|
+
export const readyTaskItems = (task) => {
|
|
282
|
+
const byId = new Map(task.tasks.map((item) => [item.id, item]));
|
|
283
|
+
if (byId.size !== task.tasks.length) throw new Error(`Duplicate execution item: ${task.id}`);
|
|
284
|
+
const visiting = new Set(); const visited = new Set();
|
|
285
|
+
const visit = (id) => {
|
|
286
|
+
if (visiting.has(id)) throw new Error(`Cyclic execution dependency: ${task.id}/${id}`);
|
|
287
|
+
if (visited.has(id)) return;
|
|
288
|
+
visiting.add(id);
|
|
289
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
290
|
+
if (dep === id || !byId.has(dep)) throw new Error(`Unknown execution dependency: ${task.id}/${id}/${dep}`);
|
|
291
|
+
visit(dep);
|
|
292
|
+
}
|
|
293
|
+
visiting.delete(id); visited.add(id);
|
|
294
|
+
};
|
|
295
|
+
for (const item of task.tasks) {
|
|
296
|
+
if (item.dependsOn !== undefined && (!Array.isArray(item.dependsOn) || new Set(item.dependsOn).size !== item.dependsOn.length || item.dependsOn.some((dep) => typeof dep !== "string" || !dep))) throw new Error(`Invalid execution dependencies: ${task.id}/${item.id}`);
|
|
297
|
+
if (item.ownedPaths !== undefined && (!Array.isArray(item.ownedPaths) || new Set(item.ownedPaths).size !== item.ownedPaths.length || item.ownedPaths.some((path) => typeof path !== "string" || !path || isAbsolute(path) || path === ".." || path.startsWith("../") || path.includes("\\") || path.split("/").includes("..")))) throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
|
|
298
|
+
visit(item.id);
|
|
299
|
+
}
|
|
300
|
+
const ready = []; const blocked = [];
|
|
301
|
+
for (const item of task.tasks) {
|
|
302
|
+
const waitingOn = (item.dependsOn ?? []).filter((dep) => byId.get(dep)?.status !== "done");
|
|
303
|
+
if (item.status === "done" && waitingOn.length) throw new Error(`Completed item has incomplete prerequisites: ${task.id}/${item.id}`);
|
|
304
|
+
if (item.status !== "todo" && item.status !== "in_progress") continue;
|
|
305
|
+
if (waitingOn.length) blocked.push({ id: item.id, waitingOn }); else ready.push(item.id);
|
|
306
|
+
}
|
|
307
|
+
return { ready, blocked };
|
|
308
|
+
};
|
|
309
|
+
|
|
279
310
|
export const nextAction = (task, root) => {
|
|
280
311
|
const rootSuffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
|
|
281
312
|
const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
|
|
@@ -288,16 +319,20 @@ export const nextAction = (task, root) => {
|
|
|
288
319
|
if (task.status === "blocked_on_user" && task.gate === "G1_review" && task.decisions.some((decision) => decision.status === "unresolved")) return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${contextCommand} ${task.id} --phase plan${rootSuffix}`, reason: "Invalid premature G1 wait; resolve every decision before presenting G1" };
|
|
289
320
|
if (task.status === "blocked_on_user" && task.gate === "G2_codereview" && (task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task))) {
|
|
290
321
|
const open = task.tasks.filter((item) => item.status === "in_progress" || item.status === "todo");
|
|
291
|
-
const
|
|
322
|
+
const ready = new Set(readyTaskItems(task).ready);
|
|
323
|
+
const item = open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
|
|
292
324
|
if (item) return { classification: "run_phase", phase: task.phase, gate: task.gate, itemId: item.id, remainingItems: open.length, command: `${contextCommand} ${task.id} --phase build --item ${item.id}${rootSuffix}`, reason: `Invalid premature G2 wait; continue build item ${item.id}` };
|
|
325
|
+
if (open.length) return { classification: "blocked", phase: task.phase, gate: task.gate, reason: `No dependency-ready build item: ${readyTaskItems(task).blocked.map((entry) => `${entry.id} waits for ${entry.waitingOn.join(",")}`).join("; ")}` };
|
|
293
326
|
return { classification: "run_phase", phase: task.phase, gate: task.gate, remainingItems: 0, command: `${contextCommand} ${task.id} --phase build${rootSuffix}`, reason: "Invalid premature G2 wait; continue verification and adversarial review before presenting G2" };
|
|
294
327
|
}
|
|
295
328
|
if (task.status === "blocked_on_user" && task.gate === "none") return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${stateCommand} task status ${task.id} --status active${rootSuffix}`, reason: "Recover invalid wait at a gateless wrap phase before continuing wrap" };
|
|
296
329
|
if (task.status === "blocked_on_user") return { classification: "await_user", phase: task.phase, gate: task.gate, command: `${stateCommand} gate approve ${task.id} --gate ${task.gate} --source <explicit-user-approval>${rootSuffix}`, reason: `Explicit human approval is required at ${task.gate}` };
|
|
297
330
|
if (task.phase === "build") {
|
|
298
331
|
const open = task.tasks.filter((item) => item.status === "in_progress" || item.status === "todo");
|
|
299
|
-
const
|
|
332
|
+
const ready = new Set(readyTaskItems(task).ready);
|
|
333
|
+
const item = open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
|
|
300
334
|
if (item) return { classification: "run_phase", phase: task.phase, gate: task.gate, itemId: item.id, remainingItems: open.length, command: `${contextCommand} ${task.id} --phase build --item ${item.id}${rootSuffix}`, reason: `Continue build item ${item.id}; ${open.length} actionable item(s) remain before verification, review, and G2` };
|
|
335
|
+
if (open.length) return { classification: "blocked", phase: task.phase, gate: task.gate, reason: `No dependency-ready build item: ${readyTaskItems(task).blocked.map((entry) => `${entry.id} waits for ${entry.waitingOn.join(",")}`).join("; ")}` };
|
|
301
336
|
return { classification: "run_phase", phase: task.phase, gate: task.gate, remainingItems: 0, command: `${contextCommand} ${task.id} --phase build${rootSuffix}`, reason: "All build items are terminal; continue verification, adversarial review, and G2 preparation" };
|
|
302
337
|
}
|
|
303
338
|
return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${contextCommand} ${task.id} --phase ${task.phase}${rootSuffix}`, reason: `Continue ${task.phase} until the next human gate, real blocker, or completion` };
|
|
@@ -322,11 +357,11 @@ export const searchLessons = (root, state, query, areas = [], limit = 5) => { co
|
|
|
322
357
|
|
|
323
358
|
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 }; };
|
|
324
359
|
|
|
325
|
-
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 (
|
|
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`; };
|
|
326
361
|
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)); } };
|
|
327
362
|
|
|
328
363
|
const safeArtifactTarget = (root, path) => { assertNoSymlinkPath(root, path); return join(resolve(root), path); };
|
|
329
|
-
const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } };
|
|
364
|
+
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 */ };
|
|
330
365
|
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) }]; } });
|
|
331
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; };
|
|
332
367
|
const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
|
|
@@ -335,7 +370,7 @@ export const formatDiagnostics = (diagnostics) => diagnostics.map((item) => `${i
|
|
|
335
370
|
|
|
336
371
|
const oneLine = (value) => value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").replace(/ACTION\s+REQUIRED/gi, "ACTION-REQUIRED").trim();
|
|
337
372
|
const markdownText = (value) => oneLine(value).replace(/([\\`*_{}\[\]()<>#+.!|>~])/g, "\\$1");
|
|
338
|
-
const gateMeta = { G0_confirm: { icon: "🟢", title: "
|
|
373
|
+
const gateMeta = { G0_confirm: { icon: "🟢", title: "Review step · Confirm requirements", action: "Reply `ok` to confirm requirements, or state changes." }, G1_review: { icon: "🔵", title: "Review step · Approve plan", action: "Reply `approve` to approve the plan and start implementation, or state decision changes." }, G2_codereview: { icon: "🟣", title: "Review step · Review implementation", action: "Approve the implementation to complete the task, or point out what to fix." } };
|
|
339
374
|
export const gateView = (task, diagnostics) => { if (task.gate === "none") throw new Error(`Task has no human gate: ${task.id}`); const errors = diagnostics.filter((item) => item.level === "ERROR"); if (errors.length) throw new Error(errors.map((item) => item.message).join("; ")); const meta = gateMeta[task.gate]; const counts = (items) => items.reduce((result, item) => ({ ...result, [item.status]: (result[item.status] ?? 0) + 1 }), {}); return { schemaVersion: 1, gate: task.gate, icon: meta.icon, title: meta.title, action: meta.action, task: { id: task.id, title: oneLine(task.title), status: task.status, risk: task.risk, areas: task.areas }, artifacts: Object.entries(task.artifacts).flatMap(([name, path]) => path ? [{ name: oneLine(name), path: oneLine(path) }] : []), decisions: counts(task.decisions), execution: counts(task.tasks), evidence: task.evidence.filter((item) => ["test", "lint", "review"].includes(item.kind)).slice(-5).map((item) => ({ kind: item.kind, result: item.result, source: oneLine(item.source) })), diagnostics: diagnostics.filter((item) => item.level !== "INFO").map((item) => ({ level: item.level, code: item.code, message: oneLine(item.message) })) }; };
|
|
340
375
|
const markdownLinkTarget = (value) => encodeURI(value).replace(/[()#?]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
341
376
|
export const formatGateView = (task, diagnostics, format = "markdown") => { const view = gateView(task, diagnostics); if (format === "json") return `${JSON.stringify(view, null, 2)}\n`; const artifacts = view.artifacts.length ? view.artifacts.map((item) => format === "markdown" ? `- **${markdownText(item.name)}:** [${markdownText(item.path)}](${markdownLinkTarget(item.path)})` : `- ${item.name}: ${item.path}`).join("\n") : "- none"; const decisions = Object.entries(view.decisions).map(([status, count]) => `${count} ${status}`).join(" · ") || "none"; const execution = Object.entries(view.execution).map(([status, count]) => `${count} ${status}`).join(" · ") || "none"; const evidence = view.evidence.length ? view.evidence.map((item) => `- ${item.kind}: ${item.result} — ${format === "markdown" ? markdownText(item.source) : item.source}`).join("\n") : "- none yet"; const warnings = view.diagnostics.length ? `\n\n${view.diagnostics.map((item) => `- ${item.level} ${item.code}: ${format === "markdown" ? markdownText(item.message) : item.message}`).join("\n")}` : ""; if (format === "plain") return [`[IMPORTANT] ${view.icon} ${view.title}`, `Task: ${view.task.id} — ${view.task.title}`, `Status: ${view.task.status} · Risk: ${view.task.risk} · Areas: ${view.task.areas.join(", ")}`, "", "Review artifacts", artifacts, "", `Decisions: ${decisions}`, `Execution: ${execution}`, "Evidence", evidence, warnings, "", `ACTION REQUIRED -> ${oneLine(view.action)}`].join("\n").replace(/\n{3,}/g, "\n\n") + "\n"; return ["> [!IMPORTANT]", `> ${view.icon} **${view.title}**`, `> **Task:** \`${view.task.id}\` — ${markdownText(view.task.title)}`, `> **Status:** \`${view.task.status}\` · **Risk:** \`${view.task.risk}\` · **Areas:** ${view.task.areas.map((area) => `\`${markdownText(area)}\``).join(", ")}`, "", "### Review artifacts", artifacts, "", `**Decisions:** ${decisions} `, `**Execution:** ${execution}`, "", "### Verification evidence", evidence, warnings, "", `> **ACTION REQUIRED →** ${view.action}`].join("\n").replace(/\n{3,}/g, "\n\n") + "\n"; };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { resolve, join } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { stdin, stdout } from "node:process";
|
|
8
|
+
import { requireLocalAuthority } from "./lib/authority.mjs";
|
|
9
|
+
import { isLoopProvider } from "./lib/loop-drivers.mjs";
|
|
10
|
+
import { prepareLoopIntake } from "./lib/loop-intake.mjs";
|
|
11
|
+
import * as localSupervisor from "./lib/loop-supervisor-run-local.mjs";
|
|
12
|
+
import { loadTask } from "./lib/store.mjs";
|
|
13
|
+
|
|
14
|
+
const [action, taskArg, rootArg, modeArg] = process.argv.slice(2);
|
|
15
|
+
const root = resolve(rootArg || ".");
|
|
16
|
+
const readRequirement = async () => {
|
|
17
|
+
if (stdin.isTTY) { const rl = createInterface({ input: stdin, output: stdout }); try { return await rl.question("Requirement: "); } finally { rl.close(); } }
|
|
18
|
+
const chunks = []; for await (const chunk of stdin) { chunks.push(chunk); if (chunks.reduce((n, c) => n + c.length, 0) > 8192) throw new Error("Requirement exceeds 8192 bytes"); } return Buffer.concat(chunks).toString("utf8");
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
if (!["start", "resume", "status"].includes(action)) throw new Error("Expected loop start, resume, or status");
|
|
22
|
+
const config = JSON.parse(readFileSync(join(root, ".agents", "config.json"), "utf8"));
|
|
23
|
+
const mode = modeArg || config.loop?.mode || "off";
|
|
24
|
+
if (!["local", "fleet"].includes(mode)) throw new Error("Loop mode is off; configure an enabled loop mode first");
|
|
25
|
+
if (mode !== config.loop?.mode) throw new Error("Requested mode does not match configured authority");
|
|
26
|
+
const agent = config.loop?.agents?.lead;
|
|
27
|
+
const workers = config.loop?.agents?.workers ?? {};
|
|
28
|
+
if (!isLoopProvider(agent) || !Object.keys(workers).length || Object.entries(workers).some(([, value]) => !isLoopProvider(value))) throw new Error("Loop requires a configured lead and worker providers");
|
|
29
|
+
const taskId = taskArg || (action === "start" ? `loop-${randomUUID()}` : "");
|
|
30
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId)) throw new Error("Valid task ID required");
|
|
31
|
+
if (mode === "fleet") {
|
|
32
|
+
const { callRemoteTool } = await import("./lib/remote-state-client.mjs");
|
|
33
|
+
const { createFleetRemoteRepository } = await import("./lib/loop-fleet-remote.mjs");
|
|
34
|
+
const { runFleetCli } = await import("./lib/loop-fleet-cli.mjs");
|
|
35
|
+
const { readIntegrationCommit } = await import("./remote-state.mjs");
|
|
36
|
+
const requiredEnv = (name) => { const value = process.env[name]; if (typeof value !== "string" || !value.trim()) throw new Error(`Fleet loop requires ${name} for the configured remote authority`); return value; };
|
|
37
|
+
const url = requiredEnv("AIDLC_FLEET_URL"), workspace = requiredEnv("AIDLC_FLEET_WORKSPACE"), token = requiredEnv("AIDLC_FLEET_TOKEN");
|
|
38
|
+
const integrationBranch = requiredEnv("AIDLC_FLEET_INTEGRATION_BRANCH");
|
|
39
|
+
if (process.env.AIDLC_FLEET_PROJECT_ROOT && resolve(process.env.AIDLC_FLEET_PROJECT_ROOT) !== root) throw new Error("AIDLC_FLEET_PROJECT_ROOT does not match the loop root; refusing to run");
|
|
40
|
+
if (process.env.AIDLC_FLEET_WORKER_IDS && process.env.AIDLC_FLEET_WORKER_IDS.split(",").map((value) => value.trim()).filter(Boolean).join(",") !== Object.keys(workers).join(",")) throw new Error("AIDLC_FLEET_WORKER_IDS does not match the configured loop workers");
|
|
41
|
+
if (process.env.AIDLC_FLEET_WORKER_PROVIDERS && process.env.AIDLC_FLEET_WORKER_PROVIDERS !== JSON.stringify(workers)) throw new Error("AIDLC_FLEET_WORKER_PROVIDERS does not match the configured loop workers");
|
|
42
|
+
const env = { ...process.env, AIDLC_FLEET_PROJECT_ROOT: root, AIDLC_FLEET_WORKER_IDS: Object.keys(workers).join(","), AIDLC_FLEET_WORKER_PROVIDERS: JSON.stringify(workers), AIDLC_FLEET_AGENT: agent };
|
|
43
|
+
const remoteState = join(root, ".agents/aidlc/scripts/remote-state.mjs");
|
|
44
|
+
if (!existsSync(remoteState)) throw new Error(`Remote lifecycle CLI is missing: ${remoteState}`);
|
|
45
|
+
const callTool = ({ url: serviceUrl, token: serviceToken, name, args }) => callRemoteTool({ url: serviceUrl, token: serviceToken }, name, args);
|
|
46
|
+
const repository = createFleetRemoteRepository({ callTool, url, token, workspace, root });
|
|
47
|
+
if (action === "status") {
|
|
48
|
+
let snapshot = null;
|
|
49
|
+
try { snapshot = await repository.readTask({ taskId }); } catch (error) { if (!String(error?.message ?? error).includes("task not found")) throw error; }
|
|
50
|
+
const checkpoint = await repository.readGateCheckpoint({ taskId });
|
|
51
|
+
console.log(JSON.stringify({ taskId, authority: "fleet", task: snapshot?.task ?? null, nextAction: snapshot?.nextAction ?? null, checkpoint }, null, 2));
|
|
52
|
+
} else {
|
|
53
|
+
if (action === "resume") {
|
|
54
|
+
const recordPath = join(root, ".agents/data/tasks", taskId, "loop-intake.json");
|
|
55
|
+
if (!existsSync(recordPath)) throw new Error("Loop record missing; use start");
|
|
56
|
+
const record = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
57
|
+
if (record.taskId !== taskId || record.authority !== "fleet") throw new Error("Loop record authority mismatch; use the recorded authority and task");
|
|
58
|
+
}
|
|
59
|
+
await callRemoteTool({ url, token }, "fleet_get", { workspace });
|
|
60
|
+
await readIntegrationCommit({ projectRoot: root, integrationBranch, gitRemote: env.AIDLC_FLEET_GIT_REMOTE ?? "origin" });
|
|
61
|
+
if (action === "start") {
|
|
62
|
+
const requirement = await readRequirement();
|
|
63
|
+
const readTask = async (id) => { try { return (await callRemoteTool({ url, token }, "task_get", { workspace, taskId: id, clientProjectRoot: root })).task; } catch { return null; } };
|
|
64
|
+
const current = await callRemoteTool({ url, token }, "state_get", { workspace });
|
|
65
|
+
const actionable = Object.values(current?.state?.tasks ?? {}).filter((task) => task.id !== taskId && task.status === "active" && !task.handoff);
|
|
66
|
+
if (actionable.length) throw new Error(`Task create is blocked while other work is actionable: ${actionable.map((task) => task.id).join(", ")}`);
|
|
67
|
+
await prepareLoopIntake({ root, taskId, requirement, authority: "fleet", readTask, createTask: async ({ taskId: id, title }) => {
|
|
68
|
+
const claim = await repository.claimLead({ taskId: id, leaseMs: 60000 });
|
|
69
|
+
try {
|
|
70
|
+
execFileSync(process.execPath, [remoteState, "task", "create", id, "--title", title], { cwd: root, encoding: "utf8", env: { ...env, AIDLC_FLEET_LEAD_EPOCH: String(claim.epoch), AIDLC_FLEET_LEAD_OWNER_TOKEN: repository.leadOwnerToken() } });
|
|
71
|
+
} finally { await repository.releaseLead({ taskId: id, epoch: claim.epoch }); }
|
|
72
|
+
} });
|
|
73
|
+
}
|
|
74
|
+
await runFleetCli(["supervisor", taskId], { callTool, env, out: (line) => console.log(line) });
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
requireLocalAuthority();
|
|
78
|
+
// The factory contract guarantees that the dispatcher is actually wired into
|
|
79
|
+
// the supervisor, not merely present in the package. No host fallback.
|
|
80
|
+
if (typeof localSupervisor.createIsolatedLocalSupervisor !== "function") throw new Error("Isolated local worker dispatcher is not integrated; loop execution is unavailable");
|
|
81
|
+
const supervisor = await localSupervisor.createIsolatedLocalSupervisor({ root, taskId, agent, workers: config.loop.agents.workers });
|
|
82
|
+
if (action === "status") {
|
|
83
|
+
console.log(JSON.stringify(await supervisor.status(), null, 2));
|
|
84
|
+
} else if (action === "start") {
|
|
85
|
+
const requirement = await readRequirement();
|
|
86
|
+
await prepareLoopIntake({ root, taskId, requirement, authority: "local", readTask: async (id) => loadTask(root, id), createTask: async ({ taskId: id, title }) => { execFileSync(process.execPath, [join(root, ".agents/aidlc/scripts/state.mjs"), "task", "create", id, "--title", title, "--root", root], { stdio: ["ignore", "pipe", "pipe"] }); } });
|
|
87
|
+
console.log(JSON.stringify(await supervisor.start(), null, 2));
|
|
88
|
+
} else console.log(JSON.stringify(await supervisor.resume(), null, 2));
|
|
89
|
+
}
|
|
90
|
+
} catch (error) { console.error(`loop: ${error.message}`); process.exitCode = 1; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This host-only terminal is deliberately not available to lead/worker containers.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { lstatSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
import { acquireStateLock, approveAndAdvance, checkGate, loadState, saveState } from "./lib/store.mjs";
|
|
10
|
+
import { formatGateView } from "./lib/runtime.mjs";
|
|
11
|
+
import { requireLocalAuthority } from "./lib/authority.mjs";
|
|
12
|
+
import { leadArtifactDigest } from "./lib/loop-lead-artifacts.mjs";
|
|
13
|
+
import { readLocalGateCheckpoint, writeLocalGateReceipt } from "./lib/loop-local-gate-receipt.mjs";
|
|
14
|
+
|
|
15
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
function snapshot(root, taskId, state) {
|
|
17
|
+
const task = state.tasks[taskId];
|
|
18
|
+
if (!task || task.status !== "blocked_on_user") throw new Error("Task is not waiting at a human gate");
|
|
19
|
+
const pending = join(root, ".agents", "data", "tasks", taskId, ".lead-artifact-journal.json");
|
|
20
|
+
try { lstatSync(pending); throw new Error("Pending artifact journal requires supervised recovery before review"); }
|
|
21
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
22
|
+
const checks = checkGate(root, state, taskId, task.gate);
|
|
23
|
+
if (checks.some((entry) => entry.level === "ERROR")) throw new Error("Gate checks failed; repair before approval");
|
|
24
|
+
const artifacts = Object.fromEntries(["intent", "design", "workplan"].map((name) => [name, leadArtifactDigest({ root, taskId, task, name })]));
|
|
25
|
+
let commit = null;
|
|
26
|
+
if (task.gate === "G2_codereview") {
|
|
27
|
+
commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", timeout: 30000 }).trim();
|
|
28
|
+
const status = execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", timeout: 30000 });
|
|
29
|
+
if (status.split("\0").filter(Boolean).some((entry) => !entry.slice(3).startsWith(".agents/data/"))) throw new Error("G2 review requires a clean integration checkout outside workflow data");
|
|
30
|
+
}
|
|
31
|
+
const checkpoint = readLocalGateCheckpoint(root, task);
|
|
32
|
+
return { taskId, phase: task.phase, gate: task.gate, taskHash: digest(task), checkpointHash: digest(checkpoint), artifacts, commit, view: formatGateView(task, checks, "plain") };
|
|
33
|
+
}
|
|
34
|
+
async function main() {
|
|
35
|
+
if (!stdin.isTTY || !stdout.isTTY) throw new Error("Gate approval requires the trusted host terminal");
|
|
36
|
+
const [taskId, ...args] = process.argv.slice(2);
|
|
37
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId ?? "") || (args.length && (args.length !== 2 || args[0] !== "--root" || !args[1]))) throw new Error("Usage: loop-gate.mjs <task-id> [--root <path>]");
|
|
38
|
+
const root = resolve(args[1] ?? "."); requireLocalAuthority();
|
|
39
|
+
let release = acquireStateLock(root), review;
|
|
40
|
+
try { review = snapshot(root, taskId, loadState(root)); } finally { release(); }
|
|
41
|
+
console.log(review.view.trimEnd());
|
|
42
|
+
console.log(`Review identity: ${JSON.stringify({ taskId, phase: review.phase, gate: review.gate, taskHash: review.taskHash, artifacts: review.artifacts, integrationCommit: review.commit })}`);
|
|
43
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
44
|
+
let answer; try { answer = await rl.question("Type approve to approve this exact review: "); } finally { rl.close(); }
|
|
45
|
+
if (answer.trim() !== "approve") throw new Error("Review was not approved");
|
|
46
|
+
release = acquireStateLock(root);
|
|
47
|
+
try {
|
|
48
|
+
const state = loadState(root), fresh = snapshot(root, taskId, state);
|
|
49
|
+
if (digest({ ...fresh, view: undefined }) !== digest({ ...review, view: undefined })) throw new Error("Review changed while waiting for approval; inspect the new review");
|
|
50
|
+
const checkpoint = readLocalGateCheckpoint(root, state.tasks[taskId]);
|
|
51
|
+
const reviewHash = digest({ ...review, view: undefined });
|
|
52
|
+
const outcome = approveAndAdvance(root, state, taskId, review.gate, `Trusted host terminal: approve (${reviewHash})`);
|
|
53
|
+
saveState(root, state);
|
|
54
|
+
writeLocalGateReceipt(root, checkpoint, outcome.task, reviewHash);
|
|
55
|
+
console.log(`Approved ${review.gate}; next action: ${JSON.stringify(outcome.nextAction)}`);
|
|
56
|
+
} finally { release(); }
|
|
57
|
+
}
|
|
58
|
+
main().catch((error) => { console.error(`loop-gate: ${error.message}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { loopStatus, runLoop } from "./lib/loop-runtime.mjs";
|
|
4
|
+
import { LOOP_PROVIDERS, runAgentTurn } from "./lib/loop-drivers.mjs";
|
|
5
|
+
import { requireLocalAuthority } from "./lib/authority.mjs";
|
|
6
|
+
|
|
7
|
+
function parse(argv) {
|
|
8
|
+
const [command, taskId, ...tail] = argv;
|
|
9
|
+
const providers = LOOP_PROVIDERS.join("|");
|
|
10
|
+
if (!["run", "resume", "status"].includes(command) || !taskId) throw new Error(`Usage: loop.mjs run <task-id> --agent ${providers} [--max-turns N] [--turn-timeout-ms N] [--max-stall-turns N] [--root PATH] | resume <task-id> [--max-turns N] [--turn-timeout-ms N] [--max-stall-turns N] [--root PATH] | status <task-id> [--root PATH]`);
|
|
11
|
+
const options = {};
|
|
12
|
+
const supported = new Set(["--root", "--agent", "--max-turns", "--turn-timeout-ms", "--max-stall-turns"]);
|
|
13
|
+
for (let i = 0; i < tail.length; i += 2) {
|
|
14
|
+
const name = tail[i], value = tail[i + 1];
|
|
15
|
+
if (!supported.has(name) || !value || value.startsWith("--") || options[name] !== undefined) throw new Error(`Invalid option: ${name}`);
|
|
16
|
+
options[name] = value;
|
|
17
|
+
}
|
|
18
|
+
if (command === "status" && Object.keys(options).some((key) => key !== "--root")) throw new Error("status only accepts --root");
|
|
19
|
+
if (command === "run" && !LOOP_PROVIDERS.includes(options["--agent"])) throw new Error(`run requires --agent ${LOOP_PROVIDERS.join("|")}`);
|
|
20
|
+
const limits = {};
|
|
21
|
+
if (options["--max-turns"] !== undefined) limits.maxTurns = Number(options["--max-turns"]);
|
|
22
|
+
if (options["--turn-timeout-ms"] !== undefined) limits.turnTimeoutMs = Number(options["--turn-timeout-ms"]);
|
|
23
|
+
if (options["--max-stall-turns"] !== undefined) limits.maxStallTurns = Number(options["--max-stall-turns"]);
|
|
24
|
+
return { command, taskId, root: resolve(options["--root"] ?? process.cwd()), agent: options["--agent"], limits };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
requireLocalAuthority();
|
|
29
|
+
const options = parse(process.argv.slice(2));
|
|
30
|
+
const result = options.command === "status" ? loopStatus(options.root, options.taskId) : await runLoop({ root: options.root, taskId: options.taskId, agent: options.agent, mode: options.command, limits: options.limits, driver: runAgentTurn, preflight: true, onEvent: (event) => { if (event?.type === "diagnostic") console.error(event.message); if (event?.type === "resume" || event?.type === "context_changed") console.error(JSON.stringify(event)); } });
|
|
31
|
+
console.log(JSON.stringify({ ok: true, result }, null, 2));
|
|
32
|
+
if (result.status && !["complete", "gate"].includes(result.status)) process.exitCode = 2;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|