@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.
- package/DEVELOPMENT.md +1 -1
- package/README.md +20 -9
- package/dist/assets/.agents/aidlc/conventions.md +8 -8
- package/dist/assets/.agents/aidlc/orchestrator.md +15 -10
- package/dist/assets/.agents/aidlc/phase-build.md +7 -7
- package/dist/assets/.agents/aidlc/phase-clarify.md +3 -3
- package/dist/assets/.agents/aidlc/phase-plan.md +6 -6
- package/dist/assets/.agents/aidlc/phase-wrap.md +1 -1
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
- package/dist/assets/.agents/aidlc/scripts/assignment.mjs +36 -0
- package/dist/assets/.agents/aidlc/scripts/context.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/gate-check.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/gate-view.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +181 -0
- package/dist/assets/.agents/aidlc/scripts/lib/authority.mjs +3 -0
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +13 -1
- package/dist/assets/.agents/aidlc/scripts/lib/feature-store.mjs +70 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +220 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +183 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +100 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +58 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +320 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +284 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +51 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-artifacts.mjs +63 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-broker.mjs +53 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-local-broker.mjs +44 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lead-sandbox.mjs +88 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +89 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-gate-receipt.mjs +32 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +162 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-planner.mjs +13 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +260 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +131 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +55 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +101 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +32 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +66 -0
- package/dist/assets/.agents/aidlc/scripts/lib/remote-state-client.mjs +112 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +41 -6
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +90 -0
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +58 -0
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +36 -0
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +311 -0
- package/dist/assets/.agents/aidlc/scripts/render.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +71 -11
- package/dist/assets/.agents/aidlc/scripts/task-next.mjs +2 -0
- package/dist/assets/.agents/aidlc/templates/model-contract.md +3 -3
- package/dist/assets/.agents/aidlc/templates/workplan.md +2 -2
- package/dist/assets/.agents/skills/aidlc-build/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-clarify/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +2 -2
- package/dist/assets/.agents/skills/aidlc-wrap/SKILL.md +1 -1
- package/dist/services/aidlc-coordination/.env.fleet.example +11 -0
- package/dist/services/aidlc-coordination/compose.yaml +23 -2
- package/dist/services/aidlc-coordination/package-lock.json +1 -0
- package/dist/services/aidlc-coordination/package.json +2 -1
- package/dist/services/aidlc-coordination/src/coordination.mjs +1 -1
- package/dist/services/aidlc-coordination/src/fleet-client.mjs +36 -0
- package/dist/services/aidlc-coordination/src/fleet-run.mjs +18 -0
- package/dist/services/aidlc-coordination/src/fleet.mjs +233 -0
- package/dist/services/aidlc-coordination/src/git-gateway.mjs +50 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +41 -6
- package/dist/services/aidlc-coordination/src/repository.mjs +167 -4
- package/dist/services/aidlc-coordination/src/server.mjs +53 -9
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +351 -0
- package/dist/services/aidlc-coordination/test/git-gateway.test.mjs +75 -0
- package/dist/services/aidlc-coordination/test/supervisor-fencing.test.mjs +59 -0
- package/dist/src/adapters.js +32 -5
- package/dist/src/cli.js +146 -70
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +51 -38
- package/dist/src/installer.d.ts +10 -1
- package/dist/src/installer.js +157 -37
- package/dist/src/managed-content.js +1 -1
- package/dist/src/model.d.ts +16 -1
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +9 -1
- package/dist/src/profiles.js +37 -3
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive.d.ts +17 -0
- package/dist/src/state/archive.js +60 -0
- package/dist/src/state/items.d.ts +25 -0
- package/dist/src/state/items.js +146 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +67 -0
- package/dist/src/state/lifecycle.d.ts +5 -0
- package/dist/src/state/lifecycle.js +78 -0
- package/dist/src/state/lock.d.ts +7 -0
- package/dist/src/state/lock.js +124 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +88 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +29 -0
- package/dist/src/state/shared.d.ts +29 -0
- package/dist/src/state/shared.js +71 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +81 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +93 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +100 -0
- package/dist/src/state.d.ts +15 -110
- package/dist/src/state.js +15 -764
- package/dist/src/tracker.d.ts +7 -0
- package/dist/src/tracker.js +3 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +5 -6
- package/dist/src/upgrade.js +211 -193
- package/dist/src/workflow.d.ts +1 -0
- package/dist/src/workflow.js +3 -2
- package/docs/README.md +4 -4
- package/docs/architecture.md +10 -2
- package/docs/command-reference.md +72 -11
- package/docs/configuration.md +26 -4
- package/docs/coordination-service.md +59 -3
- package/docs/development.md +16 -2
- package/docs/operating-workflow.md +53 -14
- package/docs/testing-and-release.md +53 -10
- package/package.json +3 -3
package/dist/src/installer.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
105
|
-
const
|
|
106
|
-
const target = safeTarget(project,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 =
|
|
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").
|
|
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
|
};
|
package/dist/src/model.d.ts
CHANGED
|
@@ -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"]);
|
package/dist/src/profiles.d.ts
CHANGED
|
@@ -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[];
|
package/dist/src/profiles.js
CHANGED
|
@@ -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,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;
|