@felixdotgo/aidlc-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPMENT.md +14 -0
- package/README.md +14 -6
- package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
- package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
- package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
- 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-authority.mjs +245 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
- package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
- package/dist/assets/.agents/aidlc/templates/design.md +9 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
- package/dist/assets/ui/bulma.min.css +3 -0
- package/dist/assets/ui/dashboard.css +414 -0
- package/dist/services/aidlc-coordination/Dockerfile +11 -3
- package/dist/services/aidlc-coordination/compose.yaml +12 -0
- package/dist/services/aidlc-coordination/package-lock.json +3 -1
- package/dist/services/aidlc-coordination/package.json +9 -4
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
- package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
- package/dist/services/aidlc-coordination/src/server.mjs +9 -3
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
- package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
- package/dist/src/adapters.d.ts +0 -3
- package/dist/src/adapters.js +11 -6
- package/dist/src/cli/argv.d.ts +12 -0
- package/dist/src/cli/argv.js +62 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.js +129 -0
- package/dist/src/cli/options.d.ts +25 -0
- package/dist/src/cli/options.js +32 -0
- package/dist/src/cli/prompt.d.ts +9 -0
- package/dist/src/cli/prompt.js +66 -0
- package/dist/src/cli.js +67 -217
- package/dist/src/context.js +2 -0
- package/dist/src/gate.d.ts +1 -0
- package/dist/src/gate.js +51 -37
- package/dist/src/installer.d.ts +19 -7
- package/dist/src/installer.js +111 -178
- package/dist/src/legacy.js +1 -0
- package/dist/src/managed-content.d.ts +7 -0
- package/dist/src/managed-content.js +15 -4
- package/dist/src/managed-planning.d.ts +65 -0
- package/dist/src/managed-planning.js +219 -0
- package/dist/src/mcp-setup.js +3 -1
- package/dist/src/model/config-contracts.d.ts +65 -0
- package/dist/src/model/config-contracts.js +1 -0
- package/dist/src/model/install-contracts.d.ts +29 -0
- package/dist/src/model/install-contracts.js +1 -0
- package/dist/src/model/lifecycle-enums.d.ts +13 -0
- package/dist/src/model/lifecycle-enums.js +1 -0
- package/dist/src/model/loop.d.ts +20 -0
- package/dist/src/model/loop.js +1 -0
- package/dist/src/model/manifest.d.ts +16 -0
- package/dist/src/model/manifest.js +1 -0
- package/dist/src/model/records.d.ts +138 -0
- package/dist/src/model/records.js +1 -0
- package/dist/src/model.d.ts +11 -264
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +8 -0
- package/dist/src/profiles.js +63 -8
- package/dist/src/project-migration.d.ts +1 -0
- package/dist/src/project-migration.js +7 -0
- package/dist/src/state/archive-record.d.ts +9 -0
- package/dist/src/state/archive-record.js +26 -0
- package/dist/src/state/commands.d.ts +18 -0
- package/dist/src/state/commands.js +27 -0
- package/dist/src/state/domain.d.ts +9 -0
- package/dist/src/state/domain.js +9 -0
- package/dist/src/state/guards.d.ts +7 -0
- package/dist/src/state/guards.js +17 -0
- package/dist/src/state/io.d.ts +10 -0
- package/dist/src/state/io.js +35 -0
- package/dist/src/state/items.d.ts +24 -0
- package/dist/src/state/items.js +147 -0
- package/dist/src/state/lessons.d.ts +7 -0
- package/dist/src/state/lessons.js +74 -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 +125 -0
- package/dist/src/state/memory.d.ts +8 -0
- package/dist/src/state/memory.js +100 -0
- package/dist/src/state/migrate.d.ts +17 -0
- package/dist/src/state/migrate.js +60 -0
- package/dist/src/state/partition.d.ts +15 -0
- package/dist/src/state/partition.js +90 -0
- package/dist/src/state/paths.d.ts +11 -0
- package/dist/src/state/paths.js +12 -0
- package/dist/src/state/render.d.ts +26 -0
- package/dist/src/state/render.js +32 -0
- package/dist/src/state/store.d.ts +4 -0
- package/dist/src/state/store.js +75 -0
- package/dist/src/state/transitions.d.ts +21 -0
- package/dist/src/state/transitions.js +95 -0
- package/dist/src/state/validate.d.ts +4 -0
- package/dist/src/state/validate.js +143 -0
- package/dist/src/state.d.ts +20 -117
- package/dist/src/state.js +20 -815
- package/dist/src/task-ui.d.ts +54 -0
- package/dist/src/task-ui.js +1570 -0
- package/dist/src/upgrade-board.d.ts +5 -0
- package/dist/src/upgrade-board.js +89 -0
- package/dist/src/upgrade.d.ts +3 -5
- package/dist/src/upgrade.js +174 -255
- package/dist/src/workflow.js +4 -4
- package/docs/README.md +6 -2
- package/docs/architecture.md +12 -8
- package/docs/command-reference.md +83 -19
- package/docs/configuration.md +30 -7
- package/docs/coordination-service.md +20 -5
- package/docs/design-decisions.md +83 -0
- package/docs/development.md +41 -2
- package/docs/loop-local-run.md +495 -0
- package/docs/loop-runbook.md +235 -0
- package/docs/operating-workflow.md +5 -2
- package/docs/testing-and-release.md +11 -7
- package/docs/troubleshooting.md +125 -0
- package/package.json +5 -2
- package/dist/src/ledger.d.ts +0 -22
- package/dist/src/ledger.js +0 -119
- package/dist/src/tracker.d.ts +0 -29
- package/dist/src/tracker.js +0 -41
package/dist/src/installer.js
CHANGED
|
@@ -1,93 +1,24 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { adapters, findAdapter,
|
|
4
|
-
import { loadProjectConfig
|
|
3
|
+
import { adapters, findAdapter, resolvedAdapters } from "./adapters.js";
|
|
4
|
+
import { loadProjectConfig } from "./profiles.js";
|
|
5
5
|
import { loadState } from "./state.js";
|
|
6
6
|
import { planProjectLayoutMigration } from "./project-migration.js";
|
|
7
|
-
import { managedBlock,
|
|
7
|
+
import { legacyManagedMarker, managedBlock, removeManagedBlock } from "./managed-content.js";
|
|
8
|
+
import { hasOwnedManifest, managedContentHash, planLoopSelection, planManagedWrites, readManifest, readManifestFile, splitInventoryKey } from "./managed-planning.js";
|
|
8
9
|
import { resolveProjectPathWithoutSymlinks } from "./project-path.js";
|
|
9
10
|
import { contentHash, coreWorkflowFiles, initialProjectFiles, manifestSpec, packageVersion } from "./workflow.js";
|
|
11
|
+
/** Manifest reading lives in the managed-planning core; this export path stays stable for existing importers. */
|
|
12
|
+
export { readManifest } from "./managed-planning.js";
|
|
10
13
|
const shortHash = (content) => contentHash(content).slice(0, 12);
|
|
11
14
|
const safeTarget = (root, path) => resolveProjectPathWithoutSymlinks(root, path, "Managed path escapes the project", "Managed path crosses a symlink");
|
|
12
|
-
|
|
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
|
-
};
|
|
22
|
-
export const managedContentHash = (spec, content) => {
|
|
23
|
-
const managed = managedBlock(spec, content);
|
|
24
|
-
return managed === undefined ? undefined : contentHash(managed);
|
|
25
|
-
};
|
|
26
|
-
export const readManifest = (root) => {
|
|
27
|
-
const path = join(resolve(root), ".agents/aidlc/manifest.json");
|
|
28
|
-
if (!existsSync(path))
|
|
29
|
-
return undefined;
|
|
30
|
-
try {
|
|
31
|
-
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
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;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
export const detectedAgents = (root) => installableAdapters().filter((adapter) => adapter.detect(root)).map((adapter) => adapter.id);
|
|
15
|
+
export const detectedAgents = (root) => resolvedAdapters().filter((adapter) => adapter.detect(root)).map((adapter) => adapter.id);
|
|
40
16
|
export const selectAgents = (options) => {
|
|
41
17
|
if (options.all)
|
|
42
18
|
return adapters.map((adapter) => adapter.id);
|
|
43
19
|
return [...new Set(options.agents)];
|
|
44
20
|
};
|
|
45
|
-
|
|
46
|
-
const target = safeTarget(root, spec.path);
|
|
47
|
-
if (spec.ownershipClass === "project" || spec.ownershipClass === "state") {
|
|
48
|
-
return existsSync(target)
|
|
49
|
-
? { ...spec, content: readFileSync(target, "utf8"), action: "preserve", reason: `${spec.ownershipClass}-owned file` }
|
|
50
|
-
: { ...spec, action: "create", reason: `new ${spec.ownershipClass}-owned file` };
|
|
51
|
-
}
|
|
52
|
-
if (spec.strategy === "managed-block") {
|
|
53
|
-
const current = existsSync(target) ? readFileSync(target, "utf8") : "";
|
|
54
|
-
const next = mergeManagedBlock(spec, current);
|
|
55
|
-
return { ...spec, content: next, action: existsSync(target) ? (next === current ? "skip" : "update") : "create", reason: existsSync(target) ? "managed prompt block" : "new managed prompt" };
|
|
56
|
-
}
|
|
57
|
-
if (!existsSync(target))
|
|
58
|
-
return { ...spec, action: "create", reason: "new managed file" };
|
|
59
|
-
const current = readFileSync(target, "utf8");
|
|
60
|
-
if (current === spec.content)
|
|
61
|
-
return { ...spec, action: "skip", reason: "already current" };
|
|
62
|
-
if (spec.path === ".agents/aidlc/manifest.json" && installed)
|
|
63
|
-
return { ...spec, action: "update", reason: "refresh manifest inventory" };
|
|
64
|
-
const installedHash = installedHashFor(installed?.files, spec);
|
|
65
|
-
if (installedHash && contentHash(current) === installedHash)
|
|
66
|
-
return { ...spec, action: "update", reason: "unchanged managed file" };
|
|
67
|
-
if (force)
|
|
68
|
-
return { ...spec, action: "update", reason: "--force permits replacement of an unmanaged file" };
|
|
69
|
-
return { ...spec, action: "conflict", reason: "managed ownership cannot be proven; use upgrade or review --force" };
|
|
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
|
-
};
|
|
21
|
+
/** Plans an init transaction: managed workflow files, manifest, and project-owned files. */
|
|
91
22
|
export const planInit = (options) => {
|
|
92
23
|
const root = resolve(options.root);
|
|
93
24
|
const installed = readManifest(root);
|
|
@@ -100,23 +31,12 @@ export const planInit = (options) => {
|
|
|
100
31
|
const agentFiles = selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
|
|
101
32
|
const managedFiles = [...coreWorkflowFiles(), ...agentFiles];
|
|
102
33
|
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
|
-
}
|
|
34
|
+
const plan = planManagedWrites(root, files, { flow: "init", force: options.force, installed, resolvePath: safeTarget });
|
|
116
35
|
planProjectLayoutMigration(root, plan);
|
|
117
36
|
planLoopSelection(plan, options.loop);
|
|
118
37
|
return plan;
|
|
119
38
|
};
|
|
39
|
+
/** Applies a planned init/uninstall transaction to disk. */
|
|
120
40
|
export const applyPlan = (root, plan) => {
|
|
121
41
|
for (const write of plan.filter((item) => item.action === "delete"))
|
|
122
42
|
rmSync(safeTarget(root, write.path), { force: true });
|
|
@@ -127,109 +47,124 @@ export const applyPlan = (root, plan) => {
|
|
|
127
47
|
}
|
|
128
48
|
};
|
|
129
49
|
export const formatPlan = (plan) => plan.map((item) => `${item.action.padEnd(8)} ${item.path} (${item.reason}; sha=${shortHash(item.contentEncoding === "base64" ? Buffer.from(item.content, "base64") : item.content)})`).join("\n");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
50
|
+
/** Maps one manifest inventory record onto its current spec and installed hash. */
|
|
51
|
+
const uninstallEntry = (key, item, currentSpecs) => {
|
|
52
|
+
const { path, owner: keyedOwner } = splitInventoryKey(key);
|
|
53
|
+
const owner = keyedOwner ?? item.owner;
|
|
54
|
+
const spec = currentSpecs.find((candidate) => candidate.path === path && (candidate.strategy !== "managed-block" || candidate.owner === owner)) ?? { path, owner, strategy: item.strategy, content: "", ownershipClass: "managed" };
|
|
55
|
+
return { spec, hash: item.hash };
|
|
56
|
+
};
|
|
57
|
+
/** Groups installed specs (or all current specs without a manifest) by path; shared blocks carry multiple owners. */
|
|
58
|
+
const uninstallGroups = (installed, currentSpecs) => {
|
|
59
|
+
const entries = installed ? Object.entries(installed.files).map(([key, item]) => uninstallEntry(key, item, currentSpecs)) : currentSpecs.map((spec) => ({ spec }));
|
|
60
|
+
const groups = new Map();
|
|
61
|
+
for (const entry of entries)
|
|
62
|
+
groups.set(entry.spec.path, [...(groups.get(entry.spec.path) ?? []), entry]);
|
|
63
|
+
return groups;
|
|
64
|
+
};
|
|
65
|
+
/** Removes each verified owner block from a shared file; stops at the first user-modified block. */
|
|
66
|
+
const stripManagedBlocks = (group, current) => {
|
|
67
|
+
let next = current;
|
|
68
|
+
let modified;
|
|
69
|
+
for (const { spec, hash } of group) {
|
|
70
|
+
const block = managedBlock(spec, next);
|
|
71
|
+
// A v0.2.1 single-marker block cannot be removed safely; retain ownership instead of orphaning it.
|
|
72
|
+
if (!block) {
|
|
73
|
+
if (next.includes(legacyManagedMarker(spec.owner)))
|
|
74
|
+
modified = spec;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (contentHash(block) !== (hash ?? contentHash(spec.content.trimEnd()))) {
|
|
78
|
+
modified = spec;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
next = removeManagedBlock(next, spec.owner);
|
|
136
82
|
}
|
|
137
|
-
|
|
138
|
-
|
|
83
|
+
return { next, modified };
|
|
84
|
+
};
|
|
85
|
+
/** Decides the plan entry for a whole-file (non shared-block) path. */
|
|
86
|
+
const uninstallWholeFile = (group, exists, current) => {
|
|
87
|
+
if (!exists)
|
|
88
|
+
return { ...group[0].spec, action: "skip", reason: "not installed" };
|
|
89
|
+
if (!group.some((entry) => entry.spec.strategy === "managed-block")) {
|
|
90
|
+
const { spec, hash } = group[0];
|
|
91
|
+
const installedHash = hash ?? contentHash(spec.content);
|
|
92
|
+
return contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" };
|
|
139
93
|
}
|
|
94
|
+
const { next, modified } = stripManagedBlocks(group, current);
|
|
95
|
+
const legacyResidue = group.find(({ spec }) => next.includes(legacyManagedMarker(spec.owner)));
|
|
96
|
+
const ownerSpec = group[0].spec;
|
|
97
|
+
if (modified)
|
|
98
|
+
return { ...modified, action: "preserve", reason: "managed block was modified" };
|
|
99
|
+
if (legacyResidue)
|
|
100
|
+
return { ...legacyResidue.spec, action: "preserve", reason: "legacy prompt marker requires manual removal" };
|
|
101
|
+
if (next === current)
|
|
102
|
+
return { ...ownerSpec, action: "skip", reason: "owned block not found" };
|
|
103
|
+
if (next)
|
|
104
|
+
return { ...ownerSpec, content: `${next}\n`, action: "update", reason: "remove owned prompt block" };
|
|
105
|
+
return { ...ownerSpec, content: "", action: "delete", reason: "remove owned prompt file" };
|
|
140
106
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
return
|
|
107
|
+
/** Keeps the ownership manifest only while modified managed files remain on disk. */
|
|
108
|
+
const manifestDisposition = (plan) => {
|
|
109
|
+
const preserved = plan.some((item) => item.action === "preserve");
|
|
110
|
+
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" };
|
|
145
111
|
};
|
|
112
|
+
/** Plans an uninstall transaction that removes only unchanged managed assets. */
|
|
146
113
|
export const planUninstall = (root) => {
|
|
147
114
|
const project = resolve(root);
|
|
148
115
|
if (!hasOwnedManifest(project))
|
|
149
|
-
throw new Error("No
|
|
116
|
+
throw new Error("No managed ownership manifest (.agents/aidlc/manifest.json) found; refusing to remove files.");
|
|
150
117
|
const installed = readManifest(project);
|
|
151
118
|
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
119
|
const plan = [];
|
|
165
|
-
for (const [path, group] of
|
|
120
|
+
for (const [path, group] of uninstallGroups(installed, currentSpecs)) {
|
|
166
121
|
const target = safeTarget(project, path);
|
|
167
122
|
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" });
|
|
123
|
+
plan.push(uninstallWholeFile(group, exists, exists ? readFileSync(target, "utf8") : ""));
|
|
207
124
|
}
|
|
208
|
-
plan.push(
|
|
125
|
+
plan.push(manifestDisposition(plan));
|
|
209
126
|
return plan;
|
|
210
127
|
};
|
|
211
|
-
export const
|
|
128
|
+
export const statusData = (root) => {
|
|
212
129
|
const manifest = readManifest(root);
|
|
213
130
|
const legacyPath = join(resolve(root), ".agents/aidlc/manifest.json");
|
|
131
|
+
return {
|
|
132
|
+
workflow: manifest || existsSync(legacyPath) ? "installed" : "not installed",
|
|
133
|
+
installedVersion: manifest?.packageVersion ?? (existsSync(legacyPath) ? "legacy/unknown" : "none"),
|
|
134
|
+
schema: `${manifest?.schemaVersion ?? (existsSync(legacyPath) ? "legacy" : "none")}`,
|
|
135
|
+
detectedAgents: detectedAgents(root)
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
/** Reports installed workflow version, schema, and detected agents. */
|
|
139
|
+
export const status = (root) => {
|
|
140
|
+
const data = statusData(root);
|
|
214
141
|
return [
|
|
215
|
-
`workflow: ${
|
|
216
|
-
`installed version: ${
|
|
217
|
-
`schema: ${
|
|
218
|
-
`detected agents: ${detectedAgents
|
|
142
|
+
`workflow: ${data.workflow}`,
|
|
143
|
+
`installed version: ${data.installedVersion}`,
|
|
144
|
+
`schema: ${data.schema}`,
|
|
145
|
+
`detected agents: ${data.detectedAgents.join(", ") || "none"}`
|
|
219
146
|
].join("\n");
|
|
220
147
|
};
|
|
148
|
+
/** Manifest inventory paths whose on-disk content no longer matches the recorded owner hash. */
|
|
149
|
+
const modifiedManagedPaths = (root, manifest) => {
|
|
150
|
+
const modified = Object.entries(manifest.files).filter(([key, item]) => {
|
|
151
|
+
const target = safeTarget(root, splitInventoryKey(key).path);
|
|
152
|
+
if (!existsSync(target))
|
|
153
|
+
return true;
|
|
154
|
+
const spec = { owner: item.owner, strategy: item.strategy };
|
|
155
|
+
return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
|
|
156
|
+
});
|
|
157
|
+
return [...new Set(modified.map(([key]) => splitInventoryKey(key).path))];
|
|
158
|
+
};
|
|
159
|
+
/** Validates the installed workflow manifest and local config/state. */
|
|
221
160
|
export const doctor = (root, strict = false) => {
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
161
|
+
const read = readManifestFile(root);
|
|
162
|
+
if (read.status === "missing")
|
|
224
163
|
return "ERROR: .agents/aidlc/manifest.json is missing. Run npx @felixdotgo/aidlc-kit init --yes.";
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
164
|
+
if (read.status === "invalid")
|
|
230
165
|
return "ERROR: workflow manifest is invalid JSON.";
|
|
231
|
-
|
|
232
|
-
if (parsed.remoteUpdates || parsed.source !== "local-package-assets" || parsed.managedBy !== "aidlc-workflow")
|
|
166
|
+
const parsed = read.value;
|
|
167
|
+
if (parsed.remoteUpdates || parsed.source !== "local-package-assets" || (parsed.managedBy !== "aidlc-kit" && parsed.managedBy !== "aidlc-workflow"))
|
|
233
168
|
return "ERROR: workflow manifest violates local-only asset policy.";
|
|
234
169
|
if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > 2)
|
|
235
170
|
return `ERROR: installed manifest schema ${parsed.schemaVersion} is newer than this CLI supports.`;
|
|
@@ -243,15 +178,13 @@ export const doctor = (root, strict = false) => {
|
|
|
243
178
|
catch (error) {
|
|
244
179
|
return `ERROR: local config/state is invalid: ${error instanceof Error ? error.message : String(error)}`;
|
|
245
180
|
}
|
|
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))];
|
|
181
|
+
const modified = modifiedManagedPaths(root, manifest);
|
|
254
182
|
if (modified.length)
|
|
255
183
|
return `${strict ? "ERROR" : "WARN"}: managed integrity differs: ${modified.join(", ")}`;
|
|
256
184
|
return `OK: local workflow ${manifest.packageVersion} is valid; remote updates are disabled.`;
|
|
257
185
|
};
|
|
186
|
+
export const doctorData = (root, strict = false) => {
|
|
187
|
+
const message = doctor(root, strict);
|
|
188
|
+
const level = message.startsWith("ERROR:") ? "error" : message.startsWith("WARN:") ? "warn" : "ok";
|
|
189
|
+
return { level, message, strict };
|
|
190
|
+
};
|
package/dist/src/legacy.js
CHANGED
|
@@ -25,6 +25,7 @@ const instruction = (agent) => [
|
|
|
25
25
|
"Read `.agents/aidlc/orchestrator.md` and `.agents/data/state/BOARD.md` before starting non-trivial work. Use phase skills under `.agents/skills/`; do not fetch remote workflow content. Respect more-specific instructions already present in this repository.",
|
|
26
26
|
""
|
|
27
27
|
].join("\n");
|
|
28
|
+
/** Frozen v0.2.1 baseline payload — intentionally does NOT reuse managed-content markers: this string is a historical fact that must not track future marker-format changes. */
|
|
28
29
|
const block = (owner, body) => `<!-- aidlc-installer:${owner} -->\n${body}`;
|
|
29
30
|
export const legacyV021AdapterContents = Object.freeze({
|
|
30
31
|
"CLAUDE.md": block("claude", instruction("Claude Code")),
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { FileSpec } from "./model.js";
|
|
2
|
+
/** Canonical managed-block markers; every emitter and detector must build them through these helpers. */
|
|
3
|
+
export declare const managedStartMarker: (owner: string) => string;
|
|
4
|
+
export declare const managedEndMarker: (owner: string) => string;
|
|
5
|
+
/** Single-marker block emitted by the frozen v0.2.1 baseline; upgrade and uninstall still detect it. */
|
|
6
|
+
export declare const legacyManagedMarker: (owner: string) => string;
|
|
2
7
|
export declare const managedBlock: (spec: Pick<FileSpec, "owner" | "strategy">, content: string) => string | undefined;
|
|
3
8
|
export declare const mergeManagedBlock: (spec: FileSpec, current: string) => string;
|
|
9
|
+
/** Removes every block owned by `owner` from `current` and normalizes the leftover blank-line runs. */
|
|
10
|
+
export declare const removeManagedBlock: (current: string, owner: string) => string;
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
/** Canonical managed-block markers; every emitter and detector must build them through these helpers. */
|
|
2
|
+
export const managedStartMarker = (owner) => `<!-- aidlc-installer:${owner}:start -->`;
|
|
3
|
+
export const managedEndMarker = (owner) => `<!-- aidlc-installer:${owner}:end -->`;
|
|
4
|
+
/** Single-marker block emitted by the frozen v0.2.1 baseline; upgrade and uninstall still detect it. */
|
|
5
|
+
export const legacyManagedMarker = (owner) => `<!-- aidlc-installer:${owner} -->`;
|
|
1
6
|
export const managedBlock = (spec, content) => {
|
|
2
7
|
if (spec.strategy !== "managed-block")
|
|
3
8
|
return content;
|
|
4
|
-
const start =
|
|
5
|
-
const end =
|
|
9
|
+
const start = managedStartMarker(spec.owner);
|
|
10
|
+
const end = managedEndMarker(spec.owner);
|
|
6
11
|
return content.match(new RegExp(`${start}[\\s\\S]*?${end}`))?.[0];
|
|
7
12
|
};
|
|
8
13
|
export const mergeManagedBlock = (spec, current) => {
|
|
9
|
-
const start =
|
|
10
|
-
const end =
|
|
14
|
+
const start = managedStartMarker(spec.owner);
|
|
15
|
+
const end = managedEndMarker(spec.owner);
|
|
11
16
|
const block = spec.content.trimEnd();
|
|
12
17
|
const matches = new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g");
|
|
13
18
|
const withoutOwnedBlocks = current.replace(matches, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
14
19
|
const next = `${withoutOwnedBlocks}${withoutOwnedBlocks ? "\n\n" : ""}${block}`;
|
|
15
20
|
return `${next.trimEnd()}\n`;
|
|
16
21
|
};
|
|
22
|
+
/** Removes every block owned by `owner` from `current` and normalizes the leftover blank-line runs. */
|
|
23
|
+
export const removeManagedBlock = (current, owner) => {
|
|
24
|
+
const start = managedStartMarker(owner);
|
|
25
|
+
const end = managedEndMarker(owner);
|
|
26
|
+
return current.replace(new RegExp(`${start}[\\s\\S]*?${end}\\n?`, "g"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
27
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { FileSpec, LoopConfig, ManifestInventoryItem, PlannedWrite, WorkflowManifest } from "./model.js";
|
|
2
|
+
/** Constructs manifest inventory keys (`path#owner` for shared managed blocks). The definition should live here; it stays in workflow.ts pending a cross-owner move. */
|
|
3
|
+
export { manifestKey } from "./workflow.js";
|
|
4
|
+
/** Raw outcome of reading the ownership manifest: missing on disk, unparseable, or parsed but unvalidated. */
|
|
5
|
+
export type ManifestFileRead = {
|
|
6
|
+
status: "missing";
|
|
7
|
+
} | {
|
|
8
|
+
status: "invalid";
|
|
9
|
+
} | {
|
|
10
|
+
status: "parsed";
|
|
11
|
+
value: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
/** The single read+parse of .agents/aidlc/manifest.json that every manifest question goes through. */
|
|
14
|
+
export declare const readManifestFile: (root: string) => ManifestFileRead;
|
|
15
|
+
/** Strict schema-v2 ownership manifest; malformed or non-conforming content reads as "not installed". */
|
|
16
|
+
export declare const readManifest: (root: string) => WorkflowManifest | undefined;
|
|
17
|
+
/** Whether the on-disk manifest is owned by this package (any schema version). */
|
|
18
|
+
export declare const hasOwnedManifest: (root: string) => boolean;
|
|
19
|
+
/** Whether the on-disk manifest is a legacy aidlc-workflow v0.x install (schema 0 or 1). */
|
|
20
|
+
export declare const legacyManifest: (root: string) => boolean;
|
|
21
|
+
/** Splits a manifest inventory key into its path and, when present, its `#owner` suffix. */
|
|
22
|
+
export declare const splitInventoryKey: (key: string) => {
|
|
23
|
+
path: string;
|
|
24
|
+
owner?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Managed prompt blocks share a path across owners, so inventory keys carry the owner. */
|
|
27
|
+
export declare const installedHashFor: (files: Record<string, ManifestInventoryItem> | undefined, spec: Pick<FileSpec, "path" | "owner" | "strategy">) => string | undefined;
|
|
28
|
+
/** Hash of the managed slice of a file: the owned block for managed-block strategies, the whole file otherwise. */
|
|
29
|
+
export declare const managedContentHash: (spec: Pick<FileSpec, "owner" | "strategy">, content: string) => string | undefined;
|
|
30
|
+
/** Plans one project/state-owned file: preserved verbatim when present, seeded from the package when absent. */
|
|
31
|
+
export declare const preserveOwnedFile: (target: string, spec: FileSpec) => PlannedWrite;
|
|
32
|
+
/** Caller-supplied project-path resolver so each flow keeps its own error persona (init refuses escapes and symlinks; upgrade refuses only escapes). */
|
|
33
|
+
export type ProjectPathResolver = (root: string, relative: string) => string;
|
|
34
|
+
/** Policy fields shared by both flows. */
|
|
35
|
+
export interface PlanPolicyBase {
|
|
36
|
+
/** Installed manifest to prove ownership against, when one exists. */
|
|
37
|
+
installed?: WorkflowManifest;
|
|
38
|
+
/** Project-path resolver carrying the caller's error persona. */
|
|
39
|
+
resolvePath: ProjectPathResolver;
|
|
40
|
+
}
|
|
41
|
+
/** Init: managed blocks merge unconditionally and --force may replace unmanaged files. */
|
|
42
|
+
export interface InitPlanPolicy extends PlanPolicyBase {
|
|
43
|
+
flow: "init";
|
|
44
|
+
force: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Upgrade: ownership is proven against installed hashes and the frozen v0.2.1 baselines. */
|
|
47
|
+
export interface UpgradePlanPolicy extends PlanPolicyBase {
|
|
48
|
+
flow: "upgrade";
|
|
49
|
+
/** True when no schema-2 manifest exists and a legacy v0.x install is being migrated. */
|
|
50
|
+
legacy: boolean;
|
|
51
|
+
/** Recognizes a v0.2.1 baseline for a spec's current managed-content hash; consulted only while `legacy` is set. */
|
|
52
|
+
legacyBaseline?: (spec: FileSpec, currentHash: string | undefined) => boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Per-flow policy for the shared managed-file ladder; the union makes each flow's knobs explicit. */
|
|
55
|
+
export type PlanPolicy = InitPlanPolicy | UpgradePlanPolicy;
|
|
56
|
+
/**
|
|
57
|
+
* Plans managed files (and, for init, the manifest and project/state files) in
|
|
58
|
+
* order; later owners of a shared managed-block path merge into the earlier
|
|
59
|
+
* entry. Init finds the earlier entry by scanning the plan; upgrade tracks the
|
|
60
|
+
* first block entry per path so a conflicted first owner stays the single
|
|
61
|
+
* reportable entry for that path.
|
|
62
|
+
*/
|
|
63
|
+
export declare const planManagedWrites: (root: string, files: FileSpec[], policy: PlanPolicy) => PlannedWrite[];
|
|
64
|
+
/** Include an explicitly selected loop config in the same preview/transaction as setup. */
|
|
65
|
+
export declare const planLoopSelection: (plan: PlannedWrite[], selection?: LoopConfig) => void;
|