@felixdotgo/aidlc-kit 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/DEVELOPMENT.md +1 -1
  2. package/README.md +20 -9
  3. package/dist/assets/.agents/aidlc/conventions.md +8 -8
  4. package/dist/assets/.agents/aidlc/orchestrator.md +15 -10
  5. package/dist/assets/.agents/aidlc/phase-build.md +7 -7
  6. package/dist/assets/.agents/aidlc/phase-clarify.md +3 -3
  7. package/dist/assets/.agents/aidlc/phase-plan.md +6 -6
  8. package/dist/assets/.agents/aidlc/phase-wrap.md +1 -1
  9. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
  10. package/dist/assets/.agents/aidlc/scripts/assignment.mjs +36 -0
  11. package/dist/assets/.agents/aidlc/scripts/context.mjs +2 -0
  12. package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +2 -0
  13. package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +2 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +181 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/authority.mjs +3 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +13 -1
  17. package/dist/assets/.agents/aidlc/scripts/lib/feature-store.mjs +70 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +220 -0
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +183 -0
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +100 -0
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +58 -0
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +320 -0
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +284 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +51 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-artifacts.mjs +63 -0
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-broker.mjs +53 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-local-broker.mjs +44 -0
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-sandbox.mjs +88 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +89 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-gate-receipt.mjs +32 -0
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +162 -0
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-planner.mjs +13 -0
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +260 -0
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +131 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +55 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +101 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +32 -0
  38. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +66 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/remote-state-client.mjs +112 -0
  40. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +41 -6
  41. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +90 -0
  42. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +58 -0
  43. package/dist/assets/.agents/aidlc/scripts/loop.mjs +36 -0
  44. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +311 -0
  45. package/dist/assets/.agents/aidlc/scripts/render.mjs +2 -0
  46. package/dist/assets/.agents/aidlc/scripts/state.mjs +71 -11
  47. package/dist/assets/.agents/aidlc/scripts/task-next.mjs +2 -0
  48. package/dist/assets/.agents/aidlc/templates/model-contract.md +3 -3
  49. package/dist/assets/.agents/aidlc/templates/workplan.md +2 -2
  50. package/dist/assets/.agents/skills/aidlc-build/SKILL.md +2 -2
  51. package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +2 -2
  52. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +2 -2
  53. package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +1 -1
  54. package/dist/services/aidlc-coordination/.env.fleet.example +11 -0
  55. package/dist/services/aidlc-coordination/compose.yaml +23 -2
  56. package/dist/services/aidlc-coordination/package-lock.json +1 -0
  57. package/dist/services/aidlc-coordination/package.json +2 -1
  58. package/dist/services/aidlc-coordination/src/coordination.mjs +1 -1
  59. package/dist/services/aidlc-coordination/src/fleet-client.mjs +36 -0
  60. package/dist/services/aidlc-coordination/src/fleet-run.mjs +18 -0
  61. package/dist/services/aidlc-coordination/src/fleet.mjs +233 -0
  62. package/dist/services/aidlc-coordination/src/git-gateway.mjs +50 -0
  63. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +41 -6
  64. package/dist/services/aidlc-coordination/src/repository.mjs +167 -4
  65. package/dist/services/aidlc-coordination/src/server.mjs +53 -9
  66. package/dist/services/aidlc-coordination/test/fleet.test.mjs +351 -0
  67. package/dist/services/aidlc-coordination/test/git-gateway.test.mjs +75 -0
  68. package/dist/services/aidlc-coordination/test/supervisor-fencing.test.mjs +59 -0
  69. package/dist/src/adapters.js +32 -5
  70. package/dist/src/cli.js +146 -70
  71. package/dist/src/context.js +2 -0
  72. package/dist/src/gate.d.ts +1 -0
  73. package/dist/src/gate.js +51 -38
  74. package/dist/src/installer.d.ts +10 -1
  75. package/dist/src/installer.js +157 -37
  76. package/dist/src/managed-content.js +1 -1
  77. package/dist/src/model.d.ts +16 -1
  78. package/dist/src/model.js +1 -1
  79. package/dist/src/profiles.d.ts +9 -1
  80. package/dist/src/profiles.js +37 -3
  81. package/dist/src/project-migration.d.ts +1 -0
  82. package/dist/src/project-migration.js +7 -0
  83. package/dist/src/state/archive.d.ts +17 -0
  84. package/dist/src/state/archive.js +60 -0
  85. package/dist/src/state/items.d.ts +25 -0
  86. package/dist/src/state/items.js +146 -0
  87. package/dist/src/state/lessons.d.ts +7 -0
  88. package/dist/src/state/lessons.js +67 -0
  89. package/dist/src/state/lifecycle.d.ts +5 -0
  90. package/dist/src/state/lifecycle.js +78 -0
  91. package/dist/src/state/lock.d.ts +7 -0
  92. package/dist/src/state/lock.js +124 -0
  93. package/dist/src/state/memory.d.ts +8 -0
  94. package/dist/src/state/memory.js +88 -0
  95. package/dist/src/state/render.d.ts +26 -0
  96. package/dist/src/state/render.js +29 -0
  97. package/dist/src/state/shared.d.ts +29 -0
  98. package/dist/src/state/shared.js +71 -0
  99. package/dist/src/state/store.d.ts +4 -0
  100. package/dist/src/state/store.js +81 -0
  101. package/dist/src/state/transitions.d.ts +21 -0
  102. package/dist/src/state/transitions.js +93 -0
  103. package/dist/src/state/validate.d.ts +4 -0
  104. package/dist/src/state/validate.js +100 -0
  105. package/dist/src/state.d.ts +15 -110
  106. package/dist/src/state.js +15 -764
  107. package/dist/src/tracker.d.ts +7 -0
  108. package/dist/src/tracker.js +3 -0
  109. package/dist/src/upgrade-board.d.ts +5 -0
  110. package/dist/src/upgrade-board.js +89 -0
  111. package/dist/src/upgrade.d.ts +5 -6
  112. package/dist/src/upgrade.js +211 -193
  113. package/dist/src/workflow.d.ts +1 -0
  114. package/dist/src/workflow.js +3 -2
  115. package/docs/README.md +4 -4
  116. package/docs/architecture.md +10 -2
  117. package/docs/command-reference.md +72 -11
  118. package/docs/configuration.md +26 -4
  119. package/docs/coordination-service.md +59 -3
  120. package/docs/development.md +16 -2
  121. package/docs/operating-workflow.md +53 -14
  122. package/docs/testing-and-release.md +53 -10
  123. package/package.json +3 -3
@@ -0,0 +1,146 @@
1
+ import { isAbsolute, join, resolve } from "node:path";
2
+ 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
+ const isPending = (item) => item.status === "in_progress" || item.status === "todo";
6
+ const pendingItems = (task) => task.tasks.filter(isPending);
7
+ /** Validates execution dependencies and reports which pending items are ready to dispatch. */
8
+ export const readyTaskItems = (task) => {
9
+ const byId = new Map(task.tasks.map((item) => [item.id, item]));
10
+ if (byId.size !== task.tasks.length)
11
+ throw new Error(`Duplicate execution item: ${task.id}`);
12
+ const visiting = new Set();
13
+ const visited = new Set();
14
+ const visit = (id) => {
15
+ if (visiting.has(id))
16
+ throw new Error(`Cyclic execution dependency: ${task.id}/${id}`);
17
+ if (visited.has(id))
18
+ return;
19
+ visiting.add(id);
20
+ for (const dep of byId.get(id)?.dependsOn ?? []) {
21
+ if (dep === id || !byId.has(dep))
22
+ throw new Error(`Unknown execution dependency: ${task.id}/${id}/${dep}`);
23
+ visit(dep);
24
+ }
25
+ visiting.delete(id);
26
+ visited.add(id);
27
+ };
28
+ for (const item of task.tasks) {
29
+ 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
+ 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(".."))))
32
+ throw new Error(`Invalid owned paths: ${task.id}/${item.id}`);
33
+ visit(item.id);
34
+ }
35
+ const ready = [];
36
+ const blocked = [];
37
+ for (const item of task.tasks) {
38
+ const waitingOn = (item.dependsOn ?? []).filter((dep) => byId.get(dep)?.status !== "done");
39
+ if (item.status === "done" && waitingOn.length)
40
+ throw new Error(`Completed item has incomplete prerequisites: ${task.id}/${item.id}`);
41
+ if (!isPending(item))
42
+ continue;
43
+ if (waitingOn.length)
44
+ blocked.push({ id: item.id, waitingOn });
45
+ else
46
+ ready.push(item.id);
47
+ }
48
+ return { ready, blocked };
49
+ };
50
+ const dispatchableItem = (open, task) => {
51
+ const ready = new Set(readyTaskItems(task).ready);
52
+ return open.find((entry) => entry.status === "in_progress" && ready.has(entry.id)) ?? open.find((entry) => ready.has(entry.id));
53
+ };
54
+ 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) => {
66
+ if (task.phase !== "build" || isLegacyG2Wait(task))
67
+ return undefined;
68
+ const bounds = repairBounds(task);
69
+ if (!bounds.verifyExhausted && !bounds.reviewExhausted)
70
+ return undefined;
71
+ const kind = bounds.reviewExhausted ? "review_exhausted" : "repair_exhausted";
72
+ 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
+ };
80
+ };
81
+ const continueItem = (item, openCount, task, suffix, contextCommand, premature) => ({
82
+ classification: "run_phase",
83
+ phase: task.phase,
84
+ gate: task.gate,
85
+ itemId: item.id,
86
+ remainingItems: openCount,
87
+ command: `${contextCommand} ${task.id} --phase build --item ${item.id}${suffix}`,
88
+ 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
+ });
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}`,
96
+ 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
+ });
98
+ const prematureG2Wait = (task, suffix, contextCommand) => {
99
+ if (task.status !== "blocked_on_user" || task.gate !== "G2_codereview")
100
+ return undefined;
101
+ if (!(task.tasks.some((item) => !["done", "deferred"].includes(item.status)) || !hasVerification(task) || !hasReview(task)))
102
+ return undefined;
103
+ const open = pendingItems(task);
104
+ const item = dispatchableItem(open, task);
105
+ if (item)
106
+ return continueItem(item, open.length, task, suffix, contextCommand, true);
107
+ if (open.length)
108
+ return { classification: "blocked", phase: task.phase, gate: task.gate, reason: noReadyItemReason(task) };
109
+ return continueVerification(task, suffix, contextCommand, true);
110
+ };
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");
115
+ if (task.phase === "done")
116
+ return { classification: "complete", phase: task.phase, gate: task.gate, reason: "Task is complete" };
117
+ 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" };
119
+ if (task.handoff)
120
+ return { classification: "blocked", phase: task.phase, gate: task.gate, reason: task.handoff.reason, actions: handoffActions(task, suffix, stateCommand) };
121
+ 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);
124
+ if (repair)
125
+ return repair;
126
+ const prematureG2 = prematureG2Wait(task, suffix, contextCommand);
127
+ if (prematureG2)
128
+ 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
+ }
132
+ 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" };
134
+ 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}` };
136
+ if (task.phase === "build") {
137
+ const open = pendingItems(task);
138
+ const item = dispatchableItem(open, task);
139
+ if (item)
140
+ return continueItem(item, open.length, task, suffix, contextCommand, false);
141
+ if (open.length)
142
+ return { classification: "blocked", phase: task.phase, gate: task.gate, reason: noReadyItemReason(task) };
143
+ return continueVerification(task, suffix, contextCommand, false);
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
+ };
@@ -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,67 @@
1
+ import { existsSync } from "node:fs";
2
+ import { validateLesson } from "./validate.js";
3
+ import { loadState, loadTask } from "./store.js";
4
+ import { assertNoSymlinkPath, audited, digest, lessonIndexPath, parseJsonFile, tokens, writeAtomic } from "./shared.js";
5
+ 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
+ export const recordLesson = (task, lesson, recordedAt = new Date().toISOString()) => {
7
+ if (task.phase !== "wrap" || task.status !== "active")
8
+ throw new Error(`Lessons can only be recorded during active wrap: ${task.id}`);
9
+ const current = task.lessons?.find((item) => item.id === lesson.id);
10
+ const next = { ...lesson, taskId: task.id, recordedAt };
11
+ validateLesson(task.id, next);
12
+ if (current) {
13
+ if (JSON.stringify({ ...current, recordedAt: undefined }) === JSON.stringify({ ...next, recordedAt: undefined }))
14
+ return current;
15
+ throw new Error(`Lesson already exists with different content: ${task.id}/${lesson.id}`);
16
+ }
17
+ task.lessons ??= [];
18
+ task.lessons.push(next);
19
+ task.lessonDisposition = { status: "captured", source: lesson.source, recordedAt };
20
+ task.updatedAt = recordedAt;
21
+ return next;
22
+ };
23
+ export const recordNoLessons = (task, reason, source, recordedAt = new Date().toISOString()) => {
24
+ if (task.phase !== "wrap" || task.status !== "active")
25
+ throw new Error(`Lesson disposition can only be recorded during active wrap: ${task.id}`);
26
+ audited(reason, source);
27
+ if (task.lessons?.length)
28
+ throw new Error(`Task already has captured lessons: ${task.id}`);
29
+ task.lessonDisposition = { status: "none", reason, source, recordedAt };
30
+ task.updatedAt = recordedAt;
31
+ };
32
+ export const lessonStateDigest = (state) => digest(JSON.stringify({
33
+ active: Object.values(state.tasks).flatMap((task) => task.lessons ?? []).sort((a, b) => `${a.taskId}:${a.id}`.localeCompare(`${b.taskId}:${b.id}`)),
34
+ archive: Object.values(state.archive ?? {}).map((item) => ({ id: item.id, digest: item.digest, lessonCount: item.lessonCount })).sort((a, b) => a.id.localeCompare(b.id))
35
+ }));
36
+ 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)) });
37
+ const allLessons = (root, state) => [
38
+ ...Object.values(state.tasks).flatMap((task) => task.lessons ?? []),
39
+ ...Object.keys(state.archive ?? {}).flatMap((id) => loadTask(root, id, state)?.lessons ?? [])
40
+ ];
41
+ export const rebuildLessonIndex = (root, state = loadState(root)) => {
42
+ 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`);
45
+ return index;
46
+ };
47
+ export const searchLessons = (root, state, query, areas = [], limit = 5) => {
48
+ const path = lessonIndexPath(root);
49
+ assertNoSymlinkPath(root, ".agents/data/lessons/index.json");
50
+ if (!existsSync(path)) {
51
+ const hasLessons = Object.values(state.tasks).some((task) => Boolean(task.lessons?.length)) || Object.values(state.archive ?? {}).some((task) => task.lessonCount > 0);
52
+ if (!hasLessons)
53
+ return [];
54
+ throw new Error("Lesson index is missing; run state.mjs lesson rebuild");
55
+ }
56
+ const index = parseJsonFile(path, "Lesson index", "run state.mjs lesson rebuild");
57
+ if (index.schemaVersion !== 1 || !Array.isArray(index.lessons) || index.sourceDigest !== lessonStateDigest(state))
58
+ throw new Error("Lesson index is stale; run state.mjs lesson rebuild");
59
+ const queryTokens = tokens(query);
60
+ const areaSet = new Set(areas.map((area) => area.toLocaleLowerCase()));
61
+ return index.lessons.map((item) => {
62
+ const haystack = new Set(tokens(`${item.summary} ${item.prevention} ${item.taskId} ${item.areas.join(" ")}`));
63
+ const areaScore = item.areas.some((area) => areaSet.has(area.toLocaleLowerCase())) ? 100 : 0;
64
+ const tokenScore = queryTokens.filter((token) => haystack.has(token)).length;
65
+ 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);
67
+ };
@@ -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 } from "./shared.js";
2
+ import { hasApproval } from "./transitions.js";
3
+ import { validateState } from "./validate.js";
4
+ import { audited, terminal } from "./shared.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,124 @@
1
+ import { randomUUID } from "node:crypto";
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";
5
+ const sleep = (milliseconds) => {
6
+ if (milliseconds > 0)
7
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
8
+ };
9
+ const lockRecord = (path) => {
10
+ try {
11
+ const value = JSON.parse(readFileSync(path, "utf8"));
12
+ 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;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ } /* intentional: unreadable lock record is treated as absent */
17
+ };
18
+ const processAlive = (pid) => {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ return error.code === "EPERM";
25
+ }
26
+ };
27
+ const tryCreateLock = (path, record) => {
28
+ try {
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ const descriptor = openSync(path, "wx", 0o600);
31
+ try {
32
+ writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
33
+ }
34
+ finally {
35
+ closeSync(descriptor);
36
+ }
37
+ return true;
38
+ }
39
+ catch (error) {
40
+ if (error.code === "EEXIST")
41
+ return false;
42
+ throw error;
43
+ }
44
+ };
45
+ const removeOwnedLock = (path, token) => {
46
+ if (lockRecord(path)?.token === token)
47
+ unlinkSync(path);
48
+ };
49
+ const lockExpired = (record, now, staleMs) => Boolean(record && now - record.createdAt >= staleMs && !processAlive(record.pid));
50
+ const releaseOwnedLock = (lockRecordOrNull, lockPath, token) => {
51
+ if (!lockRecordOrNull) {
52
+ // A corrupt/unreadable lock record must not wedge release; reclaim best-effort.
53
+ if (existsSync(lockPath)) {
54
+ try {
55
+ unlinkSync(lockPath);
56
+ }
57
+ catch { /* best-effort cleanup of a corrupt lock */ }
58
+ }
59
+ return;
60
+ }
61
+ if (lockRecordOrNull.token !== token)
62
+ return;
63
+ try {
64
+ unlinkSync(lockPath);
65
+ }
66
+ catch (error) {
67
+ throw new Error(`Failed to release owned AI-DLC state lock: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ };
70
+ /** Serializes lifecycle script mutations across local Node.js processes. */
71
+ export const acquireStateLock = (root, options = {}) => {
72
+ const timeoutMs = options.timeoutMs ?? 5_000;
73
+ const retryMs = options.retryMs ?? 25;
74
+ 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");
79
+ const token = randomUUID();
80
+ const deadline = Date.now() + timeoutMs;
81
+ const acquireGuard = () => {
82
+ const guardToken = randomUUID();
83
+ const now = Date.now();
84
+ const existing = lockRecord(guardPath);
85
+ if (existing === undefined && existsSync(guardPath)) {
86
+ try {
87
+ unlinkSync(guardPath);
88
+ }
89
+ catch { /* best-effort reclamation of a corrupt guard */ }
90
+ }
91
+ else if (lockExpired(existing, now, staleMs)) {
92
+ removeOwnedLock(guardPath, existing.token);
93
+ }
94
+ return tryCreateLock(guardPath, { token: guardToken, pid: process.pid, createdAt: now }) ? guardToken : undefined;
95
+ };
96
+ while (Date.now() <= deadline) {
97
+ const guardToken = acquireGuard();
98
+ if (!guardToken) {
99
+ sleep(retryMs);
100
+ continue;
101
+ }
102
+ try {
103
+ const now = Date.now();
104
+ const existing = lockRecord(lockPath);
105
+ if (existing === undefined && existsSync(lockPath)) {
106
+ try {
107
+ unlinkSync(lockPath);
108
+ }
109
+ catch { /* best-effort reclamation of a corrupt lock */ }
110
+ }
111
+ else if (lockExpired(existing, now, staleMs)) {
112
+ removeOwnedLock(lockPath, existing.token);
113
+ }
114
+ if (tryCreateLock(lockPath, { token, pid: process.pid, createdAt: now })) {
115
+ return () => releaseOwnedLock(lockRecord(lockPath), lockPath, token);
116
+ }
117
+ }
118
+ finally {
119
+ removeOwnedLock(guardPath, guardToken);
120
+ }
121
+ sleep(retryMs);
122
+ }
123
+ throw new Error(`Timed out waiting for AI-DLC state lock after ${timeoutMs}ms`);
124
+ };
@@ -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,88 @@
1
+ import { existsSync } from "node:fs";
2
+ import { loadState, loadTask } from "./store.js";
3
+ import { assertNoSymlinkPath, idPattern, iso, memoryRegistryPath, parseJsonFile, text, writeAtomic } from "./shared.js";
4
+ const memoryPhases = ["clarify", "plan", "build", "wrap", "*"];
5
+ 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"}`);
8
+ };
9
+ export const emptyMemoryRegistry = () => ({ schemaVersion: 1, entries: [], retired: [] });
10
+ export const validateMemoryRegistry = (value) => {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ throw new Error("Agentic memory registry must be an object");
13
+ const registry = value;
14
+ if (registry.schemaVersion !== 1 || !Array.isArray(registry.entries) || !Array.isArray(registry.retired))
15
+ throw new Error("Unsupported agentic memory registry schema");
16
+ const ids = new Set();
17
+ for (const entry of registry.entries) {
18
+ validateMemoryEntry(entry);
19
+ if (ids.has(entry.id))
20
+ throw new Error(`Duplicate agentic memory entry: ${entry.id}`);
21
+ ids.add(entry.id);
22
+ }
23
+ for (const entry of registry.retired) {
24
+ validateMemoryEntry(entry);
25
+ if (!text(entry.retiredBy) || !text(entry.reason) || !iso(entry.retiredAt))
26
+ throw new Error(`Invalid retired agentic memory entry: ${entry.id}`);
27
+ if (ids.has(entry.id))
28
+ throw new Error(`Agentic memory entry is active and retired: ${entry.id}`);
29
+ ids.add(entry.id);
30
+ }
31
+ return registry;
32
+ };
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
+ };
38
+ export const saveMemoryRegistry = (root, registry) => {
39
+ const normalized = validateMemoryRegistry(registry);
40
+ assertNoSymlinkPath(root, registryPathRelative);
41
+ writeAtomic(memoryRegistryPath(root), `${JSON.stringify(normalized, null, 2)}\n`);
42
+ };
43
+ export const selectAgenticMemory = (registry, areas, phase, maxChars) => {
44
+ 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));
46
+ const selected = [];
47
+ let used = 0;
48
+ for (const entry of matches) {
49
+ const size = `- ${entry.id} — ${entry.summary} — guidance: ${entry.guidance} — source: ${entry.sourceTaskId}/${entry.sourceLessonId}\n`.length;
50
+ if (used + size > maxChars)
51
+ continue;
52
+ selected.push(entry);
53
+ used += size;
54
+ }
55
+ return selected;
56
+ };
57
+ export const promoteAgenticMemory = (root, entry) => {
58
+ const state = loadState(root);
59
+ const source = loadTask(root, entry.sourceTaskId, state);
60
+ if (!source?.lessons?.some((lesson) => lesson.id === entry.sourceLessonId))
61
+ throw new Error(`Agentic memory source lesson is missing: ${entry.sourceTaskId}/${entry.sourceLessonId}`);
62
+ const registry = loadMemoryRegistry(root);
63
+ const current = registry.entries.find((item) => item.id === entry.id);
64
+ if (current) {
65
+ if (JSON.stringify(current) === JSON.stringify(entry))
66
+ return current;
67
+ throw new Error(`Agentic memory entry already exists with different content: ${entry.id}`);
68
+ }
69
+ if (registry.retired.some((item) => item.id === entry.id))
70
+ throw new Error(`Retired agentic memory id cannot be reused: ${entry.id}`);
71
+ registry.entries.push(entry);
72
+ registry.entries.sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
73
+ saveMemoryRegistry(root, registry);
74
+ return entry;
75
+ };
76
+ export const retireAgenticMemory = (root, id, reason, retiredBy, retiredAt = new Date().toISOString()) => {
77
+ if (!text(reason) || !text(retiredBy) || !iso(retiredAt))
78
+ throw new Error("Retiring agentic memory requires a reason, approver, and valid timestamp");
79
+ const registry = loadMemoryRegistry(root);
80
+ const index = registry.entries.findIndex((entry) => entry.id === id);
81
+ if (index < 0)
82
+ throw new Error(`Unknown active agentic memory entry: ${id}`);
83
+ const [entry] = registry.entries.splice(index, 1);
84
+ registry.retired.push({ ...entry, retiredBy, retiredAt, reason });
85
+ registry.retired.sort((left, right) => right.retiredAt.localeCompare(left.retiredAt) || left.id.localeCompare(right.id));
86
+ saveMemoryRegistry(root, registry);
87
+ return entry;
88
+ };
@@ -0,0 +1,26 @@
1
+ import type { Phase, Gate, TaskState, WorkflowState } from "../model.js";
2
+ export interface TaskSummary {
3
+ id: string;
4
+ title: string;
5
+ phase: Phase;
6
+ gate: Gate;
7
+ status: TaskState["status"];
8
+ risk: TaskState["risk"];
9
+ areas: string[];
10
+ updatedAt: string;
11
+ source: "active" | "archive";
12
+ }
13
+ export declare const listTaskSummaries: (state: WorkflowState, options?: {
14
+ includeArchive?: boolean;
15
+ statuses?: string[];
16
+ query?: string;
17
+ limit?: number;
18
+ cursor?: number;
19
+ }) => {
20
+ items: TaskSummary[];
21
+ nextCursor: string | null;
22
+ total: number;
23
+ };
24
+ /** Renders the markdown workplan for a task. */
25
+ export declare const renderWorkplan: (task: TaskState) => string;
26
+ export declare const renderViews: (root: string, state?: WorkflowState, selected?: TaskState[]) => void;
@@ -0,0 +1,29 @@
1
+ import { join, resolve } from "node:path";
2
+ import { loadState } from "./store.js";
3
+ import { assertNoSymlinkPath, tokens, writeAtomic } from "./shared.js";
4
+ export const listTaskSummaries = (state, options = {}) => {
5
+ 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
+ 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
+ const queryTokens = tokens(options.query ?? "");
8
+ 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 cursor = Math.max(0, options.cursor ?? 0);
11
+ const limit = Math.max(1, Math.min(options.limit ?? 20, 100));
12
+ const items = filtered.slice(cursor, cursor + limit);
13
+ return { items, nextCursor: cursor + items.length < filtered.length ? String(cursor + items.length) : null, total: filtered.length };
14
+ };
15
+ /** Renders the markdown workplan for a task. */
16
+ export const renderWorkplan = (task) => {
17
+ 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";
19
+ 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
+ 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
+ };
22
+ export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => {
23
+ for (const task of selected) {
24
+ if (!task.artifacts.workplan)
25
+ continue;
26
+ assertNoSymlinkPath(root, task.artifacts.workplan);
27
+ writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task));
28
+ }
29
+ };
@@ -0,0 +1,29 @@
1
+ import type { ArchivedTaskSummary, Gate, HandoffKind, Phase, TaskState, WorkflowState } from "../model.js";
2
+ export declare const statePath: (root: string) => string;
3
+ export declare const lessonIndexPath: (root: string) => string;
4
+ export declare const memoryRegistryPath: (root: string) => string;
5
+ export declare const stateLockPath: (root: string) => string;
6
+ export declare const stateLockGuardPath: (root: string) => string;
7
+ export declare const emptyState: () => WorkflowState;
8
+ export declare const legacyG2WaitSource = "schema-v4-migration";
9
+ export declare const phases: Phase[];
10
+ export declare const gates: Gate[];
11
+ export declare const handoffKinds: HandoffKind[];
12
+ export declare const terminal: (task: TaskState) => boolean;
13
+ export declare const phaseGate: (phase: Phase) => Gate;
14
+ export declare const iso: (value: unknown) => value is string;
15
+ export declare const text: (value: unknown) => value is string;
16
+ export declare const idPattern: RegExp;
17
+ export declare const digest: (content: string) => string;
18
+ export declare const archiveRecordPath: (id: string) => string;
19
+ export declare const taskRecordContent: (task: TaskState) => string;
20
+ export declare const assertNoSymlinkPath: (root: string, relative: string) => void;
21
+ export declare const writeAtomic: (path: string, content: string) => void;
22
+ export declare const parseJsonText: (content: string, label: string, recovery?: string) => unknown;
23
+ export declare const parseJsonFile: (path: string, label: string, recovery?: string) => unknown;
24
+ export declare const audited: (reason: string, source: string) => void;
25
+ export declare const tokens: (value: string) => string[];
26
+ /** Rejects artifact paths that escape the project-owned tasks directory. */
27
+ export declare const assertTaskArtifactPath: (id: string, path: string) => void;
28
+ /** Builds the immutable archive-catalog summary for a terminal task from its record content. */
29
+ export declare const archivedSummary: (task: TaskState, content?: string) => ArchivedTaskSummary;