@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.
- package/DEVELOPMENT.md +13 -26
- package/README.md +35 -39
- package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
- package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
- package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
- package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
- package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +21 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +52 -78
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +88 -14
- package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +20 -5
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +10 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +2 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +3 -2
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +9 -3
- package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +11 -3
- package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
- package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +78 -17
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +42 -10
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +66 -9
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +12 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +14 -2
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
- package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
- package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +11 -0
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
- package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
- package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +87 -4
- package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
- package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
- package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
- package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
- package/dist/assets/ui/changes.js +83 -0
- package/dist/assets/ui/common.js +664 -0
- package/dist/assets/ui/dashboard.css +281 -59
- package/dist/assets/ui/dashboard.js +77 -0
- package/dist/assets/ui/detail.js +159 -0
- package/dist/assets/ui/live-log.js +332 -0
- package/dist/services/aidlc-coordination/package.json +1 -1
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +11 -0
- package/dist/src/cli/argv.js +10 -4
- package/dist/src/cli/commands.js +52 -22
- package/dist/src/cli/loop-launcher.d.ts +47 -0
- package/dist/src/cli/loop-launcher.js +322 -0
- package/dist/src/cli.js +3 -0
- package/dist/src/installer.js +4 -2
- package/dist/src/model/records.d.ts +4 -0
- package/dist/src/state/validate.js +5 -0
- package/dist/src/task-ui.d.ts +31 -52
- package/dist/src/task-ui.js +25 -1562
- package/dist/src/ui/assets.d.ts +14 -0
- package/dist/src/ui/assets.js +45 -0
- package/dist/src/ui/changes.d.ts +83 -0
- package/dist/src/ui/changes.js +498 -0
- package/dist/src/ui/containers.d.ts +71 -0
- package/dist/src/ui/containers.js +214 -0
- package/dist/src/ui/daemon.d.ts +75 -0
- package/dist/src/ui/daemon.js +232 -0
- package/dist/src/ui/data.d.ts +42 -0
- package/dist/src/ui/data.js +89 -0
- package/dist/src/ui/docs.d.ts +13 -0
- package/dist/src/ui/docs.js +39 -0
- package/dist/src/ui/files.d.ts +3 -0
- package/dist/src/ui/files.js +20 -0
- package/dist/src/ui/http.d.ts +14 -0
- package/dist/src/ui/http.js +80 -0
- package/dist/src/ui/live.d.ts +5 -0
- package/dist/src/ui/live.js +43 -0
- package/dist/src/ui/markdown.d.ts +2 -0
- package/dist/src/ui/markdown.js +127 -0
- package/dist/src/ui/options.d.ts +48 -0
- package/dist/src/ui/options.js +81 -0
- package/dist/src/ui/security.d.ts +34 -0
- package/dist/src/ui/security.js +38 -0
- package/dist/src/ui/server.d.ts +39 -0
- package/dist/src/ui/server.js +347 -0
- package/dist/src/ui/sse.d.ts +14 -0
- package/dist/src/ui/sse.js +281 -0
- package/dist/src/ui/views/board.d.ts +7 -0
- package/dist/src/ui/views/board.js +131 -0
- package/dist/src/ui/views/changes.d.ts +19 -0
- package/dist/src/ui/views/changes.js +154 -0
- package/dist/src/ui/views/components.d.ts +56 -0
- package/dist/src/ui/views/components.js +113 -0
- package/dist/src/ui/views/docs.d.ts +15 -0
- package/dist/src/ui/views/docs.js +64 -0
- package/dist/src/ui/views/layout.d.ts +44 -0
- package/dist/src/ui/views/layout.js +233 -0
- package/dist/src/ui/views/task.d.ts +25 -0
- package/dist/src/ui/views/task.js +320 -0
- package/dist/src/upgrade.js +5 -2
- package/dist/src/workflow.d.ts +4 -0
- package/dist/src/workflow.js +33 -1
- package/docs/README.md +48 -33
- package/docs/architecture.md +101 -38
- package/docs/command-reference.md +269 -93
- package/docs/configuration.md +62 -42
- package/docs/coordination-service.md +85 -36
- package/docs/design-decisions.md +35 -31
- package/docs/development.md +111 -61
- package/docs/loop-local-run.md +135 -402
- package/docs/loop-runbook.md +84 -105
- package/docs/operating-workflow.md +55 -42
- package/docs/testing-and-release.md +70 -51
- package/docs/troubleshooting.md +165 -69
- 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 === "
|
|
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 {
|
|
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]);
|
|
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
|
-
|
|
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
|
+
})();
|