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