@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
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { adapters, findAdapter, installableAdapters, resolvedAdapters } from "./adapters.js";
4
- import { loadProjectConfig } from "./profiles.js";
4
+ import { loadProjectConfig, validateLoopConfig } from "./profiles.js";
5
5
  import { loadState } from "./state.js";
6
6
  import { planProjectLayoutMigration } from "./project-migration.js";
7
7
  import { managedBlock, mergeManagedBlock } from "./managed-content.js";
@@ -9,6 +9,16 @@ import { resolveProjectPathWithoutSymlinks } from "./project-path.js";
9
9
  import { contentHash, coreWorkflowFiles, initialProjectFiles, manifestSpec, packageVersion } from "./workflow.js";
10
10
  const shortHash = (content) => contentHash(content).slice(0, 12);
11
11
  const safeTarget = (root, path) => resolveProjectPathWithoutSymlinks(root, path, "Managed path escapes the project", "Managed path crosses a symlink");
12
+ /** Managed prompt blocks share a path across owners, so inventory keys carry the owner. */
13
+ export const installedHashFor = (files, spec) => {
14
+ if (!files)
15
+ return undefined;
16
+ const keyed = files[`${spec.path}#${spec.owner}`];
17
+ if (keyed)
18
+ return keyed.hash;
19
+ const legacy = files[spec.path];
20
+ return legacy && (spec.strategy !== "managed-block" || legacy.owner === spec.owner) ? legacy.hash : undefined;
21
+ };
12
22
  export const managedContentHash = (spec, content) => {
13
23
  const managed = managedBlock(spec, content);
14
24
  return managed === undefined ? undefined : contentHash(managed);
@@ -20,11 +30,11 @@ export const readManifest = (root) => {
20
30
  try {
21
31
  const value = JSON.parse(readFileSync(path, "utf8"));
22
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));
23
- 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;
24
34
  }
25
35
  catch {
26
36
  return undefined;
27
- }
37
+ } /* intentional: a malformed manifest is treated as "not installed" */
28
38
  };
29
39
  export const detectedAgents = (root) => installableAdapters().filter((adapter) => adapter.detect(root)).map((adapter) => adapter.id);
30
40
  export const selectAgents = (options) => {
@@ -51,13 +61,54 @@ const planFile = (root, spec, force, installed) => {
51
61
  return { ...spec, action: "skip", reason: "already current" };
52
62
  if (spec.path === ".agents/aidlc/manifest.json" && installed)
53
63
  return { ...spec, action: "update", reason: "refresh manifest inventory" };
54
- const installedHash = installed?.files[spec.path]?.hash;
64
+ const installedHash = installedHashFor(installed?.files, spec);
55
65
  if (installedHash && contentHash(current) === installedHash)
56
66
  return { ...spec, action: "update", reason: "unchanged managed file" };
57
67
  if (force)
58
68
  return { ...spec, action: "update", reason: "--force permits replacement of an unmanaged file" };
59
69
  return { ...spec, action: "conflict", reason: "managed ownership cannot be proven; use upgrade or review --force" };
60
70
  };
71
+ /** Include an explicitly selected loop config in the same preview/transaction as setup. */
72
+ export const planLoopSelection = (plan, selection) => {
73
+ if (selection === undefined)
74
+ return;
75
+ const loop = validateLoopConfig(selection);
76
+ const config = plan.find((item) => item.path === ".agents/config.json");
77
+ if (!config || !["create", "preserve", "skip", "migrate"].includes(config.action))
78
+ throw new Error("Cannot change loop configuration while the project config has another planned change");
79
+ const parsed = JSON.parse(config.content);
80
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
81
+ throw new Error("Invalid project config");
82
+ const current = parsed;
83
+ if (JSON.stringify(current.loop) === JSON.stringify(loop))
84
+ return;
85
+ const previousAction = config.action;
86
+ current.loop = loop;
87
+ config.content = `${JSON.stringify(current, null, 2)}\n`;
88
+ config.action = previousAction === "create" || previousAction === "migrate" ? previousAction : "update";
89
+ config.reason = "explicit loop mode selection";
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. */
61
112
  export const planInit = (options) => {
62
113
  const root = resolve(options.root);
63
114
  const installed = readManifest(root);
@@ -70,10 +121,12 @@ export const planInit = (options) => {
70
121
  const agentFiles = selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
71
122
  const managedFiles = [...coreWorkflowFiles(), ...agentFiles];
72
123
  const files = [...managedFiles, manifestSpec(managedFiles, selected), ...initialProjectFiles()];
73
- const plan = files.map((spec) => planFile(root, spec, options.force, installed));
124
+ const plan = planManagedWrites(root, files, options.force, installed);
74
125
  planProjectLayoutMigration(root, plan);
126
+ planLoopSelection(plan, options.loop);
75
127
  return plan;
76
128
  };
129
+ /** Applies a planned init/uninstall transaction to disk. */
77
130
  export const applyPlan = (root, plan) => {
78
131
  for (const write of plan.filter((item) => item.action === "delete"))
79
132
  rmSync(safeTarget(root, write.path), { force: true });
@@ -89,42 +142,98 @@ const hasOwnedManifest = (root) => {
89
142
  if (!existsSync(path))
90
143
  return false;
91
144
  try {
92
- 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";
93
147
  }
94
148
  catch {
95
149
  return false;
150
+ } /* intentional: a malformed/missing manifest is not owned */
151
+ };
152
+ const removeManagedBlock = (current, owner) => {
153
+ const start = `<!-- aidlc-installer:${owner}:start -->`;
154
+ const end = `<!-- aidlc-installer:${owner}:end -->`;
155
+ return current.replace(new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
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" };
96
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" };
97
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. */
98
221
  export const planUninstall = (root) => {
99
222
  const project = resolve(root);
100
223
  if (!hasOwnedManifest(project))
101
- 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.");
102
225
  const installed = readManifest(project);
103
226
  const currentSpecs = [...coreWorkflowFiles(), ...resolvedAdapters().flatMap((adapter) => adapter.files()).map((file) => ({ ...file, ownershipClass: "managed" }))];
104
- const specs = installed ? Object.entries(installed.files).map(([path, item]) => currentSpecs.find((spec) => spec.path === path) ?? ({ path, owner: item.owner, strategy: item.strategy, content: "", ownershipClass: "managed" })) : currentSpecs;
105
- const plan = specs.map((spec) => {
106
- const target = safeTarget(project, spec.path);
107
- if (!existsSync(target))
108
- return { ...spec, action: "skip", reason: "not installed" };
109
- const current = readFileSync(target, "utf8");
110
- if (spec.strategy === "managed-block") {
111
- const block = managedBlock(spec, current);
112
- if (!block)
113
- return { ...spec, action: "skip", reason: "owned block not found" };
114
- const installedHash = installed?.files[spec.path]?.hash ?? contentHash(spec.content.trimEnd());
115
- if (contentHash(block) !== installedHash)
116
- return { ...spec, action: "preserve", reason: "managed block was modified" };
117
- const start = `<!-- aidlc-installer:${spec.owner}:start -->`;
118
- const end = `<!-- aidlc-installer:${spec.owner}:end -->`;
119
- const next = current.replace(new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
120
- return next ? { ...spec, content: `${next}\n`, action: "update", reason: "remove owned prompt block" } : { ...spec, content: "", action: "delete", reason: "remove owned prompt file" };
121
- }
122
- const installedHash = installed?.files[spec.path]?.hash ?? contentHash(spec.content);
123
- return contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" };
124
- });
125
- 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" });
227
+ const plan = [];
228
+ for (const [path, group] of uninstallGroups(installed, currentSpecs)) {
229
+ const target = safeTarget(project, path);
230
+ const exists = existsSync(target);
231
+ plan.push(uninstallWholeFile(group, exists, exists ? readFileSync(target, "utf8") : ""));
232
+ }
233
+ plan.push(manifestDisposition(plan));
126
234
  return plan;
127
235
  };
236
+ /** Reports installed workflow version, schema, and detected agents. */
128
237
  export const status = (root) => {
129
238
  const manifest = readManifest(root);
130
239
  const legacyPath = join(resolve(root), ".agents/aidlc/manifest.json");
@@ -135,6 +244,23 @@ export const status = (root) => {
135
244
  `detected agents: ${detectedAgents(root).join(", ") || "none"}`
136
245
  ].join("\n");
137
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. */
138
264
  export const doctor = (root, strict = false) => {
139
265
  const manifestPath = join(resolve(root), ".agents/aidlc/manifest.json");
140
266
  if (!existsSync(manifestPath))
@@ -146,7 +272,7 @@ export const doctor = (root, strict = false) => {
146
272
  catch {
147
273
  return "ERROR: workflow manifest is invalid JSON.";
148
274
  }
149
- 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"))
150
276
  return "ERROR: workflow manifest violates local-only asset policy.";
151
277
  if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > 2)
152
278
  return `ERROR: installed manifest schema ${parsed.schemaVersion} is newer than this CLI supports.`;
@@ -160,13 +286,7 @@ export const doctor = (root, strict = false) => {
160
286
  catch (error) {
161
287
  return `ERROR: local config/state is invalid: ${error instanceof Error ? error.message : String(error)}`;
162
288
  }
163
- const modified = Object.entries(manifest.files).filter(([path, item]) => {
164
- const target = safeTarget(root, path);
165
- if (!existsSync(target))
166
- return true;
167
- const spec = { owner: item.owner, strategy: item.strategy };
168
- return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
169
- }).map(([path]) => path);
289
+ const modified = modifiedManagedPaths(root, manifest);
170
290
  if (modified.length)
171
291
  return `${strict ? "ERROR" : "WARN"}: managed integrity differs: ${modified.join(", ")}`;
172
292
  return `OK: local workflow ${manifest.packageVersion} is valid; remote updates are disabled.`;
@@ -10,7 +10,7 @@ export const mergeManagedBlock = (spec, current) => {
10
10
  const end = `<!-- aidlc-installer:${spec.owner}:end -->`;
11
11
  const block = spec.content.trimEnd();
12
12
  const matches = new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g");
13
- const withoutOwnedBlocks = current.replace(matches, "").replace(/\n{3,}/g, "\n\n").trimEnd();
13
+ const withoutOwnedBlocks = current.replace(matches, "").replace(/\n{3,}/g, "\n\n").trim();
14
14
  const next = `${withoutOwnedBlocks}${withoutOwnedBlocks ? "\n\n" : ""}${block}`;
15
15
  return `${next.trimEnd()}\n`;
16
16
  };
@@ -31,6 +31,17 @@ export interface InitOptions {
31
31
  dryRun: boolean;
32
32
  yes: boolean;
33
33
  force: boolean;
34
+ loop?: LoopConfig;
35
+ }
36
+ export type LoopMode = "off" | "local" | "fleet";
37
+ export declare const LOOP_PROVIDERS: readonly ["codex", "claude", "opencode"];
38
+ export type LoopAgent = (typeof LOOP_PROVIDERS)[number];
39
+ export interface LoopConfig {
40
+ mode: LoopMode;
41
+ agents?: {
42
+ lead: LoopAgent;
43
+ workers: Record<string, LoopAgent>;
44
+ };
34
45
  }
35
46
  export interface CommandSpec {
36
47
  command: string;
@@ -72,6 +83,7 @@ export interface ProjectConfig {
72
83
  agentState: Partial<Record<AgentId, StateMutationMode>>;
73
84
  tracker?: TrackerConfig;
74
85
  mcp: McpConfig;
86
+ loop?: LoopConfig;
75
87
  }
76
88
  export interface McpConfig {
77
89
  enabled: boolean;
@@ -127,6 +139,7 @@ export interface LessonDisposition {
127
139
  }
128
140
  export interface TaskState {
129
141
  id: string;
142
+ featureId?: string;
130
143
  title: string;
131
144
  type: "feature" | "bug" | "refactor" | "infra";
132
145
  phase: Phase;
@@ -146,6 +159,8 @@ export interface TaskState {
146
159
  id: string;
147
160
  label: string;
148
161
  status: "todo" | "in_progress" | "done" | "deferred";
162
+ dependsOn?: string[];
163
+ ownedPaths?: string[];
149
164
  }>;
150
165
  evidence: Evidence[];
151
166
  lessons?: LessonRecord[];
@@ -238,7 +253,7 @@ export interface WorkflowManifest {
238
253
  workflow: "AI-DLC";
239
254
  source: "local-package-assets";
240
255
  remoteUpdates: false;
241
- managedBy: "aidlc-workflow";
256
+ managedBy: "aidlc-kit" | "aidlc-workflow";
242
257
  adapters: AgentId[];
243
258
  files: Record<string, ManifestInventoryItem>;
244
259
  }
package/dist/src/model.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export const LOOP_PROVIDERS = Object.freeze(["codex", "claude", "opencode"]);
@@ -1,9 +1,15 @@
1
- import type { McpConfig, Profile, ProjectConfig, RiskLevel } from "./model.js";
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. */
4
+ export declare const validateLoopConfig: (value: unknown) => LoopConfig;
5
+ /** Validates a single profile file (built-in or local). */
3
6
  export declare const validateProfile: (value: unknown, label?: string) => Profile;
4
7
  export declare const defaultConfig: () => ProjectConfig;
8
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
5
9
  export declare const loadProjectConfig: (root: string) => ProjectConfig;
10
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
6
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. */
7
13
  export declare const resolveEffectiveConfig: (root: string, config: ProjectConfig) => {
8
14
  profiles: Profile[];
9
15
  discovery: {
@@ -24,5 +30,7 @@ export declare const resolveEffectiveConfig: (root: string, config: ProjectConfi
24
30
  maxChars: number;
25
31
  };
26
32
  mcp: McpConfig;
33
+ loop: LoopConfig;
27
34
  };
35
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
28
36
  export declare const includedRuleFiles: (root: string, patterns: string[]) => string[];
@@ -1,5 +1,13 @@
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";
8
+ import { LOOP_PROVIDERS } from "./model.js";
9
+ const loopProviderNames = `${LOOP_PROVIDERS.slice(0, -1).join(", ")}, or ${LOOP_PROVIDERS[LOOP_PROVIDERS.length - 1]}`;
10
+ const isLoopProvider = (value) => typeof value === "string" && LOOP_PROVIDERS.includes(value);
3
11
  const profile = (id, markers = []) => ({
4
12
  schemaVersion: 1,
5
13
  id: `topology/${id}`,
@@ -20,6 +28,24 @@ const object = (value, label) => {
20
28
  throw new Error(`${label} must be an object`);
21
29
  return value;
22
30
  };
31
+ /** Validates and normalizes a loop-execution configuration block. */
32
+ export const validateLoopConfig = (value) => {
33
+ if (value === undefined)
34
+ return { mode: "off" };
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))
37
+ throw new Error("config.loop requires a valid mode and only supported fields");
38
+ if (item.agents === undefined)
39
+ return { mode: item.mode };
40
+ const agents = object(item.agents, "config.loop.agents");
41
+ if (Object.keys(agents).some((key) => !["lead", "workers"].includes(key)) || !isLoopProvider(agents.lead))
42
+ throw new Error(`config.loop.agents.lead must be ${loopProviderNames}`);
43
+ const workers = object(agents.workers, "config.loop.agents.workers");
44
+ for (const [id, provider] of Object.entries(workers))
45
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id) || !isLoopProvider(provider))
46
+ throw new Error(`config.loop.agents.workers must map worker IDs to ${loopProviderNames}`);
47
+ return { mode: item.mode, agents: { lead: agents.lead, workers: workers } };
48
+ };
23
49
  const strings = (value, label) => {
24
50
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
25
51
  throw new Error(`${label} must be a string array`);
@@ -78,6 +104,7 @@ const mcp = (value) => {
78
104
  throw new Error("config.mcp.providers is invalid");
79
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 }) };
80
106
  };
107
+ /** Validates a single profile file (built-in or local). */
81
108
  export const validateProfile = (value, label = "profile") => {
82
109
  const item = object(value, label);
83
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))
@@ -108,8 +135,10 @@ export const defaultConfig = () => ({
108
135
  risk: { default: "normal" },
109
136
  context: { maxChars: 16_000 },
110
137
  agentState: {},
111
- mcp: { enabled: false }
138
+ mcp: { enabled: false },
139
+ loop: { mode: "off" }
112
140
  });
141
+ /** Loads and validates `.agents/config.json`, falling back to the default config when absent. */
113
142
  export const loadProjectConfig = (root) => {
114
143
  const path = join(resolve(root), ".agents/config.json");
115
144
  if (!existsSync(path))
@@ -140,7 +169,8 @@ export const loadProjectConfig = (root) => {
140
169
  context: { maxChars },
141
170
  agentState,
142
171
  tracker: tracker(item.tracker),
143
- mcp: mcp(item.mcp)
172
+ mcp: mcp(item.mcp),
173
+ loop: validateLoopConfig(item.loop)
144
174
  };
145
175
  };
146
176
  const readLocalProfile = (root, id) => {
@@ -151,6 +181,7 @@ const readLocalProfile = (root, id) => {
151
181
  throw new Error(`Profile escapes the project: ${id}`);
152
182
  return validateProfile(JSON.parse(readFileSync(path, "utf8")), id);
153
183
  };
184
+ /** Resolves a list of profile ids into an ordered, cycle-free list of profiles. */
154
185
  export const resolveProfiles = (root, ids) => {
155
186
  const resolved = [];
156
187
  const visiting = new Set();
@@ -173,6 +204,7 @@ export const resolveProfiles = (root, ids) => {
173
204
  return resolved;
174
205
  };
175
206
  const stable = (values) => [...new Set(values)];
207
+ /** Merges built-in and local profiles with the project config into the effective configuration. */
176
208
  export const resolveEffectiveConfig = (root, config) => {
177
209
  const profiles = resolveProfiles(root, config.extends);
178
210
  return {
@@ -183,9 +215,11 @@ export const resolveEffectiveConfig = (root, config) => {
183
215
  rules: { include: stable([...profiles.flatMap((item) => item.rules?.include ?? []), ...config.rules.include]) },
184
216
  risk: config.risk,
185
217
  context: config.context,
186
- mcp: config.mcp
218
+ mcp: config.mcp,
219
+ loop: config.loop ?? { mode: "off" }
187
220
  };
188
221
  };
222
+ /** Expands rule include patterns into concrete markdown file paths inside the project. */
189
223
  export const includedRuleFiles = (root, patterns) => {
190
224
  const project = resolve(root);
191
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
+ };
@@ -0,0 +1,25 @@
1
+ import type { Gate, Phase, TaskState } from "../model.js";
2
+ export interface NextAction {
3
+ classification: "await_user" | "run_phase" | "blocked" | "terminal" | "complete";
4
+ phase: Phase;
5
+ gate: Gate;
6
+ command?: string;
7
+ outcome?: "closed" | "superseded";
8
+ successorTaskId?: string;
9
+ itemId?: string;
10
+ remainingItems?: number;
11
+ actions?: Array<{
12
+ id: "reopen_g1" | "create_successor" | "supersede" | "close" | "record_handoff" | "resume";
13
+ command: string;
14
+ }>;
15
+ reason: string;
16
+ }
17
+ /** Validates execution dependencies and reports which pending items are ready to dispatch. */
18
+ export declare const readyTaskItems: (task: TaskState) => {
19
+ ready: string[];
20
+ blocked: Array<{
21
+ id: string;
22
+ waitingOn: string[];
23
+ }>;
24
+ };
25
+ export declare const nextAction: (task: TaskState, root?: string) => NextAction;