cool-workflow 0.2.4 → 0.2.5
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +202 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +124 -13
- package/dist/mcp-server.js +20 -0
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +120 -63
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +195 -23
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +32 -0
- package/dist/shell/fs-atomic.js +93 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/multi-agent-cli.js +5 -1
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/pipeline.js +2 -1
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/run-export.js +52 -4
- package/dist/shell/run-store.js +156 -0
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +104 -10
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +79 -5
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +15 -0
- package/docs/cli-mcp-parity.7.md +25 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +36 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +6 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +39 -9
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +10 -4
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +24 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +4 -2
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +128 -30
- package/scripts/release-gate.js +184 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +41 -9
- package/ui/workbench/app.js +160 -27
- package/ui/workbench/index.html +2 -2
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/ui/workbench/app.js
CHANGED
|
@@ -15,7 +15,30 @@ const PANEL_GROUPS = [
|
|
|
15
15
|
{ key: "collaboration", label: "Review & collaboration", panels: ["review", "comments"] }
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// `indexSeq` is a request sequence number: the debounced filter input can
|
|
19
|
+
// start a second /api/index fetch while an older one is still in flight, and
|
|
20
|
+
// only the NEWEST request may render (an old slow response must not
|
|
21
|
+
// overwrite a new fast one). `viewFetchedAt` is when the active run's view
|
|
22
|
+
// was fetched, shown as "as of HH:MM:SS" in the detail header.
|
|
23
|
+
const state = { activeRunId: null, activeTab: "graph", indexSeq: 0, viewFetchedAt: null };
|
|
24
|
+
|
|
25
|
+
// The host's auth token, read ONCE at startup from the page URL. When
|
|
26
|
+
// CW_WORKBENCH_TOKEN is set on the host, every /api/* request must carry it;
|
|
27
|
+
// the static UI files themselves are served without it, so the page loads
|
|
28
|
+
// and can explain what to do instead of rendering broken.
|
|
29
|
+
const TOKEN = new URLSearchParams(location.search).get("token") || "";
|
|
30
|
+
|
|
31
|
+
// Build a request URL with URLSearchParams so the token composes with any
|
|
32
|
+
// other query params (e.g. the index filter's ?text=).
|
|
33
|
+
function apiUrl(pathname, params = {}) {
|
|
34
|
+
const search = new URLSearchParams();
|
|
35
|
+
for (const [key, value] of Object.entries(params)) {
|
|
36
|
+
if (value) search.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
if (TOKEN) search.set("token", TOKEN);
|
|
39
|
+
const query = search.toString();
|
|
40
|
+
return query ? `${pathname}?${query}` : pathname;
|
|
41
|
+
}
|
|
19
42
|
|
|
20
43
|
async function getJson(url) {
|
|
21
44
|
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
@@ -24,7 +47,10 @@ async function getJson(url) {
|
|
|
24
47
|
try {
|
|
25
48
|
body = JSON.parse(text);
|
|
26
49
|
} catch {
|
|
27
|
-
throw new Error(`non-JSON response (${res.status})`);
|
|
50
|
+
throw new Error(`non-JSON response (${res.status}): ${text.slice(0, 120)}`);
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 401) {
|
|
53
|
+
throw new Error("unauthorized — reopen as /?token=<your CW_WORKBENCH_TOKEN value>");
|
|
28
54
|
}
|
|
29
55
|
if (!res.ok) throw new Error(body && body.error ? body.error : `HTTP ${res.status}`);
|
|
30
56
|
return body;
|
|
@@ -43,34 +69,71 @@ function el(tag, attrs = {}, children = []) {
|
|
|
43
69
|
return node;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
|
-
function freshnessBadge(value) {
|
|
72
|
+
function freshnessBadge(value, title) {
|
|
47
73
|
const v = String(value || "").toLowerCase();
|
|
48
|
-
|
|
74
|
+
const attrs = { class: `badge ${v || "absent"}`, text: value || "unknown" };
|
|
75
|
+
if (title) attrs.title = title;
|
|
76
|
+
return el("span", attrs);
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
async function loadIndex() {
|
|
80
|
+
const seq = ++state.indexSeq;
|
|
52
81
|
const filter = document.getElementById("filter").value.trim();
|
|
53
82
|
const list = document.getElementById("run-list");
|
|
83
|
+
// A loading placeholder, not a bare wipe: the same pattern the detail
|
|
84
|
+
// pane uses, so the sidebar never flashes empty while the fetch runs.
|
|
54
85
|
list.innerHTML = "";
|
|
86
|
+
list.appendChild(el("li", { class: "muted", text: "loading runs…" }));
|
|
55
87
|
let view;
|
|
56
88
|
try {
|
|
57
|
-
view = await getJson(
|
|
89
|
+
view = await getJson(apiUrl("/api/index", { text: filter }));
|
|
58
90
|
} catch (error) {
|
|
91
|
+
if (seq !== state.indexSeq) return;
|
|
92
|
+
list.innerHTML = "";
|
|
59
93
|
list.appendChild(el("li", { class: "err", text: `failed to load index: ${error.message}` }));
|
|
94
|
+
// Clear the stale "registry valid · scope …" line — leaving the last
|
|
95
|
+
// successful freshness next to a load error is itself a stale badge on
|
|
96
|
+
// the one panel whose whole point is freshness.
|
|
97
|
+
const fresh = document.getElementById("registry-freshness");
|
|
98
|
+
fresh.innerHTML = "";
|
|
99
|
+
fresh.append("registry ", freshnessBadge("unavailable"), " · index unreachable");
|
|
60
100
|
return;
|
|
61
101
|
}
|
|
102
|
+
// Only the newest request may render (see state.indexSeq).
|
|
103
|
+
if (seq !== state.indexSeq) return;
|
|
104
|
+
list.innerHTML = "";
|
|
62
105
|
const reg = view.registry || {};
|
|
63
106
|
const fresh = document.getElementById("registry-freshness");
|
|
64
107
|
fresh.innerHTML = "";
|
|
65
|
-
|
|
66
|
-
const
|
|
108
|
+
const regStatus = reg.freshness && reg.freshness.status;
|
|
109
|
+
const regTitle = String(regStatus || "").toLowerCase() === "absent"
|
|
110
|
+
? "no home registry data yet — runs made in this repo still show; `cw registry refresh` builds it"
|
|
111
|
+
: undefined;
|
|
112
|
+
fresh.append("registry ", freshnessBadge(regStatus, regTitle), ` · scope ${view.scope}`);
|
|
113
|
+
const runs = view.runs || {};
|
|
114
|
+
const records = runs.records || [];
|
|
67
115
|
if (!records.length) {
|
|
68
|
-
list.appendChild(el("li", { class: "muted",
|
|
116
|
+
list.appendChild(el("li", { class: "muted" }, [
|
|
117
|
+
el("div", { text: "no runs indexed in this scope" }),
|
|
118
|
+
el("div", { class: "hint", text: 'create one with: cw quickstart <app> --repo <path> --question "..."' })
|
|
119
|
+
]));
|
|
69
120
|
return;
|
|
70
121
|
}
|
|
71
|
-
|
|
122
|
+
// Tell the user when the page is only part of the run set (the server
|
|
123
|
+
// returns the newest page but caps it) — otherwise a scope with more runs
|
|
124
|
+
// than the page size silently hides the rest, and someone checking "did my
|
|
125
|
+
// run get created" is misled into thinking it doesn't exist.
|
|
126
|
+
if (typeof runs.total === "number" && runs.total > records.length) {
|
|
127
|
+
list.appendChild(el("li", { class: "muted hint", text: `showing latest ${records.length} of ${runs.total} runs` }));
|
|
128
|
+
}
|
|
129
|
+
// The server returns the page sorted oldest-first; show newest at the top.
|
|
130
|
+
for (const record of [...records].reverse()) {
|
|
72
131
|
const lifecycle = record.lifecycle || record.status || "";
|
|
73
|
-
|
|
132
|
+
// A real <button>, not a bare <li> with a click listener: Tab reaches
|
|
133
|
+
// it, Enter/Space activate it, and it gets a focus ring for free — no
|
|
134
|
+
// extra ARIA needed. Same CSS classes as before so the row/card look
|
|
135
|
+
// is unchanged (see app.css .run-list button rules).
|
|
136
|
+
const btn = el("button", { type: "button", "data-runid": record.runId, class: state.activeRunId === record.runId ? "active" : "" }, [
|
|
74
137
|
el("div", { class: "rid" }, [
|
|
75
138
|
el("span", { class: `status-dot ${lifecycle}`, title: lifecycle || "unknown" }),
|
|
76
139
|
document.createTextNode(record.runId)
|
|
@@ -80,28 +143,61 @@ async function loadIndex() {
|
|
|
80
143
|
text: [record.appId || record.workflowId, lifecycle, record.repo].filter(Boolean).join(" · ")
|
|
81
144
|
})
|
|
82
145
|
]);
|
|
83
|
-
|
|
84
|
-
list.appendChild(
|
|
146
|
+
btn.addEventListener("click", () => selectRun(record.runId));
|
|
147
|
+
list.appendChild(btn);
|
|
85
148
|
}
|
|
86
149
|
}
|
|
87
150
|
|
|
88
|
-
|
|
151
|
+
// Move the "active" highlight to the chosen run's button WITHOUT rebuilding
|
|
152
|
+
// the list. Rebuilding (the old `loadIndex()` call in selectRun) removed the
|
|
153
|
+
// very <button> the user had just clicked/Entered, dropping keyboard focus to
|
|
154
|
+
// <body> and flashing the whole sidebar on every selection.
|
|
155
|
+
function markActiveRow(runId) {
|
|
156
|
+
const list = document.getElementById("run-list");
|
|
157
|
+
for (const btn of list.querySelectorAll("button[data-runid]")) {
|
|
158
|
+
btn.classList.toggle("active", btn.getAttribute("data-runid") === runId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function selectRun(runId) {
|
|
89
163
|
state.activeRunId = runId;
|
|
90
|
-
|
|
164
|
+
markActiveRow(runId);
|
|
165
|
+
return loadRunDetail(runId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function loadRunDetail(runId) {
|
|
91
169
|
const detail = document.getElementById("run-panel");
|
|
92
170
|
detail.innerHTML = "";
|
|
93
171
|
detail.appendChild(el("p", { class: "muted", text: `loading ${runId}…` }));
|
|
94
172
|
let view;
|
|
95
173
|
try {
|
|
96
|
-
view = await getJson(`/api/run/${encodeURIComponent(runId)}`);
|
|
174
|
+
view = await getJson(apiUrl(`/api/run/${encodeURIComponent(runId)}`));
|
|
97
175
|
} catch (error) {
|
|
176
|
+
if (state.activeRunId !== runId) return;
|
|
98
177
|
detail.innerHTML = "";
|
|
99
178
|
detail.appendChild(el("p", { class: "err", text: `failed to load run: ${error.message}` }));
|
|
100
179
|
return;
|
|
101
180
|
}
|
|
181
|
+
// The user may have clicked another run while this fetch was in flight;
|
|
182
|
+
// a stale response must not overwrite the newer selection's render.
|
|
183
|
+
if (state.activeRunId !== runId) return;
|
|
184
|
+
state.viewFetchedAt = new Date();
|
|
102
185
|
renderRun(view);
|
|
103
186
|
}
|
|
104
187
|
|
|
188
|
+
// The refresh button re-derives BOTH panes from disk: the sidebar index AND
|
|
189
|
+
// the currently-open run's detail. Refreshing only the index (the old
|
|
190
|
+
// behavior) left the detail header's lifecycle badge showing stale state
|
|
191
|
+
// while the sidebar dot next to it had already changed.
|
|
192
|
+
function refreshAll() {
|
|
193
|
+
loadIndex();
|
|
194
|
+
if (state.activeRunId) loadRunDetail(state.activeRunId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatClock(date) {
|
|
198
|
+
return date ? date.toTimeString().slice(0, 8) : "";
|
|
199
|
+
}
|
|
200
|
+
|
|
105
201
|
function renderRun(view) {
|
|
106
202
|
const detail = document.getElementById("run-panel");
|
|
107
203
|
detail.innerHTML = "";
|
|
@@ -109,12 +205,29 @@ function renderRun(view) {
|
|
|
109
205
|
el("span", {}, [el("b", { text: "run " }), document.createTextNode(view.runId)]),
|
|
110
206
|
el("span", {}, [document.createTextNode("resolved "), freshnessBadge(view.resolved ? "valid" : "missing")])
|
|
111
207
|
]);
|
|
208
|
+
if (view.lifecycle) {
|
|
209
|
+
header.appendChild(el("span", {}, [document.createTextNode("lifecycle "), freshnessBadge(view.lifecycle)]));
|
|
210
|
+
}
|
|
211
|
+
if (state.viewFetchedAt) {
|
|
212
|
+
header.appendChild(el("span", { class: "muted", text: `as of ${formatClock(state.viewFetchedAt)}` }));
|
|
213
|
+
}
|
|
112
214
|
if (view.error) header.appendChild(el("span", { class: "err", text: view.error }));
|
|
113
215
|
detail.appendChild(header);
|
|
216
|
+
if (view.lifecycle === "blocked" || view.lifecycle === "failed") {
|
|
217
|
+
detail.appendChild(
|
|
218
|
+
el("p", { class: "recovery-hint", text: `${view.lifecycle} — run 'cw run status ${view.runId}' or 'cw doctor' for next steps` })
|
|
219
|
+
);
|
|
220
|
+
}
|
|
114
221
|
|
|
115
|
-
const tabs = el("div", { class: "tabs" });
|
|
222
|
+
const tabs = el("div", { class: "tabs", role: "tablist" });
|
|
116
223
|
for (const group of PANEL_GROUPS) {
|
|
117
|
-
const
|
|
224
|
+
const active = state.activeTab === group.key;
|
|
225
|
+
const btn = el("button", {
|
|
226
|
+
class: `tab ${active ? "active" : ""}`,
|
|
227
|
+
text: group.label,
|
|
228
|
+
role: "tab",
|
|
229
|
+
"aria-selected": active ? "true" : "false"
|
|
230
|
+
});
|
|
118
231
|
btn.addEventListener("click", () => {
|
|
119
232
|
state.activeTab = group.key;
|
|
120
233
|
renderRun(view);
|
|
@@ -125,9 +238,21 @@ function renderRun(view) {
|
|
|
125
238
|
|
|
126
239
|
const group = PANEL_GROUPS.find((g) => g.key === state.activeTab) || PANEL_GROUPS[0];
|
|
127
240
|
const panels = (view.panels && view.panels[group.key]) || {};
|
|
241
|
+
// Some groups declare two panel names that map to the same capability on
|
|
242
|
+
// the server (e.g. graph's compact/criticalPath both come from
|
|
243
|
+
// summary.show), so they carry byte-identical payloads. Render such a pair
|
|
244
|
+
// ONCE under a merged label instead of showing the same JSON twice under
|
|
245
|
+
// two names that promise distinct views.
|
|
246
|
+
const rendered = [];
|
|
128
247
|
for (const name of group.panels) {
|
|
129
248
|
const panel = panels[name];
|
|
130
|
-
if (panel)
|
|
249
|
+
if (!panel) continue;
|
|
250
|
+
const twin = rendered.find((entry) => JSON.stringify(entry.panel) === JSON.stringify(panel));
|
|
251
|
+
if (twin) twin.names.push(name);
|
|
252
|
+
else rendered.push({ names: [name], panel });
|
|
253
|
+
}
|
|
254
|
+
for (const entry of rendered) {
|
|
255
|
+
detail.appendChild(renderPanel(entry.names.join(" / "), entry.panel));
|
|
131
256
|
}
|
|
132
257
|
}
|
|
133
258
|
|
|
@@ -173,17 +298,27 @@ function renderStructured(data) {
|
|
|
173
298
|
return null;
|
|
174
299
|
}
|
|
175
300
|
|
|
301
|
+
// A semantically-correct data table: header cells in a <thead> with
|
|
302
|
+
// scope="col", body rows in a <tbody> — screen readers can then associate
|
|
303
|
+
// each cell with its column header.
|
|
304
|
+
function structTable(headers) {
|
|
305
|
+
const table = el("table", { class: "struct-table" }, [
|
|
306
|
+
el("thead", {}, [el("tr", {}, headers.map((h) => el("th", { scope: "col", text: h })))])
|
|
307
|
+
]);
|
|
308
|
+
const tbody = el("tbody");
|
|
309
|
+
table.appendChild(tbody);
|
|
310
|
+
return { table, tbody };
|
|
311
|
+
}
|
|
312
|
+
|
|
176
313
|
function renderGraph(data) {
|
|
177
314
|
const wrap = el("div");
|
|
178
315
|
const nodesBlock = el("div", { class: "struct-block" }, [el("div", { class: "struct-title", text: `nodes (${data.nodes.length})` })]);
|
|
179
316
|
if (data.nodes.length === 0) {
|
|
180
317
|
nodesBlock.appendChild(el("div", { class: "struct-empty", text: "none" }));
|
|
181
318
|
} else {
|
|
182
|
-
const table =
|
|
183
|
-
el("tr", {}, ["id", "kind", "status", "label"].map((h) => el("th", { text: h })))
|
|
184
|
-
]);
|
|
319
|
+
const { table, tbody } = structTable(["id", "kind", "status", "label"]);
|
|
185
320
|
for (const node of data.nodes) {
|
|
186
|
-
|
|
321
|
+
tbody.appendChild(
|
|
187
322
|
el("tr", {}, [
|
|
188
323
|
el("td", { text: node.id }),
|
|
189
324
|
el("td", { text: node.kind }),
|
|
@@ -235,11 +370,9 @@ function renderEventGroups(data, confirmedEventKeys) {
|
|
|
235
370
|
for (const key of eventKeys) {
|
|
236
371
|
const events = [...data[key]].sort((a, b) => String(a.createdAt || "").localeCompare(String(b.createdAt || "")));
|
|
237
372
|
const block = el("div", { class: "struct-block" }, [el("div", { class: "struct-title", text: `${humanizeKey(key)} (${events.length})` })]);
|
|
238
|
-
const table =
|
|
239
|
-
el("tr", {}, ["time", "kind", "decision", "source", "actor"].map((h) => el("th", { text: h })))
|
|
240
|
-
]);
|
|
373
|
+
const { table, tbody } = structTable(["time", "kind", "decision", "source", "actor"]);
|
|
241
374
|
for (const event of events) {
|
|
242
|
-
|
|
375
|
+
tbody.appendChild(
|
|
243
376
|
el("tr", {}, [
|
|
244
377
|
el("td", { text: event.createdAt || "" }),
|
|
245
378
|
el("td", { text: event.kind || "" }),
|
|
@@ -263,7 +396,7 @@ function renderEventGroups(data, confirmedEventKeys) {
|
|
|
263
396
|
return wrap;
|
|
264
397
|
}
|
|
265
398
|
|
|
266
|
-
document.getElementById("refresh").addEventListener("click",
|
|
399
|
+
document.getElementById("refresh").addEventListener("click", refreshAll);
|
|
267
400
|
document.getElementById("filter").addEventListener("input", debounce(loadIndex, 200));
|
|
268
401
|
|
|
269
402
|
function debounce(fn, ms) {
|
package/ui/workbench/index.html
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
<main>
|
|
17
17
|
<section id="index-panel" class="sidebar">
|
|
18
18
|
<div class="bar">
|
|
19
|
-
<
|
|
19
|
+
<h2>Runs</h2>
|
|
20
20
|
<button id="refresh" title="Re-derive from disk">refresh</button>
|
|
21
21
|
</div>
|
|
22
|
-
<input id="filter" type="search" placeholder="filter runs (app/status/text)" />
|
|
22
|
+
<input id="filter" type="search" placeholder="filter runs (app/status/text)" aria-label="filter runs" />
|
|
23
23
|
<div id="registry-freshness" class="freshness"></div>
|
|
24
24
|
<ul id="run-list" class="run-list"></ul>
|
|
25
25
|
</section>
|
package/docs/agent-framework.md
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# Workflow App framework
|
|
2
|
-
|
|
3
|
-
CW is made as an independent agent workflow control-plane.
|
|
4
|
-
|
|
5
|
-
The aim is to make agent development feel like building inside a platform
|
|
6
|
-
ecosystem. CW gives the runtime, contracts, storage, CLI, MCP bridge, and
|
|
7
|
-
package structure. Developers make workflow apps that keep to those contracts.
|
|
8
|
-
|
|
9
|
-
The framework is guided by five useful systems rules: small kernel, explicit
|
|
10
|
-
state, pipes you can put together, separate workers, and commits that a verifier
|
|
11
|
-
lets through. See
|
|
12
|
-
[unix-principles.md](unix-principles.md).
|
|
13
|
-
|
|
14
|
-
## Platform Contract
|
|
15
|
-
|
|
16
|
-
Every CW workflow keeps to this loop:
|
|
17
|
-
|
|
18
|
-
```text
|
|
19
|
-
interpret -> act -> observe -> adjust -> checkpoint
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
The loop maps to real framework operations:
|
|
23
|
-
|
|
24
|
-
| Loop stage | framework operation | Responsibility |
|
|
25
|
-
| --- | --- | --- |
|
|
26
|
-
| Interpret | `plan()` | Load workflow, check inputs, make tasks |
|
|
27
|
-
| Act | `dispatch()` | Move tasks that can run from pending to running |
|
|
28
|
-
| Observe | `recordResult()` | Read Markdown/JSON-RPC result evidence |
|
|
29
|
-
| Adjust | verifier gates | Check evidence and pick the next phase |
|
|
30
|
-
| Checkpoint | `commitState()` | Take a snapshot of state after important changes |
|
|
31
|
-
|
|
32
|
-
The v0.1.12 operator UX layer makes read-only summaries over run state:
|
|
33
|
-
human `status`, graph maps, report summaries, resource summaries, and
|
|
34
|
-
fixed next-step suggestions. Scripts can go on using `--json` or
|
|
35
|
-
`--format json`.
|
|
36
|
-
|
|
37
|
-
The v0.1.13 MCP app surface gives the same runtime operations to agent hosts
|
|
38
|
-
with stable JSON tools: app run, dispatch, worker inspection/output, candidate
|
|
39
|
-
scoring/selection, sandbox profile resolution, verifier-gated commit, and
|
|
40
|
-
operator status/graph/report summaries.
|
|
41
|
-
|
|
42
|
-
The v0.1.13 canonical app matrix checks and plans the kept userland
|
|
43
|
-
apps with public CLI commands:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npm run canonical-apps
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The golden path runs the full integration chain from start to end:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
npm run golden-path
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
It checks an app, plans a run, dispatches a readonly worker, takes a
|
|
56
|
-
worker-local `cw:result`, scores and picks a candidate, makes a
|
|
57
|
-
verifier-gated commit, and makes a report. See
|
|
58
|
-
[end-to-end-golden-path.7.md](end-to-end-golden-path.7.md).
|
|
59
|
-
|
|
60
|
-
## Developer Contract
|
|
61
|
-
|
|
62
|
-
A workflow app defines:
|
|
63
|
-
|
|
64
|
-
- `id`, `title`, and `summary`
|
|
65
|
-
- `schemaVersion`, app `version`, compatibility, and metadata when using the
|
|
66
|
-
first-class Workflow App framework contract
|
|
67
|
-
- needed and repeated inputs
|
|
68
|
-
- phase order
|
|
69
|
-
- agent tasks
|
|
70
|
-
- artifact tasks
|
|
71
|
-
- concurrency limits
|
|
72
|
-
- evidence requirements
|
|
73
|
-
- sandbox profile hints
|
|
74
|
-
|
|
75
|
-
Example:
|
|
76
|
-
|
|
77
|
-
```js
|
|
78
|
-
const {
|
|
79
|
-
defineWorkflowApp,
|
|
80
|
-
workflow,
|
|
81
|
-
phase,
|
|
82
|
-
agent,
|
|
83
|
-
artifact,
|
|
84
|
-
input
|
|
85
|
-
} = require("../dist/workflow-app-framework");
|
|
86
|
-
|
|
87
|
-
const inputs = [input("repo", { type: "path", required: true })];
|
|
88
|
-
|
|
89
|
-
module.exports = defineWorkflowApp({
|
|
90
|
-
schemaVersion: 1,
|
|
91
|
-
id: "example-review",
|
|
92
|
-
title: "Example Review",
|
|
93
|
-
summary: "Review a repository with evidence.",
|
|
94
|
-
version: "0.1.0",
|
|
95
|
-
inputs,
|
|
96
|
-
sandboxProfiles: ["readonly"],
|
|
97
|
-
compatibility: {
|
|
98
|
-
minVersion: "0.1.9"
|
|
99
|
-
},
|
|
100
|
-
workflow: workflow({
|
|
101
|
-
id: "example-review",
|
|
102
|
-
title: "Example Review",
|
|
103
|
-
inputs,
|
|
104
|
-
sandboxProfiles: ["readonly"],
|
|
105
|
-
phases: [
|
|
106
|
-
phase("Map", [
|
|
107
|
-
agent("map:system", "Map the system boundaries.", {
|
|
108
|
-
sandboxProfileId: "readonly"
|
|
109
|
-
})
|
|
110
|
-
]),
|
|
111
|
-
phase("Verdict", [
|
|
112
|
-
artifact("verdict", "Write the final evidence-backed verdict.", {
|
|
113
|
-
requiresEvidence: true,
|
|
114
|
-
sandboxProfileId: "readonly"
|
|
115
|
-
})
|
|
116
|
-
])
|
|
117
|
-
]
|
|
118
|
-
})
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Legacy `module.exports = ({ workflow, phase, agent, artifact }) => workflow(...)`
|
|
123
|
-
files can still be loaded. CW wraps them as compatibility apps with version `0.0.0`
|
|
124
|
-
so workflow files still plan and dispatch. When a canonical app owns the public
|
|
125
|
-
id, compatibility wrappers use explicit ids such as `legacy-research-synthesis`.
|
|
126
|
-
|
|
127
|
-
## Language Contract
|
|
128
|
-
|
|
129
|
-
The CW platform is TypeScript:
|
|
130
|
-
|
|
131
|
-
```text
|
|
132
|
-
src/*.ts -> dist/*.js
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Workflow apps are JavaScript modules:
|
|
136
|
-
|
|
137
|
-
```text
|
|
138
|
-
workflows/*.workflow.js
|
|
139
|
-
apps/<app-id>/app.json
|
|
140
|
-
apps/<app-id>/workflow.js
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
This is done on purpose. The runtime is strongly typed so it is simple to keep
|
|
144
|
-
up, while workflow scripts can run without `ts-node`.
|
|
145
|
-
|
|
146
|
-
See [workflow-app-framework.7.md](workflow-app-framework.7.md) for the full app contract,
|
|
147
|
-
the rules for checking, CLI commands, MCP tools, and state/report fields.
|
|
148
|
-
See [mcp-app-surface.7.md](mcp-app-surface.7.md) for the agent-host runtime
|
|
149
|
-
surface over MCP.
|
|
150
|
-
See [operator-ux.7.md](operator-ux.7.md) for the operator inspection surface.
|
|
151
|
-
See [canonical-workflow-apps.7.md](canonical-workflow-apps.7.md) for the
|
|
152
|
-
official app matrix.
|
|
153
|
-
See [end-to-end-golden-path.7.md](end-to-end-golden-path.7.md) for the
|
|
154
|
-
fixed release proof that those parts connect.
|
|
155
|
-
|
|
156
|
-
## Evidence Contract
|
|
157
|
-
|
|
158
|
-
Verification and verdict tasks should give back:
|
|
159
|
-
|
|
160
|
-
````text
|
|
161
|
-
```cw:result
|
|
162
|
-
{
|
|
163
|
-
"summary": "short summary",
|
|
164
|
-
"findings": [],
|
|
165
|
-
"evidence": ["/absolute/path/file.ts:42"]
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
````
|
|
169
|
-
|
|
170
|
-
CW says no to high-priority findings without evidence. This keeps agent work
|
|
171
|
-
nearer to engineering output you can look into than to free talk.
|
|
172
|
-
|
|
173
|
-
## Boundary
|
|
174
|
-
|
|
175
|
-
CW is an independent workflow control-plane by COOLWHITE LLC. It puts into effect
|
|
176
|
-
workflows that change, scheduled tasks, local scheduling, routine triggers, state
|
|
177
|
-
checkpoints, and multi-agent verification.
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# Design — Cross-agent handoff ledger
|
|
2
|
-
|
|
3
|
-
Status: DRAFT / proposal. Nothing here is built yet. This file ships no
|
|
4
|
-
behavior, no new command, no man-page contract, and changes no existing
|
|
5
|
-
output. It exists so two people (the operator and the reviewer agent) can
|
|
6
|
-
agree on the shape before any code is written.
|
|
7
|
-
|
|
8
|
-
North Star track: **Track B** (portable, verifiable state — the same
|
|
9
|
-
`run export` → `run restore` recovery story, now used as the channel
|
|
10
|
-
between two agents).
|
|
11
|
-
|
|
12
|
-
## Goal
|
|
13
|
-
|
|
14
|
-
Two agents work on two repositories:
|
|
15
|
-
|
|
16
|
-
- one agent scoped to repo **A** (for example `cool-workflow`),
|
|
17
|
-
- one agent scoped to repo **B** (for example `chime`).
|
|
18
|
-
|
|
19
|
-
The operator wants them to "share data, review each other, and each be
|
|
20
|
-
able to raise a pull request to the other". In plain terms:
|
|
21
|
-
|
|
22
|
-
- each side can hand the other a **change proposal**, and
|
|
23
|
-
- each side can hand the other a **review verdict** on a diff or PR,
|
|
24
|
-
- with saved, inspectable, fail-closed state — never a fabricated hand-off.
|
|
25
|
-
|
|
26
|
-
## The hard constraint (why the obvious design does not work)
|
|
27
|
-
|
|
28
|
-
The first idea is a shared local folder (for example `~/.chime/handoff/`)
|
|
29
|
-
that both agents read and append to. That only works when both agents run
|
|
30
|
-
on **one machine** with **one filesystem**.
|
|
31
|
-
|
|
32
|
-
In the operator's setup the two agents run as **two separate cloud
|
|
33
|
-
sessions**. Each session is a fresh, throwaway VM. Two facts follow, and
|
|
34
|
-
the design must respect both:
|
|
35
|
-
|
|
36
|
-
1. **No shared filesystem.** A file the B-agent writes to `~/.chime/handoff/`
|
|
37
|
-
in its VM is invisible to the A-agent's VM, and is gone when the session
|
|
38
|
-
ends. A local folder cannot be the channel.
|
|
39
|
-
2. **Single-repo scope.** Each session's GitHub reach is scoped to one repo
|
|
40
|
-
at launch (A-agent → repo A, B-agent → repo B). The A-agent cannot read
|
|
41
|
-
repo B through its GitHub tools, and the reverse is also true. Scope is
|
|
42
|
-
fixed at launch and cannot be widened mid-session.
|
|
43
|
-
|
|
44
|
-
The only medium both sessions can durably reach is **git / GitHub**. So the
|
|
45
|
-
ledger is a set of committed files, not a local folder — and the scope wall
|
|
46
|
-
means the hand-off still needs either a shared repo or a human relay for the
|
|
47
|
-
cross-repo step. This document is honest about that; it does not pretend the
|
|
48
|
-
wall is not there.
|
|
49
|
-
|
|
50
|
-
## What we reuse (no new trust machinery)
|
|
51
|
-
|
|
52
|
-
CW already has the parts this needs. The design adds a thin verb layer over
|
|
53
|
-
them; it invents no new crypto and no new state format.
|
|
54
|
-
|
|
55
|
-
- `run export` produces a **verifiable bundle** (file digests, telemetry
|
|
56
|
-
ledger, trust-audit hash chains).
|
|
57
|
-
- `run restore` **imports fail-closed**: it inspects first, refuses a corrupt
|
|
58
|
-
or tampered bundle without writing anything, and exits non-zero when the
|
|
59
|
-
chain does not verify. (`run import` is the exit-0 sibling; the hand-off
|
|
60
|
-
path must use the fail-closed `restore` contract.)
|
|
61
|
-
- `report verify` checks a run's evidence and citations.
|
|
62
|
-
|
|
63
|
-
A hand-off entry is therefore just a CW bundle. The receiving side trusts it
|
|
64
|
-
the same way it trusts any restored run: by verification, not by good faith.
|
|
65
|
-
|
|
66
|
-
## Two verbs
|
|
67
|
-
|
|
68
|
-
Both live under a single new `cw ledger` verb, so the existing surface is
|
|
69
|
-
untouched and the new behavior is opt-in (POLA). (The name `handoff` was already
|
|
70
|
-
taken by an unrelated collaboration primitive — ownership transfer of a run/task
|
|
71
|
-
— so the cross-agent verb is `ledger`, not `handoff`.) Stage 1 ships as
|
|
72
|
-
`cw ledger propose|review|verify`; see
|
|
73
|
-
[cross-agent-ledger](../cross-agent-ledger.7.md) for the contract.
|
|
74
|
-
|
|
75
|
-
- **`propose`** — the read-only side writes a structured *change proposal*
|
|
76
|
-
(title, rationale, target files, suggested diff) as a ledger entry. It does
|
|
77
|
-
**not** mutate the other repo. The write-capable side picks the entry up,
|
|
78
|
-
verifies it, and turns it into a **real GitHub pull request**.
|
|
79
|
-
- **`review`** — the reviewing side writes a structured *review verdict*
|
|
80
|
-
(`APPROVED` / `REJECTED`, findings, the diff or PR it judged) as a ledger
|
|
81
|
-
entry. The other side surfaces it and can act on it.
|
|
82
|
-
|
|
83
|
-
This keeps a read-only agent honest: it emits proposals and verdicts as
|
|
84
|
-
**data**, and the write-capable side is the only one that opens PRs. Neither
|
|
85
|
-
side has to be trusted to have mutated the other's code.
|
|
86
|
-
|
|
87
|
-
## Transports (how an entry actually crosses)
|
|
88
|
-
|
|
89
|
-
The verbs above produce and consume entries; the transport is how an entry
|
|
90
|
-
moves from one VM to the other. Two are in scope, smallest first.
|
|
91
|
-
|
|
92
|
-
- **T1 — Human relay (MVP, works today, zero infra).** The producing side
|
|
93
|
-
prints the entry (a verifiable bundle, or its safe text form) to stdout;
|
|
94
|
-
the operator carries it to the other session; the consuming side verifies
|
|
95
|
-
it fail-closed and acts (opens the PR, or records the verdict). This is
|
|
96
|
-
exactly the loop the operator is already running by hand. It needs no new
|
|
97
|
-
code beyond a stable print/parse shape.
|
|
98
|
-
- **T2 — Git-as-ledger.** Each entry is committed to a repo under a known
|
|
99
|
-
path (for example `handoff/<from>-<to>/<id>.bundle`). Because scope is
|
|
100
|
-
single-repo, this needs one of:
|
|
101
|
-
- **T2a — a shared handoff repo** both agents are scoped to (cleanest, but
|
|
102
|
-
the operator must create it and launch both sessions against it), or
|
|
103
|
-
- **T2b — each side writes to its own repo** and a bridge (the operator, or
|
|
104
|
-
a scheduled job that *is* scoped to both) moves entries across. The
|
|
105
|
-
cross-repo read cannot be automatic inside a single scoped session — this
|
|
106
|
-
is the scope wall, stated plainly, not a gap to be quietly filled.
|
|
107
|
-
|
|
108
|
-
## Fail-closed rules (non-negotiable)
|
|
109
|
-
|
|
110
|
-
- An entry that does not verify is **refused**, never acted on. No PR is
|
|
111
|
-
opened, no verdict is recorded, and the refusal is explicit on stderr with
|
|
112
|
-
a non-zero exit — the same contract as `run restore`.
|
|
113
|
-
- A proposal is a **suggestion only**. It never edits the target repo by
|
|
114
|
-
itself; a human-or-agent on the write side always makes the real PR, so the
|
|
115
|
-
read-only vow of the proposing side holds.
|
|
116
|
-
- stdout stays data (the entry / the machine result); stderr stays
|
|
117
|
-
diagnostics; a piped run is silent on success. `--json` is stable and
|
|
118
|
-
decoration-free.
|
|
119
|
-
|
|
120
|
-
## Non-goals / POLA
|
|
121
|
-
|
|
122
|
-
- No existing command, output byte, exit code, or file layout changes.
|
|
123
|
-
- No new runtime dependency (zero-dependency red line holds).
|
|
124
|
-
- No vendor-specific logic in core; the verbs move opaque bundles.
|
|
125
|
-
- Nothing ships until its own cycle lands with a test that fails before and
|
|
126
|
-
passes after, and a `docs/*.7.md` contract page — this design file is not
|
|
127
|
-
that contract and claims no shipped behavior.
|
|
128
|
-
|
|
129
|
-
## Suggested rollout (each stage its own reviewed cycle)
|
|
130
|
-
|
|
131
|
-
0. **This design doc** (no behavior). ← you are here.
|
|
132
|
-
1. **T1 human-relay shape** — a stable, documented print/parse form for a
|
|
133
|
-
proposal and a verdict, plus a smoke that round-trips one of each and
|
|
134
|
-
proves a tampered entry is refused.
|
|
135
|
-
2. **`cw ledger propose` / `review`** over `run export` / `restore`, with the
|
|
136
|
-
fail-closed refusal test.
|
|
137
|
-
3. **T2 git-ledger** (shared-repo first), then optionally a scoped bridge job
|
|
138
|
-
for T2b.
|
|
139
|
-
|
|
140
|
-
## Open decisions for the operator
|
|
141
|
-
|
|
142
|
-
- T2a (shared handoff repo) or T2b (own repos + bridge)? T2a is simpler and
|
|
143
|
-
should be the default unless a shared repo is not acceptable.
|
|
144
|
-
- Should a verdict be able to **block** a PR merge on the other side, or only
|
|
145
|
-
advise? Advise-only is the safer default and matches "review as data".
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Dogfood: real `builtin:claude` agent + `run resume --drive` (2026-06-14)
|
|
2
|
-
|
|
3
|
-
A live dogfood run with a REAL external agent (`CW_AGENT_COMMAND=builtin:claude`, the
|
|
4
|
-
bundled read-only claude wrapper). The model ran in the agent's own process; CW
|
|
5
|
-
spawned it and recorded the attested output — CW holds no API key and imports no
|
|
6
|
-
model SDK. This run had two purposes and delivered both: it confirmed the real-agent
|
|
7
|
-
delegation path works end to end, and — because it exercised the **CLI** rather than
|
|
8
|
-
the unit-test function path — it **caught a real shipped bug** in `run resume --drive`.
|
|
9
|
-
|
|
10
|
-
## What ran
|
|
11
|
-
|
|
12
|
-
- `cw run architecture-review --drive --once --repo <tmp> --question "…"` with a real
|
|
13
|
-
`builtin:claude` agent: **1 worker completed** end-to-end with zero hand-written
|
|
14
|
-
result.md — the worker's `result.md` was produced by real claude, passed the
|
|
15
|
-
evidence-gated acceptance, and a `report.md` (7.5 KB, "# Architecture Review …")
|
|
16
|
-
+ 3 state commits were written. The real-agent path (spawn → attested output →
|
|
17
|
-
evidence gate → commit) works.
|
|
18
|
-
- Run: `architecture-review-20260614T104416Z-upkor2`, status `in-progress` (1/14)
|
|
19
|
-
after the single `--once` step.
|
|
20
|
-
|
|
21
|
-
## The bug it caught (and the fix)
|
|
22
|
-
|
|
23
|
-
Resuming the partway run with `cw run resume <id> --drive` failed:
|
|
24
|
-
`cw: Workflow app not found: resume`. The `run` command's early `--drive` branch
|
|
25
|
-
(the `cw run <app> --drive` one-command form) intercepted the invocation *before* the
|
|
26
|
-
subcommand switch, so the `resume` keyword was misread as an app name and never
|
|
27
|
-
reached the `runResume` continuation shipped in #155.
|
|
28
|
-
|
|
29
|
-
The A1 unit smoke (`run-resume-drive-smoke`) had tested `runResume()` **directly**, so
|
|
30
|
-
it never exercised the CLI dispatch — only a real CLI run surfaced it. Fixed by
|
|
31
|
-
guarding the early app-drive route so a leading run-registry subcommand keyword
|
|
32
|
-
(resume/show/export/…) falls through to the switch; `run-resume-drive-smoke` now drives
|
|
33
|
-
`cw run resume <id> --drive` through the actual CLI and asserts it routes to the verb,
|
|
34
|
-
plus a regression guard that `run <app> --drive` still routes to the app drive.
|
|
35
|
-
|
|
36
|
-
## Takeaway
|
|
37
|
-
|
|
38
|
-
Unit tests that call the capability function directly can miss CLI-dispatch bugs.
|
|
39
|
-
Every both-surface verb that adds a flag wants at least one test through the real CLI
|
|
40
|
-
argv path, not just the exported function.
|