@felixdotgo/aidlc-kit 0.1.1 → 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.
Files changed (134) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +11 -13
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +245 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +15 -3
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +40 -15
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +14 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +42 -15
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +75 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +57 -2
  28. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  29. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  30. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  31. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  32. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  33. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  34. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  35. package/dist/assets/ui/bulma.min.css +3 -0
  36. package/dist/assets/ui/dashboard.css +414 -0
  37. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  38. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  39. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  40. package/dist/services/aidlc-coordination/package.json +9 -4
  41. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +57 -2
  42. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  43. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  44. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  45. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  46. package/dist/src/adapters.d.ts +0 -3
  47. package/dist/src/adapters.js +3 -5
  48. package/dist/src/cli/argv.d.ts +12 -0
  49. package/dist/src/cli/argv.js +62 -0
  50. package/dist/src/cli/commands.d.ts +8 -0
  51. package/dist/src/cli/commands.js +129 -0
  52. package/dist/src/cli/options.d.ts +25 -0
  53. package/dist/src/cli/options.js +32 -0
  54. package/dist/src/cli/prompt.d.ts +9 -0
  55. package/dist/src/cli/prompt.js +66 -0
  56. package/dist/src/cli.js +25 -191
  57. package/dist/src/gate.js +3 -2
  58. package/dist/src/installer.d.ts +14 -7
  59. package/dist/src/installer.js +38 -141
  60. package/dist/src/legacy.js +1 -0
  61. package/dist/src/managed-content.d.ts +7 -0
  62. package/dist/src/managed-content.js +15 -4
  63. package/dist/src/managed-planning.d.ts +65 -0
  64. package/dist/src/managed-planning.js +219 -0
  65. package/dist/src/mcp-setup.js +3 -1
  66. package/dist/src/model/config-contracts.d.ts +65 -0
  67. package/dist/src/model/config-contracts.js +1 -0
  68. package/dist/src/model/install-contracts.d.ts +29 -0
  69. package/dist/src/model/install-contracts.js +1 -0
  70. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  71. package/dist/src/model/lifecycle-enums.js +1 -0
  72. package/dist/src/model/loop.d.ts +20 -0
  73. package/dist/src/model/loop.js +1 -0
  74. package/dist/src/model/manifest.d.ts +16 -0
  75. package/dist/src/model/manifest.js +1 -0
  76. package/dist/src/model/records.d.ts +138 -0
  77. package/dist/src/model/records.js +1 -0
  78. package/dist/src/model.d.ts +11 -264
  79. package/dist/src/model.js +1 -1
  80. package/dist/src/profiles.d.ts +2 -0
  81. package/dist/src/profiles.js +53 -9
  82. package/dist/src/state/archive-record.d.ts +9 -0
  83. package/dist/src/state/archive-record.js +26 -0
  84. package/dist/src/state/commands.d.ts +18 -0
  85. package/dist/src/state/commands.js +27 -0
  86. package/dist/src/state/domain.d.ts +9 -0
  87. package/dist/src/state/domain.js +9 -0
  88. package/dist/src/state/guards.d.ts +7 -0
  89. package/dist/src/state/guards.js +17 -0
  90. package/dist/src/state/io.d.ts +10 -0
  91. package/dist/src/state/io.js +35 -0
  92. package/dist/src/state/items.d.ts +3 -4
  93. package/dist/src/state/items.js +58 -57
  94. package/dist/src/state/lessons.js +14 -7
  95. package/dist/src/state/lifecycle.js +2 -2
  96. package/dist/src/state/lock.js +7 -6
  97. package/dist/src/state/memory.js +24 -12
  98. package/dist/src/state/{archive.js → migrate.js} +7 -7
  99. package/dist/src/state/partition.d.ts +15 -0
  100. package/dist/src/state/partition.js +90 -0
  101. package/dist/src/state/paths.d.ts +11 -0
  102. package/dist/src/state/paths.js +12 -0
  103. package/dist/src/state/render.js +9 -6
  104. package/dist/src/state/store.js +9 -15
  105. package/dist/src/state/transitions.js +9 -7
  106. package/dist/src/state/validate.js +50 -7
  107. package/dist/src/state.d.ts +16 -11
  108. package/dist/src/state.js +16 -11
  109. package/dist/src/task-ui.d.ts +54 -0
  110. package/dist/src/task-ui.js +1570 -0
  111. package/dist/src/upgrade-board.js +10 -10
  112. package/dist/src/upgrade.d.ts +0 -1
  113. package/dist/src/upgrade.js +19 -83
  114. package/dist/src/workflow.js +3 -3
  115. package/docs/README.md +6 -2
  116. package/docs/architecture.md +9 -8
  117. package/docs/command-reference.md +81 -20
  118. package/docs/configuration.md +28 -8
  119. package/docs/coordination-service.md +18 -6
  120. package/docs/design-decisions.md +83 -0
  121. package/docs/development.md +27 -2
  122. package/docs/loop-local-run.md +495 -0
  123. package/docs/loop-runbook.md +235 -0
  124. package/docs/operating-workflow.md +2 -2
  125. package/docs/testing-and-release.md +5 -1
  126. package/docs/troubleshooting.md +125 -0
  127. package/package.json +4 -1
  128. package/dist/src/ledger.d.ts +0 -22
  129. package/dist/src/ledger.js +0 -119
  130. package/dist/src/state/shared.d.ts +0 -29
  131. package/dist/src/state/shared.js +0 -71
  132. package/dist/src/tracker.d.ts +0 -36
  133. package/dist/src/tracker.js +0 -44
  134. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Interactive layer: TTY detection, dynamic @inquirer/prompts loading, and the
3
+ * prompting flows (loop configuration, MCP setup selections, confirmations).
4
+ * Parsing of argv into options happens in ./argv.ts and ./options.ts.
5
+ */
6
+ import { stdin, stdout } from "node:process";
7
+ import { LOOP_PROVIDERS } from "../model.js";
8
+ import { validateLoopConfig } from "../profiles.js";
9
+ import { loopSelection, mcpArgSelections, parseWorkerMapping } from "./options.js";
10
+ export const interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
11
+ const prompts = async () => import("@inquirer/prompts");
12
+ export const chooseLoop = async (args, upgrade) => {
13
+ let { mode, lead: leadArg, workers: workersArg } = loopSelection(args);
14
+ if (!mode && (leadArg || workersArg))
15
+ throw new Error("--loop-lead and --loop-workers require --loop-mode");
16
+ if (!mode && interactive()) {
17
+ const { select } = await prompts();
18
+ mode = await select({ message: "Loop execution mode", choices: [
19
+ ...(upgrade ? [{ name: "Keep existing loop configuration", value: "keep" }] : []),
20
+ { name: "Off", value: "off" }, { name: "Local", value: "local" }, { name: "Fleet", value: "fleet" }
21
+ ] });
22
+ }
23
+ if (!mode || mode === "keep")
24
+ return undefined;
25
+ if (mode === "off") {
26
+ if (leadArg || workersArg)
27
+ throw new Error("Disabled loop cannot specify agents");
28
+ return validateLoopConfig({ mode });
29
+ }
30
+ let lead = leadArg, rawWorkers = workersArg;
31
+ if (interactive() && (!lead || !rawWorkers)) {
32
+ const { select, input } = await prompts();
33
+ lead ??= await select({ message: "Lead agent", choices: LOOP_PROVIDERS.map((name) => ({ name, value: name })) });
34
+ rawWorkers ??= await input({ message: "Workers (id=provider, comma separated)", default: "codex-worker=codex,claude-worker=claude" });
35
+ }
36
+ if (!lead || !rawWorkers)
37
+ throw new Error("An enabled loop requires --loop-lead and --loop-workers");
38
+ return validateLoopConfig({ mode, agents: { lead, workers: parseWorkerMapping(rawWorkers) } });
39
+ };
40
+ export const mcpSetupSelections = async (args) => {
41
+ const selected = mcpArgSelections(args);
42
+ if ((!selected.deployment || !selected.storage || !selected.bind || !selected.workspace) && !interactive())
43
+ throw new Error("mcp setup outside a TTY requires --deployment, --storage, --bind and --workspace");
44
+ if (interactive()) {
45
+ const { input, select, checkbox } = await prompts();
46
+ selected.deployment ??= await select({ message: "State service deployment", choices: [{ name: "Docker on this machine", value: "docker" }, { name: "Remote service (configure endpoint later)", value: "remote" }] });
47
+ selected.storage ??= await select({ message: "Database", choices: [{ name: "SQLite (local Docker volume)", value: "sqlite" }, { name: "PostgreSQL (multi-host production)", value: "postgres" }] });
48
+ selected.bind ??= await select({ message: "Network binding", choices: [{ name: "localhost only", value: "localhost" }, { name: "network (requires token/TLS)", value: "network" }] });
49
+ selected.workspace ??= await input({ message: "Workspace ID", default: "default", required: true });
50
+ if (!selected.providers.length)
51
+ selected.providers = await checkbox({ message: "Project-management connectors to configure", choices: ["jira", "trello", "github-issues"].map((value) => ({ name: value, value })) });
52
+ }
53
+ if (!Number.isInteger(selected.pollMs) || selected.pollMs < 1_000 || selected.pollMs > 3_600_000)
54
+ throw new Error("--poll-ms must be an integer from 1000 to 3600000");
55
+ return selected;
56
+ };
57
+ export const confirmGeneral = async (options, operation) => {
58
+ if (options.dryRun)
59
+ return false;
60
+ if (options.yes)
61
+ return true;
62
+ if (!interactive())
63
+ return false;
64
+ const { confirm } = await prompts();
65
+ return confirm({ message: `${operation} this plan?`, default: false });
66
+ };
package/dist/src/cli.js CHANGED
@@ -1,96 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { stdin, stdout } from "node:process";
3
- import { spawnSync } from "node:child_process";
4
- import { resolve, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { installableAdapters } from "./adapters.js";
7
- import { applyPlan, detectedAgents, doctor, formatPlan, planInit, planUninstall, selectAgents, status } from "./installer.js";
8
- import { LOOP_PROVIDERS } from "./model.js";
9
- import { loadProjectConfig, resolveProfiles, validateLoopConfig } from "./profiles.js";
2
+ import { resolvedAdapters } from "./adapters.js";
3
+ import { assertKnownOptions, flags, INIT_FLAGS, rootArg, UNINSTALL_FLAGS, UPGRADE_FLAGS, usage } from "./cli/argv.js";
4
+ import { runDoctor, runLoopCommand, runMcp, runProfile, runStatus, runUi } from "./cli/commands.js";
5
+ import { initOptions } from "./cli/options.js";
6
+ import { chooseLoop, confirmGeneral, interactive } from "./cli/prompt.js";
7
+ import { applyPlan, detectedAgents, formatPlan, planInit, planUninstall, selectAgents } from "./installer.js";
10
8
  import { applyUpgrade, planUpgrade } from "./upgrade.js";
11
9
  import { packageVersion } from "./workflow.js";
12
- import { applyMcpSetup, planMcpSetup } from "./mcp-setup.js";
13
- const usage = `Usage:
14
- npx @felixdotgo/aidlc-kit init [path] [--agent <name[,name]> | --all] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--yes] [--dry-run] [--force]
15
- npx @felixdotgo/aidlc-kit upgrade [path] [--loop-mode off|local|fleet] [--loop-lead ${LOOP_PROVIDERS.join("|")}] [--loop-workers <id=provider,...>] [--dry-run]
16
- npx @felixdotgo/aidlc-kit uninstall [path] [--yes] [--dry-run]
17
- npx @felixdotgo/aidlc-kit status [path]
18
- npx @felixdotgo/aidlc-kit doctor [path] [--strict]
19
- npx @felixdotgo/aidlc-kit profile validate [path]
20
- npx @felixdotgo/aidlc-kit loop start|status|resume [--task-id ID] [--root PATH] [--mode local|fleet]
21
- npx @felixdotgo/aidlc-kit mcp setup [path] [--dry-run] [--yes] [--storage sqlite|postgres] [--deployment docker|remote] [--bind localhost|network] [--workspace <id>] [--poll-ms <n>] [--enable]`;
22
- const interactive = () => Boolean(stdin.isTTY && stdout.isTTY);
10
+ /* Kept in this file: test/unit/cli.test.ts pins these prompt sources (and the upgrade boundary strings below) to src/cli.ts; the shared interactive flows live in ./cli/prompt.ts. */
23
11
  const prompts = async () => import("@inquirer/prompts");
24
- /* --- Argv helpers --------------------------------------------------------- */
25
- const value = (args, name) => {
26
- const index = args.indexOf(name);
27
- return index < 0 ? undefined : args[index + 1];
28
- };
29
- const flags = (args) => new Set(args.filter((item) => item.startsWith("--")));
30
- const valueFlags = new Set(["--agent", "--storage", "--deployment", "--bind", "--workspace", "--poll-ms", "--token-env", "--providers", "--loop-mode", "--loop-lead", "--loop-workers"]);
31
- const COMMAND_KEYWORDS = ["init", "upgrade", "uninstall", "status", "doctor", "validate", "run", "list"];
32
- const positional = (args) => {
33
- const result = [];
34
- for (let index = 0; index < args.length; index += 1) {
35
- if (valueFlags.has(args[index])) {
36
- index += 1;
37
- continue;
38
- }
39
- if (!args[index].startsWith("--"))
40
- result.push(args[index]);
41
- }
42
- return result;
43
- };
44
- const rootArg = (args, fallback = ".") => positional(args).find((item) => !COMMAND_KEYWORDS.includes(item)) ?? fallback;
45
- /* --- Loop configuration --------------------------------------------------- */
46
- const parseWorkerMapping = (rawWorkers) => {
47
- const workers = Object.create(null);
48
- for (const entry of rawWorkers.split(",")) {
49
- const parts = entry.split("=");
50
- if (parts.length !== 2 || !parts[0] || !parts[1] || Object.hasOwn(workers, parts[0]))
51
- throw new Error("Invalid or duplicate loop worker mapping");
52
- workers[parts[0]] = parts[1];
53
- }
54
- return workers;
55
- };
56
- const chooseLoop = async (args, upgrade) => {
57
- let mode = value(args, "--loop-mode");
58
- const leadArg = value(args, "--loop-lead"), workersArg = value(args, "--loop-workers");
59
- if (!mode && (leadArg || workersArg))
60
- throw new Error("--loop-lead and --loop-workers require --loop-mode");
61
- if (!mode && interactive()) {
62
- const { select } = await prompts();
63
- mode = await select({ message: "Loop execution mode", choices: [
64
- ...(upgrade ? [{ name: "Keep existing loop configuration", value: "keep" }] : []),
65
- { name: "Off", value: "off" }, { name: "Local", value: "local" }, { name: "Fleet", value: "fleet" }
66
- ] });
67
- }
68
- if (!mode || mode === "keep")
69
- return undefined;
70
- if (mode === "off") {
71
- if (leadArg || workersArg)
72
- throw new Error("Disabled loop cannot specify agents");
73
- return validateLoopConfig({ mode });
74
- }
75
- let lead = leadArg, rawWorkers = workersArg;
76
- if (interactive() && (!lead || !rawWorkers)) {
77
- const { select, input } = await prompts();
78
- lead ??= await select({ message: "Lead agent", choices: LOOP_PROVIDERS.map((name) => ({ name, value: name })) });
79
- rawWorkers ??= await input({ message: "Workers (id=provider, comma separated)", default: "codex-worker=codex,claude-worker=claude" });
80
- }
81
- if (!lead || !rawWorkers)
82
- throw new Error("An enabled loop requires --loop-lead and --loop-workers");
83
- return validateLoopConfig({ mode, agents: { lead, workers: parseWorkerMapping(rawWorkers) } });
84
- };
85
- /* --- Option builders and shared flows -------------------------------------- */
86
- const initOptions = (args) => ({
87
- root: rootArg(args),
88
- agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
89
- all: flags(args).has("--all"),
90
- dryRun: flags(args).has("--dry-run"),
91
- yes: flags(args).has("--yes"),
92
- force: flags(args).has("--force")
93
- });
12
+ /* --- Agent selection and plan preview -------------------------------------- */
94
13
  const chooseAgents = async (options) => {
95
14
  const selected = selectAgents(options);
96
15
  if (selected.length)
@@ -101,7 +20,7 @@ const chooseAgents = async (options) => {
101
20
  if (!interactive())
102
21
  throw new Error(`Unable to select an agent automatically. Use --agent <name> or --all. Detected: ${detected.join(", ") || "none"}`);
103
22
  const { checkbox } = await prompts();
104
- const choices = installableAdapters();
23
+ const choices = resolvedAdapters();
105
24
  const agents = await checkbox({
106
25
  message: "Select one or more agents",
107
26
  choices: choices.map((adapter) => ({
@@ -116,67 +35,9 @@ const preview = (operation, plan) => {
116
35
  console.log(`◆ ${operation} preview — changes have NOT been applied`);
117
36
  console.log(formatPlan(plan));
118
37
  };
119
- const confirmGeneral = async (options, operation) => {
120
- if (options.dryRun)
121
- return false;
122
- if (options.yes)
123
- return true;
124
- if (!interactive())
125
- return false;
126
- const { confirm } = await prompts();
127
- return confirm({ message: `${operation} this plan?`, default: false });
128
- };
129
- /* --- MCP setup options ----------------------------------------------------- */
130
- const mcpSetupSelections = async (args) => {
131
- const selected = {
132
- deployment: value(args, "--deployment"),
133
- storage: value(args, "--storage"),
134
- bind: value(args, "--bind"),
135
- workspace: value(args, "--workspace"),
136
- pollMs: Number(value(args, "--poll-ms") ?? 30_000),
137
- tokenEnv: value(args, "--token-env") ?? "AIDLC_MCP_TOKEN",
138
- providers: (value(args, "--providers") ?? "").split(",").filter(Boolean)
139
- };
140
- if ((!selected.deployment || !selected.storage || !selected.bind || !selected.workspace) && !interactive())
141
- throw new Error("mcp setup outside a TTY requires --deployment, --storage, --bind and --workspace");
142
- if (interactive()) {
143
- const { input, select, checkbox } = await prompts();
144
- selected.deployment ??= await select({ message: "State service deployment", choices: [{ name: "Docker on this machine", value: "docker" }, { name: "Remote service (configure endpoint later)", value: "remote" }] });
145
- selected.storage ??= await select({ message: "Database", choices: [{ name: "SQLite (local Docker volume)", value: "sqlite" }, { name: "PostgreSQL (multi-host production)", value: "postgres" }] });
146
- selected.bind ??= await select({ message: "Network binding", choices: [{ name: "localhost only", value: "localhost" }, { name: "network (requires token/TLS)", value: "network" }] });
147
- selected.workspace ??= await input({ message: "Workspace ID", default: "default", required: true });
148
- if (!selected.providers.length)
149
- selected.providers = await checkbox({ message: "Project-management connectors to configure", choices: ["jira", "trello", "github-issues"].map((value) => ({ name: value, value })) });
150
- }
151
- if (!Number.isInteger(selected.pollMs) || selected.pollMs < 1_000 || selected.pollMs > 3_600_000)
152
- throw new Error("--poll-ms must be an integer from 1000 to 3600000");
153
- return selected;
154
- };
155
- const mcpSetupOptions = async (args) => {
156
- const selected = await mcpSetupSelections(args);
157
- return { root: rootArg(args), template: join(fileURLToPath(new URL("../services/aidlc-coordination", import.meta.url))), dryRun: flags(args).has("--dry-run"), deployment: selected.deployment, storage: selected.storage, bind: selected.bind, workspace: selected.workspace, pollMs: selected.pollMs, tokenEnv: selected.tokenEnv, providers: selected.providers, enable: flags(args).has("--enable") };
158
- };
159
- /* --- Command handlers ------------------------------------------------------ */
160
- const runLoopCommand = (args) => {
161
- const action = args[0];
162
- if (!["start", "status", "resume"].includes(action))
163
- throw new Error("Expected loop start, status, or resume");
164
- const parsed = new Map();
165
- for (let index = 1; index < args.length; index += 2) {
166
- const key = args[index], entry = args[index + 1];
167
- if (!["--task-id", "--root", "--mode"].includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
168
- throw new Error("Invalid or duplicate loop option");
169
- parsed.set(key, entry);
170
- }
171
- const root = resolve(parsed.get("--root") ?? ".");
172
- const script = join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs");
173
- const child = spawnSync(process.execPath, [script, action, parsed.get("--task-id") ?? "", root, parsed.get("--mode") ?? ""], { stdio: "inherit" });
174
- if (child.error)
175
- throw child.error;
176
- if (child.status !== 0)
177
- process.exitCode = child.status ?? 1;
178
- };
38
+ /* --- Command handlers that consume the pinned prompts above ----------------- */
179
39
  const runInit = async (args) => {
40
+ assertKnownOptions(args, INIT_FLAGS);
180
41
  const options = { ...await chooseAgents(initOptions(args)), loop: await chooseLoop(args, false) };
181
42
  const plan = planInit(options);
182
43
  preview("INIT", plan);
@@ -194,6 +55,7 @@ const runInit = async (args) => {
194
55
  }
195
56
  };
196
57
  const runUninstall = async (args) => {
58
+ assertKnownOptions(args, UNINSTALL_FLAGS);
197
59
  const options = initOptions(args);
198
60
  const plan = planUninstall(options.root);
199
61
  preview("UNINSTALL", plan);
@@ -203,6 +65,7 @@ const runUninstall = async (args) => {
203
65
  console.log("Removed eligible managed assets; project config and state were preserved.");
204
66
  };
205
67
  const runUpgrade = async (args) => {
68
+ assertKnownOptions(args, UPGRADE_FLAGS);
206
69
  if (flags(args).has("--yes") || flags(args).has("--force"))
207
70
  throw new Error("upgrade does not support --yes or --force; apply requires a human interactive TTY confirmation");
208
71
  const root = rootArg(args);
@@ -221,48 +84,19 @@ const runUpgrade = async (args) => {
221
84
  const result = applyUpgrade(root, plan);
222
85
  console.log(`Upgrade complete. Backup: ${result.backup}; changed: ${result.changed}`);
223
86
  };
224
- const runMcpSetup = async (args) => {
225
- const options = await mcpSetupOptions(args.slice(1));
226
- const plan = planMcpSetup(options);
227
- console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
228
- console.log(JSON.stringify(plan, null, 2));
229
- if (options.dryRun)
230
- return;
231
- if (!await confirmGeneral({ ...initOptions(args), root: options.root }, "Apply MCP setup"))
232
- return;
233
- applyMcpSetup(options);
234
- console.log(`MCP setup written to ${plan.target}. MCP remains ${options.enable ? "enabled" : "disabled"}; start Docker explicitly with docker compose --env-file .env -f ${join(plan.target, "compose.yaml")} up -d.`);
235
- };
87
+ const runHelp = () => console.log(usage);
88
+ const runVersion = () => console.log(packageVersion());
89
+ const REGISTRY = { init: runInit, upgrade: runUpgrade, uninstall: runUninstall, status: runStatus, doctor: runDoctor, loop: runLoopCommand, ui: runUi, mcp: runMcp, profile: runProfile, version: runVersion, help: runHelp };
236
90
  const main = async () => {
237
- const [command = "help", ...args] = process.argv.slice(2);
238
- if (command === "help" || command === "--help")
91
+ const argv = process.argv.slice(2);
92
+ if (argv.includes("--version") || argv.includes("-v"))
93
+ return void console.log(packageVersion());
94
+ if (argv.includes("--help"))
239
95
  return void console.log(usage);
240
- if (command === "loop")
241
- return void runLoopCommand(args);
242
- if (command === "status")
243
- return void console.log(status(rootArg(args)));
244
- if (command === "doctor") {
245
- const result = doctor(rootArg(args), flags(args).has("--strict"));
246
- console.log(result);
247
- if (result.startsWith("ERROR:"))
248
- process.exitCode = 1;
249
- return;
250
- }
251
- if (command === "init")
252
- return void await runInit(args);
253
- if (command === "uninstall")
254
- return void await runUninstall(args);
255
- if (command === "upgrade")
256
- return void await runUpgrade(args);
257
- if (command === "mcp" && args[0] === "setup")
258
- return void await runMcpSetup(args);
259
- if (command === "profile" && args[0] === "validate") {
260
- const root = rootArg(args.slice(1));
261
- const config = loadProjectConfig(root);
262
- const profiles = resolveProfiles(root, config.extends);
263
- console.log(`OK: ${profiles.map((item) => item.id).join(" → ")}`);
264
- return;
265
- }
266
- throw new Error(`Unknown command: ${command}`);
96
+ const [command = "help", ...args] = argv;
97
+ const handler = Object.hasOwn(REGISTRY, command) ? REGISTRY[command] : undefined;
98
+ if (!handler)
99
+ throw new Error(`Unknown command: ${command}`);
100
+ return void await handler(args);
267
101
  };
268
102
  main().catch((error) => { console.error(`aidlc-kit: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });
package/dist/src/gate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { resolveProjectPathWithoutSymlinks } from "./project-path.js";
3
- import { hasAreaVerification, hasReview, isLegacyG2Wait, nextAction, repairBounds, transitionDiagnostics, transitionTask } from "./state.js";
3
+ import { hasAreaVerification, hasReview, isLegacyG2Wait, loopPartitionStrict, nextAction, partitionDiagnostics, repairBounds, transitionDiagnostics, transitionTask } from "./state.js";
4
4
  const safeArtifactTarget = (root, path) => resolveProjectPathWithoutSymlinks(root, path, "Unsafe artifact path", "Artifact path crosses a symlink");
5
5
  const artifactContent = (root, path) => {
6
6
  if (!path)
@@ -40,6 +40,7 @@ const reviewGateDiagnostics = (root, task) => {
40
40
  if (content !== undefined)
41
41
  for (const heading of missingHeadings(content, ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]))
42
42
  diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` });
43
+ diagnostics.push(...partitionDiagnostics(task, { strict: loopPartitionStrict(root) }));
43
44
  return diagnostics;
44
45
  };
45
46
  const codeReviewGateDiagnostics = (root, task) => {
@@ -82,7 +83,7 @@ export const checkGate = (root, state, taskId, gate) => {
82
83
  diagnostics.push(...reviewGateDiagnostics(root, task));
83
84
  if (gate === "G2_codereview")
84
85
  diagnostics.push(...codeReviewGateDiagnostics(root, task));
85
- if (!diagnostics.length)
86
+ if (!diagnostics.some((item) => item.level === "ERROR"))
86
87
  diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` });
87
88
  return diagnostics;
88
89
  };
@@ -1,12 +1,8 @@
1
- import type { AgentId, FileSpec, InitOptions, LoopConfig, ManifestInventoryItem, PlannedWrite, WorkflowManifest } from "./model.js";
2
- /** Managed prompt blocks share a path across owners, so inventory keys carry the owner. */
3
- export declare const installedHashFor: (files: Record<string, ManifestInventoryItem> | undefined, spec: Pick<FileSpec, "path" | "owner" | "strategy">) => string | undefined;
4
- export declare const managedContentHash: (spec: Pick<FileSpec, "owner" | "strategy">, content: string) => string | undefined;
5
- export declare const readManifest: (root: string) => WorkflowManifest | undefined;
1
+ import type { AgentId, InitOptions, PlannedWrite } from "./model.js";
2
+ /** Manifest reading lives in the managed-planning core; this export path stays stable for existing importers. */
3
+ export { readManifest } from "./managed-planning.js";
6
4
  export declare const detectedAgents: (root: string) => AgentId[];
7
5
  export declare const selectAgents: (options: Pick<InitOptions, "root" | "agents" | "all">) => AgentId[];
8
- /** Include an explicitly selected loop config in the same preview/transaction as setup. */
9
- export declare const planLoopSelection: (plan: PlannedWrite[], selection?: LoopConfig) => void;
10
6
  /** Plans an init transaction: managed workflow files, manifest, and project-owned files. */
11
7
  export declare const planInit: (options: InitOptions) => PlannedWrite[];
12
8
  /** Applies a planned init/uninstall transaction to disk. */
@@ -14,7 +10,18 @@ export declare const applyPlan: (root: string, plan: PlannedWrite[]) => void;
14
10
  export declare const formatPlan: (plan: PlannedWrite[]) => string;
15
11
  /** Plans an uninstall transaction that removes only unchanged managed assets. */
16
12
  export declare const planUninstall: (root: string) => PlannedWrite[];
13
+ export declare const statusData: (root: string) => {
14
+ workflow: string;
15
+ installedVersion: string;
16
+ schema: string;
17
+ detectedAgents: string[];
18
+ };
17
19
  /** Reports installed workflow version, schema, and detected agents. */
18
20
  export declare const status: (root: string) => string;
19
21
  /** Validates the installed workflow manifest and local config/state. */
20
22
  export declare const doctor: (root: string, strict?: boolean) => string;
23
+ export declare const doctorData: (root: string, strict?: boolean) => {
24
+ level: "ok" | "warn" | "error";
25
+ message: string;
26
+ strict: boolean;
27
+ };
@@ -1,113 +1,23 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { adapters, findAdapter, installableAdapters, resolvedAdapters } from "./adapters.js";
4
- import { loadProjectConfig, validateLoopConfig } from "./profiles.js";
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, mergeManagedBlock } from "./managed-content.js";
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
- /** 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
- };
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-kit" || value.managedBy === "aidlc-workflow") && Array.isArray(value.adapters) && validFiles ? value : undefined;
34
- }
35
- catch {
36
- return undefined;
37
- } /* intentional: a malformed manifest is treated as "not installed" */
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
- const planFile = (root, spec, force, installed) => {
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
- };
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
21
  /** Plans an init transaction: managed workflow files, manifest, and project-owned files. */
112
22
  export const planInit = (options) => {
113
23
  const root = resolve(options.root);
@@ -121,7 +31,7 @@ export const planInit = (options) => {
121
31
  const agentFiles = selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
122
32
  const managedFiles = [...coreWorkflowFiles(), ...agentFiles];
123
33
  const files = [...managedFiles, manifestSpec(managedFiles, selected), ...initialProjectFiles()];
124
- const plan = planManagedWrites(root, files, options.force, installed);
34
+ const plan = planManagedWrites(root, files, { flow: "init", force: options.force, installed, resolvePath: safeTarget });
125
35
  planProjectLayoutMigration(root, plan);
126
36
  planLoopSelection(plan, options.loop);
127
37
  return plan;
@@ -137,28 +47,10 @@ export const applyPlan = (root, plan) => {
137
47
  }
138
48
  };
139
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");
140
- const hasOwnedManifest = (root) => {
141
- const path = join(resolve(root), ".agents/aidlc/manifest.json");
142
- if (!existsSync(path))
143
- return false;
144
- try {
145
- const parsed = JSON.parse(readFileSync(path, "utf8"));
146
- return parsed.managedBy === "aidlc-kit" || parsed.managedBy === "aidlc-workflow";
147
- }
148
- catch {
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
50
  /** Maps one manifest inventory record onto its current spec and installed hash. */
158
51
  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;
52
+ const { path, owner: keyedOwner } = splitInventoryKey(key);
53
+ const owner = keyedOwner ?? item.owner;
162
54
  const spec = currentSpecs.find((candidate) => candidate.path === path && (candidate.strategy !== "managed-block" || candidate.owner === owner)) ?? { path, owner, strategy: item.strategy, content: "", ownershipClass: "managed" };
163
55
  return { spec, hash: item.hash };
164
56
  };
@@ -178,7 +70,7 @@ const stripManagedBlocks = (group, current) => {
178
70
  const block = managedBlock(spec, next);
179
71
  // A v0.2.1 single-marker block cannot be removed safely; retain ownership instead of orphaning it.
180
72
  if (!block) {
181
- if (next.includes(`<!-- aidlc-installer:${spec.owner} -->`))
73
+ if (next.includes(legacyManagedMarker(spec.owner)))
182
74
  modified = spec;
183
75
  continue;
184
76
  }
@@ -200,7 +92,7 @@ const uninstallWholeFile = (group, exists, current) => {
200
92
  return contentHash(current) === installedHash ? { ...spec, action: "delete", reason: "remove unchanged managed file" } : { ...spec, action: "preserve", reason: "managed file was modified" };
201
93
  }
202
94
  const { next, modified } = stripManagedBlocks(group, current);
203
- const legacyResidue = group.find(({ spec }) => next.includes(`<!-- aidlc-installer:${spec.owner} -->`));
95
+ const legacyResidue = group.find(({ spec }) => next.includes(legacyManagedMarker(spec.owner)));
204
96
  const ownerSpec = group[0].spec;
205
97
  if (modified)
206
98
  return { ...modified, action: "preserve", reason: "managed block was modified" };
@@ -233,45 +125,45 @@ export const planUninstall = (root) => {
233
125
  plan.push(manifestDisposition(plan));
234
126
  return plan;
235
127
  };
236
- /** Reports installed workflow version, schema, and detected agents. */
237
- export const status = (root) => {
128
+ export const statusData = (root) => {
238
129
  const manifest = readManifest(root);
239
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);
240
141
  return [
241
- `workflow: ${manifest || existsSync(legacyPath) ? "installed" : "not installed"}`,
242
- `installed version: ${manifest?.packageVersion ?? (existsSync(legacyPath) ? "legacy/unknown" : "none")}`,
243
- `schema: ${manifest?.schemaVersion ?? (existsSync(legacyPath) ? "legacy" : "none")}`,
244
- `detected agents: ${detectedAgents(root).join(", ") || "none"}`
142
+ `workflow: ${data.workflow}`,
143
+ `installed version: ${data.installedVersion}`,
144
+ `schema: ${data.schema}`,
145
+ `detected agents: ${data.detectedAgents.join(", ") || "none"}`
245
146
  ].join("\n");
246
147
  };
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
148
  /** Manifest inventory paths whose on-disk content no longer matches the recorded owner hash. */
253
149
  const modifiedManagedPaths = (root, manifest) => {
254
150
  const modified = Object.entries(manifest.files).filter(([key, item]) => {
255
- const target = safeTarget(root, inventoryKeyPath(key));
151
+ const target = safeTarget(root, splitInventoryKey(key).path);
256
152
  if (!existsSync(target))
257
153
  return true;
258
154
  const spec = { owner: item.owner, strategy: item.strategy };
259
155
  return managedContentHash(spec, readFileSync(target, "utf8")) !== item.hash;
260
156
  });
261
- return [...new Set(modified.map(([key]) => inventoryKeyPath(key)))];
157
+ return [...new Set(modified.map(([key]) => splitInventoryKey(key).path))];
262
158
  };
263
159
  /** Validates the installed workflow manifest and local config/state. */
264
160
  export const doctor = (root, strict = false) => {
265
- const manifestPath = join(resolve(root), ".agents/aidlc/manifest.json");
266
- if (!existsSync(manifestPath))
161
+ const read = readManifestFile(root);
162
+ if (read.status === "missing")
267
163
  return "ERROR: .agents/aidlc/manifest.json is missing. Run npx @felixdotgo/aidlc-kit init --yes.";
268
- let parsed;
269
- try {
270
- parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
271
- }
272
- catch {
164
+ if (read.status === "invalid")
273
165
  return "ERROR: workflow manifest is invalid JSON.";
274
- }
166
+ const parsed = read.value;
275
167
  if (parsed.remoteUpdates || parsed.source !== "local-package-assets" || (parsed.managedBy !== "aidlc-kit" && parsed.managedBy !== "aidlc-workflow"))
276
168
  return "ERROR: workflow manifest violates local-only asset policy.";
277
169
  if (typeof parsed.schemaVersion === "number" && parsed.schemaVersion > 2)
@@ -291,3 +183,8 @@ export const doctor = (root, strict = false) => {
291
183
  return `${strict ? "ERROR" : "WARN"}: managed integrity differs: ${modified.join(", ")}`;
292
184
  return `OK: local workflow ${manifest.packageVersion} is valid; remote updates are disabled.`;
293
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
+ };