cool-workflow 0.1.95 → 0.1.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review/workflow.js +3 -3
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/agent-config.js +2 -1
  11. package/dist/dispatch.js +12 -6
  12. package/dist/evidence-grounding.js +18 -13
  13. package/dist/execution-backend/probes.js +22 -6
  14. package/dist/execution-backend.js +37 -4
  15. package/dist/node-snapshot.js +3 -3
  16. package/dist/orchestrator/lifecycle-operations.js +13 -5
  17. package/dist/orchestrator.js +26 -1
  18. package/dist/reclamation.js +8 -2
  19. package/dist/run-registry/derive.js +4 -1
  20. package/dist/scheduler.js +14 -14
  21. package/dist/schema-validate.js +8 -2
  22. package/dist/state-explosion/helpers.js +4 -21
  23. package/dist/state-explosion/size.js +63 -0
  24. package/dist/state-explosion.js +18 -71
  25. package/dist/state.js +47 -9
  26. package/dist/trust-audit.js +27 -2
  27. package/dist/util/fingerprint.js +19 -0
  28. package/dist/util/fingerprint.test.js +27 -0
  29. package/dist/version.js +1 -1
  30. package/dist/workbench-host.js +11 -0
  31. package/dist/workbench.js +19 -17
  32. package/dist/worker-isolation.js +25 -1
  33. package/docs/agent-delegation-drive.7.md +64 -1
  34. package/docs/cli-mcp-parity.7.md +2 -0
  35. package/docs/contract-migration-tooling.7.md +2 -0
  36. package/docs/control-plane-scheduling.7.md +2 -0
  37. package/docs/durable-state-and-locking.7.md +2 -0
  38. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  39. package/docs/execution-backends.7.md +2 -0
  40. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  41. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  42. package/docs/multi-agent-operator-ux.7.md +2 -0
  43. package/docs/node-snapshot-diff-replay.7.md +2 -0
  44. package/docs/observability-cost-accounting.7.md +2 -0
  45. package/docs/project-index.md +7 -3
  46. package/docs/real-execution-backends.7.md +2 -0
  47. package/docs/release-and-migration.7.md +2 -0
  48. package/docs/release-tooling.7.md +18 -0
  49. package/docs/routines.md +30 -0
  50. package/docs/run-registry-control-plane.7.md +2 -0
  51. package/docs/run-retention-reclamation.7.md +2 -0
  52. package/docs/sandbox-profiles.7.md +15 -0
  53. package/docs/state-explosion-management.7.md +2 -0
  54. package/docs/team-collaboration.7.md +2 -0
  55. package/docs/web-desktop-workbench.7.md +2 -0
  56. package/manifest/plugin.manifest.json +1 -1
  57. package/package.json +5 -3
  58. package/scripts/agents/agent-adapter-core.js +22 -1
  59. package/scripts/agents/claude-p-agent.js +10 -2
  60. package/scripts/agents/codex-agent.js +22 -2
  61. package/scripts/agents/gemini-agent.js +10 -2
  62. package/scripts/agents/opencode-agent.js +10 -2
  63. package/scripts/canonical-apps.js +4 -4
  64. package/scripts/dogfood-release.js +1 -1
  65. package/scripts/golden-path.js +4 -4
  66. package/scripts/release-flow.js +10 -0
  67. package/scripts/release-gate.sh +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.1.95",
4
+ "version": "0.1.96",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.95",
3
+ "version": "0.1.96",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -1,4 +1,4 @@
1
- module.exports = ({ workflow, phase, agent, artifact, input }) => {
1
+ module.exports = ({ workflow, phase, parallel, agent, artifact, input }) => {
2
2
  const inputs = [
3
3
  input("repo", {
4
4
  type: "path",
@@ -33,7 +33,7 @@ module.exports = ({ workflow, phase, agent, artifact, input }) => {
33
33
  inputs,
34
34
  sandboxProfiles: ["readonly"],
35
35
  phases: [
36
- phase("Map", [
36
+ parallel("Map", [
37
37
  agent(
38
38
  "map:server-api",
39
39
  "Map server/API entrypoints, request flows, service boundaries, auth surfaces, and owned state in {{repo}} for {{question}}. Focus: {{focus}}. Invariants: {{invariant}}. Return inspected files, dependencies, invariants, and candidate risks.",
@@ -65,7 +65,7 @@ module.exports = ({ workflow, phase, agent, artifact, input }) => {
65
65
  { sandboxProfileId: "readonly" }
66
66
  )
67
67
  ]),
68
- phase("Assess", [
68
+ parallel("Assess", [
69
69
  agent(
70
70
  "assess:security",
71
71
  "Assess mapper findings through a security lens. Separate real, conditional, non-issue, and unknown risks with evidence, falsifiers, and exact files or config keys.",
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.95",
6
+ "version": "0.1.96",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -31,6 +31,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
31
31
  const node_path_1 = __importDefault(require("node:path"));
32
32
  const run_registry_1 = require("./run-registry");
33
33
  const execution_backend_1 = require("./execution-backend");
34
+ const state_1 = require("./state");
34
35
  exports.AGENT_CONFIG_SCHEMA_VERSION = 1;
35
36
  function agentConfigPath(env = process.env) {
36
37
  return node_path_1.default.join((0, run_registry_1.resolveCwHome)(env), "agent-config.json");
@@ -260,7 +261,7 @@ function setAgentConfigFile(patch, env = process.env) {
260
261
  const stored = redacted(merged);
261
262
  const file = agentConfigPath(env);
262
263
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
263
- node_fs_1.default.writeFileSync(file, `${JSON.stringify(stored, null, 2)}\n`, "utf8");
264
+ (0, state_1.writeJson)(file, stored);
264
265
  return stored;
265
266
  }
266
267
  /** Read-only, deterministic projection of the effective config (secret-stripped).
package/dist/dispatch.js CHANGED
@@ -202,15 +202,16 @@ function formatDispatchTask(task) {
202
202
  };
203
203
  }
204
204
  // Deterministic dispatch id (replay-determinism self-audit): the wall-clock stamp
205
- // is an edge timestamp (stripped on replay), but the former Math.random() suffix
206
- // made every dispatch mint a non-reproducible id. The suffix is now a per-run
207
- // sequence the count of dispatches already allocated on this run so re-running
208
- // the same workflow yields byte-identical dispatch ids while each dispatch within a
209
- // run still gets a distinct, monotonically increasing id. Mirrors the de-clock done
210
- // for worker ids in src/worker-isolation/paths.ts.
205
+ // is an edge timestamp for human readability. Set CW_DETERMINISTIC_RUN_IDS=1 to
206
+ // use a content-hash-based id without wall-clock, so re-running the same workflow
207
+ // yields byte-identical dispatch ids. The suffix is a per-run sequence, so each
208
+ // dispatch still gets a distinct, monotonically increasing id.
211
209
  function createDispatchId(run) {
212
210
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
213
211
  const seq = (run.dispatches?.length || 0) + 1;
212
+ if (/^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "")) {
213
+ return `dispatch-${String(seq).padStart(4, "0")}`;
214
+ }
214
215
  return `dispatch-${stamp}-${String(seq).padStart(4, "0")}`;
215
216
  }
216
217
  // H7: persist a CUSTOM sandbox profile DEFINITION (loaded from a FILE at dispatch)
@@ -240,5 +241,10 @@ function persistCustomSandboxProfile(run, requested) {
240
241
  if (!definition || typeof definition !== "object" || typeof definition.id !== "string" || !definition.id)
241
242
  return;
242
243
  run.customSandboxProfiles = run.customSandboxProfiles || {};
244
+ const previous = run.customSandboxProfiles[definition.id];
245
+ if (previous && JSON.stringify(previous) !== JSON.stringify(definition)) {
246
+ throw new Error(`Sandbox profile id collision: "${definition.id}" is already defined by a different custom profile ` +
247
+ `(source: ${absolute}). Use a unique id in each custom profile file.`);
248
+ }
243
249
  run.customSandboxProfiles[definition.id] = definition;
244
250
  }
@@ -27,15 +27,14 @@ const node_path_1 = __importDefault(require("node:path"));
27
27
  // are all legitimate — so we cannot require "must be a file that exists" by
28
28
  // default without breaking cross-repo, URL, and runtime-token evidence. Instead:
29
29
  //
30
- // 1. DEFAULT (pure, deterministic): require evidence to be GROUNDED — a URL, a
31
- // path-like locator, or a `namespace:value` token. This rejects bare prose
32
- // ("x", "anything", "HIGH severity") while accepting every shape CW itself
33
- // emits. Being a pure function of the string, it is replay-safe.
30
+ // 1. DEFAULT: require evidence to be GROUNDED — a URL, a path-like locator, or
31
+ // a `namespace:value` token. This rejects bare prose ("x", "anything",
32
+ // "HIGH severity") while accepting every shape CW itself emits. Being a pure
33
+ // function of the string, it is replay-safe.
34
34
  //
35
- // 2. OPT-IN (CW_REQUIRE_RESOLVABLE_EVIDENCE=1): additionally resolve path-like
36
- // locators against the run's base dirs and fail closed if a file locator does
37
- // not exist on disk. Off by default because the resolving cwd is context-
38
- // dependent; on for self-audit / compliance where evidence MUST be checkable.
35
+ // 2. DEFAULT (v0.1.95): file-style evidence locators MUST also exist on disk.
36
+ // CW_REQUIRE_RESOLVABLE_EVIDENCE=0 restores the prior shape-only behavior.
37
+ // CW_REQUIRE_RESOLVABLE_EVIDENCE=url additionally requires URL reachability.
39
38
  // ---------------------------------------------------------------------------
40
39
  const URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
41
40
  const PATH_SEP_RE = /[\\/]/;
@@ -63,14 +62,20 @@ function hasGroundedEvidence(evidence) {
63
62
  return Array.isArray(evidence) && evidence.some((entry) => isGroundedEvidence(entry));
64
63
  }
65
64
  /** Whether opt-in strict resolution is requested via the environment.
66
- * Set CW_REQUIRE_RESOLVABLE_EVIDENCE=1 for file resolution, =url for
67
- * URL reachability checks too (v0.1.63). */
65
+ * Enabled by DEFAULT (v0.1.95): file-style evidence locators MUST exist on disk.
66
+ * Set CW_REQUIRE_RESOLVABLE_EVIDENCE=0 to restore the prior shape-only check.
67
+ * Set CW_REQUIRE_RESOLVABLE_EVIDENCE=url for URL reachability checks too. */
68
68
  function requireResolvableEvidence() {
69
- return /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE || "");
69
+ const raw = process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE;
70
+ if (raw === undefined || raw === null || raw === "")
71
+ return true;
72
+ if (/^(0|false|no|off)$/i.test(raw))
73
+ return false;
74
+ return true;
70
75
  }
71
- /** Whether URL reachability checks are enabled in strict mode (v0.1.63). */
76
+ /** Whether URL reachability checks are enabled (v0.1.63). Always opt-in. */
72
77
  function requireUrlReachability() {
73
- return requireResolvableEvidence() || /url/i.test(process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE || "");
78
+ return /url/i.test(process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE || "");
74
79
  }
75
80
  function classify(raw) {
76
81
  const value = raw.trim();
@@ -22,6 +22,7 @@ exports.probeAgentBackend = probeAgentBackend;
22
22
  //
23
23
  // Readiness probe. Deterministic given the host (PATH + configured env).
24
24
  const node_fs_1 = __importDefault(require("node:fs"));
25
+ const node_path_1 = __importDefault(require("node:path"));
25
26
  const util_1 = require("./util");
26
27
  function probeNodeBackend() {
27
28
  const ok = (0, util_1.hasExecutable)("node");
@@ -63,20 +64,35 @@ function probeContainerBackend() {
63
64
  reason: docker || podman ? undefined : "no container runtime (docker/podman) found; supply --image to delegate explicitly"
64
65
  };
65
66
  }
67
+ function delegateChildExists() {
68
+ return node_fs_1.default.existsSync(node_path_1.default.resolve(__dirname, "..", "..", "scripts", "children", "http-delegate-child.js"));
69
+ }
66
70
  function probeRemoteBackend() {
67
71
  const endpoint = (process.env.CW_REMOTE_ENDPOINT || "").trim();
72
+ const scriptOk = delegateChildExists();
73
+ const checks = [
74
+ { name: "endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_REMOTE_ENDPOINT configured" : "CW_REMOTE_ENDPOINT not set" },
75
+ { name: "delegate-child-script", ok: scriptOk, detail: scriptOk ? "http-delegate-child.js found" : "http-delegate-child.js missing — reinstall cool-workflow" }
76
+ ];
77
+ const readiness = !endpoint ? "unverified" : !scriptOk ? "unavailable" : "ready";
68
78
  return {
69
- checks: [{ name: "endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_REMOTE_ENDPOINT configured" : "CW_REMOTE_ENDPOINT not set" }],
70
- readiness: endpoint ? "ready" : "unverified",
71
- reason: endpoint ? undefined : "no remote endpoint configured (set CW_REMOTE_ENDPOINT or pass --endpoint)"
79
+ checks,
80
+ readiness,
81
+ reason: readiness === "ready" ? undefined : !endpoint ? "no remote endpoint configured (set CW_REMOTE_ENDPOINT or pass --endpoint)" : "delegate child script is missing"
72
82
  };
73
83
  }
74
84
  function probeCiBackend() {
75
85
  const endpoint = (process.env.CW_CI_ENDPOINT || "").trim();
86
+ const scriptOk = delegateChildExists();
87
+ const checks = [
88
+ { name: "ci-endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_CI_ENDPOINT configured" : "CW_CI_ENDPOINT not set" },
89
+ { name: "delegate-child-script", ok: scriptOk, detail: scriptOk ? "http-delegate-child.js found" : "http-delegate-child.js missing — reinstall cool-workflow" }
90
+ ];
91
+ const readiness = !endpoint ? "unverified" : !scriptOk ? "unavailable" : "ready";
76
92
  return {
77
- checks: [{ name: "ci-endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_CI_ENDPOINT configured" : "CW_CI_ENDPOINT not set" }],
78
- readiness: endpoint ? "ready" : "unverified",
79
- reason: endpoint ? undefined : "no CI job target configured (set CW_CI_ENDPOINT or pass --job)"
93
+ checks,
94
+ readiness,
95
+ reason: readiness === "ready" ? undefined : !endpoint ? "no CI job target configured (set CW_CI_ENDPOINT or pass --job)" : "delegate child script is missing"
80
96
  };
81
97
  }
82
98
  function probeAgentBackend() {
@@ -38,6 +38,7 @@ exports.requiredSandboxDimensions = requiredSandboxDimensions;
38
38
  exports.attestSandbox = attestSandbox;
39
39
  exports.probeBackend = probeBackend;
40
40
  exports.runBackend = runBackend;
41
+ exports.delegateChildScript = delegateChildScript;
41
42
  exports.createExecutionBackend = createExecutionBackend;
42
43
  exports.backendListPayload = backendListPayload;
43
44
  exports.backendShowPayload = backendShowPayload;
@@ -404,12 +405,22 @@ function executeLocal(descriptor, policy, request, label, attestation) {
404
405
  // shell backend runs via /bin/sh -c; node/bun run the command directly
405
406
  // (bun is Node-compatible by default so evidence stays byte-stable with node).
406
407
  // spawnStyle comes from the registered driver, not a hardcoded id check.
408
+ const spawnStyle = getBackendDriver(descriptor.id)?.spawnStyle;
409
+ if (spawnStyle === "shell") {
410
+ // Shell injection guard: reject args that contain shell control characters
411
+ // (beyond template placeholders). The command itself is operator-configured.
412
+ const shellArg = [command, ...args].join(" ").replace(/\{\{[a-zA-Z0-9_.-]+\}\}/g, "");
413
+ if (/[;&|`$(){}<>!\n\r]/.test(shellArg)) {
414
+ throw new Error(`Shell backend refused: args contain shell control characters. ` +
415
+ `Use the node, bun, or agent backend instead for untrusted inputs.`);
416
+ }
417
+ }
407
418
  const isTTY = process.stderr.isTTY;
408
419
  const shortLabel = command.split("/").pop() || command;
409
420
  if (isTTY)
410
421
  process.stderr.write(`● Running ${shortLabel}...\n`);
411
422
  const startedAt = process.hrtime.bigint();
412
- const result = getBackendDriver(descriptor.id)?.spawnStyle === "shell"
423
+ const result = spawnStyle === "shell"
413
424
  ? (0, node_child_process_1.spawnSync)([command, ...args].join(" "), { ...options, shell: true })
414
425
  : (0, node_child_process_1.spawnSync)(command, args, { ...options, shell: false });
415
426
  const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
@@ -587,7 +598,17 @@ function runContainer(descriptor, policy, request, label, handle, attestation) {
587
598
  // portable and synchronous from CW's view. We spawn it BY PATH (shell:false). The
588
599
  // path is resolved from this compiled module (dist/execution-backend.js) up to the
589
600
  // package's `scripts/children/` dir, which package.json ships in "files".
590
- const HTTP_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "scripts", "children", "http-delegate-child.js");
601
+ function delegateChildScript() {
602
+ const resolved = node_path_1.default.resolve(__dirname, "..", "scripts", "children", "http-delegate-child.js");
603
+ if (!node_fs_1.default.existsSync(resolved)) {
604
+ throw new Error(`Delegate child script not found at ${resolved}. ` +
605
+ `This indicates a broken installation — reinstall cool-workflow or ensure ` +
606
+ `"scripts/children/http-delegate-child.js" is shipped in the package.`);
607
+ }
608
+ return resolved;
609
+ }
610
+ /** The shared HTTP delegation child script path (resolved at import time). */
611
+ const HTTP_DELEGATE_CHILD_SCRIPT = delegateChildScript();
591
612
  /** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
592
613
  * (and polls a returned jobId) via a Node child, then records the runner's exit +
593
614
  * stdout digest as canonical evidence. Fails closed when the endpoint is missing,
@@ -675,9 +696,17 @@ function runAgentProcess(descriptor, policy, request, label, handle, attestation
675
696
  // captured as data. CI/pipes stay silent. CW_AGENT_STREAM=0 or
676
697
  // CW_NO_STREAM=1 forces off; CW_AGENT_STREAM=1 forces on.
677
698
  const streamStderr = process.env.CW_AGENT_STREAM !== "0" && Boolean(process.stderr.isTTY) && process.env.CW_NO_STREAM !== "1";
699
+ // Build child env from sandbox policy as baseline (respects env.inherit/expose/deny),
700
+ // then re-allow CW_* + well-known API key env vars the agent needs.
701
+ const childEnv = buildChildEnv(policy);
702
+ for (const key of Object.keys(process.env)) {
703
+ if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
704
+ childEnv[key] = process.env[key];
705
+ }
706
+ }
678
707
  const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
679
708
  cwd: request.cwd,
680
- env: { ...process.env },
709
+ env: childEnv,
681
710
  encoding: "utf8",
682
711
  timeout: resolved.timeoutMs || 600000,
683
712
  maxBuffer: 32 * 1024 * 1024,
@@ -896,7 +925,11 @@ function runtimeNote(descriptor) {
896
925
  }
897
926
  // ---- Probe cache (v0.1.60) — mechanism, not policy -----------------------
898
927
  const _probeCache = new Map();
899
- const PROBE_CACHE_TTL_MS = 60_000; // 60s
928
+ const PROBE_CACHE_TTL_MS = (() => {
929
+ const raw = process.env.CW_PROBE_CACHE_TTL_MS;
930
+ const n = raw ? parseInt(raw, 10) : 60_000;
931
+ return Number.isFinite(n) && n >= 0 ? n : 60_000;
932
+ })(); // default 60s; set CW_PROBE_CACHE_TTL_MS=0 to disable
900
933
  function cachedProbeBackend(id, context) {
901
934
  const key = `${id}:${context.cwd || ''}`;
902
935
  const cached = _probeCache.get(key);
@@ -38,7 +38,7 @@ const pipeline_runner_1 = require("./pipeline-runner");
38
38
  const state_1 = require("./state");
39
39
  const multi_agent_eval_1 = require("./multi-agent-eval");
40
40
  const node_projection_1 = require("./node-projection");
41
- const state_explosion_1 = require("./state-explosion");
41
+ const fingerprint_1 = require("./util/fingerprint");
42
42
  const validation_1 = require("./validation");
43
43
  exports.NODE_SNAPSHOT_SCHEMA_VERSION = 1;
44
44
  /** Structured fail-closed error (mirrors the PipelineContractError shape). */
@@ -75,7 +75,7 @@ function snapshotBody(node) {
75
75
  /** RAW fingerprint (NOT normalized): any transition (updatedAt/status) or
76
76
  * artifact/evidence change flips it, which is how drift is detected. */
77
77
  function sourceFingerprint(node) {
78
- return (0, state_explosion_1.fingerprintStrings)([
78
+ return (0, fingerprint_1.fingerprintStrings)([
79
79
  `node:${node.id}:${node.status}:${node.updatedAt}`,
80
80
  ...node.artifacts.map((artifact) => `artifact:${artifact.id}:${artifact.path}`),
81
81
  ...node.evidence.map((evidence) => `evidence:${evidence.id}:${evidence.path || ""}`)
@@ -203,7 +203,7 @@ function replayNodeSnapshot(run, snapshot, options = {}) {
203
203
  throw new NodeSnapshotError(freshness === "stale" ? "snapshot-stale" : "snapshot-absent", reason || `cannot replay a ${freshness} snapshot of node ${snapshot.nodeId}`, { freshness, details: { runId: run.id, nodeId: snapshot.nodeId } });
204
204
  }
205
205
  const body = (0, multi_agent_eval_1.normalizeValue)(snapshot.body);
206
- const outputFingerprint = (0, state_explosion_1.fingerprintStrings)([(0, multi_agent_eval_1.replayStableStringify)(body)]);
206
+ const outputFingerprint = (0, fingerprint_1.fingerprintStrings)([(0, multi_agent_eval_1.replayStableStringify)(body)]);
207
207
  const replay = {
208
208
  schemaVersion: 1,
209
209
  replayId: `replay-${snapshot.snapshotId}-${outputFingerprint.replace("sha256:", "").slice(0, 8)}`,
@@ -599,12 +599,20 @@ function renderPrompt(prompt, inputs) {
599
599
  // worker ids in src/worker-isolation/paths.ts.
600
600
  let runIdSequence = 0;
601
601
  function createRunId(workflowId) {
602
+ // Use process.pid + monotonic counter for uniqueness (no wall-clock),
603
+ // but keep a second-resolution stamp for human readability in the id.
602
604
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
603
- // The stamp is second-resolution, so several runs of the same workflowId minted
604
- // within one second would otherwise hash to the SAME id. process.pid + a monotonic
605
- // counter break the tie across BOTH same-process (counter) and concurrent-process
606
- // (pid) minting — deterministic-by-environment, not a PRNG, so it keeps the
607
- // replay-determinism intent; the id is an edge stamp stripped on replay anyway.
605
+ // Set CW_DETERMINISTIC_RUN_IDS=1 to use a content hash instead of wall-clock,
606
+ // so two plan() calls with the same inputs produce the same id (replay-safe).
607
+ if (/^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "")) {
608
+ runIdSequence += 1;
609
+ const suffix = node_crypto_1.default
610
+ .createHash("sha256")
611
+ .update(`${workflowId}:${process.pid}:${runIdSequence}`)
612
+ .digest("hex")
613
+ .slice(0, 6);
614
+ return `${workflowId}-${suffix}`;
615
+ }
608
616
  runIdSequence += 1;
609
617
  const suffix = node_crypto_1.default
610
618
  .createHash("sha256")
@@ -56,6 +56,7 @@ const worker_isolation_1 = require("./worker-isolation");
56
56
  const sandbox_profile_1 = require("./sandbox-profile");
57
57
  const execution_backend_1 = require("./execution-backend");
58
58
  const operator_ux_1 = require("./operator-ux");
59
+ const trust_audit_1 = require("./trust-audit");
59
60
  const multi_agent_1 = require("./multi-agent");
60
61
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
61
62
  const multi_agent_eval_1 = require("./multi-agent-eval");
@@ -102,12 +103,29 @@ class CoolWorkflowRunner {
102
103
  // reads runs from disk per call (no in-memory run state), so withBaseDir hands back
103
104
  // a cheap scoped clone instead of mutating the global process cwd.
104
105
  baseDir;
106
+ // Per-request cache (v0.1.95): when set, loadRun memoizes by runId so the 17
107
+ // workbench sub-panels that each call loadRun independently share one read.
108
+ _requestCache;
105
109
  constructor({ pluginRoot, baseDir }) {
106
110
  this.pluginRoot = resolvePluginRoot(pluginRoot);
107
111
  this.workflowsDir = node_path_1.default.join(this.pluginRoot, "workflows");
108
112
  this.appsDir = node_path_1.default.join(this.pluginRoot, "apps");
109
113
  this.baseDir = baseDir ? node_path_1.default.resolve(baseDir) : undefined;
110
114
  }
115
+ /** Run fn with a per-request loadRun cache. All loadRun calls inside fn share
116
+ * the same cached run state, collapsing 18 reads into 1. Returns fn's result
117
+ * and clears the cache afterwards (never leaks between requests). */
118
+ loadWithCache(fn) {
119
+ this._requestCache = new Map();
120
+ (0, trust_audit_1.setAuditEventCache)(new Map());
121
+ try {
122
+ return fn(this);
123
+ }
124
+ finally {
125
+ this._requestCache = undefined;
126
+ (0, trust_audit_1.clearAuditEventCache)();
127
+ }
128
+ }
111
129
  /** Return a runner that resolves runs against `dir` instead of process.cwd(),
112
130
  * WITHOUT chdir-ing the process (F7). Same instance when the dir is unchanged. */
113
131
  withBaseDir(dir) {
@@ -632,7 +650,14 @@ class CoolWorkflowRunner {
632
650
  return feedbackOps.resolveFeedback(this.loadRun(runId), feedbackId, options);
633
651
  }
634
652
  loadRun(runId) {
635
- return (0, state_1.loadRunFromCwd)(runId, this.baseDir);
653
+ if (this._requestCache) {
654
+ const cached = this._requestCache.get(runId);
655
+ if (cached)
656
+ return cached;
657
+ }
658
+ const run = (0, state_1.loadRunFromCwd)(runId, this.baseDir);
659
+ this._requestCache?.set(runId, run);
660
+ return run;
636
661
  }
637
662
  invocationCwd() {
638
663
  return this.baseDir || process.cwd();
@@ -121,7 +121,10 @@ function loadReclamationLog(run) {
121
121
  return { schemaVersion: 1, runId: run.id, tombstones: [] };
122
122
  try {
123
123
  const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
124
- return { schemaVersion: 1, runId: run.id, tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
124
+ if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1 || !Array.isArray(parsed.tombstones)) {
125
+ return { schemaVersion: 1, runId: run.id, tombstones: [] };
126
+ }
127
+ return { schemaVersion: 1, runId: run.id, tombstones: parsed.tombstones };
125
128
  }
126
129
  catch {
127
130
  // A malformed overlay must NOT brick the run — fail closed to an empty chain.
@@ -420,7 +423,10 @@ function planReclamation(run, policy = {}) {
420
423
  catch {
421
424
  continue; // unreadable snapshot → retain (fail closed)
422
425
  }
423
- const node = (run.nodes || []).find((n) => n.id === snap.nodeId);
426
+ if (!snap || typeof snap !== "object" || typeof snap.nodeId !== "string")
427
+ continue;
428
+ const nodeId = snap.nodeId;
429
+ const node = (run.nodes || []).find((n) => n.id === nodeId);
424
430
  if (!node)
425
431
  continue; // source node gone → cannot reconstruct → retain
426
432
  if (repointNodeIds.has(node.id))
@@ -164,7 +164,10 @@ function loadReclaimedFromDir(runDir) {
164
164
  return { schemaVersion: 1, runId: "", tombstones: [] };
165
165
  try {
166
166
  const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
167
- return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
167
+ if (!parsed || typeof parsed !== "object" || parsed.schemaVersion !== 1 || !Array.isArray(parsed.tombstones)) {
168
+ return { schemaVersion: 1, runId: "", tombstones: [] };
169
+ }
170
+ return { schemaVersion: 1, runId: String(parsed.runId || ""), tombstones: parsed.tombstones };
168
171
  }
169
172
  catch {
170
173
  return { schemaVersion: 1, runId: "", tombstones: [] };
package/dist/scheduler.js CHANGED
@@ -274,21 +274,19 @@ function addJitter(date, jitterSeconds) {
274
274
  return new Date(date.getTime() + seconds * 1000);
275
275
  }
276
276
  // Deterministic schedule id (replay-determinism self-audit): the stamp is an edge
277
- // timestamp (recorded once), but the former Math.random() suffix made each
278
- // persisted schedule id non-reproducible. The suffix is now a content hash of the
279
- // schedule's deterministic identity (kind + the recorded stamp), so re-deriving the
280
- // id for a recorded schedule yields the byte-identical value while schedules created
281
- // at distinct instants still get distinct ids. Mirrors src/worker-isolation/paths.ts.
277
+ // timestamp (recorded once). Set CW_DETERMINISTIC_RUN_IDS=1 to use a
278
+ // content-hash-based id without wall-clock, so re-deriving the id for a recorded
279
+ // schedule yields the byte-identical value. Distinct instants still get distinct
280
+ // ids via the monotonic counter.
282
281
  let scheduleIdSequence = 0;
283
282
  function createScheduleId(kind) {
284
283
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
285
- // Second-resolution stamp: two schedules of the same kind created within one
286
- // second would otherwise collide on an identical id. process.pid + a monotonic
287
- // counter break the tie across concurrent processes and within one process,
288
- // deterministically (not a PRNG).
289
284
  scheduleIdSequence += 1;
290
- const suffix = node_crypto_1.default.createHash("sha256").update(`${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`).digest("hex").slice(0, 6);
291
- return `${kind}-${stamp}-${suffix}`;
285
+ const deterministic = /^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "");
286
+ const suffix = node_crypto_1.default.createHash("sha256")
287
+ .update(deterministic ? `${kind}:${process.pid}:${scheduleIdSequence}` : `${kind}:${stamp}:${process.pid}:${scheduleIdSequence}`)
288
+ .digest("hex").slice(0, 6);
289
+ return deterministic ? `${kind}-${suffix}` : `${kind}-${stamp}-${suffix}`;
292
290
  }
293
291
  // Deterministic schedule-run (history) id — same rationale as createScheduleId. The
294
292
  // history record stamp differs from the owning schedule's, so the hashed identity
@@ -297,10 +295,12 @@ function createScheduleId(kind) {
297
295
  let scheduleRunIdSequence = 0;
298
296
  function createScheduleRunId(kind) {
299
297
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
300
- // pid + counter break same-kind/same-second collisions (see createScheduleId).
301
298
  scheduleRunIdSequence += 1;
302
- const suffix = node_crypto_1.default.createHash("sha256").update(`run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`).digest("hex").slice(0, 6);
303
- return `run-${kind}-${stamp}-${suffix}`;
299
+ const deterministic = /^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "");
300
+ const suffix = node_crypto_1.default.createHash("sha256")
301
+ .update(deterministic ? `run:${kind}:${process.pid}:${scheduleRunIdSequence}` : `run:${kind}:${stamp}:${process.pid}:${scheduleRunIdSequence}`)
302
+ .digest("hex").slice(0, 6);
303
+ return deterministic ? `run-${kind}-${suffix}` : `run-${kind}-${stamp}-${suffix}`;
304
304
  }
305
305
  function requiredString(value, name) {
306
306
  const text = stringOption(value);
@@ -12,8 +12,8 @@
12
12
  // type (object|array|string|number|integer|boolean|null, or an array of them),
13
13
  // enum, const, required, properties, additionalProperties (false), items.
14
14
  // Unsupported keywords ($ref, allOf/anyOf/oneOf, pattern, formats, numeric
15
- // bounds) are IGNORED never silently "passed" as a constraint that wasn't
16
- // checked; a schema relying on them simply isn't enforced for those keywords.
15
+ // bounds) are surfaced as WARNINGS (a constraint that wasn't checked), so the
16
+ // operator can see their schema reliance is incomplete.
17
17
  // (If full JSON Schema is needed later, swap this module's impl for ajv behind
18
18
  // the same signature.)
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -52,6 +52,12 @@ function validateAgainstSchema(value, schema, path = "$") {
52
52
  const errors = [];
53
53
  if (!schema || typeof schema !== "object")
54
54
  return errors;
55
+ // Unsupported keywords — surface as diagnostics (Rule of Silence: stderr)
56
+ const UNSUPPORTED = new Set(["$ref", "allOf", "anyOf", "oneOf", "not", "pattern", "format", "minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems", "uniqueItems", "contains", "if", "then", "else"]);
57
+ const unsupported = Object.keys(schema).filter((k) => UNSUPPORTED.has(k));
58
+ if (unsupported.length && process.stderr.isTTY) {
59
+ process.stderr.write(`[cw] schema at ${path}: unsupported keywords ignored: ${unsupported.join(", ")}\n`);
60
+ }
55
61
  // type
56
62
  if (schema.type !== undefined) {
57
63
  const types = Array.isArray(schema.type) ? schema.type.map(String) : [String(schema.type)];
@@ -1,13 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fingerprintStrings = exports.fingerprintRecords = void 0;
6
4
  exports.isProtectedStatus = isProtectedStatus;
7
5
  exports.dominantStatus = dominantStatus;
8
6
  exports.parentMap = parentMap;
9
- exports.fingerprintRecords = fingerprintRecords;
10
- exports.fingerprintStrings = fingerprintStrings;
11
7
  exports.stableLine = stableLine;
12
8
  exports.sortKeys = sortKeys;
13
9
  exports.stripRunId = stripRunId;
@@ -15,14 +11,9 @@ exports.unique = unique;
15
11
  exports.byId = byId;
16
12
  exports.truncate = truncate;
17
13
  exports.slug = slug;
18
- // Pure, stateless helpers for the state-explosion derived-index layer —
19
- // status priority, fingerprinting, deterministic key-sorting, id/string
20
- // utilities. Carved out of state-explosion.ts (FreeBSD-audit god-module carve)
21
- // so the report/graph/digest builders no longer bundle the primitive helper
22
- // layer. Nothing here touches run state beyond its arguments; every function is
23
- // pure (`fingerprintStrings` is re-exported from state-explosion.ts to keep the
24
- // public surface byte-identical for importers).
25
- const node_crypto_1 = __importDefault(require("node:crypto"));
14
+ const fingerprint_1 = require("../util/fingerprint");
15
+ Object.defineProperty(exports, "fingerprintRecords", { enumerable: true, get: function () { return fingerprint_1.fingerprintRecords; } });
16
+ Object.defineProperty(exports, "fingerprintStrings", { enumerable: true, get: function () { return fingerprint_1.fingerprintStrings; } });
26
17
  function isProtectedStatus(status) {
27
18
  return ["failed", "blocked", "rejected", "conflicting"].includes(status);
28
19
  }
@@ -41,14 +32,6 @@ function parentMap(edges) {
41
32
  }
42
33
  return parents;
43
34
  }
44
- function fingerprintRecords(records) {
45
- return fingerprintStrings(records.map((r) => `${r.id}:${r.status || ""}`).sort());
46
- }
47
- function fingerprintStrings(values) {
48
- const hash = node_crypto_1.default.createHash("sha256");
49
- hash.update(JSON.stringify([...values].sort()));
50
- return `sha256:${hash.digest("hex").slice(0, 32)}`;
51
- }
52
35
  function stableLine(value) {
53
36
  return JSON.stringify(sortKeys(value));
54
37
  }