@felixdotgo/aidlc-kit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +6 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +6 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +8 -3
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +1 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +7 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +2 -2
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +2 -2
- package/dist/src/adapters.js +8 -1
- package/dist/src/cli.js +111 -95
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +48 -35
- package/dist/src/installer.d.ts +5 -0
- package/dist/src/installer.js +116 -80
- package/dist/src/model.d.ts +1 -1
- package/dist/src/profiles.d.ts +6 -0
- package/dist/src/profiles.js +11 -0
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive.d.ts +17 -0
- package/dist/src/state/archive.js +60 -0
- package/dist/src/state/items.d.ts +25 -0
- package/dist/src/state/items.js +146 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +67 -0
- package/dist/src/state/lifecycle.d.ts +5 -0
- package/dist/src/state/lifecycle.js +78 -0
- package/dist/src/state/lock.d.ts +7 -0
- package/dist/src/state/lock.js +124 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +88 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +29 -0
- package/dist/src/state/shared.d.ts +29 -0
- package/dist/src/state/shared.js +71 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +81 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +93 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +100 -0
- package/dist/src/state.d.ts +15 -117
- package/dist/src/state.js +15 -815
- package/dist/src/tracker.d.ts +7 -0
- package/dist/src/tracker.js +3 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +4 -5
- package/dist/src/upgrade.js +197 -214
- package/dist/src/workflow.js +1 -1
- package/docs/README.md +2 -2
- package/docs/architecture.md +4 -1
- package/docs/command-reference.md +15 -12
- package/docs/configuration.md +7 -4
- package/docs/coordination-service.md +5 -2
- package/docs/development.md +15 -1
- package/docs/operating-workflow.md +4 -1
- package/docs/testing-and-release.md +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`@felixdotgo/aidlc-kit` installs an AI-DLC workflow into an existing project. The workflow gives coding agents a shared task lifecycle, dependency-aware work, explicit human review gates, and project-owned state and rules. The normal lead runs in your Codex, Claude Code, or OpenCode conversation; an optional coordination service supports workers on multiple machines.
|
|
4
4
|
|
|
5
|
-
Workflow assets are bundled with the package, `remoteUpdates` is `false`, and normal status checks never contact a registry. Fleet execution is opt-in and uses a separately deployed service. The optional end-to-end loop is also opt-in: select a mode during `init` or a human-run `upgrade`, then start it explicitly with `aidlc-
|
|
5
|
+
Workflow assets are bundled with the package, `remoteUpdates` is `false`, and normal status checks never contact a registry. Fleet execution is opt-in and uses a separately deployed service. The optional end-to-end loop is also opt-in: select a mode during `init` or a human-run `upgrade`, then start it explicitly with `aidlc-kit loop start` — selecting a mode never starts or approves anything by itself.
|
|
6
|
+
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> The optional end-to-end loop and the AI-DLC Coordination service are **experimental and unfinished**: they contain known bugs and rough edges. The package is pre-1.0 (`0.x`), so their CLI flags, `loop.*`/`mcp.*` configuration fields, environment variables, and installed layouts can change incompatibly in any release before `1.0.0`.
|
|
6
9
|
|
|
7
10
|
## Requirements
|
|
8
11
|
|
|
@@ -15,11 +18,11 @@ Workflow assets are bundled with the package, `remoteUpdates` is `false`, and no
|
|
|
15
18
|
Run these commands from your existing project's directory to preview the installation, then apply it:
|
|
16
19
|
|
|
17
20
|
```sh
|
|
18
|
-
npx --package @felixdotgo/aidlc-kit@0.
|
|
19
|
-
npx --package @felixdotgo/aidlc-kit@0.
|
|
21
|
+
npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit init . --agent codex --dry-run
|
|
22
|
+
npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit init . --agent codex --yes
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
`npx` may ask permission to download the package on first use. These examples pin version `0.
|
|
25
|
+
`npx` may ask permission to download the package on first use. These examples pin version `0.1.1`. After installation, the project's lifecycle scripts run locally without `npx` or a registry connection.
|
|
23
26
|
|
|
24
27
|
Use the adapter that matches the coding tool:
|
|
25
28
|
|
|
@@ -34,8 +37,8 @@ Use `--all` to install every supported adapter. Codex installs workspace-write s
|
|
|
34
37
|
Confirm the installed project locally:
|
|
35
38
|
|
|
36
39
|
```sh
|
|
37
|
-
npx --package @felixdotgo/aidlc-kit@0.
|
|
38
|
-
npx --package @felixdotgo/aidlc-kit@0.
|
|
40
|
+
npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit status .
|
|
41
|
+
npx --package @felixdotgo/aidlc-kit@0.1.1 aidlc-kit doctor . --strict
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
## Documentation
|
|
@@ -55,6 +58,13 @@ The detailed documentation is organised by task and audience:
|
|
|
55
58
|
|
|
56
59
|
Only a human may initiate or apply a workflow upgrade. `upgrade` rejects `--yes` and `--force`, requires an interactive terminal, and asks the human to type the target package version. See the [command reference](./docs/command-reference.md#upgrade) before changing an existing installation.
|
|
57
60
|
|
|
61
|
+
## Recent changes
|
|
62
|
+
|
|
63
|
+
- Synced the two committed ESM lifecycle cores so `lifecycle-core-conformance` passes.
|
|
64
|
+
- Normalized Git path scopes (trailing slashes stripped) consistently across fleet, local-worker, and snapshot flows; directory scopes now work in all three.
|
|
65
|
+
- Hardened the state lock to reclaim partially written (corrupt) lock files instead of wedging mutations.
|
|
66
|
+
- Made the fleet end-to-end test skip cleanly when its Docker worker image is unavailable.
|
|
67
|
+
|
|
58
68
|
## License
|
|
59
69
|
|
|
60
70
|
MIT
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"required": ["schemaVersion", "packageVersion", "workflow", "source", "remoteUpdates", "managedBy", "adapters", "files"],
|
|
6
6
|
"properties": {
|
|
7
7
|
"schemaVersion": { "const": 2 }, "packageVersion": { "type": "string" }, "workflow": { "const": "AI-DLC" },
|
|
8
|
-
"source": { "const": "local-package-assets" }, "remoteUpdates": { "const": false }, "managedBy": { "const": "aidlc-
|
|
8
|
+
"source": { "const": "local-package-assets" }, "remoteUpdates": { "const": false }, "managedBy": { "const": "aidlc-kit" },
|
|
9
9
|
"adapters": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" } },
|
|
10
10
|
"files": { "type": "object", "additionalProperties": { "type": "object", "required": ["hash", "owner", "strategy"], "properties": { "hash": { "type": "string" }, "owner": { "type": "string" }, "strategy": { "enum": ["replace", "managed-block"] } } } }
|
|
11
11
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context packet compiler: assembles the phase packet (task state, rules,
|
|
3
|
+
* agentic memory, spec traceability) consumed by the lead and workers.
|
|
4
|
+
*/
|
|
1
5
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
6
|
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
3
7
|
import { loadMemoryRegistry, nextAction, selectAgenticMemory } from "./store.mjs";
|
|
@@ -209,6 +213,7 @@ const requiredEvidence = (task) => task.evidence.filter((item, index, entries) =
|
|
|
209
213
|
return !entries.slice(index + 1).some((candidate) => `${candidate.kind}:${candidate.area ?? ""}:${candidate.gate ?? ""}` === key);
|
|
210
214
|
}).map(({ detail, ...item }) => item);
|
|
211
215
|
|
|
216
|
+
/** Compiles a phase context packet (task state, rules, memory, spec traceability). */
|
|
212
217
|
export const compileContext = (root, task, phase = task.phase, options = {}) => {
|
|
213
218
|
if (!["clarify", "plan", "build", "wrap", "done"].includes(phase)) throw new Error(`Unsupported phase: ${phase}`);
|
|
214
219
|
const config = loadConfig(root);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent driver adapters: preflight, turn execution, and provider-specific
|
|
3
|
+
* command construction for codex/claude/opencode.
|
|
4
|
+
*/
|
|
1
5
|
import { spawn } from "node:child_process";
|
|
2
6
|
import { accessSync, constants } from "node:fs";
|
|
3
7
|
import { delimiter, join } from "node:path";
|
|
@@ -35,7 +39,7 @@ export async function preflightAgentAdapter(name, { env = process.env, root } =
|
|
|
35
39
|
const adapter = adapters.get(name);
|
|
36
40
|
if (!adapter) throw new Error(`Unknown agent adapter: ${name}`);
|
|
37
41
|
const paths = (env.PATH ?? process.env.PATH ?? "").split(delimiter);
|
|
38
|
-
if (!paths.some((entry) => { try { accessSync(join(entry || ".", adapter.command), constants.X_OK); return true; } catch { return false; } })) throw new Error(`Worker provider CLI is unavailable: ${name}`);
|
|
42
|
+
if (!paths.some((entry) => { try { accessSync(join(entry || ".", adapter.command), constants.X_OK); return true; } catch { return false; } /* intentional: missing/unreadable executable is treated as unavailable */ })) throw new Error(`Worker provider CLI is unavailable: ${name}`);
|
|
39
43
|
if (adapter.preflight) await adapter.preflight({ env, root });
|
|
40
44
|
}
|
|
41
45
|
registerAgentAdapter("codex", {
|
|
@@ -102,6 +106,7 @@ registerAgentAdapter("opencode", {
|
|
|
102
106
|
* redacted error message.
|
|
103
107
|
*/
|
|
104
108
|
export async function runAgentTurn({ agent, root, sessionId, prompt, timeoutMs, onSessionId, onEvent, signal, env, spawnProcess = spawn } = {}) {
|
|
109
|
+
// --- Validate inputs and resolve the adapter -------------------------------
|
|
105
110
|
const adapter = adapters.get(agent);
|
|
106
111
|
if (!adapter) throw new Error(`agent must be ${LOOP_PROVIDER_LIST} or a trusted registered adapter`);
|
|
107
112
|
if (typeof root !== "string" || !root) throw new Error("root must be a nonempty path");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet coordination: job reconciliation, coordinator loop, and worker loop
|
|
3
|
+
* for multi-host AI-DLC execution.
|
|
4
|
+
*/
|
|
1
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
6
|
import { readyTaskItems, formatGateView } from "./store.mjs";
|
|
3
7
|
import { isLoopProvider, LOOP_PROVIDER_LIST } from "./loop-drivers.mjs";
|
|
@@ -116,6 +120,7 @@ export async function runFleetCoordinator({ repository, taskId, ownerId = random
|
|
|
116
120
|
for (const workerId of workerIds) assertId(workerId, "worker ID");
|
|
117
121
|
if (workerProviders !== undefined && (!workerProviders || typeof workerProviders !== "object" || Array.isArray(workerProviders) || Object.keys(workerProviders).length !== workerIds.length || workerIds.some((workerId) => !isLoopProvider(workerProviders[workerId])) || Object.keys(workerProviders).some((workerId) => !workerIds.includes(workerId)))) throw new Error(`workerProviders must map each configured worker ID to ${LOOP_PROVIDER_LIST}`);
|
|
118
122
|
const limits = validateFleetLimits(inputLimits);
|
|
123
|
+
// --- Validate limits and normalize inputs ---------------------------------
|
|
119
124
|
integer(pollMs, "pollMs", 300000);
|
|
120
125
|
const pickupTimeoutMs = integer(inputPickupTimeout ?? limits.leaseMs, "pickupTimeoutMs", 3_600_000);
|
|
121
126
|
const readTask = required(repository, "readTask");
|
|
@@ -234,6 +239,7 @@ export async function runFleetWorker({ repository, workerId = randomUUID(), work
|
|
|
234
239
|
assertId(workerId, "worker ID");
|
|
235
240
|
if (workerAgent !== undefined && !isLoopProvider(workerAgent)) throw new Error(`workerAgent must be ${LOOP_PROVIDER_LIST}`);
|
|
236
241
|
const limits = validateFleetLimits(inputLimits);
|
|
242
|
+
// --- Validate limits and bind adapter methods ------------------------------
|
|
237
243
|
const claimJob = required(repository, "claimJob");
|
|
238
244
|
const renewJob = required(repository, "renewJob");
|
|
239
245
|
const checkpointJob = required(repository, "checkpointJob");
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git handoff helpers: create lead/worker/isolated worktrees, sync the
|
|
3
|
+
* integration branch, and produce scoped change reports.
|
|
4
|
+
*/
|
|
1
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
6
|
import { execFileSync } from "node:child_process";
|
|
3
7
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync } from "node:fs";
|
|
@@ -30,9 +34,9 @@ function validPath(value, label = "path") {
|
|
|
30
34
|
}
|
|
31
35
|
function scopes(input) {
|
|
32
36
|
if (!Array.isArray(input) || input.length === 0) throw new Error("Assignment requires nonempty ownedPaths");
|
|
33
|
-
return [...new Set(input.map((path) => validPath(path, "owned path")))].sort();
|
|
37
|
+
return [...new Set(input.map((path) => validPath(path, "owned path").replace(/\/+$/, "")))].filter(Boolean).sort();
|
|
34
38
|
}
|
|
35
|
-
function inScope(path, ownedPaths) { return ownedPaths.some((scope) => path === scope ||
|
|
39
|
+
function inScope(path, ownedPaths) { return ownedPaths.some((scope) => path === scope || path.startsWith(`${scope}/`)); }
|
|
36
40
|
function changedPaths(root, base, head) {
|
|
37
41
|
const raw = execFileSync("git", ["diff", "--no-renames", "--name-only", "-z", base, head], { cwd: root, encoding: "buffer", timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
|
|
38
42
|
return raw.toString("utf8").split("\0").filter(Boolean).map((path) => validPath(path, "changed path")).sort();
|
|
@@ -72,7 +76,7 @@ function fetchBranch(root, remote, branch) {
|
|
|
72
76
|
return git(root, "rev-parse", remoteRef(remote, branch));
|
|
73
77
|
}
|
|
74
78
|
function ancestor(root, base, head) {
|
|
75
|
-
try { git(root, "merge-base", "--is-ancestor", base, head); return true; } catch { return false; }
|
|
79
|
+
try { git(root, "merge-base", "--is-ancestor", base, head); return true; } catch { return false; } /* intentional: non-git/non-worktree is treated as not clean */
|
|
76
80
|
}
|
|
77
81
|
function assertClean(root) {
|
|
78
82
|
if (git(root, "status", "--porcelain=v1", "--untracked-files=all")) throw new Error("Git worktree must be clean before handoff or integration");
|
|
@@ -87,6 +91,7 @@ function normaliseReport(job) {
|
|
|
87
91
|
|
|
88
92
|
/** The coordinator calls this from its protected integration checkout. */
|
|
89
93
|
export function createLeadGitHandoff({ root: inputRoot, integrationBranch, remote = "origin", agent = "codex" }) {
|
|
94
|
+
// --- Validate inputs and resolve the integration checkout ------------------
|
|
90
95
|
const root = resolve(inputRoot);
|
|
91
96
|
validBranch(integrationBranch); validId(remote, "remote");
|
|
92
97
|
if (!isLoopProvider(agent)) throw new Error("Invalid worker agent");
|
|
@@ -37,6 +37,7 @@ export async function runLocalWorkerBatch({ root: inputRoot, taskId, jobs, workt
|
|
|
37
37
|
if (!Number.isSafeInteger(maxTurns) || maxTurns < 1 || maxTurns > 1000 || !Number.isSafeInteger(maxStallTurns) || maxStallTurns < 1 || maxStallTurns > 20) throw new Error("Invalid worker turn or stall limit");
|
|
38
38
|
const ids = new Set(), workers = new Set(), scopes = [];
|
|
39
39
|
for (const job of jobs) {
|
|
40
|
+
if (Array.isArray(job.ownedPaths)) job.ownedPaths = [...new Set(job.ownedPaths.map((path) => (typeof path === "string" ? path.replace(/\/+$/, "") : path)).filter((path) => path))];
|
|
40
41
|
if (!identifier(job.itemId) || !identifier(job.workerId) || !isLoopProvider(job.agent) || typeof job.prompt !== "string" || !job.prompt.trim() || !Array.isArray(job.ownedPaths) || !job.ownedPaths.length || job.ownedPaths.length > 64 || !job.ownedPaths.every(safePath)) throw new Error("Invalid local job");
|
|
41
42
|
if (ids.has(job.itemId) || workers.has(job.workerId)) throw new Error("Items and workers must be distinct within a batch");
|
|
42
43
|
ids.add(job.itemId); workers.add(job.workerId);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local loop runtime: record management, checkpointing, turn orchestration,
|
|
3
|
+
* and pause/resume handling for a single lead.
|
|
4
|
+
*/
|
|
1
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
6
|
import { execFileSync } from "node:child_process";
|
|
3
7
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -152,6 +156,7 @@ function outputStop(record, action, gateView) { return { taskId: record.taskId,
|
|
|
152
156
|
|
|
153
157
|
export async function runLoop({ root: rootInput, taskId, agent, mode = "run", limits: limitInput, driver, onEvent = () => {}, preflight = false, precondition }) {
|
|
154
158
|
const root = validateRoot(rootInput);
|
|
159
|
+
// --- Validate mode and load/create the loop record -------------------------
|
|
155
160
|
let record = readLoopRecord(root, taskId);
|
|
156
161
|
if (mode === "run") {
|
|
157
162
|
if (record) throw new Error(`Loop record already exists for ${taskId}; use resume`);
|
|
@@ -187,7 +192,8 @@ export async function runLoop({ root: rootInput, taskId, agent, mode = "run", li
|
|
|
187
192
|
record.lastFingerprint = initial.digest;
|
|
188
193
|
record.checkpoint = checkpoint(current.task, current.action, initial.tree, record.iteration);
|
|
189
194
|
record.status = "running"; record.stopReason = null; record.updatedAt = iso(); writeLoopRecord(root, record);
|
|
190
|
-
|
|
195
|
+
// --- Main turn loop --------------------------------------------------------
|
|
196
|
+
for (;;) {
|
|
191
197
|
const kind = classifyAction(current.action);
|
|
192
198
|
if (kind !== "run_phase") {
|
|
193
199
|
record.status = kind === "await_user" ? "gate" : kind;
|
|
@@ -51,13 +51,14 @@ export function createWorkerSnapshot(source, destination, trackedPaths) {
|
|
|
51
51
|
|
|
52
52
|
/** Validate the complete stopped-worker snapshot before host integration. */
|
|
53
53
|
export function inspectWorkerSnapshot(destination, baseline, scopes) {
|
|
54
|
-
|
|
54
|
+
const normalizedScopes = (Array.isArray(scopes) ? scopes : []).map((scope) => (typeof scope === "string" ? scope.replace(/\/+$/, "") : scope)).filter((scope) => scope);
|
|
55
|
+
if (!(baseline instanceof Map) || !normalizedScopes.length || !normalizedScopes.every(scopePath)) throw new Error('Invalid snapshot baseline or scopes');
|
|
55
56
|
const root = realpathSync(destination), current = inventory(root, false);
|
|
56
57
|
const changed = [];
|
|
57
58
|
for (const path of new Set([...baseline.keys(), ...current.keys()])) {
|
|
58
59
|
const before = baseline.get(path), after = current.get(path);
|
|
59
60
|
if (!before || !after || before.digest !== after.digest || before.executable !== after.executable) {
|
|
60
|
-
if (!scopePath(path) || !within(path,
|
|
61
|
+
if (!scopePath(path) || !within(path, normalizedScopes)) throw new Error(`Worker changed a path outside its scope: ${path}`);
|
|
61
62
|
changed.push(path);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -29,7 +29,7 @@ const lockRecord = (path) => {
|
|
|
29
29
|
try {
|
|
30
30
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
31
31
|
return typeof value.token === "string" && Number.isInteger(value.pid) && value.pid > 0 && Number.isFinite(value.createdAt) ? value : undefined;
|
|
32
|
-
} catch { return undefined; }
|
|
32
|
+
} catch { return undefined; } /* intentional: unreadable record is treated as absent */
|
|
33
33
|
};
|
|
34
34
|
const processAlive = (pid) => {
|
|
35
35
|
try { process.kill(pid, 0); return true; }
|
|
@@ -361,7 +361,7 @@ export const renderWorkplan = (task) => { const decisions = task.decisions.lengt
|
|
|
361
361
|
export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => { for (const task of selected) { if (!task.artifacts.workplan) continue; assertNoSymlinkPath(root, task.artifacts.workplan); writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task)); } };
|
|
362
362
|
|
|
363
363
|
const safeArtifactTarget = (root, path) => { assertNoSymlinkPath(root, path); return join(resolve(root), path); };
|
|
364
|
-
const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } };
|
|
364
|
+
const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } /* intentional: unreadable/unsafe artifact is treated as absent */ };
|
|
365
365
|
const artifactDiagnostics = (root, task, names) => names.flatMap((name) => { const path = task.artifacts[name]; if (!path) return [{ level: "ERROR", code: "ARTIFACT_REFERENCE", message: `${name} artifact is not referenced` }]; try { return existsSync(safeArtifactTarget(root, path)) ? [] : [{ level: "ERROR", code: "ARTIFACT_MISSING", message: `${path} does not exist` }]; } catch (error) { return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }]; } });
|
|
366
366
|
export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.length) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
|
|
367
367
|
const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
|
|
@@ -69,6 +69,7 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
|
|
|
69
69
|
if (!extra && ["task item", "decision set", "lesson record"].includes(route)) throw new Error(`${route} requires an item/decision/lesson id`);
|
|
70
70
|
|
|
71
71
|
const current = await client.get();
|
|
72
|
+
// --- Read-only routes ------------------------------------------------------
|
|
72
73
|
if (!Number.isSafeInteger(current?.revision) || !current?.state?.tasks) throw new Error("Remote coordination returned invalid canonical state");
|
|
73
74
|
const state = current.state;
|
|
74
75
|
const previous = state.tasks[id];
|
|
@@ -100,6 +101,7 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
|
|
|
100
101
|
return { ok: true, result: await client.snapshot(current.revision, id, gate, reviewText, integrationCommit) };
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// --- Mutating routes -------------------------------------------------------
|
|
103
105
|
let task;
|
|
104
106
|
if (route === "task create") {
|
|
105
107
|
if (previous || state.archive?.[id]) throw new Error(`Task already exists: ${id}`);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AI-DLC lifecycle CLI. Parses argv, dispatches to the canonical state module,
|
|
4
|
+
* and emits the JSON result with the nextAction for the lead.
|
|
5
|
+
*/
|
|
2
6
|
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
7
|
import { readLiveLease } from "./lib/loop-lease.mjs";
|
|
4
8
|
import { listFeatures, loadFeature, readyFeatureTasks, saveFeature } from "./lib/feature-store.mjs";
|
|
@@ -29,7 +29,7 @@ const lockRecord = (path) => {
|
|
|
29
29
|
try {
|
|
30
30
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
31
31
|
return typeof value.token === "string" && Number.isInteger(value.pid) && value.pid > 0 && Number.isFinite(value.createdAt) ? value : undefined;
|
|
32
|
-
} catch { return undefined; }
|
|
32
|
+
} catch { return undefined; } /* intentional: unreadable record is treated as absent */
|
|
33
33
|
};
|
|
34
34
|
const processAlive = (pid) => {
|
|
35
35
|
try { process.kill(pid, 0); return true; }
|
|
@@ -361,7 +361,7 @@ export const renderWorkplan = (task) => { const decisions = task.decisions.lengt
|
|
|
361
361
|
export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => { for (const task of selected) { if (!task.artifacts.workplan) continue; assertNoSymlinkPath(root, task.artifacts.workplan); writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task)); } };
|
|
362
362
|
|
|
363
363
|
const safeArtifactTarget = (root, path) => { assertNoSymlinkPath(root, path); return join(resolve(root), path); };
|
|
364
|
-
const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } };
|
|
364
|
+
const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } /* intentional: unreadable/unsafe artifact is treated as absent */ };
|
|
365
365
|
const artifactDiagnostics = (root, task, names) => names.flatMap((name) => { const path = task.artifacts[name]; if (!path) return [{ level: "ERROR", code: "ARTIFACT_REFERENCE", message: `${name} artifact is not referenced` }]; try { return existsSync(safeArtifactTarget(root, path)) ? [] : [{ level: "ERROR", code: "ARTIFACT_MISSING", message: `${path} does not exist` }]; } catch (error) { return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }]; } });
|
|
366
366
|
export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.length) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
|
|
367
367
|
const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
|
package/dist/src/adapters.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-tool adapters. Each adapter describes the managed prompt blocks, config
|
|
3
|
+
* files, and skill files installed for one supported coding tool (Claude, Codex,
|
|
4
|
+
* OpenCode). The `instruction` text is shared verbatim across adapters.
|
|
5
|
+
*/
|
|
1
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
7
|
import { join } from "node:path";
|
|
3
8
|
const hasManagedBlock = (root, path, owner) => {
|
|
@@ -10,13 +15,15 @@ const hasManagedBlock = (root, path, owner) => {
|
|
|
10
15
|
}
|
|
11
16
|
catch {
|
|
12
17
|
return false;
|
|
13
|
-
}
|
|
18
|
+
} /* intentional: an unreadable prompt file is treated as not containing the block */
|
|
14
19
|
};
|
|
20
|
+
/** Builds a managed prompt block for one adapter-owned file. */
|
|
15
21
|
const adapterFile = (owner, path, body) => ({
|
|
16
22
|
path,
|
|
17
23
|
owner,
|
|
18
24
|
content: `<!-- aidlc-installer:${owner}:start -->\n${body}<!-- aidlc-installer:${owner}:end -->\n`
|
|
19
25
|
});
|
|
26
|
+
/** Shared AI-DLC instruction text installed into each tool's primary prompt file. */
|
|
20
27
|
const instruction = (agent, adapter) => [
|
|
21
28
|
`# AI-DLC for ${agent}`,
|
|
22
29
|
"",
|
package/dist/src/cli.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { join } from "node:path";
|
|
7
6
|
import { installableAdapters } from "./adapters.js";
|
|
8
7
|
import { applyPlan, detectedAgents, doctor, formatPlan, planInit, planUninstall, selectAgents, status } from "./installer.js";
|
|
9
8
|
import { LOOP_PROVIDERS } from "./model.js";
|
|
@@ -22,12 +21,14 @@ const usage = `Usage:
|
|
|
22
21
|
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]`;
|
|
23
22
|
const interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
|
|
24
23
|
const prompts = async () => import("@inquirer/prompts");
|
|
24
|
+
/* --- Argv helpers --------------------------------------------------------- */
|
|
25
25
|
const value = (args, name) => {
|
|
26
26
|
const index = args.indexOf(name);
|
|
27
27
|
return index < 0 ? undefined : args[index + 1];
|
|
28
28
|
};
|
|
29
29
|
const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
|
|
30
30
|
const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--loop-mode", "--loop-lead", "--loop-workers"]);
|
|
31
|
+
const COMMAND_KEYWORDS = ["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"];
|
|
31
32
|
const positional = (args) => {
|
|
32
33
|
const result = [];
|
|
33
34
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -40,7 +41,18 @@ const positional = (args) => {
|
|
|
40
41
|
}
|
|
41
42
|
return result;
|
|
42
43
|
};
|
|
43
|
-
const rootArg = (args, fallback = ".") => positional(args).find((item) => !
|
|
44
|
+
const rootArg = (args, fallback = ".") => positional(args).find((item) => !COMMAND_KEYWORDS.includes(item)) ?? fallback;
|
|
45
|
+
/* --- Loop configuration --------------------------------------------------- */
|
|
46
|
+
const parseWorkerMapping = (rawWorkers) => {
|
|
47
|
+
const workers = Object.create(null);
|
|
48
|
+
for (const entry of rawWorkers.split(",")) {
|
|
49
|
+
const parts = entry.split("=");
|
|
50
|
+
if (parts.length !== 2 || !parts[0] || !parts[1] || Object.hasOwn(workers, parts[0]))
|
|
51
|
+
throw new Error("Invalid or duplicate loop worker mapping");
|
|
52
|
+
workers[parts[0]] = parts[1];
|
|
53
|
+
}
|
|
54
|
+
return workers;
|
|
55
|
+
};
|
|
44
56
|
const chooseLoop = async (args, upgrade) => {
|
|
45
57
|
let mode = value(args, "--loop-mode");
|
|
46
58
|
const leadArg = value(args, "--loop-lead"), workersArg = value(args, "--loop-workers");
|
|
@@ -68,15 +80,9 @@ const chooseLoop = async (args, upgrade) => {
|
|
|
68
80
|
}
|
|
69
81
|
if (!lead || !rawWorkers)
|
|
70
82
|
throw new Error("An enabled loop requires --loop-lead and --loop-workers");
|
|
71
|
-
|
|
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 } });
|
|
83
|
+
return validateLoopConfig({ mode, agents: { lead, workers: parseWorkerMapping(rawWorkers) } });
|
|
79
84
|
};
|
|
85
|
+
/* --- Option builders and shared flows -------------------------------------- */
|
|
80
86
|
const initOptions = (args) => ({
|
|
81
87
|
root: rootArg(args),
|
|
82
88
|
agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
|
|
@@ -120,7 +126,8 @@ const confirmGeneral = async (options, operation) => {
|
|
|
120
126
|
const { confirm } = await prompts();
|
|
121
127
|
return confirm({ message: `${operation} this plan?`, default: false });
|
|
122
128
|
};
|
|
123
|
-
|
|
129
|
+
/* --- MCP setup options ----------------------------------------------------- */
|
|
130
|
+
const mcpSetupSelections = async (args) => {
|
|
124
131
|
const selected = {
|
|
125
132
|
deployment: value(args, "--deployment"),
|
|
126
133
|
storage: value(args, "--storage"),
|
|
@@ -143,32 +150,95 @@ const mcpSetupOptions = async (args) => {
|
|
|
143
150
|
}
|
|
144
151
|
if (!Number.isInteger(selected.pollMs) || selected.pollMs < 1_000 || selected.pollMs > 3_600_000)
|
|
145
152
|
throw new Error("--poll-ms must be an integer from 1000 to 3600000");
|
|
153
|
+
return selected;
|
|
154
|
+
};
|
|
155
|
+
const mcpSetupOptions = async (args) => {
|
|
156
|
+
const selected = await mcpSetupSelections(args);
|
|
146
157
|
return { root: rootArg(args), template: join(fileURLToPath(new URL("../services/aidlc-coordination", import.meta.url))), dryRun: flags(args).has("--dry-run"), deployment: selected.deployment, storage: selected.storage, bind: selected.bind, workspace: selected.workspace, pollMs: selected.pollMs, tokenEnv: selected.tokenEnv, providers: selected.providers, enable: flags(args).has("--enable") };
|
|
147
158
|
};
|
|
159
|
+
/* --- Command handlers ------------------------------------------------------ */
|
|
160
|
+
const runLoopCommand = (args) => {
|
|
161
|
+
const action = args[0];
|
|
162
|
+
if (!["start", "status", "resume"].includes(action))
|
|
163
|
+
throw new Error("Expected loop start, status, or resume");
|
|
164
|
+
const parsed = new Map();
|
|
165
|
+
for (let index = 1; index < args.length; index += 2) {
|
|
166
|
+
const key = args[index], entry = args[index + 1];
|
|
167
|
+
if (!["--task-id", "--root", "--mode"].includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
|
|
168
|
+
throw new Error("Invalid or duplicate loop option");
|
|
169
|
+
parsed.set(key, entry);
|
|
170
|
+
}
|
|
171
|
+
const root = resolve(parsed.get("--root") ?? ".");
|
|
172
|
+
const script = join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs");
|
|
173
|
+
const child = spawnSync(process.execPath, [script, action, parsed.get("--task-id") ?? "", root, parsed.get("--mode") ?? ""], { stdio: "inherit" });
|
|
174
|
+
if (child.error)
|
|
175
|
+
throw child.error;
|
|
176
|
+
if (child.status !== 0)
|
|
177
|
+
process.exitCode = child.status ?? 1;
|
|
178
|
+
};
|
|
179
|
+
const runInit = async (args) => {
|
|
180
|
+
const options = { ...await chooseAgents(initOptions(args)), loop: await chooseLoop(args, false) };
|
|
181
|
+
const plan = planInit(options);
|
|
182
|
+
preview("INIT", plan);
|
|
183
|
+
if (plan.some((item) => item.action === "conflict"))
|
|
184
|
+
throw new Error("No files were written because the plan contains conflicts.");
|
|
185
|
+
if (!await confirmGeneral(options, "Apply"))
|
|
186
|
+
return;
|
|
187
|
+
if (plan.some((item) => item.action === "migrate" || item.action === "delete")) {
|
|
188
|
+
const result = applyUpgrade(options.root, plan);
|
|
189
|
+
console.log(`Installed local AI-DLC workflow assets and migrated legacy data. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
applyPlan(options.root, plan);
|
|
193
|
+
console.log("Installed local AI-DLC workflow assets.");
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const runUninstall = async (args) => {
|
|
197
|
+
const options = initOptions(args);
|
|
198
|
+
const plan = planUninstall(options.root);
|
|
199
|
+
preview("UNINSTALL", plan);
|
|
200
|
+
if (!await confirmGeneral(options, "Remove"))
|
|
201
|
+
return;
|
|
202
|
+
applyPlan(options.root, plan);
|
|
203
|
+
console.log("Removed eligible managed assets; project config and state were preserved.");
|
|
204
|
+
};
|
|
205
|
+
const runUpgrade = async (args) => {
|
|
206
|
+
if (flags(args).has("--yes") || flags(args).has("--force"))
|
|
207
|
+
throw new Error("upgrade does not support --yes or --force; apply requires a human interactive TTY confirmation");
|
|
208
|
+
const root = rootArg(args);
|
|
209
|
+
const plan = planUpgrade(root, await chooseLoop(args, true));
|
|
210
|
+
preview("UPGRADE", plan);
|
|
211
|
+
if (plan.some((item) => item.action === "conflict"))
|
|
212
|
+
throw new Error("Upgrade contains conflicts; no files were written.");
|
|
213
|
+
if (flags(args).has("--dry-run"))
|
|
214
|
+
return;
|
|
215
|
+
if (!interactive())
|
|
216
|
+
throw new Error("Upgrade apply requires an interactive TTY. AI and non-interactive processes must not apply upgrades.");
|
|
217
|
+
const expected = packageVersion();
|
|
218
|
+
const { input } = await prompts();
|
|
219
|
+
if ((await input({ message: `Type ${expected} to apply this user-initiated upgrade` })).trim() !== expected)
|
|
220
|
+
throw new Error("Upgrade confirmation did not match the target version");
|
|
221
|
+
const result = applyUpgrade(root, plan);
|
|
222
|
+
console.log(`Upgrade complete. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
223
|
+
};
|
|
224
|
+
const runMcpSetup = async (args) => {
|
|
225
|
+
const options = await mcpSetupOptions(args.slice(1));
|
|
226
|
+
const plan = planMcpSetup(options);
|
|
227
|
+
console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
|
|
228
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
229
|
+
if (options.dryRun)
|
|
230
|
+
return;
|
|
231
|
+
if (!await confirmGeneral({ ...initOptions(args), root: options.root }, "Apply MCP setup"))
|
|
232
|
+
return;
|
|
233
|
+
applyMcpSetup(options);
|
|
234
|
+
console.log(`MCP setup written to ${plan.target}. MCP remains ${options.enable ? "enabled" : "disabled"}; start Docker explicitly with docker compose --env-file .env -f ${join(plan.target, "compose.yaml")} up -d.`);
|
|
235
|
+
};
|
|
148
236
|
const main = async () => {
|
|
149
237
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
150
238
|
if (command === "help" || command === "--help")
|
|
151
239
|
return void console.log(usage);
|
|
152
|
-
if (command === "loop")
|
|
153
|
-
|
|
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
|
-
}
|
|
240
|
+
if (command === "loop")
|
|
241
|
+
return void runLoopCommand(args);
|
|
172
242
|
if (command === "status")
|
|
173
243
|
return void console.log(status(rootArg(args)));
|
|
174
244
|
if (command === "doctor") {
|
|
@@ -178,68 +248,14 @@ const main = async () => {
|
|
|
178
248
|
process.exitCode = 1;
|
|
179
249
|
return;
|
|
180
250
|
}
|
|
181
|
-
if (command === "init")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const migratesLegacyData = plan.some((item) => item.action === "migrate" || item.action === "delete");
|
|
190
|
-
if (migratesLegacyData) {
|
|
191
|
-
const result = applyUpgrade(options.root, plan);
|
|
192
|
-
console.log(`Installed local AI-DLC workflow assets and migrated legacy data. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
applyPlan(options.root, plan);
|
|
196
|
-
console.log("Installed local AI-DLC workflow assets.");
|
|
197
|
-
}
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (command === "uninstall") {
|
|
201
|
-
const options = initOptions(args);
|
|
202
|
-
const plan = planUninstall(options.root);
|
|
203
|
-
preview("UNINSTALL", plan);
|
|
204
|
-
if (!await confirmGeneral(options, "Remove"))
|
|
205
|
-
return;
|
|
206
|
-
applyPlan(options.root, plan);
|
|
207
|
-
console.log("Removed eligible managed assets; project config and state were preserved.");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (command === "upgrade") {
|
|
211
|
-
if (flags(args).has("--yes") || flags(args).has("--force"))
|
|
212
|
-
throw new Error("upgrade does not support --yes or --force; apply requires a human interactive TTY confirmation");
|
|
213
|
-
const root = rootArg(args);
|
|
214
|
-
const plan = planUpgrade(root, await chooseLoop(args, true));
|
|
215
|
-
preview("UPGRADE", plan);
|
|
216
|
-
if (plan.some((item) => item.action === "conflict"))
|
|
217
|
-
throw new Error("Upgrade contains conflicts; no files were written.");
|
|
218
|
-
if (flags(args).has("--dry-run"))
|
|
219
|
-
return;
|
|
220
|
-
if (!interactive())
|
|
221
|
-
throw new Error("Upgrade apply requires an interactive TTY. AI and non-interactive processes must not apply upgrades.");
|
|
222
|
-
const expected = packageVersion();
|
|
223
|
-
const { input } = await prompts();
|
|
224
|
-
if ((await input({ message: `Type ${expected} to apply this user-initiated upgrade` })).trim() !== expected)
|
|
225
|
-
throw new Error("Upgrade confirmation did not match the target version");
|
|
226
|
-
const result = applyUpgrade(root, plan);
|
|
227
|
-
console.log(`Upgrade complete. Backup: ${result.backup}; changed: ${result.changed}`);
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (command === "mcp" && args[0] === "setup") {
|
|
231
|
-
const options = await mcpSetupOptions(args.slice(1));
|
|
232
|
-
const plan = planMcpSetup(options);
|
|
233
|
-
console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
|
|
234
|
-
console.log(JSON.stringify(plan, null, 2));
|
|
235
|
-
if (options.dryRun)
|
|
236
|
-
return;
|
|
237
|
-
if (!await confirmGeneral({ ...initOptions(args), root: options.root }, "Apply MCP setup"))
|
|
238
|
-
return;
|
|
239
|
-
applyMcpSetup(options);
|
|
240
|
-
console.log(`MCP setup written to ${plan.target}. MCP remains ${options.enable ? "enabled" : "disabled"}; start Docker explicitly with docker compose --env-file .env -f ${join(plan.target, "compose.yaml")} up -d.`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
251
|
+
if (command === "init")
|
|
252
|
+
return void await runInit(args);
|
|
253
|
+
if (command === "uninstall")
|
|
254
|
+
return void await runUninstall(args);
|
|
255
|
+
if (command === "upgrade")
|
|
256
|
+
return void await runUpgrade(args);
|
|
257
|
+
if (command === "mcp" && args[0] === "setup")
|
|
258
|
+
return void await runMcpSetup(args);
|
|
243
259
|
if (command === "profile" && args[0] === "validate") {
|
|
244
260
|
const root = rootArg(args.slice(1));
|
|
245
261
|
const config = loadProjectConfig(root);
|
|
@@ -249,4 +265,4 @@ const main = async () => {
|
|
|
249
265
|
}
|
|
250
266
|
throw new Error(`Unknown command: ${command}`);
|
|
251
267
|
};
|
|
252
|
-
main().catch((error) => { console.error(`aidlc-
|
|
268
|
+
main().catch((error) => { console.error(`aidlc-kit: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });
|
package/dist/src/context.js
CHANGED
|
@@ -6,10 +6,12 @@ import { loadMemoryRegistry, nextAction, selectAgenticMemory } from "./state.js"
|
|
|
6
6
|
const assetRoot = fileURLToPath(new URL("../assets/.agents/aidlc", import.meta.url));
|
|
7
7
|
const phaseFile = (phase) => phase === "done" ? "phase-wrap.md" : `phase-${phase}.md`;
|
|
8
8
|
const readAsset = (path) => readFileSync(join(assetRoot, path), "utf8");
|
|
9
|
+
/** Keeps only the latest test/lint/review record per (kind, area, gate) and strips prose details. */
|
|
9
10
|
const requiredEvidence = (task) => task.evidence.filter((item, index, entries) => {
|
|
10
11
|
const key = `${item.kind}:${item.area ?? ""}:${item.gate ?? ""}`;
|
|
11
12
|
return !entries.slice(index + 1).some((candidate) => `${candidate.kind}:${candidate.area ?? ""}:${candidate.gate ?? ""}` === key);
|
|
12
13
|
}).map(({ detail: _detail, ...item }) => item);
|
|
14
|
+
/** Compact canonical view of the task for a phase packet; `itemId` narrows execution items only. */
|
|
13
15
|
const compactTask = (task, itemId) => JSON.stringify({
|
|
14
16
|
id: task.id,
|
|
15
17
|
title: task.title,
|
package/dist/src/gate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Diagnostic, Gate, TaskState, WorkflowState } from "./model.js";
|
|
2
|
+
/** Runs all diagnostics for a gate and returns them (empty-ish with GATE_OK when clean). */
|
|
2
3
|
export declare const checkGate: (root: string, state: WorkflowState, taskId: string, gate: Gate) => Diagnostic[];
|
|
3
4
|
export declare const approveAndAdvance: (root: string, state: WorkflowState, taskId: string, gate: Gate, source: string, recordedAt?: string) => {
|
|
4
5
|
task: TaskState;
|