@felixdotgo/aidlc-kit 0.2.0 → 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 (109) hide show
  1. package/DEVELOPMENT.md +13 -26
  2. package/README.md +35 -39
  3. package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
  4. package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
  5. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
  6. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
  7. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +21 -5
  8. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +52 -78
  9. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +88 -14
  10. package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +20 -5
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +10 -1
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +2 -1
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +3 -2
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +9 -3
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +11 -3
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +78 -17
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +42 -10
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +66 -9
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +12 -6
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +14 -2
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
  28. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +11 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
  31. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +87 -4
  32. package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
  33. package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
  34. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
  35. package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
  36. package/dist/assets/ui/changes.js +83 -0
  37. package/dist/assets/ui/common.js +664 -0
  38. package/dist/assets/ui/dashboard.css +281 -59
  39. package/dist/assets/ui/dashboard.js +77 -0
  40. package/dist/assets/ui/detail.js +159 -0
  41. package/dist/assets/ui/live-log.js +332 -0
  42. package/dist/services/aidlc-coordination/package.json +1 -1
  43. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +11 -0
  44. package/dist/src/cli/argv.js +10 -4
  45. package/dist/src/cli/commands.js +52 -22
  46. package/dist/src/cli/loop-launcher.d.ts +47 -0
  47. package/dist/src/cli/loop-launcher.js +322 -0
  48. package/dist/src/cli.js +3 -0
  49. package/dist/src/installer.js +4 -2
  50. package/dist/src/model/records.d.ts +4 -0
  51. package/dist/src/state/validate.js +5 -0
  52. package/dist/src/task-ui.d.ts +31 -52
  53. package/dist/src/task-ui.js +25 -1562
  54. package/dist/src/ui/assets.d.ts +14 -0
  55. package/dist/src/ui/assets.js +45 -0
  56. package/dist/src/ui/changes.d.ts +83 -0
  57. package/dist/src/ui/changes.js +498 -0
  58. package/dist/src/ui/containers.d.ts +71 -0
  59. package/dist/src/ui/containers.js +214 -0
  60. package/dist/src/ui/daemon.d.ts +75 -0
  61. package/dist/src/ui/daemon.js +232 -0
  62. package/dist/src/ui/data.d.ts +42 -0
  63. package/dist/src/ui/data.js +89 -0
  64. package/dist/src/ui/docs.d.ts +13 -0
  65. package/dist/src/ui/docs.js +39 -0
  66. package/dist/src/ui/files.d.ts +3 -0
  67. package/dist/src/ui/files.js +20 -0
  68. package/dist/src/ui/http.d.ts +14 -0
  69. package/dist/src/ui/http.js +80 -0
  70. package/dist/src/ui/live.d.ts +5 -0
  71. package/dist/src/ui/live.js +43 -0
  72. package/dist/src/ui/markdown.d.ts +2 -0
  73. package/dist/src/ui/markdown.js +127 -0
  74. package/dist/src/ui/options.d.ts +48 -0
  75. package/dist/src/ui/options.js +81 -0
  76. package/dist/src/ui/security.d.ts +34 -0
  77. package/dist/src/ui/security.js +38 -0
  78. package/dist/src/ui/server.d.ts +39 -0
  79. package/dist/src/ui/server.js +347 -0
  80. package/dist/src/ui/sse.d.ts +14 -0
  81. package/dist/src/ui/sse.js +281 -0
  82. package/dist/src/ui/views/board.d.ts +7 -0
  83. package/dist/src/ui/views/board.js +131 -0
  84. package/dist/src/ui/views/changes.d.ts +19 -0
  85. package/dist/src/ui/views/changes.js +154 -0
  86. package/dist/src/ui/views/components.d.ts +56 -0
  87. package/dist/src/ui/views/components.js +113 -0
  88. package/dist/src/ui/views/docs.d.ts +15 -0
  89. package/dist/src/ui/views/docs.js +64 -0
  90. package/dist/src/ui/views/layout.d.ts +44 -0
  91. package/dist/src/ui/views/layout.js +233 -0
  92. package/dist/src/ui/views/task.d.ts +25 -0
  93. package/dist/src/ui/views/task.js +320 -0
  94. package/dist/src/upgrade.js +5 -2
  95. package/dist/src/workflow.d.ts +4 -0
  96. package/dist/src/workflow.js +33 -1
  97. package/docs/README.md +48 -33
  98. package/docs/architecture.md +101 -38
  99. package/docs/command-reference.md +269 -93
  100. package/docs/configuration.md +62 -42
  101. package/docs/coordination-service.md +85 -36
  102. package/docs/design-decisions.md +35 -31
  103. package/docs/development.md +111 -61
  104. package/docs/loop-local-run.md +135 -402
  105. package/docs/loop-runbook.md +84 -105
  106. package/docs/operating-workflow.md +55 -42
  107. package/docs/testing-and-release.md +70 -51
  108. package/docs/troubleshooting.md +165 -69
  109. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Per-task operational data lives under the task's own directory:
3
+ *
4
+ * .agents/data/tasks/<taskId>/runtime/
5
+ * assignments.json local worker claims (`assignment-store.mjs`)
6
+ * assignments.lock short mutex around one assignments read-modify-write
7
+ * events.jsonl (+ .1) the task's event history (`loop-stream.mjs`)
8
+ * run.json the loop run record (`loop-runtime.mjs`)
9
+ * intake.json the loop intake record (`loop-intake.mjs`)
10
+ * requirement.md the intake requirement text
11
+ * workers/ container index and raw output (`loop-worker-log.mjs`)
12
+ *
13
+ * The task directory root keeps review artifacts only; `.agents/data/loop/`
14
+ * keeps daemon/process state (daemon lock and log, the global event sidecar,
15
+ * task leases). The same layout serves the no-loop workflow, the local loop,
16
+ * and fleet, so every reader finds task data without knowing which mode wrote
17
+ * it. `runtime/` is operational: never committed, handed off, or clean-checked
18
+ * (`isLoopOperationalPath` in loop-git.mjs).
19
+ *
20
+ * Older versions kept these files under `.agents/data/loop/` (assignments,
21
+ * events, `<task>.json` run record) and at the task directory root
22
+ * (`loop-intake.json`, `requirement.md`). `legacyTaskRuntimePath` names those
23
+ * locations so readers fall back to them and `task-runtime-migrate.mjs` moves
24
+ * them once.
25
+ */
26
+ import { lstatSync, mkdirSync } from "node:fs";
27
+ import { join, resolve } from "node:path";
28
+
29
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
30
+
31
+ /** Runtime file names and where an older aidlc-kit kept each one (relative to `.agents/data`). */
32
+ export const TASK_RUNTIME_FILES = Object.freeze({
33
+ "assignments.json": (taskId) => ["loop", "assignments", `${taskId}.json`],
34
+ "assignments.lock": (taskId) => ["loop", "assignments", `${taskId}.lock`],
35
+ "events.jsonl": (taskId) => ["loop", "events", `${taskId}.jsonl`],
36
+ "events.jsonl.1": (taskId) => ["loop", "events", `${taskId}.jsonl.1`],
37
+ "run.json": (taskId) => ["loop", `${taskId}.json`],
38
+ "intake.json": (taskId) => ["tasks", taskId, "loop-intake.json"],
39
+ "requirement.md": (taskId) => ["tasks", taskId, "requirement.md"],
40
+ });
41
+
42
+ /** Runtime files that `task-runtime-migrate.mjs` moves (locks are short-lived and expire on their own). */
43
+ export const MIGRATED_TASK_RUNTIME_FILES = Object.freeze(["assignments.json", "events.jsonl", "events.jsonl.1", "run.json", "intake.json", "requirement.md"]);
44
+
45
+ export const isTaskId = (value) => typeof value === "string" && ID_PATTERN.test(value);
46
+ const safeTaskId = (taskId) => { if (!isTaskId(taskId)) throw new Error(`Invalid task id: ${taskId}`); return taskId; };
47
+ const safeName = (name) => { if (!Object.hasOwn(TASK_RUNTIME_FILES, name)) throw new Error(`Unknown task runtime file: ${name}`); return name; };
48
+
49
+ /** Throw when `path` exists and is a symlink (a planted link must not redirect reads or writes). */
50
+ const refuseSymlink = (path) => {
51
+ try { if (lstatSync(path).isSymbolicLink()) throw new Error(`Task runtime path crosses symlink: ${path}`); }
52
+ catch (error) { if (error?.code !== "ENOENT") throw error; }
53
+ };
54
+
55
+ /** Join `parts` under `root`, refusing any symlinked component. */
56
+ const safeJoin = (root, parts) => {
57
+ let path = resolve(root);
58
+ for (const part of parts) { path = join(path, part); refuseSymlink(path); }
59
+ return path;
60
+ };
61
+
62
+ /** `.agents/data/tasks/<taskId>/runtime`, every component checked for symlinks. */
63
+ export function taskRuntimeDir(root, taskId) {
64
+ return safeJoin(root, [".agents", "data", "tasks", safeTaskId(taskId), "runtime"]);
65
+ }
66
+
67
+ /** The current location of one runtime file (`name` from TASK_RUNTIME_FILES). */
68
+ export function taskRuntimePath(root, taskId, name) {
69
+ return safeJoin(root, [".agents", "data", "tasks", safeTaskId(taskId), "runtime", safeName(name)]);
70
+ }
71
+
72
+ /** Where an older aidlc-kit kept the same runtime file. */
73
+ export function legacyTaskRuntimePath(root, taskId, name) {
74
+ return safeJoin(root, [".agents", "data", ...TASK_RUNTIME_FILES[safeName(name)](safeTaskId(taskId))]);
75
+ }
76
+
77
+ /** True when `path` is a regular (non-symlink) file. */
78
+ export const isRegularFile = (path) => {
79
+ try { const stat = lstatSync(path); return stat.isFile() && !stat.isSymbolicLink(); } catch (error) { if (error?.code === "ENOENT") return false; throw error; }
80
+ };
81
+
82
+ /** Read location of a runtime file: the current path when it exists, else the
83
+ * legacy path when that exists, else the current path (for a clean ENOENT). */
84
+ export function resolveTaskRuntimeRead(root, taskId, name) {
85
+ const current = taskRuntimePath(root, taskId, name);
86
+ if (isRegularFile(current)) return current;
87
+ const legacy = legacyTaskRuntimePath(root, taskId, name);
88
+ return isRegularFile(legacy) ? legacy : current;
89
+ }
90
+
91
+ /** Create the runtime directory (0700, like the task directory an intake creates) and return it. */
92
+ export function ensureTaskRuntimeDir(root, taskId) {
93
+ const directory = taskRuntimeDir(root, taskId);
94
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
95
+ refuseSymlink(directory);
96
+ return directory;
97
+ }
@@ -6,9 +6,22 @@
6
6
  * intake create a loop-owned task from a stdin requirement (≤ 8192 bytes) without running it
7
7
  * status print the canonical task, loop record/checkpoint, and next action
8
8
  * worker-image build and record a provider worker image
9
+ * env report the daemon environment: providers, env file, each variable's
10
+ * source (shell/file/derived) and missing values; secrets are never
11
+ * printed. `env [root] [--json]`; exits 1 when the daemon could not start.
12
+ * leases list loop leases, locks and claims: who holds what, until when,
13
+ * what it blocks, and how to clear it. Works with any loop mode.
14
+ * `leases [task-id] [root] [--json] [--clear-stale]`; --clear-stale
15
+ * removes only stale/old-corrupt lease, lock and guard files.
16
+ * migrate-data move per-task runtime files of older versions (loop assignments,
17
+ * events, run records, intake) under .agents/data/tasks/<id>/runtime/.
18
+ * `migrate-data [root] [--dry-run] [--json]`; idempotent, never
19
+ * overwrites or deletes (the daemon also runs it at start).
9
20
  * Dashboard JSON actions (fleet authority; local dashboards read local state in-process):
10
21
  * tasks | task <id> | gate <id> | resume-task <id> | approve <id> (source on stdin,
11
- * reviewer credential only from AIDLC_FLEET_REVIEWER_TOKEN of this process)
22
+ * reviewer credential only from AIDLC_FLEET_REVIEWER_TOKEN of this process) |
23
+ * feedback <id> (JSON `{ reason, source }` on stdin; keeps the task at its gate
24
+ * and never needs the reviewer credential)
12
25
  *
13
26
  * Positional arguments: [action, taskId, root, mode, provider, tag, title].
14
27
  * Gate approval for local tasks stays on the trusted terminal (`loop-gate.mjs`).
@@ -24,11 +37,15 @@ import { runLoopDaemon } from "./lib/loop-daemon.mjs";
24
37
  import { streamLog } from "./lib/loop-stream.mjs";
25
38
  import * as localSupervisor from "./lib/loop-supervisor-run-local.mjs";
26
39
  import { buildWorkerImage, defaultWorkerImageTag, recordWorkerImage } from "./lib/loop-worker-image.mjs";
40
+ import { formatLeases, runLeases } from "./lib/loop-leases.mjs";
41
+ import { migrateAllTaskRuntime } from "./lib/task-runtime-migrate.mjs";
42
+ import { formatLoopEnvReport, loadLoopEnv, publicReport } from "./lib/loop-env.mjs";
43
+ import { callRemoteTool } from "./lib/remote-state-client.mjs";
27
44
 
28
45
  const [action, taskArg, rootArg, modeArg, providerArg, tagArg, titleArg] = process.argv.slice(2);
29
46
  const root = resolve(rootArg || ".");
30
47
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
31
- const ACTIONS = ["start", "resume", "status", "intake", "tasks", "task", "gate", "resume-task", "approve"];
48
+ const ACTIONS = ["start", "resume", "status", "intake", "tasks", "task", "gate", "resume-task", "approve", "feedback"];
32
49
  const readStdin = async (prompt) => {
33
50
  if (stdin.isTTY) { const rl = createInterface({ input: stdin, output: stdout }); try { return await rl.question(prompt); } finally { rl.close(); } }
34
51
  const chunks = []; for await (const chunk of stdin) { chunks.push(chunk); if (chunks.reduce((n, c) => n + c.length, 0) > 8192) throw new Error("Input exceeds 8192 bytes"); } return Buffer.concat(chunks).toString("utf8");
@@ -36,8 +53,60 @@ const readStdin = async (prompt) => {
36
53
  const requireTaskId = () => { if (!ID.test(taskArg ?? "")) throw new Error("Valid task ID required"); return taskArg; };
37
54
  const print = (value) => console.log(JSON.stringify(value, null, 2));
38
55
 
56
+ /** `leases [task-id] [root] [--json] [--clear-stale]`; empty positionals are skipped. */
57
+ async function runLeasesAction(argv) {
58
+ const flagSet = new Set(argv.filter((value) => value.startsWith("--")));
59
+ for (const flag of flagSet) if (!["--json", "--clear-stale"].includes(flag)) throw new Error(`Unknown leases option: ${flag}`);
60
+ const [taskValue = "", rootValue = ""] = argv.filter((value) => !value.startsWith("--"));
61
+ if (taskValue && !ID.test(taskValue)) throw new Error("Valid task ID required");
62
+ const leasesRoot = resolve(rootValue || ".");
63
+ let config = null;
64
+ try { config = JSON.parse(readFileSync(join(leasesRoot, ".agents", "config.json"), "utf8")); } catch { config = null; }
65
+ const env = process.env;
66
+ const fleetReader = config?.loop?.mode === "fleet" && env.AIDLC_FLEET_URL && env.AIDLC_FLEET_TOKEN && env.AIDLC_FLEET_WORKSPACE
67
+ ? () => callRemoteTool({ url: env.AIDLC_FLEET_URL, token: env.AIDLC_FLEET_TOKEN }, "fleet_get", { workspace: env.AIDLC_FLEET_WORKSPACE })
68
+ : undefined;
69
+ const report = await runLeases({ root: leasesRoot, taskId: taskValue || undefined, clearStale: flagSet.has("--clear-stale"), fleetReader });
70
+ if (config?.loop?.mode === "fleet" && !fleetReader) report.fleet = { revision: null, entries: [], error: "lead credential not set (AIDLC_FLEET_URL, AIDLC_FLEET_TOKEN, AIDLC_FLEET_WORKSPACE); fleet leases not shown" };
71
+ if (flagSet.has("--json")) print(report);
72
+ else console.log(formatLeases(report));
73
+ }
74
+
75
+ /** `migrate-data [root] [--dry-run] [--json]`: move pre-runtime per-task files under `runtime/`. */
76
+ function runMigrateDataAction(argv) {
77
+ const flagSet = new Set(argv.filter((value) => value.startsWith("--")));
78
+ for (const flag of flagSet) if (!["--json", "--dry-run"].includes(flag)) throw new Error(`Unknown migrate-data option: ${flag}`);
79
+ const [rootValue = ""] = argv.filter((value) => !value.startsWith("--"));
80
+ const migrateRoot = resolve(rootValue || ".");
81
+ const result = migrateAllTaskRuntime(migrateRoot, { dryRun: flagSet.has("--dry-run") });
82
+ if (flagSet.has("--json")) { print({ schemaVersion: 1, root: migrateRoot, ...result }); return; }
83
+ const verb = result.dryRun ? "would move" : "moved";
84
+ if (!result.entries.length) { console.log(`Nothing to migrate: every task runtime file is already under .agents/data/tasks/<id>/runtime/ (${migrateRoot})`); return; }
85
+ for (const entry of result.entries) console.log(`${entry.action === "skipped" ? "skipped" : verb}: ${entry.from} -> ${entry.to}${entry.reason ? ` (${entry.reason})` : ""}`);
86
+ console.log(`${result.dryRun ? "Would move" : "Moved"} ${result.entries.filter((entry) => entry.action !== "skipped").length} file(s), skipped ${result.skipped}.`);
87
+ }
88
+
89
+ /** `env [root] [--json]`: the local daemon environment report (no secret values). */
90
+ function runEnvAction(argv) {
91
+ const flagSet = new Set(argv.filter((value) => value.startsWith("--")));
92
+ for (const flag of flagSet) if (flag !== "--json") throw new Error(`Unknown env option: ${flag}`);
93
+ const [rootValue = ""] = argv.filter((value) => !value.startsWith("--"));
94
+ const envRoot = resolve(rootValue || ".");
95
+ let config = null;
96
+ try { config = JSON.parse(readFileSync(join(envRoot, ".agents", "config.json"), "utf8")); } catch { config = null; }
97
+ const report = publicReport(loadLoopEnv({ root: envRoot, config }));
98
+ if (flagSet.has("--json")) print(report); else console.log(formatLoopEnvReport(report));
99
+ if (!report.ok) process.exitCode = 1;
100
+ }
101
+
39
102
  try {
40
- if (action === "worker-image") {
103
+ if (action === "env") {
104
+ runEnvAction(process.argv.slice(3));
105
+ } else if (action === "leases") {
106
+ await runLeasesAction(process.argv.slice(3));
107
+ } else if (action === "migrate-data") {
108
+ runMigrateDataAction(process.argv.slice(3));
109
+ } else if (action === "worker-image") {
41
110
  // Build (or rebuild) a worker image without requiring loop configuration.
42
111
  const provider = providerArg || "opencode";
43
112
  if (!isLoopProvider(provider)) throw new Error("Expected --provider codex, claude, or opencode");
@@ -47,7 +116,7 @@ try {
47
116
  console.log(`Built worker image: ${image}`);
48
117
  console.log("Recorded worker image; the loop supervisor will use it automatically.");
49
118
  } else {
50
- if (!ACTIONS.includes(action)) throw new Error("Expected loop start, status, intake, or worker-image");
119
+ if (!ACTIONS.includes(action)) throw new Error("Expected loop start, status, intake, env, leases, migrate-data, or worker-image");
51
120
  const config = JSON.parse(readFileSync(join(root, ".agents", "config.json"), "utf8"));
52
121
  const mode = modeArg || config.loop?.mode || "off";
53
122
  if (!["local", "fleet"].includes(mode)) throw new Error("Loop mode is off; configure an enabled loop mode first");
@@ -57,6 +126,13 @@ try {
57
126
  if (!isLoopProvider(agent) || !Object.keys(workers).length || Object.entries(workers).some(([, value]) => !isLoopProvider(value))) throw new Error("Loop requires a configured lead and worker providers");
58
127
  if (action === "resume") throw new Error("loop resume is removed; the daemon auto-resumes actionable tasks — run 'loop start', and re-activate a paused task from the dashboard (resume) or with the task status command");
59
128
  if (mode === "local") requireLocalAuthority();
129
+ if (mode === "local" && action === "start") {
130
+ // Zero-config start: shell > AIDLC env file > values derived from the providers' own config.
131
+ const loaded = loadLoopEnv({ root, config });
132
+ for (const problem of loaded.problems) process.stderr.write(`loop: ${problem.level === "error" ? "error" : "warning"}: ${problem.message}\n`);
133
+ if (!loaded.ok) throw new Error("environment is incomplete; fix the errors above (check with: aidlc-kit loop env)");
134
+ Object.assign(process.env, loaded.applied);
135
+ }
60
136
  if (mode === "local" && action === "status") {
61
137
  const supervisor = await localSupervisor.createIsolatedLocalSupervisor({ root, taskId: requireTaskId(), agent, workers, workerImages: config.loop?.workerImage, onEvent: streamLog });
62
138
  print(await supervisor.status());
@@ -96,6 +172,13 @@ try {
96
172
  } else if (action === "approve") {
97
173
  const taskId = requireTaskId();
98
174
  print(await authority.approve({ taskId, source: (await readStdin("Approval source: ")).trim(), reviewerToken: process.env.AIDLC_FLEET_REVIEWER_TOKEN }));
175
+ } else if (action === "feedback") {
176
+ // Human guidance that keeps the task at its current gate; no reviewer credential is involved.
177
+ const taskId = requireTaskId();
178
+ let input; try { input = JSON.parse(await readStdin("Feedback JSON: ")); } catch { throw new Error("Feedback must be a JSON object { reason, source }"); }
179
+ const reason = typeof input?.reason === "string" ? input.reason.trim() : "", source = typeof input?.source === "string" ? input.source.trim() : "";
180
+ if (!input || typeof input !== "object" || Array.isArray(input) || !reason || !source) throw new Error("Feedback must be a JSON object { reason, source } with non-empty strings");
181
+ print(await authority.feedback({ taskId, reason, source }));
99
182
  }
100
183
  }
101
184
  }
@@ -0,0 +1,101 @@
1
+ @echo off
2
+ rem AI-DLC loop launcher (managed by aidlc-kit; upgrades overwrite local edits).
3
+ rem .agents\aidlc\scripts\loop.cmd [command] [options]
4
+ rem Edit .agents\config.json ("loop") and .agents\loop.env, then run "loop.cmd up".
5
+ rem The script only locates the aidlc-kit CLI and forwards to "aidlc-kit loop <command>";
6
+ rem it never installs or upgrades anything. Local workers need Docker Desktop; the loop is
7
+ rem verified on Linux, so treat native Windows as best effort.
8
+ setlocal
9
+ for %%I in ("%~dp0..\..\..") do set "ROOT=%%~fI"
10
+
11
+ where node >nul 2>nul
12
+ if errorlevel 1 (
13
+ echo loop.cmd: Node.js 20.17 or newer is required ^(https://nodejs.org^) 1>&2
14
+ exit /b 1
15
+ )
16
+
17
+ set "CMD=%~1"
18
+ rem Options without a command run the default command: "loop.cmd --open" = "loop.cmd up --open".
19
+ if "%CMD%"=="" goto default_cmd
20
+ if /i "%CMD%"=="-h" goto named_cmd
21
+ if /i "%CMD%"=="--help" goto named_cmd
22
+ if "%CMD:~0,1%"=="-" goto default_cmd
23
+ :named_cmd
24
+ shift
25
+ goto args
26
+ :default_cmd
27
+ set "CMD=up"
28
+ :args
29
+ set "ARGS="
30
+ :collect
31
+ if "%~1"=="" goto dispatch
32
+ set ARGS=%ARGS% %1
33
+ shift
34
+ goto collect
35
+
36
+ :dispatch
37
+ if /i "%CMD%"=="help" goto usage
38
+ if /i "%CMD%"=="-h" goto usage
39
+ if /i "%CMD%"=="--help" goto usage
40
+ if /i "%CMD%"=="check" set "CMD=env"
41
+ if /i "%CMD%"=="logs" goto logs
42
+ for %%C in (up down stop status env) do if /i "%CMD%"=="%%C" goto run
43
+ echo loop.cmd: unknown command: %CMD% 1>&2
44
+ call :usage 1>&2
45
+ exit /b 2
46
+
47
+ :run
48
+ call :cli loop %CMD% --root "%ROOT%" %ARGS%
49
+ exit /b %ERRORLEVEL%
50
+
51
+ :cli
52
+ if exist "%ROOT%\node_modules\.bin\aidlc-kit.cmd" (
53
+ call "%ROOT%\node_modules\.bin\aidlc-kit.cmd" %*
54
+ goto :eof
55
+ )
56
+ set "RECORDED_CLI="
57
+ for /f "usebackq delims=" %%P in (`node -e "try{const r=require(process.argv[1]);if(typeof r.cli==='string'&&require('fs').statSync(r.cli).isFile())process.stdout.write(r.cli)}catch{}" "%ROOT%\.agents\data\loop\cli.json" 2^>nul`) do set "RECORDED_CLI=%%P"
58
+ if defined RECORDED_CLI (
59
+ node "%RECORDED_CLI%" %*
60
+ goto :eof
61
+ )
62
+ where aidlc-kit >nul 2>nul
63
+ if not errorlevel 1 (
64
+ call aidlc-kit %*
65
+ goto :eof
66
+ )
67
+ set "VERSION=latest"
68
+ for /f "usebackq delims=" %%V in (`node -p "require(process.argv[1]).packageVersion" "%ROOT%\.agents\aidlc\manifest.json" 2^>nul`) do set "VERSION=%%V"
69
+ where npx >nul 2>nul
70
+ if not errorlevel 1 (
71
+ echo loop.cmd: aidlc-kit was not found in this project or recorded by init/upgrade; trying npx --no-install ^(if it fails: npm install --save-dev @felixdotgo/aidlc-kit@%VERSION%, or rerun init/upgrade from the package or .tgz you want^) 1>&2
72
+ call npx --no-install @felixdotgo/aidlc-kit %*
73
+ goto :eof
74
+ )
75
+ echo loop.cmd: the aidlc-kit CLI was not found: install it in this project ^(npm install --save-dev @felixdotgo/aidlc-kit@%VERSION%^), or rerun init/upgrade from the package or .tgz you want 1>&2
76
+ exit /b 1
77
+
78
+ :logs
79
+ set "LOG=%ROOT%\.agents\data\loop\daemon.log"
80
+ if exist "%LOG%" goto follow_log
81
+ echo No daemon log yet: "%LOG%"
82
+ exit /b 0
83
+ :follow_log
84
+ powershell -NoProfile -Command "Get-Content -LiteralPath $env:LOG -Tail 200 -Wait"
85
+ exit /b %ERRORLEVEL%
86
+
87
+ :usage
88
+ echo Usage: .agents\aidlc\scripts\loop.cmd [command] [options]
89
+ echo.
90
+ echo Commands:
91
+ echo up check the environment, start the loop daemon, and serve the dashboard (default)
92
+ echo options: --loop-env PATH --port 4173 --host 127.0.0.1 --open --no-ui
93
+ echo down stop the dashboard started by "up" and the loop daemon (--now cancels the running turn)
94
+ echo stop stop only the loop daemon (--now cancels the running turn)
95
+ echo status daemon, dashboard, and environment at a glance (--json)
96
+ echo check show providers, the env file, and every variable the loop needs (--json)
97
+ echo logs follow the daemon log
98
+ echo help show this help
99
+ echo.
100
+ echo Configure .agents\config.json ("loop" section) and .agents\loop.env (model and API keys).
101
+ exit /b 0
@@ -0,0 +1,71 @@
1
+ #!/bin/sh
2
+ # AI-DLC loop launcher (managed by aidlc-kit; upgrades overwrite local edits).
3
+ #
4
+ # .agents/aidlc/scripts/loop.sh [command] [options]
5
+ #
6
+ # Edit .agents/config.json ("loop") and .agents/loop.env, then run `loop.sh up`.
7
+ # The script only locates the aidlc-kit CLI and forwards to `aidlc-kit loop <command>`;
8
+ # it never installs or upgrades anything.
9
+ set -eu
10
+
11
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
12
+ ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
13
+
14
+ usage() {
15
+ cat <<'EOF'
16
+ Usage: .agents/aidlc/scripts/loop.sh [command] [options]
17
+
18
+ Commands:
19
+ up check the environment, start the loop daemon, and serve the dashboard (default)
20
+ options: --loop-env PATH --port 4173 --host 127.0.0.1 --open --no-ui
21
+ down stop the dashboard started by `up` and the loop daemon (--now cancels the running turn)
22
+ stop stop only the loop daemon (--now cancels the running turn)
23
+ status daemon, dashboard, and environment at a glance (--json)
24
+ check show providers, the env file, and every variable the loop needs (--json)
25
+ logs follow the daemon log
26
+ help show this help
27
+
28
+ Configure:
29
+ .agents/config.json "loop": { "mode": "local", "agents": { "lead": "<provider>", "workers": { "worker-a": "<provider>" } } }
30
+ .agents/loop.env model and API keys (git-ignored); values your tools already store are detected
31
+ EOF
32
+ }
33
+
34
+ command -v node >/dev/null 2>&1 || { echo "loop.sh: Node.js 20.17 or newer is required (https://nodejs.org)" >&2; exit 1; }
35
+
36
+ # The installed workflow version, so hints name the matching package.
37
+ VERSION=$(node -p 'require(process.argv[1]).packageVersion' "$ROOT/.agents/aidlc/manifest.json" 2>/dev/null || echo latest)
38
+ INSTALL_HINT="install it in this project (npm install --save-dev @felixdotgo/aidlc-kit@$VERSION), or rerun init/upgrade from the package or .tgz you want"
39
+ # The CLI that installed this workflow (recorded by init/upgrade), if it still exists.
40
+ RECORDED_CLI=$(node -e 'try{const r=require(process.argv[1]);if(typeof r.cli==="string"&&require("fs").statSync(r.cli).isFile())process.stdout.write(r.cli)}catch{}' "$ROOT/.agents/data/loop/cli.json" 2>/dev/null || true)
41
+
42
+ run_cli() {
43
+ if [ -x "$ROOT/node_modules/.bin/aidlc-kit" ]; then exec "$ROOT/node_modules/.bin/aidlc-kit" "$@"; fi
44
+ if [ -n "$RECORDED_CLI" ]; then exec node "$RECORDED_CLI" "$@"; fi
45
+ if command -v aidlc-kit >/dev/null 2>&1; then exec aidlc-kit "$@"; fi
46
+ if command -v npx >/dev/null 2>&1; then
47
+ echo "loop.sh: aidlc-kit was not found in this project or recorded by init/upgrade; trying npx --no-install (if it fails: $INSTALL_HINT)" >&2
48
+ exec npx --no-install @felixdotgo/aidlc-kit "$@"
49
+ fi
50
+ echo "loop.sh: the aidlc-kit CLI was not found: $INSTALL_HINT" >&2
51
+ exit 1
52
+ }
53
+
54
+ # Options without a command run the default command: `loop.sh --open` = `loop.sh up --open`.
55
+ case "${1:-up}" in
56
+ -h|--help) cmd=help; shift ;;
57
+ -*) cmd=up ;;
58
+ *) cmd=${1:-up}; [ $# -gt 0 ] && shift ;;
59
+ esac
60
+
61
+ case "$cmd" in
62
+ up|down|stop|status) run_cli loop "$cmd" --root "$ROOT" "$@" ;;
63
+ check|env) run_cli loop env --root "$ROOT" "$@" ;;
64
+ logs)
65
+ log="$ROOT/.agents/data/loop/daemon.log"
66
+ if [ ! -f "$log" ]; then echo "No daemon log yet: $log"; exit 0; fi
67
+ exec tail -n 200 -f "$log"
68
+ ;;
69
+ help|-h|--help) usage ;;
70
+ *) echo "loop.sh: unknown command: $cmd" >&2; usage >&2; exit 2 ;;
71
+ esac
@@ -5,7 +5,9 @@ import { readFile } from "node:fs/promises";
5
5
  import { promisify } from "node:util";
6
6
  import { createRemoteReviewerClient, createRemoteStateClient, featureConfig, remoteConfig, reviewerConfig } from "./lib/remote-state-client.mjs";
7
7
  import { compileContext } from "./lib/context-runtime.mjs";
8
- import { checkGate, closeTask, handoffTask, nextAction, readyTaskItems, recordLesson, recordNoLessons, reopenTask, transitionTask, validateState } from "./lib/store.mjs";
8
+ import { explainFleetConflict } from "./lib/loop-holder.mjs";
9
+ import { checkGate, closeTask, handoffTask, nextAction, readyTaskItems, recordLesson, recordNoLessons, recordTaskCommit, reopenTask, transitionTask, validateState } from "./lib/store.mjs";
10
+ import { headCommit } from "./lib/repo-head.mjs";
9
11
 
10
12
  const parse = (argv) => {
11
13
  const positionals = []; const options = new Map();
@@ -54,9 +56,9 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
54
56
  required(id, "task id");
55
57
  const allowed = {
56
58
  "task show": [], "task next": [], "task ready": [], "gate check": ["--gate"], "phase context": ["--phase", "--mode", "--item"],
57
- "task create": ["--title", "--type", "--language", "--risk", "--area", "--feature"],
59
+ "task create": ["--title", "--type", "--language", "--risk", "--area", "--feature", "--base-commit"],
58
60
  "task status": ["--status", "--mode", "--reason", "--source"], "task item": ["--status", "--label", "--depends-on", "--owns"],
59
- "task transition": ["--to", "--mode", "--reason", "--source"],
61
+ "task transition": ["--to", "--mode", "--reason", "--source", "--final-commit"],
60
62
  "task handoff": ["--kind", "--reason", "--source"], "task close": ["--reason", "--source"], "task reopen": ["--to", "--reason", "--source"],
61
63
  "decision set": ["--status", "--label", "--resolution"],
62
64
  "evidence add": ["--kind", "--result", "--gate", "--area", "--source", "--detail"],
@@ -118,6 +120,8 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
118
120
  areas: (option("--area") ?? "root").split(",").map((item) => item.trim()).filter(Boolean), branch: "—",
119
121
  artifacts: { intent: `.agents/data/tasks/${id}/intent.md`, design: `.agents/data/tasks/${id}/design.md`, workplan: `.agents/data/tasks/${id}/workplan.md` },
120
122
  decisions: [], tasks: [], evidence: [], createdAt: recordedAt, updatedAt: recordedAt };
123
+ const baseCommit = option("--base-commit") ?? headCommit(root);
124
+ if (baseCommit !== undefined) recordTaskCommit(task, "baseCommit", baseCommit);
121
125
  } else {
122
126
  if (!previous) throw new Error(`Unknown task: ${id}`);
123
127
  task = structuredClone(previous);
@@ -161,6 +165,7 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
161
165
  if (option("--mode") !== "audited") throw new Error("Transition requires --mode audited");
162
166
  const reason = required(option("--reason"), "--reason"); const source = required(option("--source"), "--source");
163
167
  const target = required(option("--to"), "--to");
168
+ if (target !== "done" && option("--final-commit") !== undefined) throw new Error("--final-commit is only accepted with --to done");
164
169
  if (target === "done" && task.featureId) {
165
170
  const manifest = await client.featureGet(task.featureId);
166
171
  const member = manifest?.feature?.tasks?.find((entry) => entry.id === id);
@@ -171,6 +176,7 @@ export const runRemoteState = async (argv, client, root, git = {}) => {
171
176
  const clone = structuredClone(state); clone.tasks[id] = task;
172
177
  transitionTask(clone, id, target);
173
178
  task = clone.tasks[id];
179
+ if (target === "done") { const finalCommit = option("--final-commit") ?? headCommit(root); if (finalCommit !== undefined) recordTaskCommit(task, "finalCommit", finalCommit); }
174
180
  task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Audited transition ${previous.phase} → ${target}: ${reason}`, recordedAt: now() });
175
181
  } else if (route === "decision set") {
176
182
  const status = required(option("--status"), "--status");
@@ -307,5 +313,5 @@ if (isEntry) {
307
313
  const result = isReview ? await runRemoteReview(argv, createRemoteReviewerClient(config), config) : argv[0] === "feature" ? await runRemoteFeature(argv, createRemoteStateClient(config)) : argv[0] === "run" ? await runRemoteLease(argv, createRemoteStateClient(config)) : await runRemoteState(argv, createRemoteStateClient(config), config.projectRoot, config);
308
314
  console.log(JSON.stringify(result, null, 2));
309
315
  if (!result.ok) process.exitCode = 1;
310
- } catch (error) { console.error(JSON.stringify({ ok: false, error: error.message, code: error.code, currentRevision: error.current?.revision }, null, 2)); process.exitCode = 1; }
316
+ } catch (error) { console.error(JSON.stringify({ ok: false, error: error.code === "FLEET_CONFLICT" ? explainFleetConflict(error.message) : error.message, code: error.code, currentRevision: error.current?.revision }, null, 2)); process.exitCode = 1; }
311
317
  }
@@ -6,13 +6,14 @@
6
6
  import { acquireStateLock, approveAndAdvance, checkGate, closeTask, failEntry, handoffTask, listTaskSummaries, loadMemoryRegistry, loadState, loadTask, migrateState, nextAction, option, parseArguments, promoteAgenticMemory, rebuildLessonIndex, recordLesson, recordNoLessons, renderViews, reopenTask, retireAgenticMemory, rootOption, saveState, searchLessons, supersedeTask, transitionTask, validateArguments } from "./lib/runtime.mjs";
7
7
  import { readLiveLease } from "./lib/loop-lease.mjs";
8
8
  import { listFeatures, loadFeature, readyFeatureTasks, saveFeature } from "./lib/feature-store.mjs";
9
- import { readyTaskItems } from "./lib/store.mjs";
9
+ import { readyTaskItems, recordTaskCommit } from "./lib/store.mjs";
10
+ import { headCommit } from "./lib/repo-head.mjs";
10
11
  import { requireLocalAuthority } from "./lib/authority.mjs";
11
12
 
12
13
  try {
13
14
  requireLocalAuthority();
14
15
  const raw = process.argv.slice(2);
15
- const valueOptions = ["--root", "--status", "--limit", "--cursor", "--query", "--title", "--type", "--language", "--risk", "--area", "--mode", "--reason", "--source", "--to", "--kind", "--successor", "--label", "--resolution", "--result", "--gate", "--detail", "--summary", "--prevention", "--example", "--promotion", "--guidance", "--phase", "--priority", "--source-task", "--source-lesson", "--approved-by", "--switch-from", "--feature", "--depends-on", "--branch", "--owns"];
16
+ const valueOptions = ["--root", "--status", "--limit", "--cursor", "--query", "--title", "--type", "--language", "--risk", "--area", "--mode", "--reason", "--source", "--to", "--kind", "--successor", "--label", "--resolution", "--result", "--gate", "--detail", "--summary", "--prevention", "--example", "--promotion", "--guidance", "--phase", "--priority", "--source-task", "--source-lesson", "--approved-by", "--switch-from", "--feature", "--depends-on", "--branch", "--owns", "--base-commit", "--final-commit"];
16
17
  const parsed = parseArguments(raw, { valueOptions, booleanOptions: ["--include-archive"] });
17
18
  const args = parsed.positionals;
18
19
  const [group, action, rawId] = args;
@@ -24,9 +25,9 @@ const schemas = {
24
25
  "task find": { minPositionals: 2, valueOptions: ["--query", "--status", "--limit", "--cursor"], booleanOptions: ["--include-archive"], requiredOptions: ["--query"] },
25
26
  "task next": { minPositionals: 3 },
26
27
  "task ready": { minPositionals: 3 },
27
- "task create": { minPositionals: 3, valueOptions: ["--title", "--type", "--language", "--risk", "--area", "--switch-from", "--feature", "--depends-on"], requiredOptions: ["--title"] },
28
- "task transition": { minPositionals: 3, valueOptions: ["--mode", "--reason", "--source", "--to"], requiredOptions: ["--mode", "--reason", "--source", "--to"], usage: "task transition requires --mode audited, --reason, --source, and --to" },
29
- "task archive": { minPositionals: 3, valueOptions: ["--reason", "--source"], requiredOptions: ["--reason", "--source"] },
28
+ "task create": { minPositionals: 3, valueOptions: ["--title", "--type", "--language", "--risk", "--area", "--switch-from", "--feature", "--depends-on", "--base-commit"], requiredOptions: ["--title"] },
29
+ "task transition": { minPositionals: 3, valueOptions: ["--mode", "--reason", "--source", "--to", "--final-commit"], requiredOptions: ["--mode", "--reason", "--source", "--to"], usage: "task transition requires --mode audited, --reason, --source, and --to" },
30
+ "task archive": { minPositionals: 3, valueOptions: ["--reason", "--source", "--final-commit"], requiredOptions: ["--reason", "--source"] },
30
31
  "task handoff": { minPositionals: 3, valueOptions: ["--kind", "--reason", "--source"], requiredOptions: ["--kind", "--reason", "--source"] },
31
32
  "task close": { minPositionals: 3, valueOptions: ["--reason", "--source"], requiredOptions: ["--reason", "--source"] },
32
33
  "task supersede": { minPositionals: 3, valueOptions: ["--successor", "--reason", "--source"], requiredOptions: ["--successor", "--reason", "--source"] },
@@ -75,6 +76,7 @@ const summaryOptions = (query) => ({
75
76
  cursor: numberOption("--cursor", 0)
76
77
  });
77
78
  const warnings = [];
79
+ const recordFinalCommit = (task) => { const sha = option(parsed, "--final-commit") ?? headCommit(root); if (sha !== undefined) recordTaskCommit(task, "finalCommit", sha); };
78
80
  const derivedStep = (code, hint, step) => { try { step(); } catch (error) { warnings.push({ code, message: error instanceof Error ? error.message : String(error), hint }); } };
79
81
  const persist = (tasks, rebuildLessons = false) => {
80
82
  const archived = saveState(root, state);
@@ -158,6 +160,8 @@ if (group === "state" && action === "migrate") {
158
160
  artifacts: { intent: `.agents/data/tasks/${id}/intent.md`, design: `.agents/data/tasks/${id}/design.md`, workplan: `.agents/data/tasks/${id}/workplan.md` },
159
161
  decisions: [], tasks: [], evidence: [], createdAt: recordedAt, updatedAt: recordedAt
160
162
  };
163
+ const baseCommit = option(parsed, "--base-commit") ?? headCommit(root);
164
+ if (baseCommit !== undefined) recordTaskCommit(task, "baseCommit", baseCommit);
161
165
  const acknowledgedTasks = acknowledged.map((ack) => state.tasks[ack]);
162
166
  for (const prior of acknowledgedTasks) {
163
167
  prior.evidence.push({ kind: "diagnostic", result: "pass", source: `task create ${id}`, detail: `Switch acknowledged: created ${id} while ${prior.id} was actionable; ${prior.id} keeps its state — resume with task next ${prior.id}`, recordedAt });
@@ -172,13 +176,16 @@ if (group === "state" && action === "migrate") {
172
176
  } else if (group === "task" && action === "transition") {
173
177
  const mode = option(parsed, "--mode"); const reason = option(parsed, "--reason"); const source = option(parsed, "--source");
174
178
  if (mode !== "audited" || !reason?.trim() || !source?.trim()) throw new Error("task transition requires --mode audited, --reason, and --source");
175
- const from = state.tasks[id]?.phase; const target = option(parsed, "--to"); if (target === "done") requireFeaturePrerequisites(state.tasks[id]); const task = transitionTask(state, id, target);
179
+ const from = state.tasks[id]?.phase; const target = option(parsed, "--to"); if (target === "done") requireFeaturePrerequisites(state.tasks[id]);
180
+ if (target !== "done" && option(parsed, "--final-commit") !== undefined) throw new Error("--final-commit is only accepted with --to done");
181
+ const task = transitionTask(state, id, target);
182
+ if (target === "done") recordFinalCommit(task);
176
183
  task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Audited transition ${from} → ${target}: ${reason}`, recordedAt: now() });
177
184
  persist([task]); respond(task);
178
185
  } else if (group === "task" && action === "archive") {
179
186
  const reason = option(parsed, "--reason"); const source = option(parsed, "--source");
180
187
  if (!reason?.trim() || !source?.trim()) throw new Error("task archive requires --reason and --source");
181
- requireFeaturePrerequisites(state.tasks[id]); const task = transitionTask(state, id, "done");
188
+ requireFeaturePrerequisites(state.tasks[id]); const task = transitionTask(state, id, "done"); recordFinalCommit(task);
182
189
  task.evidence.push({ kind: "diagnostic", result: "pass", source, detail: `Archive to done: ${reason}`, recordedAt: now() });
183
190
  persist([task], true); respond(task);
184
191
  } else if (group === "task" && action === "handoff") {
@@ -270,7 +277,8 @@ if (group === "state" && action === "migrate") {
270
277
  } else if (group === "gate" && action === "approve") {
271
278
  const gate = option(parsed, "--gate"); const source = option(parsed, "--source");
272
279
  if (!id || !gate || !source) throw new Error("gate approve requires active task id, --gate, and explicit --source");
273
- if (readLiveLease(root, id)) throw new Error(`Gate approval is unavailable while loop runner owns ${id}; wait for the runner to stop at the human gate`);
280
+ const runner = readLiveLease(root, id);
281
+ if (runner) throw new Error(`Gate approval is unavailable while loop runner owns ${id}; wait for the runner to stop at the human gate (task lease held by pid ${runner.pid}${runner.host ? ` on ${runner.host}` : ""}${runner.activity ? `, activity ${runner.activity}` : ""}${runner.expiresAt ? `, expires ${runner.expiresAt}` : ""}; inspect: node .agents/aidlc/scripts/loop-entry.mjs leases ${id})`);
274
282
  const outcome = approveAndAdvance(root, state, id, gate, source); persist([outcome.task]); emit({ task: outcome.task, idempotent: outcome.idempotent }, outcome.task);
275
283
  } else {
276
284
  throw new Error(usage);
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Task changes page. The diff is server-rendered (and escaped); this script only
3
+ * filters the item sections in place when the selector changes (the form still
4
+ * works without it), scrolls smoothly to a file, and applies the diff display
5
+ * settings from aidlcSettings: `diffWrap` toggles `is-wrapped` on the diff
6
+ * containers; `diffIgnoreWs` reloads the page with `?ws=ignore`, which the server
7
+ * maps to `git diff -w`. Loaded after common.js.
8
+ */
9
+ (() => {
10
+ const settings = window.aidlcSettings;
11
+ const select = document.querySelector("[data-changes-select]");
12
+ const submit = document.querySelector("[data-changes-submit]");
13
+ const showItem = (item) => {
14
+ document.querySelectorAll("[data-change-item]").forEach((section) => {
15
+ section.hidden = Boolean(item) && section.getAttribute("data-change-item") !== item;
16
+ });
17
+ };
18
+ /* The URL with `?ws=ignore` set or cleared (other params and the hash are kept). */
19
+ const wsUrl = (ignore) => {
20
+ const params = new URLSearchParams(location.search);
21
+ if (ignore) params.set("ws", "ignore"); else params.delete("ws");
22
+ const query = params.toString();
23
+ return location.pathname + (query ? "?" + query : "") + location.hash;
24
+ };
25
+ const wsInUrl = () => new URLSearchParams(location.search).get("ws") === "ignore";
26
+ const applyWrap = (wrap) => {
27
+ document.querySelectorAll(".ad-changes-diff").forEach((diff) => diff.classList.toggle("is-wrapped", Boolean(wrap)));
28
+ };
29
+
30
+ if (settings) {
31
+ const current = settings.get();
32
+ applyWrap(current.diffWrap);
33
+ // The whitespace mode is a server-side diff option: the saved setting wins over the URL.
34
+ if (current.diffIgnoreWs !== wsInUrl()) { location.replace(wsUrl(current.diffIgnoreWs)); return; }
35
+ const options = document.querySelector("[data-changes-options]");
36
+ const wrapToggle = document.querySelector("[data-changes-wrap]");
37
+ const wsToggle = document.querySelector("[data-changes-ws]");
38
+ if (options) options.hidden = false;
39
+ if (wrapToggle) {
40
+ wrapToggle.checked = current.diffWrap;
41
+ wrapToggle.addEventListener("change", () => settings.set({ diffWrap: wrapToggle.checked }));
42
+ }
43
+ if (wsToggle) {
44
+ wsToggle.checked = current.diffIgnoreWs;
45
+ wsToggle.addEventListener("change", () => settings.set({ diffIgnoreWs: wsToggle.checked }));
46
+ }
47
+ // Covers the toggles here, the Settings dialog, and another tab saving.
48
+ settings.onChange((next, changed) => {
49
+ if (changed.includes("diffWrap")) { applyWrap(next.diffWrap); if (wrapToggle) wrapToggle.checked = next.diffWrap; }
50
+ if (changed.includes("diffIgnoreWs")) {
51
+ if (wsToggle) wsToggle.checked = next.diffIgnoreWs;
52
+ if (next.diffIgnoreWs !== wsInUrl()) location.assign(wsUrl(next.diffIgnoreWs));
53
+ }
54
+ });
55
+ }
56
+
57
+ if (select) {
58
+ if (submit) submit.hidden = true;
59
+ select.addEventListener("change", () => {
60
+ const item = select.value;
61
+ const params = new URLSearchParams(location.search);
62
+ if (item) params.set("item", item); else params.delete("item");
63
+ const query = params.toString();
64
+ const url = location.pathname + (query ? "?" + query : "");
65
+ // An item whose patch was left out of the whole-task view is loaded from the server.
66
+ const section = item ? document.querySelector('[data-change-item="' + CSS.escape(item) + '"]') : null;
67
+ if (section && section.hasAttribute("data-patch-omitted")) { location.assign(url); return; }
68
+ showItem(item);
69
+ history.replaceState(history.state, "", url);
70
+ });
71
+ }
72
+ document.addEventListener("click", (event) => {
73
+ const link = event.target instanceof Element ? event.target.closest("a[data-changes-file]") : null;
74
+ if (!link) return;
75
+ const target = document.getElementById((link.getAttribute("href") || "").slice(1));
76
+ if (!target) return;
77
+ event.preventDefault();
78
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
79
+ history.replaceState(history.state, "", location.pathname + location.search + "#" + target.id);
80
+ target.classList.add("is-target");
81
+ setTimeout(() => target.classList.remove("is-target"), 1500);
82
+ });
83
+ })();