cool-workflow
Advanced tools
| # DEMO(7) | ||
| ## NAME | ||
| `cw demo` — prove CW trust guarantees with one command | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js demo tamper [--json] | ||
| node dist/cli.js demo bundle [--json] | ||
| ``` | ||
| ## DESCRIPTION | ||
| `cw demo` is a self-contained proof of CW's central trust claims. It works | ||
| without an agent and without a network connection. Every run is hermetic | ||
| (fully self-contained) — it builds its own state, tampers with it in known | ||
| ways, and checks that the tampering is caught. Nothing is read from or | ||
| written to the real file system outside a short-term temp directory. | ||
| No agent is needed; both demos work when the setup has no agent at all. | ||
| ## DEMO TAMPER | ||
| `cw demo tamper` proves that CW catches forged records offline — with only a | ||
| public key, no server. It: | ||
| 1. Builds a signed telemetry ledger with three hops. | ||
| 2. Tampers with it in three layers: | ||
| - **Hashes**: Changes a record's data and recomputes the record hash to hide | ||
| it. The hash chain breaks — the next record's `previousHash` does not | ||
| match, so the chain is no longer valid. | ||
| - **Signatures**: Inflates token counts and keeps the old signature. The | ||
| signature does not match the new data — the verifier catches it. | ||
| - **Findings**: Edits a signed finding (severity HIGH → LOW) after it was | ||
| signed by the agent. The signature check on the ed25519 envelope fails | ||
| because the signed bytes changed. | ||
| 3. Verifies each tampered ledger with only the public key. | ||
| If all three forgeries are caught, the proof holds and the demo exits 0. | ||
| If any tamper goes undetected, the demo exits 1 — this is a regression in | ||
| the integrity guarantee. | ||
| ## DEMO BUNDLE | ||
| `cw demo bundle` proves that exported report bundles are verifiable offline. It: | ||
| 1. Builds a full telemetry chain, signs it, and exports a sealed portable | ||
| bundle (archive bytes + telemetry chain + trust-audit chain + embedded | ||
| public key). | ||
| 2. Tampers with the bundle in two ways: | ||
| - **Telemetry chain**: Forges a record in the chain. The archive's file | ||
| digests stay valid (the archive was built from the tampered bytes), but | ||
| `report verify-bundle` re-checks the chain and catches it. | ||
| - **Signature + usage**: Inflates token counts and reseals. The signature | ||
| check and hash chain both break. | ||
| 3. Verifies each tampered bundle with `report verify-bundle`. | ||
| If all forgeries are caught with only the bundle's own public key, the proof | ||
| holds. No repo, no server, no key handed over. | ||
| ## EXIT CODES | ||
| | Exit | Meaning | | ||
| | --- | --- | | ||
| | 0 | All tampering was caught — trust guarantees hold | | ||
| | 1 | A tamper went undetected — integrity guarantee regression | | ||
| ## FILES | ||
| ```text | ||
| src/telemetry-demo.ts | ||
| ``` | ||
| ## SEE ALSO | ||
| report-verifiable-bundle.7.md — offline bundle verification in detail | ||
| trust-model.md — the trust model and its limits | ||
| security-trust-hardening.7.md — security and trust hardening |
| # DOCTOR(7) | ||
| ## NAME | ||
| `cw doctor` — check the setup and name all problems with their fixes | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js doctor | ||
| node dist/cli.js doctor --json | ||
| node dist/cli.js doctor --fix | ||
| node dist/cli.js doctor --onramp | ||
| node dist/cli.js doctor --onramp --changed-from origin/main | ||
| ``` | ||
| ## DESCRIPTION | ||
| `cw doctor` is a read-only check of your CW setup, based on `brew doctor`. It | ||
| probes your machine and says what is wrong and what to do about it — before a | ||
| run fails with a strange error. | ||
| The command never makes any file; it only reads. Running it changes nothing on | ||
| disk. | ||
| It gives back a report with one line for every check. Each check has a status | ||
| (`ok`, `warn`, or `fail`) and a clear note. Checks that are not `ok` carry a | ||
| `fix` line with the right command or step to put things right. | ||
| If any check has status `fail`, the command exits with code 1 (non-zero). A | ||
| `warn` (for example, no agent yet — demos and previews still work) does not | ||
| make the exit fail. | ||
| ## CHECKS | ||
| The command runs six checks in order: | ||
| **node** | ||
| : The Node.js version. CW needs v18 or higher. A `fail` here stops everything. | ||
| **agent** | ||
| : The AI agent backend. CW can auto-detect agents (Claude, Codex, Gemini, | ||
| OpenCode) or take one from `CW_AGENT_COMMAND` / `--agent-command`. Without one, | ||
| real runs report `status: blocked`, but `demo` and `--preview` still work. | ||
| **agent-binary** | ||
| : When the agent is set by a command name (not auto or HTTP), this check sees if | ||
| the binary is on `$PATH`. Missing here gives a `warn` — the run will get a clear | ||
| error later, but CW will not guess at a different agent. | ||
| **git** | ||
| : The `git` command. CW uses it for commit place of origin. A `warn` here means | ||
| commit roots will be recorded as absent; no other part of a run needs git. | ||
| **home-registry** | ||
| : The cross-repo run index at `$CW_HOME` (default `$HOME/.local/state/cool-workflow`). | ||
| This location must be writable. A `fail` here blocks discovery across repos. | ||
| **repo-state** | ||
| : The per-repo run store under `<cwd>/.cw`. Must be writable. A `warn` here | ||
| means runs stay in-memory only — you can use `--cwd PATH` to point at another | ||
| writable root. | ||
| ## OPTIONS | ||
| `--json` | ||
| : Give back the full report as a stable JSON object. Good for scripts. | ||
| `--fix` | ||
| : Give back only the fix commands for every non-ok check. Same as running `cw fix` | ||
| by itself. | ||
| `--onramp` | ||
| : Add a quick-start guide to the human output, with recommended checks and a | ||
| three-step path to your first report. | ||
| `--changed-from <ref>` | ||
| : When used with `--onramp`, make the quick-start checks cover only files changed | ||
| since `<ref>` (a Git branch, tag, or commit). Good for CI and code reading. | ||
| ## FILES | ||
| ```text | ||
| src/doctor.ts | ||
| dist/doctor.js | ||
| ``` | ||
| ## EXIT CODES | ||
| | Exit | Meaning | | ||
| | --- | --- | | ||
| | 0 | All checks ok (may have warnings) | | ||
| | 1 | One or more checks have status `fail` | | ||
| ## SEE ALSO | ||
| cw fix — the same checks, but gives back only the fix commands |
| # FIX(7) | ||
| ## NAME | ||
| `cw fix` — give back the fix commands for all setup problems | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js fix | ||
| node dist/cli.js fix --json | ||
| ``` | ||
| ## DESCRIPTION | ||
| `cw fix` runs the same setup checks as `cw doctor`, but gives back only the | ||
| fix commands — one numbered step for every check that has a problem. No | ||
| running check detail, no status glyphs; just the directions you need to put | ||
| things right. | ||
| When the output is empty ("No fixes needed."), the setup is clean and nothing | ||
| needs doing. | ||
| Like `cw doctor`, the command only reads — it never makes a file or does a | ||
| fix on its own. You are meant to run the fix commands yourself. | ||
| If any check has status `fail`, the command exits with code 1. | ||
| ## OPTIONS | ||
| `--json` | ||
| : Give back the full doctor report as a stable JSON object, with the same shape | ||
| as `cw doctor --json`. The `checks` array carries every fix string. | ||
| ## EXIT CODES | ||
| | Exit | Meaning | | ||
| | --- | --- | | ||
| | 0 | No fixes needed — all checks ok or only warnings | | ||
| | 1 | One or more checks have status `fail` | | ||
| ## SEE ALSO | ||
| cw doctor — the full setup check with detail for every check |
| # INIT(7) | ||
| ## NAME | ||
| `cw init` — scaffold a new workflow definition from nothing | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js init <workflow-id> [--title TITLE] [--output PATH] [--force] | ||
| ``` | ||
| ## DESCRIPTION | ||
| `cw init` makes a new workflow definition file — a `.workflow.js` file filled | ||
| with a simple template. The template has a basic run shape: one step with a | ||
| sandbox profile, one evidence gate, and the hooks you need to add your own | ||
| steps. | ||
| This is how you start a new workflow app from zero. After `init`, you have a | ||
| real file you can edit to make your own run shape. | ||
| The workflow id you give is turned into a safe file name (spaces become dashes, | ||
| special signs are taken out). By default, the file is written to the current | ||
| working directory, but you can point it somewhere else with `--output`. | ||
| If a file of that name is already there, the command refuses to overwrite it | ||
| unless you pass `--force`. | ||
| ## OPTIONS | ||
| `--title TITLE` | ||
| : A human name for the workflow. If not given, a title is made from the id. | ||
| `--output PATH` | ||
| : Where to write the workflow file. Default is `<id>.workflow.js` in the | ||
| current directory. | ||
| `--force` | ||
| : Overwrite an existing file. Without this flag, the command fails if the | ||
| file already exists. | ||
| ## EXIT CODES | ||
| | Exit | Meaning | | ||
| | --- | --- | | ||
| | 0 | Workflow file written | | ||
| | 1 | Missing workflow id, invalid id, or file exists without `--force` | | ||
| ## FILES | ||
| ```text | ||
| src/orchestrator.ts (init method) | ||
| src/workflow-app-framework.ts (template renderer) | ||
| ``` | ||
| ## SEE ALSO | ||
| cw list — see all workflow apps you have | ||
| cw info <id> — read the shape of a workflow app | ||
| workflow-app-framework.7.md — the full framework for writing workflow apps | ||
| pipeline-verbs.7.md — plan, dispatch, result (the pipeline engine) |
| # PIPELINE-VERBS(7) | ||
| ## NAME | ||
| `cw plan`, `cw dispatch`, `cw result` — the three core pipeline engine verbs | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js plan <workflow-id> [--question Q] [--repo PATH] [--sandbox PROFILE] | ||
| node dist/cli.js dispatch <run-id> [--sandbox PROFILE] | ||
| node dist/cli.js result <run-id> <task-id> <result-file> | ||
| ``` | ||
| ## DESCRIPTION | ||
| These three verbs are the engine that drives every CW run. A run goes through | ||
| three stages: plan (get ready), dispatch (hand out work), and result (take work | ||
| back). Together they make the CW pipeline loop — a worker gets a task, does it, | ||
| and hands in a result file; CW checks the result and moves the run forward. | ||
| None of these verbs starts or stops the agent host. They give the control-plane | ||
| data that the host reads and acts on. The host keeps its own loop: call | ||
| `dispatch`, give the task to an agent, get back a result file, call `result`. | ||
| ## PLAN | ||
| `cw plan <workflow-id>` makes a new run and gives back its canonical plan | ||
| summary in JSON. The plan has the run id, the first task (or tasks) to do, | ||
| the sandbox profile, and the state of the run. | ||
| The workflow id names a workflow app that gives the run its shape: inputs, | ||
| steps, evidence gates, and sandbox policy. Use `cw list` to see the workflow | ||
| apps you have. | ||
| The plan output is stable JSON, good for scripts and the agent host. | ||
| Options: | ||
| : `--question`, `--repo`, `--sandbox` — the same inputs the workflow app | ||
| expects. Different apps take different inputs; see `cw info <workflow-id>` | ||
| for the list. | ||
| ## DISPATCH | ||
| `cw dispatch <run-id>` makes the next task ready for a worker. It gives back | ||
| a dispatch manifest in JSON: the task id, the prompt, the sandbox profile, and | ||
| the input and output paths the worker should use. | ||
| The dispatch picks the next runnable task in the pipeline. If no task is ready | ||
| — for example, all tasks are done or waiting on evidence — the dispatch payload | ||
| says so, and the host should wait or check the run status. | ||
| Options: | ||
| : `--sandbox PROFILE` — pick a sandbox profile for the worker. The default is | ||
| the one the workflow app asked for. | ||
| ## RESULT | ||
| `cw result <run-id> <task-id> <result-file>` records a worker's result against | ||
| a task. The result file is a Markdown file the agent wrote — it must have a | ||
| `cw:result` JSON fence with the agent's `findings` and `evidence`. | ||
| CW accepts the result, checks it, and advances the run pipeline. If the result | ||
| is bad (missing, broken, or the evidence does not check out), CW rejects it and | ||
| gives back an error feedback record. The host can then try again or give the | ||
| task a different agent. | ||
| After `result`, the run may be done or have more tasks waiting. Check with | ||
| `cw status <run-id>` or `cw next <run-id>`. | ||
| ## FILES | ||
| ```text | ||
| .cw/runs/<run-id>/state.json | ||
| .cw/runs/<run-id>/dispatches/<dispatch-id>.json | ||
| .cw/runs/<run-id>/tasks/<task-id>.json | ||
| .cw/runs/<run-id>/results/<task-id>.md | ||
| .cw/runs/<run-id>/workers/<worker-id>/worker.json | ||
| ``` | ||
| ## PIPELINE FLOW | ||
| ```text | ||
| plan -> dispatch -> [agent does work] -> result -> [dispatch...] -> done | ||
| └─ rejected -> feedback -> retry | ||
| ``` | ||
| ## SEE ALSO | ||
| cw init — make a new workflow definition from nothing | ||
| cw status — see the current state of a run | ||
| cw next — find the next action for a run | ||
| pipeline-runner.7.md — the full pipeline engine detail |
| # ROUTINE(7) | ||
| ## NAME | ||
| `cw routine` — make and manage trigger-based workflow routines | ||
| ## SYNOPSIS | ||
| ```text | ||
| node dist/cli.js routine create --kind api|github --prompt PROMPT [--match JSON] | ||
| node dist/cli.js routine list [--kind KIND] | ||
| node dist/cli.js routine delete <trigger-id> | ||
| node dist/cli.js routine fire <kind> <payload-file> | ||
| node dist/cli.js routine events [<trigger-id>] | ||
| ``` | ||
| ## DESCRIPTION | ||
| `cw routine` is the local trigger bridge for CW. It lets you make named | ||
| triggers that fire when something happens — an API event, a GitHub webhook, or | ||
| another outside signal. Each trigger carries a prompt template; when fired, | ||
| the prompt gets filled with the event data and handed to an agent host. | ||
| CW keeps routine data in: | ||
| ```text | ||
| .cw/routines/triggers.json | ||
| .cw/routines/payloads/ | ||
| ``` | ||
| CW itself does not run a web server or listen for webhooks. The routine bridge | ||
| is a local data store that can be joined to GitHub Actions, webhooks, cron, or | ||
| a small HTTP adapter. | ||
| ## COMMANDS | ||
| **create** | ||
| : Make a new trigger. `--kind` is `api` or `github`. `--prompt` is the prompt | ||
| template the agent will see. `--match` is an optional JSON object that filters | ||
| events (for example, `{"action":"opened"}` for GitHub pull requests). | ||
| **list** | ||
| : List all triggers, or filter by kind with `--kind`. | ||
| **delete** | ||
| : Remove a trigger by its id. | ||
| **fire** | ||
| : Record an event against a trigger. Give the trigger kind and a path to a | ||
| JSON payload file. CW matches the payload against the trigger's match rules | ||
| and fills out the prompt. | ||
| **events** | ||
| : List the events that have been recorded for a trigger. | ||
| ## FILES | ||
| ```text | ||
| .cw/routines/triggers.json | ||
| .cw/routines/payloads/<event-id>.json | ||
| ``` | ||
| ## EXIT CODES | ||
| | Exit | Meaning | | ||
| | --- | --- | | ||
| | 0 | Command done | | ||
| | 1 | Error (bad arguments, missing trigger, etc.) | | ||
| ## SEE ALSO | ||
| cw sched — durable run-queue scheduling for workflow runs | ||
| control-plane-scheduling.7.md — the full scheduling and run management design |
| { | ||
| "name": "cool-workflow", | ||
| "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.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": { | ||
@@ -6,0 +6,0 @@ "name": "COOLWHITE LLC" |
| { | ||
| "name": "cool-workflow", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "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,0 +5,0 @@ "author": { |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Deterministic one-worker workflow app for proving the CW integration chain.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -6,3 +6,3 @@ { | ||
| "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "author": "COOLWHITE LLC", | ||
@@ -9,0 +9,0 @@ "inputs": [ |
@@ -118,3 +118,3 @@ "use strict"; | ||
| // forged candidate.json must throw here rather than flow into the run. | ||
| const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))); | ||
| const candidate = (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file)); | ||
| upsertCandidate(run, candidate); | ||
@@ -541,3 +541,3 @@ return candidate; | ||
| // throws rather than entering the candidate set as a trusted cast. | ||
| .map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")))); | ||
| .map((file) => (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file))); | ||
| } | ||
@@ -555,3 +555,3 @@ function readScores(run, candidateId) { | ||
| // throw, not silently widen the normalized/verdict surface the gate reads. | ||
| .map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")))); | ||
| .map((file) => (0, validation_1.validateCandidateScore)((0, state_1.readJson)(node_path_1.default.join(dir, file)))); | ||
| } | ||
@@ -558,0 +558,0 @@ function candidateArtifacts(run, candidate) { |
@@ -291,3 +291,8 @@ "use strict"; | ||
| const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY; | ||
| return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output), { trustPublicKey }); | ||
| const resolvedOutput = node_path_1.default.resolve(base, output); | ||
| const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//; | ||
| if (sysDirs.test(resolvedOutput)) { | ||
| throw new Error(`Refusing to write archive to a system directory: ${output}`); | ||
| } | ||
| return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), resolvedOutput, { trustPublicKey }); | ||
| } | ||
@@ -294,0 +299,0 @@ function runImportArchive(runner, args) { |
@@ -80,3 +80,9 @@ "use strict"; | ||
| const kind = (0, io_1.required)(idOrKind, "trigger kind"); | ||
| const payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options; | ||
| let payload; | ||
| try { | ||
| payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options; | ||
| } | ||
| catch (e) { | ||
| throw new Error(`Failed to parse payload${payloadPath ? ` file "${payloadPath}"` : ""}: ${String(e && e.message || e)}`); | ||
| } | ||
| (0, io_1.printJson)(triggers.fire(kind, payload)); | ||
@@ -83,0 +89,0 @@ return; |
+10
-0
@@ -478,2 +478,12 @@ "use strict"; | ||
| if (job) { | ||
| const sandboxPolicy = manifest.sandboxPolicy; | ||
| if (sandboxPolicy) { | ||
| const filteredEnv = (0, execution_backend_1.buildChildEnv)(sandboxPolicy); | ||
| for (const key of Object.keys(process.env)) { | ||
| if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) { | ||
| filteredEnv[key] = process.env[key]; | ||
| } | ||
| } | ||
| job.env = filteredEnv; | ||
| } | ||
| jobs.push(job); | ||
@@ -480,0 +490,0 @@ jobTaskIds.push(taskId); |
@@ -42,2 +42,3 @@ "use strict"; | ||
| exports.delegateChildScript = delegateChildScript; | ||
| exports.shouldStreamAgentStderr = shouldStreamAgentStderr; | ||
| exports.createExecutionBackend = createExecutionBackend; | ||
@@ -414,3 +415,3 @@ exports.backendListPayload = backendListPayload; | ||
| const shellArg = [command, ...args].join(" ").replace(/\{\{[a-zA-Z0-9_.-]+\}\}/g, ""); | ||
| if (/[;&|`$(){}<>!\n\r]/.test(shellArg)) { | ||
| if (/[;&|`$(){}<>!\n\r#*?~]/.test(shellArg)) { | ||
| throw new Error(`Shell backend refused: args contain shell control characters. ` + | ||
@@ -680,2 +681,21 @@ `Use the node, bun, or agent backend instead for untrusted inputs.`); | ||
| // stateful runners below build the refusal/delegated envelopes and stay here. | ||
| /** Decide whether cw FORWARDS the agent wrapper's live stderr view (stdio | ||
| * "inherit") or captures it ("pipe"). Default follows isTTY — interactive shows | ||
| * the live view, a pipe/CI stays silent (Rule of Silence). The two env knobs are | ||
| * explicit opt-out/opt-in, honored regardless of isTTY so a piped/CI run can still | ||
| * opt into the wrapper's plain append-only trace, exactly as the man page promises | ||
| * (agent-delegation-drive.7.md "Live output"): | ||
| * CW_NO_STREAM=1 — master off (wins over everything) | ||
| * CW_AGENT_STREAM=0 — off | ||
| * CW_AGENT_STREAM=1 — on, even without a TTY | ||
| * With no env set this returns isTTY — byte-identical to the prior inline gate (POLA). */ | ||
| function shouldStreamAgentStderr(env, isTTY) { | ||
| if (env.CW_NO_STREAM === "1") | ||
| return false; | ||
| if (env.CW_AGENT_STREAM === "0") | ||
| return false; | ||
| if (env.CW_AGENT_STREAM === "1") | ||
| return true; | ||
| return isTTY; | ||
| } | ||
| function runAgentProcess(descriptor, policy, request, label, handle, attestation) { | ||
@@ -698,6 +718,6 @@ const resolved = (0, agent_1.resolveAgentInvocation)(request); | ||
| else { | ||
| // Live output on by default when stderr is a TTY. stdout is always | ||
| // captured as data. CI/pipes stay silent. CW_AGENT_STREAM=0 or | ||
| // CW_NO_STREAM=1 forces off; CW_AGENT_STREAM=1 forces on. | ||
| const streamStderr = process.env.CW_AGENT_STREAM !== "0" && Boolean(process.stderr.isTTY) && process.env.CW_NO_STREAM !== "1"; | ||
| // Live output on by default when stderr is a TTY. stdout is always captured | ||
| // as data. CI/pipes stay silent unless CW_AGENT_STREAM=1 opts them into the | ||
| // wrapper's plain append-only trace; CW_AGENT_STREAM=0 / CW_NO_STREAM=1 force off. | ||
| const streamStderr = shouldStreamAgentStderr(process.env, Boolean(process.stderr.isTTY)); | ||
| // Build child env from sandbox policy as baseline (respects env.inherit/expose/deny), | ||
@@ -704,0 +724,0 @@ // then re-allow CW_* + well-known API key env vars the agent needs. |
@@ -279,3 +279,3 @@ "use strict"; | ||
| encoding: "utf8", | ||
| maxBuffer: 33 * 1024 * 1024 * jobs.length, | ||
| maxBuffer: Math.min(33 * 1024 * 1024 * jobs.length, 512 * 1024 * 1024), | ||
| timeout: maxTimeout + 30000 | ||
@@ -282,0 +282,0 @@ }); |
@@ -38,2 +38,6 @@ #!/usr/bin/env node | ||
| } | ||
| if (message === null || typeof message !== "object" || Array.isArray(message)) { | ||
| sendError(null, -32600, "Invalid Request: not a JSON-RPC object"); | ||
| return; | ||
| } | ||
| try { | ||
@@ -40,0 +44,0 @@ if (message.method === "initialize") { |
+2
-0
@@ -389,2 +389,4 @@ "use strict"; | ||
| function verifyRef(root, ref) { | ||
| if (ref.startsWith("-")) | ||
| throw new Error(`Invalid onramp base ref (must not start with '-'): ${ref}`); | ||
| const resolved = gitOne(root, ["rev-parse", "--verify", `${ref}^{commit}`]); | ||
@@ -391,0 +393,0 @@ if (!resolved) |
@@ -172,2 +172,8 @@ "use strict"; | ||
| const destinationDir = resolveFromBase(String(options.directory || options.output || node_path_1.default.join(appsDir, id))); | ||
| // Reject writes to system-owned directories. The operator may provide any | ||
| // output path, but writing to /etc, /bin, /usr etc. is never valid. | ||
| const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//; | ||
| if (sysDirs.test(node_path_1.default.resolve(destinationDir))) { | ||
| throw new Error(`Refusing to create app in a system directory: ${destinationDir}`); | ||
| } | ||
| const manifestPath = node_path_1.default.join(destinationDir, "app.json"); | ||
@@ -174,0 +180,0 @@ const entrypointPath = node_path_1.default.join(destinationDir, "workflow.js"); |
@@ -115,4 +115,10 @@ "use strict"; | ||
| return raw; | ||
| if (typeof raw === "string") | ||
| return JSON.parse(raw); | ||
| if (typeof raw === "string") { | ||
| try { | ||
| return JSON.parse(raw); | ||
| } | ||
| catch { | ||
| throw new Error(`Invalid JSON in --metadata: ${String(raw).slice(0, 80)}`); | ||
| } | ||
| } | ||
| return undefined; | ||
@@ -119,0 +125,0 @@ } |
@@ -231,2 +231,5 @@ "use strict"; | ||
| const absoluteResultPath = node_path_1.default.resolve(resultPath); | ||
| if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absoluteResultPath)) { | ||
| throw new Error(`Result path must not be a system directory: ${resultPath}`); | ||
| } | ||
| if (!node_fs_1.default.existsSync(absoluteResultPath)) { | ||
@@ -233,0 +236,0 @@ throw new Error(`Result file does not exist: ${absoluteResultPath}`); |
@@ -43,3 +43,3 @@ "use strict"; | ||
| throw new Error(`Migration target not found: ${target}`); | ||
| return { snapshot: JSON.parse(node_fs_1.default.readFileSync(file, "utf8")), contract, dir: node_path_1.default.dirname(file) }; | ||
| return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) }; | ||
| } |
+10
-1
@@ -456,3 +456,12 @@ "use strict"; | ||
| reportExtractedTo = node_path_1.default.resolve(options.extractReportTo); | ||
| node_fs_1.default.writeFileSync(reportExtractedTo, reportContent); | ||
| if (options.cwd) { | ||
| const baseCwd = node_path_1.default.resolve(options.cwd); | ||
| if (!(0, state_1.isContainedPath)(reportExtractedTo, baseCwd)) { | ||
| failedChecks.push({ name: "extract-report", code: "path-outside-working-directory" }); | ||
| reportExtractedTo = undefined; | ||
| } | ||
| } | ||
| if (reportExtractedTo) { | ||
| node_fs_1.default.writeFileSync(reportExtractedTo, reportContent); | ||
| } | ||
| } | ||
@@ -459,0 +468,0 @@ } |
@@ -135,3 +135,8 @@ "use strict"; | ||
| // A non-bundled, non-file id still fails closed via showBundledSandboxProfile. | ||
| const absolute = node_path_1.default.resolve(requested); | ||
| const absolute = node_path_1.default.resolve(context.cwd, requested); | ||
| if (!absolute.startsWith(node_path_1.default.resolve(context.cwd) + node_path_1.default.sep) && absolute !== node_path_1.default.resolve(context.cwd)) { | ||
| throw new SandboxProfileError("sandbox-profile-path-escape", `Custom profile path traversal denied: ${requested}`, { | ||
| details: { requested } | ||
| }); | ||
| } | ||
| if (node_fs_1.default.existsSync(absolute) && node_fs_1.default.statSync(absolute).isFile()) { | ||
@@ -138,0 +143,0 @@ const result = validateSandboxProfileFile(requested, context); |
+7
-1
@@ -150,3 +150,9 @@ "use strict"; | ||
| return value; | ||
| const parsed = JSON.parse(String(value)); | ||
| let parsed; | ||
| try { | ||
| parsed = JSON.parse(String(value)); | ||
| } | ||
| catch { | ||
| throw new Error("Expected a JSON object, got invalid JSON"); | ||
| } | ||
| if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { | ||
@@ -153,0 +159,0 @@ throw new Error("Expected JSON object"); |
+1
-1
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| 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; | ||
| exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.96"; | ||
| exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.97"; | ||
| exports.WORKFLOW_APP_SCHEMA_VERSION = 1; | ||
@@ -6,0 +6,0 @@ exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1; |
@@ -27,2 +27,3 @@ "use strict"; | ||
| const node_path_1 = __importDefault(require("node:path")); | ||
| const node_crypto_1 = __importDefault(require("node:crypto")); | ||
| const workbench_1 = require("./workbench"); | ||
@@ -97,3 +98,8 @@ const ALLOWED_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]); | ||
| const queryToken = url.searchParams.get("token") || ""; | ||
| if (bearer !== requiredToken && queryToken !== requiredToken) { | ||
| const tokenBuf = Buffer.from(requiredToken); | ||
| const bearerBuf = Buffer.from(bearer); | ||
| const queryBuf = Buffer.from(queryToken); | ||
| const tokenOk = bearerBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(bearerBuf, tokenBuf); | ||
| const queryOk = queryBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(queryBuf, tokenBuf); | ||
| if (!tokenOk && !queryOk) { | ||
| return this.send(res, 401, { error: "unauthorized: token mismatch" }); | ||
@@ -100,0 +106,0 @@ } |
@@ -412,1 +412,3 @@ # Agent Delegation Drive | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -554,1 +554,3 @@ # CLI ↔ MCP Parity | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -172,1 +172,3 @@ # Contract Migration Tooling | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -156,1 +156,3 @@ # Control-Plane Scheduling | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -155,1 +155,3 @@ # Durable State & Locking | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -316,1 +316,3 @@ # Evidence Adoption Reasoning Chain | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -346,1 +346,3 @@ # EXECUTION-BACKENDS(7) | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -322,1 +322,3 @@ # Multi-Agent CLI + MCP Surface | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -348,1 +348,3 @@ # Multi-Agent Eval & Replay Harness | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -360,1 +360,3 @@ # Multi-Agent Operator UX | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -181,1 +181,3 @@ # Node Snapshot / Diff / Replay | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -240,1 +240,3 @@ # Observability + Cost Accounting | ||
| 0.1.96 | ||
| 0.1.97 |
| # Cool Workflow Project Index | ||
| Generated from the current repository code on 2026-06-28 by `npm run sync:project-index`. | ||
| Generated from the current repository code on 2026-06-30 by `npm run sync:project-index`. | ||
@@ -8,7 +8,7 @@ ## Snapshot | ||
| - Package: `cool-workflow` | ||
| - Version: `0.1.96` | ||
| - Version: `0.1.97` | ||
| - Source modules: `68` | ||
| - Workflow apps: `8` | ||
| - Docs: `53` | ||
| - Smoke tests: `158` | ||
| - Docs: `59` | ||
| - Smoke tests: `164` | ||
| - Repository: https://github.com/coo1white/cool-workflow | ||
@@ -149,2 +149,4 @@ | ||
| - [Coordinator / Blackboard](coordinator-blackboard.7.md) | ||
| - [DEMO(7)](demo.7.md) | ||
| - [DOCTOR(7)](doctor.7.md) | ||
| - [Dogfood One Real Repo](dogfood-one-real-repo.7.md) | ||
@@ -156,4 +158,6 @@ - [Durable State & Locking](durable-state-and-locking.7.md) | ||
| - [EXECUTION-BACKENDS(7)](execution-backends.7.md) | ||
| - [FIX(7)](fix.7.md) | ||
| - [Getting Started](getting-started.md) | ||
| - [Cool Workflow Docs](index.md) | ||
| - [INIT(7)](init.7.md) | ||
| - [MCP App Surface](mcp-app-surface.7.md) | ||
@@ -170,2 +174,3 @@ - [Multi-Agent CLI + MCP Surface](multi-agent-cli-mcp-surface.7.md) | ||
| - [PIPELINE-RUNNER(7)](pipeline-runner.7.md) | ||
| - [PIPELINE-VERBS(7)](pipeline-verbs.7.md) | ||
| - [Cool Workflow Project Index](project-index.md) | ||
@@ -179,2 +184,3 @@ - [Cool Workflow](readme-v0.1.87-full.md) | ||
| - [Verifiable Report Bundle](report-verifiable-bundle.7.md) | ||
| - [ROUTINE(7)](routine.7.md) | ||
| - [Routines](routines.md) | ||
@@ -204,2 +210,3 @@ - [Run Registry / Control Plane](run-registry-control-plane.7.md) | ||
| - [agent-delegation-drive-smoke.js](../test/agent-delegation-drive-smoke.js) | ||
| - [agent-stream-gate-smoke.js](../test/agent-stream-gate-smoke.js) | ||
| - [append-run-node-no-realloc-smoke.js](../test/append-run-node-no-realloc-smoke.js) | ||
@@ -291,2 +298,5 @@ - [architecture-review-fast-automation-smoke.js](../test/architecture-review-fast-automation-smoke.js) | ||
| - [parity-doc-sync-smoke.js](../test/parity-doc-sync-smoke.js) | ||
| - [parse-guard-smoke.js](../test/parse-guard-smoke.js) | ||
| - [parse-hardening-round2-smoke.js](../test/parse-hardening-round2-smoke.js) | ||
| - [path-containment-smoke.js](../test/path-containment-smoke.js) | ||
| - [pdca-blackboard-loop-smoke.js](../test/pdca-blackboard-loop-smoke.js) | ||
@@ -310,2 +320,3 @@ - [pii-redaction-smoke.js](../test/pii-redaction-smoke.js) | ||
| - [release-gate-smoke.js](../test/release-gate-smoke.js) | ||
| - [release-pipeline-hygiene-smoke.js](../test/release-pipeline-hygiene-smoke.js) | ||
| - [release-tooling-smoke.js](../test/release-tooling-smoke.js) | ||
@@ -334,2 +345,3 @@ - [remote-link-archive-smoke.js](../test/remote-link-archive-smoke.js) | ||
| - [sample-determinism-smoke.js](../test/sample-determinism-smoke.js) | ||
| - [sandbox-env-batch-hardening-smoke.js](../test/sandbox-env-batch-hardening-smoke.js) | ||
| - [sandbox-profile-smoke.js](../test/sandbox-profile-smoke.js) | ||
@@ -336,0 +348,0 @@ - [sched-policy-validation-smoke.js](../test/sched-policy-validation-smoke.js) |
@@ -188,1 +188,3 @@ # Real Execution Backend Integrations | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -328,1 +328,3 @@ # Release And Migration Discipline | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -291,1 +291,3 @@ # Release Tooling | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -471,1 +471,3 @@ # Run Registry / Control Plane | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -239,1 +239,3 @@ # Run Retention & Provable Reclamation | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -317,1 +317,3 @@ # State Explosion Management | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -253,1 +253,3 @@ # Team Collaboration | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -261,1 +261,3 @@ # Web / Desktop Workbench | ||
| 0.1.96 | ||
| 0.1.97 |
@@ -5,3 +5,3 @@ { | ||
| "name": "cool-workflow", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "license": "BSD-2-Clause", | ||
@@ -8,0 +8,0 @@ "homepage": "https://github.com/coo1white/cool-workflow", |
+15
-9
| # Vendor Manifest Source of Truth | ||
| Every agent host scans a different, hard-coded manifest directory | ||
| (`.claude-plugin/`, `.codex-plugin/`, `.agents/`) with a different JSON shape. | ||
| You cannot unify the directory or the schema — so we do not try. Instead, all | ||
| vendor manifests are **generated** from one neutral source and point at the same | ||
| shared runtime (`skills/`, `dist/`, `apps/`, the MCP server). No vendor forks the | ||
| logic; each manifest is a thin adapter. | ||
| (`.claude-plugin/`, `.codex-plugin/`, `.agents/`, `.gemini-plugin/`, | ||
| `.opencode-plugin/`) with a different JSON shape. You cannot unify the directory | ||
| or the schema — so we do not try. Instead, all vendor manifests are **generated** | ||
| from one neutral source and point at the same shared runtime (`skills/`, `dist/`, | ||
| `apps/`, the MCP server). No vendor forks the logic; each manifest is a thin | ||
| adapter. | ||
@@ -28,9 +29,14 @@ This is the mechanism/policy split: shared assets are mechanism, per-vendor | ||
| Five vendors are generated today. Paths are repo-root-relative. | ||
| | Vendor | Marketplace | Plugin manifest | MCP config | MCP path var | | ||
| | --- | --- | --- | --- | --- | | ||
| | Claude Code | `../../../.claude-plugin/marketplace.json` | `../.claude-plugin/plugin.json` | `../.mcp.json` (auto-discovered) | `${CLAUDE_PLUGIN_ROOT}/` | | ||
| | Codex / `.agents` | `../../../.agents/plugins/marketplace.json` | `../.codex-plugin/plugin.json` | `../.codex-plugin/mcp.json` | `./` | | ||
| | Claude Code | `.claude-plugin/marketplace.json` | `plugins/cool-workflow/.claude-plugin/plugin.json` | `plugins/cool-workflow/.mcp.json` (auto-discovered) | `${CLAUDE_PLUGIN_ROOT}/` | | ||
| | Codex | `.agents/plugins/marketplace.json` | `plugins/cool-workflow/.codex-plugin/plugin.json` | `plugins/cool-workflow/.codex-plugin/mcp.json` | `./` | | ||
| | `.agents` | — | `.agents/plugins/cool-workflow/plugin.json` | `.agents/plugins/cool-workflow/mcp.json` | `./` | | ||
| | Gemini | — | `plugins/cool-workflow/.gemini-plugin/plugin.json` | `plugins/cool-workflow/.gemini-plugin/mcp.json` | `./` | | ||
| | OpenCode | — | `plugins/cool-workflow/.opencode-plugin/plugin.json` | `plugins/cool-workflow/.opencode-plugin/mcp.json` | `./` | | ||
| The two vendors read **different** MCP files, so the plugin-root path variable | ||
| never collides. | ||
| Each vendor reads its **own** MCP file, so the plugin-root path variable never | ||
| collides. | ||
@@ -37,0 +43,0 @@ ## Adding a new vendor (Cursor, Windsurf, …) |
+1
-1
| { | ||
| "name": "cool-workflow", | ||
| "version": "0.1.96", | ||
| "version": "0.1.97", | ||
| "bin": { | ||
@@ -5,0 +5,0 @@ "cool-workflow": "scripts/cw.js", |
@@ -460,5 +460,8 @@ #!/usr/bin/env node | ||
| function persistStderr(resultPath, text) { | ||
| const t = String(text || "").trim(); | ||
| let t = String(text || "").trim(); | ||
| if (!t || !resultPath) return; | ||
| try { | ||
| t = String(t).replace(/\b(sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,}|xox[bprs]-[A-Za-z0-9-]{20,}|Bearer\s+\S+|Authorization:\s*\S+|api[_-]?key[=:]\s*\S+|token[=:]\s*\S+)/gi, (match) => match.slice(0, 4) + "***[REDACTED]"); | ||
| const cap = 4096; | ||
| if (t.length > cap) t = t.slice(0, cap) + `\n [truncated from ${t.length} bytes]`; | ||
| const dir = path.join(path.dirname(resultPath), "logs"); | ||
@@ -465,0 +468,0 @@ fs.mkdirSync(dir, { recursive: true }); |
@@ -86,3 +86,3 @@ #!/usr/bin/env node | ||
| "--scope", | ||
| "Cool Workflow v0.1.96", | ||
| "Cool Workflow v0.1.97", | ||
| "--freshness", | ||
@@ -121,3 +121,3 @@ "as of release preparation" | ||
| assert.equal(summary.legacy, false); | ||
| assert.equal(summary.version, "0.1.96"); | ||
| assert.equal(summary.version, "0.1.97"); | ||
@@ -129,3 +129,3 @@ const validation = runJson(["app", "validate", manifestPath]); | ||
| assert.equal(shown.app.id, app.id); | ||
| assert.equal(shown.app.version, "0.1.96"); | ||
| assert.equal(shown.app.version, "0.1.97"); | ||
| assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`); | ||
@@ -141,3 +141,3 @@ assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`); | ||
| assert.equal(state.workflow.app.id, app.id); | ||
| assert.equal(state.workflow.app.version, "0.1.96"); | ||
| assert.equal(state.workflow.app.version, "0.1.97"); | ||
| assert.equal(state.workflow.app.metadata.canonical, true); | ||
@@ -144,0 +144,0 @@ assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`); |
@@ -21,6 +21,13 @@ #!/usr/bin/env node | ||
| let raw = ""; | ||
| const MAX_STDIN_BYTES = 32 * 1024 * 1024; | ||
| process.stdin.setEncoding("utf8"); | ||
| process.stdin.on("data", (d) => (raw += d)); | ||
| process.stdin.on("data", (d) => { if (raw.length < MAX_STDIN_BYTES) raw += d; }); | ||
| process.stdin.on("end", () => { | ||
| const jobs = JSON.parse(raw); | ||
| let jobs; | ||
| try { | ||
| jobs = JSON.parse(raw); | ||
| } catch (e) { | ||
| process.stdout.write(JSON.stringify([{ spawnError: `invalid stdin JSON: ${String(e && e.message || e)}`, exitCode: null, stdout: "" }])); | ||
| return; | ||
| } | ||
| if (!jobs.length) { process.stdout.write("[]"); return; } | ||
@@ -41,3 +48,3 @@ const out = new Array(jobs.length); | ||
| try { | ||
| child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false }); | ||
| child = spawn(job.binary, job.args, { cwd: job.cwd, env: job.env || process.env, shell: false }); | ||
| } catch (error) { | ||
@@ -44,0 +51,0 @@ settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" }); |
@@ -18,3 +18,4 @@ #!/usr/bin/env node | ||
| (async () => { | ||
| const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => (b += c)); process.stdin.on("end", () => res(b)); }); | ||
| const MAX_STDIN_BYTES = 32 * 1024 * 1024; | ||
| const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); }); | ||
| try { | ||
@@ -21,0 +22,0 @@ const job = JSON.parse((await read()) || "{}"); |
@@ -9,3 +9,3 @@ #!/usr/bin/env node | ||
| const TARGET_VERSION = "0.1.96"; | ||
| const TARGET_VERSION = "0.1.97"; | ||
| const PREVIOUS_VERSION = "0.1.31"; | ||
@@ -12,0 +12,0 @@ const pluginRoot = path.resolve(__dirname, ".."); |
@@ -36,3 +36,3 @@ #!/usr/bin/env node | ||
| assert.equal(appValidation.summary.id, "end-to-end-golden-path"); | ||
| assert.equal(appValidation.summary.version, "0.1.96"); | ||
| assert.equal(appValidation.summary.version, "0.1.97"); | ||
@@ -46,3 +46,3 @@ const plan = runJson( | ||
| "--question", | ||
| "Prove the deterministic v0.1.96 end-to-end golden path." | ||
| "Prove the deterministic v0.1.97 end-to-end golden path." | ||
| ], | ||
@@ -57,3 +57,3 @@ pluginRoot | ||
| assert.equal(state.workflow.app.id, "end-to-end-golden-path"); | ||
| assert.equal(state.workflow.app.version, "0.1.96"); | ||
| assert.equal(state.workflow.app.version, "0.1.97"); | ||
| assert.equal(state.loopStage, "interpret"); | ||
@@ -201,3 +201,3 @@ | ||
| const report = fs.readFileSync(reportPath, "utf8"); | ||
| assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.96/); | ||
| assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.97/); | ||
| assert.match(report, /## Candidates/); | ||
@@ -204,0 +204,0 @@ assert.match(report, /## Trust Audit/); |
+24
-16
@@ -157,2 +157,7 @@ #!/usr/bin/env node | ||
| // ---- 2. independent reviewer, delegated to the configured agent ------------- | ||
| // Default reviewer deadline. The zero-trust reviewer re-runs release-gate.sh, | ||
| // whose sequential test suite alone is ~12 min, then reads + reasons over the | ||
| // diff — so a 10-min default guaranteed a timeout on a real release. 30 min gives | ||
| // headroom; override with CW_AGENT_TIMEOUT_MS (or --agent-timeout-ms). | ||
| const REVIEWER_TIMEOUT_MS = 1800000; | ||
| function reviewerPromptBody() { | ||
@@ -284,3 +289,3 @@ // Reuse the committed reviewer spec as the prompt; strip YAML frontmatter. | ||
| encoding: "utf8", | ||
| timeout: cfg.timeoutMs || 600000, | ||
| timeout: cfg.timeoutMs || REVIEWER_TIMEOUT_MS, | ||
| shell: false, | ||
@@ -315,3 +320,3 @@ stdio: ["ignore", "pipe", "inherit"], | ||
| const lib = cfg.endpoint.startsWith("https:") ? https : http; | ||
| const text = postSync(lib, cfg.endpoint, body, cfg.timeoutMs || 600000); | ||
| const text = postSync(lib, cfg.endpoint, body, cfg.timeoutMs || REVIEWER_TIMEOUT_MS); | ||
| if (text === null) die("reviewer endpoint call failed — no verdict trusted."); | ||
@@ -508,14 +513,14 @@ fs.writeFileSync(resultPath, text.endsWith("\n") ? text : `${text}\n`); | ||
| // Regenerate the gated project index after the version bump (PR #87 gate). | ||
| spawnSync("npm", ["run", "sync:project-index", "--", "--repo-only"], { cwd: pluginRoot, stdio: "inherit" }); | ||
| // Belt-and-suspenders: the reviewer agent writes a narration transcript into | ||
| // .cw-release/ that may carry local paths (the operator's home dir). It is | ||
| // .gitignored, but `git add -A` would still stage it if it were ever tracked, | ||
| // and a tracked transcript leaks PII into the immutable tag commit (it tripped | ||
| // pii-redaction-smoke and red-failed release-gate for v0.1.96). Remove any | ||
| // transcript before staging so it can never ride into the tag. | ||
| const releaseDir = path.join(repoRoot, ".cw-release"); | ||
| for (const f of fs.existsSync(releaseDir) ? fs.readdirSync(releaseDir) : []) { | ||
| if (/^transcript.*\.md$/.test(f)) fs.rmSync(path.join(releaseDir, f), { force: true }); | ||
| } | ||
| git(["add", "-A"]); | ||
| // Fail closed: a failed regen must not bake a stale index into the immutable tag. | ||
| const sync = spawnSync("npm", ["run", "sync:project-index", "--", "--repo-only"], { cwd: pluginRoot, encoding: "utf8", stdio: "inherit" }); | ||
| if (sync.status !== 0) die("sync:project-index failed — refusing to cut with a stale project index"); | ||
| // Stage ONLY tracked-file modifications (the bump surfaces, project-index, dist) | ||
| // plus the ONE intended new file: the reviewer verdict. NEVER `git add -A` — an | ||
| // untracked stray (e.g. the reviewer's narration transcript, which carries the | ||
| // operator's local home path) must never ride into the immutable tag commit | ||
| // (that tripped pii-redaction-smoke and red-failed release-gate for v0.1.96). | ||
| // `git add -u` touches tracked files only, so no untracked file can be swept in; | ||
| // the verdict is the single new path the cut is allowed to add. | ||
| git(["add", "-u"]); | ||
| git(["add", "--", path.relative(repoRoot, resultPath)]); | ||
| const commit = git(["commit", "-m", `chore(release): record APPROVED reviewer verdict for v${cutVersion}`]); | ||
@@ -526,4 +531,7 @@ if (commit.code !== 0) die("verdict commit failed", commit.err); | ||
| if (PUSH) { | ||
| git(["push", "origin", "HEAD"]); | ||
| git(["push", "origin", `v${cutVersion}`]); | ||
| // Atomic: the verdict commit on HEAD and the tag land together or not at all. | ||
| // A non-atomic two-push could leave main advanced with no tag, so CI's | ||
| // release-gate (which fires on the tag) never runs and the release silently stalls. | ||
| const push = git(["push", "--atomic", "origin", "HEAD", `v${cutVersion}`]); | ||
| if (push.code !== 0) die("atomic push of HEAD + tag failed (nothing partially pushed)", push.err); | ||
| } | ||
@@ -530,0 +538,0 @@ say(`tagged v${cutVersion}${PUSH ? " and pushed" : " (local only; push when ready)"}`); |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2871192
0.74%310
1.97%46091
0.23%220
0.46%