@felixdotgo/aidlc-kit 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPMENT.md +14 -13
- package/README.md +37 -43
- package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
- package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
- package/dist/assets/.agents/aidlc/orchestrator.md +2 -2
- package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
- package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
- package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
- package/dist/assets/.agents/aidlc/schemas/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 +261 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
- package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
- package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
- package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
- package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
- 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 +116 -28
- package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
- package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
- 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 +149 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
- package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
- 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 +68 -2
- 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 +168 -73
- package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
- package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
- package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
- 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/.agents/aidlc/templates/design.md +9 -0
- package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
- package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
- package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
- package/dist/assets/ui/bulma.min.css +3 -0
- package/dist/assets/ui/changes.js +83 -0
- package/dist/assets/ui/common.js +664 -0
- package/dist/assets/ui/dashboard.css +636 -0
- 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/Dockerfile +11 -3
- package/dist/services/aidlc-coordination/compose.yaml +12 -0
- package/dist/services/aidlc-coordination/package-lock.json +3 -1
- package/dist/services/aidlc-coordination/package.json +9 -4
- package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
- package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
- package/dist/services/aidlc-coordination/src/server.mjs +9 -3
- package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
- package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
- package/dist/src/adapters.d.ts +0 -3
- package/dist/src/adapters.js +3 -5
- package/dist/src/cli/argv.d.ts +12 -0
- package/dist/src/cli/argv.js +68 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.js +159 -0
- package/dist/src/cli/loop-launcher.d.ts +47 -0
- package/dist/src/cli/loop-launcher.js +322 -0
- package/dist/src/cli/options.d.ts +25 -0
- package/dist/src/cli/options.js +32 -0
- package/dist/src/cli/prompt.d.ts +9 -0
- package/dist/src/cli/prompt.js +66 -0
- package/dist/src/cli.js +28 -191
- package/dist/src/gate.js +3 -2
- package/dist/src/installer.d.ts +14 -7
- package/dist/src/installer.js +42 -143
- package/dist/src/legacy.js +1 -0
- package/dist/src/managed-content.d.ts +7 -0
- package/dist/src/managed-content.js +15 -4
- package/dist/src/managed-planning.d.ts +65 -0
- package/dist/src/managed-planning.js +219 -0
- package/dist/src/mcp-setup.js +3 -1
- package/dist/src/model/config-contracts.d.ts +65 -0
- package/dist/src/model/config-contracts.js +1 -0
- package/dist/src/model/install-contracts.d.ts +29 -0
- package/dist/src/model/install-contracts.js +1 -0
- package/dist/src/model/lifecycle-enums.d.ts +13 -0
- package/dist/src/model/lifecycle-enums.js +1 -0
- package/dist/src/model/loop.d.ts +20 -0
- package/dist/src/model/loop.js +1 -0
- package/dist/src/model/manifest.d.ts +16 -0
- package/dist/src/model/manifest.js +1 -0
- package/dist/src/model/records.d.ts +142 -0
- package/dist/src/model/records.js +1 -0
- package/dist/src/model.d.ts +11 -264
- package/dist/src/model.js +1 -1
- package/dist/src/profiles.d.ts +2 -0
- package/dist/src/profiles.js +53 -9
- package/dist/src/state/archive-record.d.ts +9 -0
- package/dist/src/state/archive-record.js +26 -0
- package/dist/src/state/commands.d.ts +18 -0
- package/dist/src/state/commands.js +27 -0
- package/dist/src/state/domain.d.ts +9 -0
- package/dist/src/state/domain.js +9 -0
- package/dist/src/state/guards.d.ts +7 -0
- package/dist/src/state/guards.js +17 -0
- package/dist/src/state/io.d.ts +10 -0
- package/dist/src/state/io.js +35 -0
- package/dist/src/state/items.d.ts +3 -4
- package/dist/src/state/items.js +58 -57
- package/dist/src/state/lessons.js +14 -7
- package/dist/src/state/lifecycle.js +2 -2
- package/dist/src/state/lock.js +7 -6
- package/dist/src/state/memory.js +24 -12
- package/dist/src/state/{archive.js → migrate.js} +7 -7
- package/dist/src/state/partition.d.ts +15 -0
- package/dist/src/state/partition.js +90 -0
- package/dist/src/state/paths.d.ts +11 -0
- package/dist/src/state/paths.js +12 -0
- package/dist/src/state/render.js +9 -6
- package/dist/src/state/store.js +9 -15
- package/dist/src/state/transitions.js +9 -7
- package/dist/src/state/validate.js +55 -7
- package/dist/src/state.d.ts +16 -11
- package/dist/src/state.js +16 -11
- package/dist/src/task-ui.d.ts +33 -0
- package/dist/src/task-ui.js +33 -0
- 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-board.js +10 -10
- package/dist/src/upgrade.d.ts +0 -1
- package/dist/src/upgrade.js +24 -85
- package/dist/src/workflow.d.ts +4 -0
- package/dist/src/workflow.js +36 -4
- package/docs/README.md +41 -22
- package/docs/architecture.md +101 -37
- package/docs/command-reference.md +289 -52
- package/docs/configuration.md +70 -30
- package/docs/coordination-service.md +91 -30
- package/docs/design-decisions.md +87 -0
- package/docs/development.md +124 -49
- package/docs/loop-local-run.md +228 -0
- package/docs/loop-runbook.md +214 -0
- package/docs/operating-workflow.md +55 -42
- package/docs/testing-and-release.md +72 -49
- package/docs/troubleshooting.md +221 -0
- package/package.json +4 -1
- package/dist/src/ledger.d.ts +0 -22
- package/dist/src/ledger.js +0 -119
- package/dist/src/state/shared.d.ts +0 -29
- package/dist/src/state/shared.js +0 -71
- package/dist/src/tracker.d.ts +0 -36
- package/dist/src/tracker.js +0 -44
- /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { taskContainers } from "../containers.js";
|
|
2
|
+
import { ARTIFACT_NAMES, artifactPayload, pendingFeedback, readUiState, readUiTask, taskGatePayload } from "../data.js";
|
|
3
|
+
import { escapeHtml } from "../http.js";
|
|
4
|
+
import { markdownToHtml } from "../markdown.js";
|
|
5
|
+
import { uiMode } from "../options.js";
|
|
6
|
+
import { DECISION_TONE, feedbackPill, GATE_LABELS, gateLabel, gatePill, ITEM_TONE, phaseStepper, pill, relativeTime, renderGateViewHtml, RESULT_TONE, riskPill, statusPill } from "./components.js";
|
|
7
|
+
import { changesHref } from "./changes.js";
|
|
8
|
+
import { liveRegion, pageHead, pageScripts, renderHeader, renderModals, reviewQueue } from "./layout.js";
|
|
9
|
+
/* ---------------------------------------------------------------------------
|
|
10
|
+
* Task detail (page and side-panel fragment share the same body).
|
|
11
|
+
* ------------------------------------------------------------------------- */
|
|
12
|
+
/** The artifact a reviewer most likely needs at each gate opens by default. */
|
|
13
|
+
const GATE_ARTIFACT = { G0_confirm: "intent", G1_review: "design", G2_codereview: "workplan" };
|
|
14
|
+
/** Lines kept by the task page's live log and shown by the side panel's compact one. */
|
|
15
|
+
const FULL_LOG_LINES = 2000;
|
|
16
|
+
export const DRAWER_LOG_LINES = 20;
|
|
17
|
+
/** Task id as a copy-to-clipboard button (handled by the delegated \`[data-copy]\` click in common.js). */
|
|
18
|
+
export const copyIdButton = (id) => `<button type="button" class="ad-copy" data-copy="${escapeHtml(id)}" title="Copy task ID" aria-label="Copy task ID ${escapeHtml(id)}"><code>${escapeHtml(id)}</code><svg aria-hidden="true" viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="8.5" height="8.5" rx="1.5"/><path d="M10.5 5V3.5A1.5 1.5 0 0 0 9 2H3.5A1.5 1.5 0 0 0 2 3.5V9a1.5 1.5 0 0 0 1.5 1.5H5"/></svg></button>`;
|
|
19
|
+
const panel = (title, body, extra = "", flush = false) => `<section class="ad-panel" data-key="panel:${escapeHtml(title)}"><header class="ad-panel-head"><h2>${escapeHtml(title)}</h2>${extra}</header><div class="ad-panel-body${flush ? " is-flush" : ""}">${body}</div></section>`;
|
|
20
|
+
/* ---------------------------------------------------------------------------
|
|
21
|
+
* Containers panel (worker containers recorded under the task's runtime data)
|
|
22
|
+
* ------------------------------------------------------------------------- */
|
|
23
|
+
const UUID_PATTERN = /([0-9a-f]{8})-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
24
|
+
/** `aidlc-worker-3f2a1b9c-…` → `3f2a1b9c`; names without a uuid are shown whole. */
|
|
25
|
+
export const shortContainerName = (name) => UUID_PATTERN.exec(name)?.[1] ?? name;
|
|
26
|
+
/** `endedAt - startedAt` as `4s`, `3m 12s`, or `1h 05m`; empty when either timestamp is unusable. */
|
|
27
|
+
export const durationText = (startedAt, endedAt) => {
|
|
28
|
+
const start = Date.parse(startedAt ?? "");
|
|
29
|
+
const end = Date.parse(endedAt ?? "");
|
|
30
|
+
if (Number.isNaN(start) || Number.isNaN(end))
|
|
31
|
+
return "";
|
|
32
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
33
|
+
if (seconds < 60)
|
|
34
|
+
return `${seconds}s`;
|
|
35
|
+
if (seconds < 3600)
|
|
36
|
+
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
37
|
+
return `${Math.floor(seconds / 3600)}h ${String(Math.floor((seconds % 3600) / 60)).padStart(2, "0")}m`;
|
|
38
|
+
};
|
|
39
|
+
const containerStatusPill = (entry) => {
|
|
40
|
+
if (entry.running)
|
|
41
|
+
return pill("running", "is-success is-live", "the container is running");
|
|
42
|
+
if (!entry.endedAt)
|
|
43
|
+
return pill("unknown", "is-warning", "no end recorded and docker does not list the container");
|
|
44
|
+
if (entry.exitCode === null || entry.exitCode === undefined)
|
|
45
|
+
return pill("signal", "is-warning", "ended without an exit code (signal or finalized)");
|
|
46
|
+
return pill(`exited ${entry.exitCode}`, entry.exitCode === 0 ? "is-ghost" : "is-danger", `exit code ${entry.exitCode}`);
|
|
47
|
+
};
|
|
48
|
+
/** The Containers panel: a table of the task's recorded worker containers (newest first) and the log viewer. */
|
|
49
|
+
export const renderContainersPanel = (entries, docker) => {
|
|
50
|
+
const rows = [...entries].reverse().map((entry) => {
|
|
51
|
+
const logButton = entry.local
|
|
52
|
+
? `<button type="button" class="button is-small" data-container-log="${escapeHtml(entry.assignmentId)}" data-container-name="${escapeHtml(entry.name)}" data-container-running="${entry.running ? "1" : "0"}" title="Show the raw container output">Log</button>`
|
|
53
|
+
: `<button type="button" class="button is-small" disabled title="log is on another host">Log</button>`;
|
|
54
|
+
return `<tr data-key="container:${escapeHtml(entry.name)}:${escapeHtml(String(entry.turn ?? ""))}">
|
|
55
|
+
<td><code title="${escapeHtml(entry.name)}">${escapeHtml(shortContainerName(entry.name))}</code>${entry.local || !entry.host ? "" : `<div class="is-size-7 has-text-grey" title="recorded on another host">${escapeHtml(entry.host)}</div>`}</td>
|
|
56
|
+
<td class="is-size-7">${escapeHtml(entry.workerId ?? "")}${entry.agent ? `<div class="has-text-grey">${escapeHtml(entry.agent)}</div>` : ""}</td>
|
|
57
|
+
<td><code>${escapeHtml(entry.itemId ?? "")}</code></td>
|
|
58
|
+
<td class="is-size-7">${entry.turn === undefined ? "" : escapeHtml(String(entry.turn))}</td>
|
|
59
|
+
<td>${containerStatusPill(entry)}</td>
|
|
60
|
+
<td class="is-size-7 has-text-grey">${relativeTime(entry.startedAt)}${entry.endedAt ? `<div title="duration">${escapeHtml(durationText(entry.startedAt, entry.endedAt))}</div>` : ""}</td>
|
|
61
|
+
<td>${logButton}</td>
|
|
62
|
+
</tr>`;
|
|
63
|
+
});
|
|
64
|
+
const table = rows.length
|
|
65
|
+
? `<table class="table ad-table is-fullwidth"><thead><tr>${["container", "worker", "item", "turn", "status", "started", ""].map((h) => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead><tbody>${rows.join("\n")}</tbody></table>`
|
|
66
|
+
: `<div class="ad-empty-line"><p>No worker containers recorded for this task</p><p class="is-size-7 mt-1">Containers appear here while the loop builds the task's work items; a task driven from the terminal without the loop never starts any.</p></div>`;
|
|
67
|
+
/* The viewer is client-owned (detail.js): live updates never morph inside it, so an open log survives polling. */
|
|
68
|
+
const viewer = `<section class="ad-terminal ad-container-log" id="container-log" data-live-ignore hidden>
|
|
69
|
+
<div class="ad-terminal-head"><strong>Container log</strong><span class="ad-log-state" data-container-log-name></span><span class="ad-log-state" data-container-log-state></span>
|
|
70
|
+
<div class="ad-terminal-tools"><button type="button" data-container-follow aria-pressed="true" title="Re-fetch the tail while the container runs">Follow</button><button type="button" data-container-close>Close</button></div>
|
|
71
|
+
</div>
|
|
72
|
+
<pre class="log-panel" data-container-log-body role="log" aria-live="off"></pre>
|
|
73
|
+
</section>`;
|
|
74
|
+
const tools = `<span class="ad-panel-tools">${pill(`docker · ${docker}`, docker === "ok" ? "is-ghost" : "is-warning", docker === "ok" ? "docker ps answers; running state is live" : "docker did not answer; running state follows the index only", false)}<span class="ad-count">${entries.length}</span></span>`;
|
|
75
|
+
return panel("Containers", table + viewer, tools, true);
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Task detail body shared by the full page (`detail` live region), the side
|
|
79
|
+
* panel (`fragment`, `drawer` live region), and `/api/live`. Undefined when the
|
|
80
|
+
* task does not exist. Keyed panels (`id`/`data-key`) let the client morph them
|
|
81
|
+
* in place: open artifacts, typed feedback, and the live log survive updates.
|
|
82
|
+
*/
|
|
83
|
+
export const renderTaskBody = async (options, state, id, fragment, maxAgeMs = 0) => {
|
|
84
|
+
const task = readUiTask(options, id, state);
|
|
85
|
+
const fleet = uiMode(options) === "fleet";
|
|
86
|
+
if (!task)
|
|
87
|
+
return undefined;
|
|
88
|
+
const waiting = task.status === "blocked_on_user" && task.gate !== "none";
|
|
89
|
+
// A gate whose structural checks now fail (e.g. an artifact edited after the
|
|
90
|
+
// wait was set) makes gateView throw; show the diagnostics instead of a 500.
|
|
91
|
+
let gateBlock = "";
|
|
92
|
+
// At code review the reviewer needs the diff: link the changes page from the gate panel.
|
|
93
|
+
const gateChanges = task.gate === "G2_codereview"
|
|
94
|
+
? `<p class="mt-3"><a class="button is-small is-link is-light" href="${changesHref(task.id)}" data-changes-link="gate">View code changes →</a></p>`
|
|
95
|
+
: "";
|
|
96
|
+
if (waiting) {
|
|
97
|
+
try {
|
|
98
|
+
const gate = await taskGatePayload(options, id, state, maxAgeMs);
|
|
99
|
+
if (gate.ok && gate.view)
|
|
100
|
+
gateBlock = renderGateViewHtml(gate.view, gateChanges);
|
|
101
|
+
else if (gate.ok && gate.snapshot)
|
|
102
|
+
gateBlock = `<section class="ad-panel is-warning" data-key="gate-view"><header class="ad-panel-head"><h2>${escapeHtml(gateLabel(task.gate))} · review snapshot</h2><span class="ad-pill is-ghost ad-panel-tools">${escapeHtml(gate.snapshot.id ?? "")}</span></header><div class="ad-panel-body"><div class="content artifact">${markdownToHtml(gate.snapshot.reviewText ?? "")}</div>${gateChanges}</div></section>`;
|
|
103
|
+
else if (fleet)
|
|
104
|
+
gateBlock = `<section class="ad-panel is-warning" data-key="gate-view"><header class="ad-panel-head"><h2>${escapeHtml(gateLabel(task.gate))}</h2></header><div class="ad-panel-body"><p>${escapeHtml(gate.error ?? "No review snapshot yet")}</p></div></section>`;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const problems = (error instanceof Error ? error.message : String(error)).split("; ").filter(Boolean);
|
|
108
|
+
gateBlock = `<section class="ad-panel is-danger" data-key="gate-view">
|
|
109
|
+
<header class="ad-panel-head"><span aria-hidden="true">⚠</span><h2>${escapeHtml(gateLabel(task.gate))} · gate check failing</h2><span class="ad-pill is-danger ad-panel-tools">${escapeHtml(task.gate)}</span></header>
|
|
110
|
+
<div class="ad-panel-body">
|
|
111
|
+
<p class="mb-3">The task waits at this gate, but its structural checks no longer pass. Approval will be rejected until these are fixed:</p>
|
|
112
|
+
<ul class="ad-plain-list">${problems.map((problem) => `<li>${pill("ERROR", "is-danger")} ${escapeHtml(problem)}</li>`).join("")}</ul>
|
|
113
|
+
<p class="is-size-7 has-text-grey mt-3">Re-check with <code>node .agents/aidlc/scripts/gate-check.mjs ${escapeHtml(task.id)} --gate ${escapeHtml(task.gate)}</code>.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</section>`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/* Pending feedback: `Cancelled gate wait` evidence with no later approval of
|
|
119
|
+
* that gate. While the task is active the lead is revising for it; while it
|
|
120
|
+
* waits again, the reviewer should check the revision reflects it. */
|
|
121
|
+
const pending = pendingFeedback(task);
|
|
122
|
+
const pendingBlock = !fragment && pending.length
|
|
123
|
+
? `<section class="ad-panel is-info" id="feedback-pending-panel">
|
|
124
|
+
<header class="ad-panel-head"><h2>Feedback pending</h2><span class="ad-pill is-info ad-panel-tools">${pending.length}</span></header>
|
|
125
|
+
<div class="ad-panel-body">
|
|
126
|
+
<p class="mb-2">${waiting
|
|
127
|
+
? "This gate was re-presented after human feedback. Check that the review artifact reflects it before approving; approving records the feedback as handled."
|
|
128
|
+
: "The task stays at its gate. The loop lead is revising the review artifact for this feedback and will re-present the same gate."}</p>
|
|
129
|
+
<ul class="ad-plain-list">${pending.map((entry) => `<li>${pill(gateLabel(entry.gate), "is-info", `gate: ${entry.gate}`, false)} ${escapeHtml(entry.text)} <span class="is-size-7 has-text-grey">· ${relativeTime(entry.recordedAt)}</span></li>`).join("")}</ul>
|
|
130
|
+
</div>
|
|
131
|
+
</section>`
|
|
132
|
+
: "";
|
|
133
|
+
/* Feedback never needs the fleet reviewer credential (it never approves), so
|
|
134
|
+
* the block renders in both modes whenever the task waits at a gate. */
|
|
135
|
+
const feedbackBlock = !fragment && waiting
|
|
136
|
+
? `<section class="ad-panel is-info" id="feedback-panel" data-gate="${escapeHtml(task.gate)}" data-gate-label="${escapeHtml(gateLabel(task.gate))}">
|
|
137
|
+
<header class="ad-panel-head"><h2>Feedback · ${escapeHtml(gateLabel(task.gate))}</h2><span class="ad-pill is-ghost ad-panel-tools">stays at this gate</span></header>
|
|
138
|
+
<div class="ad-panel-body">
|
|
139
|
+
<p class="mb-2">Not ready to approve? Send guidance back to the loop. The task stays at <strong>${escapeHtml(gateLabel(task.gate))}</strong>; the lead revises the review artifact and re-presents this gate.</p>
|
|
140
|
+
<div class="field">
|
|
141
|
+
<label class="label is-small" for="feedback">Feedback <span class="has-text-grey has-text-weight-normal">(required, sent to the loop lead)</span></label>
|
|
142
|
+
<div class="control"><textarea id="feedback" class="textarea" rows="3" placeholder="e.g. the scope misses the fleet path; split item B; the assumption about X is wrong"></textarea></div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="is-flex is-align-items-center" style="gap:0.75rem">
|
|
145
|
+
<button id="feedback-button" class="button is-info">✎ Send feedback (stay at gate)</button>
|
|
146
|
+
<span id="feedback-result" class="help m-0" role="status"></span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</section>`
|
|
150
|
+
: "";
|
|
151
|
+
const approveBlock = !fragment && waiting && fleet && !options.reviewerToken
|
|
152
|
+
? `<section class="ad-panel" id="approve-panel">
|
|
153
|
+
<header class="ad-panel-head"><h2>Approve · ${escapeHtml(gateLabel(task.gate))}</h2><span class="ad-pill is-ghost ad-panel-tools">${escapeHtml(task.gate)}</span></header>
|
|
154
|
+
<div class="ad-panel-body">
|
|
155
|
+
<p class="mb-2">This dashboard has no fleet reviewer credential. Approve from a reviewer terminal, or restart <code>aidlc-kit ui</code> with <code>AIDLC_FLEET_REVIEWER_TOKEN</code> or <code>--reviewer-token-file</code>. Feedback above needs no reviewer credential.</p>
|
|
156
|
+
<pre class="is-size-7">node .agents/aidlc/scripts/remote-state.mjs gate inbox
|
|
157
|
+
node .agents/aidlc/scripts/remote-state.mjs gate approve ${escapeHtml(task.id)} --snapshot <snapshot-id> --source "<explicit approval>"</pre>
|
|
158
|
+
</div>
|
|
159
|
+
</section>`
|
|
160
|
+
: !fragment && waiting
|
|
161
|
+
? `<section class="ad-panel is-success" id="approve-panel" data-gate="${escapeHtml(task.gate)}" data-gate-label="${escapeHtml(gateLabel(task.gate))}">
|
|
162
|
+
<header class="ad-panel-head"><h2>Approve · ${escapeHtml(gateLabel(task.gate))}</h2><span class="ad-pill is-ghost ad-panel-tools">${escapeHtml(task.gate)}</span></header>
|
|
163
|
+
<div class="ad-panel-body">
|
|
164
|
+
<p class="mb-2">Approving moves the task to the next phase. To request changes instead, use <strong>Feedback</strong> above — the note below is provenance only, not guidance for the loop.</p>
|
|
165
|
+
${pending.length ? `<p class="help is-warning mb-3" id="approve-pending-warning">⚠ ${pending.length} feedback item${pending.length === 1 ? "" : "s"} still pending — make sure the revised review addresses ${pending.length === 1 ? "it" : "them"} before approving.</p>` : ""}
|
|
166
|
+
<div class="field">
|
|
167
|
+
<label class="label is-small" for="source">Approval note <span class="has-text-grey has-text-weight-normal">(required, recorded as provenance)</span></label>
|
|
168
|
+
<div class="control"><textarea id="source" class="textarea" rows="2" placeholder="e.g. reviewed intent and scope — ok"></textarea></div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="is-flex is-align-items-center" style="gap:0.75rem">
|
|
171
|
+
<button id="approve-button" class="button is-success">✓ Approve</button>
|
|
172
|
+
<span id="approve-result" class="help m-0" role="status"></span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</section>`
|
|
176
|
+
: "";
|
|
177
|
+
/* Confirmation dialog: the only control that sends the approve request. */
|
|
178
|
+
const approveModal = !fragment && waiting && !(fleet && !options.reviewerToken)
|
|
179
|
+
? `<div id="approve-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="approve-modal-title" data-live-preserve="class">
|
|
180
|
+
<div class="modal-background" data-close></div>
|
|
181
|
+
<div class="modal-card">
|
|
182
|
+
<header class="modal-card-head">
|
|
183
|
+
<p class="modal-card-title" id="approve-modal-title">Approve ${escapeHtml(gateLabel(task.gate))}?</p>
|
|
184
|
+
<button class="delete" aria-label="close" data-close></button>
|
|
185
|
+
</header>
|
|
186
|
+
<section class="modal-card-body">
|
|
187
|
+
<p>This approves <strong>${escapeHtml(gateLabel(task.gate))}</strong> (<code>${escapeHtml(task.gate)}</code>) for <code>${escapeHtml(task.id)}</code> and moves the task to the next phase. It cannot be undone from the dashboard.</p>
|
|
188
|
+
<p class="mt-2">To request changes instead, cancel and use <strong>Feedback</strong> — the task then stays at this gate.</p>
|
|
189
|
+
<p class="is-size-7 has-text-grey mt-3 mb-1">Approval note</p>
|
|
190
|
+
<blockquote id="approve-modal-note" class="box is-size-7 py-2 px-3 mb-0"></blockquote>
|
|
191
|
+
</section>
|
|
192
|
+
<footer class="modal-card-foot">
|
|
193
|
+
<button class="button" data-close data-autofocus>Cancel</button>
|
|
194
|
+
<button id="approve-confirm" class="button is-success">✓ Confirm approve</button>
|
|
195
|
+
</footer>
|
|
196
|
+
</div>
|
|
197
|
+
</div>`
|
|
198
|
+
: "";
|
|
199
|
+
const openArtifact = waiting ? GATE_ARTIFACT[task.gate] : undefined;
|
|
200
|
+
const artifacts = ARTIFACT_NAMES.map((name) => {
|
|
201
|
+
const artifactPath = task.artifacts?.[name];
|
|
202
|
+
if (!artifactPath)
|
|
203
|
+
return "";
|
|
204
|
+
const payload = artifactPayload(options.root, id, name, task);
|
|
205
|
+
const content = payload.ok && payload.content !== undefined
|
|
206
|
+
? `<div class="content artifact">${markdownToHtml(payload.content)}</div>`
|
|
207
|
+
: `<p class="help is-danger px-5 pb-4">${escapeHtml(payload.error ?? "unavailable")}</p>`;
|
|
208
|
+
return `<details class="ad-artifact" data-key="artifact:${name}"${name === openArtifact ? " open" : ""}><summary>${escapeHtml(name)}<code>${escapeHtml(artifactPath)}</code></summary>${content}</details>`;
|
|
209
|
+
}).join("\n");
|
|
210
|
+
const table = (head, rows, empty) => rows.length
|
|
211
|
+
? `<table class="table ad-table is-fullwidth"><thead><tr>${head.map((h) => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead><tbody>${rows.join("\n")}</tbody></table>`
|
|
212
|
+
: `<p class="ad-empty-line">${escapeHtml(empty)}</p>`;
|
|
213
|
+
const decisions = table(["decision", "status", "resolution"], task.decisions.map((decision) => `<tr><td><code>${escapeHtml(decision.id)}</code><div class="is-size-7 has-text-grey mt-1">${escapeHtml(decision.label ?? "")}</div></td><td>${pill(decision.status, DECISION_TONE[decision.status] ?? "")}</td><td class="is-size-7">${escapeHtml(decision.resolution ?? "")}</td></tr>`), "No decisions recorded");
|
|
214
|
+
const doneItems = task.tasks.filter((item) => item.status === "done").length;
|
|
215
|
+
const workItems = table(["item", "status", "label"], task.tasks.map((item) => `<tr><td><code>${escapeHtml(item.id)}</code></td><td>${pill(item.status.replace("_", " "), ITEM_TONE[item.status] ?? "")}</td><td class="is-size-7">${escapeHtml(item.label ?? "")}</td></tr>`), "No work items yet");
|
|
216
|
+
const workProgress = task.tasks.length
|
|
217
|
+
? `<div class="px-5 pt-4 pb-1"><progress class="progress is-small is-primary mb-0" value="${doneItems}" max="${task.tasks.length}" aria-label="work items done">${doneItems}/${task.tasks.length}</progress></div>`
|
|
218
|
+
: "";
|
|
219
|
+
const evidence = table(["kind", "result", "gate", "source", "recorded"], task.evidence.map((entry) => `<tr><td><strong>${escapeHtml(entry.kind)}</strong>${entry.area ? `<div class="is-size-7 has-text-grey">${escapeHtml(entry.area)}</div>` : ""}</td><td>${pill(entry.result, RESULT_TONE[entry.result] ?? "")}</td><td class="is-size-7">${escapeHtml(entry.gate ?? "")}</td><td class="is-size-7">${escapeHtml(entry.source ?? "")}${entry.detail ? `<div class="has-text-grey mt-1">${escapeHtml(entry.detail)}</div>` : ""}</td><td class="is-size-7 has-text-grey">${relativeTime(entry.recordedAt)}</td></tr>`), "No verification evidence yet");
|
|
220
|
+
/* Worker containers (full page only): the task's runtime record, the same in every
|
|
221
|
+
* mode; `docker ps` only refines `running` and is shared across live polls. */
|
|
222
|
+
let containersPanel = "";
|
|
223
|
+
if (!fragment) {
|
|
224
|
+
const containers = await taskContainers(options, id);
|
|
225
|
+
containersPanel = containers.status === 200 ? renderContainersPanel(containers.payload.containers, containers.payload.docker) : renderContainersPanel([], "unavailable");
|
|
226
|
+
}
|
|
227
|
+
const handoff = task.handoff
|
|
228
|
+
? `<section class="ad-panel" data-key="handoff" style="box-shadow:0 0 0 1px var(--bulma-danger)"><header class="ad-panel-head"><h2>Handed off · ${escapeHtml(task.handoff.kind)}</h2></header><div class="ad-panel-body"><p>${escapeHtml(task.handoff.reason)}</p><p class="is-size-7 has-text-grey mt-2">Follow the handoff actions from <code>node .agents/aidlc/scripts/state.mjs task next ${escapeHtml(task.id)}</code>.</p></div></section>`
|
|
229
|
+
: "";
|
|
230
|
+
const meta = `<dl class="ad-meta">
|
|
231
|
+
<dt>Status</dt><dd>${statusPill(task.status, Boolean(task.handoff))}</dd>
|
|
232
|
+
<dt>Gate</dt><dd>${task.gate === "none" ? '<span class="has-text-grey">none</span>' : `${escapeHtml(gateLabel(task.gate))}<div class="is-size-7 has-text-grey">${escapeHtml(task.gate)}</div>`}</dd>
|
|
233
|
+
<dt>Risk</dt><dd>${riskPill(task.risk)}</dd>
|
|
234
|
+
<dt>Type</dt><dd>${escapeHtml(task.type)}</dd>
|
|
235
|
+
<dt>Language</dt><dd>${escapeHtml(task.language)}</dd>
|
|
236
|
+
<dt>Areas</dt><dd>${escapeHtml(task.areas.join(", "))}</dd>
|
|
237
|
+
<dt>Branch</dt><dd><code>${escapeHtml(task.branch)}</code></dd>
|
|
238
|
+
<dt>Created</dt><dd>${relativeTime(task.createdAt)}</dd>
|
|
239
|
+
<dt>Updated</dt><dd>${relativeTime(task.updatedAt)}</dd>
|
|
240
|
+
</dl>`;
|
|
241
|
+
/* Live log (assets/ui/live-log.js): client-owned SSE lines, so live updates never
|
|
242
|
+
* morph inside it ([data-live-ignore]). The full page gets the grouped log with its
|
|
243
|
+
* filter and Raw JSON switch; the side panel a compact tail of recent lines keyed by
|
|
244
|
+
* task, so switching tasks replaces it and a re-render keeps it. */
|
|
245
|
+
const logAttrs = (mode, limit) => `data-live-log data-log-mode="${mode}" data-log-limit="${limit}" data-task-id="${escapeHtml(task.id)}" data-gate-labels="${escapeHtml(JSON.stringify(GATE_LABELS))}"`;
|
|
246
|
+
const liveLog = fragment
|
|
247
|
+
? `<section class="ad-terminal ad-live-log is-compact" data-key="drawer-log:${escapeHtml(task.id)}" data-live-ignore ${logAttrs("compact", DRAWER_LOG_LINES)}>
|
|
248
|
+
<div class="ad-terminal-head"><strong>Live log</strong><span class="ad-log-state" data-log-state>connecting…</span>
|
|
249
|
+
<div class="ad-terminal-tools"><a href="/tasks/${encodeURIComponent(task.id)}#live-log" data-log-open>Open full log →</a></div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="log-panel is-compact" data-log-body role="log" aria-live="off"><span class="ad-log-muted">connecting…</span></div>
|
|
252
|
+
</section>`
|
|
253
|
+
: `<section class="ad-terminal ad-live-log" id="live-log" data-live-ignore ${logAttrs("full", FULL_LOG_LINES)}>
|
|
254
|
+
<div class="ad-terminal-head"><span class="ad-lights" aria-hidden="true"><span></span><span></span><span></span></span><strong>Live log</strong><span id="log-state" class="ad-log-state" data-log-state>connecting…</span>
|
|
255
|
+
<div class="ad-terminal-tools">
|
|
256
|
+
<span class="ad-log-switch" role="group" aria-label="Which events to show"><button type="button" data-log-filter="important" aria-pressed="true" title="Hide bookkeeping events; show the first line of agent messages">Important</button><button type="button" data-log-filter="all" aria-pressed="false" title="Every event, agent messages in full">All</button></span>
|
|
257
|
+
<button type="button" data-log-raw aria-pressed="false" title="Show each event's original JSON">Raw JSON</button>
|
|
258
|
+
<button type="button" id="log-follow" data-log-follow aria-pressed="true">Follow</button><button type="button" id="log-clear" data-log-clear>Clear</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div id="log" class="log-panel" data-log-body role="log" aria-live="off"><span class="ad-log-muted">connecting…</span></div>
|
|
262
|
+
</section>`;
|
|
263
|
+
/* The side panel stays read-only; when a human action is due, point at the
|
|
264
|
+
* full page that hosts it right at the top. */
|
|
265
|
+
const href = `/tasks/${encodeURIComponent(task.id)}`;
|
|
266
|
+
const drawerCta = !fragment ? ""
|
|
267
|
+
: task.handoff
|
|
268
|
+
? `<div class="ad-callout is-danger ad-drawer-cta" data-key="drawer-cta"><span>Handed off · ${escapeHtml(task.handoff.kind)} — follow the handoff actions on the full page.</span><a class="button is-small is-danger" href="${href}">Open task →</a></div>`
|
|
269
|
+
: waiting
|
|
270
|
+
? `<div class="ad-callout ad-drawer-cta" data-key="drawer-cta"><span>Waiting at <strong>${escapeHtml(gateLabel(task.gate))}</strong>${pending.length ? ` · ${pending.length} feedback pending` : ""} — send feedback or approve on the full page.</span><a class="button is-small is-warning" href="${href}" data-review-cta>Open review →</a></div>`
|
|
271
|
+
: "";
|
|
272
|
+
const body = `${drawerCta}<h1 class="ad-page-title">${escapeHtml(task.title)}</h1>
|
|
273
|
+
<div class="ad-page-sub">${copyIdButton(task.id)}${statusPill(task.status, Boolean(task.handoff))}${gatePill(task.gate)}${riskPill(task.risk)}${feedbackPill(pending.length)}</div>
|
|
274
|
+
${phaseStepper(task.phase, task.gate, task.status, Boolean(task.handoff))}
|
|
275
|
+
${fragment ? liveLog : ""}
|
|
276
|
+
<div class="columns is-desktop">
|
|
277
|
+
<div class="column is-8-desktop">
|
|
278
|
+
${handoff}
|
|
279
|
+
${pendingBlock}
|
|
280
|
+
${gateBlock}
|
|
281
|
+
${feedbackBlock}
|
|
282
|
+
${approveBlock}
|
|
283
|
+
${panel("Artifacts", artifacts || '<p class="ad-empty-line">No artifacts recorded</p>', `<span class="ad-count">${ARTIFACT_NAMES.filter((name) => task.artifacts?.[name]).length}</span>`, true)}
|
|
284
|
+
${panel("Decisions", decisions, `<span class="ad-count">${task.decisions.length}</span>`, true)}
|
|
285
|
+
${panel("Work items", workProgress + workItems, `<span class="ad-panel-tools"><a class="button is-small" href="${changesHref(task.id)}" data-changes-link="items" title="Code changes of this task">Changes</a><span class="ad-count">${doneItems}/${task.tasks.length}</span></span>`, true)}
|
|
286
|
+
${containersPanel}
|
|
287
|
+
${panel("Evidence", evidence, `<span class="ad-count">${task.evidence.length}</span>`, true)}
|
|
288
|
+
${fragment ? "" : liveLog}
|
|
289
|
+
</div>
|
|
290
|
+
<aside class="column is-4-desktop">
|
|
291
|
+
<div class="ad-sticky">
|
|
292
|
+
${panel("Details", meta)}
|
|
293
|
+
</div>
|
|
294
|
+
</aside>
|
|
295
|
+
</div>`;
|
|
296
|
+
return { task, html: fragment ? body : `${body}${approveModal}` };
|
|
297
|
+
};
|
|
298
|
+
export const taskNotFound = (options, id) => `<div class="ad-empty"><div class="ad-empty-icon" aria-hidden="true">🔍</div><h2>Task not found</h2><p><code>${escapeHtml(id)}</code> is not in the ${uiMode(options) === "fleet" ? "fleet" : "local"} canonical state.</p><a class="button" href="/">← Back to dashboard</a></div>`;
|
|
299
|
+
export const drawerNotFound = (id) => `<p class="has-text-danger">Task not found: ${escapeHtml(id)}</p>`;
|
|
300
|
+
export const renderTaskDetail = async (options, id, fragment = false) => {
|
|
301
|
+
const state = await readUiState(options);
|
|
302
|
+
const rendered = await renderTaskBody(options, state, id, fragment);
|
|
303
|
+
if (fragment)
|
|
304
|
+
return rendered ? rendered.html : drawerNotFound(id);
|
|
305
|
+
const reviews = reviewQueue(state);
|
|
306
|
+
if (!rendered)
|
|
307
|
+
return `${pageHead("Task not found · Loop")}${renderHeader(options.root, uiMode(options), reviews)}<main class="section ad-main"><div class="container is-fluid">${taskNotFound(options, id)}</div></main>${renderModals()}${pageScripts("common")}</body></html>`;
|
|
308
|
+
return `${pageHead(`${rendered.task.title} · Loop`, rendered.task.id)}
|
|
309
|
+
${renderHeader(options.root, uiMode(options), reviews, rendered.task.id)}
|
|
310
|
+
<main class="section ad-main">
|
|
311
|
+
<div class="container is-fluid">
|
|
312
|
+
<a class="ad-breadcrumb" href="/">← Dashboard</a>
|
|
313
|
+
${liveRegion("detail", rendered.html)}
|
|
314
|
+
</div>
|
|
315
|
+
</main>
|
|
316
|
+
${renderModals()}
|
|
317
|
+
${pageScripts("common", "detail", "live-log")}
|
|
318
|
+
</body>
|
|
319
|
+
</html>`;
|
|
320
|
+
};
|
|
@@ -12,7 +12,7 @@ const parseFrontmatter = (content) => {
|
|
|
12
12
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
13
13
|
if (!match)
|
|
14
14
|
return {};
|
|
15
|
-
return Object.fromEntries(match[1].split("\n").flatMap((line) => {
|
|
15
|
+
return Object.fromEntries((match[1] ?? "").split("\n").flatMap((line) => {
|
|
16
16
|
const separator = line.indexOf(":");
|
|
17
17
|
return separator < 0 ? [] : [[line.slice(0, separator).trim(), line.slice(separator + 1).trim().replace(/^['"]|['"]$/g, "")]];
|
|
18
18
|
}));
|
|
@@ -30,7 +30,7 @@ const legacyEvidence = (phase, recordedAt) => {
|
|
|
30
30
|
/** Maps one legacy BOARD row to a canonical task, copying its prose files into the stable artifact layout. */
|
|
31
31
|
const taskFromLegacyRow = (root, files, row, recordedAt) => {
|
|
32
32
|
const [id, title, phaseRaw, gateRaw, statusRaw, branch, areasRaw, doc] = row;
|
|
33
|
-
const oldDoc = safePath(root, doc);
|
|
33
|
+
const oldDoc = safePath(root, doc ?? "");
|
|
34
34
|
const source = existsSync(oldDoc) ? readFileSync(oldDoc, "utf8") : "";
|
|
35
35
|
const frontmatter = parseFrontmatter(source);
|
|
36
36
|
const stableDir = `.agents/data/tasks/${id}`;
|
|
@@ -38,21 +38,21 @@ const taskFromLegacyRow = (root, files, row, recordedAt) => {
|
|
|
38
38
|
const workplanPath = `${stableDir}/workplan.md`;
|
|
39
39
|
if (source)
|
|
40
40
|
files.push({ path: prosePath, owner: "aidlc-state", ownershipClass: "state", content: source });
|
|
41
|
-
const oldWorkplan = doc.replace(/\.md$/, ".workplan.md");
|
|
41
|
+
const oldWorkplan = (doc ?? "").replace(/\.md$/, ".workplan.md");
|
|
42
42
|
if (existsSync(safePath(root, oldWorkplan)))
|
|
43
43
|
files.push({ path: `${stableDir}/legacy-workplan.md`, owner: "aidlc-state", ownershipClass: "state", content: readFileSync(safePath(root, oldWorkplan), "utf8") });
|
|
44
|
-
const phase = ["clarify", "plan", "build", "wrap", "done"].includes(phaseRaw) ? phaseRaw : "clarify";
|
|
45
|
-
const gate = ["none", "G0_confirm", "G1_review", "G2_codereview"].includes(gateRaw) ? gateRaw : "G0_confirm";
|
|
44
|
+
const phase = (phaseRaw !== undefined && ["clarify", "plan", "build", "wrap", "done"].includes(phaseRaw) ? phaseRaw : "clarify");
|
|
45
|
+
const gate = (gateRaw !== undefined && ["none", "G0_confirm", "G1_review", "G2_codereview"].includes(gateRaw) ? gateRaw : "G0_confirm");
|
|
46
46
|
return {
|
|
47
|
-
id,
|
|
48
|
-
title,
|
|
49
|
-
type: (["feature", "bug", "refactor", "infra"].includes(frontmatter.type) ? frontmatter.type : "infra"),
|
|
47
|
+
id: id ?? "",
|
|
48
|
+
title: title ?? "",
|
|
49
|
+
type: (["feature", "bug", "refactor", "infra"].includes(frontmatter.type ?? "") ? frontmatter.type : "infra"),
|
|
50
50
|
phase,
|
|
51
51
|
gate,
|
|
52
|
-
status: (["active", "blocked_on_user", "paused", "done"].includes(statusRaw) ? statusRaw : "paused"),
|
|
52
|
+
status: (statusRaw !== undefined && ["active", "blocked_on_user", "paused", "done"].includes(statusRaw) ? statusRaw : "paused"),
|
|
53
53
|
language: frontmatter.language === "en" ? "en" : "vi",
|
|
54
54
|
risk: "normal",
|
|
55
|
-
areas: areasRaw.split(",").map((area) => area.trim()).filter(Boolean),
|
|
55
|
+
areas: (areasRaw ?? "").split(",").map((area) => area.trim()).filter(Boolean),
|
|
56
56
|
branch: branch || "—",
|
|
57
57
|
artifacts: { intent: source ? prosePath : undefined, design: source ? prosePath : undefined, workplan: workplanPath },
|
|
58
58
|
decisions: [],
|
package/dist/src/upgrade.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { LoopConfig, PlannedWrite } from "./model.js";
|
|
2
|
-
export { migrateLegacyBoard } from "./upgrade-board.js";
|
|
3
2
|
/** Plans an upgrade transaction from the installed manifest to the current package. */
|
|
4
3
|
export declare const planUpgrade: (root: string, loop?: LoopConfig) => PlannedWrite[];
|
|
5
4
|
export interface UpgradeResult {
|
package/dist/src/upgrade.js
CHANGED
|
@@ -1,95 +1,32 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { installedHashFor, managedContentHash, planLoopSelection, readManifest } from "./installer.js";
|
|
3
|
+
import { findAdapter, resolvedAdapters } from "./adapters.js";
|
|
5
4
|
import { legacyV021AdapterContents, legacyV021Hashes } from "./legacy.js";
|
|
6
5
|
import { planProjectLayoutMigration } from "./project-migration.js";
|
|
7
|
-
import {
|
|
6
|
+
import { legacyManifest, managedContentHash, planLoopSelection, planManagedWrites, preserveOwnedFile, readManifest, splitInventoryKey } from "./managed-planning.js";
|
|
8
7
|
import { resolveProjectPath, resolveProjectPathWithoutSymlinks } from "./project-path.js";
|
|
9
8
|
import { prepareV4Migration, validateState } from "./state.js";
|
|
10
|
-
import { contentHash, coreWorkflowFiles, initialProjectFiles, manifestSpec } from "./workflow.js";
|
|
9
|
+
import { contentHash, coreWorkflowFiles, initialProjectFiles, isExecutableAsset, manifestSpec } from "./workflow.js";
|
|
11
10
|
import { migrateLegacyBoard } from "./upgrade-board.js";
|
|
12
|
-
export { migrateLegacyBoard } from "./upgrade-board.js";
|
|
13
11
|
const safePath = (root, relative) => resolveProjectPath(root, relative, "Unsafe upgrade path");
|
|
14
12
|
const assertNoSymlinkPath = (root, relative) => { resolveProjectPathWithoutSymlinks(root, relative, "Unsafe upgrade path", "Refusing to upgrade symlink path"); };
|
|
15
|
-
const legacyManifest = (root) => {
|
|
16
|
-
const path = join(resolve(root), ".agents/aidlc/manifest.json");
|
|
17
|
-
if (!existsSync(path))
|
|
18
|
-
return false;
|
|
19
|
-
try {
|
|
20
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
21
|
-
return parsed.managedBy === "aidlc-workflow" && (parsed.schemaVersion === 0 || parsed.schemaVersion === 1);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return false;
|
|
25
|
-
} /* intentional: a malformed/missing manifest is not a legacy v0.x install */
|
|
26
|
-
};
|
|
27
13
|
/* --- Managed file planning -------------------------------------------------- */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
38
|
-
if (installedHash && currentHash === installedHash)
|
|
39
|
-
return { ...spec, content: next, action: "update", reason: "unchanged managed file" };
|
|
40
|
-
const legacyAdapter = legacyV021AdapterContents[spec.path];
|
|
41
|
-
const legacyAdapterHash = legacyAdapter ? contentHash(spec.strategy === "managed-block" ? legacyAdapter.trimEnd() : legacyAdapter) : undefined;
|
|
42
|
-
if (legacy && (legacyV021Hashes[spec.path] === currentHash || legacyAdapterHash === currentHash))
|
|
43
|
-
return { ...spec, content: next, action: "update", reason: "recognized v0.2.1 baseline" };
|
|
44
|
-
if (spec.strategy === "managed-block" && current.includes(`<!-- aidlc-installer:${spec.owner} -->`))
|
|
45
|
-
return { ...spec, content: next, action: "conflict", reason: "managed prompt block was modified" };
|
|
46
|
-
return { ...spec, content: next, action: "conflict", reason: legacy ? "legacy core differs from known baseline" : "managed file differs from installed hash" };
|
|
47
|
-
};
|
|
48
|
-
/** Merges a later owner of a shared managed-block path into the already-emitted plan entry. */
|
|
49
|
-
const mergeSharedBlockOwner = (project, previous, spec, installed) => {
|
|
50
|
-
if (!["create", "update", "skip"].includes(previous.action))
|
|
51
|
-
return;
|
|
52
|
-
const target = safePath(project, spec.path);
|
|
53
|
-
const current = existsSync(target) ? readFileSync(target, "utf8") : "";
|
|
54
|
-
// Every owner block is checked before merging so a user edit to a later
|
|
55
|
-
// adapter's block is reported instead of being silently overwritten.
|
|
56
|
-
const installedBlockHash = installedHashFor(installed?.files, spec);
|
|
57
|
-
const currentBlockHash = managedContentHash(spec, current);
|
|
58
|
-
if (installedBlockHash !== undefined && currentBlockHash !== undefined && installedBlockHash !== currentBlockHash) {
|
|
59
|
-
previous.action = "conflict";
|
|
60
|
-
previous.reason = "managed prompt block was modified";
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
previous.content = mergeManagedBlock(spec, previous.content);
|
|
64
|
-
previous.action = existsSync(target) ? (previous.content === current ? "skip" : "update") : "create";
|
|
65
|
-
previous.reason = "managed prompt block";
|
|
66
|
-
};
|
|
67
|
-
/** Plans managed replacements and shared managed-block writes across every owner. */
|
|
68
|
-
const planManagedFiles = (project, managed, installed, legacy) => {
|
|
69
|
-
const plan = [];
|
|
70
|
-
const mergedBlocks = new Map();
|
|
71
|
-
for (const spec of managed) {
|
|
72
|
-
const installedHash = installedHashFor(installed?.files, spec);
|
|
73
|
-
if (spec.strategy !== "managed-block") {
|
|
74
|
-
plan.push(planManaged(project, spec, installedHash, legacy));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const previous = mergedBlocks.get(spec.path);
|
|
78
|
-
if (!previous) {
|
|
79
|
-
const write = planManaged(project, spec, installedHash, legacy);
|
|
80
|
-
plan.push(write);
|
|
81
|
-
mergedBlocks.set(spec.path, write);
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
mergeSharedBlockOwner(project, previous, spec, installed);
|
|
14
|
+
/** Upgrade's planning policy: ownership is proven against installed hashes and the frozen v0.2.1 baselines. */
|
|
15
|
+
const upgradePlanPolicy = (installed, legacy) => ({
|
|
16
|
+
flow: "upgrade",
|
|
17
|
+
installed,
|
|
18
|
+
legacy,
|
|
19
|
+
resolvePath: safePath,
|
|
20
|
+
legacyBaseline: (spec, currentHash) => {
|
|
21
|
+
const legacyAdapter = legacyV021AdapterContents[spec.path];
|
|
22
|
+
const legacyAdapterHash = legacyAdapter ? contentHash(spec.strategy === "managed-block" ? legacyAdapter.trimEnd() : legacyAdapter) : undefined;
|
|
23
|
+
return legacyV021Hashes[spec.path] === currentHash || legacyAdapterHash === currentHash;
|
|
85
24
|
}
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
/* --- Obsolete files and project-owned files ---------------------------------- */
|
|
25
|
+
});
|
|
89
26
|
/** Deletes obsolete managed files tracked by the installed manifest that no longer ship in this release. */
|
|
90
27
|
const planObsoleteRemovals = (project, plan, installed, managed) => {
|
|
91
28
|
for (const [key, item] of Object.entries(installed.files)) {
|
|
92
|
-
const path = key
|
|
29
|
+
const path = splitInventoryKey(key).path;
|
|
93
30
|
// Shared prompt blocks are upgraded through the merged plan above; never delete their whole path here.
|
|
94
31
|
if (item.strategy === "managed-block")
|
|
95
32
|
continue;
|
|
@@ -107,8 +44,7 @@ const preserveProjectFiles = (project, plan, legacy) => {
|
|
|
107
44
|
for (const spec of initialProjectFiles()) {
|
|
108
45
|
if (legacy && spec.path === ".agents/data/state/aidlc-state.json" && !existsSync(safePath(project, spec.path)))
|
|
109
46
|
continue;
|
|
110
|
-
|
|
111
|
-
plan.push(existsSync(target) ? { ...spec, content: readFileSync(target, "utf8"), action: "preserve", reason: `${spec.ownershipClass}-owned file` } : { ...spec, action: "create", reason: `new ${spec.ownershipClass}-owned file` });
|
|
47
|
+
plan.push(preserveOwnedFile(safePath(project, spec.path), spec));
|
|
112
48
|
}
|
|
113
49
|
};
|
|
114
50
|
/* --- Canonical state and legacy BOARD migration -------------------------------- */
|
|
@@ -171,11 +107,11 @@ const removeLegacyBoard = (project, plan) => {
|
|
|
171
107
|
/* --- planUpgrade orchestration ------------------------------------------------ */
|
|
172
108
|
/** Selects which adapters to upgrade: manifest-registered ones, else auto-detected installable ones. */
|
|
173
109
|
const selectedUpgradeAgents = (project, installed) => {
|
|
174
|
-
const upgradeAdapters =
|
|
110
|
+
const upgradeAdapters = resolvedAdapters();
|
|
175
111
|
const requested = installed?.adapters?.length ? installed.adapters : upgradeAdapters.filter((adapter) => adapter.detect(project)).map((adapter) => adapter.id);
|
|
176
|
-
return requested.filter((id) =>
|
|
112
|
+
return requested.filter((id) => findAdapter(id));
|
|
177
113
|
};
|
|
178
|
-
const agentUpgradeFiles = (selected) => selected.flatMap((id) =>
|
|
114
|
+
const agentUpgradeFiles = (selected) => selected.flatMap((id) => findAdapter(id)?.files() ?? []).map((file) => ({ ...file, ownershipClass: "managed" }));
|
|
179
115
|
/** Plans an upgrade transaction from the installed manifest to the current package. */
|
|
180
116
|
export const planUpgrade = (root, loop) => {
|
|
181
117
|
const project = resolve(root);
|
|
@@ -185,7 +121,7 @@ export const planUpgrade = (root, loop) => {
|
|
|
185
121
|
throw new Error("No owned AI-DLC installation found. Use init instead of upgrade.");
|
|
186
122
|
const selected = selectedUpgradeAgents(project, installed);
|
|
187
123
|
const managed = [...coreWorkflowFiles(), ...agentUpgradeFiles(selected)];
|
|
188
|
-
const plan =
|
|
124
|
+
const plan = planManagedWrites(project, managed, upgradePlanPolicy(installed, legacy));
|
|
189
125
|
if (installed)
|
|
190
126
|
planObsoleteRemovals(project, plan, installed, managed);
|
|
191
127
|
preserveProjectFiles(project, plan, legacy);
|
|
@@ -215,6 +151,9 @@ const stageUpgradedWrites = (project, writes, backup, stage, journal) => {
|
|
|
215
151
|
const staged = join(stage, item.path);
|
|
216
152
|
mkdirSync(dirname(staged), { recursive: true });
|
|
217
153
|
writeFileSync(staged, item.contentEncoding === "base64" ? Buffer.from(item.content, "base64") : item.content);
|
|
154
|
+
// The commit step renames the staged file, so its mode carries over to the target.
|
|
155
|
+
if (isExecutableAsset(item.path) && process.platform !== "win32")
|
|
156
|
+
chmodSync(staged, 0o755);
|
|
218
157
|
if (item.path.endsWith(".json") && item.contentEncoding !== "base64")
|
|
219
158
|
JSON.parse(item.content);
|
|
220
159
|
}
|
package/dist/src/workflow.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AgentId, FileSpec, WorkflowManifest } from "./model.js";
|
|
2
2
|
export declare const contentHash: (content: string | Uint8Array) => string;
|
|
3
3
|
export declare const coreWorkflowFiles: () => FileSpec[];
|
|
4
|
+
/** Commented template for the loop daemon's env file; seeded once and never overwritten. */
|
|
5
|
+
export declare const LOOP_ENV_TEMPLATE = "# AI-DLC loop environment, read by `loop.sh up` / `aidlc-kit loop start` (local mode).\n# Precedence: shell exports > this file > values detected from your tools' own config.\n# Keep real keys here only; .agents/.gitignore excludes this file from git.\n# See what the loop will use with: .agents/aidlc/scripts/loop.sh check\n#\n# --- OpenCode (lead or workers set to \"opencode\") ---\n# Model as provider/model[#variant] (list them with: opencode models).\n# When unset, \"model\" from opencode.json is used.\n# AIDLC_OPENCODE_MODEL=opencode-go/<model>\n# Worker key; when unset it is read from ~/.local/share/opencode/auth.json.\n# The key follows the model prefix: openai/ -> OPENAI_API_KEY, anthropic/ -> ANTHROPIC_API_KEY, otherwise OPENCODE_API_KEY.\n# OPENCODE_API_KEY=\n#\n# --- Codex workers ---\n# When unset, read from ~/.codex/auth.json (after: codex login --with-api-key).\n# OPENAI_API_KEY=\n#\n# --- Claude Code workers ---\n# When unset, read from \"env\" in .claude/settings.local.json, .claude/settings.json, or ~/.claude/settings.json.\n# ANTHROPIC_API_KEY=\n#\n# --- Optional ---\n# Override the auto-built worker image:\n# AIDLC_WORKER_DOCKER_IMAGE=\n# Where worker worktrees live (default: the system temp directory):\n# AIDLC_LOCAL_WORKTREES_ROOT=\n";
|
|
6
|
+
/** Installed files that must be executable (POSIX launchers). */
|
|
7
|
+
export declare const isExecutableAsset: (path: string) => boolean;
|
|
4
8
|
export declare const initialProjectFiles: () => FileSpec[];
|
|
5
9
|
export declare const manifestKey: (spec: Pick<FileSpec, "path" | "owner" | "strategy">) => string;
|
|
6
10
|
export declare const manifestFor: (files: FileSpec[], adapters: AgentId[]) => WorkflowManifest;
|