cool-workflow 0.1.95 → 0.1.97

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 (89) 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/candidate-scoring.js +3 -3
  12. package/dist/capability-core.js +6 -1
  13. package/dist/cli/handlers/scheduling.js +7 -1
  14. package/dist/dispatch.js +12 -6
  15. package/dist/drive.js +10 -0
  16. package/dist/evidence-grounding.js +18 -13
  17. package/dist/execution-backend/agent.js +1 -1
  18. package/dist/execution-backend/probes.js +22 -6
  19. package/dist/execution-backend.js +61 -8
  20. package/dist/mcp-server.js +4 -0
  21. package/dist/node-snapshot.js +3 -3
  22. package/dist/onramp.js +2 -0
  23. package/dist/orchestrator/app-operations.js +6 -0
  24. package/dist/orchestrator/cli-options.js +8 -2
  25. package/dist/orchestrator/lifecycle-operations.js +16 -5
  26. package/dist/orchestrator/migration-operations.js +1 -1
  27. package/dist/orchestrator.js +26 -1
  28. package/dist/reclamation.js +8 -2
  29. package/dist/run-export.js +10 -1
  30. package/dist/run-registry/derive.js +4 -1
  31. package/dist/sandbox-profile.js +6 -1
  32. package/dist/scheduler.js +14 -14
  33. package/dist/schema-validate.js +8 -2
  34. package/dist/state-explosion/helpers.js +4 -21
  35. package/dist/state-explosion/size.js +63 -0
  36. package/dist/state-explosion.js +18 -71
  37. package/dist/state.js +47 -9
  38. package/dist/triggers.js +7 -1
  39. package/dist/trust-audit.js +27 -2
  40. package/dist/util/fingerprint.js +19 -0
  41. package/dist/util/fingerprint.test.js +27 -0
  42. package/dist/version.js +1 -1
  43. package/dist/workbench-host.js +17 -0
  44. package/dist/workbench.js +19 -17
  45. package/dist/worker-isolation.js +25 -1
  46. package/docs/agent-delegation-drive.7.md +66 -1
  47. package/docs/cli-mcp-parity.7.md +4 -0
  48. package/docs/contract-migration-tooling.7.md +4 -0
  49. package/docs/control-plane-scheduling.7.md +4 -0
  50. package/docs/demo.7.md +80 -0
  51. package/docs/doctor.7.md +97 -0
  52. package/docs/durable-state-and-locking.7.md +4 -0
  53. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  54. package/docs/execution-backends.7.md +4 -0
  55. package/docs/fix.7.md +44 -0
  56. package/docs/init.7.md +62 -0
  57. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  58. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  59. package/docs/multi-agent-operator-ux.7.md +4 -0
  60. package/docs/node-snapshot-diff-replay.7.md +4 -0
  61. package/docs/observability-cost-accounting.7.md +4 -0
  62. package/docs/pipeline-verbs.7.md +93 -0
  63. package/docs/project-index.md +20 -4
  64. package/docs/real-execution-backends.7.md +4 -0
  65. package/docs/release-and-migration.7.md +4 -0
  66. package/docs/release-tooling.7.md +20 -0
  67. package/docs/routine.7.md +73 -0
  68. package/docs/routines.md +30 -0
  69. package/docs/run-registry-control-plane.7.md +4 -0
  70. package/docs/run-retention-reclamation.7.md +4 -0
  71. package/docs/sandbox-profiles.7.md +15 -0
  72. package/docs/state-explosion-management.7.md +4 -0
  73. package/docs/team-collaboration.7.md +4 -0
  74. package/docs/web-desktop-workbench.7.md +4 -0
  75. package/manifest/README.md +16 -10
  76. package/manifest/plugin.manifest.json +1 -1
  77. package/package.json +5 -3
  78. package/scripts/agents/agent-adapter-core.js +25 -1
  79. package/scripts/agents/claude-p-agent.js +10 -2
  80. package/scripts/agents/codex-agent.js +22 -2
  81. package/scripts/agents/gemini-agent.js +10 -2
  82. package/scripts/agents/opencode-agent.js +10 -2
  83. package/scripts/canonical-apps.js +4 -4
  84. package/scripts/children/batch-delegate-child.js +10 -3
  85. package/scripts/children/http-delegate-child.js +2 -1
  86. package/scripts/dogfood-release.js +1 -1
  87. package/scripts/golden-path.js +4 -4
  88. package/scripts/release-flow.js +24 -6
  89. package/scripts/release-gate.sh +1 -1
@@ -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))
@@ -453,7 +453,16 @@ function verifyReportBundle(archivePath, options = {}) {
453
453
  }
454
454
  if (options.extractReportTo && reportContent !== undefined) {
455
455
  reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
456
- node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
456
+ if (options.cwd) {
457
+ const baseCwd = node_path_1.default.resolve(options.cwd);
458
+ if (!(0, state_1.isContainedPath)(reportExtractedTo, baseCwd)) {
459
+ failedChecks.push({ name: "extract-report", code: "path-outside-working-directory" });
460
+ reportExtractedTo = undefined;
461
+ }
462
+ }
463
+ if (reportExtractedTo) {
464
+ node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
465
+ }
457
466
  }
458
467
  }
459
468
  catch (error) {
@@ -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: [] };
@@ -132,7 +132,12 @@ function resolveSandboxProfileById(id, context = defaultSandboxContext()) {
132
132
  // This closes the gap where `sandbox validate` accepted a custom profile that
133
133
  // dispatch/worker-isolation then refused — validated but never enforceable.
134
134
  // A non-bundled, non-file id still fails closed via showBundledSandboxProfile.
135
- const absolute = node_path_1.default.resolve(requested);
135
+ const absolute = node_path_1.default.resolve(context.cwd, requested);
136
+ if (!absolute.startsWith(node_path_1.default.resolve(context.cwd) + node_path_1.default.sep) && absolute !== node_path_1.default.resolve(context.cwd)) {
137
+ throw new SandboxProfileError("sandbox-profile-path-escape", `Custom profile path traversal denied: ${requested}`, {
138
+ details: { requested }
139
+ });
140
+ }
136
141
  if (node_fs_1.default.existsSync(absolute) && node_fs_1.default.statSync(absolute).isFile()) {
137
142
  const result = validateSandboxProfileFile(requested, context);
138
143
  if (!result.valid || !result.profile) {
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
  }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.STATE_EXPLOSION_SCHEMA_VERSION = void 0;
4
+ exports.computeStateSize = computeStateSize;
5
+ exports.computeStateSizeWithGraph = computeStateSizeWithGraph;
6
+ const multi_agent_operator_ux_1 = require("../multi-agent-operator-ux");
7
+ exports.STATE_EXPLOSION_SCHEMA_VERSION = 1;
8
+ exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = {
9
+ graphNodes: 40,
10
+ graphEdges: 60,
11
+ blackboardMessages: 25,
12
+ blackboardRecords: 40,
13
+ collapseBucket: 6,
14
+ totalRecords: 80
15
+ };
16
+ function computeStateSize(run, thresholds = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS) {
17
+ return computeStateSizeWithGraph(run, thresholds, (0, multi_agent_operator_ux_1.buildMultiAgentOperatorGraph)(run));
18
+ }
19
+ function computeStateSizeWithGraph(run, thresholds, graph) {
20
+ const ma = run.multiAgent || { runs: [], roles: [], groups: [], memberships: [], fanouts: [], fanins: [] };
21
+ const bb = run.blackboard || { topics: [], messages: [], contexts: [], artifacts: [], snapshots: [], decisions: [] };
22
+ const counts = {
23
+ multiAgentRuns: (ma.runs || []).length,
24
+ roles: (ma.roles || []).length,
25
+ groups: (ma.groups || []).length,
26
+ memberships: (ma.memberships || []).length,
27
+ fanouts: (ma.fanouts || []).length,
28
+ fanins: (ma.fanins || []).length,
29
+ topics: (bb.topics || []).length,
30
+ messages: (bb.messages || []).length,
31
+ contexts: (bb.contexts || []).length,
32
+ artifacts: (bb.artifacts || []).length,
33
+ snapshots: (bb.snapshots || []).length,
34
+ decisions: (bb.decisions || []).length,
35
+ graphNodes: graph.nodes.length,
36
+ graphEdges: graph.edges.length
37
+ };
38
+ const total = counts.multiAgentRuns +
39
+ counts.roles +
40
+ counts.groups +
41
+ counts.memberships +
42
+ counts.fanouts +
43
+ counts.fanins +
44
+ counts.topics +
45
+ counts.messages +
46
+ counts.contexts +
47
+ counts.artifacts +
48
+ counts.snapshots +
49
+ counts.decisions;
50
+ const reasons = [];
51
+ if (counts.graphNodes > thresholds.graphNodes)
52
+ reasons.push(`graph has ${counts.graphNodes} nodes (> ${thresholds.graphNodes})`);
53
+ if (counts.graphEdges > thresholds.graphEdges)
54
+ reasons.push(`graph has ${counts.graphEdges} edges (> ${thresholds.graphEdges})`);
55
+ if (counts.messages > thresholds.blackboardMessages)
56
+ reasons.push(`blackboard has ${counts.messages} messages (> ${thresholds.blackboardMessages})`);
57
+ const bbRecords = counts.topics + counts.messages + counts.contexts + counts.artifacts + counts.snapshots + counts.decisions;
58
+ if (bbRecords > thresholds.blackboardRecords)
59
+ reasons.push(`blackboard has ${bbRecords} records (> ${thresholds.blackboardRecords})`);
60
+ if (total > thresholds.totalRecords)
61
+ reasons.push(`run has ${total} multi-agent records (> ${thresholds.totalRecords})`);
62
+ return { ...counts, total, compactionRecommended: reasons.length > 0, reasons: reasons.sort() };
63
+ }
@@ -3,8 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.stateExplosionReportLines = exports.formatBlackboardDigest = exports.formatCompactGraph = exports.formatStateExplosionReport = exports.fingerprintStrings = exports.GRAPH_VIEWS = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.STATE_EXPLOSION_SCHEMA_VERSION = void 0;
7
- exports.computeStateSize = computeStateSize;
6
+ exports.stateExplosionReportLines = exports.formatBlackboardDigest = exports.formatCompactGraph = exports.formatStateExplosionReport = exports.fingerprintStrings = exports.GRAPH_VIEWS = exports.STATE_EXPLOSION_SCHEMA_VERSION = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = exports.computeStateSizeWithGraph = exports.computeStateSize = void 0;
8
7
  exports.summarizeBlackboardDigest = summarizeBlackboardDigest;
9
8
  exports.buildCompactGraph = buildCompactGraph;
10
9
  exports.buildStateExplosionReport = buildStateExplosionReport;
@@ -21,15 +20,11 @@ const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
21
20
  const trust_audit_1 = require("./trust-audit");
22
21
  const evidence_reasoning_1 = require("./evidence-reasoning");
23
22
  const helpers_1 = require("./state-explosion/helpers");
24
- exports.STATE_EXPLOSION_SCHEMA_VERSION = 1;
25
- exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS = {
26
- graphNodes: 40,
27
- graphEdges: 60,
28
- blackboardMessages: 25,
29
- blackboardRecords: 40,
30
- collapseBucket: 6,
31
- totalRecords: 80
32
- };
23
+ const size_1 = require("./state-explosion/size");
24
+ Object.defineProperty(exports, "computeStateSize", { enumerable: true, get: function () { return size_1.computeStateSize; } });
25
+ Object.defineProperty(exports, "computeStateSizeWithGraph", { enumerable: true, get: function () { return size_1.computeStateSizeWithGraph; } });
26
+ Object.defineProperty(exports, "DEFAULT_STATE_EXPLOSION_THRESHOLDS", { enumerable: true, get: function () { return size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS; } });
27
+ Object.defineProperty(exports, "STATE_EXPLOSION_SCHEMA_VERSION", { enumerable: true, get: function () { return size_1.STATE_EXPLOSION_SCHEMA_VERSION; } });
33
28
  exports.GRAPH_VIEWS = [
34
29
  "full",
35
30
  "compact",
@@ -79,65 +74,17 @@ function graphKey(view, options) {
79
74
  view,
80
75
  options.focus || "",
81
76
  options.depth === undefined ? "" : String(options.depth),
82
- thresholdsKey(options.thresholds || exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS)
77
+ thresholdsKey(options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS)
83
78
  ].join("\0");
84
79
  }
85
80
  // ---------------------------------------------------------------------------
86
- // State size
81
+ // State size (implementation in state-explosion/size.ts)
87
82
  // ---------------------------------------------------------------------------
88
- function computeStateSize(run, thresholds = exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS) {
89
- return computeStateSizeWithGraph(run, thresholds, (0, multi_agent_operator_ux_1.buildMultiAgentOperatorGraph)(run));
90
- }
91
- function computeStateSizeWithGraph(run, thresholds, graph) {
92
- const ma = run.multiAgent || { runs: [], roles: [], groups: [], memberships: [], fanouts: [], fanins: [] };
93
- const bb = run.blackboard || { topics: [], messages: [], contexts: [], artifacts: [], snapshots: [], decisions: [] };
94
- const counts = {
95
- multiAgentRuns: (ma.runs || []).length,
96
- roles: (ma.roles || []).length,
97
- groups: (ma.groups || []).length,
98
- memberships: (ma.memberships || []).length,
99
- fanouts: (ma.fanouts || []).length,
100
- fanins: (ma.fanins || []).length,
101
- topics: (bb.topics || []).length,
102
- messages: (bb.messages || []).length,
103
- contexts: (bb.contexts || []).length,
104
- artifacts: (bb.artifacts || []).length,
105
- snapshots: (bb.snapshots || []).length,
106
- decisions: (bb.decisions || []).length,
107
- graphNodes: graph.nodes.length,
108
- graphEdges: graph.edges.length
109
- };
110
- const total = counts.multiAgentRuns +
111
- counts.roles +
112
- counts.groups +
113
- counts.memberships +
114
- counts.fanouts +
115
- counts.fanins +
116
- counts.topics +
117
- counts.messages +
118
- counts.contexts +
119
- counts.artifacts +
120
- counts.snapshots +
121
- counts.decisions;
122
- const reasons = [];
123
- if (counts.graphNodes > thresholds.graphNodes)
124
- reasons.push(`graph has ${counts.graphNodes} nodes (> ${thresholds.graphNodes})`);
125
- if (counts.graphEdges > thresholds.graphEdges)
126
- reasons.push(`graph has ${counts.graphEdges} edges (> ${thresholds.graphEdges})`);
127
- if (counts.messages > thresholds.blackboardMessages)
128
- reasons.push(`blackboard has ${counts.messages} messages (> ${thresholds.blackboardMessages})`);
129
- const bbRecords = counts.topics + counts.messages + counts.contexts + counts.artifacts + counts.snapshots + counts.decisions;
130
- if (bbRecords > thresholds.blackboardRecords)
131
- reasons.push(`blackboard has ${bbRecords} records (> ${thresholds.blackboardRecords})`);
132
- if (total > thresholds.totalRecords)
133
- reasons.push(`run has ${total} multi-agent records (> ${thresholds.totalRecords})`);
134
- return { ...counts, total, compactionRecommended: reasons.length > 0, reasons: reasons.sort() };
135
- }
136
83
  function stateSizeFor(run, thresholds, context) {
137
84
  const key = thresholdsKey(thresholds);
138
85
  let size = context.stateSizes.get(key);
139
86
  if (!size) {
140
- size = computeStateSizeWithGraph(run, thresholds, fullGraphFor(run, context));
87
+ size = (0, size_1.computeStateSizeWithGraph)(run, thresholds, fullGraphFor(run, context));
141
88
  context.stateSizes.set(key, size);
142
89
  }
143
90
  return size;
@@ -321,7 +268,7 @@ function summarizeBlackboardDigest(run, blackboardId) {
321
268
  ]);
322
269
  const fingerprint = (0, helpers_1.fingerprintRecords)([...topics, ...messages, ...contexts, ...artifacts, ...decisions]);
323
270
  return {
324
- schemaVersion: exports.STATE_EXPLOSION_SCHEMA_VERSION,
271
+ schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
325
272
  runId: run.id,
326
273
  id: `blackboard-digest${boardId ? `:${boardId}` : ""}`,
327
274
  scope: "blackboard",
@@ -368,7 +315,7 @@ function buildCompactGraph(run, view = "compact", options = {}) {
368
315
  return buildCompactGraphWithContext(run, view, options, createStateExplosionBuildContext());
369
316
  }
370
317
  function buildCompactGraphWithContext(run, view, options, context) {
371
- const thresholds = options.thresholds || exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
318
+ const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
372
319
  const key = graphKey(view, { ...options, thresholds });
373
320
  const cached = context.graphRecords.get(key);
374
321
  if (cached)
@@ -517,7 +464,7 @@ function finalizeGraphRecord(run, view, options, full, built) {
517
464
  ...built.syntheticNodes.filter((s) => s.blockedReason).map((s) => s.blockedReason)
518
465
  ]);
519
466
  return {
520
- schemaVersion: exports.STATE_EXPLOSION_SCHEMA_VERSION,
467
+ schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
521
468
  runId: run.id,
522
469
  id: `graph-${view}${options.focus ? `:focus:${(0, helpers_1.slug)(options.focus)}` : ""}`,
523
470
  scope: "run",
@@ -754,7 +701,7 @@ function buildOperatorDigestWithContext(run, thresholds, context) {
754
701
  ...compact.syntheticNodes.map((syn) => syn.expansionCommand)
755
702
  ]);
756
703
  return {
757
- schemaVersion: exports.STATE_EXPLOSION_SCHEMA_VERSION,
704
+ schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
758
705
  runId: run.id,
759
706
  id: "operator-digest",
760
707
  scope: "run",
@@ -817,7 +764,7 @@ function buildStateExplosionReport(run, options = {}) {
817
764
  return buildStateExplosionReportWithContext(run, options, createStateExplosionBuildContext());
818
765
  }
819
766
  function buildStateExplosionReportWithContext(run, options, context) {
820
- const thresholds = options.thresholds || exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
767
+ const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
821
768
  const stateSize = stateSizeFor(run, thresholds, context);
822
769
  const compactGraph = buildCompactGraphWithContext(run, "compact", { thresholds }, context);
823
770
  const criticalPathGraph = buildCompactGraphWithContext(run, "critical-path", { thresholds }, context);
@@ -847,7 +794,7 @@ function buildStateExplosionReportWithContext(run, options, context) {
847
794
  ? `node scripts/cw.js summary refresh ${run.id}`
848
795
  : operatorDigest.nextAction;
849
796
  return {
850
- schemaVersion: exports.STATE_EXPLOSION_SCHEMA_VERSION,
797
+ schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
851
798
  runId: run.id,
852
799
  generatedAt: new Date().toISOString(),
853
800
  stateSize,
@@ -887,7 +834,7 @@ function currentEntryFingerprint(run, entry, records) {
887
834
  * BSD: mechanism (check + refresh); policy (when to call) is at the call site. */
888
835
  function maybeCompactRun(run) {
889
836
  try {
890
- const size = computeStateSize(run);
837
+ const size = (0, size_1.computeStateSize)(run);
891
838
  if (size.compactionRecommended) {
892
839
  refreshStateExplosionSummaries(run);
893
840
  }
@@ -900,7 +847,7 @@ function summariesDir(run) {
900
847
  return node_path_1.default.join(run.paths.runDir, "summaries");
901
848
  }
902
849
  function refreshStateExplosionSummaries(run, options = {}) {
903
- const thresholds = options.thresholds || exports.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
850
+ const thresholds = options.thresholds || size_1.DEFAULT_STATE_EXPLOSION_THRESHOLDS;
904
851
  const context = createStateExplosionBuildContext();
905
852
  const dir = summariesDir(run);
906
853
  node_fs_1.default.mkdirSync(dir, { recursive: true });
@@ -923,7 +870,7 @@ function refreshStateExplosionSummaries(run, options = {}) {
923
870
  const compactGraph = buildCompactGraphWithContext(run, "compact", { thresholds }, context);
924
871
  const reportPath = node_path_1.default.join(dir, "state-explosion-report.json");
925
872
  const index = {
926
- schemaVersion: exports.STATE_EXPLOSION_SCHEMA_VERSION,
873
+ schemaVersion: size_1.STATE_EXPLOSION_SCHEMA_VERSION,
927
874
  runId: run.id,
928
875
  id: "multi-agent-summary-index",
929
876
  scope: "run",
package/dist/state.js CHANGED
@@ -70,6 +70,7 @@ function ensureRunDirs(paths) {
70
70
  function loadRunFromCwd(runId, cwd = process.cwd()) {
71
71
  if (!runId)
72
72
  throw new Error("Missing run id");
73
+ assertSafeRunId(runId);
73
74
  const statePath = node_path_1.default.join(cwd, ".cw", "runs", runId, "state.json");
74
75
  const result = loadRunStateFile(statePath, { dryRun: true });
75
76
  if (result.report.status === "unsupported") {
@@ -98,8 +99,11 @@ function migrateRunStateFile(statePath, options = {}) {
98
99
  }
99
100
  function saveCheckpoint(run) {
100
101
  run.updatedAt = new Date().toISOString();
101
- // state.json is the single source of truth — write it DURABLY (v0.1.40).
102
- writeJson(run.paths.state, run, { durable: true });
102
+ // state.json is the single source of truth — write it DURABLY with a lock
103
+ // so concurrent processes never lose an update (v0.1.95).
104
+ withFileLock(run.paths.state, () => {
105
+ writeJson(run.paths.state, run, { durable: true });
106
+ });
103
107
  }
104
108
  /** Compact a run checkpoint by stripping empty optional arrays and null values
105
109
  * that don't carry semantic meaning (v0.1.60). The normalization layer
@@ -245,6 +249,12 @@ function isContainedPath(candidate, allowed) {
245
249
  // per-run reclamation chain) so a concurrent writer can never lose a record.
246
250
  // O_EXCL (`wx`) is portable (no native flock); a stale holder is stolen so a
247
251
  // crashed process can never wedge the store forever.
252
+ //
253
+ // v0.1.95: the lock mtime is refreshed immediately before fn() runs, and
254
+ // verified AFTER fn() returns — if another process stole the lock mid-operation
255
+ // (the stale check fired and it overwrote our lock), the holder refuses to
256
+ // release and throws. This guards long-running RMW operations (e.g. GC over a
257
+ // large run) against mid-operation theft.
248
258
  // ---------------------------------------------------------------------------
249
259
  const FILE_LOCK_STALE_MS = 30_000;
250
260
  function sleepSync(ms) {
@@ -254,11 +264,12 @@ function sleepSync(ms) {
254
264
  function withFileLock(targetPath, fn) {
255
265
  const lock = `${targetPath}.lock`;
256
266
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(lock), { recursive: true });
267
+ const pid = String(process.pid);
257
268
  let acquired = false;
258
269
  for (let attempt = 0; attempt < 240 && !acquired; attempt++) {
259
270
  try {
260
271
  const fd = node_fs_1.default.openSync(lock, "wx");
261
- node_fs_1.default.writeFileSync(fd, `${process.pid}@${new Date().toISOString()}\n`, "utf8");
272
+ node_fs_1.default.writeFileSync(fd, `${pid}@${new Date().toISOString()}\n`, "utf8");
262
273
  node_fs_1.default.closeSync(fd);
263
274
  acquired = true;
264
275
  }
@@ -272,19 +283,45 @@ function withFileLock(targetPath, fn) {
272
283
  }
273
284
  }
274
285
  catch {
275
- continue; // lock vanished between open and stat — retry immediately
286
+ continue;
276
287
  }
277
288
  sleepSync(25);
278
289
  }
279
290
  }
280
291
  if (!acquired)
281
292
  throw new Error(`could not acquire file lock for ${targetPath}`);
293
+ // Refresh mtime right before the critical section
282
294
  try {
283
- return fn();
295
+ node_fs_1.default.utimesSync(lock, new Date(), new Date());
296
+ }
297
+ catch { /* best-effort */ }
298
+ try {
299
+ const result = fn();
300
+ // Verify lock was not stolen during fn(). The lock content may have changed
301
+ // if another process opened it with wx after stealing it. If our PID is not
302
+ // in the lock file, the lock was stolen — do NOT release the stolen lock
303
+ // (the thief owns it now, and releasing would corrupt its operation).
304
+ try {
305
+ const current = node_fs_1.default.readFileSync(lock, "utf8");
306
+ if (!current.startsWith(pid + "@")) {
307
+ throw new Error(`File lock for ${targetPath} was stolen during the critical section ` +
308
+ `(lock now owned by another process). The operation may have lost ` +
309
+ `cross-process isolation — increase FILE_LOCK_STALE_MS or split the work.`);
310
+ }
311
+ }
312
+ catch (checkError) {
313
+ if (checkError instanceof Error && checkError.message.includes("stolen"))
314
+ throw checkError;
315
+ /* lock vanished — another process already released it, nothing to clean up */
316
+ }
317
+ return result;
284
318
  }
285
319
  finally {
286
320
  try {
287
- node_fs_1.default.rmSync(lock, { force: true });
321
+ // Only release if we still own the lock
322
+ const current = node_fs_1.default.readFileSync(lock, "utf8");
323
+ if (current.startsWith(pid + "@"))
324
+ node_fs_1.default.rmSync(lock, { force: true });
288
325
  }
289
326
  catch {
290
327
  /* releasing a missing lock is fine */
@@ -299,7 +336,8 @@ function safeFileName(value) {
299
336
  * runs directory via a separator or a `..`/`.` component (path traversal). A
300
337
  * real run id is `${workflowId}-${stamp}-${suffix}` (createRunId), and a
301
338
  * workflow id is alnum-bounded [a-z0-9.-] (validateWorkflowId) — all within
302
- * [A-Za-z0-9._-]. Because the charset already forbids any separator, the whole
339
+ * [A-Za-z0-9._:-]. Sub-workflow ids also include `:` from the task id prefix.
340
+ * Because the charset already forbids any separator, the whole
303
341
  * id is ONE path component, so the only values that could traverse are the
304
342
  * components `.` and `..` themselves; an embedded `..` (e.g. a workflow id like
305
343
  * `v1..2`) is a safe directory name and is allowed. A separator, an absolute
@@ -309,8 +347,8 @@ function assertSafeRunId(value, context = "run id") {
309
347
  if (typeof value !== "string" || value.length === 0) {
310
348
  throw new Error(`Invalid ${context}: expected a non-empty string`);
311
349
  }
312
- if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..") {
313
- throw new Error(`Unsafe ${context}: ${JSON.stringify(value)} must be a single path segment ([A-Za-z0-9._-], not '.' or '..')`);
350
+ if (!/^[A-Za-z0-9._:-]+$/.test(value) || value === "." || value === "..") {
351
+ throw new Error(`Unsafe ${context}: ${JSON.stringify(value)} must be a single path segment ([A-Za-z0-9._:-], not '.' or '..')`);
314
352
  }
315
353
  return value;
316
354
  }
package/dist/triggers.js CHANGED
@@ -147,7 +147,13 @@ function parseJsonObject(value) {
147
147
  return undefined;
148
148
  if (typeof value === "object")
149
149
  return value;
150
- const parsed = JSON.parse(String(value));
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(String(value));
153
+ }
154
+ catch {
155
+ throw new Error("Expected a JSON object, got invalid JSON");
156
+ }
151
157
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
152
158
  throw new Error("Expected JSON object");
153
159
  }