@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
package/dist/src/cli.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { resolve } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
import { installableAdapters } from "./adapters.js";
|
|
6
8
|
import { applyPlan, detectedAgents, doctor, formatPlan, planInit, planUninstall, selectAgents, status } from "./installer.js";
|
|
7
|
-
import {
|
|
9
|
+
import { LOOP_PROVIDERS } from "./model.js";
|
|
10
|
+
import { loadProjectConfig, resolveProfiles, validateLoopConfig } from "./profiles.js";
|
|
8
11
|
import { applyUpgrade, planUpgrade } from "./upgrade.js";
|
|
9
12
|
import { packageVersion } from "./workflow.js";
|
|
10
13
|
import { applyMcpSetup, planMcpSetup } from "./mcp-setup.js";
|
|
11
14
|
const usage = `Usage:
|
|
12
|
-
npx @felixdotgo/aidlc-kit init [path] [--agent <name[,name]> | --all] [--yes] [--dry-run] [--force]
|
|
13
|
-
npx @felixdotgo/aidlc-kit upgrade [path] [--dry-run]
|
|
15
|
+
npx @felixdotgo/aidlc-kit init [path] [--agent <name[,name]> | --all] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--yes] [--dry-run] [--force]
|
|
16
|
+
npx @felixdotgo/aidlc-kit upgrade [path] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--dry-run]
|
|
14
17
|
npx @felixdotgo/aidlc-kit uninstall [path] [--yes] [--dry-run]
|
|
15
18
|
npx @felixdotgo/aidlc-kit status [path]
|
|
16
19
|
npx @felixdotgo/aidlc-kit doctor [path] [--strict]
|
|
17
20
|
npx @felixdotgo/aidlc-kit profile validate [path]
|
|
21
|
+
npx @felixdotgo/aidlc-kit loop start|status|resume [--task-id ID] [--root PATH] [--mode local|fleet]
|
|
18
22
|
npx @felixdotgo/aidlc-kit mcp setup [path] [--dry-run] [--yes] [--storage sqlite|postgres] [--deployment docker|remote] [--bind localhost|network] [--workspace <id>] [--poll-ms <n>] [--enable]`;
|
|
19
23
|
const interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
|
|
20
24
|
const prompts = async () => import("@inquirer/prompts");
|
|
@@ -23,7 +27,7 @@ const value = (args, name) => {
|
|
|
23
27
|
return index < 0 ? undefined : args[index + 1];
|
|
24
28
|
};
|
|
25
29
|
const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
|
|
26
|
-
const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers"]);
|
|
30
|
+
const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--loop-mode", "--loop-lead", "--loop-workers"]);
|
|
27
31
|
const positional = (args) => {
|
|
28
32
|
const result = [];
|
|
29
33
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -37,6 +41,42 @@ const positional = (args) => {
|
|
|
37
41
|
return result;
|
|
38
42
|
};
|
|
39
43
|
const rootArg = (args, fallback = ".") => positional(args).find((item) => !["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"].includes(item)) ?? fallback;
|
|
44
|
+
const chooseLoop = async (args, upgrade) => {
|
|
45
|
+
let mode = value(args, "--loop-mode");
|
|
46
|
+
const leadArg = value(args, "--loop-lead"), workersArg = value(args, "--loop-workers");
|
|
47
|
+
if (!mode && (leadArg || workersArg))
|
|
48
|
+
throw new Error("--loop-lead and --loop-workers require --loop-mode");
|
|
49
|
+
if (!mode && interactive()) {
|
|
50
|
+
const { select } = await prompts();
|
|
51
|
+
mode = await select({ message: "Loop execution mode", choices: [
|
|
52
|
+
...(upgrade ? [{ name: "Keep existing loop configuration", value: "keep" }] : []),
|
|
53
|
+
{ name: "Off", value: "off" }, { name: "Local", value: "local" }, { name: "Fleet", value: "fleet" }
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
if (!mode || mode === "keep")
|
|
57
|
+
return undefined;
|
|
58
|
+
if (mode === "off") {
|
|
59
|
+
if (leadArg || workersArg)
|
|
60
|
+
throw new Error("Disabled loop cannot specify agents");
|
|
61
|
+
return validateLoopConfig({ mode });
|
|
62
|
+
}
|
|
63
|
+
let lead = leadArg, rawWorkers = workersArg;
|
|
64
|
+
if (interactive() && (!lead || !rawWorkers)) {
|
|
65
|
+
const { select, input } = await prompts();
|
|
66
|
+
lead ??= await select({ message: "Lead agent", choices: LOOP_PROVIDERS.map((name) => ({ name, value: name })) });
|
|
67
|
+
rawWorkers ??= await input({ message: "Workers (id=provider, comma separated)", default: "codex-worker=codex,claude-worker=claude" });
|
|
68
|
+
}
|
|
69
|
+
if (!lead || !rawWorkers)
|
|
70
|
+
throw new Error("An enabled loop requires --loop-lead and --loop-workers");
|
|
71
|
+
const workers = Object.create(null);
|
|
72
|
+
for (const entry of rawWorkers.split(",")) {
|
|
73
|
+
const parts = entry.split("=");
|
|
74
|
+
if (parts.length !== 2 || !parts[0] || !parts[1] || Object.hasOwn(workers, parts[0]))
|
|
75
|
+
throw new Error("Invalid or duplicate loop worker mapping");
|
|
76
|
+
workers[parts[0]] = parts[1];
|
|
77
|
+
}
|
|
78
|
+
return validateLoopConfig({ mode, agents: { lead, workers } });
|
|
79
|
+
};
|
|
40
80
|
const initOptions = (args) => ({
|
|
41
81
|
root: rootArg(args),
|
|
42
82
|
agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
|
|
@@ -109,6 +149,26 @@ const main = async () => {
|
|
|
109
149
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
110
150
|
if (command === "help" || command === "--help")
|
|
111
151
|
return void console.log(usage);
|
|
152
|
+
if (command === "loop") {
|
|
153
|
+
const action = args[0];
|
|
154
|
+
if (!["start", "status", "resume"].includes(action))
|
|
155
|
+
throw new Error("Expected loop start, status, or resume");
|
|
156
|
+
const parsed = new Map();
|
|
157
|
+
for (let index = 1; index < args.length; index += 2) {
|
|
158
|
+
const key = args[index], entry = args[index + 1];
|
|
159
|
+
if (!["--task-id", "--root", "--mode"].includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
|
|
160
|
+
throw new Error("Invalid or duplicate loop option");
|
|
161
|
+
parsed.set(key, entry);
|
|
162
|
+
}
|
|
163
|
+
const root = resolve(parsed.get("--root") ?? ".");
|
|
164
|
+
const script = join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs");
|
|
165
|
+
const child = spawnSync(process.execPath, [script, action, parsed.get("--task-id") ?? "", root, parsed.get("--mode") ?? ""], { stdio: "inherit" });
|
|
166
|
+
if (child.error)
|
|
167
|
+
throw child.error;
|
|
168
|
+
if (child.status !== 0)
|
|
169
|
+
process.exitCode = child.status ?? 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
112
172
|
if (command === "status")
|
|
113
173
|
return void console.log(status(rootArg(args)));
|
|
114
174
|
if (command === "doctor") {
|
|
@@ -119,7 +179,7 @@ const main = async () => {
|
|
|
119
179
|
return;
|
|
120
180
|
}
|
|
121
181
|
if (command === "init") {
|
|
122
|
-
const options = await chooseAgents(initOptions(args));
|
|
182
|
+
const options = { ...await chooseAgents(initOptions(args)), loop: await chooseLoop(args, false) };
|
|
123
183
|
const plan = planInit(options);
|
|
124
184
|
preview("INIT", plan);
|
|
125
185
|
if (plan.some((item) => item.action === "conflict"))
|
|
@@ -151,7 +211,7 @@ const main = async () => {
|
|
|
151
211
|
if (flags(args).has("--yes") || flags(args).has("--force"))
|
|
152
212
|
throw new Error("upgrade does not support --yes or --force; apply requires a human interactive TTY confirmation");
|
|
153
213
|
const root = rootArg(args);
|
|
154
|
-
const plan = planUpgrade(root);
|
|
214
|
+
const plan = planUpgrade(root, await chooseLoop(args, true));
|
|
155
215
|
preview("UPGRADE", plan);
|
|
156
216
|
if (plan.some((item) => item.action === "conflict"))
|
|
157
217
|
throw new Error("Upgrade contains conflicts; no files were written.");
|
package/dist/src/gate.js
CHANGED
|
@@ -98,9 +98,9 @@ export const formatDiagnostics = (diagnostics) => diagnostics.map((item) => `${i
|
|
|
98
98
|
const oneLine = (value) => value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").replace(/ACTION\s+REQUIRED/gi, "ACTION-REQUIRED").trim();
|
|
99
99
|
const markdownText = (value) => oneLine(value).replace(/([\\`*_{}\[\]()<>#+.!|>~])/g, "\\$1");
|
|
100
100
|
const gateMeta = {
|
|
101
|
-
G0_confirm: { icon: "🟢", title: "
|
|
102
|
-
G1_review: { icon: "🔵", title: "
|
|
103
|
-
G2_codereview: { icon: "🟣", title: "
|
|
101
|
+
G0_confirm: { icon: "🟢", title: "Review step · Confirm requirements", action: "Reply `ok` to confirm requirements, or state changes." },
|
|
102
|
+
G1_review: { icon: "🔵", title: "Review step · Approve plan", action: "Reply `approve` to approve the plan and start implementation, or state decision changes." },
|
|
103
|
+
G2_codereview: { icon: "🟣", title: "Review step · Review implementation", action: "Approve the implementation to complete the task, or point out what to fix." }
|
|
104
104
|
};
|
|
105
105
|
const artifactRows = (task) => Object.entries(task.artifacts).flatMap(([name, path]) => path ? [{ name: oneLine(name), path: oneLine(path) }] : []);
|
|
106
106
|
const markdownLinkTarget = (value) => encodeURI(value).replace(/[()#?]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
package/dist/src/installer.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type { AgentId, FileSpec, InitOptions, PlannedWrite, WorkflowManifest } from "./model.js";
|
|
1
|
+
import type { AgentId, FileSpec, InitOptions, LoopConfig, ManifestInventoryItem, PlannedWrite, WorkflowManifest } from "./model.js";
|
|
2
|
+
/** Managed prompt blocks share a path across owners, so inventory keys carry the owner. */
|
|
3
|
+
export declare const installedHashFor: (files: Record<string, ManifestInventoryItem> | undefined, spec: Pick<FileSpec, "path" | "owner" | "strategy">) => string | undefined;
|
|
2
4
|
export declare const managedContentHash: (spec: Pick<FileSpec, "owner" | "strategy">, content: string) => string | undefined;
|
|
3
5
|
export declare const readManifest: (root: string) => WorkflowManifest | undefined;
|
|
4
6
|
export declare const detectedAgents: (root: string) => AgentId[];
|
|
5
7
|
export declare const selectAgents: (options: Pick<InitOptions, "root" | "agents" | "all">) => AgentId[];
|
|
8
|
+
/** Include an explicitly selected loop config in the same preview/transaction as setup. */
|
|
9
|
+
export declare const planLoopSelection: (plan: PlannedWrite[], selection?: LoopConfig) => void;
|
|
6
10
|
export declare const planInit: (options: InitOptions) => PlannedWrite[];
|
|
7
11
|
export declare const applyPlan: (root: string, plan: PlannedWrite[]) => void;
|
|
8
12
|
export declare const formatPlan: (plan: PlannedWrite[]) => string;
|
package/dist/src/installer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { adapters, findAdapter, installableAdapters, resolvedAdapters } from "./adapters.js";
|
|
4
|
-
import { loadProjectConfig } from "./profiles.js";
|
|
4
|
+
import { loadProjectConfig, validateLoopConfig } from "./profiles.js";
|
|
5
5
|
import { loadState } from "./state.js";
|
|
6
6
|
import { planProjectLayoutMigration } from "./project-migration.js";
|
|
7
7
|
import { managedBlock, mergeManagedBlock } from "./managed-content.js";
|
|
@@ -9,6 +9,16 @@ import { resolveProjectPathWithoutSymlinks } from "./project-path.js";
|
|
|
9
9
|
import { contentHash, coreWorkflowFiles, initialProjectFiles, manifestSpec, packageVersion } from "./workflow.js";
|
|
10
10
|
const shortHash = (content) => contentHash(content).slice(0, 12);
|
|
11
11
|
const safeTarget = (root, path) => resolveProjectPathWithoutSymlinks(root, path, "Managed path escapes the project", "Managed path crosses a symlink");
|
|
12
|
+
/** Managed prompt blocks share a path across owners, so inventory keys carry the owner. */
|
|
13
|
+
export const installedHashFor = (files, spec) => {
|
|
14
|
+
if (!files)
|
|
15
|
+
return undefined;
|
|
16
|
+
const keyed = files[`${spec.path}#${spec.owner}`];
|
|
17
|
+
if (keyed)
|
|
18
|
+
return keyed.hash;
|
|
19
|
+
const legacy = files[spec.path];
|
|
20
|
+
return legacy && (spec.strategy !== "managed-block" || legacy.owner === spec.owner) ? legacy.hash : undefined;
|
|
21
|
+
};
|
|
12
22
|
export const managedContentHash = (spec, content) => {
|
|
13
23
|
const managed = managedBlock(spec, content);
|
|
14
24
|
return managed === undefined ? undefined : contentHash(managed);
|
|
@@ -51,13 +61,33 @@ const planFile = (root, spec, force, installed) => {
|
|
|
51
61
|
return { ...spec, action: "skip", reason: "already current" };
|
|
52
62
|
if (spec.path === ".agents/aidlc/manifest.json" && installed)
|
|
53
63
|
return { ...spec, action: "update", reason: "refresh manifest inventory" };
|
|
54
|
-
const installedHash = installed?.files
|
|
64
|
+
const installedHash = installedHashFor(installed?.files, spec);
|
|
55
65
|
if (installedHash && contentHash(current) === installedHash)
|
|
56
66
|
return { ...spec, action: "update", reason: "unchanged managed file" };
|
|
57
67
|
if (force)
|
|
58
68
|
return { ...spec, action: "update", reason: "--force permits replacement of an unmanaged file" };
|
|
59
69
|
return { ...spec, action: "conflict", reason: "managed ownership cannot be proven; use upgrade or review --force" };
|
|
60
70
|
};
|
|
71
|
+
/** Include an explicitly selected loop config in the same preview/transaction as setup. */
|
|
72
|
+
export const planLoopSelection = (plan, selection) => {
|
|
73
|
+
if (selection === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const loop = validateLoopConfig(selection);
|
|
76
|
+
const config = plan.find((item) => item.path === ".agents/config.json");
|
|
77
|
+
if (!config || !["create", "preserve", "skip", "migrate"].includes(config.action))
|
|
78
|
+
throw new Error("Cannot change loop configuration while the project config has another planned change");
|
|
79
|
+
const parsed = JSON.parse(config.content);
|
|
80
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
81
|
+
throw new Error("Invalid project config");
|
|
82
|
+
const current = parsed;
|
|
83
|
+
if (JSON.stringify(current.loop) === JSON.stringify(loop))
|
|
84
|
+
return;
|
|
85
|
+
const previousAction = config.action;
|
|
86
|
+
current.loop = loop;
|
|
87
|
+
config.content = `${JSON.stringify(current, null, 2)}\n`;
|
|
88
|
+
config.action = previousAction === "create" || previousAction === "migrate" ? previousAction : "update";
|
|
89
|
+
config.reason = "explicit loop mode selection";
|
|
90
|
+
};
|
|
61
91
|
export const planInit = (options) => {
|
|
62
92
|
const root = resolve(options.root);
|
|
63
93
|
const installed = readManifest(root);
|
|
@@ -70,8 +100,21 @@ export const planInit = (options) => {
|
|
|
70
100
|
const agentFiles = selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
|
|
71
101
|
const managedFiles = [...coreWorkflowFiles(), ...agentFiles];
|
|
72
102
|
const files = [...managedFiles, manifestSpec(managedFiles, selected), ...initialProjectFiles()];
|
|
73
|
-
const plan =
|
|
103
|
+
const plan = [];
|
|
104
|
+
for (const spec of files) {
|
|
105
|
+
const previous = spec.strategy === "managed-block" ? plan.find((item) => item.path === spec.path && ["create", "update", "skip"].includes(item.action)) : undefined;
|
|
106
|
+
if (!previous) {
|
|
107
|
+
plan.push(planFile(root, spec, options.force, installed));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const target = safeTarget(root, spec.path);
|
|
111
|
+
const current = existsSync(target) ? readFileSync(target, "utf8") : "";
|
|
112
|
+
previous.content = mergeManagedBlock(spec, previous.content);
|
|
113
|
+
previous.action = existsSync(target) ? (previous.content === current ? "skip" : "update") : "create";
|
|
114
|
+
previous.reason = existsSync(target) ? "managed prompt block" : "new managed prompt";
|
|
115
|
+
}
|
|
74
116
|
planProjectLayoutMigration(root, plan);
|
|
117
|
+
planLoopSelection(plan, options.loop);
|
|
75
118
|
return plan;
|
|
76
119
|
};
|
|
77
120
|
export const applyPlan = (root, plan) => {
|
|
@@ -95,33 +138,73 @@ const hasOwnedManifest = (root) => {
|
|
|
95
138
|
return false;
|
|
96
139
|
}
|
|
97
140
|
};
|
|
141
|
+
const removeManagedBlock = (current, owner) => {
|
|
142
|
+
const start = `<!-- aidlc-installer:${owner}:start -->`;
|
|
143
|
+
const end = `<!-- aidlc-installer:${owner}:end -->`;
|
|
144
|
+
return current.replace(new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
145
|
+
};
|
|
98
146
|
export const planUninstall = (root) => {
|
|
99
147
|
const project = resolve(root);
|
|
100
148
|
if (!hasOwnedManifest(project))
|
|
101
149
|
throw new Error("No aidlc-workflow ownership manifest found; refusing to remove files.");
|
|
102
150
|
const installed = readManifest(project);
|
|
103
151
|
const currentSpecs = [...coreWorkflowFiles(), ...resolvedAdapters().flatMap((adapter) => adapter.files()).map((file) => ({ ...file, ownershipClass: "managed" }))];
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
const entries = installed
|
|
153
|
+
? Object.entries(installed.files).map(([key, item]) => {
|
|
154
|
+
const separator = key.lastIndexOf("#");
|
|
155
|
+
const path = separator > 0 ? key.slice(0, separator) : key;
|
|
156
|
+
const owner = separator > 0 ? key.slice(separator + 1) : item.owner;
|
|
157
|
+
const spec = currentSpecs.find((candidate) => candidate.path === path && (candidate.strategy !== "managed-block" || candidate.owner === owner)) ?? { path, owner, strategy: item.strategy, content: "", ownershipClass: "managed" };
|
|
158
|
+
return { spec, hash: item.hash };
|
|
159
|
+
})
|
|
160
|
+
: currentSpecs.map((spec) => ({ spec }));
|
|
161
|
+
const grouped = new Map();
|
|
162
|
+
for (const entry of entries)
|
|
163
|
+
grouped.set(entry.spec.path, [...(grouped.get(entry.spec.path) ?? []), entry]);
|
|
164
|
+
const plan = [];
|
|
165
|
+
for (const [path, group] of grouped) {
|
|
166
|
+
const target = safeTarget(project, path);
|
|
167
|
+
const exists = existsSync(target);
|
|
168
|
+
const current = exists ? readFileSync(target, "utf8") : "";
|
|
169
|
+
if (!exists) {
|
|
170
|
+
plan.push({ ...group[0].spec, action: "skip", reason: "not installed" });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!group.some((entry) => entry.spec.strategy === "managed-block")) {
|
|
174
|
+
const { spec, hash } = group[0];
|
|
175
|
+
const installedHash = hash ?? contentHash(spec.content);
|
|
176
|
+
plan.push(contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" });
|
|
177
|
+
continue;
|
|
121
178
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
179
|
+
let next = current;
|
|
180
|
+
let modified;
|
|
181
|
+
for (const { spec, hash } of group) {
|
|
182
|
+
const block = managedBlock(spec, next);
|
|
183
|
+
// A v0.2.1 single-marker block cannot be removed safely; retain ownership instead of orphaning it.
|
|
184
|
+
if (!block) {
|
|
185
|
+
if (next.includes(`<!-- aidlc-installer:${spec.owner} -->`))
|
|
186
|
+
modified = spec;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (contentHash(block) !== (hash ?? contentHash(spec.content.trimEnd()))) {
|
|
190
|
+
modified = spec;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
next = removeManagedBlock(next, spec.owner);
|
|
194
|
+
}
|
|
195
|
+
const ownerSpec = group[0].spec;
|
|
196
|
+
const legacyResidue = group.find(({ spec }) => next.includes(`<!-- aidlc-installer:${spec.owner} -->`));
|
|
197
|
+
if (modified)
|
|
198
|
+
plan.push({ ...modified, action: "preserve", reason: "managed block was modified" });
|
|
199
|
+
else if (legacyResidue)
|
|
200
|
+
plan.push({ ...legacyResidue.spec, action: "preserve", reason: "legacy prompt marker requires manual removal" });
|
|
201
|
+
else if (next === current)
|
|
202
|
+
plan.push({ ...ownerSpec, action: "skip", reason: "owned block not found" });
|
|
203
|
+
else if (next)
|
|
204
|
+
plan.push({ ...ownerSpec, content: `${next}\n`, action: "update", reason: "remove owned prompt block" });
|
|
205
|
+
else
|
|
206
|
+
plan.push({ ...ownerSpec, content: "", action: "delete", reason: "remove owned prompt file" });
|
|
207
|
+
}
|
|
125
208
|
plan.push({ path: ".agents/aidlc/manifest.json", owner: "aidlc-core", content: "", ownershipClass: "managed", action: plan.some((item) => item.action === "preserve") ? "preserve" : "delete", reason: plan.some((item) => item.action === "preserve") ? "retained while modified managed files remain" : "remove ownership manifest" });
|
|
126
209
|
return plan;
|
|
127
210
|
};
|
|
@@ -160,13 +243,14 @@ export const doctor = (root, strict = false) => {
|
|
|
160
243
|
catch (error) {
|
|
161
244
|
return `ERROR: local config/state is invalid: ${error instanceof Error ? error.message : String(error)}`;
|
|
162
245
|
}
|
|
163
|
-
const modified = Object.entries(manifest.files).filter(([
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
246
|
+
const modified = [...new Set(Object.entries(manifest.files).filter(([key, item]) => {
|
|
247
|
+
const separator = key.lastIndexOf("#");
|
|
248
|
+
const target = safeTarget(root, separator > 0 ? key.slice(0, separator) : key);
|
|
249
|
+
if (!existsSync(target))
|
|
250
|
+
return true;
|
|
251
|
+
const spec = { owner: item.owner, strategy: item.strategy };
|
|
252
|
+
return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
|
|
253
|
+
}).map(([key]) => key.includes("#") ? key.slice(0, key.lastIndexOf("#")) : key))];
|
|
170
254
|
if (modified.length)
|
|
171
255
|
return `${strict ? "ERROR" : "WARN"}: managed integrity differs: ${modified.join(", ")}`;
|
|
172
256
|
return `OK: local workflow ${manifest.packageVersion} is valid; remote updates are disabled.`;
|
|
@@ -10,7 +10,7 @@ export const mergeManagedBlock = (spec, current) => {
|
|
|
10
10
|
const end = `<!-- aidlc-installer:${spec.owner}:end -->`;
|
|
11
11
|
const block = spec.content.trimEnd();
|
|
12
12
|
const matches = new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g");
|
|
13
|
-
const withoutOwnedBlocks = current.replace(matches, "").replace(/\n{3,}/g, "\n\n").
|
|
13
|
+
const withoutOwnedBlocks = current.replace(matches, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
14
14
|
const next = `${withoutOwnedBlocks}${withoutOwnedBlocks ? "\n\n" : ""}${block}`;
|
|
15
15
|
return `${next.trimEnd()}\n`;
|
|
16
16
|
};
|
package/dist/src/model.d.ts
CHANGED
|
@@ -31,6 +31,17 @@ export interface InitOptions {
|
|
|
31
31
|
dryRun: boolean;
|
|
32
32
|
yes: boolean;
|
|
33
33
|
force: boolean;
|
|
34
|
+
loop?: LoopConfig;
|
|
35
|
+
}
|
|
36
|
+
export type LoopMode = "off" | "local" | "fleet";
|
|
37
|
+
export declare const LOOP_PROVIDERS: readonly ["codex", "claude", "opencode"];
|
|
38
|
+
export type LoopAgent = (typeof LOOP_PROVIDERS)[number];
|
|
39
|
+
export interface LoopConfig {
|
|
40
|
+
mode: LoopMode;
|
|
41
|
+
agents?: {
|
|
42
|
+
lead: LoopAgent;
|
|
43
|
+
workers: Record<string, LoopAgent>;
|
|
44
|
+
};
|
|
34
45
|
}
|
|
35
46
|
export interface CommandSpec {
|
|
36
47
|
command: string;
|
|
@@ -72,6 +83,7 @@ export interface ProjectConfig {
|
|
|
72
83
|
agentState: Partial<Record<AgentId, StateMutationMode>>;
|
|
73
84
|
tracker?: TrackerConfig;
|
|
74
85
|
mcp: McpConfig;
|
|
86
|
+
loop?: LoopConfig;
|
|
75
87
|
}
|
|
76
88
|
export interface McpConfig {
|
|
77
89
|
enabled: boolean;
|
|
@@ -127,6 +139,7 @@ export interface LessonDisposition {
|
|
|
127
139
|
}
|
|
128
140
|
export interface TaskState {
|
|
129
141
|
id: string;
|
|
142
|
+
featureId?: string;
|
|
130
143
|
title: string;
|
|
131
144
|
type: "feature" | "bug" | "refactor" | "infra";
|
|
132
145
|
phase: Phase;
|
|
@@ -146,6 +159,8 @@ export interface TaskState {
|
|
|
146
159
|
id: string;
|
|
147
160
|
label: string;
|
|
148
161
|
status: "todo" | "in_progress" | "done" | "deferred";
|
|
162
|
+
dependsOn?: string[];
|
|
163
|
+
ownedPaths?: string[];
|
|
149
164
|
}>;
|
|
150
165
|
evidence: Evidence[];
|
|
151
166
|
lessons?: LessonRecord[];
|
package/dist/src/model.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const LOOP_PROVIDERS = Object.freeze(["codex", "claude", "opencode"]);
|
package/dist/src/profiles.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
|
|
1
|
+
import type { LoopConfig, McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
|
|
2
2
|
export declare const builtInProfiles: Readonly<Record<string, Profile>>;
|
|
3
|
+
export declare const validateLoopConfig: (value: unknown) => LoopConfig;
|
|
3
4
|
export declare const validateProfile: (value: unknown, label?: string) => Profile;
|
|
4
5
|
export declare const defaultConfig: () => ProjectConfig;
|
|
5
6
|
export declare const loadProjectConfig: (root: string) => ProjectConfig;
|
|
@@ -24,5 +25,6 @@ export declare const resolveEffectiveConfig: (root: string, config: ProjectConfi
|
|
|
24
25
|
maxChars: number;
|
|
25
26
|
};
|
|
26
27
|
mcp: McpConfig;
|
|
28
|
+
loop: LoopConfig;
|
|
27
29
|
};
|
|
28
30
|
export declare const includedRuleFiles: (root: string, patterns: string[]) => string[];
|
package/dist/src/profiles.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
3
|
+
import { LOOP_PROVIDERS } from "./model.js";
|
|
4
|
+
const loopProviderNames = `${LOOP_PROVIDERS.slice(0, -1).join(", ")}, or ${LOOP_PROVIDERS[LOOP_PROVIDERS.length - 1]}`;
|
|
5
|
+
const isLoopProvider = (value) => typeof value === "string" && LOOP_PROVIDERS.includes(value);
|
|
3
6
|
const profile = (id, markers = []) => ({
|
|
4
7
|
schemaVersion: 1,
|
|
5
8
|
id: `topology/${id}`,
|
|
@@ -20,6 +23,23 @@ const object = (value, label) => {
|
|
|
20
23
|
throw new Error(`${label} must be an object`);
|
|
21
24
|
return value;
|
|
22
25
|
};
|
|
26
|
+
export const validateLoopConfig = (value) => {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return { mode: "off" };
|
|
29
|
+
const item = object(value, "config.loop");
|
|
30
|
+
if (Object.keys(item).some((key) => !["mode", "agents"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
|
|
31
|
+
throw new Error("config.loop requires a valid mode and only supported fields");
|
|
32
|
+
if (item.agents === undefined)
|
|
33
|
+
return { mode: item.mode };
|
|
34
|
+
const agents = object(item.agents, "config.loop.agents");
|
|
35
|
+
if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
|
|
36
|
+
throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
|
|
37
|
+
const workers = object(agents.workers, "config.loop.agents.workers");
|
|
38
|
+
for (const [id, provider] of Object.entries(workers))
|
|
39
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
|
|
40
|
+
throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
|
|
41
|
+
return { mode: item.mode, agents: { lead: agents.lead, workers: workers } };
|
|
42
|
+
};
|
|
23
43
|
const strings = (value, label) => {
|
|
24
44
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
25
45
|
throw new Error(`${label} must be a string array`);
|
|
@@ -108,7 +128,8 @@ export const defaultConfig = () => ({
|
|
|
108
128
|
risk: { default: "normal" },
|
|
109
129
|
context: { maxChars: 16_000 },
|
|
110
130
|
agentState: {},
|
|
111
|
-
mcp: { enabled: false }
|
|
131
|
+
mcp: { enabled: false },
|
|
132
|
+
loop: { mode: "off" }
|
|
112
133
|
});
|
|
113
134
|
export const loadProjectConfig = (root) => {
|
|
114
135
|
const path = join(resolve(root), ".agents/config.json");
|
|
@@ -140,7 +161,8 @@ export const loadProjectConfig = (root) => {
|
|
|
140
161
|
context: { maxChars },
|
|
141
162
|
agentState,
|
|
142
163
|
tracker: tracker(item.tracker),
|
|
143
|
-
mcp: mcp(item.mcp)
|
|
164
|
+
mcp: mcp(item.mcp),
|
|
165
|
+
loop: validateLoopConfig(item.loop)
|
|
144
166
|
};
|
|
145
167
|
};
|
|
146
168
|
const readLocalProfile = (root, id) => {
|
|
@@ -183,7 +205,8 @@ export const resolveEffectiveConfig = (root, config) => {
|
|
|
183
205
|
rules: { include: stable([...profiles.flatMap((item) => item.rules?.include ?? []), ...config.rules.include]) },
|
|
184
206
|
risk: config.risk,
|
|
185
207
|
context: config.context,
|
|
186
|
-
mcp: config.mcp
|
|
208
|
+
mcp: config.mcp,
|
|
209
|
+
loop: config.loop ?? { mode: "off" }
|
|
187
210
|
};
|
|
188
211
|
};
|
|
189
212
|
export const includedRuleFiles = (root, patterns) => {
|
package/dist/src/state.d.ts
CHANGED
|
@@ -73,6 +73,13 @@ export interface NextAction {
|
|
|
73
73
|
}>;
|
|
74
74
|
reason: string;
|
|
75
75
|
}
|
|
76
|
+
export declare const readyTaskItems: (task: TaskState) => {
|
|
77
|
+
ready: string[];
|
|
78
|
+
blocked: Array<{
|
|
79
|
+
id: string;
|
|
80
|
+
waitingOn: string[];
|
|
81
|
+
}>;
|
|
82
|
+
};
|
|
76
83
|
export declare const nextAction: (task: TaskState, root?: string) => NextAction;
|
|
77
84
|
export declare const handoffTask: (state: WorkflowState, id: string, kind: HandoffKind, reason: string, source: string, recordedAt?: string) => TaskState;
|
|
78
85
|
export declare const closeTask: (state: WorkflowState, id: string, reason: string, source: string, recordedAt?: string) => TaskState;
|
package/dist/src/state.js
CHANGED
|
@@ -241,8 +241,11 @@ const validateTask = (id, task) => {
|
|
|
241
241
|
throw new Error(`Invalid task language, risk, or areas: ${id}`);
|
|
242
242
|
if (task.decisions.some((item) => !item.id || !item.label || !["unresolved", "approved", "changed", "dropped"].includes(item.status) || (item.status === "changed" && !item.resolution)))
|
|
243
243
|
throw new Error(`Invalid decisions: ${id}`);
|
|
244
|
+
if (task.featureId !== undefined && !idPattern.test(task.featureId))
|
|
245
|
+
throw new Error(`Invalid feature id: ${id}`);
|
|
244
246
|
if (task.tasks.some((item) => !item.id || !item.label || !["todo", "in_progress", "done", "deferred"].includes(item.status)))
|
|
245
247
|
throw new Error(`Invalid execution tasks: ${id}`);
|
|
248
|
+
readyTaskItems(task);
|
|
246
249
|
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"))))
|
|
247
250
|
throw new Error(`Invalid evidence: ${id}`);
|
|
248
251
|
const lessonIds = new Set();
|
|
@@ -551,6 +554,48 @@ export const transitionTask = (state, id, target) => {
|
|
|
551
554
|
task.updatedAt = new Date().toISOString();
|
|
552
555
|
return task;
|
|
553
556
|
};
|
|
557
|
+
export const readyTaskItems = (task) => {
|
|
558
|
+
const byId = new Map(task.tasks.map((item) => [item.id, item]));
|
|
559
|
+
if (byId.size !== task.tasks.length)
|
|
560
|
+
throw new Error(`Duplicate execution item: ${task.id}`);
|
|
561
|
+
const visiting = new Set();
|
|
562
|
+
const visited = new Set();
|
|
563
|
+
const visit = (id) => {
|
|
564
|
+
if (visiting.has(id))
|
|
565
|
+
throw new Error(`Cyclic execution dependency: ${task.id}/${id}`);
|
|
566
|
+
if (visited.has(id))
|
|
567
|
+
return;
|
|
568
|
+
visiting.add(id);
|
|
569
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
570
|
+
if (dep === id || !byId.has(dep))
|
|
571
|
+
throw new Error(`Unknown execution dependency: ${task.id}/${id}/${dep}`);
|
|
572
|
+
visit(dep);
|
|
573
|
+
}
|
|
574
|
+
visiting.delete(id);
|
|
575
|
+
visited.add(id);
|
|
576
|
+
};
|
|
577
|
+
for (const item of task.tasks) {
|
|
578
|
+
if (item.dependsOn !== undefined && (!Array.isArray(item.dependsOn) || new Set(item.dependsOn).size !== item.dependsOn.length || item.dependsOn.some((dep) => typeof dep !== "string" || !dep)))
|
|
579
|
+
throw new Error(`Invalid execution dependencies: ${task.id}/${item.id}`);
|
|
580
|
+
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(".."))))
|
|
581
|
+
throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
|
|
582
|
+
visit(item.id);
|
|
583
|
+
}
|
|
584
|
+
const ready = [];
|
|
585
|
+
const blocked = [];
|
|
586
|
+
for (const item of task.tasks) {
|
|
587
|
+
const waitingOn = (item.dependsOn ?? []).filter((dep) => byId.get(dep)?.status !== "done");
|
|
588
|
+
if (item.status === "done" && waitingOn.length)
|
|
589
|
+
throw new Error(`Completed item has incomplete prerequisites: ${task.id}/${item.id}`);
|
|
590
|
+
if (item.status !== "todo" && item.status !== "in_progress")
|
|
591
|
+
continue;
|
|
592
|
+
if (waitingOn.length)
|
|
593
|
+
blocked.push({ id: item.id, waitingOn });
|
|
594
|
+
else
|
|
595
|
+
ready.push(item.id);
|
|
596
|
+
}
|
|
597
|
+
return { ready, blocked };
|
|
598
|
+
};
|
|
554
599
|
export const nextAction = (task, root) => {
|
|
555
600
|
const rootSuffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
|
|
556
601
|
const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
|
|
@@ -579,9 +624,12 @@ export const nextAction = (task, root) => {
|
|
|
579
624
|
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" };
|
|
580
625
|
if (task.status === "blocked_on_user" && task.gate === "G2_codereview" && (task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task))) {
|
|
581
626
|
const open = task.tasks.filter((item) => item.status === "in_progress" || item.status === "todo");
|
|
582
|
-
const
|
|
627
|
+
const ready = new Set(readyTaskItems(task).ready);
|
|
628
|
+
const item = open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
|
|
583
629
|
if (item)
|
|
584
630
|
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}` };
|
|
631
|
+
if (open.length)
|
|
632
|
+
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("; ")}` };
|
|
585
633
|
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" };
|
|
586
634
|
}
|
|
587
635
|
if (task.status === "blocked_on_user" && task.gate === "none")
|
|
@@ -590,9 +638,12 @@ export const nextAction = (task, root) => {
|
|
|
590
638
|
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}` };
|
|
591
639
|
if (task.phase === "build") {
|
|
592
640
|
const open = task.tasks.filter((item) => item.status === "in_progress" || item.status === "todo");
|
|
593
|
-
const
|
|
641
|
+
const ready = new Set(readyTaskItems(task).ready);
|
|
642
|
+
const item = open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
|
|
594
643
|
if (item)
|
|
595
644
|
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` };
|
|
645
|
+
if (open.length)
|
|
646
|
+
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("; ")}` };
|
|
596
647
|
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" };
|
|
597
648
|
}
|
|
598
649
|
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` };
|
|
@@ -752,7 +803,7 @@ export const renderWorkplan = (task) => {
|
|
|
752
803
|
const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None";
|
|
753
804
|
const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}`).join("\n") : "- None";
|
|
754
805
|
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");
|
|
755
|
-
return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (
|
|
806
|
+
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`;
|
|
756
807
|
};
|
|
757
808
|
export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => {
|
|
758
809
|
for (const task of selected) {
|
package/dist/src/upgrade.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { FileSpec, PlannedWrite, WorkflowState } from "./model.js";
|
|
1
|
+
import type { FileSpec, LoopConfig, PlannedWrite, WorkflowState } from "./model.js";
|
|
2
2
|
export declare const migrateLegacyBoard: (root: string) => {
|
|
3
3
|
state: WorkflowState;
|
|
4
4
|
files: FileSpec[];
|
|
5
5
|
};
|
|
6
|
-
export declare const planUpgrade: (root: string) => PlannedWrite[];
|
|
6
|
+
export declare const planUpgrade: (root: string, loop?: LoopConfig) => PlannedWrite[];
|
|
7
7
|
export interface UpgradeResult {
|
|
8
8
|
backup: string;
|
|
9
9
|
changed: number;
|