@felixdotgo/aidlc-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPMENT.md +14 -0
- package/README.md +14 -6
- package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
- package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
- package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
- 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-authority.mjs +245 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
- package/dist/assets/.agents/aidlc/templates/design.md +9 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
- package/dist/assets/ui/bulma.min.css +3 -0
- package/dist/assets/ui/dashboard.css +414 -0
- package/dist/services/aidlc-coordination/Dockerfile +11 -3
- package/dist/services/aidlc-coordination/compose.yaml +12 -0
- package/dist/services/aidlc-coordination/package-lock.json +3 -1
- package/dist/services/aidlc-coordination/package.json +9 -4
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
- package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
- package/dist/services/aidlc-coordination/src/server.mjs +9 -3
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
- package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
- package/dist/src/adapters.d.ts +0 -3
- package/dist/src/adapters.js +11 -6
- package/dist/src/cli/argv.d.ts +12 -0
- package/dist/src/cli/argv.js +62 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.js +129 -0
- package/dist/src/cli/options.d.ts +25 -0
- package/dist/src/cli/options.js +32 -0
- package/dist/src/cli/prompt.d.ts +9 -0
- package/dist/src/cli/prompt.js +66 -0
- package/dist/src/cli.js +67 -217
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +51 -37
- package/dist/src/installer.d.ts +19 -7
- package/dist/src/installer.js +111 -178
- package/dist/src/legacy.js +1 -0
- package/dist/src/managed-content.d.ts +7 -0
- package/dist/src/managed-content.js +15 -4
- package/dist/src/managed-planning.d.ts +65 -0
- package/dist/src/managed-planning.js +219 -0
- package/dist/src/mcp-setup.js +3 -1
- package/dist/src/model/config-contracts.d.ts +65 -0
- package/dist/src/model/config-contracts.js +1 -0
- package/dist/src/model/install-contracts.d.ts +29 -0
- package/dist/src/model/install-contracts.js +1 -0
- package/dist/src/model/lifecycle-enums.d.ts +13 -0
- package/dist/src/model/lifecycle-enums.js +1 -0
- package/dist/src/model/loop.d.ts +20 -0
- package/dist/src/model/loop.js +1 -0
- package/dist/src/model/manifest.d.ts +16 -0
- package/dist/src/model/manifest.js +1 -0
- package/dist/src/model/records.d.ts +138 -0
- package/dist/src/model/records.js +1 -0
- package/dist/src/model.d.ts +11 -264
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +8 -0
- package/dist/src/profiles.js +63 -8
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive-record.d.ts +9 -0
- package/dist/src/state/archive-record.js +26 -0
- package/dist/src/state/commands.d.ts +18 -0
- package/dist/src/state/commands.js +27 -0
- package/dist/src/state/domain.d.ts +9 -0
- package/dist/src/state/domain.js +9 -0
- package/dist/src/state/guards.d.ts +7 -0
- package/dist/src/state/guards.js +17 -0
- package/dist/src/state/io.d.ts +10 -0
- package/dist/src/state/io.js +35 -0
- package/dist/src/state/items.d.ts +24 -0
- package/dist/src/state/items.js +147 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +74 -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 +125 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +100 -0
- package/dist/src/state/migrate.d.ts +17 -0
- package/dist/src/state/migrate.js +60 -0
- package/dist/src/state/partition.d.ts +15 -0
- package/dist/src/state/partition.js +90 -0
- package/dist/src/state/paths.d.ts +11 -0
- package/dist/src/state/paths.js +12 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +32 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +75 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +95 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +143 -0
- package/dist/src/state.d.ts +20 -117
- package/dist/src/state.js +20 -815
- package/dist/src/task-ui.d.ts +54 -0
- package/dist/src/task-ui.js +1570 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +3 -5
- package/dist/src/upgrade.js +174 -255
- package/dist/src/workflow.js +4 -4
- package/docs/README.md +6 -2
- package/docs/architecture.md +12 -8
- package/docs/command-reference.md +83 -19
- package/docs/configuration.md +30 -7
- package/docs/coordination-service.md +20 -5
- package/docs/design-decisions.md +83 -0
- package/docs/development.md +41 -2
- package/docs/loop-local-run.md +495 -0
- package/docs/loop-runbook.md +235 -0
- package/docs/operating-workflow.md +5 -2
- package/docs/testing-and-release.md +11 -7
- package/docs/troubleshooting.md +125 -0
- package/package.json +5 -2
- package/dist/src/ledger.d.ts +0 -22
- package/dist/src/ledger.js +0 -119
- package/dist/src/tracker.d.ts +0 -29
- package/dist/src/tracker.js +0 -41
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy v0.2.x BOARD.md migration. Parses the markdown task table into
|
|
3
|
+
* canonical lifecycle state and preserves each legacy task's prose files in
|
|
4
|
+
* the stable artifact layout so no legacy content is lost.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import { resolveProjectPath } from "./project-path.js";
|
|
9
|
+
import { emptyState, renderWorkplan, validateState } from "./state.js";
|
|
10
|
+
const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe upgrade path");
|
|
11
|
+
const parseFrontmatter = (content) => {
|
|
12
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return {};
|
|
15
|
+
return Object.fromEntries((match[1] ?? "").split("\n").flatMap((line) => {
|
|
16
|
+
const separator = line.indexOf(":");
|
|
17
|
+
return separator < 0 ? [] : [[line.slice(0, separator).trim(), line.slice(separator + 1).trim().replace(/^['"]|['"]$/g, "")]];
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
const cells = (line) => line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/^`|`$/g, ""));
|
|
21
|
+
/* Approval evidence is rebuilt from the row's phase: everything beyond plan implies G0 passed, etc. */
|
|
22
|
+
const legacyEvidence = (phase, recordedAt) => {
|
|
23
|
+
const evidence = [];
|
|
24
|
+
if (["plan", "build", "wrap", "done"].includes(phase))
|
|
25
|
+
evidence.push({ kind: "approval", gate: "G0_confirm", source: "legacy state migration", result: "pass", recordedAt });
|
|
26
|
+
if (["build", "wrap", "done"].includes(phase))
|
|
27
|
+
evidence.push({ kind: "approval", gate: "G1_review", source: "legacy state migration", result: "pass", recordedAt });
|
|
28
|
+
return evidence;
|
|
29
|
+
};
|
|
30
|
+
/** Maps one legacy BOARD row to a canonical task, copying its prose files into the stable artifact layout. */
|
|
31
|
+
const taskFromLegacyRow = (root, files, row, recordedAt) => {
|
|
32
|
+
const [id, title, phaseRaw, gateRaw, statusRaw, branch, areasRaw, doc] = row;
|
|
33
|
+
const oldDoc = safePath(root, doc ?? "");
|
|
34
|
+
const source = existsSync(oldDoc) ? readFileSync(oldDoc, "utf8") : "";
|
|
35
|
+
const frontmatter = parseFrontmatter(source);
|
|
36
|
+
const stableDir = `.agents/data/tasks/${id}`;
|
|
37
|
+
const prosePath = `${stableDir}/intent-design.md`;
|
|
38
|
+
const workplanPath = `${stableDir}/workplan.md`;
|
|
39
|
+
if (source)
|
|
40
|
+
files.push({ path: prosePath, owner: "aidlc-state", ownershipClass: "state", content: source });
|
|
41
|
+
const oldWorkplan = (doc ?? "").replace(/\.md$/, ".workplan.md");
|
|
42
|
+
if (existsSync(safePath(root, oldWorkplan)))
|
|
43
|
+
files.push({ path: `${stableDir}/legacy-workplan.md`, owner: "aidlc-state", ownershipClass: "state", content: readFileSync(safePath(root, oldWorkplan), "utf8") });
|
|
44
|
+
const phase = (phaseRaw !== undefined && ["clarify", "plan", "build", "wrap", "done"].includes(phaseRaw) ? phaseRaw : "clarify");
|
|
45
|
+
const gate = (gateRaw !== undefined && ["none", "G0_confirm", "G1_review", "G2_codereview"].includes(gateRaw) ? gateRaw : "G0_confirm");
|
|
46
|
+
return {
|
|
47
|
+
id: id ?? "",
|
|
48
|
+
title: title ?? "",
|
|
49
|
+
type: (["feature", "bug", "refactor", "infra"].includes(frontmatter.type ?? "") ? frontmatter.type : "infra"),
|
|
50
|
+
phase,
|
|
51
|
+
gate,
|
|
52
|
+
status: (statusRaw !== undefined && ["active", "blocked_on_user", "paused", "done"].includes(statusRaw) ? statusRaw : "paused"),
|
|
53
|
+
language: frontmatter.language === "en" ? "en" : "vi",
|
|
54
|
+
risk: "normal",
|
|
55
|
+
areas: (areasRaw ?? "").split(",").map((area) => area.trim()).filter(Boolean),
|
|
56
|
+
branch: branch || "—",
|
|
57
|
+
artifacts: { intent: source ? prosePath : undefined, design: source ? prosePath : undefined, workplan: workplanPath },
|
|
58
|
+
decisions: [],
|
|
59
|
+
tasks: [],
|
|
60
|
+
evidence: [...legacyEvidence(phase, recordedAt), { kind: "diagnostic", source: "legacy state migration", result: "pass", detail: `Original artifacts preserved at ${doc}`, recordedAt }],
|
|
61
|
+
createdAt: recordedAt,
|
|
62
|
+
updatedAt: recordedAt
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export const migrateLegacyBoard = (root) => {
|
|
66
|
+
const boardPath = join(resolve(root), ".agents/data/state/BOARD.md");
|
|
67
|
+
if (!existsSync(boardPath))
|
|
68
|
+
return { state: emptyState(), files: [] };
|
|
69
|
+
const content = readFileSync(boardPath, "utf8");
|
|
70
|
+
const state = emptyState();
|
|
71
|
+
const files = [];
|
|
72
|
+
let migratedRows = 0;
|
|
73
|
+
for (const line of content.split("\n")) {
|
|
74
|
+
if (!line.startsWith("|") || /^\|[- ]+\|/.test(line) || line.includes("| id |"))
|
|
75
|
+
continue;
|
|
76
|
+
const row = cells(line);
|
|
77
|
+
if (row.length < 8)
|
|
78
|
+
throw new Error("Legacy BOARD contains a malformed task row; refusing lossy migration");
|
|
79
|
+
migratedRows += 1;
|
|
80
|
+
const task = taskFromLegacyRow(root, files, row, new Date().toISOString());
|
|
81
|
+
state.tasks[task.id] = task;
|
|
82
|
+
}
|
|
83
|
+
if (!migratedRows && !content.includes("_No tasks in flight._"))
|
|
84
|
+
throw new Error("Legacy BOARD contains no recognizable task rows; refusing lossy migration");
|
|
85
|
+
for (const task of Object.values(state.tasks))
|
|
86
|
+
files.push({ path: task.artifacts.workplan, owner: "aidlc-state", ownershipClass: "state", content: renderWorkplan(task) });
|
|
87
|
+
validateState(state);
|
|
88
|
+
return { state, files };
|
|
89
|
+
};
|
package/dist/src/upgrade.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
state: WorkflowState;
|
|
4
|
-
files: FileSpec[];
|
|
5
|
-
};
|
|
1
|
+
import type { LoopConfig, PlannedWrite } from "./model.js";
|
|
2
|
+
/** Plans an upgrade transaction from the installed manifest to the current package. */
|
|
6
3
|
export declare const planUpgrade: (root: string, loop?: LoopConfig) => PlannedWrite[];
|
|
7
4
|
export interface UpgradeResult {
|
|
8
5
|
backup: string;
|
|
9
6
|
changed: number;
|
|
10
7
|
}
|
|
8
|
+
/** Applies a planned upgrade transaction with backup and rollback on failure. */
|
|
11
9
|
export declare const applyUpgrade: (root: string, plan: PlannedWrite[]) => UpgradeResult;
|
package/dist/src/upgrade.js
CHANGED
|
@@ -1,236 +1,201 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { installedHashFor, managedContentHash, planLoopSelection, readManifest } from "./installer.js";
|
|
3
|
+
import { findAdapter, resolvedAdapters } from "./adapters.js";
|
|
5
4
|
import { legacyV021AdapterContents, legacyV021Hashes } from "./legacy.js";
|
|
6
5
|
import { planProjectLayoutMigration } from "./project-migration.js";
|
|
7
|
-
import {
|
|
6
|
+
import { legacyManifest, managedContentHash, planLoopSelection, planManagedWrites, preserveOwnedFile, readManifest, splitInventoryKey } from "./managed-planning.js";
|
|
8
7
|
import { resolveProjectPath, resolveProjectPathWithoutSymlinks } from "./project-path.js";
|
|
9
|
-
import {
|
|
8
|
+
import { prepareV4Migration, validateState } from "./state.js";
|
|
10
9
|
import { contentHash, coreWorkflowFiles, initialProjectFiles, manifestSpec } from "./workflow.js";
|
|
10
|
+
import { migrateLegacyBoard } from "./upgrade-board.js";
|
|
11
11
|
const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe upgrade path");
|
|
12
12
|
const assertNoSymlinkPath = (root, relative) => { resolveProjectPathWithoutSymlinks(root, relative, "Unsafe upgrade path", "Refusing to upgrade symlink path"); };
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/* --- Managed file planning -------------------------------------------------- */
|
|
14
|
+
/** Upgrade's planning policy: ownership is proven against installed hashes and the frozen v0.2.1 baselines. */
|
|
15
|
+
const upgradePlanPolicy = (installed, legacy) => ({
|
|
16
|
+
flow: "upgrade",
|
|
17
|
+
installed,
|
|
18
|
+
legacy,
|
|
19
|
+
resolvePath: safePath,
|
|
20
|
+
legacyBaseline: (spec, currentHash) => {
|
|
21
|
+
const legacyAdapter = legacyV021AdapterContents[spec.path];
|
|
22
|
+
const legacyAdapterHash = legacyAdapter ? contentHash(spec.strategy === "managed-block" ? legacyAdapter.trimEnd() : legacyAdapter) : undefined;
|
|
23
|
+
return legacyV021Hashes[spec.path] === currentHash || legacyAdapterHash === currentHash;
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
});
|
|
26
|
+
/** Deletes obsolete managed files tracked by the installed manifest that no longer ship in this release. */
|
|
27
|
+
const planObsoleteRemovals = (project, plan, installed, managed) => {
|
|
28
|
+
for (const [key, item] of Object.entries(installed.files)) {
|
|
29
|
+
const path = splitInventoryKey(key).path;
|
|
30
|
+
// Shared prompt blocks are upgraded through the merged plan above; never delete their whole path here.
|
|
31
|
+
if (item.strategy === "managed-block")
|
|
32
|
+
continue;
|
|
33
|
+
if (managed.some((spec) => spec.path === path))
|
|
34
|
+
continue;
|
|
35
|
+
const target = safePath(project, path);
|
|
36
|
+
if (!existsSync(target))
|
|
37
|
+
continue;
|
|
38
|
+
const current = readFileSync(target, "utf8");
|
|
39
|
+
const hash = managedContentHash({ owner: item.owner, strategy: item.strategy }, current);
|
|
40
|
+
plan.push({ path, owner: item.owner, strategy: item.strategy, ownershipClass: "managed", content: "", action: hash === item.hash ? "delete" : "conflict", reason: hash === item.hash ? "obsolete unchanged managed file" : "obsolete managed file was modified" });
|
|
23
41
|
}
|
|
24
42
|
};
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return {};
|
|
29
|
-
return Object.fromEntries(match[1].split("\n").flatMap((line) => {
|
|
30
|
-
const separator = line.indexOf(":");
|
|
31
|
-
return separator < 0 ? [] : [[line.slice(0, separator).trim(), line.slice(separator + 1).trim().replace(/^['"]|['"]$/g, "")]];
|
|
32
|
-
}));
|
|
33
|
-
};
|
|
34
|
-
const cells = (line) => line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/^`|`$/g, ""));
|
|
35
|
-
export const migrateLegacyBoard = (root) => {
|
|
36
|
-
const boardPath = join(resolve(root), ".agents/data/state/BOARD.md");
|
|
37
|
-
if (!existsSync(boardPath))
|
|
38
|
-
return { state: emptyState(), files: [] };
|
|
39
|
-
const state = emptyState();
|
|
40
|
-
const files = [];
|
|
41
|
-
const boardContent = readFileSync(boardPath, "utf8");
|
|
42
|
-
let migratedRows = 0;
|
|
43
|
-
for (const line of boardContent.split("\n")) {
|
|
44
|
-
if (!line.startsWith("|") || /^\|[- ]+\|/.test(line) || line.includes("| id |"))
|
|
43
|
+
const preserveProjectFiles = (project, plan, legacy) => {
|
|
44
|
+
for (const spec of initialProjectFiles()) {
|
|
45
|
+
if (legacy && spec.path === ".agents/data/state/aidlc-state.json" && !existsSync(safePath(project, spec.path)))
|
|
45
46
|
continue;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
evidence.push({ kind: "approval", gate: "G1_review", source: "legacy state migration", result: "pass", recordedAt: now });
|
|
70
|
-
state.tasks[id] = {
|
|
71
|
-
id,
|
|
72
|
-
title,
|
|
73
|
-
type: (["feature", "bug", "refactor", "infra"].includes(frontmatter.type) ? frontmatter.type : "infra"),
|
|
74
|
-
phase,
|
|
75
|
-
gate,
|
|
76
|
-
status: (["active", "blocked_on_user", "paused", "done"].includes(statusRaw) ? statusRaw : "paused"),
|
|
77
|
-
language: frontmatter.language === "en" ? "en" : "vi",
|
|
78
|
-
risk: "normal",
|
|
79
|
-
areas: areasRaw.split(",").map((area) => area.trim()).filter(Boolean),
|
|
80
|
-
branch: branch || "—",
|
|
81
|
-
artifacts: { intent: source ? prosePath : undefined, design: source ? prosePath : undefined, workplan: workplanPath },
|
|
82
|
-
decisions: [],
|
|
83
|
-
tasks: [],
|
|
84
|
-
evidence: [...evidence, { kind: "diagnostic", source: "legacy state migration", result: "pass", detail: `Original artifacts preserved at ${doc}`, recordedAt: now }],
|
|
85
|
-
createdAt: now,
|
|
86
|
-
updatedAt: now
|
|
87
|
-
};
|
|
47
|
+
plan.push(preserveOwnedFile(safePath(project, spec.path), spec));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/* --- Canonical state and legacy BOARD migration -------------------------------- */
|
|
51
|
+
/** Migrates a preserved canonical state file (schema v1–v3) to v4 with immutable archived-task records. */
|
|
52
|
+
const migrateCanonicalState = (project, plan, canonicalState) => {
|
|
53
|
+
if (canonicalState.action !== "preserve")
|
|
54
|
+
return;
|
|
55
|
+
const parsed = JSON.parse(canonicalState.content);
|
|
56
|
+
const normalized = validateState(parsed);
|
|
57
|
+
if (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2 && parsed.schemaVersion !== 3)
|
|
58
|
+
return;
|
|
59
|
+
const migration = prepareV4Migration(normalized);
|
|
60
|
+
canonicalState.content = `${JSON.stringify(migration.state, null, 2)}\n`;
|
|
61
|
+
canonicalState.action = "migrate";
|
|
62
|
+
canonicalState.reason = `migrate canonical state schema v${parsed.schemaVersion} to v4 compact catalog${migration.stamped.length ? ` and stamp pre-existing ready G2 waits: ${migration.stamped.join(", ")}` : " without changing task content"}`;
|
|
63
|
+
for (const record of migration.records) {
|
|
64
|
+
assertNoSymlinkPath(project, record.path);
|
|
65
|
+
const target = safePath(project, record.path);
|
|
66
|
+
if (!existsSync(target))
|
|
67
|
+
plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "migrate", reason: "write immutable terminal task record" });
|
|
68
|
+
else if (readFileSync(target, "utf8") !== record.content)
|
|
69
|
+
plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "conflict", reason: "terminal task record conflicts with prior canonical content" });
|
|
88
70
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
71
|
+
const lessonIndex = plan.find((item) => item.path === ".agents/data/lessons/index.json");
|
|
72
|
+
if (!lessonIndex)
|
|
73
|
+
throw new Error("Lesson index plan is missing");
|
|
74
|
+
lessonIndex.content = `${JSON.stringify(migration.lessonIndex, null, 2)}\n`;
|
|
75
|
+
lessonIndex.action = "migrate";
|
|
76
|
+
lessonIndex.reason = "build derived lesson index from canonical task records";
|
|
95
77
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!existsSync(
|
|
99
|
-
return
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { ...spec, content: next, action: "conflict", reason: "managed prompt block was modified" };
|
|
114
|
-
return { ...spec, content: next, action: "conflict", reason: legacy ? "legacy core differs from known baseline" : "managed file differs from installed hash" };
|
|
78
|
+
/** Seeds the plan from a legacy BOARD.md when no canonical state file exists yet. */
|
|
79
|
+
const migrateLegacyState = (project, plan, legacy) => {
|
|
80
|
+
if (!legacy || existsSync(safePath(project, ".agents/data/state/aidlc-state.json")))
|
|
81
|
+
return;
|
|
82
|
+
const migration = migrateLegacyBoard(project);
|
|
83
|
+
const compact = prepareV4Migration(migration.state);
|
|
84
|
+
plan.push({ path: ".agents/data/state/aidlc-state.json", owner: "aidlc-state", ownershipClass: "state", content: `${JSON.stringify(compact.state, null, 2)}\n`, action: "migrate", reason: "migrate legacy BOARD to canonical v4 state" });
|
|
85
|
+
for (const record of compact.records)
|
|
86
|
+
plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "migrate", reason: "preserve legacy terminal task as immutable record" });
|
|
87
|
+
const lessonIndex = plan.find((item) => item.path === ".agents/data/lessons/index.json");
|
|
88
|
+
if (lessonIndex) {
|
|
89
|
+
lessonIndex.content = `${JSON.stringify(compact.lessonIndex, null, 2)}\n`;
|
|
90
|
+
lessonIndex.action = "migrate";
|
|
91
|
+
lessonIndex.reason = "build derived lesson index during legacy migration";
|
|
92
|
+
}
|
|
93
|
+
for (const file of migration.files)
|
|
94
|
+
plan.push({ ...file, action: "migrate", reason: "preserve legacy task prose in stable task directory" });
|
|
115
95
|
};
|
|
96
|
+
/** Removes the duplicate BOARD.md projection only when the planned canonical state validates. */
|
|
97
|
+
const removeLegacyBoard = (project, plan) => {
|
|
98
|
+
const boardPath = safePath(project, ".agents/data/state/BOARD.md");
|
|
99
|
+
if (!existsSync(boardPath))
|
|
100
|
+
return;
|
|
101
|
+
const stateItem = plan.find((item) => item.path === ".agents/data/state/aidlc-state.json");
|
|
102
|
+
if (!stateItem)
|
|
103
|
+
throw new Error("Canonical state must be present before removing legacy BOARD");
|
|
104
|
+
validateState(JSON.parse(stateItem.content));
|
|
105
|
+
plan.push({ path: ".agents/data/state/BOARD.md", owner: "aidlc-state", ownershipClass: "state", content: "", action: "delete", reason: "remove duplicate lifecycle-state projection after canonical JSON validation" });
|
|
106
|
+
};
|
|
107
|
+
/* --- planUpgrade orchestration ------------------------------------------------ */
|
|
108
|
+
/** Selects which adapters to upgrade: manifest-registered ones, else auto-detected installable ones. */
|
|
109
|
+
const selectedUpgradeAgents = (project, installed) => {
|
|
110
|
+
const upgradeAdapters = resolvedAdapters();
|
|
111
|
+
const requested = installed?.adapters?.length ? installed.adapters : upgradeAdapters.filter((adapter) => adapter.detect(project)).map((adapter) => adapter.id);
|
|
112
|
+
return requested.filter((id) => findAdapter(id));
|
|
113
|
+
};
|
|
114
|
+
const agentUpgradeFiles = (selected) => selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
|
|
115
|
+
/** Plans an upgrade transaction from the installed manifest to the current package. */
|
|
116
116
|
export const planUpgrade = (root, loop) => {
|
|
117
117
|
const project = resolve(root);
|
|
118
118
|
const installed = readManifest(project);
|
|
119
119
|
const legacy = !installed && legacyManifest(project);
|
|
120
120
|
if (!installed && !legacy)
|
|
121
121
|
throw new Error("No owned AI-DLC installation found. Use init instead of upgrade.");
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (installedBlockHash !== undefined && currentBlockHash !== undefined && installedBlockHash !== currentBlockHash) {
|
|
150
|
-
previous.action = "conflict";
|
|
151
|
-
previous.reason = "managed prompt block was modified";
|
|
152
|
-
continue;
|
|
122
|
+
const selected = selectedUpgradeAgents(project, installed);
|
|
123
|
+
const managed = [...coreWorkflowFiles(), ...agentUpgradeFiles(selected)];
|
|
124
|
+
const plan = planManagedWrites(project, managed, upgradePlanPolicy(installed, legacy));
|
|
125
|
+
if (installed)
|
|
126
|
+
planObsoleteRemovals(project, plan, installed, managed);
|
|
127
|
+
preserveProjectFiles(project, plan, legacy);
|
|
128
|
+
const canonicalState = plan.find((item) => item.path === ".agents/data/state/aidlc-state.json");
|
|
129
|
+
if (canonicalState)
|
|
130
|
+
migrateCanonicalState(project, plan, canonicalState);
|
|
131
|
+
planProjectLayoutMigration(project, plan);
|
|
132
|
+
migrateLegacyState(project, plan, legacy);
|
|
133
|
+
removeLegacyBoard(project, plan);
|
|
134
|
+
plan.push({ ...manifestSpec(managed, selected), action: "update", reason: legacy ? "migrate manifest to schema v2" : "update manifest inventory" });
|
|
135
|
+
planLoopSelection(plan, loop);
|
|
136
|
+
return plan;
|
|
137
|
+
};
|
|
138
|
+
/** Copies each affected original into the backup and stages the new content outside the live tree. */
|
|
139
|
+
const stageUpgradedWrites = (project, writes, backup, stage, journal) => {
|
|
140
|
+
for (const item of writes) {
|
|
141
|
+
const target = safePath(project, item.path);
|
|
142
|
+
assertNoSymlinkPath(project, item.path);
|
|
143
|
+
const existed = existsSync(target);
|
|
144
|
+
journal.push({ path: item.path, existed, action: item.action });
|
|
145
|
+
if (existed) {
|
|
146
|
+
const destination = join(backup, item.path);
|
|
147
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
148
|
+
copyFileSync(target, destination);
|
|
153
149
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const path = key.includes("#") ? key.slice(0, key.lastIndexOf("#")) : key;
|
|
161
|
-
// Shared prompt blocks are upgraded through the merged plan above; never delete their whole path here.
|
|
162
|
-
if (item.strategy === "managed-block")
|
|
163
|
-
continue;
|
|
164
|
-
if (managed.some((spec) => spec.path === path))
|
|
165
|
-
continue;
|
|
166
|
-
const target = safePath(project, path);
|
|
167
|
-
if (!existsSync(target))
|
|
168
|
-
continue;
|
|
169
|
-
const current = readFileSync(target, "utf8");
|
|
170
|
-
const hash = managedContentHash({ owner: item.owner, strategy: item.strategy }, current);
|
|
171
|
-
plan.push({ path, owner: item.owner, strategy: item.strategy, ownershipClass: "managed", content: "", action: hash === item.hash ? "delete" : "conflict", reason: hash === item.hash ? "obsolete unchanged managed file" : "obsolete managed file was modified" });
|
|
150
|
+
if (item.action !== "delete") {
|
|
151
|
+
const staged = join(stage, item.path);
|
|
152
|
+
mkdirSync(dirname(staged), { recursive: true });
|
|
153
|
+
writeFileSync(staged, item.contentEncoding === "base64" ? Buffer.from(item.content, "base64") : item.content);
|
|
154
|
+
if (item.path.endsWith(".json") && item.contentEncoding !== "base64")
|
|
155
|
+
JSON.parse(item.content);
|
|
172
156
|
}
|
|
173
157
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const record of migration.records) {
|
|
190
|
-
assertNoSymlinkPath(project, record.path);
|
|
191
|
-
const target = safePath(project, record.path);
|
|
192
|
-
if (!existsSync(target))
|
|
193
|
-
plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "migrate", reason: "write immutable terminal task record" });
|
|
194
|
-
else if (readFileSync(target, "utf8") !== record.content)
|
|
195
|
-
plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "conflict", reason: "terminal task record conflicts with prior canonical content" });
|
|
196
|
-
}
|
|
197
|
-
const lessonIndex = plan.find((item) => item.path === ".agents/data/lessons/index.json");
|
|
198
|
-
if (!lessonIndex)
|
|
199
|
-
throw new Error("Lesson index plan is missing");
|
|
200
|
-
lessonIndex.content = `${JSON.stringify(migration.lessonIndex, null, 2)}\n`;
|
|
201
|
-
lessonIndex.action = "migrate";
|
|
202
|
-
lessonIndex.reason = "build derived lesson index from canonical task records";
|
|
158
|
+
};
|
|
159
|
+
const writeUpgradeJournal = (backup, journal) => {
|
|
160
|
+
mkdirSync(backup, { recursive: true });
|
|
161
|
+
writeFileSync(join(backup, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
|
|
162
|
+
};
|
|
163
|
+
const commitStagedWrites = (project, writes, stage) => {
|
|
164
|
+
for (const item of writes) {
|
|
165
|
+
const target = safePath(project, item.path);
|
|
166
|
+
if (item.action === "delete")
|
|
167
|
+
rmSync(target, { force: true });
|
|
168
|
+
else {
|
|
169
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
170
|
+
if (existsSync(target))
|
|
171
|
+
rmSync(target, { force: true });
|
|
172
|
+
renameSync(join(stage, item.path), target);
|
|
203
173
|
}
|
|
204
174
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
175
|
+
};
|
|
176
|
+
const pruneEmptyDirectories = (directory) => {
|
|
177
|
+
if (!existsSync(directory) || !lstatSync(directory).isDirectory())
|
|
178
|
+
return;
|
|
179
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }))
|
|
180
|
+
if (entry.isDirectory())
|
|
181
|
+
pruneEmptyDirectories(join(directory, entry.name));
|
|
182
|
+
if (readdirSync(directory).length === 0)
|
|
183
|
+
rmdirSync(directory);
|
|
184
|
+
};
|
|
185
|
+
/** Restores the journaled originals so a failed upgrade leaves no partial state. */
|
|
186
|
+
const restoreFromJournal = (project, journal, backup) => {
|
|
187
|
+
for (const entry of journal.reverse()) {
|
|
188
|
+
const target = safePath(project, entry.path);
|
|
189
|
+
const source = join(backup, entry.path);
|
|
190
|
+
if (entry.existed && existsSync(source)) {
|
|
191
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
192
|
+
copyFileSync(source, target);
|
|
217
193
|
}
|
|
218
|
-
|
|
219
|
-
|
|
194
|
+
else if (!entry.existed)
|
|
195
|
+
rmSync(target, { force: true });
|
|
220
196
|
}
|
|
221
|
-
const boardPath = safePath(project, ".agents/data/state/BOARD.md");
|
|
222
|
-
if (existsSync(boardPath)) {
|
|
223
|
-
const stateItem = plan.find((item) => item.path === ".agents/data/state/aidlc-state.json");
|
|
224
|
-
if (!stateItem)
|
|
225
|
-
throw new Error("Canonical state must be present before removing legacy BOARD");
|
|
226
|
-
validateState(JSON.parse(stateItem.content));
|
|
227
|
-
plan.push({ path: ".agents/data/state/BOARD.md", owner: "aidlc-state", ownershipClass: "state", content: "", action: "delete", reason: "remove duplicate lifecycle-state projection after canonical JSON validation" });
|
|
228
|
-
}
|
|
229
|
-
const manifest = manifestSpec(managed, selected);
|
|
230
|
-
plan.push({ ...manifest, action: "update", reason: legacy ? "migrate manifest to schema v2" : "update manifest inventory" });
|
|
231
|
-
planLoopSelection(plan, loop);
|
|
232
|
-
return plan;
|
|
233
197
|
};
|
|
198
|
+
/** Applies a planned upgrade transaction with backup and rollback on failure. */
|
|
234
199
|
export const applyUpgrade = (root, plan) => {
|
|
235
200
|
const conflicts = plan.filter((item) => item.action === "conflict");
|
|
236
201
|
if (conflicts.length)
|
|
@@ -244,61 +209,15 @@ export const applyUpgrade = (root, plan) => {
|
|
|
244
209
|
mkdirSync(stage, { recursive: true });
|
|
245
210
|
const journal = [];
|
|
246
211
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
journal.push({ path: item.path, existed, action: item.action });
|
|
252
|
-
if (existed) {
|
|
253
|
-
const destination = join(backup, item.path);
|
|
254
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
255
|
-
copyFileSync(target, destination);
|
|
256
|
-
}
|
|
257
|
-
if (item.action !== "delete") {
|
|
258
|
-
const staged = join(stage, item.path);
|
|
259
|
-
mkdirSync(dirname(staged), { recursive: true });
|
|
260
|
-
writeFileSync(staged, item.contentEncoding === "base64" ? Buffer.from(item.content, "base64") : item.content);
|
|
261
|
-
if (item.path.endsWith(".json") && item.contentEncoding !== "base64")
|
|
262
|
-
JSON.parse(item.content);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
mkdirSync(backup, { recursive: true });
|
|
266
|
-
writeFileSync(join(backup, "journal.json"), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
|
|
267
|
-
for (const item of writes) {
|
|
268
|
-
const target = safePath(project, item.path);
|
|
269
|
-
if (item.action === "delete")
|
|
270
|
-
rmSync(target, { force: true });
|
|
271
|
-
else {
|
|
272
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
273
|
-
if (existsSync(target))
|
|
274
|
-
rmSync(target, { force: true });
|
|
275
|
-
renameSync(join(stage, item.path), target);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const pruneEmpty = (directory) => {
|
|
279
|
-
if (!existsSync(directory) || !lstatSync(directory).isDirectory())
|
|
280
|
-
return;
|
|
281
|
-
for (const entry of readdirSync(directory, { withFileTypes: true }))
|
|
282
|
-
if (entry.isDirectory())
|
|
283
|
-
pruneEmpty(join(directory, entry.name));
|
|
284
|
-
if (readdirSync(directory).length === 0)
|
|
285
|
-
rmdirSync(directory);
|
|
286
|
-
};
|
|
287
|
-
pruneEmpty(safePath(project, ".aidlc"));
|
|
212
|
+
stageUpgradedWrites(project, writes, backup, stage, journal);
|
|
213
|
+
writeUpgradeJournal(backup, journal);
|
|
214
|
+
commitStagedWrites(project, writes, stage);
|
|
215
|
+
pruneEmptyDirectories(safePath(project, ".aidlc"));
|
|
288
216
|
rmSync(stage, { recursive: true, force: true });
|
|
289
217
|
return { backup: backupRelative, changed: writes.length };
|
|
290
218
|
}
|
|
291
219
|
catch (error) {
|
|
292
|
-
|
|
293
|
-
const target = safePath(project, entry.path);
|
|
294
|
-
const source = join(backup, entry.path);
|
|
295
|
-
if (entry.existed && existsSync(source)) {
|
|
296
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
297
|
-
copyFileSync(source, target);
|
|
298
|
-
}
|
|
299
|
-
else if (!entry.existed)
|
|
300
|
-
rmSync(target, { force: true });
|
|
301
|
-
}
|
|
220
|
+
restoreFromJournal(project, journal, backup);
|
|
302
221
|
rmSync(stage, { recursive: true, force: true });
|
|
303
222
|
throw error;
|
|
304
223
|
}
|