cool-workflow 0.2.3 → 0.2.4
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/cli/dispatch.js +2 -1
- package/dist/cli/io.js +6 -20
- package/dist/core/capability-data.js +2 -2
- package/dist/core/format/help.js +7 -1
- package/dist/core/util/cli-args.js +33 -0
- package/dist/core/version.js +1 -1
- package/dist/shell/drive.js +14 -6
- package/dist/shell/execution-backend/agent.js +10 -1
- package/dist/shell/execution-backend/local.js +15 -10
- package/dist/shell/pipeline-cli.js +20 -2
- package/dist/shell/reclamation-io.js +74 -6
- package/dist/shell/registry-cli.js +4 -0
- package/dist/shell/run-registry-io.js +1 -0
- package/dist/shell/workflow-app-loader.js +67 -1
- package/dist/wiring/capability-table/basics.js +2 -2
- package/dist/wiring/capability-table/exec-backend.js +9 -9
- package/dist/wiring/capability-table/multi-agent.js +69 -69
- package/dist/wiring/capability-table/pipeline.js +28 -28
- package/dist/wiring/capability-table/registry-core.js +3 -3
- package/dist/wiring/capability-table/reporting.js +38 -38
- package/dist/wiring/capability-table/scheduling-registry.js +58 -58
- package/dist/wiring/capability-table/state.js +34 -34
- package/dist/wiring/capability-table/trust-ledger.js +12 -12
- package/dist/wiring/capability-table/workflow-apps.js +15 -15
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +18 -13
- 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 +8 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +20 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +22 -3
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +2 -1
- package/scripts/block-unapproved-tag.sh +19 -4
- package/scripts/bump-version.js +27 -10
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +0 -30
- package/scripts/release-flow.js +137 -12
- package/scripts/release-oneclick.js +407 -0
- package/scripts/version-sync-check.js +39 -22
|
@@ -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.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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.2.
|
|
6
|
+
"version": "0.2.4",
|
|
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.2.
|
|
6
|
+
"version": "0.2.4",
|
|
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.2.
|
|
6
|
+
"version": "0.2.4",
|
|
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.2.
|
|
6
|
+
"version": "0.2.4",
|
|
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.2.
|
|
6
|
+
"version": "0.2.4",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -27,6 +27,7 @@ const capability_table_1 = require("../core/capability-table");
|
|
|
27
27
|
const parseargv_1 = require("./parseargv");
|
|
28
28
|
Object.defineProperty(exports, "KNOWN_COMMANDS", { enumerable: true, get: function () { return parseargv_1.KNOWN_COMMANDS; } });
|
|
29
29
|
const io_1 = require("./io");
|
|
30
|
+
const cli_args_1 = require("../core/util/cli-args");
|
|
30
31
|
function firstPositional(args, index = 0) {
|
|
31
32
|
return args.positionals[index];
|
|
32
33
|
}
|
|
@@ -38,7 +39,7 @@ function firstPositional(args, index = 0) {
|
|
|
38
39
|
* `--json`/`--format json`.
|
|
39
40
|
* - `"human"` — always prints `result.text`; there is no JSON form. */
|
|
40
41
|
function renderCliResult(result, jsonMode, options) {
|
|
41
|
-
const useJson = jsonMode === "default" || (jsonMode === "flag" && (0,
|
|
42
|
+
const useJson = jsonMode === "default" || (jsonMode === "flag" && (0, cli_args_1.wantsJson)(options));
|
|
42
43
|
if (useJson && result.json !== undefined) {
|
|
43
44
|
(0, io_1.printJson)(result.json);
|
|
44
45
|
}
|
package/dist/cli/io.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// cli/io.ts — shared CLI input/output helpers.
|
|
3
3
|
//
|
|
4
|
-
// Byte-exact port of src/cli/io.ts in the old build. Pure + zero-dep
|
|
5
|
-
//
|
|
4
|
+
// Byte-exact port of src/cli/io.ts in the old build. Pure + zero-dep JSON
|
|
5
|
+
// stdout, plus one impure TTY-aware help formatter. See SPEC/cli-surface.md
|
|
6
|
+
// "Shared io helpers". Arg-coercion helpers (`required`/`optionalArg`/
|
|
7
|
+
// `wantsJson`) moved to core/util/cli-args.ts (architecture-review P2) —
|
|
8
|
+
// wiring/capability-table/*.ts needed them directly, and a cli/-layer file
|
|
9
|
+
// may not be imported by wiring/ (scripts/purity-gate.js's layer rule).
|
|
6
10
|
//
|
|
7
11
|
// MILESTONE 11 (reporting/observability) adds `styledHelp` — the one
|
|
8
12
|
// place `formatHelp()`'s plain text gets its "Cool Workflow" header
|
|
@@ -11,10 +15,7 @@
|
|
|
11
15
|
// shell/term.ts's env/TTY read.
|
|
12
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
17
|
exports.styledHelp = styledHelp;
|
|
14
|
-
exports.required = required;
|
|
15
|
-
exports.optionalArg = optionalArg;
|
|
16
18
|
exports.printJson = printJson;
|
|
17
|
-
exports.wantsJson = wantsJson;
|
|
18
19
|
const help_1 = require("../core/format/help");
|
|
19
20
|
const safe_json_1 = require("../core/format/safe-json");
|
|
20
21
|
const term_1 = require("../shell/term");
|
|
@@ -24,17 +25,6 @@ function styledHelp() {
|
|
|
24
25
|
const text = (0, help_1.formatHelp)();
|
|
25
26
|
return text.replace(/^Cool Workflow\n/, `${(0, term_1.bold)("Cool Workflow")}\n`);
|
|
26
27
|
}
|
|
27
|
-
/** Require a positional/option value or fail with a copy-pasteable recovery tip. */
|
|
28
|
-
function required(value, label) {
|
|
29
|
-
if (!value) {
|
|
30
|
-
throw new Error(`Missing ${label}.\n Tip: find run ids with "cw run list" or create one with "cw quickstart"`);
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
33
|
-
}
|
|
34
|
-
/** Normalize an optional CLI arg to a trimmed non-empty string, else undefined. */
|
|
35
|
-
function optionalArg(value) {
|
|
36
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
37
|
-
}
|
|
38
28
|
/** Machine payload to stdout (stdout = data; never colored, never chrome).
|
|
39
29
|
* Byte-capped via safeJsonStringify — an aggregate result too large to be
|
|
40
30
|
* useful (or large enough to blow V8's string limit) prints a small
|
|
@@ -42,7 +32,3 @@ function optionalArg(value) {
|
|
|
42
32
|
function printJson(value) {
|
|
43
33
|
process.stdout.write(`${(0, safe_json_1.safeJsonStringify)(value)}\n`);
|
|
44
34
|
}
|
|
45
|
-
/** True when the caller asked for JSON output (`--json` or `--format json`). */
|
|
46
|
-
function wantsJson(options) {
|
|
47
|
-
return Boolean(options.json || options.format === "json");
|
|
48
|
-
}
|
|
@@ -257,8 +257,8 @@ exports.MCP_TOOL_DATA = [
|
|
|
257
257
|
{ tool: "cw_sched_reset", capability: "sched.reset", requiredArgs: [], properties: ["cwd", "id"], description: "Reset a parked entry to ready (operator recovery)." },
|
|
258
258
|
{ tool: "cw_sched_policy_show", capability: "sched.policy.show", requiredArgs: [], properties: ["cwd"], description: "Show the scheduling policy (file or default)." },
|
|
259
259
|
{ tool: "cw_sched_policy_set", capability: "sched.policy.set", requiredArgs: [], properties: ["cwd", "maxConcurrent", "maxAttempts", "leaseTtlMs", "backoffBaseMs", "backoffFactor", "backoffCapMs"], description: "Set scheduling policy fields (concurrency/attempts/backoff/TTL)." },
|
|
260
|
-
{ tool: "cw_gc_plan", capability: "gc.plan", requiredArgs: [], properties: ["cwd", "scope", "runId", "reclaimAfterArchiveDays", "keepScratch", "keepSnapshots"], description: "Dry-run plan of run reclamation (per-kind bytes + capability downgrade); frees nothing." },
|
|
261
|
-
{ tool: "cw_gc_run", capability: "gc.run", requiredArgs: [], properties: ["cwd", "scope", "runId", "reclaimAfterArchiveDays", "keepScratch", "keepSnapshots", "limit", "actor"], description: "Execute the write-ahead reclamation transaction (skeleton -> tombstone -> fsync -> free)." },
|
|
260
|
+
{ tool: "cw_gc_plan", capability: "gc.plan", requiredArgs: [], properties: ["cwd", "scope", "runId", "reclaimAfterArchiveDays", "keepScratch", "keepSnapshots", "keepCommits"], description: "Dry-run plan of run reclamation (per-kind bytes + capability downgrade); frees nothing." },
|
|
261
|
+
{ tool: "cw_gc_run", capability: "gc.run", requiredArgs: [], properties: ["cwd", "scope", "runId", "reclaimAfterArchiveDays", "keepScratch", "keepSnapshots", "keepCommits", "limit", "actor"], description: "Execute the write-ahead reclamation transaction (skeleton -> tombstone -> fsync -> free)." },
|
|
262
262
|
{ tool: "cw_gc_verify", capability: "gc.verify", requiredArgs: ["runId"], properties: ["cwd", "scope", "runId"], description: "Re-prove a reclaimed run: skeleton-complete, tombstone chain untampered, artifacts reconstructable." },
|
|
263
263
|
{ tool: "cw_clones_list", capability: "clones.list", requiredArgs: [], properties: [], description: "List the cached remote-source checkouts that --link/URL reviews populate (origin URL, kind, commit, age, bytes). Read-only." },
|
|
264
264
|
{ tool: "cw_clones_gc", capability: "clones.gc", requiredArgs: [], properties: ["olderThanDays", "all"], description: "Reclaim cached remote-source checkouts: a TTL sweep (--older-than-days, default 30) or --all. Deletes only inside the clones cache." },
|
package/dist/core/format/help.js
CHANGED
|
@@ -228,7 +228,12 @@ const COMMAND_HELP_ROWS = {
|
|
|
228
228
|
* capability-table rows that now support `--json`/`--format json`
|
|
229
229
|
* (io.ts's wantsJson) — the flag existed but was never documented here —
|
|
230
230
|
* and for a `--quiet` Flags row, a new CLI spelling of the existing
|
|
231
|
-
* CW_DRIVE_PROGRESS=0 env var (see cli/entry.ts)
|
|
231
|
+
* CW_DRIVE_PROGRESS=0 env var (see cli/entry.ts); and for a `--resume`
|
|
232
|
+
* Flags row (architecture-review-driven fix), documenting the existing
|
|
233
|
+
* `--resume --run <id>` continuation flag (shell/pipeline-cli.ts's
|
|
234
|
+
* quickstartRun) that previously existed only in code comments and an
|
|
235
|
+
* auto-generated continue hint — a user who never happened to type it
|
|
236
|
+
* first had no way to discover it via `cw help`. */
|
|
232
237
|
function formatHelp() {
|
|
233
238
|
const moreCommandsLines = wrapPipeJoined(exports.MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
|
|
234
239
|
const lines = [
|
|
@@ -253,6 +258,7 @@ function formatHelp() {
|
|
|
253
258
|
" --no-color Disable ANSI color (also honors NO_COLOR / FORCE_COLOR)",
|
|
254
259
|
" --json Print JSON for commands that support it",
|
|
255
260
|
" --quiet Suppress [drive] progress lines (not agent output)",
|
|
261
|
+
" --resume --run <id> Continue an interrupted run to completion",
|
|
256
262
|
"",
|
|
257
263
|
"More commands",
|
|
258
264
|
...moreCommandsLines,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/util/cli-args.ts — shared CLI argv / MCP tool-call arg-coercion
|
|
3
|
+
// helpers: required(), optionalArg(), wantsJson().
|
|
4
|
+
//
|
|
5
|
+
// Pure. No fs, no child_process, no net, no process.env, no Date.now(), no
|
|
6
|
+
// Math.random().
|
|
7
|
+
//
|
|
8
|
+
// Moved out of cli/io.ts (architecture-review P2): a cli/-layer file may
|
|
9
|
+
// not be imported by wiring/ (see scripts/purity-gate.js's layer rule),
|
|
10
|
+
// but every wiring/capability-table/*.ts slice called these three pure
|
|
11
|
+
// functions directly to coerce its own handler's args. Shared by CLI argv
|
|
12
|
+
// (cli/dispatch.ts, the wiring slices) AND MCP tool-call args (the same
|
|
13
|
+
// wiring slices' MCP handler bodies) — see core/util/numeric-flag.ts for
|
|
14
|
+
// the same CLI/MCP-shared framing.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.required = required;
|
|
17
|
+
exports.optionalArg = optionalArg;
|
|
18
|
+
exports.wantsJson = wantsJson;
|
|
19
|
+
/** Require a positional/option value or fail with a copy-pasteable recovery tip. */
|
|
20
|
+
function required(value, label) {
|
|
21
|
+
if (!value) {
|
|
22
|
+
throw new Error(`Missing ${label}.\n Tip: find run ids with "cw run list" or create one with "cw quickstart"`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
/** Normalize an optional CLI arg to a trimmed non-empty string, else undefined. */
|
|
27
|
+
function optionalArg(value) {
|
|
28
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** True when the caller asked for JSON output (`--json` or `--format json`). */
|
|
31
|
+
function wantsJson(options) {
|
|
32
|
+
return Boolean(options.json || options.format === "json");
|
|
33
|
+
}
|
package/dist/core/version.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA
|
|
|
7
7
|
// `cw version` output does not change bytes just because the code under it
|
|
8
8
|
// was rebuilt. See SPEC/cli-surface.md "Exact outputs > Version" and
|
|
9
9
|
// conformance/cases/version-basic.case.js (regex `/^\d+\.\d+\.\d+\n$/`).
|
|
10
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.
|
|
10
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.4";
|
|
11
11
|
// State-kernel schema version constants (SPEC/state-core.md "Version
|
|
12
12
|
// constants"). Pinned to the old build's src/version.ts byte-for-byte.
|
|
13
13
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
package/dist/shell/drive.js
CHANGED
|
@@ -717,6 +717,14 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
717
717
|
}
|
|
718
718
|
if (jobs.length) {
|
|
719
719
|
emitProgress(`⇉ concurrent round: ${jobs.length} agent${jobs.length > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
720
|
+
// Every task above that reached "pending" got dispatched (workerId
|
|
721
|
+
// assigned) in the round-cached run object, but nothing durable was
|
|
722
|
+
// written for it — unlike the serial path's own dispatch branch, which
|
|
723
|
+
// always checkpoints immediately. Flush now, BEFORE the batch's long
|
|
724
|
+
// spawn window opens: a crash mid-spawn (which can run for minutes)
|
|
725
|
+
// then leaves state.json correctly showing these tasks as dispatched,
|
|
726
|
+
// instead of losing the dispatch entirely.
|
|
727
|
+
(0, run_store_1.saveCheckpoint)(loadRun(ctx));
|
|
720
728
|
}
|
|
721
729
|
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
722
730
|
const outcomes = new Map();
|
|
@@ -896,14 +904,14 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
|
|
|
896
904
|
* finishes its own bookkeeping first. A SECOND signal means the caller
|
|
897
905
|
* wants out right now, not a graceful stop, and force-exits immediately
|
|
898
906
|
* with the conventional 128+signum code. */
|
|
899
|
-
function createStopSignalController() {
|
|
907
|
+
function createStopSignalController(runId) {
|
|
900
908
|
let interruptedBy;
|
|
901
909
|
let stopSignalHits = 0;
|
|
902
910
|
const onStopSignal = (signal) => {
|
|
903
911
|
stopSignalHits += 1;
|
|
904
912
|
if (stopSignalHits === 1) {
|
|
905
913
|
interruptedBy = signal;
|
|
906
|
-
emitProgress(`received ${signal} — stopping after the current step (
|
|
914
|
+
emitProgress(`received ${signal} — stopping after the current step (resume: cw run resume ${runId} --drive)`);
|
|
907
915
|
return;
|
|
908
916
|
}
|
|
909
917
|
// A second signal means the caller wants out right now, not a graceful stop.
|
|
@@ -952,11 +960,11 @@ function finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhau
|
|
|
952
960
|
if (interruptedBy) {
|
|
953
961
|
exhaustedMaxIterations = false;
|
|
954
962
|
if (!alreadyComplete) {
|
|
955
|
-
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive interrupted by ${interruptedBy} —
|
|
963
|
+
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive interrupted by ${interruptedBy} — resume: cw run resume ${ctx.runId} --drive` }));
|
|
956
964
|
}
|
|
957
965
|
}
|
|
958
966
|
else if (exhaustedMaxIterations) {
|
|
959
|
-
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state` }));
|
|
967
|
+
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state — resume: cw run resume ${ctx.runId} --drive` }));
|
|
960
968
|
}
|
|
961
969
|
const statusInputs = {
|
|
962
970
|
once: Boolean(options.once),
|
|
@@ -1002,7 +1010,7 @@ function drive(runId, cwd, options = {}) {
|
|
|
1002
1010
|
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1003
1011
|
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1004
1012
|
let exhaustedMaxIterations = !options.once;
|
|
1005
|
-
const stopSignal = createStopSignalController();
|
|
1013
|
+
const stopSignal = createStopSignalController(runId);
|
|
1006
1014
|
stopSignal.install();
|
|
1007
1015
|
try {
|
|
1008
1016
|
for (let i = 0; i < maxIter; i++) {
|
|
@@ -1051,7 +1059,7 @@ async function driveAsync(runId, cwd, options = {}) {
|
|
|
1051
1059
|
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1052
1060
|
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1053
1061
|
let exhaustedMaxIterations = !options.once;
|
|
1054
|
-
const stopSignal = createStopSignalController();
|
|
1062
|
+
const stopSignal = createStopSignalController(runId);
|
|
1055
1063
|
stopSignal.install();
|
|
1056
1064
|
try {
|
|
1057
1065
|
for (let i = 0; i < maxIter; i++) {
|
|
@@ -87,14 +87,23 @@ const AGENT_PROVIDER_KEY_ENV_RE = /^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CO
|
|
|
87
87
|
* worker.agent-env trust-audit event. */
|
|
88
88
|
function buildAgentChildEnv(policy, baseEnv = process.env) {
|
|
89
89
|
const env = (0, local_1.buildChildEnv)(policy, baseEnv);
|
|
90
|
+
// deny must win here too: buildChildEnv already applied it above, but the
|
|
91
|
+
// provider-key/USER re-add below exists specifically to put vars BACK
|
|
92
|
+
// that buildChildEnv's readonly/locked-down defaults strip — without this
|
|
93
|
+
// check that re-add silently overrode an operator's explicit deny (e.g.
|
|
94
|
+
// deny:["AWS_SECRET_ACCESS_KEY"] still forwarded it, since AWS_ matches
|
|
95
|
+
// AGENT_PROVIDER_KEY_ENV_RE).
|
|
96
|
+
const denied = new Set(policy.env.deny || []);
|
|
90
97
|
const forwarded = [];
|
|
91
98
|
for (const key of Object.keys(baseEnv)) {
|
|
99
|
+
if (denied.has(key))
|
|
100
|
+
continue;
|
|
92
101
|
if (AGENT_PROVIDER_KEY_ENV_RE.test(key)) {
|
|
93
102
|
env[key] = baseEnv[key];
|
|
94
103
|
forwarded.push(key);
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
|
-
if (baseEnv.USER !== undefined && env.USER === undefined) {
|
|
106
|
+
if (!denied.has("USER") && baseEnv.USER !== undefined && env.USER === undefined) {
|
|
98
107
|
env.USER = baseEnv.USER;
|
|
99
108
|
forwarded.push("USER");
|
|
100
109
|
}
|
|
@@ -19,16 +19,21 @@ function messageOf(error) {
|
|
|
19
19
|
return error instanceof Error ? error.message : String(error);
|
|
20
20
|
}
|
|
21
21
|
function buildChildEnv(policy, baseEnv = process.env) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
// deny must win regardless of inherit: a custom profile combining
|
|
23
|
+
// inherit:true with deny:[...] (a valid, normalized combination — see
|
|
24
|
+
// sandbox-profile.ts's normalizeEnv) means "everything EXCEPT these".
|
|
25
|
+
// An early return on inherit before this loop ran meant deny was
|
|
26
|
+
// silently skipped whenever inherit was true.
|
|
27
|
+
const env = policy.env.inherit ? { ...baseEnv } : {};
|
|
28
|
+
if (!policy.env.inherit) {
|
|
29
|
+
if (baseEnv.PATH !== undefined)
|
|
30
|
+
env.PATH = baseEnv.PATH;
|
|
31
|
+
if (baseEnv.HOME !== undefined)
|
|
32
|
+
env.HOME = baseEnv.HOME;
|
|
33
|
+
for (const name of policy.env.expose || []) {
|
|
34
|
+
if (baseEnv[name] !== undefined)
|
|
35
|
+
env[name] = baseEnv[name];
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
for (const name of policy.env.deny || []) {
|
|
34
39
|
delete env[name];
|
|
@@ -159,6 +159,24 @@ function planInputsFor(args) {
|
|
|
159
159
|
function invocationCwd(args) {
|
|
160
160
|
return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
|
|
161
161
|
}
|
|
162
|
+
/** Same as invocationCwd, but ALSO falls back to `--repo` before
|
|
163
|
+
* process.cwd() -- for resolving an EXISTING run's directory specifically
|
|
164
|
+
* (loadRunFromCwd's target), never for a fresh plan()'s repo input (see
|
|
165
|
+
* planRun's own comment on why THAT path deliberately never auto-fills
|
|
166
|
+
* repo from cwd). A caller resuming a run they didn't originally start
|
|
167
|
+
* from this exact directory (the common case: `cw run resume <id>
|
|
168
|
+
* --drive` or `cw quickstart <app> --resume --run <id>` typed from
|
|
169
|
+
* wherever the terminal happens to be, not necessarily the run's own
|
|
170
|
+
* --repo) must not have that already-known fact silently discarded in
|
|
171
|
+
* favor of process.cwd() -- this mirrors the --preview branch's own
|
|
172
|
+
* repoCwd fallback a few lines below, which already gets this right. */
|
|
173
|
+
function existingRunCwd(args) {
|
|
174
|
+
if (typeof args.cwd === "string" && args.cwd.trim())
|
|
175
|
+
return path.resolve(args.cwd);
|
|
176
|
+
if (typeof args.repo === "string" && args.repo.trim())
|
|
177
|
+
return path.resolve(args.repo);
|
|
178
|
+
return process.cwd();
|
|
179
|
+
}
|
|
162
180
|
/** True for a value that counts as "not supplied" — byte-exact to the old
|
|
163
181
|
* build's cli-options.isMissing (undefined / null / empty string). */
|
|
164
182
|
function isMissingInput(value) {
|
|
@@ -239,7 +257,7 @@ async function runDriveStep(args) {
|
|
|
239
257
|
incremental: Boolean(args.incremental),
|
|
240
258
|
};
|
|
241
259
|
if (existingRunId) {
|
|
242
|
-
const cwd =
|
|
260
|
+
const cwd = existingRunCwd(args);
|
|
243
261
|
const run = (0, run_store_1.loadRunFromCwd)(existingRunId, cwd);
|
|
244
262
|
return (0, drive_1.driveAsync)(existingRunId, run.cwd, options);
|
|
245
263
|
}
|
|
@@ -502,7 +520,7 @@ async function quickstartRun(args) {
|
|
|
502
520
|
};
|
|
503
521
|
let run;
|
|
504
522
|
if (existingRunId) {
|
|
505
|
-
run = (0, run_store_1.loadRunFromCwd)(existingRunId,
|
|
523
|
+
run = (0, run_store_1.loadRunFromCwd)(existingRunId, existingRunCwd(args));
|
|
506
524
|
}
|
|
507
525
|
else {
|
|
508
526
|
run = (0, pipeline_1.plan)(resolveWorkflowAppForPlan(appId), planInputsFor(args));
|
|
@@ -472,6 +472,31 @@ function planReclamation(run, policy = {}) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
|
+
// (3) Superseded, non-verifier-gated commit snapshots. Each commitState()
|
|
476
|
+
// call embeds the FULL run into commits/<id>.json, so these grow without
|
|
477
|
+
// bound (both in count and per-file size) with no reclamation path today.
|
|
478
|
+
// Only the run's LATEST commit and any verifier-gated commit (the actual
|
|
479
|
+
// audit-significant milestones) are kept — an intermediate, non-gated
|
|
480
|
+
// "checkpoint" commit's only value is as a point-in-time snapshot, and
|
|
481
|
+
// state.json (not commits/) is the source of truth for resume. Not
|
|
482
|
+
// reconstructable (no recipe): a commit snapshot is a genuine
|
|
483
|
+
// point-in-time capture, not a projection derivable from retained data.
|
|
484
|
+
let reclaimedCommitSnapshot = false;
|
|
485
|
+
if (!policy.keepCommits) {
|
|
486
|
+
const commits = run.commits || [];
|
|
487
|
+
for (let i = 0; i < commits.length - 1; i++) {
|
|
488
|
+
const commit = commits[i];
|
|
489
|
+
if (commit.verifierGated)
|
|
490
|
+
continue;
|
|
491
|
+
if (!commit.snapshotPath || !fs.existsSync(commit.snapshotPath))
|
|
492
|
+
continue;
|
|
493
|
+
const bytes = dirBytes(commit.snapshotPath);
|
|
494
|
+
if (bytes <= 0)
|
|
495
|
+
continue;
|
|
496
|
+
freeable.push({ path: rel(commit.snapshotPath), absPath: commit.snapshotPath, kind: "commit-snapshot", bytes });
|
|
497
|
+
reclaimedCommitSnapshot = true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
475
500
|
// Determinism (HARD constraint): sort by path BEFORE anything hashes it,
|
|
476
501
|
// so tombstoneHash is reproducible across hosts regardless of
|
|
477
502
|
// fs.readdirSync's filesystem-dependent order.
|
|
@@ -484,7 +509,15 @@ function planReclamation(run, policy = {}) {
|
|
|
484
509
|
}
|
|
485
510
|
let capability = "re-runnable";
|
|
486
511
|
let capabilityReason = "scratch-only-reclaimed";
|
|
487
|
-
|
|
512
|
+
// A reclaimed commit snapshot is never reconstructable (a genuine
|
|
513
|
+
// point-in-time capture, no recipe) — it caps capability at "verify-only"
|
|
514
|
+
// regardless of what node-snapshot reclamation achieved, same as an
|
|
515
|
+
// unreconstructable node snapshot would.
|
|
516
|
+
if (reclaimedCommitSnapshot) {
|
|
517
|
+
capability = "verify-only";
|
|
518
|
+
capabilityReason = "snapshot-reclaimed-no-reconstruction";
|
|
519
|
+
}
|
|
520
|
+
else if (reclaimedSnapshot && reconstructableSnapshot) {
|
|
488
521
|
capability = "re-runnable-by-reconstruction";
|
|
489
522
|
capabilityReason = "inputs-and-expectdigest-retained";
|
|
490
523
|
}
|
|
@@ -595,13 +628,22 @@ function commitTombstone(run, tombstone) {
|
|
|
595
628
|
function prepareFree(run, tombstone) {
|
|
596
629
|
const runDir = run.paths.runDir;
|
|
597
630
|
const scratchDirs = tombstone.freed.filter((f) => f.kind === "scratch").map((f) => (0, fs_atomic_1.realResolve)(path.join(runDir, f.path)));
|
|
598
|
-
|
|
631
|
+
const commitSnapshotPaths = tombstone.freed.filter((f) => f.kind === "commit-snapshot").map((f) => (0, fs_atomic_1.realResolve)(path.join(runDir, f.path)));
|
|
632
|
+
if (!scratchDirs.length && !commitSnapshotPaths.length)
|
|
599
633
|
return;
|
|
600
634
|
const repointed = new Set();
|
|
601
635
|
for (const scratchDir of scratchDirs) {
|
|
602
636
|
for (const id of repointResultNodeArtifacts(run, scratchDir))
|
|
603
637
|
repointed.add(id);
|
|
604
638
|
}
|
|
639
|
+
// Unlike scratch (which has a retained "result" artifact to repoint to),
|
|
640
|
+
// a reclaimed commit snapshot has no surviving alternative — its OWN
|
|
641
|
+
// StateNode's "snapshot" artifact (recordCommitNode, shell/commit.ts) is
|
|
642
|
+
// the only reference to it, so it is stripped outright rather than
|
|
643
|
+
// repointed. node.outputs.snapshotPath (a plain metadata string, not an
|
|
644
|
+
// artifact the check below inspects) is left as a historical record,
|
|
645
|
+
// same as commit.snapshotPath itself staying in state.json.
|
|
646
|
+
stripCommitSnapshotArtifacts(run, commitSnapshotPaths);
|
|
605
647
|
persistRunDurable(run);
|
|
606
648
|
for (const node of run.nodes || []) {
|
|
607
649
|
for (const artifact of node.artifacts || []) {
|
|
@@ -613,6 +655,9 @@ function prepareFree(run, tombstone) {
|
|
|
613
655
|
throw new ReclamationError("repoint-incomplete", `node ${node.id} artifact ${artifact.id} still references freed scratch path ${artifact.path}`, { nodeId: node.id, artifactId: artifact.id, path: artifact.path });
|
|
614
656
|
}
|
|
615
657
|
}
|
|
658
|
+
if (commitSnapshotPaths.includes(resolved)) {
|
|
659
|
+
throw new ReclamationError("repoint-incomplete", `node ${node.id} artifact ${artifact.id} still references freed commit snapshot ${artifact.path}`, { nodeId: node.id, artifactId: artifact.id, path: artifact.path });
|
|
660
|
+
}
|
|
616
661
|
}
|
|
617
662
|
}
|
|
618
663
|
for (const nodeId of repointed) {
|
|
@@ -682,6 +727,28 @@ function repointResultNodeArtifacts(run, freedScratchDir) {
|
|
|
682
727
|
}
|
|
683
728
|
return changedIds;
|
|
684
729
|
}
|
|
730
|
+
/** Removes the "snapshot" artifact entry from any node that references one
|
|
731
|
+
* of `freedCommitSnapshotPaths` — there is no retained alternative to
|
|
732
|
+
* repoint to (unlike a scratch dir's "result" copy), so the reference is
|
|
733
|
+
* dropped outright. StateArtifact.path is a required string, so the
|
|
734
|
+
* artifact entry is filtered out rather than nulled. */
|
|
735
|
+
function stripCommitSnapshotArtifacts(run, freedCommitSnapshotPaths) {
|
|
736
|
+
if (!freedCommitSnapshotPaths.length)
|
|
737
|
+
return [];
|
|
738
|
+
const freedSet = new Set(freedCommitSnapshotPaths);
|
|
739
|
+
const changedIds = [];
|
|
740
|
+
for (const node of run.nodes || []) {
|
|
741
|
+
if (!node.artifacts || !node.artifacts.length)
|
|
742
|
+
continue;
|
|
743
|
+
const before = node.artifacts.length;
|
|
744
|
+
node.artifacts = node.artifacts.filter((artifact) => !artifact.path || !freedSet.has((0, fs_atomic_1.realResolve)(artifact.path)));
|
|
745
|
+
if (node.artifacts.length !== before) {
|
|
746
|
+
node.updatedAt = new Date().toISOString();
|
|
747
|
+
changedIds.push(node.id);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return changedIds;
|
|
751
|
+
}
|
|
685
752
|
/** Execute the write-ahead, fail-closed reclamation transaction. */
|
|
686
753
|
function runReclamation(run, options = {}) {
|
|
687
754
|
const skeleton = extractSkeleton(run);
|
|
@@ -827,7 +894,7 @@ function gcPlan(host, options = {}) {
|
|
|
827
894
|
let plan;
|
|
828
895
|
try {
|
|
829
896
|
const run = host.loadRun(record.repo, record.runId);
|
|
830
|
-
plan = planReclamation(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots });
|
|
897
|
+
plan = planReclamation(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots, keepCommits: policy.keepCommits });
|
|
831
898
|
}
|
|
832
899
|
catch {
|
|
833
900
|
entries.push({
|
|
@@ -871,6 +938,7 @@ function gcPlan(host, options = {}) {
|
|
|
871
938
|
reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays ?? 0,
|
|
872
939
|
keepSnapshots: Boolean(policy.keepSnapshots),
|
|
873
940
|
keepScratch: Boolean(policy.keepScratch),
|
|
941
|
+
keepCommits: Boolean(policy.keepCommits),
|
|
874
942
|
reclaimStates: policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"],
|
|
875
943
|
},
|
|
876
944
|
total: entries.length,
|
|
@@ -911,8 +979,8 @@ function gcRun(host, options = {}) {
|
|
|
911
979
|
const result = runReclamation(run, {
|
|
912
980
|
now: nowIso,
|
|
913
981
|
actor: options.actor,
|
|
914
|
-
policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
|
|
915
|
-
reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
|
|
982
|
+
policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots, keepCommits: policy.keepCommits },
|
|
983
|
+
reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots, keepCommits: policy.keepCommits },
|
|
916
984
|
});
|
|
917
985
|
reclaimed.push({
|
|
918
986
|
runId: record.runId,
|
|
@@ -1004,7 +1072,7 @@ function gcVerify(host, runId, options = {}) {
|
|
|
1004
1072
|
function formatGcPlan(result) {
|
|
1005
1073
|
const lines = [
|
|
1006
1074
|
`GC Plan (${result.scope}): ${result.eligibleCount}/${result.total} eligible, ${result.bytesToFree} byte(s) would be freed [DRY-RUN, frees nothing]`,
|
|
1007
|
-
` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots}`,
|
|
1075
|
+
` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots} keepCommits=${result.policy.keepCommits}`,
|
|
1008
1076
|
];
|
|
1009
1077
|
for (const entry of result.entries) {
|
|
1010
1078
|
if (entry.eligible) {
|
|
@@ -297,6 +297,10 @@ function gcPolicyOverridesFrom(options) {
|
|
|
297
297
|
overrides.keepSnapshots = Boolean(options.keepSnapshots);
|
|
298
298
|
if (options["keep-snapshots"] !== undefined)
|
|
299
299
|
overrides.keepSnapshots = Boolean(options["keep-snapshots"]);
|
|
300
|
+
if (options.keepCommits !== undefined)
|
|
301
|
+
overrides.keepCommits = Boolean(options.keepCommits);
|
|
302
|
+
if (options["keep-commits"] !== undefined)
|
|
303
|
+
overrides.keepCommits = Boolean(options["keep-commits"]);
|
|
300
304
|
if (options.state !== undefined)
|
|
301
305
|
overrides.reclaimStates = Array.isArray(options.state) ? options.state : [options.state];
|
|
302
306
|
return overrides;
|