cool-workflow 0.2.2 → 0.2.3
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 +11 -5
- package/dist/cli/entry.js +41 -1
- package/dist/cli/io.js +6 -2
- package/dist/cli/parseargv.js +1 -0
- package/dist/core/capability-data.js +1 -0
- package/dist/core/format/completion.js +68 -0
- package/dist/core/format/help.js +25 -6
- package/dist/core/format/safe-json.js +73 -0
- package/dist/core/format/state-explosion-text.js +1 -1
- package/dist/core/multi-agent/candidate-scoring.js +5 -1
- package/dist/core/multi-agent/collaboration.js +3 -3
- package/dist/core/multi-agent/coordinator.js +5 -5
- package/dist/core/multi-agent/runtime.js +4 -4
- package/dist/core/multi-agent/topology.js +3 -3
- package/dist/core/pipeline/dispatch.js +18 -4
- package/dist/core/pipeline/drive-decide.js +2 -1
- package/dist/core/state/migrations.js +16 -1
- package/dist/core/state/state-explosion/digest.js +13 -13
- package/dist/core/state/state-explosion/graph.js +17 -11
- package/dist/core/state/state-explosion/report.js +6 -6
- package/dist/core/util/numeric-flag.js +40 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +99 -11
- package/dist/shell/audit-cli.js +57 -25
- package/dist/shell/coordinator-io.js +73 -13
- package/dist/shell/dispatch.js +1 -1
- package/dist/shell/doctor.js +80 -1
- package/dist/shell/drive.js +243 -49
- package/dist/shell/eval-text.js +2 -2
- package/dist/shell/evidence-reasoning.js +4 -4
- package/dist/shell/execution-backend/agent.js +20 -1
- package/dist/shell/execution-backend/container.js +4 -1
- package/dist/shell/execution-backend/local.js +4 -1
- package/dist/shell/feedback-cli.js +6 -6
- package/dist/shell/fs-atomic.js +218 -29
- package/dist/shell/man-cli.js +6 -0
- package/dist/shell/metrics-cli.js +2 -1
- package/dist/shell/multi-agent-cli.js +367 -323
- package/dist/shell/multi-agent-host.js +9 -9
- package/dist/shell/multi-agent-operator-ux.js +80 -38
- package/dist/shell/node-store.js +10 -4
- package/dist/shell/observability.js +1 -1
- package/dist/shell/operator-ux-text.js +22 -22
- package/dist/shell/operator-ux.js +15 -14
- package/dist/shell/orchestrator.js +49 -38
- package/dist/shell/pipeline-cli.js +87 -40
- package/dist/shell/reclamation-io.js +14 -4
- package/dist/shell/registry-cli.js +19 -17
- package/dist/shell/remote-source.js +13 -8
- package/dist/shell/report-cli.js +45 -0
- package/dist/shell/report.js +2 -1
- package/dist/shell/run-registry-io.js +77 -19
- package/dist/shell/run-store.js +74 -2
- package/dist/shell/scheduling-io.js +12 -13
- package/dist/shell/state-cli.js +2 -7
- package/dist/shell/state-explosion-cli.js +17 -9
- package/dist/shell/topology-io.js +36 -5
- package/dist/shell/trust-audit.js +224 -22
- package/dist/shell/trust-policy-io.js +1 -1
- package/dist/shell/worker-cli.js +35 -31
- package/dist/wiring/capability-table/basics.js +33 -8
- package/dist/wiring/capability-table/exec-backend.js +20 -11
- package/dist/wiring/capability-table/multi-agent.js +172 -165
- package/dist/wiring/capability-table/parity.js +1 -0
- package/dist/wiring/capability-table/pipeline.js +54 -42
- package/dist/wiring/capability-table/registry-core.js +29 -10
- package/dist/wiring/capability-table/reporting.js +137 -96
- package/dist/wiring/capability-table/scheduling-registry.js +195 -161
- package/dist/wiring/capability-table/state.js +36 -28
- package/dist/wiring/capability-table/trust-ledger.js +46 -22
- package/dist/wiring/capability-table/workflow-apps.js +83 -68
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/candidate-scoring.7.md +1 -1
- package/docs/canonical-workflow-apps.7.md +7 -7
- package/docs/cli-mcp-parity.7.md +8 -3
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/coordinator-blackboard.7.md +17 -17
- package/docs/dogfood-one-real-repo.7.md +11 -11
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/end-to-end-golden-path.7.md +14 -14
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/getting-started.md +37 -37
- package/docs/multi-agent-cli-mcp-surface.7.md +19 -17
- package/docs/multi-agent-eval-replay-harness.7.md +15 -13
- package/docs/multi-agent-operator-ux.7.md +21 -19
- package/docs/multi-agent-runtime-core.7.md +22 -22
- package/docs/multi-agent-topologies.7.md +6 -6
- package/docs/multi-agent-trust-policy-audit.7.md +11 -11
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/operator-ux.7.md +34 -34
- package/docs/pipeline-runner.7.md +4 -4
- package/docs/project-index.md +27 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +3 -1
- package/docs/release-tooling.7.md +2 -0
- package/docs/routines.md +4 -4
- package/docs/run-registry-control-plane.7.md +21 -19
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +15 -15
- package/docs/state-explosion-management.7.md +12 -10
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/unix-principles.md +3 -1
- package/docs/verifier-gated-commit.7.md +1 -1
- package/docs/web-desktop-workbench.7.md +2 -0
- package/docs/workflow-app-framework.7.md +13 -13
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/agents/claude-p-agent.js +2 -2
- package/scripts/block-unapproved-tag.sh +23 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +52 -2
- package/scripts/dogfood-release.js +1 -1
- package/scripts/fake-date-for-reproduction.js +44 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +3 -0
- package/scripts/release-flow.js +57 -1
- package/scripts/verdict-keygen.js +83 -0
- package/scripts/verify-bump-reproduction.sh +148 -0
- package/scripts/verify-verdict-signature.js +61 -0
- package/skills/cool-workflow/SKILL.md +9 -9
- package/skills/cool-workflow/references/commands.md +89 -88
- package/ui/workbench/app.css +37 -1
- package/ui/workbench/app.js +124 -6
|
@@ -52,6 +52,8 @@ exports.recordResultRun = recordResultRun;
|
|
|
52
52
|
exports.commitRun = commitRun;
|
|
53
53
|
const fs = __importStar(require("node:fs"));
|
|
54
54
|
const path = __importStar(require("node:path"));
|
|
55
|
+
const readlinePromises = __importStar(require("node:readline/promises"));
|
|
56
|
+
const numeric_flag_1 = require("../core/util/numeric-flag");
|
|
55
57
|
const pipeline_1 = require("./pipeline");
|
|
56
58
|
const workflow_app_loader_1 = require("./workflow-app-loader");
|
|
57
59
|
const drive_1 = require("./drive");
|
|
@@ -223,8 +225,11 @@ function runDrivePreview(args) {
|
|
|
223
225
|
return (0, drive_1.drivePreview)(runId, cwd, args);
|
|
224
226
|
}
|
|
225
227
|
/** `cw run <app|--run id> --drive [--once]` — plans a fresh run (unless
|
|
226
|
-
* `--run` continues an existing one) and drives it.
|
|
227
|
-
|
|
228
|
+
* `--run` continues an existing one) and drives it. Uses driveAsync (not
|
|
229
|
+
* the plain synchronous drive()) so a live multi-round run actually
|
|
230
|
+
* responds to Ctrl-C/SIGTERM instead of silently ignoring it — see
|
|
231
|
+
* shell/drive.ts's driveAsync doc comment. */
|
|
232
|
+
async function runDriveStep(args) {
|
|
228
233
|
const existingRunId = String(args.runId || args.run || "");
|
|
229
234
|
const options = {
|
|
230
235
|
once: Boolean(args.once),
|
|
@@ -236,7 +241,7 @@ function runDriveStep(args) {
|
|
|
236
241
|
if (existingRunId) {
|
|
237
242
|
const cwd = invocationCwd(args);
|
|
238
243
|
const run = (0, run_store_1.loadRunFromCwd)(existingRunId, cwd);
|
|
239
|
-
return (0, drive_1.
|
|
244
|
+
return (0, drive_1.driveAsync)(existingRunId, run.cwd, options);
|
|
240
245
|
}
|
|
241
246
|
const appId = String(args.appId || args.app || args.positionalApp || "");
|
|
242
247
|
if (!appId)
|
|
@@ -245,7 +250,7 @@ function runDriveStep(args) {
|
|
|
245
250
|
args.repo = invocationCwd(args);
|
|
246
251
|
const app = (0, workflow_app_loader_1.loadWorkflowApp)(appId);
|
|
247
252
|
const run = (0, pipeline_1.plan)(app, planInputsFor(args));
|
|
248
|
-
return (0, drive_1.
|
|
253
|
+
return (0, drive_1.driveAsync)(run.id, run.cwd, options);
|
|
249
254
|
}
|
|
250
255
|
/** `cw quickstart [app] --check` — read-only preflight: does the app
|
|
251
256
|
* resolve, is the repo readable/writable, is a question set, is an
|
|
@@ -385,6 +390,21 @@ function remoteQuickstartCheck(appId, args, candidate) {
|
|
|
385
390
|
const nextCommand = `cw quickstart ${shellWord(appId)} --link ${shellWord(validation.url)}${question ? ` --question ${shellWord(question)}` : ""}`;
|
|
386
391
|
return { schemaVersion: 1, mode: "check", ok, appId, repo: validation.url, checks, nextCommand };
|
|
387
392
|
}
|
|
393
|
+
/** Prompts `Question: ` on STDERR (stdout stays data-only, matching the
|
|
394
|
+
* Rule of Silence) and reads one line from stdin. Returns the trimmed
|
|
395
|
+
* answer, or `undefined` for a blank line. Callers must only invoke this
|
|
396
|
+
* when `process.stdin.isTTY` — it would otherwise block forever on a
|
|
397
|
+
* piped/CI invocation. */
|
|
398
|
+
async function promptForQuestion() {
|
|
399
|
+
const rl = readlinePromises.createInterface({ input: process.stdin, output: process.stderr });
|
|
400
|
+
try {
|
|
401
|
+
const answer = await rl.question("Question: ");
|
|
402
|
+
return answer.trim() || undefined;
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
rl.close();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
388
408
|
/** `cw quickstart [app] --question ...` — composes plan -> runDrive ->
|
|
389
409
|
* report in one call. Default app is architecture-review. `--check` is a
|
|
390
410
|
* read-only preflight that never plans/drives/writes (see
|
|
@@ -392,7 +412,7 @@ function remoteQuickstartCheck(appId, args, candidate) {
|
|
|
392
412
|
* projection (never drives), `--resume` advances one step (no --run) or
|
|
393
413
|
* continues a named run to completion (--run <id>) — both ported byte-for-
|
|
394
414
|
* byte from the old build's src/capability-core.ts quickstart(). */
|
|
395
|
-
function quickstartRun(args) {
|
|
415
|
+
async function quickstartRun(args) {
|
|
396
416
|
const appId = String(args.appId || args.app || args.workflowId || QUICKSTART_DEFAULT_APP);
|
|
397
417
|
// Remote source: a `--link <url>` — or a URL passed to `--repo`/`-dir` — is
|
|
398
418
|
// materialized to a LOCAL checkout HERE (capability/shell layer). Cloning is
|
|
@@ -406,6 +426,37 @@ function quickstartRun(args) {
|
|
|
406
426
|
args.repo = invocationCwd(args);
|
|
407
427
|
if (Boolean(args.check))
|
|
408
428
|
return quickstartCheck(appId, args, remoteCandidate);
|
|
429
|
+
// `--resume`: a discoverability flag over the existing continuation. With no
|
|
430
|
+
// `--run`, advance exactly ONE step (reuse the `--once` path) and print a
|
|
431
|
+
// copy-pasteable continue line; with `--run <id>`, continue that run to
|
|
432
|
+
// completion (the default drive). It adds no new execution path. Byte-exact to
|
|
433
|
+
// the old build's src/capability-core.ts quickstart(). Hoisted above the TTY
|
|
434
|
+
// prompt below: `existingRunId` is the one fact (mirrored at both the
|
|
435
|
+
// `--preview` branch's `if (!previewRunId)` and the main path's `if
|
|
436
|
+
// (existingRunId) {...} else { run = plan(...) }`) that decides whether THIS
|
|
437
|
+
// invocation plans a fresh run at all — an existing run never needs a
|
|
438
|
+
// question, no matter which mode (--preview/--resume/plain) is asking for it.
|
|
439
|
+
const resume = Boolean(args.resume);
|
|
440
|
+
const existingRunId = String(args.runId || args.run || "");
|
|
441
|
+
const resumeRunId = resume && existingRunId ? existingRunId : undefined;
|
|
442
|
+
// On an interactive TTY with no --question, ask for one instead of
|
|
443
|
+
// failing closed with "Missing required input: question" (byte-behavior
|
|
444
|
+
// port of the old build's TTY prompt — SPEC/cli-surface.md:34,502). A
|
|
445
|
+
// blank/empty answer leaves `question` unset, same as never passing
|
|
446
|
+
// --question at all — plan()'s own required-input check still fails
|
|
447
|
+
// closed for an app that declares `question` required. Never fires
|
|
448
|
+
// under --check (returned above; --check reports a missing question as
|
|
449
|
+
// a preflight issue, not something to prompt for), off a TTY (a piped
|
|
450
|
+
// invocation must never block on stdin), or when `existingRunId` is set
|
|
451
|
+
// (a `--preview --run <id>` or `--resume --run <id>` continues an
|
|
452
|
+
// already-planned run and never calls `plan()` again, so it never
|
|
453
|
+
// needs a question — prompting there would block on stdin for no
|
|
454
|
+
// reason; a review on this cycle caught exactly that regression).
|
|
455
|
+
if (process.stdin.isTTY && !existingRunId && !(typeof args.question === "string" && args.question.trim())) {
|
|
456
|
+
const answer = await promptForQuestion();
|
|
457
|
+
if (answer)
|
|
458
|
+
args.question = answer;
|
|
459
|
+
}
|
|
409
460
|
// Materialize the remote NOW — after `--check` (never fetches) and before any
|
|
410
461
|
// plan/drive — so the core only ever sees the local checkout. Fails closed: a
|
|
411
462
|
// bad URL / blocked scheme / missing git / fetch failure throws here.
|
|
@@ -424,14 +475,6 @@ function quickstartRun(args) {
|
|
|
424
475
|
if (remoteSource.ref)
|
|
425
476
|
args.sourceRef = remoteSource.ref;
|
|
426
477
|
}
|
|
427
|
-
// `--resume`: a discoverability flag over the existing continuation. With no
|
|
428
|
-
// `--run`, advance exactly ONE step (reuse the `--once` path) and print a
|
|
429
|
-
// copy-pasteable continue line; with `--run <id>`, continue that run to
|
|
430
|
-
// completion (the default drive). It adds no new execution path. Byte-exact to
|
|
431
|
-
// the old build's src/capability-core.ts quickstart().
|
|
432
|
-
const resume = Boolean(args.resume);
|
|
433
|
-
const existingRunId = String(args.runId || args.run || "");
|
|
434
|
-
const resumeRunId = resume && existingRunId ? existingRunId : undefined;
|
|
435
478
|
// `--preview`: read-only, deterministic next-step projection (no spawn, no
|
|
436
479
|
// commit). Plan a fresh run (the read-only first verb) then project the next
|
|
437
480
|
// drive step. Never drives.
|
|
@@ -464,7 +507,7 @@ function quickstartRun(args) {
|
|
|
464
507
|
else {
|
|
465
508
|
run = (0, pipeline_1.plan)(resolveWorkflowAppForPlan(appId), planInputsFor(args));
|
|
466
509
|
}
|
|
467
|
-
const result = (0, drive_1.
|
|
510
|
+
const result = await (0, drive_1.driveAsync)(run.id, run.cwd, options);
|
|
468
511
|
const finalRun = (0, run_store_1.loadRunFromCwd)(run.id, run.cwd);
|
|
469
512
|
(0, report_1.writeReport)(finalRun);
|
|
470
513
|
// Tamper-evident provenance: bind the remote origin (url@sha) into the run's
|
|
@@ -560,7 +603,7 @@ function dispatchRun(args) {
|
|
|
560
603
|
const v = args[kebab] ?? args[camel];
|
|
561
604
|
return typeof v === "string" && v.trim() ? v : undefined;
|
|
562
605
|
};
|
|
563
|
-
const manifest = (0, dispatch_1.createDispatchManifest)(run,
|
|
606
|
+
const manifest = (0, dispatch_1.createDispatchManifest)(run, (0, numeric_flag_1.requiredNumberFlag)(args.limit, "--limit"), {
|
|
564
607
|
sandboxProfileId: typeof args.sandbox === "string" ? args.sandbox : undefined,
|
|
565
608
|
sandbox: typeof args.sandbox === "string" ? args.sandbox : undefined,
|
|
566
609
|
backendId: typeof args.backend === "string" ? args.backend : undefined,
|
|
@@ -649,29 +692,33 @@ function recordResultRun(args) {
|
|
|
649
692
|
* still leaves the run's report/state current on disk. */
|
|
650
693
|
function commitRun(args) {
|
|
651
694
|
const runId = String(args.runId);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
695
|
+
// Hold the state.json lock across the whole load -> commit -> save (both the
|
|
696
|
+
// success and the fail-closed catch persist) so a concurrent run mutation
|
|
697
|
+
// cannot drop this commit (lost-update class, matching dispatchRun/recordResultRun).
|
|
698
|
+
return (0, run_store_1.withRunStateLock)(runId, invocationCwd(args), (run) => {
|
|
699
|
+
const allowCheckpoint = Boolean(args.allowUnverifiedCheckpoint || args["allow-unverified-checkpoint"]);
|
|
700
|
+
const hasGateOption = Boolean(args.verifier || args.verifierNode || args["verifier-node"] || args.candidate || args.selection);
|
|
701
|
+
try {
|
|
702
|
+
const commit = (0, commit_1.commitState)(run, {
|
|
703
|
+
reason: typeof args.reason === "string" && args.reason ? args.reason : "manual",
|
|
704
|
+
verifierNodeId: (typeof args.verifier === "string" && args.verifier) ||
|
|
705
|
+
(typeof args.verifierNode === "string" && args.verifierNode) ||
|
|
706
|
+
(typeof args["verifier-node"] === "string" && args["verifier-node"]) ||
|
|
707
|
+
undefined,
|
|
708
|
+
candidateId: typeof args.candidate === "string" ? args.candidate : undefined,
|
|
709
|
+
selectionId: typeof args.selection === "string" ? args.selection : undefined,
|
|
710
|
+
verifierGated: hasGateOption || !allowCheckpoint,
|
|
711
|
+
allowUnverifiedCheckpoint: allowCheckpoint,
|
|
712
|
+
source: "cli",
|
|
713
|
+
});
|
|
714
|
+
(0, report_1.writeReport)(run);
|
|
715
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
716
|
+
return { runId: run.id, commit };
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
(0, report_1.writeReport)(run);
|
|
720
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
677
724
|
}
|
|
@@ -639,6 +639,16 @@ function freeBulk(run, tombstone) {
|
|
|
639
639
|
let freedBytes = 0;
|
|
640
640
|
for (const entry of tombstone.freed) {
|
|
641
641
|
const abs = path.join(runDir, entry.path);
|
|
642
|
+
// planReclamation always derives entry.path as a relative path already
|
|
643
|
+
// confined under runDir, but a tampered/imported state.json could carry a
|
|
644
|
+
// worker/artifact path that resolves outside it — re-check containment
|
|
645
|
+
// right before the recursive delete so that can never turn into an
|
|
646
|
+
// out-of-tree rmSync.
|
|
647
|
+
if (!(0, fs_atomic_1.isContainedPath)(abs, runDir)) {
|
|
648
|
+
throw new ReclamationError("unsafe-free-path", `refusing to free path outside the run directory: ${entry.path}`, {
|
|
649
|
+
path: entry.path,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
642
652
|
const before = dirBytes(abs);
|
|
643
653
|
fs.rmSync(abs, { recursive: true, force: true });
|
|
644
654
|
freedBytes += before;
|
|
@@ -867,7 +877,7 @@ function gcPlan(host, options = {}) {
|
|
|
867
877
|
eligibleCount,
|
|
868
878
|
bytesToFree,
|
|
869
879
|
entries,
|
|
870
|
-
nextAction: eligibleCount ? "
|
|
880
|
+
nextAction: eligibleCount ? "cw gc run" : "cw run search",
|
|
871
881
|
};
|
|
872
882
|
}
|
|
873
883
|
function gcRun(host, options = {}) {
|
|
@@ -936,7 +946,7 @@ function gcRun(host, options = {}) {
|
|
|
936
946
|
reclaimed,
|
|
937
947
|
refused,
|
|
938
948
|
totalBytesFreed,
|
|
939
|
-
nextAction: reclaimed.length ? "
|
|
949
|
+
nextAction: reclaimed.length ? "cw gc verify <run-id>" : "cw gc plan",
|
|
940
950
|
};
|
|
941
951
|
}
|
|
942
952
|
function gcVerify(host, runId, options = {}) {
|
|
@@ -952,7 +962,7 @@ function gcVerify(host, runId, options = {}) {
|
|
|
952
962
|
capability: "re-runnable",
|
|
953
963
|
chainLength: 0,
|
|
954
964
|
checks: [{ name: "located", pass: false, code: "not-reclaimed", detail: "run source not found" }],
|
|
955
|
-
nextAction: "
|
|
965
|
+
nextAction: "cw registry refresh" + (scope === "home" ? " --scope home" : ""),
|
|
956
966
|
};
|
|
957
967
|
}
|
|
958
968
|
const run = host.loadRun(located.record.repo, runId);
|
|
@@ -985,7 +995,7 @@ function gcVerify(host, runId, options = {}) {
|
|
|
985
995
|
tombstoneHash: last?.tombstoneHash,
|
|
986
996
|
chainLength: result.tombstones.length,
|
|
987
997
|
checks,
|
|
988
|
-
nextAction: verified ? "
|
|
998
|
+
nextAction: verified ? "cw run show " + runId : "cw gc plan",
|
|
989
999
|
};
|
|
990
1000
|
}
|
|
991
1001
|
// ---------------------------------------------------------------------------
|
|
@@ -82,6 +82,7 @@ exports.clonesListCli = clonesListCli;
|
|
|
82
82
|
exports.clonesGcCli = clonesGcCli;
|
|
83
83
|
const fs = __importStar(require("node:fs"));
|
|
84
84
|
const path = __importStar(require("node:path"));
|
|
85
|
+
const numeric_flag_1 = require("../core/util/numeric-flag");
|
|
85
86
|
const run_registry_io_1 = require("./run-registry-io");
|
|
86
87
|
const pipeline_1 = require("./pipeline");
|
|
87
88
|
const workflow_app_loader_1 = require("./workflow-app-loader");
|
|
@@ -186,16 +187,16 @@ function runSearchCli(options = {}) {
|
|
|
186
187
|
since: optionalString(options.since),
|
|
187
188
|
until: optionalString(options.until),
|
|
188
189
|
includeArchived: options.includeArchived === undefined ? undefined : Boolean(options.includeArchived),
|
|
189
|
-
limit:
|
|
190
|
-
offset:
|
|
190
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
191
|
+
offset: (0, numeric_flag_1.requiredNumberFlag)(options.offset, "--offset"),
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
function runListCli(options = {}) {
|
|
194
195
|
return new run_registry_io_1.RunRegistry(resolveCwd(options)).list({
|
|
195
196
|
scope: scopeOf(options, "home"),
|
|
196
197
|
includeArchived: options.includeArchived === undefined ? undefined : Boolean(options.includeArchived),
|
|
197
|
-
limit:
|
|
198
|
-
offset:
|
|
198
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
199
|
+
offset: (0, numeric_flag_1.requiredNumberFlag)(options.offset, "--offset"),
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
202
|
function runShowCli(runId, options = {}) {
|
|
@@ -205,21 +206,21 @@ function runShowCli(runId, options = {}) {
|
|
|
205
206
|
* is read-only and byte-identical to the registry resume payload; with
|
|
206
207
|
* `--drive`/`--once` the SAME run (nothing re-planned) is handed to the
|
|
207
208
|
* real drive loop and the payload gains a `drive: DriveResult` field. */
|
|
208
|
-
function runResumeCli(runId, options = {}) {
|
|
209
|
+
async function runResumeCli(runId, options = {}) {
|
|
209
210
|
const base = new run_registry_io_1.RunRegistry(resolveCwd(options)).resume(runId, {
|
|
210
211
|
scope: scopeOf(options, "home"),
|
|
211
|
-
limit:
|
|
212
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
212
213
|
});
|
|
213
214
|
if (!options.drive && !options.once)
|
|
214
215
|
return base;
|
|
215
|
-
const drive = (0, pipeline_cli_1.runDriveStep)({ ...options, runId: base.runId, repo: base.repo, once: Boolean(options.once) });
|
|
216
|
+
const drive = await (0, pipeline_cli_1.runDriveStep)({ ...options, runId: base.runId, repo: base.repo, once: Boolean(options.once) });
|
|
216
217
|
return { ...base, drive };
|
|
217
218
|
}
|
|
218
219
|
function runArchiveCli(runId, options = {}) {
|
|
219
220
|
const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
|
|
220
221
|
if (!runId) {
|
|
221
|
-
const olderThanDays =
|
|
222
|
-
if (
|
|
222
|
+
const olderThanDays = (0, numeric_flag_1.requiredNumberFlag)(options.olderThanDays ?? options["older-than-days"], "--older-than-days");
|
|
223
|
+
if (olderThanDays === undefined)
|
|
223
224
|
throw new Error("Missing run id (or --older-than-days N for the retention policy path).");
|
|
224
225
|
const states = Array.isArray(options.state) ? options.state : options.state ? [options.state] : undefined;
|
|
225
226
|
return registry.archiveByPolicy({
|
|
@@ -253,8 +254,8 @@ function historyCli(options = {}) {
|
|
|
253
254
|
scope: scopeOf(options, "home"),
|
|
254
255
|
app: optionalString(options.app),
|
|
255
256
|
status: optionalString(options.status),
|
|
256
|
-
limit:
|
|
257
|
-
offset:
|
|
257
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
258
|
+
offset: (0, numeric_flag_1.requiredNumberFlag)(options.offset, "--offset"),
|
|
258
259
|
});
|
|
259
260
|
}
|
|
260
261
|
function queueAddCli(options = {}) {
|
|
@@ -263,7 +264,7 @@ function queueAddCli(options = {}) {
|
|
|
263
264
|
appId: optionalString(options.app || options.appId),
|
|
264
265
|
workflowId: optionalString(options.workflow || options.workflowId),
|
|
265
266
|
repo: optionalString(options.repo),
|
|
266
|
-
priority:
|
|
267
|
+
priority: (0, numeric_flag_1.requiredNumberFlag)(options.priority, "--priority"),
|
|
267
268
|
note: optionalString(options.note),
|
|
268
269
|
id: optionalString(options.id),
|
|
269
270
|
});
|
|
@@ -276,7 +277,7 @@ function queueShowCli(id, options = {}) {
|
|
|
276
277
|
}
|
|
277
278
|
function queueDrainCli(options = {}) {
|
|
278
279
|
return new run_registry_io_1.RunRegistry(resolveCwd(options)).queueDrain({
|
|
279
|
-
limit:
|
|
280
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
280
281
|
repo: optionalString(options.repo),
|
|
281
282
|
});
|
|
282
283
|
}
|
|
@@ -285,8 +286,9 @@ function queueDrainCli(options = {}) {
|
|
|
285
286
|
// ---------------------------------------------------------------------
|
|
286
287
|
function gcPolicyOverridesFrom(options) {
|
|
287
288
|
const overrides = {};
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const reclaimAfterArchiveDays = (0, numeric_flag_1.requiredNumberFlag)(options.reclaimAfterArchiveDays, "--reclaimAfterArchiveDays");
|
|
290
|
+
if (reclaimAfterArchiveDays !== undefined)
|
|
291
|
+
overrides.reclaimAfterArchiveDays = reclaimAfterArchiveDays;
|
|
290
292
|
if (options.keepScratch !== undefined)
|
|
291
293
|
overrides.keepScratch = Boolean(options.keepScratch);
|
|
292
294
|
if (options["keep-scratch"] !== undefined)
|
|
@@ -316,7 +318,7 @@ function gcRunCli(runId, options = {}) {
|
|
|
316
318
|
policy: gcPolicyOverridesFrom(options),
|
|
317
319
|
now: optionalString(options.now),
|
|
318
320
|
actor: optionalString(options.actor),
|
|
319
|
-
limit:
|
|
321
|
+
limit: (0, numeric_flag_1.requiredNumberFlag)(options.limit, "--limit"),
|
|
320
322
|
});
|
|
321
323
|
}
|
|
322
324
|
function gcVerifyCli(runId, options = {}) {
|
|
@@ -331,7 +333,7 @@ function orphansGcCli(options = {}) {
|
|
|
331
333
|
const registry = new run_registry_io_1.RunRegistry(resolveCwd(options));
|
|
332
334
|
return (0, reclamation_io_1.gcOrphanRuns)(registry, {
|
|
333
335
|
scope: scopeOf(options, "home"),
|
|
334
|
-
minAgeMinutes:
|
|
336
|
+
minAgeMinutes: (0, numeric_flag_1.requiredNumberFlag)(options.minAgeMinutes ?? options["min-age-minutes"], "--min-age-minutes"),
|
|
335
337
|
all: Boolean(options.all),
|
|
336
338
|
now: optionalString(options.now),
|
|
337
339
|
});
|
|
@@ -310,9 +310,12 @@ function fetchArchiveBytes(rawUrl, sanitizedUrl, dest, opts) {
|
|
|
310
310
|
}
|
|
311
311
|
/** List an archive's entry NAMES WITHOUT extracting (the zip-slip/tar-slip name guard runs on
|
|
312
312
|
* this). Symlinks/specials and decompression bombs are caught separately (below). */
|
|
313
|
-
function listArchive(file, isZip) {
|
|
313
|
+
function listArchive(file, isZip, timeoutMs) {
|
|
314
314
|
const cmd = isZip ? ["unzip", "-Z1", "--", file] : ["tar", "-tf", file];
|
|
315
|
-
|
|
315
|
+
// Same unset-timeout hazard as a hung local/container task (post-v0.2.2
|
|
316
|
+
// robustness hardening): a pathological archive must not hang the CLI
|
|
317
|
+
// forever with no kill path. Matches git()'s own fallback below.
|
|
318
|
+
const result = (0, node_child_process_1.spawnSync)(cmd[0], cmd.slice(1), { encoding: "utf8", timeout: timeoutMs || 120000 });
|
|
316
319
|
if (result.status !== 0) {
|
|
317
320
|
// Distinguish "unzip not installed" (ENOENT) from "archive is corrupt" — never conflate the
|
|
318
321
|
// two (a corrupt .tar mislabeled .zip used to surface a bogus "unzip not found").
|
|
@@ -338,10 +341,10 @@ function assertNoTraversal(entries, sanitizedUrl) {
|
|
|
338
341
|
/** Declared uncompressed size, read WITHOUT extracting — gzip's ISIZE trailer for `.tar.gz`/
|
|
339
342
|
* `.tgz`, or `unzip -l`'s total for `.zip`. Best-effort (undefined when unknown); the
|
|
340
343
|
* post-extraction walk is authoritative. Lets us reject a bomb BEFORE it fills the disk. */
|
|
341
|
-
function declaredUncompressedSize(file, isZip) {
|
|
344
|
+
function declaredUncompressedSize(file, isZip, timeoutMs) {
|
|
342
345
|
try {
|
|
343
346
|
if (isZip) {
|
|
344
|
-
const r = (0, node_child_process_1.spawnSync)("unzip", ["-l", "--", file], { encoding: "utf8" });
|
|
347
|
+
const r = (0, node_child_process_1.spawnSync)("unzip", ["-l", "--", file], { encoding: "utf8", timeout: timeoutMs || 120000 });
|
|
345
348
|
if (r.status !== 0)
|
|
346
349
|
return undefined;
|
|
347
350
|
const lines = String(r.stdout || "").trim().split(/\r?\n/);
|
|
@@ -421,15 +424,17 @@ function downloadArchive(rawUrl, sanitizedUrl, opts) {
|
|
|
421
424
|
const commit = (0, node_crypto_1.createHash)("sha256").update(node_fs_1.default.readFileSync(tmpFile)).digest("hex"); // content address
|
|
422
425
|
// Bomb defense (BEFORE extracting, to avoid filling the disk): reject a declared
|
|
423
426
|
// uncompressed size over the cap. assertSafeTree re-checks the ACTUAL size afterward.
|
|
424
|
-
const declared = declaredUncompressedSize(tmpFile, isZip);
|
|
427
|
+
const declared = declaredUncompressedSize(tmpFile, isZip, opts.timeoutMs);
|
|
425
428
|
if (declared !== undefined && declared > MAX_EXTRACTED_BYTES) {
|
|
426
429
|
throw new Error(`refusing archive ${sanitizedUrl}: declared uncompressed size ${declared} exceeds ${MAX_EXTRACTED_BYTES} bytes (possible decompression bomb)`);
|
|
427
430
|
}
|
|
428
|
-
assertNoTraversal(listArchive(tmpFile, isZip), sanitizedUrl);
|
|
431
|
+
assertNoTraversal(listArchive(tmpFile, isZip, opts.timeoutMs), sanitizedUrl);
|
|
429
432
|
const extractDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(staging, "x-"));
|
|
433
|
+
// Same unset-timeout hazard as above: extraction runs on downloaded,
|
|
434
|
+
// attacker-influenced bytes and must not hang forever.
|
|
430
435
|
const ex = isZip
|
|
431
|
-
? (0, node_child_process_1.spawnSync)("unzip", ["-q", "-o", "-d", extractDir, "--", tmpFile], { encoding: "utf8" })
|
|
432
|
-
: (0, node_child_process_1.spawnSync)("tar", ["-xf", tmpFile, "-C", extractDir], { encoding: "utf8" });
|
|
436
|
+
? (0, node_child_process_1.spawnSync)("unzip", ["-q", "-o", "-d", extractDir, "--", tmpFile], { encoding: "utf8", timeout: opts.timeoutMs || 120000 })
|
|
437
|
+
: (0, node_child_process_1.spawnSync)("tar", ["-xf", tmpFile, "-C", extractDir], { encoding: "utf8", timeout: opts.timeoutMs || 120000 });
|
|
433
438
|
if (ex.status !== 0)
|
|
434
439
|
throw new Error(`could not extract archive ${sanitizedUrl}: ${String(ex.stderr || "").trim() || `exit ${ex.status}`}`);
|
|
435
440
|
// Fail closed on symlinks/specials and on an over-cap actual extracted size.
|
package/dist/shell/report-cli.js
CHANGED
|
@@ -43,9 +43,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
44
|
exports.reportBundleCli = reportBundleCli;
|
|
45
45
|
exports.reportVerifyBundleCli = reportVerifyBundleCli;
|
|
46
|
+
exports.formatReportVerifyBundle = formatReportVerifyBundle;
|
|
46
47
|
const path = __importStar(require("node:path"));
|
|
47
48
|
const run_export_1 = require("./run-export");
|
|
48
49
|
const run_store_1 = require("./run-store");
|
|
50
|
+
const term_1 = require("./term");
|
|
49
51
|
function optionalString(value) {
|
|
50
52
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
51
53
|
}
|
|
@@ -99,3 +101,46 @@ function reportVerifyBundleCli(args) {
|
|
|
99
101
|
requireSignatures: Boolean(args["require-signatures"] || args.requireSignatures || args.requireSigs),
|
|
100
102
|
});
|
|
101
103
|
}
|
|
104
|
+
/** `cw report verify-bundle`'s human render (default; `--json` prints
|
|
105
|
+
* `ReportBundleVerification` verbatim instead). Mirrors `doctor`'s
|
|
106
|
+
* checks-list-then-verdict shape (`shell/doctor.ts`'s
|
|
107
|
+
* `formatDoctorReport`) rather than `demo bundle`'s narrative one
|
|
108
|
+
* (`shell/telemetry-demo.ts`'s `formatBundleDemo`) — verify-bundle is a
|
|
109
|
+
* flat set of pass/fail checks on one already-sealed archive, not a
|
|
110
|
+
* multi-step forge-and-catch demonstration. */
|
|
111
|
+
function formatReportVerifyBundle(r) {
|
|
112
|
+
const lines = [(0, term_1.bold)(`cw report verify-bundle ${r.archivePath}`), ""];
|
|
113
|
+
const checks = [
|
|
114
|
+
[r.archiveOk, "archive intact — file digests match, no tamper"],
|
|
115
|
+
[r.telemetryVerified, "telemetry hash chain verifies"],
|
|
116
|
+
[r.trustAuditVerified, "trust-audit chain verifies"],
|
|
117
|
+
[r.reportFindingsVerified, "report.md matches every signed result"],
|
|
118
|
+
];
|
|
119
|
+
for (const [ok, label] of checks)
|
|
120
|
+
lines.push(` ${(0, term_1.doctorGlyph)(ok ? "ok" : "fail")} ${label}`);
|
|
121
|
+
const sigDetail = r.signatureKeyProvided
|
|
122
|
+
? `${r.signaturesReverified}/${r.signaturesChecked} signature(s) reverified (key source: ${r.trustKeySource})`
|
|
123
|
+
: "no public key available — signatures not checked";
|
|
124
|
+
lines.push(` trust: ${r.trustLevel} — ${sigDetail}`);
|
|
125
|
+
if (r.reportExtractedTo)
|
|
126
|
+
lines.push(` report.md extracted to: ${r.reportExtractedTo}`);
|
|
127
|
+
if (r.failedChecks.length > 0) {
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push(" Failed checks");
|
|
130
|
+
for (const c of r.failedChecks) {
|
|
131
|
+
// Most check sites in run-export.ts's verifyReportBundle set `code`
|
|
132
|
+
// to a short slug (e.g. "digest-mismatch"); the top-level restore
|
|
133
|
+
// catch-all sets it to the caught error's full message instead
|
|
134
|
+
// (run-export.ts's "restore" failedChecks.push) — too long/sentence-
|
|
135
|
+
// shaped to cram into "name [code]" without reading as a run-on.
|
|
136
|
+
// Give it its own indented line instead.
|
|
137
|
+
const isSlug = c.code !== undefined && c.code.length <= 40 && !c.code.includes(" ");
|
|
138
|
+
lines.push(` - ${c.name}${isSlug ? ` [${c.code}]` : ""}`);
|
|
139
|
+
if (c.code !== undefined && !isSlug)
|
|
140
|
+
lines.push(` ${c.code}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push(`${(0, term_1.doctorGlyph)(r.ok ? "ok" : "fail")} ${r.ok ? "bundle verifies" : "bundle verification FAILED"}`);
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
package/dist/shell/report.js
CHANGED
|
@@ -338,7 +338,8 @@ function writeReport(run) {
|
|
|
338
338
|
"| Phase | Status | Completed | Total |",
|
|
339
339
|
"| --- | --- | ---: | ---: |",
|
|
340
340
|
...run.phases.map((phase) => {
|
|
341
|
-
const
|
|
341
|
+
const taskIds = new Set(phase.taskIds);
|
|
342
|
+
const phaseTasks = run.tasks.filter((t) => taskIds.has(t.id));
|
|
342
343
|
const completed = phaseTasks.filter((t) => t.status === "completed").length;
|
|
343
344
|
return `| ${phase.name} | ${phase.status} | ${completed} | ${phaseTasks.length} |`;
|
|
344
345
|
}),
|