cool-workflow 0.1.93 → 0.1.94

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 (43) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/version.js +1 -1
  10. package/docs/agent-delegation-drive.7.md +12 -6
  11. package/docs/cli-mcp-parity.7.md +2 -0
  12. package/docs/contract-migration-tooling.7.md +2 -0
  13. package/docs/control-plane-scheduling.7.md +2 -0
  14. package/docs/durable-state-and-locking.7.md +2 -0
  15. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  16. package/docs/execution-backends.7.md +2 -0
  17. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  18. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  19. package/docs/multi-agent-operator-ux.7.md +2 -0
  20. package/docs/node-snapshot-diff-replay.7.md +2 -0
  21. package/docs/observability-cost-accounting.7.md +2 -0
  22. package/docs/project-index.md +6 -3
  23. package/docs/real-execution-backends.7.md +2 -0
  24. package/docs/release-and-migration.7.md +2 -0
  25. package/docs/release-tooling.7.md +2 -0
  26. package/docs/run-registry-control-plane.7.md +2 -0
  27. package/docs/run-retention-reclamation.7.md +2 -0
  28. package/docs/state-explosion-management.7.md +2 -0
  29. package/docs/team-collaboration.7.md +2 -0
  30. package/docs/web-desktop-workbench.7.md +2 -0
  31. package/manifest/plugin.manifest.json +1 -1
  32. package/package.json +1 -1
  33. package/scripts/agents/builtin-templates.json +3 -2
  34. package/scripts/agents/claude-p-agent.js +6 -6
  35. package/scripts/agents/codex-agent.js +17 -3
  36. package/scripts/agents/deepseek-agent.js +23 -0
  37. package/scripts/agents/gemini-opencode-agent.js +25 -0
  38. package/scripts/agents/opencode-agent.js +57 -8
  39. package/scripts/canonical-apps.js +4 -4
  40. package/scripts/dogfood-release.js +1 -1
  41. package/scripts/golden-path.js +4 -4
  42. package/scripts/release-flow.js +35 -0
  43. package/scripts/vendor-preflight.js +127 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.1.93",
4
+ "version": "0.1.94",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.93",
3
+ "version": "0.1.94",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.93",
6
+ "version": "0.1.94",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
4
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.93";
4
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.94";
5
5
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
6
6
  exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
7
7
  exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
@@ -239,14 +239,18 @@ never polluted.
239
239
  The built-in templates are:
240
240
 
241
241
  ```text
242
- --agent-command builtin:claude
243
- --agent-command builtin:codex
242
+ --agent-command builtin:claude # claude -p (native)
243
+ --agent-command builtin:codex # codex exec (native)
244
+ --agent-command builtin:gemini # Gemini via opencode (google/gemini-3.5-flash)
245
+ --agent-command builtin:gemini-cli # native gemini CLI (needs GEMINI_API_KEY)
246
+ --agent-command builtin:opencode # opencode (its configured default model)
247
+ --agent-command builtin:deepseek # DeepSeek via opencode (deepseek/deepseek-chat)
244
248
  ```
245
249
 
246
- Gemini, OpenCode, DeepSeek, and GLM stay as external agent commands or HTTP
247
- endpoints until their stream format is proven by a local, deterministic wrapper
248
- smoke. DeepSeek and GLM are best reached through OpenCode or an HTTP agent
249
- adapter first; CW still imports no model SDK.
250
+ claude and codex run their own CLIs; gemini and deepseek route through opencode
251
+ (where their keys live), each proven by a local, deterministic wrapper smoke
252
+ (override the model with CW_GEMINI_MODEL / CW_DEEPSEEK_MODEL). GLM stays an
253
+ external agent command or HTTP endpoint. CW still imports no model SDK.
250
254
 
251
255
  ## Compatibility
252
256
 
@@ -338,3 +342,5 @@ The one-command `cw -q` headline now routes the question and defaults the repo t
338
342
  0.1.91
339
343
 
340
344
  0.1.93
345
+
346
+ 0.1.94
@@ -545,3 +545,5 @@ CLI golden-path fixes: `cw -q "…"` routes the question (was read as an app id
545
545
  0.1.91
546
546
 
547
547
  0.1.93
548
+
549
+ 0.1.94
@@ -163,3 +163,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
163
163
  0.1.91
164
164
 
165
165
  0.1.93
166
+
167
+ 0.1.94
@@ -147,3 +147,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
147
147
  0.1.91
148
148
 
149
149
  0.1.93
150
+
151
+ 0.1.94
@@ -146,3 +146,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
146
146
  0.1.91
147
147
 
148
148
  0.1.93
149
+
150
+ 0.1.94
@@ -307,3 +307,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
307
307
  0.1.91
308
308
 
309
309
  0.1.93
310
+
311
+ 0.1.94
@@ -337,3 +337,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
337
337
  0.1.91
338
338
 
339
339
  0.1.93
340
+
341
+ 0.1.94
@@ -305,3 +305,5 @@ The host-facing surface tracks the CLI golden-path fixes (`cw -q` routing + repo
305
305
  0.1.91
306
306
 
307
307
  0.1.93
308
+
309
+ 0.1.94
@@ -339,3 +339,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
339
339
  0.1.91
340
340
 
341
341
  0.1.93
342
+
343
+ 0.1.94
@@ -351,3 +351,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
351
351
  0.1.91
352
352
 
353
353
  0.1.93
354
+
355
+ 0.1.94
@@ -172,3 +172,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
172
172
  0.1.91
173
173
 
174
174
  0.1.93
175
+
176
+ 0.1.94
@@ -231,3 +231,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
231
231
  0.1.91
232
232
 
233
233
  0.1.93
234
+
235
+ 0.1.94
@@ -1,15 +1,15 @@
1
1
  # Cool Workflow Project Index
2
2
 
3
- Generated from the current repository code on 2026-06-25 by `npm run sync:project-index`.
3
+ Generated from the current repository code on 2026-06-26 by `npm run sync:project-index`.
4
4
 
5
5
  ## Snapshot
6
6
 
7
7
  - Package: `cool-workflow`
8
- - Version: `0.1.93`
8
+ - Version: `0.1.94`
9
9
  - Source modules: `68`
10
10
  - Workflow apps: `7`
11
11
  - Docs: `53`
12
- - Smoke tests: `149`
12
+ - Smoke tests: `152`
13
13
  - Repository: https://github.com/coo1white/cool-workflow
14
14
 
15
15
  ## Architecture
@@ -231,6 +231,7 @@ Smoke tests mirror the public contracts. The high-signal suites are:
231
231
  - [coordinator-blackboard-smoke.js](../test/coordinator-blackboard-smoke.js)
232
232
  - [cw-help-per-command-smoke.js](../test/cw-help-per-command-smoke.js)
233
233
  - [dead-export-removal-guard-smoke.js](../test/dead-export-removal-guard-smoke.js)
234
+ - [deepseek-agent-wrapper-smoke.js](../test/deepseek-agent-wrapper-smoke.js)
234
235
  - [demo-bundle-smoke.js](../test/demo-bundle-smoke.js)
235
236
  - [det-ids-b-smoke.js](../test/det-ids-b-smoke.js)
236
237
  - [doctor-smoke.js](../test/doctor-smoke.js)
@@ -248,6 +249,7 @@ Smoke tests mirror the public contracts. The high-signal suites are:
248
249
  - [execution-backends-smoke.js](../test/execution-backends-smoke.js)
249
250
  - [freebsd-audit-fixes-smoke.js](../test/freebsd-audit-fixes-smoke.js)
250
251
  - [gemini-agent-wrapper-smoke.js](../test/gemini-agent-wrapper-smoke.js)
252
+ - [gemini-opencode-agent-wrapper-smoke.js](../test/gemini-opencode-agent-wrapper-smoke.js)
251
253
  - [h7-custom-profile-persist-smoke.js](../test/h7-custom-profile-persist-smoke.js)
252
254
  - [headline-commands-smoke.js](../test/headline-commands-smoke.js)
253
255
  - [incremental-resume-smoke.js](../test/incremental-resume-smoke.js)
@@ -335,6 +337,7 @@ Smoke tests mirror the public contracts. The high-signal suites are:
335
337
  - [telemetry-verify-signatures-smoke.js](../test/telemetry-verify-signatures-smoke.js)
336
338
  - [token-budget-enforcement-smoke.js](../test/token-budget-enforcement-smoke.js)
337
339
  - [vendor-manifest-load-smoke.js](../test/vendor-manifest-load-smoke.js)
340
+ - [vendor-preflight-smoke.js](../test/vendor-preflight-smoke.js)
338
341
  - [verifier-gated-commit-smoke.js](../test/verifier-gated-commit-smoke.js)
339
342
  - [verify-import-audit-chain-smoke.js](../test/verify-import-audit-chain-smoke.js)
340
343
  - [web-desktop-workbench-smoke.js](../test/web-desktop-workbench-smoke.js)
@@ -179,3 +179,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
179
179
  0.1.91
180
180
 
181
181
  0.1.93
182
+
183
+ 0.1.94
@@ -319,3 +319,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
319
319
  0.1.91
320
320
 
321
321
  0.1.93
322
+
323
+ 0.1.94
@@ -266,3 +266,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
266
266
  0.1.91
267
267
 
268
268
  0.1.93
269
+
270
+ 0.1.94
@@ -462,3 +462,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
462
462
  0.1.91
463
463
 
464
464
  0.1.93
465
+
466
+ 0.1.94
@@ -230,3 +230,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
230
230
  0.1.91
231
231
 
232
232
  0.1.93
233
+
234
+ 0.1.94
@@ -308,3 +308,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
308
308
  0.1.91
309
309
 
310
310
  0.1.93
311
+
312
+ 0.1.94
@@ -244,3 +244,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
244
244
  0.1.91
245
245
 
246
246
  0.1.93
247
+
248
+ 0.1.94
@@ -252,3 +252,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
252
252
  0.1.91
253
253
 
254
254
  0.1.93
255
+
256
+ 0.1.94
@@ -2,7 +2,7 @@
2
2
  "_comment": "SINGLE SOURCE OF TRUTH for every vendor manifest. Edit THIS file, then run `npm run gen:manifests`. Do NOT hand-edit the generated vendor manifests (.claude-plugin/, .codex-plugin/, .agents/, .mcp.json) — `npm run gen:manifests -- --check` (run by release:check) will fail if they drift from this source.",
3
3
  "identity": {
4
4
  "name": "cool-workflow",
5
- "version": "0.1.93",
5
+ "version": "0.1.94",
6
6
  "license": "BSD-2-Clause",
7
7
  "homepage": "https://github.com/coo1white/cool-workflow",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.93",
3
+ "version": "0.1.94",
4
4
  "bin": {
5
5
  "cool-workflow": "scripts/cw.js",
6
6
  "cw": "scripts/cw.js"
@@ -4,8 +4,9 @@
4
4
  "templates": {
5
5
  "claude": "claude-p-agent.js",
6
6
  "codex": "codex-agent.js",
7
- "gemini": "gemini-agent.js",
7
+ "gemini": "gemini-opencode-agent.js",
8
+ "gemini-cli": "gemini-agent.js",
8
9
  "opencode": "opencode-agent.js",
9
- "deepseek": "opencode-agent.js"
10
+ "deepseek": "deepseek-agent.js"
10
11
  }
11
12
  }
@@ -12,12 +12,12 @@
12
12
  // worker's result.md ({{result}}), and forward claude's JSON on STDOUT so CW
13
13
  // records the agent-reported provenance.
14
14
  //
15
- // LIVE OUTPUT (Unix discipline): default output is the legacy `--output-format
16
- // json` contract, forwarded verbatim on stdout. Set CW_AGENT_STREAM=1 to opt in
17
- // to claude `stream-json`; only then does this wrapper render a human-readable
18
- // trace to stderr, and only when stderr is a TTY. Diagnostics stay off stdout,
19
- // and vendor-specific stream parsing lives HERE in the wrapper (policy), not in
20
- // CW's core (which only forwards, never parses).
15
+ // LIVE OUTPUT (Unix discipline): the DEFAULT is claude `stream-json` — this
16
+ // wrapper renders a human-readable trace to stderr (only when stderr is a TTY).
17
+ // Set CW_AGENT_STREAM=0 (or CW_NO_STREAM=1) to fall back to the legacy
18
+ // `--output-format json` contract, forwarded verbatim on stdout. Either way
19
+ // diagnostics stay off stdout, and vendor-specific stream parsing lives HERE in
20
+ // the wrapper (policy), not in CW's core (which only forwards, never parses).
21
21
  //
22
22
  // READ-ONLY by design: claude gets NO Write tool; the architecture-review app
23
23
  // declares the `readonly` sandbox profile. This wrapper (the transport) writes
@@ -16,6 +16,7 @@
16
16
  // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
17
17
 
18
18
  const fs = require("node:fs");
19
+ const os = require("node:os");
19
20
  const path = require("node:path");
20
21
  const { spawn } = require("node:child_process");
21
22
  const {
@@ -27,6 +28,21 @@ const {
27
28
  writeResult
28
29
  } = require("./agent-adapter-core");
29
30
 
31
+ // codex exec --json (>=0.139) emits NO model field in its JSONL (only thread/turn
32
+ // events + usage). For provenance, fall back to the model codex is configured to
33
+ // use: $CODEX_HOME/config.toml (or ~/.codex/config.toml), key `model`. Best effort
34
+ // — a stream model field (older/newer codex) still wins.
35
+ function detectCodexModel() {
36
+ try {
37
+ const home = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
38
+ const cfg = fs.readFileSync(path.join(home, "config.toml"), "utf8");
39
+ const m = cfg.match(/^\s*model\s*=\s*["']([^"']+)["']/m);
40
+ return m ? m[1] : undefined;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
30
46
  const inputPath = process.argv[2];
31
47
  const resultPath = process.argv[3];
32
48
  if (!inputPath || !resultPath) {
@@ -65,8 +81,6 @@ const args = [
65
81
  finalPath,
66
82
  "--sandbox",
67
83
  "read-only",
68
- "--ask-for-approval",
69
- "never",
70
84
  "--color",
71
85
  "never",
72
86
  "-"
@@ -133,5 +147,5 @@ child.on("close", (code) => {
133
147
  process.exit(1);
134
148
  }
135
149
 
136
- emitReport(state.model, state.usage, resultText);
150
+ emitReport(state.model || detectCodexModel(), state.usage, resultText);
137
151
  });
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // deepseek-agent.js - DeepSeek (via opencode) adapter for CW Agent Delegation Drive.
5
+ //
6
+ // DeepSeek ships no standalone CLI here; CW reaches it through
7
+ // `opencode run --model deepseek/...`. This is a thin variant of the opencode
8
+ // runner: it sets the variant env (display label + model) and delegates to
9
+ // opencode-agent.js, so the run/parse/provenance logic lives in ONE place.
10
+ //
11
+ // Override the model with CW_DEEPSEEK_MODEL (default deepseek/deepseek-chat).
12
+ // Requires opencode installed AND a DeepSeek provider configured in opencode
13
+ // (`opencode auth login` or DEEPSEEK_API_KEY). Without that, opencode exits
14
+ // nonzero and CW fails closed - it never fabricates a result.
15
+ //
16
+ // Contract (unchanged from opencode-agent.js):
17
+ // argv[2] = {{input}} worker input.md
18
+ // argv[3] = {{result}} worker result.md to persist
19
+
20
+ process.env.CW_OPENCODE_LABEL = "deepseek";
21
+ process.env.CW_OPENCODE_MODEL = process.env.CW_DEEPSEEK_MODEL || "deepseek/deepseek-chat";
22
+
23
+ require("./opencode-agent.js");
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // gemini-opencode-agent.js - Gemini (via opencode) adapter for CW Agent Delegation.
5
+ //
6
+ // Reaches Gemini through `opencode run --model google/...`, the same gateway CW
7
+ // uses for DeepSeek. Use this when the Gemini key lives in opencode (provider
8
+ // "google") rather than the standalone `gemini` CLI. The native Gemini CLI path
9
+ // is still available as builtin:gemini-cli (scripts/agents/gemini-agent.js).
10
+ //
11
+ // Thin variant of the opencode runner: it sets the variant env (display label +
12
+ // model) and delegates to opencode-agent.js, so run/parse/provenance live in ONE
13
+ // place. Override the model with CW_GEMINI_MODEL (default google/gemini-3.5-flash).
14
+ // Requires opencode installed AND a "google" provider authed in opencode
15
+ // (`opencode auth login` -> google). Without it, opencode exits nonzero and CW
16
+ // fails closed - it never fabricates a result.
17
+ //
18
+ // Contract (unchanged from opencode-agent.js):
19
+ // argv[2] = {{input}} worker input.md
20
+ // argv[3] = {{result}} worker result.md to persist
21
+
22
+ process.env.CW_OPENCODE_LABEL = "gemini";
23
+ process.env.CW_OPENCODE_MODEL = process.env.CW_GEMINI_MODEL || "google/gemini-3.5-flash";
24
+
25
+ require("./opencode-agent.js");
@@ -37,11 +37,35 @@ if (!inputPath || !resultPath) {
37
37
  process.exit(2);
38
38
  }
39
39
 
40
+ // Same runner serves both `opencode` (provider default model) and `deepseek`
41
+ // (DeepSeek via opencode). The variant sets CW_OPENCODE_LABEL + CW_OPENCODE_MODEL;
42
+ // see deepseek-agent.js. Unset = plain opencode, unchanged behavior.
43
+ const label = process.env.CW_OPENCODE_LABEL || "opencode";
44
+ const requestedModel = process.env.CW_OPENCODE_MODEL || "";
45
+
40
46
  const prompt = buildPrompt(inputPath);
41
- const render = createRenderer({ env: process.env, stderr: process.stderr, label: "opencode" });
47
+ const render = createRenderer({ env: process.env, stderr: process.stderr, label });
42
48
  const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
43
- const state = { provider: "opencode", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined, renderer: render };
49
+ const state = {
50
+ provider: label,
51
+ buffer: "",
52
+ // opencode --format json carries NO model field; record the model we asked for.
53
+ model: requestedModel || undefined,
54
+ usage: undefined,
55
+ textFragments: [],
56
+ finalResult: undefined,
57
+ lastMessageId: undefined,
58
+ lastMessageText: "",
59
+ renderer: render
60
+ };
44
61
  let childStderr = "";
62
+
63
+ // opencode (>=1.x) --format json emits JSONL events of shape { type, part }:
64
+ // type:"text" -> part.text assistant text, grouped by part.messageID
65
+ // type:"step_finish" -> part.tokens { input, output, ... } per step
66
+ // The final answer is the LAST message's text (earlier "text" parts are mid-run
67
+ // narration). Older opencode shapes (ev.result / ev.text / ev.delta) are still
68
+ // accepted as a fallback so the wrapper is not pinned to one CLI version.
45
69
  function recordJsonLine(line) {
46
70
  let ev;
47
71
  try {
@@ -50,22 +74,47 @@ function recordJsonLine(line) {
50
74
  state.invalidJson = true;
51
75
  return;
52
76
  }
77
+ const part = ev && typeof ev.part === "object" && ev.part ? ev.part : {};
78
+
79
+ if (ev.type === "text" && typeof part.text === "string") {
80
+ const mid = typeof part.messageID === "string" ? part.messageID : "";
81
+ if (mid !== state.lastMessageId) {
82
+ state.lastMessageId = mid;
83
+ state.lastMessageText = "";
84
+ }
85
+ state.lastMessageText += (state.lastMessageText ? "\n" : "") + part.text;
86
+ if (part.text.trim()) state.textFragments.push(part.text);
87
+ return;
88
+ }
89
+
90
+ if (ev.type === "step_finish" && part.tokens && typeof part.tokens === "object") {
91
+ state.usage = state.usage || { input_tokens: 0, output_tokens: 0 };
92
+ state.usage.input_tokens += Number(part.tokens.input) || 0;
93
+ state.usage.output_tokens += Number(part.tokens.output) || 0;
94
+ return;
95
+ }
96
+
97
+ // Fallback: older opencode JSON shapes (single result object / delta stream).
53
98
  if (ev.result && typeof ev.result === "string") {
54
99
  state.finalResult = ev.result;
55
- } else {
56
- const text = typeof ev.text === "string" ? ev.text : (ev.delta ? (typeof ev.delta === "string" ? ev.delta : ev.delta.text) : undefined);
57
- if (typeof text === "string" && text.trim()) state.textFragments.push(text);
100
+ return;
58
101
  }
102
+ const legacy = typeof ev.text === "string" ? ev.text : (ev.delta ? (typeof ev.delta === "string" ? ev.delta : ev.delta.text) : undefined);
103
+ if (typeof legacy === "string" && legacy.trim()) state.textFragments.push(legacy);
59
104
  }
60
105
 
61
- render.action("opencode: reading the repo…");
106
+ render.action(`${label}: reading the repo…`);
62
107
 
108
+ // `opencode run` takes the message as a POSITIONAL arg ("opencode run [message..]");
109
+ // there is no --prompt flag. Pass the prompt last, as the positional message.
110
+ // --model (provider/model, e.g. deepseek/deepseek-chat) is added only when a
111
+ // variant requests it; otherwise opencode uses its configured default model.
63
112
  const args = [
64
113
  "run",
65
114
  "--format",
66
115
  "json",
67
116
  "--dangerously-skip-permissions",
68
- "--prompt",
117
+ ...(requestedModel ? ["--model", requestedModel] : []),
69
118
  prompt
70
119
  ];
71
120
 
@@ -105,7 +154,7 @@ child.on("close", (code) => {
105
154
  process.exit(1);
106
155
  }
107
156
 
108
- const resultText = state.finalResult || state.textFragments.join("\n\n");
157
+ const resultText = state.finalResult || state.lastMessageText || state.textFragments.join("\n\n");
109
158
  if (!resultText.trim()) {
110
159
  process.stderr.write("opencode produced no result text - refusing to fabricate a result\n");
111
160
  process.exit(1);
@@ -83,7 +83,7 @@ const canonicalApps = [
83
83
  "--source",
84
84
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
85
85
  "--scope",
86
- "Cool Workflow v0.1.93",
86
+ "Cool Workflow v0.1.94",
87
87
  "--freshness",
88
88
  "as of release preparation"
89
89
  ]
@@ -117,14 +117,14 @@ function main() {
117
117
  assert.ok(summary, `${app.id} must appear in app list`);
118
118
  assert.equal(summary.sourceKind, "app-directory");
119
119
  assert.equal(summary.legacy, false);
120
- assert.equal(summary.version, "0.1.93");
120
+ assert.equal(summary.version, "0.1.94");
121
121
 
122
122
  const validation = runJson(["app", "validate", manifestPath]);
123
123
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
124
124
 
125
125
  const shown = runJson(["app", "show", app.id]);
126
126
  assert.equal(shown.app.id, app.id);
127
- assert.equal(shown.app.version, "0.1.93");
127
+ assert.equal(shown.app.version, "0.1.94");
128
128
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
129
129
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
130
130
  assertTaskIdsUnique(shown);
@@ -135,7 +135,7 @@ function main() {
135
135
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
136
136
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
137
137
  assert.equal(state.workflow.app.id, app.id);
138
- assert.equal(state.workflow.app.version, "0.1.93");
138
+ assert.equal(state.workflow.app.version, "0.1.94");
139
139
  assert.equal(state.workflow.app.metadata.canonical, true);
140
140
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
141
141
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
@@ -6,7 +6,7 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { CoolWorkflowRunner } = require("../dist/orchestrator.js");
8
8
 
9
- const TARGET_VERSION = "0.1.93";
9
+ const TARGET_VERSION = "0.1.94";
10
10
  const PREVIOUS_VERSION = "0.1.31";
11
11
  const pluginRoot = path.resolve(__dirname, "..");
12
12
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -33,7 +33,7 @@ function main() {
33
33
  const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
34
34
  assert.equal(appValidation.valid, true);
35
35
  assert.equal(appValidation.summary.id, "end-to-end-golden-path");
36
- assert.equal(appValidation.summary.version, "0.1.93");
36
+ assert.equal(appValidation.summary.version, "0.1.94");
37
37
 
38
38
  const plan = runJson(
39
39
  [
@@ -42,7 +42,7 @@ function main() {
42
42
  "--repo",
43
43
  tmp,
44
44
  "--question",
45
- "Prove the deterministic v0.1.93 end-to-end golden path."
45
+ "Prove the deterministic v0.1.94 end-to-end golden path."
46
46
  ],
47
47
  pluginRoot
48
48
  );
@@ -52,7 +52,7 @@ function main() {
52
52
 
53
53
  let state = readJson(plan.statePath);
54
54
  assert.equal(state.workflow.app.id, "end-to-end-golden-path");
55
- assert.equal(state.workflow.app.version, "0.1.93");
55
+ assert.equal(state.workflow.app.version, "0.1.94");
56
56
  assert.equal(state.loopStage, "interpret");
57
57
 
58
58
  const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
@@ -195,7 +195,7 @@ function main() {
195
195
  assert.equal(reportPath, plan.reportPath);
196
196
  assert.ok(fs.existsSync(reportPath));
197
197
  const report = fs.readFileSync(reportPath, "utf8");
198
- assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.93/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.94/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -119,6 +119,40 @@ function runGate() {
119
119
  if (r.status !== 0) die("deterministic gate FAILED — fix findings in normal cycles, do not retry the release here.");
120
120
  }
121
121
 
122
+ // ---- 1b. vendor liveness preflight (cut only) ------------------------------
123
+ // CW promises claude/codex/gemini/deepseek all work; this proves it before a cut.
124
+ // LIVE: spends real tokens on each vendor, so it runs on --cut (release machine
125
+ // has the keys), not on --check. HARD-BLOCK: any vendor not green stops the cut.
126
+ // Escape hatch CW_SKIP_VENDOR_PREFLIGHT=1 (emergencies/offline). Test seam
127
+ // CW_RELEASE_FLOW_PREFLIGHT_CMD overrides the command (smoke stubs it).
128
+ function runVendorPreflight() {
129
+ if (!MODE_CUT) {
130
+ say("[1b/3] vendor preflight — skipped (check mode; live vendor calls run on --cut)");
131
+ return;
132
+ }
133
+ if (process.env.CW_SKIP_VENDOR_PREFLIGHT === "1") {
134
+ say("[1b/3] vendor preflight — SKIPPED via CW_SKIP_VENDOR_PREFLIGHT=1 (operator override)");
135
+ return;
136
+ }
137
+ const vendors = (process.env.CW_PREFLIGHT_VENDORS || "claude,codex,gemini,deepseek").trim();
138
+ const override = (process.env.CW_RELEASE_FLOW_PREFLIGHT_CMD || "").trim();
139
+ say(`[1b/3] vendor preflight — live liveness check: ${vendors}`);
140
+ let r;
141
+ if (override) {
142
+ // Test/override seam — shell:true, smoke/operator only, never the delegated agent.
143
+ r = spawnSync(override, { cwd: repoRoot, encoding: "utf8", shell: true, stdio: "inherit" });
144
+ } else {
145
+ r = spawnSync(process.execPath, [path.join(scriptsDir, "vendor-preflight.js"), "--vendors", vendors], {
146
+ cwd: repoRoot,
147
+ encoding: "utf8",
148
+ stdio: "inherit"
149
+ });
150
+ }
151
+ if (r.status !== 0) {
152
+ die("vendor preflight FAILED — a promised vendor is not live (missing CLI/key or broken). Users must not be limited to one vendor; fix before cutting, or set CW_SKIP_VENDOR_PREFLIGHT=1 to override.");
153
+ }
154
+ }
155
+
122
156
  // ---- 2. independent reviewer, delegated to the configured agent -------------
123
157
  function reviewerPromptBody() {
124
158
  // Reuse the committed reviewer spec as the prompt; strip YAML frontmatter.
@@ -476,6 +510,7 @@ function main() {
476
510
  const inputPath = path.join(markerDir, `review-input-${HEAD}.md`);
477
511
 
478
512
  runGate();
513
+ runVendorPreflight();
479
514
  fs.writeFileSync(inputPath, buildReviewerInput(resultPath));
480
515
  delegateReview(resultPath, inputPath);
481
516
  const capability = verifyVerdict(resultPath);
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // vendor-preflight.js — pre-release liveness gate for ALL agent vendors.
5
+ //
6
+ // CW promises that claude, codex, gemini, and deepseek all work. This gate
7
+ // proves it before a release: it runs EACH builtin wrapper against a tiny
8
+ // throwaway git repo with a trivial question and checks the wrapper returns a
9
+ // real, non-empty result. It is the enforcement behind that promise.
10
+ //
11
+ // HARD-BLOCK policy (operator chose this): a vendor that does not return a valid
12
+ // result — missing CLI, auth error, empty/garbage output — counts as FAIL and
13
+ // the gate exits nonzero. There is no "skip"; an unconfigured vendor blocks.
14
+ //
15
+ // This is a LIVE gate: it spends real tokens on each configured vendor. It is
16
+ // meant for the release machine (where all keys/logins exist), NOT the offline
17
+ // CI test suite. The offline smoke (test/vendor-preflight-smoke.js) exercises
18
+ // this script's LOGIC with PATH shims, no keys.
19
+ //
20
+ // Zero runtime dependency: node + git only. stdout is data (the matrix), stderr
21
+ // is diagnostics — same discipline as the rest of CW.
22
+ //
23
+ // Usage:
24
+ // node vendor-preflight.js [--vendors claude,codex,deepseek] [--question "..."]
25
+ // [--timeout-ms 180000] [--json] [--keep]
26
+ //
27
+ // Test seam (smoke/operator only):
28
+ // CW_PREFLIGHT_AGENTS_DIR override where wrapper scripts + builtin-templates.json
29
+ // are read from (the smoke points this at a shim dir).
30
+
31
+ const fs = require("node:fs");
32
+ const os = require("node:os");
33
+ const path = require("node:path");
34
+ const { spawnSync } = require("node:child_process");
35
+
36
+ function argValue(flag, fallback) {
37
+ const i = process.argv.indexOf(flag);
38
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback;
39
+ }
40
+ const hasFlag = (flag) => process.argv.includes(flag);
41
+
42
+ const agentsDir = process.env.CW_PREFLIGHT_AGENTS_DIR || path.join(__dirname, "agents");
43
+ const manifestPath = path.join(agentsDir, "builtin-templates.json");
44
+ const question = argValue("--question", "In one sentence, what does this repo do? Keep it short.");
45
+ const timeoutMs = Number(argValue("--timeout-ms", "180000")) || 180000;
46
+ const asJson = hasFlag("--json");
47
+ const keep = hasFlag("--keep");
48
+
49
+ function die(msg) {
50
+ process.stderr.write(msg + "\n");
51
+ process.exit(2);
52
+ }
53
+
54
+ let templates;
55
+ try {
56
+ templates = JSON.parse(fs.readFileSync(manifestPath, "utf8")).templates || {};
57
+ } catch (e) {
58
+ die(`vendor-preflight: cannot read ${manifestPath}: ${e.message}`);
59
+ }
60
+
61
+ const onlyRaw = argValue("--vendors", "");
62
+ const only = onlyRaw ? onlyRaw.split(",").map((s) => s.trim()).filter(Boolean) : null;
63
+ const vendors = Object.keys(templates).filter((v) => !only || only.includes(v));
64
+ if (only) {
65
+ for (const v of only) if (!templates[v]) die(`vendor-preflight: unknown vendor "${v}" — known: ${Object.keys(templates).join(", ")}`);
66
+ }
67
+ if (!vendors.length) die("vendor-preflight: no vendors to check");
68
+
69
+ // One throwaway git repo + worker input, reused read-only across vendors.
70
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), "cw-preflight-"));
71
+ const repo = path.join(work, "repo");
72
+ fs.mkdirSync(repo, { recursive: true });
73
+ spawnSync("git", ["init", "-q"], { cwd: repo });
74
+ fs.writeFileSync(path.join(repo, "README.md"), "# Tiny App\n\nA one-file demo. main.js reads input and prints it.\n");
75
+ const inputPath = path.join(repo, "input.md");
76
+ fs.writeFileSync(inputPath, `# Worker preflight\n\n- Result: ${path.join(repo, "result.md")}\n\n## Task\n\n${question}\n`);
77
+
78
+ function checkVendor(name) {
79
+ const script = templates[name];
80
+ const wrapper = path.join(agentsDir, script);
81
+ const resultPath = path.join(repo, `result-${name}.md`);
82
+ try { fs.rmSync(resultPath, { force: true }); } catch { /* ignore */ }
83
+ const started = process.hrtime.bigint();
84
+ const r = spawnSync(process.execPath, [wrapper, inputPath, resultPath], {
85
+ cwd: repo,
86
+ encoding: "utf8",
87
+ timeout: timeoutMs,
88
+ env: { ...process.env, CW_AGENT_STREAM: "0" }
89
+ });
90
+ const ms = Number((process.hrtime.bigint() - started) / 1000000n);
91
+ const result = (() => { try { return fs.readFileSync(resultPath, "utf8"); } catch { return ""; } })();
92
+ let model;
93
+ try { model = JSON.parse(r.stdout || "{}").model; } catch { /* provenance optional */ }
94
+
95
+ let status = "PASS";
96
+ let reason = "";
97
+ if (r.error && r.error.code === "ETIMEDOUT") {
98
+ status = "FAIL"; reason = `timed out after ${timeoutMs}ms`;
99
+ } else if (r.status !== 0) {
100
+ status = "FAIL"; reason = (r.stderr || "").trim().split("\n").pop() || `exit ${r.status}`;
101
+ } else if (!result.trim()) {
102
+ status = "FAIL"; reason = "exit 0 but empty result";
103
+ }
104
+ return { vendor: name, status, reason, ms, model, hasContract: /```cw:result/.test(result) };
105
+ }
106
+
107
+ const rows = vendors.map(checkVendor);
108
+ if (!keep) { try { fs.rmSync(work, { recursive: true, force: true }); } catch { /* ignore */ } }
109
+
110
+ const failed = rows.filter((r) => r.status !== "PASS");
111
+
112
+ if (asJson) {
113
+ process.stdout.write(JSON.stringify({ ok: failed.length === 0, vendors: rows }, null, 2) + "\n");
114
+ } else {
115
+ for (const r of rows) {
116
+ const mark = r.status === "PASS" ? "✓" : "✗";
117
+ const extra = r.status === "PASS"
118
+ ? `${r.ms}ms${r.model ? `, ${r.model}` : ""}${r.hasContract ? "" : ", no cw:result block"}`
119
+ : r.reason;
120
+ process.stdout.write(` ${mark} ${r.vendor.padEnd(10)} ${r.status} (${extra})\n`);
121
+ }
122
+ process.stdout.write(failed.length === 0
123
+ ? `vendor-preflight: ok — ${rows.length}/${rows.length} vendors live\n`
124
+ : `vendor-preflight: BLOCKED — ${failed.length}/${rows.length} vendor(s) not live: ${failed.map((r) => r.vendor).join(", ")}\n`);
125
+ }
126
+
127
+ process.exit(failed.length === 0 ? 0 : 1);