@felixdotgo/aidlc-kit 0.1.1 → 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 (134) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +11 -13
  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/scripts/lib/loop-authority.mjs +245 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +15 -3
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +40 -15
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +14 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +42 -15
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +75 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +57 -2
  28. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  29. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  30. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  31. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  32. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  33. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  34. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  35. package/dist/assets/ui/bulma.min.css +3 -0
  36. package/dist/assets/ui/dashboard.css +414 -0
  37. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  38. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  39. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  40. package/dist/services/aidlc-coordination/package.json +9 -4
  41. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +57 -2
  42. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  43. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  44. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  45. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  46. package/dist/src/adapters.d.ts +0 -3
  47. package/dist/src/adapters.js +3 -5
  48. package/dist/src/cli/argv.d.ts +12 -0
  49. package/dist/src/cli/argv.js +62 -0
  50. package/dist/src/cli/commands.d.ts +8 -0
  51. package/dist/src/cli/commands.js +129 -0
  52. package/dist/src/cli/options.d.ts +25 -0
  53. package/dist/src/cli/options.js +32 -0
  54. package/dist/src/cli/prompt.d.ts +9 -0
  55. package/dist/src/cli/prompt.js +66 -0
  56. package/dist/src/cli.js +25 -191
  57. package/dist/src/gate.js +3 -2
  58. package/dist/src/installer.d.ts +14 -7
  59. package/dist/src/installer.js +38 -141
  60. package/dist/src/legacy.js +1 -0
  61. package/dist/src/managed-content.d.ts +7 -0
  62. package/dist/src/managed-content.js +15 -4
  63. package/dist/src/managed-planning.d.ts +65 -0
  64. package/dist/src/managed-planning.js +219 -0
  65. package/dist/src/mcp-setup.js +3 -1
  66. package/dist/src/model/config-contracts.d.ts +65 -0
  67. package/dist/src/model/config-contracts.js +1 -0
  68. package/dist/src/model/install-contracts.d.ts +29 -0
  69. package/dist/src/model/install-contracts.js +1 -0
  70. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  71. package/dist/src/model/lifecycle-enums.js +1 -0
  72. package/dist/src/model/loop.d.ts +20 -0
  73. package/dist/src/model/loop.js +1 -0
  74. package/dist/src/model/manifest.d.ts +16 -0
  75. package/dist/src/model/manifest.js +1 -0
  76. package/dist/src/model/records.d.ts +138 -0
  77. package/dist/src/model/records.js +1 -0
  78. package/dist/src/model.d.ts +11 -264
  79. package/dist/src/model.js +1 -1
  80. package/dist/src/profiles.d.ts +2 -0
  81. package/dist/src/profiles.js +53 -9
  82. package/dist/src/state/archive-record.d.ts +9 -0
  83. package/dist/src/state/archive-record.js +26 -0
  84. package/dist/src/state/commands.d.ts +18 -0
  85. package/dist/src/state/commands.js +27 -0
  86. package/dist/src/state/domain.d.ts +9 -0
  87. package/dist/src/state/domain.js +9 -0
  88. package/dist/src/state/guards.d.ts +7 -0
  89. package/dist/src/state/guards.js +17 -0
  90. package/dist/src/state/io.d.ts +10 -0
  91. package/dist/src/state/io.js +35 -0
  92. package/dist/src/state/items.d.ts +3 -4
  93. package/dist/src/state/items.js +58 -57
  94. package/dist/src/state/lessons.js +14 -7
  95. package/dist/src/state/lifecycle.js +2 -2
  96. package/dist/src/state/lock.js +7 -6
  97. package/dist/src/state/memory.js +24 -12
  98. package/dist/src/state/{archive.js → migrate.js} +7 -7
  99. package/dist/src/state/partition.d.ts +15 -0
  100. package/dist/src/state/partition.js +90 -0
  101. package/dist/src/state/paths.d.ts +11 -0
  102. package/dist/src/state/paths.js +12 -0
  103. package/dist/src/state/render.js +9 -6
  104. package/dist/src/state/store.js +9 -15
  105. package/dist/src/state/transitions.js +9 -7
  106. package/dist/src/state/validate.js +50 -7
  107. package/dist/src/state.d.ts +16 -11
  108. package/dist/src/state.js +16 -11
  109. package/dist/src/task-ui.d.ts +54 -0
  110. package/dist/src/task-ui.js +1570 -0
  111. package/dist/src/upgrade-board.js +10 -10
  112. package/dist/src/upgrade.d.ts +0 -1
  113. package/dist/src/upgrade.js +19 -83
  114. package/dist/src/workflow.js +3 -3
  115. package/docs/README.md +6 -2
  116. package/docs/architecture.md +9 -8
  117. package/docs/command-reference.md +81 -20
  118. package/docs/configuration.md +28 -8
  119. package/docs/coordination-service.md +18 -6
  120. package/docs/design-decisions.md +83 -0
  121. package/docs/development.md +27 -2
  122. package/docs/loop-local-run.md +495 -0
  123. package/docs/loop-runbook.md +235 -0
  124. package/docs/operating-workflow.md +2 -2
  125. package/docs/testing-and-release.md +5 -1
  126. package/docs/troubleshooting.md +125 -0
  127. package/package.json +4 -1
  128. package/dist/src/ledger.d.ts +0 -22
  129. package/dist/src/ledger.js +0 -119
  130. package/dist/src/state/shared.d.ts +0 -29
  131. package/dist/src/state/shared.js +0 -71
  132. package/dist/src/tracker.d.ts +0 -36
  133. package/dist/src/tracker.js +0 -44
  134. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -1,7 +1,6 @@
1
- import { isAbsolute, join, resolve } from "node:path";
1
+ import { isAbsolute } from "node:path";
2
+ import { commandKit } from "./commands.js";
2
3
  import { hasReview, hasVerification, isLegacyG2Wait, repairBounds } from "./transitions.js";
3
- const rootSuffix = (root) => root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
4
- const script = (root, name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
5
4
  const isPending = (item) => item.status === "in_progress" || item.status === "todo";
6
5
  const pendingItems = (task) => task.tasks.filter(isPending);
7
6
  /** Validates execution dependencies and reports which pending items are ready to dispatch. */
@@ -28,7 +27,15 @@ export const readyTaskItems = (task) => {
28
27
  for (const item of task.tasks) {
29
28
  if (item.dependsOn !== undefined && (!Array.isArray(item.dependsOn) || new Set(item.dependsOn).size !== item.dependsOn.length || item.dependsOn.some((dep) => typeof dep !== "string" || !dep)))
30
29
  throw new Error(`Invalid execution dependencies: ${task.id}/${item.id}`);
31
- if (item.ownedPaths !== undefined && (!Array.isArray(item.ownedPaths) || new Set(item.ownedPaths).size !== item.ownedPaths.length || item.ownedPaths.some((path) => typeof path !== "string" || !path || isAbsolute(path) || path === ".." || path.startsWith("../") || path.includes("\\") || path.split("/").includes(".."))))
30
+ if (item.ownedPaths !== undefined && (!Array.isArray(item.ownedPaths) ||
31
+ new Set(item.ownedPaths).size !== item.ownedPaths.length ||
32
+ item.ownedPaths.some((path) => typeof path !== "string" ||
33
+ !path ||
34
+ isAbsolute(path) ||
35
+ path === ".." ||
36
+ path.startsWith("../") ||
37
+ path.includes("\\") ||
38
+ path.split("/").includes(".."))))
32
39
  throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
33
40
  visit(item.id);
34
41
  }
@@ -52,50 +59,27 @@ const dispatchableItem = (open, task) => {
52
59
  return open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
53
60
  };
54
61
  const noReadyItemReason = (task) => `No dependency-ready build item: ${readyTaskItems(task).blocked.map((entry) => `${entry.id} waits for ${entry.waitingOn.join(",")}`).join("; ")}`;
55
- const handoffActions = (task, suffix, stateCommand) => [
56
- ...(task.phase === "build" ? [{ id: "reopen_g1", command: `${stateCommand} task reopen ${task.id} --to plan --reason <reason> --source <explicit-user-request>${suffix}` }] : []),
57
- { id: "create_successor", command: `${stateCommand} task create <new-task-id> --title <title>${suffix}` },
58
- { id: "supersede", command: `${stateCommand} task supersede ${task.id} --successor <new-task-id> --reason <reason> --source <explicit-user-request>${suffix}` },
59
- { id: "close", command: `${stateCommand} task close ${task.id} --reason <reason> --source <explicit-user-request>${suffix}` }
60
- ];
61
- const pausedActions = (task, suffix, stateCommand) => [
62
- { id: "resume", command: `${stateCommand} task status ${task.id} --status active${suffix}` },
63
- { id: "record_handoff", command: `${stateCommand} task handoff ${task.id} --kind <kind> --reason <reason> --source <source>${suffix}` }
64
- ];
65
- const repairExhausted = (task, suffix, stateCommand) => {
62
+ const repairExhausted = (task) => {
66
63
  if (task.phase !== "build" || isLegacyG2Wait(task))
67
64
  return undefined;
68
65
  const bounds = repairBounds(task);
69
66
  if (!bounds.verifyExhausted && !bounds.reviewExhausted)
70
67
  return undefined;
71
- const kind = bounds.reviewExhausted ? "review_exhausted" : "repair_exhausted";
68
+ const handoffKind = bounds.reviewExhausted ? "review_exhausted" : "repair_exhausted";
72
69
  const detail = bounds.verifyExhausted ? `3+ failed verify cycles in: ${bounds.exhaustedAreas.join(", ")}` : `${bounds.reviewFailures} failed review passes`;
73
- return {
74
- classification: "blocked",
75
- phase: task.phase,
76
- gate: task.gate,
77
- reason: `Repair bound exhausted (${detail}); record a durable handoff`,
78
- actions: [{ id: "record_handoff", command: `${stateCommand} task handoff ${task.id} --kind ${kind} --reason <summary-of-exhausted-repairs> --source <agent>${suffix}` }]
79
- };
70
+ return { kind: "repair_exhausted", handoffKind, reason: `Repair bound exhausted (${detail}); record a durable handoff` };
80
71
  };
81
- const continueItem = (item, openCount, task, suffix, contextCommand, premature) => ({
82
- classification: "run_phase",
83
- phase: task.phase,
84
- gate: task.gate,
72
+ const continueItem = (item, openCount, premature) => ({
73
+ kind: "continue_item",
85
74
  itemId: item.id,
86
75
  remainingItems: openCount,
87
- command: `${contextCommand} ${task.id} --phase build --item ${item.id}${suffix}`,
88
76
  reason: premature ? `Invalid premature G2 wait; continue build item ${item.id}` : `Continue build item ${item.id}; ${openCount} actionable item(s) remain before verification, review, and G2`
89
77
  });
90
- const continueVerification = (task, suffix, contextCommand, premature) => ({
91
- classification: "run_phase",
92
- phase: task.phase,
93
- gate: task.gate,
94
- remainingItems: 0,
95
- command: `${contextCommand} ${task.id} --phase build${suffix}`,
78
+ const continueVerification = (premature) => ({
79
+ kind: "continue_verification",
96
80
  reason: premature ? "Invalid premature G2 wait; continue verification and adversarial review before presenting G2" : "All build items are terminal; continue verification, adversarial review, and G2 preparation"
97
81
  });
98
- const prematureG2Wait = (task, suffix, contextCommand) => {
82
+ const prematureG2Wait = (task) => {
99
83
  if (task.status !== "blocked_on_user" || task.gate !== "G2_codereview")
100
84
  return undefined;
101
85
  if (!(task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task)))
@@ -103,44 +87,61 @@ const prematureG2Wait = (task, suffix, contextCommand) => {
103
87
  const open = pendingItems(task);
104
88
  const item = dispatchableItem(open, task);
105
89
  if (item)
106
- return continueItem(item, open.length, task, suffix, contextCommand, true);
90
+ return continueItem(item, open.length, true);
107
91
  if (open.length)
108
- return { classification: "blocked", phase: task.phase, gate: task.gate, reason: noReadyItemReason(task) };
109
- return continueVerification(task, suffix, contextCommand, true);
92
+ return { kind: "no_ready_item", reason: noReadyItemReason(task) };
93
+ return continueVerification(true);
110
94
  };
111
- export const nextAction = (task, root) => {
112
- const suffix = rootSuffix(root);
113
- const stateCommand = script(root, "state.mjs");
114
- const contextCommand = script(root, "context.mjs");
95
+ /** Pure classification: maps a task onto its next-action decision without touching any command string. */
96
+ const classify = (task) => {
115
97
  if (task.phase === "done")
116
- return { classification: "complete", phase: task.phase, gate: task.gate, reason: "Task is complete" };
98
+ return { kind: "complete", reason: "Task is complete" };
117
99
  if (task.status === "closed" || task.status === "superseded")
118
- return { classification: "terminal", phase: task.phase, gate: task.gate, outcome: task.status, successorTaskId: task.successorTaskId, reason: task.closure?.reason ?? "Task ended without successful completion" };
100
+ return { kind: "terminal", outcome: task.status, successorTaskId: task.successorTaskId, reason: task.closure?.reason ?? "Task ended without successful completion" };
119
101
  if (task.handoff)
120
- return { classification: "blocked", phase: task.phase, gate: task.gate, reason: task.handoff.reason, actions: handoffActions(task, suffix, stateCommand) };
102
+ return { kind: "handoff", reason: task.handoff.reason };
121
103
  if (task.status === "paused")
122
- return { classification: "blocked", phase: task.phase, gate: task.gate, reason: "Task is paused", actions: pausedActions(task, suffix, stateCommand) };
123
- const repair = repairExhausted(task, suffix, stateCommand);
104
+ return { kind: "paused", reason: "Task is paused" };
105
+ const repair = repairExhausted(task);
124
106
  if (repair)
125
107
  return repair;
126
- const prematureG2 = prematureG2Wait(task, suffix, contextCommand);
108
+ const prematureG2 = prematureG2Wait(task);
127
109
  if (prematureG2)
128
110
  return prematureG2;
129
- if (task.status === "blocked_on_user" && task.gate === "G1_review" && task.decisions.some((decision) => decision.status === "unresolved")) {
130
- return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${contextCommand} ${task.id} --phase plan${suffix}`, reason: "Invalid premature G1 wait; resolve every decision before presenting G1" };
131
- }
111
+ if (task.status === "blocked_on_user" && task.gate === "G1_review" && task.decisions.some((decision) => decision.status === "unresolved"))
112
+ return { kind: "recover_g1_wait", reason: "Invalid premature G1 wait; resolve every decision before presenting G1" };
132
113
  if (task.status === "blocked_on_user" && task.gate === "none")
133
- return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${stateCommand} task status ${task.id} --status active${suffix}`, reason: "Recover invalid wait at a gateless wrap phase before continuing wrap" };
114
+ return { kind: "recover_gateless_wrap", reason: "Recover invalid wait at a gateless wrap phase before continuing wrap" };
134
115
  if (task.status === "blocked_on_user")
135
- return { classification: "await_user", phase: task.phase, gate: task.gate, command: `${stateCommand} gate approve ${task.id} --gate ${task.gate} --source <explicit-user-approval>${suffix}`, reason: `Explicit human approval is required at ${task.gate}` };
116
+ return { kind: "await_user", reason: `Explicit human approval is required at ${task.gate}` };
136
117
  if (task.phase === "build") {
137
118
  const open = pendingItems(task);
138
119
  const item = dispatchableItem(open, task);
139
120
  if (item)
140
- return continueItem(item, open.length, task, suffix, contextCommand, false);
121
+ return continueItem(item, open.length, false);
141
122
  if (open.length)
142
- return { classification: "blocked", phase: task.phase, gate: task.gate, reason: noReadyItemReason(task) };
143
- return continueVerification(task, suffix, contextCommand, false);
123
+ return { kind: "no_ready_item", reason: noReadyItemReason(task) };
124
+ return continueVerification(false);
125
+ }
126
+ return { kind: "continue_phase", reason: `Continue ${task.phase} until the next human gate, real blocker, or completion` };
127
+ };
128
+ /** Attaches the command strings a decision implies; the single call site of the command kit. */
129
+ const realize = (task, decision, commands) => {
130
+ const base = { phase: task.phase, gate: task.gate };
131
+ switch (decision.kind) {
132
+ case "complete": return { classification: "complete", ...base, reason: decision.reason };
133
+ case "terminal": return { classification: "terminal", ...base, outcome: decision.outcome, successorTaskId: decision.successorTaskId, reason: decision.reason };
134
+ case "handoff": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.handoffActions(task) };
135
+ case "paused": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.pausedActions(task.id) };
136
+ case "repair_exhausted": return { classification: "blocked", ...base, reason: decision.reason, actions: commands.repairHandoffAction(task.id, decision.handoffKind) };
137
+ case "continue_item": return { classification: "run_phase", ...base, itemId: decision.itemId, remainingItems: decision.remainingItems, command: commands.continueItem(task.id, decision.itemId), reason: decision.reason };
138
+ case "continue_verification": return { classification: "run_phase", ...base, remainingItems: 0, command: commands.continueVerification(task.id), reason: decision.reason };
139
+ case "no_ready_item": return { classification: "blocked", ...base, reason: decision.reason };
140
+ case "recover_g1_wait": return { classification: "run_phase", ...base, command: commands.recoverG1Wait(task.id), reason: decision.reason };
141
+ case "recover_gateless_wrap": return { classification: "run_phase", ...base, command: commands.recoverGatelessWrap(task.id), reason: decision.reason };
142
+ case "await_user": return { classification: "await_user", ...base, command: commands.approveGate(task.id, task.gate), reason: decision.reason };
143
+ case "continue_phase": return { classification: "run_phase", ...base, command: commands.continuePhase(task.id, task.phase), reason: decision.reason };
144
144
  }
145
- return { classification: "run_phase", phase: task.phase, gate: task.gate, command: `${contextCommand} ${task.id} --phase ${task.phase}${suffix}`, reason: `Continue ${task.phase} until the next human gate, real blocker, or completion` };
146
145
  };
146
+ /** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
147
+ export const nextAction = (task, root) => realize(task, classify(task), commandKit(root));
@@ -1,7 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
- import { validateLesson } from "./validate.js";
2
+ import { digest } from "./domain.js";
3
+ import { audited, tokens } from "./guards.js";
4
+ import { assertNoSymlinkPath, parseJsonFile, saveJson } from "./io.js";
5
+ import { lessonIndexPath, lessonIndexPathRelative } from "./paths.js";
3
6
  import { loadState, loadTask } from "./store.js";
4
- import { assertNoSymlinkPath, audited, digest, lessonIndexPath, parseJsonFile, tokens, writeAtomic } from "./shared.js";
7
+ import { validateLesson } from "./validate.js";
5
8
  const lessonEntry = (lesson) => ({ id: lesson.id, taskId: lesson.taskId, areas: lesson.areas, summary: lesson.summary, prevention: lesson.prevention, source: lesson.source, recordedAt: lesson.recordedAt });
6
9
  export const recordLesson = (task, lesson, recordedAt = new Date().toISOString()) => {
7
10
  if (task.phase !== "wrap" || task.status !== "active")
@@ -40,13 +43,12 @@ const allLessons = (root, state) => [
40
43
  ];
41
44
  export const rebuildLessonIndex = (root, state = loadState(root)) => {
42
45
  const index = createLessonIndex(allLessons(root, state), lessonStateDigest(state));
43
- assertNoSymlinkPath(root, ".agents/data/lessons/index.json");
44
- writeAtomic(lessonIndexPath(root), `${JSON.stringify(index, null, 2)}\n`);
46
+ saveJson(root, lessonIndexPathRelative, index);
45
47
  return index;
46
48
  };
47
49
  export const searchLessons = (root, state, query, areas = [], limit = 5) => {
48
50
  const path = lessonIndexPath(root);
49
- assertNoSymlinkPath(root, ".agents/data/lessons/index.json");
51
+ assertNoSymlinkPath(root, lessonIndexPathRelative);
50
52
  if (!existsSync(path)) {
51
53
  const hasLessons = Object.values(state.tasks).some((task) => Boolean(task.lessons?.length)) || Object.values(state.archive ?? {}).some((task) => task.lessonCount > 0);
52
54
  if (!hasLessons)
@@ -58,10 +60,15 @@ export const searchLessons = (root, state, query, areas = [], limit = 5) => {
58
60
  throw new Error("Lesson index is stale; run state.mjs lesson rebuild");
59
61
  const queryTokens = tokens(query);
60
62
  const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
61
- return index.lessons.map((item) => {
63
+ return index.lessons
64
+ .map((item) => {
62
65
  const haystack = new Set(tokens(`${item.summary} ${item.prevention} ${item.taskId} ${item.areas.join(" ")}`));
63
66
  const areaScore = item.areas.some((area) => areaSet.has(area.toLocaleLowerCase())) ? 100 : 0;
64
67
  const tokenScore = queryTokens.filter((token) => haystack.has(token)).length;
65
68
  return { item, score: areaScore + tokenScore };
66
- }).filter(({ score }) => score > 0 || (!queryTokens.length && !areaSet.size)).sort((a, b) => b.score - a.score || b.item.recordedAt.localeCompare(a.item.recordedAt)).slice(0, Math.max(1, Math.min(limit, 20))).map(({ item }) => item);
69
+ })
70
+ .filter(({ score }) => score > 0 || (!queryTokens.length && !areaSet.size))
71
+ .sort((a, b) => b.score - a.score || b.item.recordedAt.localeCompare(a.item.recordedAt))
72
+ .slice(0, Math.max(1, Math.min(limit, 20)))
73
+ .map(({ item }) => item);
67
74
  };
@@ -1,7 +1,7 @@
1
- import { handoffKinds } from "./shared.js";
1
+ import { handoffKinds, terminal } from "./domain.js";
2
+ import { audited } from "./guards.js";
2
3
  import { hasApproval } from "./transitions.js";
3
4
  import { validateState } from "./validate.js";
4
- import { audited, terminal } from "./shared.js";
5
5
  export const handoffTask = (state, id, kind, reason, source, recordedAt = new Date().toISOString()) => {
6
6
  const task = state.tasks[id];
7
7
  if (!task || terminal(task) || !handoffKinds.includes(kind))
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
4
- import { assertNoSymlinkPath } from "./shared.js";
3
+ import { dirname } from "node:path";
4
+ import { assertNoSymlinkPath } from "./io.js";
5
+ import { stateLockGuardPath, stateLockGuardPathRelative, stateLockPath, stateLockPathRelative } from "./paths.js";
5
6
  const sleep = (milliseconds) => {
6
7
  if (milliseconds > 0)
7
8
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
@@ -72,10 +73,10 @@ export const acquireStateLock = (root, options = {}) => {
72
73
  const timeoutMs = options.timeoutMs ?? 5_000;
73
74
  const retryMs = options.retryMs ?? 25;
74
75
  const staleMs = options.staleMs ?? 30_000;
75
- const lockPath = join(resolve(root), ".agents/data/state/.aidlc-state.lock");
76
- const guardPath = join(resolve(root), ".agents/data/state/.aidlc-state.lock.guard");
77
- assertNoSymlinkPath(root, ".agents/data/state/.aidlc-state.lock");
78
- assertNoSymlinkPath(root, ".agents/data/state/.aidlc-state.lock.guard");
76
+ const lockPath = stateLockPath(root);
77
+ const guardPath = stateLockGuardPath(root);
78
+ assertNoSymlinkPath(root, stateLockPathRelative);
79
+ assertNoSymlinkPath(root, stateLockGuardPathRelative);
79
80
  const token = randomUUID();
80
81
  const deadline = Date.now() + timeoutMs;
81
82
  const acquireGuard = () => {
@@ -1,10 +1,22 @@
1
- import { existsSync } from "node:fs";
1
+ import { idPattern, iso, text } from "./guards.js";
2
+ import { loadJsonOr, saveJson } from "./io.js";
3
+ import { memoryRegistryPathRelative } from "./paths.js";
2
4
  import { loadState, loadTask } from "./store.js";
3
- import { assertNoSymlinkPath, idPattern, iso, memoryRegistryPath, parseJsonFile, text, writeAtomic } from "./shared.js";
4
5
  const memoryPhases = ["clarify", "plan", "build", "wrap", "*"];
5
6
  const validateMemoryEntry = (entry) => {
6
- if (!entry || !idPattern.test(entry.id) || !text(entry.summary) || !text(entry.guidance) || !Array.isArray(entry.areas) || !entry.areas.length || entry.areas.some((area) => !text(area)) || !Array.isArray(entry.phases) || !entry.phases.length || entry.phases.some((phase) => !memoryPhases.includes(phase)) || !Number.isInteger(entry.priority) || entry.priority < 0 || entry.priority > 100 || !idPattern.test(entry.sourceTaskId) || !idPattern.test(entry.sourceLessonId) || !text(entry.approvedBy) || !iso(entry.approvedAt))
7
- throw new Error(`Invalid agentic memory entry: ${entry?.id ?? "unknown"}`);
7
+ const id = entry?.id ?? "unknown";
8
+ if (!entry)
9
+ throw new Error(`Invalid agentic memory entry: ${id}`);
10
+ if (!idPattern.test(entry.id) || !text(entry.summary) || !text(entry.guidance))
11
+ throw new Error(`Invalid agentic memory entry: ${id}`);
12
+ if (!Array.isArray(entry.areas) || !entry.areas.length || entry.areas.some((area) => !text(area)))
13
+ throw new Error(`Invalid agentic memory entry: ${id}`);
14
+ if (!Array.isArray(entry.phases) || !entry.phases.length || entry.phases.some((phase) => !memoryPhases.includes(phase)))
15
+ throw new Error(`Invalid agentic memory entry: ${id}`);
16
+ if (!Number.isInteger(entry.priority) || entry.priority < 0 || entry.priority > 100)
17
+ throw new Error(`Invalid agentic memory entry: ${id}`);
18
+ if (!idPattern.test(entry.sourceTaskId) || !idPattern.test(entry.sourceLessonId) || !text(entry.approvedBy) || !iso(entry.approvedAt))
19
+ throw new Error(`Invalid agentic memory entry: ${id}`);
8
20
  };
9
21
  export const emptyMemoryRegistry = () => ({ schemaVersion: 1, entries: [], retired: [] });
10
22
  export const validateMemoryRegistry = (value) => {
@@ -30,19 +42,17 @@ export const validateMemoryRegistry = (value) => {
30
42
  }
31
43
  return registry;
32
44
  };
33
- const registryPathRelative = ".agents/data/memory/agentic-memory.json";
34
- export const loadMemoryRegistry = (root) => {
35
- assertNoSymlinkPath(root, registryPathRelative);
36
- return existsSync(memoryRegistryPath(root)) ? validateMemoryRegistry(parseJsonFile(memoryRegistryPath(root), "Agentic memory registry")) : emptyMemoryRegistry();
37
- };
45
+ export const loadMemoryRegistry = (root) => loadJsonOr(root, memoryRegistryPathRelative, validateMemoryRegistry, emptyMemoryRegistry, "Agentic memory registry");
38
46
  export const saveMemoryRegistry = (root, registry) => {
39
47
  const normalized = validateMemoryRegistry(registry);
40
- assertNoSymlinkPath(root, registryPathRelative);
41
- writeAtomic(memoryRegistryPath(root), `${JSON.stringify(normalized, null, 2)}\n`);
48
+ saveJson(root, memoryRegistryPathRelative, normalized);
42
49
  };
43
50
  export const selectAgenticMemory = (registry, areas, phase, maxChars) => {
44
51
  const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
45
- const matches = registry.entries.filter((entry) => entry.phases.includes("*") || entry.phases.includes(phase)).filter((entry) => entry.areas.includes("*") || entry.areas.some((area) => areaSet.has(area.toLocaleLowerCase()))).sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
52
+ const matches = registry.entries
53
+ .filter((entry) => entry.phases.includes("*") || entry.phases.includes(phase))
54
+ .filter((entry) => entry.areas.includes("*") || entry.areas.some((area) => areaSet.has(area.toLocaleLowerCase())))
55
+ .sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
46
56
  const selected = [];
47
57
  let used = 0;
48
58
  for (const entry of matches) {
@@ -81,6 +91,8 @@ export const retireAgenticMemory = (root, id, reason, retiredBy, retiredAt = new
81
91
  if (index < 0)
82
92
  throw new Error(`Unknown active agentic memory entry: ${id}`);
83
93
  const [entry] = registry.entries.splice(index, 1);
94
+ if (!entry)
95
+ throw new Error(`Unknown active agentic memory entry: ${id}`);
84
96
  registry.retired.push({ ...entry, retiredBy, retiredAt, reason });
85
97
  registry.retired.sort((left, right) => right.retiredAt.localeCompare(left.retiredAt) || left.id.localeCompare(right.id));
86
98
  saveMemoryRegistry(root, registry);
@@ -1,10 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { buildArchiveRecord } from "./archive-record.js";
3
+ import { legacyG2WaitSource, terminal } from "./domain.js";
4
+ import { safeResolvedPath, writeAtomic } from "./io.js";
3
5
  import { createLessonIndex, lessonStateDigest, rebuildLessonIndex } from "./lessons.js";
4
6
  import { hasReview, hasVerification } from "./transitions.js";
5
7
  import { loadState, saveState } from "./store.js";
6
8
  import { validateState } from "./validate.js";
7
- import { archiveRecordPath, assertNoSymlinkPath, legacyG2WaitSource, taskRecordContent, terminal, writeAtomic, archivedSummary } from "./shared.js";
8
9
  /** Marks pre-existing ready G2 waits during migration; the marker is the only exemption from repair-bound enforcement. */
9
10
  export const stampLegacyG2Waits = (state) => {
10
11
  const stamped = [];
@@ -28,9 +29,9 @@ export const prepareV4Migration = (value) => {
28
29
  if (!terminal(task))
29
30
  next.tasks[task.id] = task;
30
31
  else {
31
- const content = taskRecordContent(task);
32
- next.archive[task.id] = archivedSummary(task, content);
33
- records.push({ path: archiveRecordPath(task.id), content });
32
+ const { content, summary } = buildArchiveRecord(task);
33
+ next.archive[task.id] = summary;
34
+ records.push({ path: summary.record, content });
34
35
  }
35
36
  }
36
37
  for (const item of Object.values(source.archive ?? {}))
@@ -45,8 +46,7 @@ export const migrateState = (root, state = loadState(root)) => {
45
46
  return { migrated: false, archived: [], stamped: [] };
46
47
  const migration = prepareV4Migration(state);
47
48
  for (const record of migration.records) {
48
- assertNoSymlinkPath(root, record.path);
49
- const path = join(resolve(root), record.path);
49
+ const path = safeResolvedPath(root, record.path);
50
50
  if (existsSync(path)) {
51
51
  if (readFileSync(path, "utf8") !== record.content)
52
52
  throw new Error(`Archived task record conflicts: ${record.path}`);
@@ -0,0 +1,15 @@
1
+ import type { Diagnostic, TaskState } from "../model.js";
2
+ type Item = TaskState["tasks"][number];
3
+ export declare const pathOverlaps: (left: string, right: string) => boolean;
4
+ export declare const pathsOverlap: (left?: readonly string[], right?: readonly string[]) => boolean;
5
+ /** Pairs of open (not done/deferred) items that are not ordered by `dependsOn`. */
6
+ export declare const concurrentItemPairs: (task: TaskState) => Array<[Item, Item]>;
7
+ /** Approve-plan partition check. `strict` (enabled loop) makes findings ERRORs; otherwise WARNs. */
8
+ export declare const partitionDiagnostics: (task: TaskState, { strict }?: {
9
+ strict?: boolean;
10
+ }) => Diagnostic[];
11
+ /** Strict when `loop.mode` is local/fleet or inside a fleet-enabled coordination service; a missing config is `off`; an unreadable one fails closed. */
12
+ export declare const loopPartitionStrict: (root: string | undefined) => boolean;
13
+ /** ` — owns: a, b · after: x` suffix for a rendered workplan line. */
14
+ export declare const workplanScope: (item: Item) => string;
15
+ export {};
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Parallel work partition checks, mirroring `assets/aidlc/scripts/lib/store.mjs`
3
+ * (`pathOverlaps`, `pathsOverlap`, `concurrentItemPairs`, `partitionDiagnostics`,
4
+ * `loopPartitionStrict`). Owned paths overlap by path segment (`a` overlaps
5
+ * `a/b`, not `ab`); two open items can run concurrently unless one is a direct
6
+ * or transitive dependency of the other.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ const ownedPathKey = (path) => String(path).replace(/\/+$/, "");
11
+ export const pathOverlaps = (left, right) => {
12
+ const a = ownedPathKey(left);
13
+ const b = ownedPathKey(right);
14
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
15
+ };
16
+ export const pathsOverlap = (left = [], right = []) => left.some((a) => right.some((b) => pathOverlaps(a, b)));
17
+ const itemAncestors = (task) => {
18
+ const byId = new Map(task.tasks.map((item) => [item.id, item]));
19
+ const memo = new Map();
20
+ const walk = (id, trail = new Set()) => {
21
+ const cached = memo.get(id);
22
+ if (cached)
23
+ return cached;
24
+ const found = new Set();
25
+ if (trail.has(id))
26
+ return found;
27
+ trail.add(id);
28
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
29
+ if (!byId.has(dep))
30
+ continue;
31
+ found.add(dep);
32
+ for (const deeper of walk(dep, trail))
33
+ found.add(deeper);
34
+ }
35
+ trail.delete(id);
36
+ memo.set(id, found);
37
+ return found;
38
+ };
39
+ return (id) => walk(id);
40
+ };
41
+ /** Pairs of open (not done/deferred) items that are not ordered by `dependsOn`. */
42
+ export const concurrentItemPairs = (task) => {
43
+ const open = task.tasks.filter((item) => item.status !== "done" && item.status !== "deferred");
44
+ const ancestors = itemAncestors(task);
45
+ const pairs = [];
46
+ for (let i = 0; i < open.length; i += 1)
47
+ for (let j = i + 1; j < open.length; j += 1) {
48
+ const a = open[i];
49
+ const b = open[j];
50
+ if (!ancestors(a.id).has(b.id) && !ancestors(b.id).has(a.id))
51
+ pairs.push([a, b]);
52
+ }
53
+ return pairs;
54
+ };
55
+ /** Approve-plan partition check. `strict` (enabled loop) makes findings ERRORs; otherwise WARNs. */
56
+ export const partitionDiagnostics = (task, { strict = true } = {}) => {
57
+ const level = strict ? "ERROR" : "WARN";
58
+ const diagnostics = [];
59
+ const unscoped = task.tasks.filter((item) => item.status !== "done" && item.status !== "deferred" && !(Array.isArray(item.ownedPaths) && item.ownedPaths.length));
60
+ if (unscoped.length)
61
+ diagnostics.push({ level, code: "PARTITION_SCOPE", message: `Build items need owned paths (--owns) for parallel workers: ${unscoped.map((item) => item.id).join(", ")}` });
62
+ for (const [a, b] of concurrentItemPairs(task)) {
63
+ const shared = (a.ownedPaths ?? []).filter((path) => (b.ownedPaths ?? []).some((other) => pathOverlaps(path, other)));
64
+ if (shared.length)
65
+ diagnostics.push({ level, code: "PARTITION_OVERLAP", message: `Items ${a.id} and ${b.id} can run concurrently but both own ${shared.join(", ")}; split the paths or add --depends-on` });
66
+ }
67
+ return diagnostics;
68
+ };
69
+ /** Strict when `loop.mode` is local/fleet or inside a fleet-enabled coordination service; a missing config is `off`; an unreadable one fails closed. */
70
+ export const loopPartitionStrict = (root) => {
71
+ if (process.env["MCP_FLEET_ENABLED"] === "1")
72
+ return true;
73
+ if (root === undefined)
74
+ return false;
75
+ const path = join(resolve(root), ".agents", "config.json");
76
+ if (!existsSync(path))
77
+ return false;
78
+ try {
79
+ const mode = JSON.parse(readFileSync(path, "utf8"))?.loop?.mode;
80
+ return mode === "local" || mode === "fleet";
81
+ }
82
+ catch {
83
+ return true;
84
+ }
85
+ };
86
+ /** ` — owns: a, b · after: x` suffix for a rendered workplan line. */
87
+ export const workplanScope = (item) => {
88
+ const parts = [item.ownedPaths?.length ? `owns: ${item.ownedPaths.join(", ")}` : "", item.dependsOn?.length ? `after: ${item.dependsOn.join(", ")}` : ""].filter(Boolean);
89
+ return parts.length ? ` — ${parts.join(" · ")}` : "";
90
+ };
@@ -0,0 +1,11 @@
1
+ export declare const statePathRelative = ".agents/data/state/aidlc-state.json";
2
+ export declare const lessonIndexPathRelative = ".agents/data/lessons/index.json";
3
+ export declare const memoryRegistryPathRelative = ".agents/data/memory/agentic-memory.json";
4
+ export declare const stateLockPathRelative = ".agents/data/state/.aidlc-state.lock";
5
+ export declare const stateLockGuardPathRelative = ".agents/data/state/.aidlc-state.lock.guard";
6
+ export declare const statePath: (root: string) => string;
7
+ export declare const lessonIndexPath: (root: string) => string;
8
+ export declare const memoryRegistryPath: (root: string) => string;
9
+ export declare const stateLockPath: (root: string) => string;
10
+ export declare const stateLockGuardPath: (root: string) => string;
11
+ export declare const archiveRecordPath: (id: string) => string;
@@ -0,0 +1,12 @@
1
+ import { join, resolve } from "node:path";
2
+ export const statePathRelative = ".agents/data/state/aidlc-state.json";
3
+ export const lessonIndexPathRelative = ".agents/data/lessons/index.json";
4
+ export const memoryRegistryPathRelative = ".agents/data/memory/agentic-memory.json";
5
+ export const stateLockPathRelative = ".agents/data/state/.aidlc-state.lock";
6
+ export const stateLockGuardPathRelative = ".agents/data/state/.aidlc-state.lock.guard";
7
+ export const statePath = (root) => join(resolve(root), statePathRelative);
8
+ export const lessonIndexPath = (root) => join(resolve(root), lessonIndexPathRelative);
9
+ export const memoryRegistryPath = (root) => join(resolve(root), memoryRegistryPathRelative);
10
+ export const stateLockPath = (root) => join(resolve(root), stateLockPathRelative);
11
+ export const stateLockGuardPath = (root) => join(resolve(root), stateLockGuardPathRelative);
12
+ export const archiveRecordPath = (id) => `.agents/data/state/archive/${id}.json`;
@@ -1,12 +1,16 @@
1
- import { join, resolve } from "node:path";
1
+ import { tokens } from "./guards.js";
2
+ import { safeResolvedPath, writeAtomic } from "./io.js";
2
3
  import { loadState } from "./store.js";
3
- import { assertNoSymlinkPath, tokens, writeAtomic } from "./shared.js";
4
+ import { workplanScope } from "./partition.js";
4
5
  export const listTaskSummaries = (state, options = {}) => {
5
6
  const active = Object.values(state.tasks).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "active" }));
6
7
  const archived = options.includeArchive ? Object.values(state.archive ?? {}).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "archive" })) : [];
7
8
  const queryTokens = tokens(options.query ?? "");
8
9
  const statuses = new Set(options.statuses ?? []);
9
- const filtered = [...active, ...archived].filter((item) => (!statuses.size || statuses.has(item.status)) && (!queryTokens.length || queryTokens.every((token) => tokens(`${item.id} ${item.title} ${item.areas.join(" ")}`).includes(token)))).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
10
+ const filtered = [...active, ...archived]
11
+ .filter((item) => (!statuses.size || statuses.has(item.status)) &&
12
+ (!queryTokens.length || queryTokens.every((token) => tokens(`${item.id} ${item.title} ${item.areas.join(" ")}`).includes(token))))
13
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
10
14
  const cursor = Math.max(0, options.cursor ?? 0);
11
15
  const limit = Math.max(1, Math.min(options.limit ?? 20, 100));
12
16
  const items = filtered.slice(cursor, cursor + limit);
@@ -15,7 +19,7 @@ export const listTaskSummaries = (state, options = {}) => {
15
19
  /** Renders the markdown workplan for a task. */
16
20
  export const renderWorkplan = (task) => {
17
21
  const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None";
18
- const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}`).join("\n") : "- None";
22
+ const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}${workplanScope(item)}`).join("\n") : "- None";
19
23
  const lifecycle = [`- Status: \`${task.status}\``, task.handoff ? `- Handoff: \`${task.handoff.kind}\` — ${task.handoff.reason}` : "", task.closure ? `- Closure: ${task.closure.reason}` : "", task.predecessorTaskId ? `- Predecessor: \`${task.predecessorTaskId}\`` : "", task.successorTaskId ? `- Successor: \`${task.successorTaskId}\`` : "", task.lessonDisposition ? `- Lessons: \`${task.lessonDisposition.status}\`` : ""].filter(Boolean).join("\n");
20
24
  return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (Approve plan — before implementation)\n${decisions}\n\n## 🧩 Tasks (Review implementation — work items)\n${tasks}\n`;
21
25
  };
@@ -23,7 +27,6 @@ export const renderViews = (root, state = loadState(root), selected = Object.val
23
27
  for (const task of selected) {
24
28
  if (!task.artifacts.workplan)
25
29
  continue;
26
- assertNoSymlinkPath(root, task.artifacts.workplan);
27
- writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task));
30
+ writeAtomic(safeResolvedPath(root, task.artifacts.workplan), renderWorkplan(task));
28
31
  }
29
32
  };
@@ -1,20 +1,17 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { buildArchiveRecord } from "./archive-record.js";
3
+ import { digest, emptyState, terminal } from "./domain.js";
4
+ import { loadJsonOr, parseJsonText, safeResolvedPath, saveJson, writeAtomic } from "./io.js";
5
+ import { statePath, statePathRelative } from "./paths.js";
3
6
  import { validateState, validateTask } from "./validate.js";
4
- import { assertNoSymlinkPath, digest, emptyState, parseJsonFile, parseJsonText, statePath, taskRecordContent, terminal, writeAtomic, archivedSummary } from "./shared.js";
5
- export const loadState = (root) => {
6
- const path = statePath(root);
7
- assertNoSymlinkPath(root, ".agents/data/state/aidlc-state.json");
8
- return existsSync(path) ? validateState(parseJsonFile(path, "AI-DLC state file", "restore it from git history or the archive records")) : emptyState();
9
- };
7
+ export const loadState = (root) => loadJsonOr(root, statePathRelative, validateState, emptyState, "AI-DLC state file", "restore it from git history or the archive records");
10
8
  export const loadTask = (root, id, state = loadState(root)) => {
11
9
  if (state.tasks[id])
12
10
  return state.tasks[id];
13
11
  const summary = state.archive?.[id];
14
12
  if (!summary)
15
13
  return undefined;
16
- const path = join(resolve(root), summary.record);
17
- assertNoSymlinkPath(root, summary.record);
14
+ const path = safeResolvedPath(root, summary.record);
18
15
  if (!existsSync(path))
19
16
  throw new Error(`Archived task record is missing: ${summary.record}`);
20
17
  const content = readFileSync(path, "utf8");
@@ -32,10 +29,8 @@ const archiveTerminalTasks = (normalized, persisted, root) => {
32
29
  for (const task of Object.values(normalized.tasks)) {
33
30
  if (!terminal(task))
34
31
  continue;
35
- const content = taskRecordContent(task);
36
- const summary = archivedSummary(task, content);
37
- const path = join(resolve(root), summary.record);
38
- assertNoSymlinkPath(root, summary.record);
32
+ const { content, summary } = buildArchiveRecord(task);
33
+ const path = safeResolvedPath(root, summary.record);
39
34
  if (existsSync(path)) {
40
35
  const current = readFileSync(path, "utf8");
41
36
  if (digest(current) !== summary.digest)
@@ -72,8 +67,7 @@ export const saveState = (root, state) => {
72
67
  const archived = normalized.schemaVersion >= 3 ? archiveTerminalTasks(normalized, persisted, root) : [];
73
68
  if (normalized.schemaVersion >= 3)
74
69
  validateState(normalized);
75
- assertNoSymlinkPath(root, ".agents/data/state/aidlc-state.json");
76
- writeAtomic(statePath(root), `${JSON.stringify(normalized, null, 2)}\n`);
70
+ saveJson(root, statePathRelative, normalized);
77
71
  state.tasks = normalized.tasks;
78
72
  state.archive = normalized.archive;
79
73
  state.schemaVersion = normalized.schemaVersion;
@@ -1,8 +1,8 @@
1
- import { phases, phaseGate, terminal } from "./shared.js";
1
+ import { phaseGate, phases, terminal } from "./domain.js";
2
2
  export const latestApproval = (task, gate) => {
3
3
  for (let index = task.evidence.length - 1; index >= 0; index -= 1) {
4
4
  const item = task.evidence[index];
5
- if (item.kind === "approval" && item.gate === gate)
5
+ if (item && item.kind === "approval" && item.gate === gate)
6
6
  return item;
7
7
  }
8
8
  return undefined;
@@ -12,7 +12,7 @@ export const buildBoundary = (task) => {
12
12
  let boundary;
13
13
  for (let index = 0; index < task.evidence.length; index += 1) {
14
14
  const item = task.evidence[index];
15
- if (item.kind === "approval" && item.gate === "G1_review" && item.result === "pass")
15
+ if (item && item.kind === "approval" && item.gate === "G1_review" && item.result === "pass")
16
16
  boundary = index;
17
17
  }
18
18
  return boundary;
@@ -23,9 +23,11 @@ const currentBuildEvidence = (task) => {
23
23
  return boundary === undefined ? [] : task.evidence.slice(boundary + 1);
24
24
  };
25
25
  const latestInOrder = (items, predicate) => {
26
- for (let index = items.length - 1; index >= 0; index -= 1)
27
- if (predicate(items[index]))
28
- return items[index];
26
+ for (let index = items.length - 1; index >= 0; index -= 1) {
27
+ const item = items[index];
28
+ if (item && predicate(item))
29
+ return item;
30
+ }
29
31
  return undefined;
30
32
  };
31
33
  export const hasCurrentBuildApproval = (task, gate) => latestInOrder(currentBuildEvidence(task), (item) => item.kind === "approval" && item.gate === gate)?.result === "pass";
@@ -45,7 +47,7 @@ export const repairBounds = (task) => {
45
47
  verifyFailures[area] = (verifyFailures[area] ?? 0) + 1;
46
48
  }
47
49
  const reviewFailures = evidence.filter((item) => item.kind === "review" && item.result === "fail").length;
48
- const exhaustedAreas = Object.keys(verifyFailures).filter((area) => verifyFailures[area] >= 3).sort();
50
+ const exhaustedAreas = Object.keys(verifyFailures).filter((area) => (verifyFailures[area] ?? 0) >= 3).sort();
49
51
  return { verifyFailures, reviewFailures, exhaustedAreas, verifyExhausted: exhaustedAreas.length > 0, reviewExhausted: reviewFailures >= 2 };
50
52
  };
51
53
  /** True only for a migration-stamped legacy G2 wait that is still fully ready; the stamp is written exclusively by schema migration. */