@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.
Files changed (150) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +14 -6
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
  11. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +245 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
  30. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
  31. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  32. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  33. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  34. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
  35. package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
  36. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  37. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  38. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  39. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  40. package/dist/assets/ui/bulma.min.css +3 -0
  41. package/dist/assets/ui/dashboard.css +414 -0
  42. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  43. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  44. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  45. package/dist/services/aidlc-coordination/package.json +9 -4
  46. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
  47. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  48. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  49. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  50. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  51. package/dist/src/adapters.d.ts +0 -3
  52. package/dist/src/adapters.js +11 -6
  53. package/dist/src/cli/argv.d.ts +12 -0
  54. package/dist/src/cli/argv.js +62 -0
  55. package/dist/src/cli/commands.d.ts +8 -0
  56. package/dist/src/cli/commands.js +129 -0
  57. package/dist/src/cli/options.d.ts +25 -0
  58. package/dist/src/cli/options.js +32 -0
  59. package/dist/src/cli/prompt.d.ts +9 -0
  60. package/dist/src/cli/prompt.js +66 -0
  61. package/dist/src/cli.js +67 -217
  62. package/dist/src/context.js +2 -0
  63. package/dist/src/gate.d.ts +1 -0
  64. package/dist/src/gate.js +51 -37
  65. package/dist/src/installer.d.ts +19 -7
  66. package/dist/src/installer.js +111 -178
  67. package/dist/src/legacy.js +1 -0
  68. package/dist/src/managed-content.d.ts +7 -0
  69. package/dist/src/managed-content.js +15 -4
  70. package/dist/src/managed-planning.d.ts +65 -0
  71. package/dist/src/managed-planning.js +219 -0
  72. package/dist/src/mcp-setup.js +3 -1
  73. package/dist/src/model/config-contracts.d.ts +65 -0
  74. package/dist/src/model/config-contracts.js +1 -0
  75. package/dist/src/model/install-contracts.d.ts +29 -0
  76. package/dist/src/model/install-contracts.js +1 -0
  77. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  78. package/dist/src/model/lifecycle-enums.js +1 -0
  79. package/dist/src/model/loop.d.ts +20 -0
  80. package/dist/src/model/loop.js +1 -0
  81. package/dist/src/model/manifest.d.ts +16 -0
  82. package/dist/src/model/manifest.js +1 -0
  83. package/dist/src/model/records.d.ts +138 -0
  84. package/dist/src/model/records.js +1 -0
  85. package/dist/src/model.d.ts +11 -264
  86. package/dist/src/model.js +1 -1
  87. package/dist/src/profiles.d.ts +8 -0
  88. package/dist/src/profiles.js +63 -8
  89. package/dist/src/project-migration.d.ts +1 -0
  90. package/dist/src/project-migration.js +7 -0
  91. package/dist/src/state/archive-record.d.ts +9 -0
  92. package/dist/src/state/archive-record.js +26 -0
  93. package/dist/src/state/commands.d.ts +18 -0
  94. package/dist/src/state/commands.js +27 -0
  95. package/dist/src/state/domain.d.ts +9 -0
  96. package/dist/src/state/domain.js +9 -0
  97. package/dist/src/state/guards.d.ts +7 -0
  98. package/dist/src/state/guards.js +17 -0
  99. package/dist/src/state/io.d.ts +10 -0
  100. package/dist/src/state/io.js +35 -0
  101. package/dist/src/state/items.d.ts +24 -0
  102. package/dist/src/state/items.js +147 -0
  103. package/dist/src/state/lessons.d.ts +7 -0
  104. package/dist/src/state/lessons.js +74 -0
  105. package/dist/src/state/lifecycle.d.ts +5 -0
  106. package/dist/src/state/lifecycle.js +78 -0
  107. package/dist/src/state/lock.d.ts +7 -0
  108. package/dist/src/state/lock.js +125 -0
  109. package/dist/src/state/memory.d.ts +8 -0
  110. package/dist/src/state/memory.js +100 -0
  111. package/dist/src/state/migrate.d.ts +17 -0
  112. package/dist/src/state/migrate.js +60 -0
  113. package/dist/src/state/partition.d.ts +15 -0
  114. package/dist/src/state/partition.js +90 -0
  115. package/dist/src/state/paths.d.ts +11 -0
  116. package/dist/src/state/paths.js +12 -0
  117. package/dist/src/state/render.d.ts +26 -0
  118. package/dist/src/state/render.js +32 -0
  119. package/dist/src/state/store.d.ts +4 -0
  120. package/dist/src/state/store.js +75 -0
  121. package/dist/src/state/transitions.d.ts +21 -0
  122. package/dist/src/state/transitions.js +95 -0
  123. package/dist/src/state/validate.d.ts +4 -0
  124. package/dist/src/state/validate.js +143 -0
  125. package/dist/src/state.d.ts +20 -117
  126. package/dist/src/state.js +20 -815
  127. package/dist/src/task-ui.d.ts +54 -0
  128. package/dist/src/task-ui.js +1570 -0
  129. package/dist/src/upgrade-board.d.ts +5 -0
  130. package/dist/src/upgrade-board.js +89 -0
  131. package/dist/src/upgrade.d.ts +3 -5
  132. package/dist/src/upgrade.js +174 -255
  133. package/dist/src/workflow.js +4 -4
  134. package/docs/README.md +6 -2
  135. package/docs/architecture.md +12 -8
  136. package/docs/command-reference.md +83 -19
  137. package/docs/configuration.md +30 -7
  138. package/docs/coordination-service.md +20 -5
  139. package/docs/design-decisions.md +83 -0
  140. package/docs/development.md +41 -2
  141. package/docs/loop-local-run.md +495 -0
  142. package/docs/loop-runbook.md +235 -0
  143. package/docs/operating-workflow.md +5 -2
  144. package/docs/testing-and-release.md +11 -7
  145. package/docs/troubleshooting.md +125 -0
  146. package/package.json +5 -2
  147. package/dist/src/ledger.d.ts +0 -22
  148. package/dist/src/ledger.js +0 -119
  149. package/dist/src/tracker.d.ts +0 -29
  150. package/dist/src/tracker.js +0 -41
@@ -0,0 +1,5 @@
1
+ import type { FileSpec, WorkflowState } from "./model.js";
2
+ export declare const migrateLegacyBoard: (root: string) => {
3
+ state: WorkflowState;
4
+ files: FileSpec[];
5
+ };
@@ -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
+ };
@@ -1,11 +1,9 @@
1
- import type { FileSpec, LoopConfig, PlannedWrite, WorkflowState } from "./model.js";
2
- export declare const migrateLegacyBoard: (root: string) => {
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;
@@ -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 { findAdapterForUpgrade, resolvedAdaptersForUpgrade } from "./adapters.js";
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 { mergeManagedBlock } from "./managed-content.js";
6
+ import { legacyManifest, managedContentHash, planLoopSelection, planManagedWrites, preserveOwnedFile, readManifest, splitInventoryKey } from "./managed-planning.js";
8
7
  import { resolveProjectPath, resolveProjectPathWithoutSymlinks } from "./project-path.js";
9
- import { emptyState, prepareV4Migration, renderWorkplan, validateState } from "./state.js";
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
- const legacyManifest = (root) => {
14
- const path = join(resolve(root), ".agents/aidlc/manifest.json");
15
- if (!existsSync(path))
16
- return false;
17
- try {
18
- const parsed = JSON.parse(readFileSync(path, "utf8"));
19
- return parsed.managedBy === "aidlc-workflow" && (parsed.schemaVersion === 0 || parsed.schemaVersion === 1);
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
- catch {
22
- return false;
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 parseFrontmatter = (content) => {
26
- const match = content.match(/^---\n([\s\S]*?)\n---/);
27
- if (!match)
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
- const row = cells(line);
47
- if (row.length < 8)
48
- throw new Error("Legacy BOARD contains a malformed task row; refusing lossy migration");
49
- const [id, title, phaseRaw, gateRaw, statusRaw, branch, areasRaw, doc] = row;
50
- migratedRows += 1;
51
- const oldDoc = safePath(root, doc);
52
- const source = existsSync(oldDoc) ? readFileSync(oldDoc, "utf8") : "";
53
- const frontmatter = parseFrontmatter(source);
54
- const stableDir = `.agents/data/tasks/${id}`;
55
- const prosePath = `${stableDir}/intent-design.md`;
56
- const workplanPath = `${stableDir}/workplan.md`;
57
- if (source)
58
- files.push({ path: prosePath, owner: "aidlc-state", ownershipClass: "state", content: source });
59
- const oldWorkplan = doc.replace(/\.md$/, ".workplan.md");
60
- if (existsSync(safePath(root, oldWorkplan)))
61
- files.push({ path: `${stableDir}/legacy-workplan.md`, owner: "aidlc-state", ownershipClass: "state", content: readFileSync(safePath(root, oldWorkplan), "utf8") });
62
- const phase = ["clarify", "plan", "build", "wrap", "done"].includes(phaseRaw) ? phaseRaw : "clarify";
63
- const gate = ["none", "G0_confirm", "G1_review", "G2_codereview"].includes(gateRaw) ? gateRaw : "G0_confirm";
64
- const evidence = [];
65
- const now = new Date().toISOString();
66
- if (["plan", "build", "wrap", "done"].includes(phase))
67
- evidence.push({ kind: "approval", gate: "G0_confirm", source: "legacy state migration", result: "pass", recordedAt: now });
68
- if (["build", "wrap", "done"].includes(phase))
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
- if (!migratedRows && !boardContent.includes("_No tasks in flight._"))
90
- throw new Error("Legacy BOARD contains no recognizable task rows; refusing lossy migration");
91
- for (const task of Object.values(state.tasks))
92
- files.push({ path: task.artifacts.workplan, owner: "aidlc-state", ownershipClass: "state", content: renderWorkplan(task) });
93
- validateState(state);
94
- return { state, files };
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
- const planManaged = (root, spec, installedHash, legacy) => {
97
- const target = safePath(root, spec.path);
98
- if (!existsSync(target))
99
- return { ...spec, action: "create", reason: "new managed file" };
100
- const current = readFileSync(target, "utf8");
101
- const next = spec.strategy === "managed-block" ? mergeManagedBlock(spec, current) : spec.content;
102
- const currentHash = managedContentHash(spec, current);
103
- const nextHash = contentHash(spec.strategy === "managed-block" ? spec.content.trimEnd() : spec.content);
104
- if (currentHash === nextHash)
105
- return { ...spec, content: next, action: next === current ? "skip" : "update", reason: "managed content already current" };
106
- if (installedHash && currentHash === installedHash)
107
- return { ...spec, content: next, action: "update", reason: "unchanged managed file" };
108
- const legacyAdapter = legacyV021AdapterContents[spec.path];
109
- const legacyAdapterHash = legacyAdapter ? contentHash(spec.strategy === "managed-block" ? legacyAdapter.trimEnd() : legacyAdapter) : undefined;
110
- if (legacy && (legacyV021Hashes[spec.path] === currentHash || legacyAdapterHash === currentHash))
111
- return { ...spec, content: next, action: "update", reason: "recognized v0.2.1 baseline" };
112
- if (spec.strategy === "managed-block" && current.includes(`<!-- aidlc-installer:${spec.owner} -->`))
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 upgradeAdapters = resolvedAdaptersForUpgrade();
123
- const requested = installed?.adapters?.length ? installed.adapters : upgradeAdapters.filter((adapter) => adapter.detect(project)).map((adapter) => adapter.id);
124
- const selected = requested.filter((id) => findAdapterForUpgrade(id));
125
- const agentFiles = selected.flatMap((id) => findAdapterForUpgrade(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
126
- const managed = [...coreWorkflowFiles(), ...agentFiles];
127
- const plan = [];
128
- const mergedBlocks = new Map();
129
- for (const spec of managed) {
130
- if (spec.strategy !== "managed-block") {
131
- plan.push(planManaged(project, spec, installedHashFor(installed?.files, spec), legacy));
132
- continue;
133
- }
134
- const previous = mergedBlocks.get(spec.path);
135
- if (!previous) {
136
- const write = planManaged(project, spec, installedHashFor(installed?.files, spec), legacy);
137
- plan.push(write);
138
- mergedBlocks.set(spec.path, write);
139
- continue;
140
- }
141
- if (!["create", "update", "skip"].includes(previous.action))
142
- continue;
143
- const target = safePath(project, spec.path);
144
- const current = existsSync(target) ? readFileSync(target, "utf8") : "";
145
- // Every owner block is checked before merging so a user edit to a later
146
- // adapter's block is reported instead of being silently overwritten.
147
- const installedBlockHash = installedHashFor(installed?.files, spec);
148
- const currentBlockHash = managedContentHash(spec, current);
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
- previous.content = mergeManagedBlock(spec, previous.content);
155
- previous.action = existsSync(target) ? (previous.content === current ? "skip" : "update") : "create";
156
- previous.reason = "managed prompt block";
157
- }
158
- if (installed) {
159
- for (const [key, item] of Object.entries(installed.files)) {
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
- for (const spec of initialProjectFiles()) {
175
- if (legacy && spec.path === ".agents/data/state/aidlc-state.json" && !existsSync(safePath(project, spec.path)))
176
- continue;
177
- const target = safePath(project, spec.path);
178
- plan.push(existsSync(target) ? { ...spec, content: readFileSync(target, "utf8"), action: "preserve", reason: `${spec.ownershipClass}-owned file` } : { ...spec, action: "create", reason: `new ${spec.ownershipClass}-owned file` });
179
- }
180
- const canonicalState = plan.find((item) => item.path === ".agents/data/state/aidlc-state.json");
181
- if (canonicalState && canonicalState.action === "preserve") {
182
- const parsed = JSON.parse(canonicalState.content);
183
- const normalized = validateState(parsed);
184
- if (parsed.schemaVersion === 1 || parsed.schemaVersion === 2 || parsed.schemaVersion === 3) {
185
- const migration = prepareV4Migration(normalized);
186
- canonicalState.content = `${JSON.stringify(migration.state, null, 2)}\n`;
187
- canonicalState.action = "migrate";
188
- 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"}`;
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
- planProjectLayoutMigration(project, plan);
206
- if (legacy && !existsSync(safePath(project, ".agents/data/state/aidlc-state.json"))) {
207
- const migration = migrateLegacyBoard(project);
208
- const compact = prepareV4Migration(migration.state);
209
- 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" });
210
- for (const record of compact.records)
211
- plan.push({ path: record.path, owner: "aidlc-state", ownershipClass: "state", content: record.content, action: "migrate", reason: "preserve legacy terminal task as immutable record" });
212
- const lessonIndex = plan.find((item) => item.path === ".agents/data/lessons/index.json");
213
- if (lessonIndex) {
214
- lessonIndex.content = `${JSON.stringify(compact.lessonIndex, null, 2)}\n`;
215
- lessonIndex.action = "migrate";
216
- lessonIndex.reason = "build derived lesson index during legacy migration";
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
- for (const file of migration.files)
219
- plan.push({ ...file, action: "migrate", reason: "preserve legacy task prose in stable task directory" });
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
- for (const item of writes) {
248
- const target = safePath(project, item.path);
249
- assertNoSymlinkPath(project, item.path);
250
- const existed = existsSync(target);
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
- for (const entry of journal.reverse()) {
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
  }