cool-workflow 0.2.3 → 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.
Files changed (143) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +202 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +33 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/io.js +6 -20
  18. package/dist/cli/parseargv.js +7 -1
  19. package/dist/core/capability-data.js +339 -10
  20. package/dist/core/format/help.js +80 -4
  21. package/dist/core/format/recovery-hint.js +32 -0
  22. package/dist/core/multi-agent/collaboration.js +35 -6
  23. package/dist/core/multi-agent/runtime.js +7 -0
  24. package/dist/core/multi-agent/trust-policy.js +7 -1
  25. package/dist/core/pipeline/contract.js +7 -0
  26. package/dist/core/pipeline/error-feedback.js +2 -2
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +55 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +124 -13
  32. package/dist/mcp-server.js +20 -0
  33. package/dist/shell/commit.js +8 -2
  34. package/dist/shell/coordinator-io.js +73 -1
  35. package/dist/shell/drive.js +129 -64
  36. package/dist/shell/error-feedback-io.js +6 -0
  37. package/dist/shell/execution-backend/agent.js +205 -24
  38. package/dist/shell/execution-backend/container.js +44 -10
  39. package/dist/shell/execution-backend/local.js +47 -10
  40. package/dist/shell/fs-atomic.js +93 -12
  41. package/dist/shell/ledger-cli.js +9 -2
  42. package/dist/shell/multi-agent-cli.js +5 -1
  43. package/dist/shell/onramp.js +48 -5
  44. package/dist/shell/pipeline-cli.js +20 -2
  45. package/dist/shell/pipeline.js +2 -1
  46. package/dist/shell/reclamation-io.js +76 -7
  47. package/dist/shell/registry-cli.js +4 -0
  48. package/dist/shell/run-export.js +52 -4
  49. package/dist/shell/run-registry-io.js +1 -0
  50. package/dist/shell/run-store.js +156 -0
  51. package/dist/shell/scheduler-io.js +101 -10
  52. package/dist/shell/telemetry-ledger-io.js +36 -24
  53. package/dist/shell/trust-audit.js +104 -10
  54. package/dist/shell/workbench-host.js +121 -10
  55. package/dist/shell/workbench.js +79 -5
  56. package/dist/shell/worker-isolation.js +1 -1
  57. package/dist/shell/workflow-app-loader.js +67 -1
  58. package/dist/wiring/capability-table/basics.js +7 -2
  59. package/dist/wiring/capability-table/exec-backend.js +42 -24
  60. package/dist/wiring/capability-table/multi-agent.js +69 -69
  61. package/dist/wiring/capability-table/pipeline.js +60 -28
  62. package/dist/wiring/capability-table/registry-core.js +29 -6
  63. package/dist/wiring/capability-table/reporting.js +45 -39
  64. package/dist/wiring/capability-table/scheduling-registry.js +66 -60
  65. package/dist/wiring/capability-table/state.js +34 -34
  66. package/dist/wiring/capability-table/trust-ledger.js +66 -24
  67. package/dist/wiring/capability-table/workflow-apps.js +15 -15
  68. package/docs/agent-delegation-drive.7.md +17 -0
  69. package/docs/cli-mcp-parity.7.md +43 -13
  70. package/docs/contract-migration-tooling.7.md +4 -0
  71. package/docs/control-plane-scheduling.7.md +38 -0
  72. package/docs/cross-agent-ledger.7.md +20 -8
  73. package/docs/durable-state-and-locking.7.md +4 -0
  74. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  75. package/docs/execution-backends.7.md +4 -0
  76. package/docs/fix.7.md +4 -4
  77. package/docs/getting-started.md +40 -32
  78. package/docs/index.md +17 -0
  79. package/docs/launch/demo.tape +4 -3
  80. package/docs/mcp-app-surface.7.md +6 -0
  81. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  82. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  83. package/docs/multi-agent-operator-ux.7.md +4 -0
  84. package/docs/node-snapshot-diff-replay.7.md +4 -0
  85. package/docs/observability-cost-accounting.7.md +4 -0
  86. package/docs/project-index.md +43 -9
  87. package/docs/real-execution-backends.7.md +4 -0
  88. package/docs/release-and-migration.7.md +4 -0
  89. package/docs/release-tooling.7.md +22 -0
  90. package/docs/routine.7.md +22 -0
  91. package/docs/run-registry-control-plane.7.md +4 -0
  92. package/docs/run-retention-reclamation.7.md +28 -3
  93. package/docs/state-explosion-management.7.md +4 -0
  94. package/docs/team-collaboration.7.md +4 -0
  95. package/docs/trust-audit-anchor.7.md +4 -0
  96. package/docs/web-desktop-workbench.7.md +26 -2
  97. package/docs/workflow-app-framework.7.md +31 -0
  98. package/manifest/plugin.manifest.json +1 -1
  99. package/manifest/source-context-profiles.json +4 -3
  100. package/package.json +5 -2
  101. package/scripts/agents/agent-adapter-core.js +26 -0
  102. package/scripts/agents/claude-p-agent.js +4 -1
  103. package/scripts/agents/codex-agent.js +4 -0
  104. package/scripts/agents/cw-attest-wrap.js +1 -1
  105. package/scripts/agents/gemini-agent.js +4 -0
  106. package/scripts/agents/opencode-agent.js +5 -0
  107. package/scripts/block-unapproved-tag.js +160 -0
  108. package/scripts/bump-version.js +27 -10
  109. package/scripts/canonical-apps.js +4 -4
  110. package/scripts/children/batch-delegate-child.js +72 -4
  111. package/scripts/children/http-batch-delegate-child.js +132 -0
  112. package/scripts/children/http-delegate-child.js +8 -0
  113. package/scripts/dogfood-release.js +2 -2
  114. package/scripts/fake-date-for-reproduction.js +1 -1
  115. package/scripts/golden-path.js +4 -4
  116. package/scripts/lang-policy-check.js +82 -0
  117. package/scripts/purity-baseline.json +0 -30
  118. package/scripts/purity-gate.js +16 -3
  119. package/scripts/release-check.js +2 -1
  120. package/scripts/release-flow.js +264 -41
  121. package/scripts/release-gate.js +184 -0
  122. package/scripts/release-oneclick.js +438 -0
  123. package/scripts/release-tags.js +47 -0
  124. package/scripts/verdict-keygen.js +2 -2
  125. package/scripts/verify-bump-reproduction.js +193 -0
  126. package/scripts/verify-verdict-signature.js +1 -1
  127. package/scripts/version-sync-check.js +39 -22
  128. package/ui/workbench/app.css +41 -9
  129. package/ui/workbench/app.js +160 -27
  130. package/ui/workbench/index.html +2 -2
  131. package/docs/agent-framework.md +0 -177
  132. package/docs/designs/handoff-ledger.md +0 -145
  133. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  134. package/docs/launch/launch-kit.md +0 -195
  135. package/docs/launch/pre-launch-checklist.md +0 -53
  136. package/docs/readme-v0.1.87-full.md +0 -301
  137. package/docs/routines.md +0 -101
  138. package/docs/scheduled-tasks.md +0 -80
  139. package/scripts/agents/claude-p-agent.sh +0 -9
  140. package/scripts/block-unapproved-tag.sh +0 -60
  141. package/scripts/release-gate.sh +0 -94
  142. package/scripts/verify-bump-reproduction.sh +0 -148
  143. package/scripts/verify-container-selfref.js +0 -64
@@ -15,7 +15,30 @@ const PANEL_GROUPS = [
15
15
  { key: "collaboration", label: "Review & collaboration", panels: ["review", "comments"] }
16
16
  ];
17
17
 
18
- const state = { activeRunId: null, activeTab: "graph" };
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
- return el("span", { class: `badge ${v || "absent"}`, text: value || "unknown" });
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(`/api/index${filter ? `?text=${encodeURIComponent(filter)}` : ""}`);
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
- fresh.append("registry ", freshnessBadge(reg.freshness && reg.freshness.status), ` · scope ${view.scope}`);
66
- const records = (view.runs && view.runs.records) || [];
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", text: "no runs indexed in this scope" }));
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
- for (const record of records) {
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
- const li = el("li", { class: state.activeRunId === record.runId ? "active" : "" }, [
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
- li.addEventListener("click", () => selectRun(record.runId));
84
- list.appendChild(li);
146
+ btn.addEventListener("click", () => selectRun(record.runId));
147
+ list.appendChild(btn);
85
148
  }
86
149
  }
87
150
 
88
- async function selectRun(runId) {
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
- loadIndex();
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 btn = el("button", { class: `tab ${state.activeTab === group.key ? "active" : ""}`, text: group.label });
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) detail.appendChild(renderPanel(name, 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 = el("table", { class: "struct-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
- table.appendChild(
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 = el("table", { class: "struct-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
- table.appendChild(
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", loadIndex);
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) {
@@ -16,10 +16,10 @@
16
16
  <main>
17
17
  <section id="index-panel" class="sidebar">
18
18
  <div class="bar">
19
- <strong>Runs</strong>
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>
@@ -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.