@felixdotgo/aidlc-kit 0.1.1 → 0.3.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 (198) hide show
  1. package/DEVELOPMENT.md +14 -13
  2. package/README.md +37 -43
  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 +2 -2
  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/schemas/state.schema.json +2 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
  12. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
  38. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
  40. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
  41. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  42. package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
  43. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  44. package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
  45. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
  46. package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
  47. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  48. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  49. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  50. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  51. package/dist/assets/ui/bulma.min.css +3 -0
  52. package/dist/assets/ui/changes.js +83 -0
  53. package/dist/assets/ui/common.js +664 -0
  54. package/dist/assets/ui/dashboard.css +636 -0
  55. package/dist/assets/ui/dashboard.js +77 -0
  56. package/dist/assets/ui/detail.js +159 -0
  57. package/dist/assets/ui/live-log.js +332 -0
  58. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  59. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  60. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  61. package/dist/services/aidlc-coordination/package.json +9 -4
  62. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
  63. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  64. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  65. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  66. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  67. package/dist/src/adapters.d.ts +0 -3
  68. package/dist/src/adapters.js +3 -5
  69. package/dist/src/cli/argv.d.ts +12 -0
  70. package/dist/src/cli/argv.js +68 -0
  71. package/dist/src/cli/commands.d.ts +8 -0
  72. package/dist/src/cli/commands.js +159 -0
  73. package/dist/src/cli/loop-launcher.d.ts +47 -0
  74. package/dist/src/cli/loop-launcher.js +322 -0
  75. package/dist/src/cli/options.d.ts +25 -0
  76. package/dist/src/cli/options.js +32 -0
  77. package/dist/src/cli/prompt.d.ts +9 -0
  78. package/dist/src/cli/prompt.js +66 -0
  79. package/dist/src/cli.js +28 -191
  80. package/dist/src/gate.js +3 -2
  81. package/dist/src/installer.d.ts +14 -7
  82. package/dist/src/installer.js +42 -143
  83. package/dist/src/legacy.js +1 -0
  84. package/dist/src/managed-content.d.ts +7 -0
  85. package/dist/src/managed-content.js +15 -4
  86. package/dist/src/managed-planning.d.ts +65 -0
  87. package/dist/src/managed-planning.js +219 -0
  88. package/dist/src/mcp-setup.js +3 -1
  89. package/dist/src/model/config-contracts.d.ts +65 -0
  90. package/dist/src/model/config-contracts.js +1 -0
  91. package/dist/src/model/install-contracts.d.ts +29 -0
  92. package/dist/src/model/install-contracts.js +1 -0
  93. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  94. package/dist/src/model/lifecycle-enums.js +1 -0
  95. package/dist/src/model/loop.d.ts +20 -0
  96. package/dist/src/model/loop.js +1 -0
  97. package/dist/src/model/manifest.d.ts +16 -0
  98. package/dist/src/model/manifest.js +1 -0
  99. package/dist/src/model/records.d.ts +142 -0
  100. package/dist/src/model/records.js +1 -0
  101. package/dist/src/model.d.ts +11 -264
  102. package/dist/src/model.js +1 -1
  103. package/dist/src/profiles.d.ts +2 -0
  104. package/dist/src/profiles.js +53 -9
  105. package/dist/src/state/archive-record.d.ts +9 -0
  106. package/dist/src/state/archive-record.js +26 -0
  107. package/dist/src/state/commands.d.ts +18 -0
  108. package/dist/src/state/commands.js +27 -0
  109. package/dist/src/state/domain.d.ts +9 -0
  110. package/dist/src/state/domain.js +9 -0
  111. package/dist/src/state/guards.d.ts +7 -0
  112. package/dist/src/state/guards.js +17 -0
  113. package/dist/src/state/io.d.ts +10 -0
  114. package/dist/src/state/io.js +35 -0
  115. package/dist/src/state/items.d.ts +3 -4
  116. package/dist/src/state/items.js +58 -57
  117. package/dist/src/state/lessons.js +14 -7
  118. package/dist/src/state/lifecycle.js +2 -2
  119. package/dist/src/state/lock.js +7 -6
  120. package/dist/src/state/memory.js +24 -12
  121. package/dist/src/state/{archive.js → migrate.js} +7 -7
  122. package/dist/src/state/partition.d.ts +15 -0
  123. package/dist/src/state/partition.js +90 -0
  124. package/dist/src/state/paths.d.ts +11 -0
  125. package/dist/src/state/paths.js +12 -0
  126. package/dist/src/state/render.js +9 -6
  127. package/dist/src/state/store.js +9 -15
  128. package/dist/src/state/transitions.js +9 -7
  129. package/dist/src/state/validate.js +55 -7
  130. package/dist/src/state.d.ts +16 -11
  131. package/dist/src/state.js +16 -11
  132. package/dist/src/task-ui.d.ts +33 -0
  133. package/dist/src/task-ui.js +33 -0
  134. package/dist/src/ui/assets.d.ts +14 -0
  135. package/dist/src/ui/assets.js +45 -0
  136. package/dist/src/ui/changes.d.ts +83 -0
  137. package/dist/src/ui/changes.js +498 -0
  138. package/dist/src/ui/containers.d.ts +71 -0
  139. package/dist/src/ui/containers.js +214 -0
  140. package/dist/src/ui/daemon.d.ts +75 -0
  141. package/dist/src/ui/daemon.js +232 -0
  142. package/dist/src/ui/data.d.ts +42 -0
  143. package/dist/src/ui/data.js +89 -0
  144. package/dist/src/ui/docs.d.ts +13 -0
  145. package/dist/src/ui/docs.js +39 -0
  146. package/dist/src/ui/files.d.ts +3 -0
  147. package/dist/src/ui/files.js +20 -0
  148. package/dist/src/ui/http.d.ts +14 -0
  149. package/dist/src/ui/http.js +80 -0
  150. package/dist/src/ui/live.d.ts +5 -0
  151. package/dist/src/ui/live.js +43 -0
  152. package/dist/src/ui/markdown.d.ts +2 -0
  153. package/dist/src/ui/markdown.js +127 -0
  154. package/dist/src/ui/options.d.ts +48 -0
  155. package/dist/src/ui/options.js +81 -0
  156. package/dist/src/ui/security.d.ts +34 -0
  157. package/dist/src/ui/security.js +38 -0
  158. package/dist/src/ui/server.d.ts +39 -0
  159. package/dist/src/ui/server.js +347 -0
  160. package/dist/src/ui/sse.d.ts +14 -0
  161. package/dist/src/ui/sse.js +281 -0
  162. package/dist/src/ui/views/board.d.ts +7 -0
  163. package/dist/src/ui/views/board.js +131 -0
  164. package/dist/src/ui/views/changes.d.ts +19 -0
  165. package/dist/src/ui/views/changes.js +154 -0
  166. package/dist/src/ui/views/components.d.ts +56 -0
  167. package/dist/src/ui/views/components.js +113 -0
  168. package/dist/src/ui/views/docs.d.ts +15 -0
  169. package/dist/src/ui/views/docs.js +64 -0
  170. package/dist/src/ui/views/layout.d.ts +44 -0
  171. package/dist/src/ui/views/layout.js +233 -0
  172. package/dist/src/ui/views/task.d.ts +25 -0
  173. package/dist/src/ui/views/task.js +320 -0
  174. package/dist/src/upgrade-board.js +10 -10
  175. package/dist/src/upgrade.d.ts +0 -1
  176. package/dist/src/upgrade.js +24 -85
  177. package/dist/src/workflow.d.ts +4 -0
  178. package/dist/src/workflow.js +36 -4
  179. package/docs/README.md +41 -22
  180. package/docs/architecture.md +101 -37
  181. package/docs/command-reference.md +289 -52
  182. package/docs/configuration.md +70 -30
  183. package/docs/coordination-service.md +91 -30
  184. package/docs/design-decisions.md +87 -0
  185. package/docs/development.md +124 -49
  186. package/docs/loop-local-run.md +228 -0
  187. package/docs/loop-runbook.md +214 -0
  188. package/docs/operating-workflow.md +55 -42
  189. package/docs/testing-and-release.md +72 -49
  190. package/docs/troubleshooting.md +221 -0
  191. package/package.json +4 -1
  192. package/dist/src/ledger.d.ts +0 -22
  193. package/dist/src/ledger.js +0 -119
  194. package/dist/src/state/shared.d.ts +0 -29
  195. package/dist/src/state/shared.js +0 -71
  196. package/dist/src/tracker.d.ts +0 -36
  197. package/dist/src/tracker.js +0 -44
  198. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Command handlers for loop, MCP setup, status, doctor, and profile validate.
3
+ * Interactive prompting stays in ./prompt.ts and is invoked from here after
4
+ * argv parsing.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { doctor, doctorData, status, statusData } from "../installer.js";
10
+ import { applyMcpSetup, planMcpSetup } from "../mcp-setup.js";
11
+ import { loadProjectConfig, resolveProfiles } from "../profiles.js";
12
+ import { assertKnownOptions, DOCTOR_FLAGS, flags, MCP_FLAGS, rootArg, STATUS_FLAGS, UI_FLAGS, value } from "./argv.js";
13
+ import { loopDown, loopEnv, loopOverview, loopStop, loopUp, serveDashboard } from "./loop-launcher.js";
14
+ import { confirmGeneral, mcpSetupSelections } from "./prompt.js";
15
+ /** `loop leases [--task-id ID] [--root PATH] [--json] [--clear-stale]` and
16
+ * `loop migrate-data [--root PATH] [--json] [--dry-run]`: forwarded to
17
+ * loop-entry.mjs as `<action> [task-id] <root> [switches]`. */
18
+ const loopDiagnosticArgs = (action, args) => {
19
+ const allowedSwitches = action === "leases" ? ["--json", "--clear-stale"] : ["--json", "--dry-run"];
20
+ const allowedValues = action === "leases" ? ["--task-id", "--root"] : ["--root"];
21
+ const parsed = new Map();
22
+ const switches = new Set();
23
+ for (let index = 0; index < args.length; index += 1) {
24
+ const key = args[index] ?? "";
25
+ if (allowedSwitches.includes(key)) {
26
+ if (switches.has(key))
27
+ throw new Error("Invalid or duplicate loop option");
28
+ switches.add(key);
29
+ continue;
30
+ }
31
+ const entry = args[index + 1];
32
+ if (!allowedValues.includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
33
+ throw new Error("Invalid or duplicate loop option");
34
+ parsed.set(key, entry);
35
+ index += 1;
36
+ }
37
+ const root = resolve(parsed.get("--root") ?? ".");
38
+ return { root, argv: action === "leases" ? ["leases", parsed.get("--task-id") ?? "", root, ...switches] : ["migrate-data", root, ...switches] };
39
+ };
40
+ export const runLoopCommand = async (args) => {
41
+ const action = args[0] ?? "";
42
+ if (!["up", "down", "stop", "env", "start", "status", "resume", "intake", "worker-image", "leases", "migrate-data"].includes(action))
43
+ throw new Error("Expected loop up, down, stop, env, start, status, intake, leases, migrate-data, or worker-image");
44
+ if (action === "up")
45
+ return loopUp(args.slice(1));
46
+ if (action === "down")
47
+ return loopDown(args.slice(1));
48
+ if (action === "stop")
49
+ return loopStop(args.slice(1));
50
+ if (action === "env")
51
+ return loopEnv(args.slice(1));
52
+ if (action === "status" && !args.includes("--task-id"))
53
+ return loopOverview(args.slice(1));
54
+ if (action === "leases" || action === "migrate-data") {
55
+ const { root, argv } = loopDiagnosticArgs(action, args.slice(1));
56
+ const child = spawn(process.execPath, [join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs"), ...argv], { stdio: "inherit" });
57
+ const code = await new Promise((resolveExit, rejectExit) => { child.once("error", rejectExit); child.once("exit", (exitCode) => resolveExit(exitCode)); });
58
+ if (code !== 0)
59
+ process.exitCode = code ?? 1;
60
+ return;
61
+ }
62
+ const parsed = new Map();
63
+ for (let index = 1; index < args.length; index += 2) {
64
+ const key = args[index];
65
+ const entry = args[index + 1];
66
+ if (key === undefined || !["--task-id", "--title", "--root", "--mode", "--provider", "--tag", "--loop-env"].includes(key) || !entry || entry.startsWith("--") || parsed.has(key))
67
+ throw new Error("Invalid or duplicate loop option");
68
+ parsed.set(key, entry);
69
+ }
70
+ const root = resolve(parsed.get("--root") ?? ".");
71
+ const script = join(root, ".agents", "aidlc", "scripts", "loop-entry.mjs");
72
+ const child = spawn(process.execPath, [script, action, parsed.get("--task-id") ?? "", root, parsed.get("--mode") ?? "", parsed.get("--provider") ?? "", parsed.get("--tag") ?? "", parsed.get("--title") ?? ""], { stdio: "inherit", env: { ...process.env, ...(parsed.has("--loop-env") ? { AIDLC_ENV_FILE: resolve(parsed.get("--loop-env") ?? "") } : {}) } });
73
+ // Stay alive while the child runs so its own staged shutdown (stop after the
74
+ // current turn → cancel the turn → exit) stays reachable. A terminal Ctrl-C
75
+ // already reaches the child through the process group, so SIGINT is only
76
+ // swallowed here (forwarding it would count twice); SIGTERM sent to this
77
+ // wrapper alone (e.g. `kill <pid>`) is forwarded instead of orphaning the child.
78
+ const onInterrupt = () => { };
79
+ const onTerminate = () => { if (child.exitCode === null && child.signalCode === null)
80
+ child.kill("SIGTERM"); };
81
+ process.on("SIGINT", onInterrupt);
82
+ process.on("SIGTERM", onTerminate);
83
+ try {
84
+ const { code, signal } = await new Promise((resolveExit, rejectExit) => {
85
+ child.once("error", rejectExit);
86
+ child.once("exit", (exitCode, exitSignal) => resolveExit({ code: exitCode, signal: exitSignal }));
87
+ });
88
+ if (code !== 0)
89
+ process.exitCode = code ?? (signal === "SIGINT" ? 130 : 1);
90
+ }
91
+ finally {
92
+ process.off("SIGINT", onInterrupt);
93
+ process.off("SIGTERM", onTerminate);
94
+ }
95
+ };
96
+ export const runUi = async (args) => {
97
+ assertKnownOptions(args, UI_FLAGS);
98
+ const reviewerTokenFile = value(args, "--reviewer-token-file");
99
+ const port = Number(value(args, "--port") ?? 4173);
100
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
101
+ throw new Error("--port must be an integer between 1 and 65535");
102
+ await serveDashboard({
103
+ root: resolve(rootArg(args)), host: value(args, "--host") ?? "127.0.0.1", port, open: flags(args).has("--open"),
104
+ ...(reviewerTokenFile ? { reviewerTokenFile } : {}),
105
+ });
106
+ };
107
+ const mcpSetupOptions = async (args) => {
108
+ const selected = await mcpSetupSelections(args);
109
+ 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") };
110
+ };
111
+ export const runMcpSetup = async (args) => {
112
+ assertKnownOptions(args.slice(1), MCP_FLAGS);
113
+ const options = await mcpSetupOptions(args.slice(1));
114
+ const plan = planMcpSetup(options);
115
+ console.log(`◆ MCP SETUP preview — ${options.dryRun ? "no files will be written" : "review before applying"}`);
116
+ console.log(JSON.stringify(plan, null, 2));
117
+ if (options.dryRun)
118
+ return;
119
+ if (!await confirmGeneral({ dryRun: options.dryRun, yes: flags(args.slice(1)).has("--yes") }, "Apply MCP setup"))
120
+ return;
121
+ applyMcpSetup(options);
122
+ 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.`);
123
+ };
124
+ export const runMcp = async (args) => { if (args[0] !== "setup")
125
+ throw new Error("Unknown command: mcp"); return runMcpSetup(args); };
126
+ export const runStatus = (args) => {
127
+ assertKnownOptions(args, STATUS_FLAGS);
128
+ const root = rootArg(args);
129
+ if (flags(args).has("--json"))
130
+ console.log(JSON.stringify(statusData(root)));
131
+ else
132
+ console.log(status(root));
133
+ };
134
+ export const runDoctor = (args) => {
135
+ assertKnownOptions(args, DOCTOR_FLAGS);
136
+ const root = rootArg(args);
137
+ const strict = flags(args).has("--strict");
138
+ if (flags(args).has("--json")) {
139
+ const data = doctorData(root, strict);
140
+ console.log(JSON.stringify(data));
141
+ if (data.level === "error")
142
+ process.exitCode = 1;
143
+ }
144
+ else {
145
+ const result = doctor(root, strict);
146
+ console.log(result);
147
+ if (result.startsWith("ERROR:"))
148
+ process.exitCode = 1;
149
+ }
150
+ };
151
+ export const runProfileValidate = (args) => {
152
+ assertKnownOptions(args.slice(1), new Set());
153
+ const root = rootArg(args.slice(1));
154
+ const config = loadProjectConfig(root);
155
+ const profiles = resolveProfiles(root, config.extends);
156
+ console.log(`OK: ${profiles.map((item) => item.id).join(" → ")}`);
157
+ };
158
+ export const runProfile = (args) => { if (args[0] !== "validate")
159
+ throw new Error("Unknown command: profile"); return runProfileValidate(args); };
@@ -0,0 +1,47 @@
1
+ export interface LauncherArgs {
2
+ root: string;
3
+ values: Map<string, string>;
4
+ switches: Set<string>;
5
+ }
6
+ /** Parses `--flag value` / `--switch` options against the per-action allowlist. */
7
+ export declare const parseLauncherArgs: (args: string[], allowed: string[]) => LauncherArgs;
8
+ export declare const installerCliPath: (root: string) => string;
9
+ export declare const recordInstallerCli: (root: string) => void;
10
+ export declare const uiStatePath: (root: string) => string;
11
+ interface UiRecord {
12
+ pid: number;
13
+ url: string;
14
+ startedAt: string;
15
+ instance?: string;
16
+ }
17
+ /** The recorded dashboard, if any. `running` only means the pid exists; use
18
+ * `dashboardReachable` before trusting or signalling it (the pid may have been reused). */
19
+ export declare const readUiRecord: (root: string) => (UiRecord & {
20
+ running: boolean;
21
+ }) | undefined;
22
+ /** True when the recorded dashboard itself still answers: the URL serves the same random
23
+ * instance id the record holds (another project's dashboard on that port does not match). */
24
+ export declare const dashboardReachable: (record: {
25
+ url: string;
26
+ instance?: string;
27
+ }) => Promise<boolean>;
28
+ export interface ServeOptions {
29
+ root: string;
30
+ host: string;
31
+ port: number;
32
+ open: boolean;
33
+ reviewerTokenFile?: string;
34
+ record?: boolean;
35
+ }
36
+ export declare const isLoopbackHost: (host: string) => boolean;
37
+ /** Printed when the dashboard listens on a non-loopback address: it has no authentication. */
38
+ export declare const EXPOSED_WARNING = "Warning: the dashboard has no authentication; expose it only on a trusted network.";
39
+ /** Serves the dashboard until SIGINT/SIGTERM. `record` writes ui.json so `loop down`/`up` can find it. */
40
+ export declare const serveDashboard: (options: ServeOptions) => Promise<void>;
41
+ export declare const loopUp: (args: string[]) => Promise<void>;
42
+ export declare const loopStop: (args: string[]) => Promise<void>;
43
+ export declare const loopDown: (args: string[]) => Promise<void>;
44
+ export declare const loopEnv: (args: string[]) => void;
45
+ /** `loop status` without a task id: daemon, dashboard, and environment at a glance. */
46
+ export declare const loopOverview: (args: string[]) => Promise<void>;
47
+ export {};
@@ -0,0 +1,322 @@
1
+ /**
2
+ * One-command local loop: `loop up | down | stop | env | status` (no task id).
3
+ *
4
+ * `up` checks the configuration and environment (`loop-entry.mjs env`), starts the
5
+ * daemon detached, then serves the dashboard in the foreground. Ctrl-C closes only
6
+ * the dashboard; the daemon keeps running until `loop down` / `loop stop`. The
7
+ * shipped `.agents/aidlc/scripts/loop.sh|loop.cmd` wrappers forward to these commands.
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ import { randomUUID } from "node:crypto";
11
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
12
+ import { join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { packageVersion } from "../workflow.js";
15
+ import { daemonHealth, daemonStatus, ensureDaemon, pidAlive, stopDaemon } from "../ui/daemon.js";
16
+ import { daemonLogPath, loopDataDir, loopEntryPath, openBrowser, readCredentialFile, scrubbedEnv } from "../ui/options.js";
17
+ import { startUiServer } from "../ui/server.js";
18
+ const SWITCHES = new Set(["--open", "--no-ui", "--now", "--json"]);
19
+ const VALUES = new Set(["--root", "--loop-env", "--port", "--host", "--reviewer-token-file"]);
20
+ /** Parses `--flag value` / `--switch` options against the per-action allowlist. */
21
+ export const parseLauncherArgs = (args, allowed) => {
22
+ const values = new Map();
23
+ const switches = new Set();
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const key = args[index] ?? "";
26
+ if (!allowed.includes(key))
27
+ throw new Error(`Unknown loop option: ${key}`);
28
+ if (SWITCHES.has(key)) {
29
+ switches.add(key);
30
+ continue;
31
+ }
32
+ const entry = args[index + 1];
33
+ if (!VALUES.has(key) || entry === undefined || entry.startsWith("--") || values.has(key))
34
+ throw new Error(`${key} requires a value`);
35
+ values.set(key, entry);
36
+ index += 1;
37
+ }
38
+ return { root: resolve(values.get("--root") ?? "."), values, switches };
39
+ };
40
+ /** `AIDLC_ENV_FILE` for children when `--loop-env` is given (resolved against the caller's cwd). */
41
+ const envOverlay = (parsed) => {
42
+ const file = parsed.values.get("--loop-env");
43
+ return file ? { AIDLC_ENV_FILE: resolve(file) } : {};
44
+ };
45
+ const readConfig = (root) => {
46
+ try {
47
+ return JSON.parse(readFileSync(join(root, ".agents", "config.json"), "utf8"));
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ };
53
+ /** Fails with actionable guidance unless the project is installed with local loop mode. */
54
+ const requireLocalLoop = (root) => {
55
+ if (!existsSync(loopEntryPath(root)))
56
+ throw new Error(`AI-DLC is not installed in ${root}; run: npx @felixdotgo/aidlc-kit init --loop-mode local`);
57
+ const mode = readConfig(root)?.loop?.mode ?? "off";
58
+ if (mode === "fleet")
59
+ throw new Error("loop up/down manage the local daemon; this project uses fleet mode (see docs/loop-runbook.md#fleet-multi-machine)");
60
+ if (mode !== "local")
61
+ throw new Error([
62
+ "Loop mode is off. Enable it in .agents/config.json:",
63
+ ' "loop": { "mode": "local", "agents": { "lead": "opencode", "workers": { "worker-a": "opencode" } } }',
64
+ "(providers: codex, claude, or opencode), or run: npx @felixdotgo/aidlc-kit init --loop-mode local",
65
+ ].join("\n"));
66
+ };
67
+ /** Runs `loop-entry.mjs env` for `root`; returns its exit status (output inherited or captured). */
68
+ const envReport = (root, overlay, options = {}) => {
69
+ const result = spawnSync(process.execPath, [loopEntryPath(root), "env", root, ...(options.json ? ["--json"] : [])], {
70
+ cwd: root, encoding: "utf8", env: scrubbedEnv(overlay), stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
71
+ });
72
+ if (result.error)
73
+ throw result.error;
74
+ return { status: result.status ?? 1, stdout: result.stdout ?? "" };
75
+ };
76
+ /* ---- Installing CLI record (`.agents/data/loop/cli.json`) ----
77
+ * `init`/`upgrade` record the CLI that wrote `.agents/`, so `loop.sh`/`loop.cmd` run that
78
+ * exact build even when it came from the npx cache or a local `.tgz` (npx caches those by
79
+ * spec, so `npx --no-install @felixdotgo/aidlc-kit` cannot find them). Machine-local data,
80
+ * next to daemon.lock and daemon.log; never part of the managed manifest. */
81
+ export const installerCliPath = (root) => join(loopDataDir(root), "cli.json");
82
+ export const recordInstallerCli = (root) => {
83
+ try {
84
+ mkdirSync(loopDataDir(root), { recursive: true });
85
+ const cli = fileURLToPath(new URL("../cli.js", import.meta.url));
86
+ writeFileSync(installerCliPath(root), `${JSON.stringify({ cli, version: packageVersion(), recordedAt: new Date().toISOString() }, null, 2)}\n`);
87
+ }
88
+ catch (error) {
89
+ console.warn(`warning: could not record the aidlc-kit CLI for the loop launcher (${error instanceof Error ? error.message : String(error)}); install the package in the project to use loop.sh`);
90
+ }
91
+ };
92
+ /* ---- Dashboard process record (`.agents/data/loop/ui.json`) ---- */
93
+ export const uiStatePath = (root) => join(loopDataDir(root), "ui.json");
94
+ /** The recorded dashboard, if any. `running` only means the pid exists; use
95
+ * `dashboardReachable` before trusting or signalling it (the pid may have been reused). */
96
+ export const readUiRecord = (root) => {
97
+ try {
98
+ const record = JSON.parse(readFileSync(uiStatePath(root), "utf8"));
99
+ if (typeof record.pid !== "number" || typeof record.url !== "string")
100
+ return undefined;
101
+ return { pid: record.pid, url: record.url, startedAt: String(record.startedAt ?? ""), ...(typeof record.instance === "string" ? { instance: record.instance } : {}), running: pidAlive(record.pid) };
102
+ }
103
+ catch {
104
+ return undefined;
105
+ }
106
+ };
107
+ /** True when the recorded dashboard itself still answers: the URL serves the same random
108
+ * instance id the record holds (another project's dashboard on that port does not match). */
109
+ export const dashboardReachable = async (record) => {
110
+ if (!record.instance)
111
+ return false;
112
+ try {
113
+ const response = await fetch(`${record.url}/api/instance`, { signal: AbortSignal.timeout(3000) });
114
+ const body = await response.json();
115
+ return response.ok && body.instance === record.instance;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ };
121
+ /** A URL this machine can reach for a wildcard bind (0.0.0.0 / ::), else the bound host. */
122
+ const recordHost = (host) => host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host;
123
+ const validPort = (raw) => {
124
+ const port = Number(raw ?? 4173);
125
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
126
+ throw new Error("--port must be an integer between 1 and 65535");
127
+ return port;
128
+ };
129
+ /** Loopback binds are reachable only from this machine; anything else exposes the dashboard. */
130
+ const LOOPBACK_BINDS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
131
+ export const isLoopbackHost = (host) => LOOPBACK_BINDS.has(host.toLowerCase());
132
+ /** Printed when the dashboard listens on a non-loopback address: it has no authentication. */
133
+ export const EXPOSED_WARNING = "Warning: the dashboard has no authentication; expose it only on a trusted network.";
134
+ /** Serves the dashboard until SIGINT/SIGTERM. `record` writes ui.json so `loop down`/`up` can find it. */
135
+ export const serveDashboard = async (options) => {
136
+ const { root, host } = options;
137
+ const port = validPort(String(options.port));
138
+ const reviewerToken = options.reviewerTokenFile ? readCredentialFile(root, options.reviewerTokenFile, { create: false }) : undefined;
139
+ const instance = randomUUID();
140
+ const { server, port: boundPort } = await startUiServer({ root, host, port, instance, ...(reviewerToken ? { reviewerToken } : {}) });
141
+ const base = `http://${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${boundPort}`;
142
+ console.log(`Loop (AI-DLC UI) listening on ${base} (Ctrl-C to stop)`);
143
+ console.log(`Open: ${base}`);
144
+ if (!isLoopbackHost(host))
145
+ console.log(EXPOSED_WARNING);
146
+ if (options.record) {
147
+ mkdirSync(loopDataDir(root), { recursive: true });
148
+ const local = recordHost(host);
149
+ const url = `http://${local.includes(":") ? `[${local}]` : local}:${boundPort}`;
150
+ writeFileSync(uiStatePath(root), `${JSON.stringify({ pid: process.pid, url, instance, startedAt: new Date().toISOString() })}\n`);
151
+ }
152
+ if (options.open)
153
+ openBrowser(base);
154
+ await new Promise((resolveClose) => {
155
+ const close = () => { server.close(() => resolveClose()); server.closeAllConnections?.(); };
156
+ process.once("SIGINT", close);
157
+ process.once("SIGTERM", close);
158
+ });
159
+ if (options.record && readUiRecord(root)?.pid === process.pid)
160
+ rmSync(uiStatePath(root), { force: true });
161
+ };
162
+ const sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
163
+ const logTail = (root, lines = 15) => {
164
+ try {
165
+ return readFileSync(daemonLogPath(root), "utf8").trimEnd().split("\n").slice(-lines).join("\n");
166
+ }
167
+ catch {
168
+ return "";
169
+ }
170
+ };
171
+ /** Env file of this start: `--loop-env`, `AIDLC_ENV_FILE`, or the default. */
172
+ const envFilePath = (root, overlay) => resolve(root, overlay.AIDLC_ENV_FILE ?? process.env.AIDLC_ENV_FILE ?? join(".agents", "loop.env"));
173
+ /** Starts the daemon unless it runs; confirms it survived start-up. Returns false on failure. */
174
+ const startDaemon = async (root, overlay) => {
175
+ const current = daemonStatus(root);
176
+ if (current.running) {
177
+ console.log(`Loop daemon already running (pid ${current.pid}${current.startedAt ? `, started ${current.startedAt}` : ""})`);
178
+ try {
179
+ if (current.startedAt && statSync(envFilePath(root, overlay)).mtimeMs > Date.parse(current.startedAt)) {
180
+ console.log(" note: the env file changed after the daemon started; restart it to apply: loop down, then loop up");
181
+ }
182
+ }
183
+ catch { /* no env file */ }
184
+ return true;
185
+ }
186
+ const started = ensureDaemon(root, overlay);
187
+ // The daemon writes its lock right after the environment check; an early exit means start-up failed.
188
+ for (let waited = 0; waited < 5000; waited += 250) {
189
+ await sleep(250);
190
+ if (daemonStatus(root).running) {
191
+ console.log(`Started the loop daemon (pid ${daemonStatus(root).pid}); log: ${daemonLogPath(root)}`);
192
+ return true;
193
+ }
194
+ if (started.pid !== null && !pidAlive(started.pid))
195
+ break;
196
+ }
197
+ if (daemonStatus(root).running)
198
+ return true;
199
+ if (started.pid !== null && pidAlive(started.pid)) {
200
+ console.log(`Loop daemon is starting (pid ${started.pid}); log: ${daemonLogPath(root)}`);
201
+ return true;
202
+ }
203
+ console.error(`The loop daemon exited during start-up. Last lines of ${daemonLogPath(root)}:`);
204
+ console.error(logTail(root) || " (empty)");
205
+ return false;
206
+ };
207
+ export const loopUp = async (args) => {
208
+ const parsed = parseLauncherArgs(args, ["--root", "--loop-env", "--port", "--host", "--open", "--no-ui", "--reviewer-token-file"]);
209
+ const { root } = parsed;
210
+ requireLocalLoop(root);
211
+ const port = validPort(parsed.values.get("--port"));
212
+ const overlay = envOverlay(parsed);
213
+ if (envReport(root, overlay).status !== 0) {
214
+ console.error("\nFix the errors above, then run loop up again.");
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+ console.log("");
219
+ if (!await startDaemon(root, overlay)) {
220
+ process.exitCode = 1;
221
+ return;
222
+ }
223
+ if (parsed.switches.has("--no-ui")) {
224
+ console.log("Dashboard not started (--no-ui). Start it with: aidlc-kit ui");
225
+ return;
226
+ }
227
+ const ui = readUiRecord(root);
228
+ if (ui?.running && ui.pid !== process.pid && await dashboardReachable(ui)) {
229
+ console.log(`Dashboard already running at ${ui.url} (pid ${ui.pid}); open it there, or stop it with loop down.`);
230
+ return;
231
+ }
232
+ // A daemon the dashboard starts later (New task after loop stop) loads the same env file.
233
+ if (overlay.AIDLC_ENV_FILE)
234
+ process.env.AIDLC_ENV_FILE = overlay.AIDLC_ENV_FILE;
235
+ await serveDashboard({
236
+ root, host: parsed.values.get("--host") ?? "127.0.0.1", port, open: parsed.switches.has("--open"), record: true,
237
+ ...(parsed.values.has("--reviewer-token-file") ? { reviewerTokenFile: parsed.values.get("--reviewer-token-file") } : {}),
238
+ });
239
+ console.log("Dashboard stopped. The loop daemon keeps running; stop it with: loop down");
240
+ };
241
+ const reportStop = (result, now) => {
242
+ if (!result.wasRunning) {
243
+ console.log("Loop daemon is not running");
244
+ return;
245
+ }
246
+ if (result.stopped) {
247
+ console.log(`Stopped the loop daemon (pid ${result.pid})`);
248
+ return;
249
+ }
250
+ console.log(now
251
+ ? `Loop daemon (pid ${result.pid}) is still shutting down; check again with: loop status`
252
+ : `Loop daemon (pid ${result.pid}) will stop after its current agent turn. To cancel that turn now: loop stop --now`);
253
+ };
254
+ export const loopStop = async (args) => {
255
+ const parsed = parseLauncherArgs(args, ["--root", "--now"]);
256
+ const now = parsed.switches.has("--now");
257
+ reportStop(await stopDaemon(parsed.root, { now }), now);
258
+ };
259
+ export const loopDown = async (args) => {
260
+ const parsed = parseLauncherArgs(args, ["--root", "--now"]);
261
+ const { root } = parsed;
262
+ const ui = readUiRecord(root);
263
+ // Signal only a dashboard that still answers at its recorded URL: after a crash or reboot
264
+ // the recorded pid may belong to an unrelated process.
265
+ if (ui?.running && ui.pid !== process.pid && await dashboardReachable(ui)) {
266
+ try {
267
+ process.kill(ui.pid, "SIGTERM");
268
+ }
269
+ catch { /* already gone */ }
270
+ for (let waited = 0; waited < 5000 && pidAlive(ui.pid); waited += 200)
271
+ await sleep(200);
272
+ console.log(pidAlive(ui.pid) ? `Dashboard (pid ${ui.pid}) did not stop yet` : `Stopped the dashboard (pid ${ui.pid})`);
273
+ if (!pidAlive(ui.pid))
274
+ rmSync(uiStatePath(root), { force: true });
275
+ }
276
+ else if (ui?.running && ui.pid !== process.pid) {
277
+ // Not provably ours (busy, or the pid was reused): never signal it; keep the record.
278
+ console.log(`Dashboard record (pid ${ui.pid}, ${ui.url}) did not answer as this project's dashboard; not stopping it. Close it with Ctrl-C, or delete ${uiStatePath(root)} if it is gone.`);
279
+ }
280
+ else if (ui)
281
+ rmSync(uiStatePath(root), { force: true });
282
+ const now = parsed.switches.has("--now");
283
+ reportStop(await stopDaemon(root, { now }), now);
284
+ };
285
+ export const loopEnv = (args) => {
286
+ const parsed = parseLauncherArgs(args, ["--root", "--loop-env", "--json"]);
287
+ if (!existsSync(loopEntryPath(parsed.root)))
288
+ throw new Error(`AI-DLC is not installed in ${parsed.root}`);
289
+ const { status } = envReport(parsed.root, envOverlay(parsed), { json: parsed.switches.has("--json") });
290
+ if (status !== 0)
291
+ process.exitCode = status;
292
+ };
293
+ /** `loop status` without a task id: daemon, dashboard, and environment at a glance. */
294
+ export const loopOverview = async (args) => {
295
+ const parsed = parseLauncherArgs(args, ["--root", "--loop-env", "--json"]);
296
+ const { root } = parsed;
297
+ if (!existsSync(loopEntryPath(root)))
298
+ throw new Error(`AI-DLC is not installed in ${root}`);
299
+ const daemon = daemonStatus(root);
300
+ const errors = daemon.running ? daemonHealth(root) : [];
301
+ const record = readUiRecord(root);
302
+ const ui = record ? { ...record, running: record.running && await dashboardReachable(record) } : undefined;
303
+ if (parsed.switches.has("--json")) {
304
+ const env = envReport(root, envOverlay(parsed), { json: true, capture: true });
305
+ let parsedEnv = null;
306
+ try {
307
+ parsedEnv = JSON.parse(env.stdout);
308
+ }
309
+ catch {
310
+ parsedEnv = null;
311
+ }
312
+ console.log(JSON.stringify({ daemon: { ...daemon, errors }, ui: ui ?? null, env: parsedEnv }, null, 2));
313
+ return;
314
+ }
315
+ console.log(daemon.running ? `Daemon : running · pid ${daemon.pid}${daemon.startedAt ? ` · started ${daemon.startedAt}` : ""}` : "Daemon : stopped (start it with: loop up)");
316
+ for (const error of errors)
317
+ console.log(` error : ${error.taskId ? `${error.taskId}: ` : "poll: "}${error.message}`);
318
+ console.log(ui?.running ? `Dashboard : ${ui.url} · pid ${ui.pid}` : "Dashboard : not running (loop up starts it)");
319
+ console.log(`Log : ${daemonLogPath(root)}`);
320
+ console.log("");
321
+ envReport(root, envOverlay(parsed));
322
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure argv→options builders: init options, loop flag reads, worker mapping
3
+ * parsing, and the argv-provided part of MCP setup selections. Prompting for
4
+ * anything still missing happens in ./prompt.ts.
5
+ */
6
+ import type { InitOptions } from "../model.js";
7
+ import type { McpSetupOptions } from "../mcp-setup.js";
8
+ export declare const initOptions: (args: string[]) => InitOptions;
9
+ export declare const loopSelection: (args: string[]) => {
10
+ mode?: string;
11
+ lead?: string;
12
+ workers?: string;
13
+ };
14
+ export declare const parseWorkerMapping: (rawWorkers: string) => Record<string, string>;
15
+ /** Argv-provided MCP selections: connection choices stay optional until prompted; the rest always carry defaults. */
16
+ export type McpArgSelections = {
17
+ deployment?: McpSetupOptions["deployment"];
18
+ storage?: McpSetupOptions["storage"];
19
+ bind?: McpSetupOptions["bind"];
20
+ workspace?: string;
21
+ pollMs: number;
22
+ tokenEnv: string;
23
+ providers: string[];
24
+ };
25
+ export declare const mcpArgSelections: (args: string[]) => McpArgSelections;
@@ -0,0 +1,32 @@
1
+ import { flags, rootArg, value } from "./argv.js";
2
+ export const initOptions = (args) => ({
3
+ root: rootArg(args),
4
+ agents: (value(args, "--agent") ?? "").split(",").filter(Boolean),
5
+ all: flags(args).has("--all"),
6
+ dryRun: flags(args).has("--dry-run"),
7
+ yes: flags(args).has("--yes"),
8
+ force: flags(args).has("--force")
9
+ });
10
+ export const loopSelection = (args) => ({ mode: value(args, "--loop-mode"), lead: value(args, "--loop-lead"), workers: value(args, "--loop-workers") });
11
+ export const parseWorkerMapping = (rawWorkers) => {
12
+ const workers = Object.create(null);
13
+ for (const entry of rawWorkers.split(",")) {
14
+ const parts = entry.split("=");
15
+ if (parts.length !== 2 || !parts[0] || !parts[1] || Object.hasOwn(workers, parts[0]))
16
+ throw new Error("Invalid or duplicate loop worker mapping");
17
+ workers[parts[0]] = parts[1];
18
+ }
19
+ return workers;
20
+ };
21
+ export const mcpArgSelections = (args) => {
22
+ const selected = {
23
+ deployment: value(args, "--deployment"),
24
+ storage: value(args, "--storage"),
25
+ bind: value(args, "--bind"),
26
+ workspace: value(args, "--workspace"),
27
+ pollMs: Number(value(args, "--poll-ms") ?? 30_000),
28
+ tokenEnv: value(args, "--token-env") ?? "AIDLC_MCP_TOKEN",
29
+ providers: (value(args, "--providers") ?? "").split(",").filter(Boolean)
30
+ };
31
+ return selected;
32
+ };
@@ -0,0 +1,9 @@
1
+ import type { LoopConfig } from "../model.js";
2
+ import type { McpSetupOptions } from "../mcp-setup.js";
3
+ export declare const interactive: () => boolean;
4
+ export declare const chooseLoop: (args: string[], upgrade: boolean) => Promise<LoopConfig | undefined>;
5
+ export declare const mcpSetupSelections: (args: string[]) => Promise<Partial<Omit<McpSetupOptions, "root" | "template" | "dryRun" | "enable">>>;
6
+ export declare const confirmGeneral: (options: {
7
+ dryRun: boolean;
8
+ yes: boolean;
9
+ }, operation: string) => Promise<boolean>;