@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.
- package/README.md +16 -6
- package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +6 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +6 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +8 -3
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +1 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +7 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +2 -2
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +2 -2
- package/dist/src/adapters.js +8 -1
- package/dist/src/cli.js +111 -95
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +48 -35
- package/dist/src/installer.d.ts +5 -0
- package/dist/src/installer.js +116 -80
- package/dist/src/model.d.ts +1 -1
- package/dist/src/profiles.d.ts +6 -0
- package/dist/src/profiles.js +11 -0
- 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 -117
- package/dist/src/state.js +15 -815
- 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 +4 -5
- package/dist/src/upgrade.js +197 -214
- package/dist/src/workflow.js +1 -1
- package/docs/README.md +2 -2
- package/docs/architecture.md +4 -1
- package/docs/command-reference.md +15 -12
- package/docs/configuration.md +7 -4
- package/docs/coordination-service.md +5 -2
- package/docs/development.md +15 -1
- package/docs/operating-workflow.md +4 -1
- package/docs/testing-and-release.md +7 -7
- 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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (gate === "
|
|
51
|
-
diagnostics.push(...
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
package/dist/src/installer.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/installer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
228
|
+
for (const [path, group] of uninstallGroups(installed, currentSpecs)) {
|
|
166
229
|
const target = safeTarget(project, path);
|
|
167
230
|
const exists = existsSync(target);
|
|
168
|
-
|
|
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(
|
|
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 =
|
|
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.`;
|
package/dist/src/model.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/src/profiles.d.ts
CHANGED
|
@@ -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[];
|
package/dist/src/profiles.js
CHANGED
|
@@ -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,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
|
+
};
|