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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- 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/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +12 -6
- package/docs/cli-mcp-parity.7.md +2 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- 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/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 +6 -3
- 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/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/agents/builtin-templates.json +3 -2
- package/scripts/agents/claude-p-agent.js +6 -6
- package/scripts/agents/codex-agent.js +17 -3
- package/scripts/agents/deepseek-agent.js +23 -0
- package/scripts/agents/gemini-opencode-agent.js +25 -0
- package/scripts/agents/opencode-agent.js +57 -8
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +35 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
package/docs/cli-mcp-parity.7.md
CHANGED
package/docs/project-index.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Cool Workflow Project Index
|
|
2
2
|
|
|
3
|
-
Generated from the current repository code on 2026-06-
|
|
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.
|
|
8
|
+
- Version: `0.1.94`
|
|
9
9
|
- Source modules: `68`
|
|
10
10
|
- Workflow apps: `7`
|
|
11
11
|
- Docs: `53`
|
|
12
|
-
- Smoke tests: `
|
|
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)
|
|
@@ -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.
|
|
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
|
@@ -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": "
|
|
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):
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// and vendor-specific stream parsing lives HERE 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
|
|
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 = {
|
|
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
|
-
|
|
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(
|
|
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
|
-
"--
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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, "..", "..");
|
package/scripts/golden-path.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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\.
|
|
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/);
|
package/scripts/release-flow.js
CHANGED
|
@@ -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);
|