@felixdotgo/aidlc-kit 0.1.0 β†’ 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 (65) hide show
  1. package/README.md +16 -6
  2. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
  3. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
  4. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +6 -1
  5. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +6 -0
  6. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +8 -3
  7. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +1 -0
  8. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +7 -1
  9. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
  10. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +2 -2
  11. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
  12. package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
  13. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +2 -2
  14. package/dist/src/adapters.js +8 -1
  15. package/dist/src/cli.js +111 -95
  16. package/dist/src/context.js +2 -0
  17. package/dist/src/gate.d.ts +1 -0
  18. package/dist/src/gate.js +48 -35
  19. package/dist/src/installer.d.ts +5 -0
  20. package/dist/src/installer.js +116 -80
  21. package/dist/src/model.d.ts +1 -1
  22. package/dist/src/profiles.d.ts +6 -0
  23. package/dist/src/profiles.js +11 -0
  24. package/dist/src/project-migration.d.ts +1 -0
  25. package/dist/src/project-migration.js +7 -0
  26. package/dist/src/state/archive.d.ts +17 -0
  27. package/dist/src/state/archive.js +60 -0
  28. package/dist/src/state/items.d.ts +25 -0
  29. package/dist/src/state/items.js +146 -0
  30. package/dist/src/state/lessons.d.ts +7 -0
  31. package/dist/src/state/lessons.js +67 -0
  32. package/dist/src/state/lifecycle.d.ts +5 -0
  33. package/dist/src/state/lifecycle.js +78 -0
  34. package/dist/src/state/lock.d.ts +7 -0
  35. package/dist/src/state/lock.js +124 -0
  36. package/dist/src/state/memory.d.ts +8 -0
  37. package/dist/src/state/memory.js +88 -0
  38. package/dist/src/state/render.d.ts +26 -0
  39. package/dist/src/state/render.js +29 -0
  40. package/dist/src/state/shared.d.ts +29 -0
  41. package/dist/src/state/shared.js +71 -0
  42. package/dist/src/state/store.d.ts +4 -0
  43. package/dist/src/state/store.js +81 -0
  44. package/dist/src/state/transitions.d.ts +21 -0
  45. package/dist/src/state/transitions.js +93 -0
  46. package/dist/src/state/validate.d.ts +4 -0
  47. package/dist/src/state/validate.js +100 -0
  48. package/dist/src/state.d.ts +15 -117
  49. package/dist/src/state.js +15 -815
  50. package/dist/src/tracker.d.ts +7 -0
  51. package/dist/src/tracker.js +3 -0
  52. package/dist/src/upgrade-board.d.ts +5 -0
  53. package/dist/src/upgrade-board.js +89 -0
  54. package/dist/src/upgrade.d.ts +4 -5
  55. package/dist/src/upgrade.js +197 -214
  56. package/dist/src/workflow.js +1 -1
  57. package/docs/README.md +2 -2
  58. package/docs/architecture.md +4 -1
  59. package/docs/command-reference.md +15 -12
  60. package/docs/configuration.md +7 -4
  61. package/docs/coordination-service.md +5 -2
  62. package/docs/development.md +15 -1
  63. package/docs/operating-workflow.md +4 -1
  64. package/docs/testing-and-release.md +7 -7
  65. package/package.json +2 -2
package/dist/src/gate.js CHANGED
@@ -11,7 +11,7 @@ const artifactContent = (root, path) => {
11
11
  }
12
12
  catch {
13
13
  return undefined;
14
- }
14
+ } /* intentional: unreadable/unsafe artifact is treated as absent */
15
15
  };
16
16
  const artifactDiagnostics = (root, task, names) => names.flatMap((name) => {
17
17
  const path = task.artifacts[name];
@@ -24,10 +24,39 @@ const artifactDiagnostics = (root, task, names) => names.flatMap((name) => {
24
24
  return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }];
25
25
  }
26
26
  });
27
- export const checkGate = (root, state, taskId, gate) => {
28
- const task = state.tasks[taskId];
29
- if (!task)
30
- return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown task: ${taskId}` }];
27
+ const missingHeadings = (content, headings) => headings.filter((heading) => !content.includes(heading));
28
+ const confirmGateDiagnostics = (root, task) => {
29
+ const diagnostics = artifactDiagnostics(root, task, ["intent"]);
30
+ const content = artifactContent(root, task.artifacts.intent);
31
+ if (content !== undefined)
32
+ for (const heading of missingHeadings(content, ["## πŸ“‹ Problem", "## πŸ—ΊοΈ Affected areas", "## πŸ’­ Assumptions", "## ❓ Open questions", "## 🎯 Scope"]))
33
+ diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` });
34
+ return diagnostics;
35
+ };
36
+ const reviewGateDiagnostics = (root, task) => {
37
+ const diagnostics = artifactDiagnostics(root, task, ["intent", "design", "workplan"]);
38
+ diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION"));
39
+ const content = artifactContent(root, task.artifacts.design);
40
+ if (content !== undefined)
41
+ for (const heading of missingHeadings(content, ["## 🧩 Solution per affected area", "## πŸ“Œ Spec traceability", "## πŸ”— Cross-service contracts", "## ⚠️ Risks / edge cases"]))
42
+ diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` });
43
+ return diagnostics;
44
+ };
45
+ const codeReviewGateDiagnostics = (root, task) => {
46
+ const diagnostics = artifactDiagnostics(root, task, ["intent", "design", "workplan"]);
47
+ if (task.tasks.some((item) => item.status !== "done" && item.status !== "deferred"))
48
+ diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" });
49
+ for (const area of task.areas)
50
+ if (!hasAreaVerification(task, area))
51
+ diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` });
52
+ if (!hasReview(task))
53
+ diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 review evidence must pass" });
54
+ const bounds = repairBounds(task);
55
+ if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted))
56
+ diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" });
57
+ return diagnostics;
58
+ };
59
+ const generalGateDiagnostics = (taskId, task, gate) => {
31
60
  const diagnostics = [];
32
61
  if (task.gate !== gate)
33
62
  diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` });
@@ -39,36 +68,20 @@ export const checkGate = (root, state, taskId, gate) => {
39
68
  diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" });
40
69
  if (!task.title.trim() || !task.areas.length)
41
70
  diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" });
42
- if (gate === "G0_confirm") {
43
- diagnostics.push(...artifactDiagnostics(root, task, ["intent"]));
44
- const content = artifactContent(root, task.artifacts.intent);
45
- if (content !== undefined)
46
- for (const heading of ["## πŸ“‹ Problem", "## πŸ—ΊοΈ Affected areas", "## πŸ’­ Assumptions", "## ❓ Open questions", "## 🎯 Scope"])
47
- if (!content.includes(heading))
48
- diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` });
49
- }
50
- if (gate === "G1_review") {
51
- diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"]));
52
- diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION"));
53
- const content = artifactContent(root, task.artifacts.design);
54
- if (content !== undefined)
55
- for (const heading of ["## 🧩 Solution per affected area", "## πŸ“Œ Spec traceability", "## πŸ”— Cross-service contracts", "## ⚠️ Risks / edge cases"])
56
- if (!content.includes(heading))
57
- diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` });
58
- }
59
- if (gate === "G2_codereview") {
60
- diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"]));
61
- if (task.tasks.some((item) => item.status !== "done" && item.status !== "deferred"))
62
- diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" });
63
- for (const area of task.areas)
64
- if (!hasAreaVerification(task, area))
65
- diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` });
66
- if (!hasReview(task))
67
- diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 review evidence must pass" });
68
- const bounds = repairBounds(task);
69
- if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted))
70
- diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" });
71
- }
71
+ return diagnostics;
72
+ };
73
+ /** Runs all diagnostics for a gate and returns them (empty-ish with GATE_OK when clean). */
74
+ export const checkGate = (root, state, taskId, gate) => {
75
+ const task = state.tasks[taskId];
76
+ if (!task)
77
+ return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown task: ${taskId}` }];
78
+ const diagnostics = generalGateDiagnostics(taskId, task, gate);
79
+ if (gate === "G0_confirm")
80
+ diagnostics.push(...confirmGateDiagnostics(root, task));
81
+ if (gate === "G1_review")
82
+ diagnostics.push(...reviewGateDiagnostics(root, task));
83
+ if (gate === "G2_codereview")
84
+ diagnostics.push(...codeReviewGateDiagnostics(root, task));
72
85
  if (!diagnostics.length)
73
86
  diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` });
74
87
  return diagnostics;
@@ -7,9 +7,14 @@ export declare const detectedAgents: (root: string) => AgentId[];
7
7
  export declare const selectAgents: (options: Pick<InitOptions, "root" | "agents" | "all">) => AgentId[];
8
8
  /** Include an explicitly selected loop config in the same preview/transaction as setup. */
9
9
  export declare const planLoopSelection: (plan: PlannedWrite[], selection?: LoopConfig) => void;
10
+ /** Plans an init transaction: managed workflow files, manifest, and project-owned files. */
10
11
  export declare const planInit: (options: InitOptions) => PlannedWrite[];
12
+ /** Applies a planned init/uninstall transaction to disk. */
11
13
  export declare const applyPlan: (root: string, plan: PlannedWrite[]) => void;
12
14
  export declare const formatPlan: (plan: PlannedWrite[]) => string;
15
+ /** Plans an uninstall transaction that removes only unchanged managed assets. */
13
16
  export declare const planUninstall: (root: string) => PlannedWrite[];
17
+ /** Reports installed workflow version, schema, and detected agents. */
14
18
  export declare const status: (root: string) => string;
19
+ /** Validates the installed workflow manifest and local config/state. */
15
20
  export declare const doctor: (root: string, strict?: boolean) => string;
@@ -30,11 +30,11 @@ export const readManifest = (root) => {
30
30
  try {
31
31
  const value = JSON.parse(readFileSync(path, "utf8"));
32
32
  const validFiles = value.files && typeof value.files === "object" && !Array.isArray(value.files) && Object.values(value.files).every((item) => item && typeof item.hash === "string" && typeof item.owner === "string" && ["replace", "managed-block"].includes(item.strategy));
33
- return value.schemaVersion === 2 && typeof value.packageVersion === "string" && value.workflow === "AI-DLC" && value.source === "local-package-assets" && value.remoteUpdates === false && value.managedBy === "aidlc-workflow" && Array.isArray(value.adapters) && validFiles ? value : undefined;
33
+ return value.schemaVersion === 2 && typeof value.packageVersion === "string" && value.workflow === "AI-DLC" && value.source === "local-package-assets" && value.remoteUpdates === false && (value.managedBy === "aidlc-kit" || value.managedBy === "aidlc-workflow") && Array.isArray(value.adapters) && validFiles ? value : undefined;
34
34
  }
35
35
  catch {
36
36
  return undefined;
37
- }
37
+ } /* intentional: a malformed manifest is treated as "not installed" */
38
38
  };
39
39
  export const detectedAgents = (root) => installableAdapters().filter((adapter) => adapter.detect(root)).map((adapter) => adapter.id);
40
40
  export const selectAgents = (options) => {
@@ -88,6 +88,27 @@ export const planLoopSelection = (plan, selection) => {
88
88
  config.action = previousAction === "create" || previousAction === "migrate" ? previousAction : "update";
89
89
  config.reason = "explicit loop mode selection";
90
90
  };
91
+ /** Merges a later owner of an already-planned shared managed-block path into the emitted entry. */
92
+ const mergeIntoPlannedBlock = (root, previous, spec) => {
93
+ const target = safeTarget(root, spec.path);
94
+ const current = existsSync(target) ? readFileSync(target, "utf8") : "";
95
+ previous.content = mergeManagedBlock(spec, previous.content);
96
+ previous.action = existsSync(target) ? (previous.content === current ? "skip" : "update") : "create";
97
+ previous.reason = existsSync(target) ? "managed prompt block" : "new managed prompt";
98
+ };
99
+ /** Plans managed workflow files, the manifest, and project-owned files; later block owners merge in line. */
100
+ const planManagedWrites = (root, files, force, installed) => {
101
+ const plan = [];
102
+ for (const spec of files) {
103
+ const previous = spec.strategy === "managed-block" ? plan.find((item) => item.path === spec.path && ["create", "update", "skip"].includes(item.action)) : undefined;
104
+ if (!previous)
105
+ plan.push(planFile(root, spec, force, installed));
106
+ else
107
+ mergeIntoPlannedBlock(root, previous, spec);
108
+ }
109
+ return plan;
110
+ };
111
+ /** Plans an init transaction: managed workflow files, manifest, and project-owned files. */
91
112
  export const planInit = (options) => {
92
113
  const root = resolve(options.root);
93
114
  const installed = readManifest(root);
@@ -100,23 +121,12 @@ export const planInit = (options) => {
100
121
  const agentFiles = selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
101
122
  const managedFiles = [...coreWorkflowFiles(), ...agentFiles];
102
123
  const files = [...managedFiles, manifestSpec(managedFiles, selected), ...initialProjectFiles()];
103
- const plan = [];
104
- for (const spec of files) {
105
- const previous = spec.strategy === "managed-block" ? plan.find((item) => item.path === spec.path && ["create", "update", "skip"].includes(item.action)) : undefined;
106
- if (!previous) {
107
- plan.push(planFile(root, spec, options.force, installed));
108
- continue;
109
- }
110
- const target = safeTarget(root, spec.path);
111
- const current = existsSync(target) ? readFileSync(target, "utf8") : "";
112
- previous.content = mergeManagedBlock(spec, previous.content);
113
- previous.action = existsSync(target) ? (previous.content === current ? "skip" : "update") : "create";
114
- previous.reason = existsSync(target) ? "managed prompt block" : "new managed prompt";
115
- }
124
+ const plan = planManagedWrites(root, files, options.force, installed);
116
125
  planProjectLayoutMigration(root, plan);
117
126
  planLoopSelection(plan, options.loop);
118
127
  return plan;
119
128
  };
129
+ /** Applies a planned init/uninstall transaction to disk. */
120
130
  export const applyPlan = (root, plan) => {
121
131
  for (const write of plan.filter((item) => item.action === "delete"))
122
132
  rmSync(safeTarget(root, write.path), { force: true });
@@ -132,82 +142,98 @@ const hasOwnedManifest = (root) => {
132
142
  if (!existsSync(path))
133
143
  return false;
134
144
  try {
135
- return JSON.parse(readFileSync(path, "utf8")).managedBy === "aidlc-workflow";
145
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
146
+ return parsed.managedBy === "aidlc-kit" || parsed.managedBy === "aidlc-workflow";
136
147
  }
137
148
  catch {
138
149
  return false;
139
- }
150
+ } /* intentional: a malformed/missing manifest is not owned */
140
151
  };
141
152
  const removeManagedBlock = (current, owner) => {
142
153
  const start = `<!-- aidlc-installer:${owner}:start -->`;
143
154
  const end = `<!-- aidlc-installer:${owner}:end -->`;
144
155
  return current.replace(new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
145
156
  };
157
+ /** Maps one manifest inventory record onto its current spec and installed hash. */
158
+ const uninstallEntry = (key, item, currentSpecs) => {
159
+ const separator = key.lastIndexOf("#");
160
+ const path = separator > 0 ? key.slice(0, separator) : key;
161
+ const owner = separator > 0 ? key.slice(separator + 1) : item.owner;
162
+ const spec = currentSpecs.find((candidate) => candidate.path === path && (candidate.strategy !== "managed-block" || candidate.owner === owner)) ?? { path, owner, strategy: item.strategy, content: "", ownershipClass: "managed" };
163
+ return { spec, hash: item.hash };
164
+ };
165
+ /** Groups installed specs (or all current specs without a manifest) by path; shared blocks carry multiple owners. */
166
+ const uninstallGroups = (installed, currentSpecs) => {
167
+ const entries = installed ? Object.entries(installed.files).map(([key, item]) => uninstallEntry(key, item, currentSpecs)) : currentSpecs.map((spec) => ({ spec }));
168
+ const groups = new Map();
169
+ for (const entry of entries)
170
+ groups.set(entry.spec.path, [...(groups.get(entry.spec.path) ?? []), entry]);
171
+ return groups;
172
+ };
173
+ /** Removes each verified owner block from a shared file; stops at the first user-modified block. */
174
+ const stripManagedBlocks = (group, current) => {
175
+ let next = current;
176
+ let modified;
177
+ for (const { spec, hash } of group) {
178
+ const block = managedBlock(spec, next);
179
+ // A v0.2.1 single-marker block cannot be removed safely; retain ownership instead of orphaning it.
180
+ if (!block) {
181
+ if (next.includes(`<!-- aidlc-installer:${spec.owner} -->`))
182
+ modified = spec;
183
+ continue;
184
+ }
185
+ if (contentHash(block) !== (hash ?? contentHash(spec.content.trimEnd()))) {
186
+ modified = spec;
187
+ break;
188
+ }
189
+ next = removeManagedBlock(next, spec.owner);
190
+ }
191
+ return { next, modified };
192
+ };
193
+ /** Decides the plan entry for a whole-file (non shared-block) path. */
194
+ const uninstallWholeFile = (group, exists, current) => {
195
+ if (!exists)
196
+ return { ...group[0].spec, action: "skip", reason: "not installed" };
197
+ if (!group.some((entry) => entry.spec.strategy === "managed-block")) {
198
+ const { spec, hash } = group[0];
199
+ const installedHash = hash ?? contentHash(spec.content);
200
+ return contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" };
201
+ }
202
+ const { next, modified } = stripManagedBlocks(group, current);
203
+ const legacyResidue = group.find(({ spec }) => next.includes(`<!-- aidlc-installer:${spec.owner} -->`));
204
+ const ownerSpec = group[0].spec;
205
+ if (modified)
206
+ return { ...modified, action: "preserve", reason: "managed block was modified" };
207
+ if (legacyResidue)
208
+ return { ...legacyResidue.spec, action: "preserve", reason: "legacy prompt marker requires manual removal" };
209
+ if (next === current)
210
+ return { ...ownerSpec, action: "skip", reason: "owned block not found" };
211
+ if (next)
212
+ return { ...ownerSpec, content: `${next}\n`, action: "update", reason: "remove owned prompt block" };
213
+ return { ...ownerSpec, content: "", action: "delete", reason: "remove owned prompt file" };
214
+ };
215
+ /** Keeps the ownership manifest only while modified managed files remain on disk. */
216
+ const manifestDisposition = (plan) => {
217
+ const preserved = plan.some((item) => item.action === "preserve");
218
+ return { path: ".agents/aidlc/manifest.json", owner: "aidlc-core", content: "", ownershipClass: "managed", action: preserved ? "preserve" : "delete", reason: preserved ? "retained while modified managed files remain" : "remove ownership manifest" };
219
+ };
220
+ /** Plans an uninstall transaction that removes only unchanged managed assets. */
146
221
  export const planUninstall = (root) => {
147
222
  const project = resolve(root);
148
223
  if (!hasOwnedManifest(project))
149
- throw new Error("No aidlc-workflow ownership manifest found; refusing to remove files.");
224
+ throw new Error("No managed ownership manifest (.agents/aidlc/manifest.json) found; refusing to remove files.");
150
225
  const installed = readManifest(project);
151
226
  const currentSpecs = [...coreWorkflowFiles(), ...resolvedAdapters().flatMap((adapter) => adapter.files()).map((file) => ({ ...file, ownershipClass: "managed" }))];
152
- const entries = installed
153
- ? Object.entries(installed.files).map(([key, item]) => {
154
- const separator = key.lastIndexOf("#");
155
- const path = separator > 0 ? key.slice(0, separator) : key;
156
- const owner = separator > 0 ? key.slice(separator + 1) : item.owner;
157
- const spec = currentSpecs.find((candidate) => candidate.path === path && (candidate.strategy !== "managed-block" || candidate.owner === owner)) ?? { path, owner, strategy: item.strategy, content: "", ownershipClass: "managed" };
158
- return { spec, hash: item.hash };
159
- })
160
- : currentSpecs.map((spec) => ({ spec }));
161
- const grouped = new Map();
162
- for (const entry of entries)
163
- grouped.set(entry.spec.path, [...(grouped.get(entry.spec.path) ?? []), entry]);
164
227
  const plan = [];
165
- for (const [path, group] of grouped) {
228
+ for (const [path, group] of uninstallGroups(installed, currentSpecs)) {
166
229
  const target = safeTarget(project, path);
167
230
  const exists = existsSync(target);
168
- const current = exists ? readFileSync(target, "utf8") : "";
169
- if (!exists) {
170
- plan.push({ ...group[0].spec, action: "skip", reason: "not installed" });
171
- continue;
172
- }
173
- if (!group.some((entry) => entry.spec.strategy === "managed-block")) {
174
- const { spec, hash } = group[0];
175
- const installedHash = hash ?? contentHash(spec.content);
176
- plan.push(contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" });
177
- continue;
178
- }
179
- let next = current;
180
- let modified;
181
- for (const { spec, hash } of group) {
182
- const block = managedBlock(spec, next);
183
- // A v0.2.1 single-marker block cannot be removed safely; retain ownership instead of orphaning it.
184
- if (!block) {
185
- if (next.includes(`<!-- aidlc-installer:${spec.owner} -->`))
186
- modified = spec;
187
- continue;
188
- }
189
- if (contentHash(block) !== (hash ?? contentHash(spec.content.trimEnd()))) {
190
- modified = spec;
191
- break;
192
- }
193
- next = removeManagedBlock(next, spec.owner);
194
- }
195
- const ownerSpec = group[0].spec;
196
- const legacyResidue = group.find(({ spec }) => next.includes(`<!-- aidlc-installer:${spec.owner} -->`));
197
- if (modified)
198
- plan.push({ ...modified, action: "preserve", reason: "managed block was modified" });
199
- else if (legacyResidue)
200
- plan.push({ ...legacyResidue.spec, action: "preserve", reason: "legacy prompt marker requires manual removal" });
201
- else if (next === current)
202
- plan.push({ ...ownerSpec, action: "skip", reason: "owned block not found" });
203
- else if (next)
204
- plan.push({ ...ownerSpec, content: `${next}\n`, action: "update", reason: "remove owned prompt block" });
205
- else
206
- plan.push({ ...ownerSpec, content: "", action: "delete", reason: "remove owned prompt file" });
231
+ plan.push(uninstallWholeFile(group, exists, exists ? readFileSync(target, "utf8") : ""));
207
232
  }
208
- plan.push({ path: ".agents/aidlc/manifest.json", owner: "aidlc-core", content: "", ownershipClass: "managed", action: plan.some((item) => item.action === "preserve") ? "preserve" : "delete", reason: plan.some((item) => item.action === "preserve") ? "retained while modified managed files remain" : "remove ownership manifest" });
233
+ plan.push(manifestDisposition(plan));
209
234
  return plan;
210
235
  };
236
+ /** Reports installed workflow version, schema, and detected agents. */
211
237
  export const status = (root) => {
212
238
  const manifest = readManifest(root);
213
239
  const legacyPath = join(resolve(root), ".agents/aidlc/manifest.json");
@@ -218,6 +244,23 @@ export const status = (root) => {
218
244
  `detected agents: ${detectedAgents(root).join(", ") || "none"}`
219
245
  ].join("\n");
220
246
  };
247
+ /** Path portion of a manifest inventory key (`path#owner` for shared managed blocks). */
248
+ const inventoryKeyPath = (key) => {
249
+ const separator = key.lastIndexOf("#");
250
+ return separator > 0 ? key.slice(0, separator) : key;
251
+ };
252
+ /** Manifest inventory paths whose on-disk content no longer matches the recorded owner hash. */
253
+ const modifiedManagedPaths = (root, manifest) => {
254
+ const modified = Object.entries(manifest.files).filter(([key, item]) => {
255
+ const target = safeTarget(root, inventoryKeyPath(key));
256
+ if (!existsSync(target))
257
+ return true;
258
+ const spec = { owner: item.owner, strategy: item.strategy };
259
+ return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
260
+ });
261
+ return [...new Set(modified.map(([key]) => inventoryKeyPath(key)))];
262
+ };
263
+ /** Validates the installed workflow manifest and local config/state. */
221
264
  export const doctor = (root, strict = false) => {
222
265
  const manifestPath = join(resolve(root), ".agents/aidlc/manifest.json");
223
266
  if (!existsSync(manifestPath))
@@ -229,7 +272,7 @@ export const doctor = (root, strict = false) => {
229
272
  catch {
230
273
  return "ERROR: workflow manifest is invalid JSON.";
231
274
  }
232
- if (parsed.remoteUpdates || parsed.source !== "local-package-assets" || parsed.managedBy !== "aidlc-workflow")
275
+ if (parsed.remoteUpdates || parsed.source !== "local-package-assets" || (parsed.managedBy !== "aidlc-kit" && parsed.managedBy !== "aidlc-workflow"))
233
276
  return "ERROR: workflow manifest violates local-only asset policy.";
234
277
  if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > 2)
235
278
  return `ERROR: installed manifest schema ${parsed.schemaVersion} is newer than this CLI supports.`;
@@ -243,14 +286,7 @@ export const doctor = (root, strict = false) => {
243
286
  catch (error) {
244
287
  return `ERROR: local config/state is invalid: ${error instanceof Error ? error.message : String(error)}`;
245
288
  }
246
- const modified = [...new Set(Object.entries(manifest.files).filter(([key, item]) => {
247
- const separator = key.lastIndexOf("#");
248
- const target = safeTarget(root, separator > 0 ? key.slice(0, separator) : key);
249
- if (!existsSync(target))
250
- return true;
251
- const spec = { owner: item.owner, strategy: item.strategy };
252
- return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
253
- }).map(([key]) => key.includes("#") ? key.slice(0, key.lastIndexOf("#")) : key))];
289
+ const modified = modifiedManagedPaths(root, manifest);
254
290
  if (modified.length)
255
291
  return `${strict ? "ERROR" : "WARN"}: managed integrity differs: ${modified.join(", ")}`;
256
292
  return `OK: local workflow ${manifest.packageVersion} is valid; remote updates are disabled.`;
@@ -253,7 +253,7 @@ export interface WorkflowManifest {
253
253
  workflow: "AI-DLC";
254
254
  source: "local-package-assets";
255
255
  remoteUpdates: false;
256
- managedBy: "aidlc-workflow";
256
+ managedBy: "aidlc-kit" | "aidlc-workflow";
257
257
  adapters: AgentId[];
258
258
  files: Record<string, ManifestInventoryItem>;
259
259
  }
@@ -1,10 +1,15 @@
1
1
  import type { LoopConfig, McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
2
2
  export declare const builtInProfiles: Readonly<Record<string, Profile>>;
3
+ /** Validates and normalizes a loop-execution configuration block. */
3
4
  export declare const validateLoopConfig: (value: unknown) => LoopConfig;
5
+ /** Validates a single profile file (built-in or local). */
4
6
  export declare const validateProfile: (value: unknown, label?: string) => Profile;
5
7
  export declare const defaultConfig: () => ProjectConfig;
8
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
6
9
  export declare const loadProjectConfig: (root: string) => ProjectConfig;
10
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
7
11
  export declare const resolveProfiles: (root: string, ids: string[]) => Profile[];
12
+ /** Merges built-in and local profiles with the project config into the effective configuration. */
8
13
  export declare const resolveEffectiveConfig: (root: string, config: ProjectConfig) => {
9
14
  profiles: Profile[];
10
15
  discovery: {
@@ -27,4 +32,5 @@ export declare const resolveEffectiveConfig: (root: string, config: ProjectConfi
27
32
  mcp: McpConfig;
28
33
  loop: LoopConfig;
29
34
  };
35
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
30
36
  export declare const includedRuleFiles: (root: string, patterns: string[]) => string[];
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Project profiles and configuration. Loads `.agents/config.json`, validates
3
+ * every nested section (loop, tracker, MCP, risk, context), and resolves the
4
+ * effective configuration by merging built-in and local profiles.
5
+ */
1
6
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
2
7
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
3
8
  import { LOOP_PROVIDERS } from "./model.js";
@@ -23,6 +28,7 @@ const object = (value, label) => {
23
28
  throw new Error(`${label} must be an object`);
24
29
  return value;
25
30
  };
31
+ /** Validates and normalizes a loop-execution configuration block. */
26
32
  export const validateLoopConfig = (value) => {
27
33
  if (value === undefined)
28
34
  return { mode: "off" };
@@ -98,6 +104,7 @@ const mcp = (value) => {
98
104
  throw new Error("config.mcp.providers is invalid");
99
105
  return { enabled: true, endpoint: item.endpoint, workspace: item.workspace, ...(item.tokenEnv === undefined ? {} : { tokenEnv: item.tokenEnv }), ...(item.pollMs === undefined ? {} : { pollMs: Number(item.pollMs) }), ...(providers === undefined ? {} : { providers: providers }) };
100
106
  };
107
+ /** Validates a single profile file (built-in or local). */
101
108
  export const validateProfile = (value, label = "profile") => {
102
109
  const item = object(value, label);
103
110
  if (item.schemaVersion !== 1 || typeof item.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(item.id) || typeof item.topology !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(item.topology))
@@ -131,6 +138,7 @@ export const defaultConfig = () => ({
131
138
  mcp: { enabled: false },
132
139
  loop: { mode: "off" }
133
140
  });
141
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
134
142
  export const loadProjectConfig = (root) => {
135
143
  const path = join(resolve(root), ".agents/config.json");
136
144
  if (!existsSync(path))
@@ -173,6 +181,7 @@ const readLocalProfile = (root, id) => {
173
181
  throw new Error(`Profile escapes the project: ${id}`);
174
182
  return validateProfile(JSON.parse(readFileSync(path, "utf8")), id);
175
183
  };
184
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
176
185
  export const resolveProfiles = (root, ids) => {
177
186
  const resolved = [];
178
187
  const visiting = new Set();
@@ -195,6 +204,7 @@ export const resolveProfiles = (root, ids) => {
195
204
  return resolved;
196
205
  };
197
206
  const stable = (values) => [...new Set(values)];
207
+ /** Merges built-in and local profiles with the project config into the effective configuration. */
198
208
  export const resolveEffectiveConfig = (root, config) => {
199
209
  const profiles = resolveProfiles(root, config.extends);
200
210
  return {
@@ -209,6 +219,7 @@ export const resolveEffectiveConfig = (root, config) => {
209
219
  loop: config.loop ?? { mode: "off" }
210
220
  };
211
221
  };
222
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
212
223
  export const includedRuleFiles = (root, patterns) => {
213
224
  const project = resolve(root);
214
225
  const realProject = realpathSync(project);
@@ -1,2 +1,3 @@
1
1
  import type { PlannedWrite } from "./model.js";
2
+ /** Plans the full legacy-layout migration: config rewrite, `.aidlc/` tree, and legacy index files. */
2
3
  export declare const planProjectLayoutMigration: (project: string, plan: PlannedWrite[]) => void;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { resolveProjectPath } from "./project-path.js";
3
3
  const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe migration path");
4
+ /* Legacy `.aidlc/...` path translation into the canonical `.agents/` layout. */
4
5
  const legacyReference = (value) => {
5
6
  if (value === ".aidlc/config.json")
6
7
  return ".agents/config.json";
@@ -15,6 +16,7 @@ const legacyReference = (value) => {
15
16
  return value;
16
17
  };
17
18
  const rewriteStrings = (value) => Array.isArray(value) ? value.map((entry) => typeof entry === "string" ? legacyReference(entry) : entry) : value;
19
+ /** Rewrites legacy `.aidlc/` path references inside the JSON file formats that are known to hold them. */
18
20
  const rewriteKnownJson = (relative, content) => {
19
21
  const isConfig = relative === "config.json";
20
22
  const isProfile = /^profiles\/.+\/profile\.json$/.test(relative);
@@ -63,6 +65,7 @@ const legacyDestination = (relative) => {
63
65
  return `.agents/data/index/${relative.slice("index/".length)}`;
64
66
  return `.agents/project/${relative}`;
65
67
  };
68
+ /** Plans the copy of one legacy file into its destination, including a tracked delete of the source. */
66
69
  const planMigrationFile = (project, plan, sourcePath, destinationPath, legacyRelative) => {
67
70
  const source = safePath(project, sourcePath);
68
71
  const payload = migrationPayload(legacyRelative, readFileSync(source));
@@ -95,6 +98,7 @@ const planMigrationFile = (project, plan, sourcePath, destinationPath, legacyRel
95
98
  plan.push({ path: sourcePath, owner: "aidlc-project", ownershipClass: "project", content: "", action: "delete", reason: `remove migrated legacy project file after creating ${destinationPath}` });
96
99
  }
97
100
  };
101
+ /** Walks the legacy `.aidlc/` tree, refusing symlinks and special entries with a conflicted plan entry. */
98
102
  const planLegacyTreeMigration = (project, plan) => {
99
103
  const legacyRoot = safePath(project, ".aidlc");
100
104
  if (!existsSync(legacyRoot))
@@ -118,6 +122,7 @@ const planLegacyTreeMigration = (project, plan) => {
118
122
  };
119
123
  visit(legacyRoot);
120
124
  };
125
+ /** Rewrites legacy `.aidlc/` references inside an already-installed canonical project config. */
121
126
  const planExistingConfigRewrite = (project, plan) => {
122
127
  const path = ".agents/config.json";
123
128
  const target = safePath(project, path);
@@ -130,6 +135,7 @@ const planExistingConfigRewrite = (project, plan) => {
130
135
  if (item)
131
136
  Object.assign(item, { content: rewritten.content, action: "migrate", reason: "rewrite legacy .aidlc references in canonical project config" });
132
137
  };
138
+ /** Moves the legacy `.agents/data/state/{repo-map,specs-index}` files into the data/index directory. */
133
139
  const planLegacyIndexMigration = (project, plan) => {
134
140
  for (const name of ["repo-map.md", "specs-index.md"]) {
135
141
  const source = `.agents/data/state/${name}`;
@@ -137,6 +143,7 @@ const planLegacyIndexMigration = (project, plan) => {
137
143
  planMigrationFile(project, plan, source, `.agents/data/index/${name}`, `index/${name}`);
138
144
  }
139
145
  };
146
+ /** Plans the full legacy-layout migration: config rewrite, `.aidlc/` tree, and legacy index files. */
140
147
  export const planProjectLayoutMigration = (project, plan) => {
141
148
  planExistingConfigRewrite(project, plan);
142
149
  planLegacyTreeMigration(project, plan);
@@ -0,0 +1,17 @@
1
+ import type { LessonIndex, WorkflowState } from "../model.js";
2
+ /** Marks pre-existing ready G2 waits during migration; the marker is the only exemption from repair-bound enforcement. */
3
+ export declare const stampLegacyG2Waits: (state: WorkflowState) => string[];
4
+ export declare const prepareV4Migration: (value: WorkflowState) => {
5
+ state: WorkflowState;
6
+ records: Array<{
7
+ path: string;
8
+ content: string;
9
+ }>;
10
+ lessonIndex: LessonIndex;
11
+ stamped: string[];
12
+ };
13
+ export declare const migrateState: (root: string, state?: WorkflowState) => {
14
+ migrated: boolean;
15
+ archived: string[];
16
+ stamped: string[];
17
+ };
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { createLessonIndex, lessonStateDigest, rebuildLessonIndex } from "./lessons.js";
4
+ import { hasReview, hasVerification } from "./transitions.js";
5
+ import { loadState, saveState } from "./store.js";
6
+ import { validateState } from "./validate.js";
7
+ import { archiveRecordPath, assertNoSymlinkPath, legacyG2WaitSource, taskRecordContent, terminal, writeAtomic, archivedSummary } from "./shared.js";
8
+ /** Marks pre-existing ready G2 waits during migration; the marker is the only exemption from repair-bound enforcement. */
9
+ export const stampLegacyG2Waits = (state) => {
10
+ const stamped = [];
11
+ for (const task of Object.values(state.tasks)) {
12
+ if (task.legacyG2Wait !== undefined)
13
+ continue;
14
+ if (task.phase !== "build" || task.gate !== "G2_codereview" || task.status !== "blocked_on_user")
15
+ continue;
16
+ if (!task.tasks.every((item) => item.status === "done" || item.status === "deferred") || !hasVerification(task) || !hasReview(task))
17
+ continue;
18
+ task.legacyG2Wait = { migratedAt: task.updatedAt, source: legacyG2WaitSource };
19
+ stamped.push(task.id);
20
+ }
21
+ return stamped;
22
+ };
23
+ export const prepareV4Migration = (value) => {
24
+ const source = validateState(value);
25
+ const next = { schemaVersion: 4, tasks: {}, archive: {} };
26
+ const records = [];
27
+ for (const task of Object.values(source.tasks)) {
28
+ if (!terminal(task))
29
+ next.tasks[task.id] = task;
30
+ else {
31
+ const content = taskRecordContent(task);
32
+ next.archive[task.id] = archivedSummary(task, content);
33
+ records.push({ path: archiveRecordPath(task.id), content });
34
+ }
35
+ }
36
+ for (const item of Object.values(source.archive ?? {}))
37
+ next.archive[item.id] = item;
38
+ const stamped = Number(source.schemaVersion) < 4 ? stampLegacyG2Waits(next) : [];
39
+ validateState(next);
40
+ const lessons = Object.values(source.tasks).flatMap((task) => task.lessons ?? []);
41
+ return { state: next, records, lessonIndex: createLessonIndex(lessons, lessonStateDigest(next)), stamped };
42
+ };
43
+ export const migrateState = (root, state = loadState(root)) => {
44
+ if (state.schemaVersion === 4)
45
+ return { migrated: false, archived: [], stamped: [] };
46
+ const migration = prepareV4Migration(state);
47
+ for (const record of migration.records) {
48
+ assertNoSymlinkPath(root, record.path);
49
+ const path = join(resolve(root), record.path);
50
+ if (existsSync(path)) {
51
+ if (readFileSync(path, "utf8") !== record.content)
52
+ throw new Error(`Archived task record conflicts: ${record.path}`);
53
+ }
54
+ else
55
+ writeAtomic(path, record.content);
56
+ }
57
+ saveState(root, migration.state);
58
+ rebuildLessonIndex(root, migration.state);
59
+ return { migrated: true, archived: Object.keys(migration.state.archive ?? {}), stamped: migration.stamped };
60
+ };