@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,35 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { resolveProjectPathWithoutSymlinks } from "../project-path.js";
4
+ export const assertNoSymlinkPath = (root, relative) => {
5
+ resolveProjectPathWithoutSymlinks(root, relative, "Unsafe state path", "State path crosses a symlink");
6
+ };
7
+ export const writeAtomic = (path, content) => {
8
+ mkdirSync(dirname(path), { recursive: true });
9
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
10
+ writeFileSync(temporary, content, "utf8");
11
+ renameSync(temporary, path);
12
+ };
13
+ export const parseJsonText = (content, label, recovery = "") => {
14
+ try {
15
+ return JSON.parse(content);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`${label} is corrupted or unreadable: ${error instanceof Error ? error.message : String(error)}${recovery ? `; ${recovery}` : ""}`);
19
+ }
20
+ };
21
+ export const parseJsonFile = (path, label, recovery = "") => parseJsonText(readFileSync(path, "utf8"), label, recovery);
22
+ /** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
23
+ export const safeResolvedPath = (root, relative) => {
24
+ assertNoSymlinkPath(root, relative);
25
+ return join(resolve(root), relative);
26
+ };
27
+ /** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
28
+ export const loadJsonOr = (root, relative, validate, fallback, label, recovery = "") => {
29
+ const path = safeResolvedPath(root, relative);
30
+ return existsSync(path) ? validate(parseJsonFile(path, label, recovery)) : fallback();
31
+ };
32
+ /** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
33
+ export const saveJson = (root, relative, value) => {
34
+ writeAtomic(safeResolvedPath(root, relative), `${JSON.stringify(value, null, 2)}\n`);
35
+ };
@@ -0,0 +1,24 @@
1
+ import type { Gate, Phase, TaskState } from "../model.js";
2
+ import { type NextActionActions } from "./commands.js";
3
+ export interface NextAction {
4
+ classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
5
+ phase: Phase;
6
+ gate: Gate;
7
+ command?: string;
8
+ outcome?: "closed" | "superseded";
9
+ successorTaskId?: string;
10
+ itemId?: string;
11
+ remainingItems?: number;
12
+ actions?: NextActionActions;
13
+ reason: string;
14
+ }
15
+ /** Validates execution dependencies and reports which pending items are ready to dispatch. */
16
+ export declare const readyTaskItems: (task: TaskState) => {
17
+ ready: string[];
18
+ blocked: Array<{
19
+ id: string;
20
+ waitingOn: string[];
21
+ }>;
22
+ };
23
+ /** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
24
+ export declare const nextAction: (task: TaskState, root?: string) => NextAction;
@@ -0,0 +1,147 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { commandKit } from "./commands.js";
3
+ import { hasReview, hasVerification, isLegacyG2Wait, repairBounds } from "./transitions.js";
4
+ const isPending = (item) => item.status === "in_progress" || item.status === "todo";
5
+ const pendingItems = (task) => task.tasks.filter(isPending);
6
+ /** Validates execution dependencies and reports which pending items are ready to dispatch. */
7
+ export const readyTaskItems = (task) => {
8
+ const byId = new Map(task.tasks.map((item) => [item.id, item]));
9
+ if (byId.size !== task.tasks.length)
10
+ throw new Error(`Duplicate execution item: ${task.id}`);
11
+ const visiting = new Set();
12
+ const visited = new Set();
13
+ const visit = (id) => {
14
+ if (visiting.has(id))
15
+ throw new Error(`Cyclic execution dependency: ${task.id}/${id}`);
16
+ if (visited.has(id))
17
+ return;
18
+ visiting.add(id);
19
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
20
+ if (dep === id || !byId.has(dep))
21
+ throw new Error(`Unknown execution dependency: ${task.id}/${id}/${dep}`);
22
+ visit(dep);
23
+ }
24
+ visiting.delete(id);
25
+ visited.add(id);
26
+ };
27
+ for (const item of task.tasks) {
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)))
29
+ throw new Error(`Invalid execution dependencies: ${task.id}/${item.id}`);
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(".."))))
39
+ throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
40
+ visit(item.id);
41
+ }
42
+ const ready = [];
43
+ const blocked = [];
44
+ for (const item of task.tasks) {
45
+ const waitingOn = (item.dependsOn ?? []).filter((dep) => byId.get(dep)?.status !== "done");
46
+ if (item.status === "done" && waitingOn.length)
47
+ throw new Error(`Completed item has incomplete prerequisites: ${task.id}/${item.id}`);
48
+ if (!isPending(item))
49
+ continue;
50
+ if (waitingOn.length)
51
+ blocked.push({ id: item.id, waitingOn });
52
+ else
53
+ ready.push(item.id);
54
+ }
55
+ return { ready, blocked };
56
+ };
57
+ const dispatchableItem = (open, task) => {
58
+ const ready = new Set(readyTaskItems(task).ready);
59
+ return open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
60
+ };
61
+ const noReadyItemReason = (task) => `No dependency-ready build item: ${readyTaskItems(task).blocked.map((entry) => `${entry.id} waits for ${entry.waitingOn.join(",")}`).join("; ")}`;
62
+ const repairExhausted = (task) => {
63
+ if (task.phase !== "build" || isLegacyG2Wait(task))
64
+ return undefined;
65
+ const bounds = repairBounds(task);
66
+ if (!bounds.verifyExhausted && !bounds.reviewExhausted)
67
+ return undefined;
68
+ const handoffKind = bounds.reviewExhausted ? "review_exhausted" : "repair_exhausted";
69
+ const detail = bounds.verifyExhausted ? `3+ failed verify cycles in: ${bounds.exhaustedAreas.join(", ")}` : `${bounds.reviewFailures} failed review passes`;
70
+ return { kind: "repair_exhausted", handoffKind, reason: `Repair bound exhausted (${detail}); record a durable handoff` };
71
+ };
72
+ const continueItem = (item, openCount, premature) => ({
73
+ kind: "continue_item",
74
+ itemId: item.id,
75
+ remainingItems: openCount,
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`
77
+ });
78
+ const continueVerification = (premature) => ({
79
+ kind: "continue_verification",
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"
81
+ });
82
+ const prematureG2Wait = (task) => {
83
+ if (task.status !== "blocked_on_user" || task.gate !== "G2_codereview")
84
+ return undefined;
85
+ if (!(task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task)))
86
+ return undefined;
87
+ const open = pendingItems(task);
88
+ const item = dispatchableItem(open, task);
89
+ if (item)
90
+ return continueItem(item, open.length, true);
91
+ if (open.length)
92
+ return { kind: "no_ready_item", reason: noReadyItemReason(task) };
93
+ return continueVerification(true);
94
+ };
95
+ /** Pure classification: maps a task onto its next-action decision without touching any command string. */
96
+ const classify = (task) => {
97
+ if (task.phase === "done")
98
+ return { kind: "complete", reason: "Task is complete" };
99
+ if (task.status === "closed" || task.status === "superseded")
100
+ return { kind: "terminal", outcome: task.status, successorTaskId: task.successorTaskId, reason: task.closure?.reason ?? "Task ended without successful completion" };
101
+ if (task.handoff)
102
+ return { kind: "handoff", reason: task.handoff.reason };
103
+ if (task.status === "paused")
104
+ return { kind: "paused", reason: "Task is paused" };
105
+ const repair = repairExhausted(task);
106
+ if (repair)
107
+ return repair;
108
+ const prematureG2 = prematureG2Wait(task);
109
+ if (prematureG2)
110
+ return prematureG2;
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" };
113
+ if (task.status === "blocked_on_user" && task.gate === "none")
114
+ return { kind: "recover_gateless_wrap", reason: "Recover invalid wait at a gateless wrap phase before continuing wrap" };
115
+ if (task.status === "blocked_on_user")
116
+ return { kind: "await_user", reason: `Explicit human approval is required at ${task.gate}` };
117
+ if (task.phase === "build") {
118
+ const open = pendingItems(task);
119
+ const item = dispatchableItem(open, task);
120
+ if (item)
121
+ return continueItem(item, open.length, false);
122
+ if (open.length)
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
+ }
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));
@@ -0,0 +1,7 @@
1
+ import type { LessonIndex, LessonIndexEntry, LessonRecord, TaskState, WorkflowState } from "../model.js";
2
+ export declare const recordLesson: (task: TaskState, lesson: Omit<LessonRecord, "taskId" | "recordedAt">, recordedAt?: string) => LessonRecord;
3
+ export declare const recordNoLessons: (task: TaskState, reason: string, source: string, recordedAt?: string) => void;
4
+ export declare const lessonStateDigest: (state: WorkflowState) => string;
5
+ export declare const createLessonIndex: (lessons: LessonRecord[], sourceDigest?: string) => LessonIndex;
6
+ export declare const rebuildLessonIndex: (root: string, state?: WorkflowState) => LessonIndex;
7
+ export declare const searchLessons: (root: string, state: WorkflowState, query: string, areas?: string[], limit?: number) => LessonIndexEntry[];
@@ -0,0 +1,74 @@
1
+ import { existsSync } from "node:fs";
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";
6
+ import { loadState, loadTask } from "./store.js";
7
+ import { validateLesson } from "./validate.js";
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 });
9
+ export const recordLesson = (task, lesson, recordedAt = new Date().toISOString()) => {
10
+ if (task.phase !== "wrap" || task.status !== "active")
11
+ throw new Error(`Lessons can only be recorded during active wrap: ${task.id}`);
12
+ const current = task.lessons?.find((item) => item.id === lesson.id);
13
+ const next = { ...lesson, taskId: task.id, recordedAt };
14
+ validateLesson(task.id, next);
15
+ if (current) {
16
+ if (JSON.stringify({ ...current, recordedAt: undefined }) === JSON.stringify({ ...next, recordedAt: undefined }))
17
+ return current;
18
+ throw new Error(`Lesson already exists with different content: ${task.id}/${lesson.id}`);
19
+ }
20
+ task.lessons ??= [];
21
+ task.lessons.push(next);
22
+ task.lessonDisposition = { status: "captured", source: lesson.source, recordedAt };
23
+ task.updatedAt = recordedAt;
24
+ return next;
25
+ };
26
+ export const recordNoLessons = (task, reason, source, recordedAt = new Date().toISOString()) => {
27
+ if (task.phase !== "wrap" || task.status !== "active")
28
+ throw new Error(`Lesson disposition can only be recorded during active wrap: ${task.id}`);
29
+ audited(reason, source);
30
+ if (task.lessons?.length)
31
+ throw new Error(`Task already has captured lessons: ${task.id}`);
32
+ task.lessonDisposition = { status: "none", reason, source, recordedAt };
33
+ task.updatedAt = recordedAt;
34
+ };
35
+ export const lessonStateDigest = (state) => digest(JSON.stringify({
36
+ active: Object.values(state.tasks).flatMap((task) => task.lessons ?? []).sort((a, b) => `${a.taskId}:${a.id}`.localeCompare(`${b.taskId}:${b.id}`)),
37
+ archive: Object.values(state.archive ?? {}).map((item) => ({ id: item.id, digest: item.digest, lessonCount: item.lessonCount })).sort((a, b) => a.id.localeCompare(b.id))
38
+ }));
39
+ export const createLessonIndex = (lessons, sourceDigest) => ({ schemaVersion: 1, sourceDigest: sourceDigest ?? digest(JSON.stringify(lessons)), lessons: lessons.map(lessonEntry).sort((a, b) => b.recordedAt.localeCompare(a.recordedAt) || a.taskId.localeCompare(b.taskId) || a.id.localeCompare(b.id)) });
40
+ const allLessons = (root, state) => [
41
+ ...Object.values(state.tasks).flatMap((task) => task.lessons ?? []),
42
+ ...Object.keys(state.archive ?? {}).flatMap((id) => loadTask(root, id, state)?.lessons ?? [])
43
+ ];
44
+ export const rebuildLessonIndex = (root, state = loadState(root)) => {
45
+ const index = createLessonIndex(allLessons(root, state), lessonStateDigest(state));
46
+ saveJson(root, lessonIndexPathRelative, index);
47
+ return index;
48
+ };
49
+ export const searchLessons = (root, state, query, areas = [], limit = 5) => {
50
+ const path = lessonIndexPath(root);
51
+ assertNoSymlinkPath(root, lessonIndexPathRelative);
52
+ if (!existsSync(path)) {
53
+ const hasLessons = Object.values(state.tasks).some((task) => Boolean(task.lessons?.length)) || Object.values(state.archive ?? {}).some((task) => task.lessonCount > 0);
54
+ if (!hasLessons)
55
+ return [];
56
+ throw new Error("Lesson index is missing; run state.mjs lesson rebuild");
57
+ }
58
+ const index = parseJsonFile(path, "Lesson index", "run state.mjs lesson rebuild");
59
+ if (index.schemaVersion !== 1 || !Array.isArray(index.lessons) || index.sourceDigest !== lessonStateDigest(state))
60
+ throw new Error("Lesson index is stale; run state.mjs lesson rebuild");
61
+ const queryTokens = tokens(query);
62
+ const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
63
+ return index.lessons
64
+ .map((item) => {
65
+ const haystack = new Set(tokens(`${item.summary} ${item.prevention} ${item.taskId} ${item.areas.join(" ")}`));
66
+ const areaScore = item.areas.some((area) => areaSet.has(area.toLocaleLowerCase())) ? 100 : 0;
67
+ const tokenScore = queryTokens.filter((token) => haystack.has(token)).length;
68
+ return { item, score: areaScore + tokenScore };
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);
74
+ };
@@ -0,0 +1,5 @@
1
+ import type { HandoffKind, TaskState, WorkflowState } from "../model.js";
2
+ export declare const handoffTask: (state: WorkflowState, id: string, kind: HandoffKind, reason: string, source: string, recordedAt?: string) => TaskState;
3
+ export declare const closeTask: (state: WorkflowState, id: string, reason: string, source: string, recordedAt?: string) => TaskState;
4
+ export declare const supersedeTask: (state: WorkflowState, id: string, successorId: string, reason: string, source: string, recordedAt?: string) => TaskState;
5
+ export declare const reopenTask: (state: WorkflowState, id: string, target: "plan", reason: string, source: string, recordedAt?: string) => TaskState;
@@ -0,0 +1,78 @@
1
+ import { handoffKinds, terminal } from "./domain.js";
2
+ import { audited } from "./guards.js";
3
+ import { hasApproval } from "./transitions.js";
4
+ import { validateState } from "./validate.js";
5
+ export const handoffTask = (state, id, kind, reason, source, recordedAt = new Date().toISOString()) => {
6
+ const task = state.tasks[id];
7
+ if (!task || terminal(task) || !handoffKinds.includes(kind))
8
+ throw new Error(`Task cannot enter handoff: ${id}`);
9
+ audited(reason, source);
10
+ if (task.handoff) {
11
+ if (task.handoff.kind === kind && task.handoff.reason === reason && task.handoff.source === source)
12
+ return task;
13
+ throw new Error(`Task already has a different handoff: ${id}`);
14
+ }
15
+ task.status = "paused";
16
+ task.handoff = { kind, reason, source, recordedAt };
17
+ delete task.legacyG2Wait;
18
+ task.updatedAt = recordedAt;
19
+ return task;
20
+ };
21
+ export const closeTask = (state, id, reason, source, recordedAt = new Date().toISOString()) => {
22
+ const task = state.tasks[id];
23
+ if (!task || task.status === "done" || task.status === "superseded")
24
+ throw new Error(`Task cannot be closed: ${id}`);
25
+ audited(reason, source);
26
+ if (task.status === "closed") {
27
+ if (task.closure?.reason === reason && task.closure.source === source)
28
+ return task;
29
+ throw new Error(`Task is already closed with different metadata: ${id}`);
30
+ }
31
+ task.status = "closed";
32
+ task.closure = { reason, source, recordedAt };
33
+ delete task.handoff;
34
+ delete task.successorTaskId;
35
+ delete task.legacyG2Wait;
36
+ task.updatedAt = recordedAt;
37
+ return task;
38
+ };
39
+ export const supersedeTask = (state, id, successorId, reason, source, recordedAt = new Date().toISOString()) => {
40
+ const task = state.tasks[id];
41
+ const successor = state.tasks[successorId];
42
+ audited(reason, source);
43
+ if (!task || task.status === "done" || task.status === "closed" || !successor || id === successorId)
44
+ throw new Error(`Task cannot be superseded: ${id}`);
45
+ if (task.status === "superseded") {
46
+ if (task.successorTaskId === successorId && task.closure?.reason === reason && task.closure.source === source)
47
+ return task;
48
+ throw new Error(`Task is already superseded with different metadata: ${id}`);
49
+ }
50
+ if (successor.phase !== "clarify" || successor.gate !== "G0_confirm" || terminal(successor) || hasApproval(successor, "G0_confirm") || (successor.predecessorTaskId && successor.predecessorTaskId !== id))
51
+ throw new Error(`Successor must be a fresh pre-G0 task: ${successorId}`);
52
+ task.status = "superseded";
53
+ task.closure = { reason, source, recordedAt };
54
+ task.successorTaskId = successorId;
55
+ delete task.handoff;
56
+ delete task.legacyG2Wait;
57
+ task.updatedAt = recordedAt;
58
+ successor.predecessorTaskId = id;
59
+ successor.updatedAt = recordedAt;
60
+ validateState(state);
61
+ return task;
62
+ };
63
+ export const reopenTask = (state, id, target, reason, source, recordedAt = new Date().toISOString()) => {
64
+ const task = state.tasks[id];
65
+ audited(reason, source);
66
+ if (!task || target !== "plan" || task.phase !== "build" || task.status !== "paused" || !task.handoff)
67
+ throw new Error(`Task cannot reopen G1: ${id}`);
68
+ task.evidence.push({ kind: "approval", gate: "G1_review", result: "fail", source, detail: `Reopened G1: ${reason}`, recordedAt });
69
+ for (const item of task.tasks)
70
+ if (item.status !== "deferred")
71
+ item.status = "todo";
72
+ task.phase = "plan";
73
+ task.gate = "G1_review";
74
+ task.status = "active";
75
+ delete task.handoff;
76
+ task.updatedAt = recordedAt;
77
+ return task;
78
+ };
@@ -0,0 +1,7 @@
1
+ export interface StateLockOptions {
2
+ timeoutMs?: number;
3
+ retryMs?: number;
4
+ staleMs?: number;
5
+ }
6
+ /** Serializes lifecycle script mutations across local Node.js processes. */
7
+ export declare const acquireStateLock: (root: string, options?: StateLockOptions) => (() => void);
@@ -0,0 +1,125 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { assertNoSymlinkPath } from "./io.js";
5
+ import { stateLockGuardPath, stateLockGuardPathRelative, stateLockPath, stateLockPathRelative } from "./paths.js";
6
+ const sleep = (milliseconds) => {
7
+ if (milliseconds > 0)
8
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
9
+ };
10
+ const lockRecord = (path) => {
11
+ try {
12
+ const value = JSON.parse(readFileSync(path, "utf8"));
13
+ return typeof value.token === "string" && typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.createdAt === "number" && Number.isFinite(value.createdAt) ? value : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ } /* intentional: unreadable lock record is treated as absent */
18
+ };
19
+ const processAlive = (pid) => {
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ }
24
+ catch (error) {
25
+ return error.code === "EPERM";
26
+ }
27
+ };
28
+ const tryCreateLock = (path, record) => {
29
+ try {
30
+ mkdirSync(dirname(path), { recursive: true });
31
+ const descriptor = openSync(path, "wx", 0o600);
32
+ try {
33
+ writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
34
+ }
35
+ finally {
36
+ closeSync(descriptor);
37
+ }
38
+ return true;
39
+ }
40
+ catch (error) {
41
+ if (error.code === "EEXIST")
42
+ return false;
43
+ throw error;
44
+ }
45
+ };
46
+ const removeOwnedLock = (path, token) => {
47
+ if (lockRecord(path)?.token === token)
48
+ unlinkSync(path);
49
+ };
50
+ const lockExpired = (record, now, staleMs) => Boolean(record && now - record.createdAt >= staleMs && !processAlive(record.pid));
51
+ const releaseOwnedLock = (lockRecordOrNull, lockPath, token) => {
52
+ if (!lockRecordOrNull) {
53
+ // A corrupt/unreadable lock record must not wedge release; reclaim best-effort.
54
+ if (existsSync(lockPath)) {
55
+ try {
56
+ unlinkSync(lockPath);
57
+ }
58
+ catch { /* best-effort cleanup of a corrupt lock */ }
59
+ }
60
+ return;
61
+ }
62
+ if (lockRecordOrNull.token !== token)
63
+ return;
64
+ try {
65
+ unlinkSync(lockPath);
66
+ }
67
+ catch (error) {
68
+ throw new Error(`Failed to release owned AI-DLC state lock: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ };
71
+ /** Serializes lifecycle script mutations across local Node.js processes. */
72
+ export const acquireStateLock = (root, options = {}) => {
73
+ const timeoutMs = options.timeoutMs ?? 5_000;
74
+ const retryMs = options.retryMs ?? 25;
75
+ const staleMs = options.staleMs ?? 30_000;
76
+ const lockPath = stateLockPath(root);
77
+ const guardPath = stateLockGuardPath(root);
78
+ assertNoSymlinkPath(root, stateLockPathRelative);
79
+ assertNoSymlinkPath(root, stateLockGuardPathRelative);
80
+ const token = randomUUID();
81
+ const deadline = Date.now() + timeoutMs;
82
+ const acquireGuard = () => {
83
+ const guardToken = randomUUID();
84
+ const now = Date.now();
85
+ const existing = lockRecord(guardPath);
86
+ if (existing === undefined && existsSync(guardPath)) {
87
+ try {
88
+ unlinkSync(guardPath);
89
+ }
90
+ catch { /* best-effort reclamation of a corrupt guard */ }
91
+ }
92
+ else if (lockExpired(existing, now, staleMs)) {
93
+ removeOwnedLock(guardPath, existing.token);
94
+ }
95
+ return tryCreateLock(guardPath, { token: guardToken, pid: process.pid, createdAt: now }) ? guardToken : undefined;
96
+ };
97
+ while (Date.now() <= deadline) {
98
+ const guardToken = acquireGuard();
99
+ if (!guardToken) {
100
+ sleep(retryMs);
101
+ continue;
102
+ }
103
+ try {
104
+ const now = Date.now();
105
+ const existing = lockRecord(lockPath);
106
+ if (existing === undefined && existsSync(lockPath)) {
107
+ try {
108
+ unlinkSync(lockPath);
109
+ }
110
+ catch { /* best-effort reclamation of a corrupt lock */ }
111
+ }
112
+ else if (lockExpired(existing, now, staleMs)) {
113
+ removeOwnedLock(lockPath, existing.token);
114
+ }
115
+ if (tryCreateLock(lockPath, { token, pid: process.pid, createdAt: now })) {
116
+ return () => releaseOwnedLock(lockRecord(lockPath), lockPath, token);
117
+ }
118
+ }
119
+ finally {
120
+ removeOwnedLock(guardPath, guardToken);
121
+ }
122
+ sleep(retryMs);
123
+ }
124
+ throw new Error(`Timed out waiting for AI-DLC state lock after ${timeoutMs}ms`);
125
+ };
@@ -0,0 +1,8 @@
1
+ import type { AgenticMemoryEntry, AgenticMemoryRegistry, Phase } from "../model.js";
2
+ export declare const emptyMemoryRegistry: () => AgenticMemoryRegistry;
3
+ export declare const validateMemoryRegistry: (value: unknown) => AgenticMemoryRegistry;
4
+ export declare const loadMemoryRegistry: (root: string) => AgenticMemoryRegistry;
5
+ export declare const saveMemoryRegistry: (root: string, registry: AgenticMemoryRegistry) => void;
6
+ export declare const selectAgenticMemory: (registry: AgenticMemoryRegistry, areas: string[], phase: Exclude<Phase, "done">, maxChars: number) => AgenticMemoryEntry[];
7
+ export declare const promoteAgenticMemory: (root: string, entry: AgenticMemoryEntry) => AgenticMemoryEntry;
8
+ export declare const retireAgenticMemory: (root: string, id: string, reason: string, retiredBy: string, retiredAt?: string) => AgenticMemoryEntry;
@@ -0,0 +1,100 @@
1
+ import { idPattern, iso, text } from "./guards.js";
2
+ import { loadJsonOr, saveJson } from "./io.js";
3
+ import { memoryRegistryPathRelative } from "./paths.js";
4
+ import { loadState, loadTask } from "./store.js";
5
+ const memoryPhases = ["clarify", "plan", "build", "wrap", "*"];
6
+ const validateMemoryEntry = (entry) => {
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}`);
20
+ };
21
+ export const emptyMemoryRegistry = () => ({ schemaVersion: 1, entries: [], retired: [] });
22
+ export const validateMemoryRegistry = (value) => {
23
+ if (!value || typeof value !== "object" || Array.isArray(value))
24
+ throw new Error("Agentic memory registry must be an object");
25
+ const registry = value;
26
+ if (registry.schemaVersion !== 1 || !Array.isArray(registry.entries) || !Array.isArray(registry.retired))
27
+ throw new Error("Unsupported agentic memory registry schema");
28
+ const ids = new Set();
29
+ for (const entry of registry.entries) {
30
+ validateMemoryEntry(entry);
31
+ if (ids.has(entry.id))
32
+ throw new Error(`Duplicate agentic memory entry: ${entry.id}`);
33
+ ids.add(entry.id);
34
+ }
35
+ for (const entry of registry.retired) {
36
+ validateMemoryEntry(entry);
37
+ if (!text(entry.retiredBy) || !text(entry.reason) || !iso(entry.retiredAt))
38
+ throw new Error(`Invalid retired agentic memory entry: ${entry.id}`);
39
+ if (ids.has(entry.id))
40
+ throw new Error(`Agentic memory entry is active and retired: ${entry.id}`);
41
+ ids.add(entry.id);
42
+ }
43
+ return registry;
44
+ };
45
+ export const loadMemoryRegistry = (root) => loadJsonOr(root, memoryRegistryPathRelative, validateMemoryRegistry, emptyMemoryRegistry, "Agentic memory registry");
46
+ export const saveMemoryRegistry = (root, registry) => {
47
+ const normalized = validateMemoryRegistry(registry);
48
+ saveJson(root, memoryRegistryPathRelative, normalized);
49
+ };
50
+ export const selectAgenticMemory = (registry, areas, phase, maxChars) => {
51
+ const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
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));
56
+ const selected = [];
57
+ let used = 0;
58
+ for (const entry of matches) {
59
+ const size = `- ${entry.id} — ${entry.summary} — guidance: ${entry.guidance} — source: ${entry.sourceTaskId}/${entry.sourceLessonId}\n`.length;
60
+ if (used + size > maxChars)
61
+ continue;
62
+ selected.push(entry);
63
+ used += size;
64
+ }
65
+ return selected;
66
+ };
67
+ export const promoteAgenticMemory = (root, entry) => {
68
+ const state = loadState(root);
69
+ const source = loadTask(root, entry.sourceTaskId, state);
70
+ if (!source?.lessons?.some((lesson) => lesson.id === entry.sourceLessonId))
71
+ throw new Error(`Agentic memory source lesson is missing: ${entry.sourceTaskId}/${entry.sourceLessonId}`);
72
+ const registry = loadMemoryRegistry(root);
73
+ const current = registry.entries.find((item) => item.id === entry.id);
74
+ if (current) {
75
+ if (JSON.stringify(current) === JSON.stringify(entry))
76
+ return current;
77
+ throw new Error(`Agentic memory entry already exists with different content: ${entry.id}`);
78
+ }
79
+ if (registry.retired.some((item) => item.id === entry.id))
80
+ throw new Error(`Retired agentic memory id cannot be reused: ${entry.id}`);
81
+ registry.entries.push(entry);
82
+ registry.entries.sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
83
+ saveMemoryRegistry(root, registry);
84
+ return entry;
85
+ };
86
+ export const retireAgenticMemory = (root, id, reason, retiredBy, retiredAt = new Date().toISOString()) => {
87
+ if (!text(reason) || !text(retiredBy) || !iso(retiredAt))
88
+ throw new Error("Retiring agentic memory requires a reason, approver, and valid timestamp");
89
+ const registry = loadMemoryRegistry(root);
90
+ const index = registry.entries.findIndex((entry) => entry.id === id);
91
+ if (index < 0)
92
+ throw new Error(`Unknown active agentic memory entry: ${id}`);
93
+ const [entry] = registry.entries.splice(index, 1);
94
+ if (!entry)
95
+ throw new Error(`Unknown active agentic memory entry: ${id}`);
96
+ registry.retired.push({ ...entry, retiredBy, retiredAt, reason });
97
+ registry.retired.sort((left, right) => right.retiredAt.localeCompare(left.retiredAt) || left.id.localeCompare(right.id));
98
+ saveMemoryRegistry(root, registry);
99
+ return entry;
100
+ };
@@ -0,0 +1,17 @@
1
+ import type { LessonIndex, WorkflowState } from "../model.js";
2
+ /** Marks pre-existing ready G2 waits during migration; the marker is the only exemption from repair-bound enforcement. */
3
+ export declare const stampLegacyG2Waits: (state: WorkflowState) => string[];
4
+ export declare const prepareV4Migration: (value: WorkflowState) => {
5
+ state: WorkflowState;
6
+ records: Array<{
7
+ path: string;
8
+ content: string;
9
+ }>;
10
+ lessonIndex: LessonIndex;
11
+ stamped: string[];
12
+ };
13
+ export declare const migrateState: (root: string, state?: WorkflowState) => {
14
+ migrated: boolean;
15
+ archived: string[];
16
+ stamped: string[];
17
+ };