@felixdotgo/aidlc-kit 0.1.1 → 0.3.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 (198) hide show
  1. package/DEVELOPMENT.md +14 -13
  2. package/README.md +37 -43
  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 +2 -2
  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/state.schema.json +2 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
  12. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
  38. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
  40. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
  41. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  42. package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
  43. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  44. package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
  45. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
  46. package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
  47. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  48. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  49. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  50. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  51. package/dist/assets/ui/bulma.min.css +3 -0
  52. package/dist/assets/ui/changes.js +83 -0
  53. package/dist/assets/ui/common.js +664 -0
  54. package/dist/assets/ui/dashboard.css +636 -0
  55. package/dist/assets/ui/dashboard.js +77 -0
  56. package/dist/assets/ui/detail.js +159 -0
  57. package/dist/assets/ui/live-log.js +332 -0
  58. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  59. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  60. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  61. package/dist/services/aidlc-coordination/package.json +9 -4
  62. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
  63. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  64. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  65. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  66. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  67. package/dist/src/adapters.d.ts +0 -3
  68. package/dist/src/adapters.js +3 -5
  69. package/dist/src/cli/argv.d.ts +12 -0
  70. package/dist/src/cli/argv.js +68 -0
  71. package/dist/src/cli/commands.d.ts +8 -0
  72. package/dist/src/cli/commands.js +159 -0
  73. package/dist/src/cli/loop-launcher.d.ts +47 -0
  74. package/dist/src/cli/loop-launcher.js +322 -0
  75. package/dist/src/cli/options.d.ts +25 -0
  76. package/dist/src/cli/options.js +32 -0
  77. package/dist/src/cli/prompt.d.ts +9 -0
  78. package/dist/src/cli/prompt.js +66 -0
  79. package/dist/src/cli.js +28 -191
  80. package/dist/src/gate.js +3 -2
  81. package/dist/src/installer.d.ts +14 -7
  82. package/dist/src/installer.js +42 -143
  83. package/dist/src/legacy.js +1 -0
  84. package/dist/src/managed-content.d.ts +7 -0
  85. package/dist/src/managed-content.js +15 -4
  86. package/dist/src/managed-planning.d.ts +65 -0
  87. package/dist/src/managed-planning.js +219 -0
  88. package/dist/src/mcp-setup.js +3 -1
  89. package/dist/src/model/config-contracts.d.ts +65 -0
  90. package/dist/src/model/config-contracts.js +1 -0
  91. package/dist/src/model/install-contracts.d.ts +29 -0
  92. package/dist/src/model/install-contracts.js +1 -0
  93. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  94. package/dist/src/model/lifecycle-enums.js +1 -0
  95. package/dist/src/model/loop.d.ts +20 -0
  96. package/dist/src/model/loop.js +1 -0
  97. package/dist/src/model/manifest.d.ts +16 -0
  98. package/dist/src/model/manifest.js +1 -0
  99. package/dist/src/model/records.d.ts +142 -0
  100. package/dist/src/model/records.js +1 -0
  101. package/dist/src/model.d.ts +11 -264
  102. package/dist/src/model.js +1 -1
  103. package/dist/src/profiles.d.ts +2 -0
  104. package/dist/src/profiles.js +53 -9
  105. package/dist/src/state/archive-record.d.ts +9 -0
  106. package/dist/src/state/archive-record.js +26 -0
  107. package/dist/src/state/commands.d.ts +18 -0
  108. package/dist/src/state/commands.js +27 -0
  109. package/dist/src/state/domain.d.ts +9 -0
  110. package/dist/src/state/domain.js +9 -0
  111. package/dist/src/state/guards.d.ts +7 -0
  112. package/dist/src/state/guards.js +17 -0
  113. package/dist/src/state/io.d.ts +10 -0
  114. package/dist/src/state/io.js +35 -0
  115. package/dist/src/state/items.d.ts +3 -4
  116. package/dist/src/state/items.js +58 -57
  117. package/dist/src/state/lessons.js +14 -7
  118. package/dist/src/state/lifecycle.js +2 -2
  119. package/dist/src/state/lock.js +7 -6
  120. package/dist/src/state/memory.js +24 -12
  121. package/dist/src/state/{archive.js → migrate.js} +7 -7
  122. package/dist/src/state/partition.d.ts +15 -0
  123. package/dist/src/state/partition.js +90 -0
  124. package/dist/src/state/paths.d.ts +11 -0
  125. package/dist/src/state/paths.js +12 -0
  126. package/dist/src/state/render.js +9 -6
  127. package/dist/src/state/store.js +9 -15
  128. package/dist/src/state/transitions.js +9 -7
  129. package/dist/src/state/validate.js +55 -7
  130. package/dist/src/state.d.ts +16 -11
  131. package/dist/src/state.js +16 -11
  132. package/dist/src/task-ui.d.ts +33 -0
  133. package/dist/src/task-ui.js +33 -0
  134. package/dist/src/ui/assets.d.ts +14 -0
  135. package/dist/src/ui/assets.js +45 -0
  136. package/dist/src/ui/changes.d.ts +83 -0
  137. package/dist/src/ui/changes.js +498 -0
  138. package/dist/src/ui/containers.d.ts +71 -0
  139. package/dist/src/ui/containers.js +214 -0
  140. package/dist/src/ui/daemon.d.ts +75 -0
  141. package/dist/src/ui/daemon.js +232 -0
  142. package/dist/src/ui/data.d.ts +42 -0
  143. package/dist/src/ui/data.js +89 -0
  144. package/dist/src/ui/docs.d.ts +13 -0
  145. package/dist/src/ui/docs.js +39 -0
  146. package/dist/src/ui/files.d.ts +3 -0
  147. package/dist/src/ui/files.js +20 -0
  148. package/dist/src/ui/http.d.ts +14 -0
  149. package/dist/src/ui/http.js +80 -0
  150. package/dist/src/ui/live.d.ts +5 -0
  151. package/dist/src/ui/live.js +43 -0
  152. package/dist/src/ui/markdown.d.ts +2 -0
  153. package/dist/src/ui/markdown.js +127 -0
  154. package/dist/src/ui/options.d.ts +48 -0
  155. package/dist/src/ui/options.js +81 -0
  156. package/dist/src/ui/security.d.ts +34 -0
  157. package/dist/src/ui/security.js +38 -0
  158. package/dist/src/ui/server.d.ts +39 -0
  159. package/dist/src/ui/server.js +347 -0
  160. package/dist/src/ui/sse.d.ts +14 -0
  161. package/dist/src/ui/sse.js +281 -0
  162. package/dist/src/ui/views/board.d.ts +7 -0
  163. package/dist/src/ui/views/board.js +131 -0
  164. package/dist/src/ui/views/changes.d.ts +19 -0
  165. package/dist/src/ui/views/changes.js +154 -0
  166. package/dist/src/ui/views/components.d.ts +56 -0
  167. package/dist/src/ui/views/components.js +113 -0
  168. package/dist/src/ui/views/docs.d.ts +15 -0
  169. package/dist/src/ui/views/docs.js +64 -0
  170. package/dist/src/ui/views/layout.d.ts +44 -0
  171. package/dist/src/ui/views/layout.js +233 -0
  172. package/dist/src/ui/views/task.d.ts +25 -0
  173. package/dist/src/ui/views/task.js +320 -0
  174. package/dist/src/upgrade-board.js +10 -10
  175. package/dist/src/upgrade.d.ts +0 -1
  176. package/dist/src/upgrade.js +24 -85
  177. package/dist/src/workflow.d.ts +4 -0
  178. package/dist/src/workflow.js +36 -4
  179. package/docs/README.md +41 -22
  180. package/docs/architecture.md +101 -37
  181. package/docs/command-reference.md +289 -52
  182. package/docs/configuration.md +70 -30
  183. package/docs/coordination-service.md +91 -30
  184. package/docs/design-decisions.md +87 -0
  185. package/docs/development.md +124 -49
  186. package/docs/loop-local-run.md +228 -0
  187. package/docs/loop-runbook.md +214 -0
  188. package/docs/operating-workflow.md +55 -42
  189. package/docs/testing-and-release.md +72 -49
  190. package/docs/troubleshooting.md +221 -0
  191. package/package.json +4 -1
  192. package/dist/src/ledger.d.ts +0 -22
  193. package/dist/src/ledger.js +0 -119
  194. package/dist/src/state/shared.d.ts +0 -29
  195. package/dist/src/state/shared.js +0 -71
  196. package/dist/src/tracker.d.ts +0 -36
  197. package/dist/src/tracker.js +0 -44
  198. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -33,10 +33,15 @@ export const validateLoopConfig = (value) => {
33
33
  if (value === undefined)
34
34
  return { mode: "off" };
35
35
  const item = object(value, "config.loop");
36
- if (Object.keys(item).some((key) => !["mode", "agents"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
36
+ if (Object.keys(item).some((key) => !["mode", "agents", "workerImage", "pollMs", "limits"].includes(key)) || typeof item.mode !== "string" || !["off", "local", "fleet"].includes(item.mode))
37
37
  throw new Error("config.loop requires a valid mode and only supported fields");
38
+ const workerImage = validateWorkerImageMap(item.workerImage);
39
+ if (item.pollMs !== undefined && (!Number.isSafeInteger(item.pollMs) || Number(item.pollMs) < 1_000 || Number(item.pollMs) > 300_000))
40
+ throw new Error("config.loop.pollMs must be an integer from 1000 to 300000");
41
+ const limits = validateLoopLimits(item.limits);
42
+ const optional = { ...(workerImage ? { workerImage } : {}), ...(item.pollMs !== undefined ? { pollMs: Number(item.pollMs) } : {}), ...(limits ? { limits } : {}) };
38
43
  if (item.agents === undefined)
39
- return { mode: item.mode };
44
+ return { mode: item.mode, ...optional };
40
45
  const agents = object(item.agents, "config.loop.agents");
41
46
  if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
42
47
  throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
@@ -44,7 +49,34 @@ export const validateLoopConfig = (value) => {
44
49
  for (const [id, provider] of Object.entries(workers))
45
50
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
46
51
  throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
47
- return { mode: item.mode, agents: { lead: agents.lead, workers: workers } };
52
+ return { mode: item.mode, agents: { lead: agents.lead, workers: workers }, ...optional };
53
+ };
54
+ /** Bounds mirror `validateLimits` in assets/aidlc/scripts/lib/loop-runtime.mjs. */
55
+ const LOOP_LIMIT_BOUNDS = { maxTurns: 1_000, turnTimeoutMs: 3_600_000, maxStallTurns: 20 };
56
+ const validateLoopLimits = (value) => {
57
+ if (value === undefined)
58
+ return undefined;
59
+ const map = object(value, "config.loop.limits");
60
+ const result = {};
61
+ for (const [key, entry] of Object.entries(map)) {
62
+ if (!Object.hasOwn(LOOP_LIMIT_BOUNDS, key))
63
+ throw new Error(`config.loop.limits supports only ${Object.keys(LOOP_LIMIT_BOUNDS).join(", ")}`);
64
+ const max = LOOP_LIMIT_BOUNDS[key];
65
+ if (!Number.isSafeInteger(entry) || Number(entry) < 1 || Number(entry) > max)
66
+ throw new Error(`config.loop.limits.${key} must be an integer from 1 to ${max}`);
67
+ result[key] = Number(entry);
68
+ }
69
+ return result;
70
+ };
71
+ const validateWorkerImageMap = (value) => {
72
+ if (value === undefined)
73
+ return undefined;
74
+ const map = object(value, "config.loop.workerImage");
75
+ for (const [provider, tag] of Object.entries(map)) {
76
+ if (!isLoopProvider(provider) || typeof tag !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{0,255}$/.test(tag))
77
+ throw new Error(`config.loop.workerImage must map ${loopProviderNames} to image tags`);
78
+ }
79
+ return map;
48
80
  };
49
81
  const strings = (value, label) => {
50
82
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
@@ -127,6 +159,7 @@ export const validateProfile = (value, label = "profile") => {
127
159
  };
128
160
  };
129
161
  export const defaultConfig = () => ({
162
+ $schema: "./aidlc/schemas/config.schema.json",
130
163
  schemaVersion: 2,
131
164
  extends: ["topology/single"],
132
165
  specs: { roots: [] },
@@ -138,12 +171,9 @@ export const defaultConfig = () => ({
138
171
  mcp: { enabled: false },
139
172
  loop: { mode: "off" }
140
173
  });
141
- /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
142
- export const loadProjectConfig = (root) => {
143
- const path = join(resolve(root), ".agents/config.json");
144
- if (!existsSync(path))
145
- return defaultConfig();
146
- const item = object(JSON.parse(readFileSync(path, "utf8")), ".agents/config.json");
174
+ /** Validates and normalizes a parsed `.agents/config.json` value. */
175
+ export const validateProjectConfig = (value) => {
176
+ const item = object(value, ".agents/config.json");
147
177
  const risk = object(item.risk ?? {}, "config.risk");
148
178
  const context = object(item.context ?? {}, "config.context");
149
179
  const levels = ["low", "normal", "high", "regulated"];
@@ -159,7 +189,14 @@ export const loadProjectConfig = (root) => {
159
189
  const maxChars = Number(context.maxChars ?? 16_000);
160
190
  if (!Number.isInteger(maxChars) || maxChars < 4_000 || maxChars > 128_000)
161
191
  throw new Error("context.maxChars must be an integer from 4000 to 128000");
192
+ let schema;
193
+ if (item.$schema !== undefined) {
194
+ if (typeof item.$schema !== "string" || item.$schema.length > 512)
195
+ throw new Error("config.$schema must be a string of at most 512 characters");
196
+ schema = item.$schema;
197
+ }
162
198
  return {
199
+ $schema: schema,
163
200
  schemaVersion: Number(item.schemaVersion),
164
201
  extends: strings(item.extends ?? ["topology/single"], "config.extends"),
165
202
  specs: { roots: strings(object(item.specs ?? {}, "config.specs").roots ?? [], "config.specs.roots").map((entry) => safeRelative(entry, "config.specs.roots")) },
@@ -173,6 +210,13 @@ export const loadProjectConfig = (root) => {
173
210
  loop: validateLoopConfig(item.loop)
174
211
  };
175
212
  };
213
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
214
+ export const loadProjectConfig = (root) => {
215
+ const path = join(resolve(root), ".agents/config.json");
216
+ if (!existsSync(path))
217
+ return defaultConfig();
218
+ return validateProjectConfig(JSON.parse(readFileSync(path, "utf8")));
219
+ };
176
220
  const readLocalProfile = (root, id) => {
177
221
  const path = join(resolve(root), ".agents/project/profiles", safeRelative(id, "profile id"), "profile.json");
178
222
  if (!existsSync(path))
@@ -0,0 +1,9 @@
1
+ import type { ArchivedTaskSummary, TaskState } from "../model.js";
2
+ export declare const taskRecordContent: (task: TaskState) => string;
3
+ /** Builds the immutable archive-catalog summary for a terminal task from its record content. */
4
+ export declare const archivedSummary: (task: TaskState, content?: string) => ArchivedTaskSummary;
5
+ /** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
6
+ export declare const buildArchiveRecord: (task: TaskState) => {
7
+ content: string;
8
+ summary: ArchivedTaskSummary;
9
+ };
@@ -0,0 +1,26 @@
1
+ import { digest } from "./domain.js";
2
+ import { archiveRecordPath } from "./paths.js";
3
+ export const taskRecordContent = (task) => `${JSON.stringify(task, null, 2)}\n`;
4
+ /** Builds the immutable archive-catalog summary for a terminal task from its record content. */
5
+ export const archivedSummary = (task, content = taskRecordContent(task)) => ({
6
+ id: task.id,
7
+ title: task.title,
8
+ type: task.type,
9
+ phase: task.phase,
10
+ gate: task.gate,
11
+ status: task.status,
12
+ risk: task.risk,
13
+ areas: task.areas,
14
+ record: archiveRecordPath(task.id),
15
+ digest: digest(content),
16
+ lessonCount: task.lessons?.length ?? 0,
17
+ predecessorTaskId: task.predecessorTaskId,
18
+ successorTaskId: task.successorTaskId,
19
+ createdAt: task.createdAt,
20
+ updatedAt: task.updatedAt
21
+ });
22
+ /** Single terminal-to-archive policy shared by auto-archive on save and schema migration. */
23
+ export const buildArchiveRecord = (task) => {
24
+ const content = taskRecordContent(task);
25
+ return { content, summary: archivedSummary(task, content) };
26
+ };
@@ -0,0 +1,18 @@
1
+ import type { TaskState } from "../model.js";
2
+ export type NextActionActions = Array<{
3
+ id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
4
+ command: string;
5
+ }>;
6
+ export interface CommandKit {
7
+ continueItem: (taskId: string, itemId: string) => string;
8
+ continueVerification: (taskId: string) => string;
9
+ continuePhase: (taskId: string, phase: string) => string;
10
+ recoverG1Wait: (taskId: string) => string;
11
+ recoverGatelessWrap: (taskId: string) => string;
12
+ approveGate: (taskId: string, gate: string) => string;
13
+ handoffActions: (task: TaskState) => NextActionActions;
14
+ pausedActions: (taskId: string) => NextActionActions;
15
+ repairHandoffAction: (taskId: string, kind: "review_exhausted" | "repair_exhausted") => NextActionActions;
16
+ }
17
+ /** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
18
+ export declare const commandKit: (root?: string) => CommandKit;
@@ -0,0 +1,27 @@
1
+ import { join, resolve } from "node:path";
2
+ /** Command-string synthesis for `nextAction`: script paths, `--root` suffixes, and CLI placeholders exist only here. */
3
+ export const commandKit = (root) => {
4
+ const suffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
5
+ const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
6
+ const stateScript = script("state.mjs");
7
+ const contextScript = script("context.mjs");
8
+ return {
9
+ continueItem: (taskId, itemId) => `${contextScript} ${taskId} --phase build --item ${itemId}${suffix}`,
10
+ continueVerification: (taskId) => `${contextScript} ${taskId} --phase build${suffix}`,
11
+ continuePhase: (taskId, phase) => `${contextScript} ${taskId} --phase ${phase}${suffix}`,
12
+ recoverG1Wait: (taskId) => `${contextScript} ${taskId} --phase plan${suffix}`,
13
+ recoverGatelessWrap: (taskId) => `${stateScript} task status ${taskId} --status active${suffix}`,
14
+ approveGate: (taskId, gate) => `${stateScript} gate approve ${taskId} --gate ${gate} --source <explicit-user-approval>${suffix}`,
15
+ handoffActions: (task) => [
16
+ ...(task.phase === "build" ? [{ id: "reopen_g1", command: `${stateScript} task reopen ${task.id} --to plan --reason <reason> --source <explicit-user-request>${suffix}` }] : []),
17
+ { id: "create_successor", command: `${stateScript} task create <new-task-id> --title <title>${suffix}` },
18
+ { id: "supersede", command: `${stateScript} task supersede ${task.id} --successor <new-task-id> --reason <reason> --source <explicit-user-request>${suffix}` },
19
+ { id: "close", command: `${stateScript} task close ${task.id} --reason <reason> --source <explicit-user-request>${suffix}` }
20
+ ],
21
+ pausedActions: (taskId) => [
22
+ { id: "resume", command: `${stateScript} task status ${taskId} --status active${suffix}` },
23
+ { id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind <kind> --reason <reason> --source <source>${suffix}` }
24
+ ],
25
+ repairHandoffAction: (taskId, kind) => [{ id: "record_handoff", command: `${stateScript} task handoff ${taskId} --kind ${kind} --reason <summary-of-exhausted-repairs> --source <agent>${suffix}` }]
26
+ };
27
+ };
@@ -0,0 +1,9 @@
1
+ import type { Gate, HandoffKind, Phase, TaskState, WorkflowState } from "../model.js";
2
+ export declare const emptyState: () => WorkflowState;
3
+ export declare const legacyG2WaitSource = "schema-v4-migration";
4
+ export declare const phases: Phase[];
5
+ export declare const gates: Gate[];
6
+ export declare const handoffKinds: HandoffKind[];
7
+ export declare const terminal: (task: TaskState) => boolean;
8
+ export declare const phaseGate: (phase: Phase) => Gate;
9
+ export declare const digest: (content: string) => string;
@@ -0,0 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+ export const emptyState = () => ({ schemaVersion: 4, tasks: {}, archive: {} });
3
+ export const legacyG2WaitSource = "schema-v4-migration";
4
+ export const phases = ["clarify", "plan", "build", "wrap", "done"];
5
+ export const gates = ["none", "G0_confirm", "G1_review", "G2_codereview"];
6
+ export const handoffKinds = ["repair_exhausted", "review_exhausted", "g2_failed", "release_failed", "structural_change", "other"];
7
+ export const terminal = (task) => task.status === "done" || task.status === "closed" || task.status === "superseded";
8
+ export const phaseGate = (phase) => phase === "clarify" ? "G0_confirm" : phase === "plan" ? "G1_review" : phase === "build" ? "G2_codereview" : "none";
9
+ export const digest = (content) => createHash("sha256").update(content).digest("hex");
@@ -0,0 +1,7 @@
1
+ export declare const iso: (value: unknown) => value is string;
2
+ export declare const text: (value: unknown) => value is string;
3
+ export declare const idPattern: RegExp;
4
+ export declare const audited: (reason: string, source: string) => void;
5
+ export declare const tokens: (value: string) => string[];
6
+ /** Rejects artifact paths that escape the project-owned tasks directory. */
7
+ export declare const assertTaskArtifactPath: (id: string, path: string) => void;
@@ -0,0 +1,17 @@
1
+ import { isAbsolute, normalize } from "node:path";
2
+ export const iso = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(Date.parse(value)).toISOString() === value;
3
+ export const text = (value) => typeof value === "string" && Boolean(value.trim());
4
+ export const idPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ export const audited = (reason, source) => {
6
+ if (!text(reason) || !text(source))
7
+ throw new Error("Lifecycle mutation requires non-empty --reason and --source");
8
+ };
9
+ export const tokens = (value) => [...new Set(value.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter((item) => item.length > 1))];
10
+ /** Rejects artifact paths that escape the project-owned tasks directory. */
11
+ export const assertTaskArtifactPath = (id, path) => {
12
+ const separator = process.platform === "win32" ? "\\" : "/";
13
+ const cleaned = normalize(path);
14
+ if (isAbsolute(cleaned) || cleaned === ".." || cleaned.startsWith(`..${separator}`) || !cleaned.startsWith(`.agents${separator}data${separator}tasks${separator}`)) {
15
+ throw new Error(`Invalid artifact path for ${id}: ${path}`);
16
+ }
17
+ };
@@ -0,0 +1,10 @@
1
+ export declare const assertNoSymlinkPath: (root: string, relative: string) => void;
2
+ export declare const writeAtomic: (path: string, content: string) => void;
3
+ export declare const parseJsonText: (content: string, label: string, recovery?: string) => unknown;
4
+ export declare const parseJsonFile: (path: string, label: string, recovery?: string) => unknown;
5
+ /** Resolves a project-relative state path to its absolute form, rejecting symlink-crossing variants first. */
6
+ export declare const safeResolvedPath: (root: string, relative: string) => string;
7
+ /** Loads and validates a canonical JSON state file, or produces the fallback when the file does not exist. */
8
+ export declare const loadJsonOr: <T>(root: string, relative: string, validate: (value: unknown) => T, fallback: () => T, label: string, recovery?: string) => T;
9
+ /** Persists a JSON state file atomically in the canonical 2-space, trailing-newline formatting. */
10
+ export declare const saveJson: (root: string, relative: string, value: unknown) => void;
@@ -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
+ };
@@ -1,4 +1,5 @@
1
1
  import type { Gate, Phase, TaskState } from "../model.js";
2
+ import { type NextActionActions } from "./commands.js";
2
3
  export interface NextAction {
3
4
  classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
4
5
  phase: Phase;
@@ -8,10 +9,7 @@ export interface NextAction {
8
9
  successorTaskId?: string;
9
10
  itemId?: string;
10
11
  remainingItems?: number;
11
- actions?: Array<{
12
- id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
13
- command: string;
14
- }>;
12
+ actions?: NextActionActions;
15
13
  reason: string;
16
14
  }
17
15
  /** Validates execution dependencies and reports which pending items are ready to dispatch. */
@@ -22,4 +20,5 @@ export declare const readyTaskItems: (task: TaskState) => {
22
20
  waitingOn: string[];
23
21
  }>;
24
22
  };
23
+ /** Classifies the next lifecycle action for a task and attaches root-qualified command strings. */
25
24
  export declare const nextAction: (task: TaskState, root?: string) => NextAction;
@@ -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 = () => {