cool-workflow 0.1.82 → 0.1.83

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 (79) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/README.md +124 -120
  4. package/apps/architecture-review/app.json +1 -1
  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/capability-core.js +16 -8
  11. package/dist/capability-registry.js +8 -0
  12. package/dist/cli.js +12 -1
  13. package/dist/commit.js +5 -1
  14. package/dist/doctor.js +153 -0
  15. package/dist/mcp-server.js +11 -0
  16. package/dist/orchestrator.js +13 -0
  17. package/dist/reclamation/hash.js +72 -0
  18. package/dist/reclamation.js +25 -78
  19. package/dist/run-registry/queue.js +6 -7
  20. package/dist/run-registry.js +35 -24
  21. package/dist/scheduler.js +78 -53
  22. package/dist/version.js +1 -1
  23. package/dist/worker-isolation.js +22 -2
  24. package/docs/agent-delegation-drive.7.md +90 -86
  25. package/docs/agent-framework.md +33 -32
  26. package/docs/candidate-scoring.7.md +26 -24
  27. package/docs/canonical-workflow-apps.7.md +40 -40
  28. package/docs/capability-topology-registry.7.md +24 -24
  29. package/docs/cli-mcp-parity.7.md +226 -154
  30. package/docs/contract-migration-tooling.7.md +48 -41
  31. package/docs/control-plane-scheduling.7.md +45 -41
  32. package/docs/coordinator-blackboard.7.md +30 -30
  33. package/docs/dogfood-one-real-repo.7.md +44 -44
  34. package/docs/durable-state-and-locking.7.md +34 -30
  35. package/docs/end-to-end-golden-path.7.md +29 -29
  36. package/docs/error-feedback.7.md +27 -27
  37. package/docs/evidence-adoption-reasoning-chain.7.md +62 -58
  38. package/docs/execution-backends.7.md +84 -80
  39. package/docs/getting-started.md +35 -18
  40. package/docs/index.md +3 -3
  41. package/docs/mcp-app-surface.7.md +64 -64
  42. package/docs/multi-agent-cli-mcp-surface.7.md +82 -77
  43. package/docs/multi-agent-eval-replay-harness.7.md +59 -55
  44. package/docs/multi-agent-operator-ux.7.md +69 -65
  45. package/docs/multi-agent-runtime-core.7.md +39 -39
  46. package/docs/multi-agent-topologies.7.md +24 -24
  47. package/docs/multi-agent-trust-policy-audit.7.md +38 -38
  48. package/docs/node-snapshot-diff-replay.7.md +26 -22
  49. package/docs/observability-cost-accounting.7.md +49 -45
  50. package/docs/operator-ux.7.md +30 -30
  51. package/docs/pipeline-runner.7.md +31 -31
  52. package/docs/project-index.md +10 -5
  53. package/docs/real-execution-backends.7.md +47 -43
  54. package/docs/release-and-migration.7.md +42 -38
  55. package/docs/release-tooling.7.md +53 -49
  56. package/docs/routines.md +16 -16
  57. package/docs/run-registry-control-plane.7.md +120 -116
  58. package/docs/run-retention-reclamation.7.md +45 -41
  59. package/docs/sandbox-profiles.7.md +32 -32
  60. package/docs/scheduled-tasks.md +14 -14
  61. package/docs/security-trust-hardening.7.md +29 -29
  62. package/docs/source-context-profiles.7.md +28 -28
  63. package/docs/state-explosion-management.7.md +63 -59
  64. package/docs/state-node.7.md +8 -8
  65. package/docs/team-collaboration.7.md +62 -58
  66. package/docs/trust-model.md +126 -126
  67. package/docs/unix-principles.md +80 -80
  68. package/docs/vendor-manifest-loadability.7.md +20 -20
  69. package/docs/verifier-gated-commit.7.md +16 -16
  70. package/docs/web-desktop-workbench.7.md +69 -65
  71. package/docs/worker-isolation.7.md +34 -37
  72. package/docs/workflow-app-framework.7.md +38 -38
  73. package/manifest/plugin.manifest.json +4 -4
  74. package/package.json +3 -2
  75. package/scripts/canonical-apps.js +4 -4
  76. package/scripts/dogfood-release.js +1 -1
  77. package/scripts/gen-parity-doc.js +106 -0
  78. package/scripts/golden-path.js +4 -4
  79. package/dist/verifier-registry.js +0 -46
@@ -33,15 +33,14 @@ function queueFilePath(host) {
33
33
  }
34
34
  function loadQueue(host) {
35
35
  const file = queueFilePath(host);
36
+ // Absent => empty queue. A present-but-corrupt queue must FAIL CLOSED rather
37
+ // than read as empty: silently draining to [] would lose every queued run and
38
+ // let scheduling/lease ops proceed as if the store were clean. readJson throws
39
+ // `Invalid JSON in <file>` on a present, unparseable store; let it propagate.
36
40
  if (!node_fs_1.default.existsSync(file))
37
41
  return [];
38
- try {
39
- const parsed = (0, state_1.readJson)(file);
40
- return Array.isArray(parsed.entries) ? parsed.entries : [];
41
- }
42
- catch {
43
- return [];
44
- }
42
+ const parsed = (0, state_1.readJson)(file);
43
+ return Array.isArray(parsed.entries) ? parsed.entries : [];
45
44
  }
46
45
  function saveQueue(host, entries) {
47
46
  (0, state_1.writeJson)(queueFilePath(host), { schemaVersion: 1, entries }, { durable: true });
@@ -162,29 +162,36 @@ class RunRegistry {
162
162
  return node_path_1.default.join(this.homeRoot, "registry");
163
163
  }
164
164
  // ---- per-repo overlays (plain files) ------------------------------------
165
+ // Overlay reads distinguish ABSENT (clean default) from PRESENT-but-corrupt
166
+ // (fail closed). readJson throws `Invalid JSON in <file>` on a present file
167
+ // that won't parse; we let that propagate instead of swallowing it. Swallowing
168
+ // is the absent-vs-corrupt conflation telemetry-ledger.ts flags as the bug that
169
+ // "let a corrupt overlay verify green" — here it would silently un-archive every
170
+ // archived run / drop every provenance link. This is authoritative durable state.
171
+ // A present overlay must parse to a JSON OBJECT. readJson already fails closed
172
+ // on unparseable bytes; this catches the next shape over: valid JSON that is
173
+ // `null`, an array, or a scalar. Without it `parsed.archived` throws a cryptic
174
+ // TypeError (null) or silently reads `undefined` (array) and the whole registry
175
+ // scan breaks. Fail closed with a clear message instead.
176
+ requireOverlayObject(parsed, file) {
177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
+ throw new Error(`Corrupt overlay ${file}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : parsed === null ? "null" : typeof parsed}`);
179
+ }
180
+ return parsed;
181
+ }
165
182
  loadArchiveOverlay(repo) {
166
183
  const file = node_path_1.default.join(this.repoRegistryDir(repo), "archive.json");
167
184
  if (!node_fs_1.default.existsSync(file))
168
185
  return { schemaVersion: 1, archived: {} };
169
- try {
170
- const parsed = (0, state_1.readJson)(file);
171
- return { schemaVersion: 1, archived: parsed.archived || {} };
172
- }
173
- catch {
174
- return { schemaVersion: 1, archived: {} };
175
- }
186
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
187
+ return { schemaVersion: 1, archived: parsed.archived || {} };
176
188
  }
177
189
  loadProvenanceOverlay(repo) {
178
190
  const file = node_path_1.default.join(this.repoRegistryDir(repo), "provenance.json");
179
191
  if (!node_fs_1.default.existsSync(file))
180
192
  return { schemaVersion: 1, links: {} };
181
- try {
182
- const parsed = (0, state_1.readJson)(file);
183
- return { schemaVersion: 1, links: parsed.links || {} };
184
- }
185
- catch {
186
- return { schemaVersion: 1, links: {} };
187
- }
193
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
194
+ return { schemaVersion: 1, links: parsed.links || {} };
188
195
  }
189
196
  loadRepoOverlays(repo) {
190
197
  return {
@@ -203,15 +210,14 @@ class RunRegistry {
203
210
  }
204
211
  loadRepos() {
205
212
  const file = this.reposFilePath();
213
+ // Absent => no registered repos. Present-but-corrupt must fail closed: a
214
+ // swallowed parse error here silently drops every cross-repo root the
215
+ // operator registered, shrinking the home index to the current repo with no
216
+ // signal. Let readJson's `Invalid JSON` throw surface the corruption.
206
217
  if (!node_fs_1.default.existsSync(file))
207
218
  return { schemaVersion: 1, repos: [] };
208
- try {
209
- const parsed = (0, state_1.readJson)(file);
210
- return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
211
- }
212
- catch {
213
- return { schemaVersion: 1, repos: [] };
214
- }
219
+ const parsed = this.requireOverlayObject((0, state_1.readJson)(file), file);
220
+ return { schemaVersion: 1, repos: Array.isArray(parsed.repos) ? parsed.repos : [] };
215
221
  }
216
222
  /** Persisted union of registered repo roots and the current repo, deduped and
217
223
  * sorted. Read-only: does NOT write repos.json (reads stay pure). */
@@ -745,9 +751,14 @@ class RunRegistry {
745
751
  // Record provenance in the per-repo overlay (derived metadata), NOT in the
746
752
  // original run's source state — the past is never mutated.
747
753
  const provFile = node_path_1.default.join(this.repoRegistryDir(original.repo), "provenance.json");
748
- const provOverlay = this.loadProvenanceOverlay(original.repo);
749
- provOverlay.links[newRun.id] = provenance;
750
- (0, state_1.writeJson)(provFile, provOverlay, { durable: true });
754
+ // Lock the read-modify-write: a concurrent rerun/archive on the same repo
755
+ // overlay would otherwise last-writer-wins and drop a provenance link. The
756
+ // sibling writers (registerRepo, archive) already serialize via withFileLock.
757
+ (0, state_1.withFileLock)(provFile, () => {
758
+ const provOverlay = this.loadProvenanceOverlay(original.repo);
759
+ provOverlay.links[newRun.id] = provenance;
760
+ (0, state_1.writeJson)(provFile, provOverlay, { durable: true });
761
+ });
751
762
  return {
752
763
  schemaVersion: 1,
753
764
  originalRunId: runId,
package/dist/scheduler.js CHANGED
@@ -16,6 +16,16 @@ class Scheduler {
16
16
  this.cwd = node_path_1.default.resolve(cwd);
17
17
  this.storePath = node_path_1.default.join(this.cwd, ".cw", "schedules", "tasks.json");
18
18
  }
19
+ // Every mutation is a cross-process read-modify-write of the one store file
20
+ // (the daemon polls `due` while CLI calls create/complete/delete concurrently).
21
+ // Without serialization two writers both load, both atomically rename their
22
+ // copy back, and the second silently clobbers the first's new task / status /
23
+ // history record. `locked` holds the same advisory lock the queue and
24
+ // reclamation stores already use. Reads (list/history) need no lock: the atomic
25
+ // rename means a reader always sees a whole old-or-new store.
26
+ locked(fn) {
27
+ return (0, state_1.withFileLock)(this.storePath, fn);
28
+ }
19
29
  create(options) {
20
30
  const kind = normalizeKind(options.kind);
21
31
  const now = new Date();
@@ -43,23 +53,30 @@ class Scheduler {
43
53
  maxRuns: numberOption(options.maxRuns),
44
54
  runCount: 0
45
55
  };
46
- const store = this.load();
47
- store.tasks.push(task);
48
- this.save(store);
49
- return task;
56
+ return this.locked(() => {
57
+ const store = this.load();
58
+ store.tasks.push(task);
59
+ this.save(store);
60
+ return task;
61
+ });
50
62
  }
51
63
  list(status) {
52
64
  const store = this.load();
53
65
  return status ? store.tasks.filter((task) => task.status === status) : store.tasks;
54
66
  }
55
67
  delete(id) {
56
- const store = this.load();
57
- const before = store.tasks.length;
58
- store.tasks = store.tasks.filter((task) => task.id !== id);
59
- this.save(store);
60
- return { deleted: store.tasks.length !== before, id };
68
+ return this.locked(() => {
69
+ const store = this.load();
70
+ const before = store.tasks.length;
71
+ store.tasks = store.tasks.filter((task) => task.id !== id);
72
+ this.save(store);
73
+ return { deleted: store.tasks.length !== before, id };
74
+ });
61
75
  }
62
76
  due(now = new Date()) {
77
+ return this.locked(() => this.dueLocked(now));
78
+ }
79
+ dueLocked(now) {
63
80
  const store = this.load();
64
81
  let changed = false;
65
82
  for (const task of store.tasks) {
@@ -87,63 +104,71 @@ class Scheduler {
87
104
  return dueTasks;
88
105
  }
89
106
  complete(id, options = {}) {
90
- const store = this.load();
91
- const task = store.tasks.find((candidate) => candidate.id === id);
92
- if (!task)
93
- throw new Error(`Scheduled task not found: ${id}`);
94
- const now = new Date();
95
- task.runCount += 1;
96
- task.lastRunAt = now.toISOString();
97
- task.updatedAt = now.toISOString();
98
- const maxRuns = numberOption(options.maxRuns) ?? task.maxRuns;
99
- if (maxRuns !== undefined)
100
- task.maxRuns = maxRuns;
101
- if (task.kind === "reminder" || (task.maxRuns !== undefined && task.runCount >= task.maxRuns)) {
102
- task.status = "completed";
103
- }
104
- else {
105
- task.nextRunAt = computeNextRunAt(task, now).toISOString();
106
- }
107
- this.save(store);
108
- return task;
107
+ return this.locked(() => {
108
+ const store = this.load();
109
+ const task = store.tasks.find((candidate) => candidate.id === id);
110
+ if (!task)
111
+ throw new Error(`Scheduled task not found: ${id}`);
112
+ const now = new Date();
113
+ task.runCount += 1;
114
+ task.lastRunAt = now.toISOString();
115
+ task.updatedAt = now.toISOString();
116
+ const maxRuns = numberOption(options.maxRuns) ?? task.maxRuns;
117
+ if (maxRuns !== undefined)
118
+ task.maxRuns = maxRuns;
119
+ if (task.kind === "reminder" || (task.maxRuns !== undefined && task.runCount >= task.maxRuns)) {
120
+ task.status = "completed";
121
+ }
122
+ else {
123
+ task.nextRunAt = computeNextRunAt(task, now).toISOString();
124
+ }
125
+ this.save(store);
126
+ return task;
127
+ });
109
128
  }
110
129
  pause(id) {
111
130
  return this.setStatus(id, "paused");
112
131
  }
113
132
  resume(id) {
114
- const store = this.load();
115
- const task = findTask(store, id);
116
- const now = new Date();
117
- task.status = "active";
118
- task.updatedAt = now.toISOString();
119
- if (new Date(task.nextRunAt).getTime() <= now.getTime()) {
120
- task.nextRunAt = computeNextRunAt(task, now).toISOString();
121
- }
122
- this.save(store);
123
- return task;
133
+ return this.locked(() => {
134
+ const store = this.load();
135
+ const task = findTask(store, id);
136
+ const now = new Date();
137
+ task.status = "active";
138
+ task.updatedAt = now.toISOString();
139
+ if (new Date(task.nextRunAt).getTime() <= now.getTime()) {
140
+ task.nextRunAt = computeNextRunAt(task, now).toISOString();
141
+ }
142
+ this.save(store);
143
+ return task;
144
+ });
124
145
  }
125
146
  runNow(id) {
126
- const store = this.load();
127
- const task = findTask(store, id);
128
- const now = new Date();
129
- task.lastDueAt = now.toISOString();
130
- task.updatedAt = now.toISOString();
131
- const record = createHistoryRecord(task, "started", this.cwd, now);
132
- store.history.push(record);
133
- this.save(store);
134
- return record;
147
+ return this.locked(() => {
148
+ const store = this.load();
149
+ const task = findTask(store, id);
150
+ const now = new Date();
151
+ task.lastDueAt = now.toISOString();
152
+ task.updatedAt = now.toISOString();
153
+ const record = createHistoryRecord(task, "started", this.cwd, now);
154
+ store.history.push(record);
155
+ this.save(store);
156
+ return record;
157
+ });
135
158
  }
136
159
  history(id) {
137
160
  const store = this.load();
138
161
  return id ? store.history.filter((record) => record.scheduleId === id) : store.history;
139
162
  }
140
163
  setStatus(id, status) {
141
- const store = this.load();
142
- const task = findTask(store, id);
143
- task.status = status;
144
- task.updatedAt = new Date().toISOString();
145
- this.save(store);
146
- return task;
164
+ return this.locked(() => {
165
+ const store = this.load();
166
+ const task = findTask(store, id);
167
+ task.status = status;
168
+ task.updatedAt = new Date().toISOString();
169
+ this.save(store);
170
+ return task;
171
+ });
147
172
  }
148
173
  load() {
149
174
  if (!node_fs_1.default.existsSync(this.storePath))
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
4
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.82";
4
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.83";
5
5
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
6
6
  exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
7
7
  exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
@@ -270,7 +270,15 @@ function getWorkerScope(run, workerId) {
270
270
  const file = node_path_1.default.join(workerRoot(run), (0, state_1.safeFileName)(workerId), paths_1.WORKER_SCOPE_FILE);
271
271
  if (!node_fs_1.default.existsSync(file))
272
272
  return undefined;
273
- const scope = (0, validation_1.validateWorkerScope)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
273
+ let scope;
274
+ try {
275
+ scope = (0, validation_1.validateWorkerScope)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
276
+ }
277
+ catch (error) {
278
+ // A present-but-corrupt scope fails closed with context, not a raw
279
+ // SyntaxError/validation throw bubbling up from deep in the call stack.
280
+ throw new Error(`Corrupt worker scope ${file}: ${error instanceof Error ? error.message : String(error)}`);
281
+ }
274
282
  upsertWorkerScope(run, scope);
275
283
  return scope;
276
284
  }
@@ -871,7 +879,19 @@ function loadWorkerScopesFromDisk(run) {
871
879
  .filter((entry) => entry.isDirectory())
872
880
  .map((entry) => node_path_1.default.join(workerRoot(run), entry.name, paths_1.WORKER_SCOPE_FILE))
873
881
  .filter((file) => node_fs_1.default.existsSync(file))
874
- .map((file) => (0, validation_1.validateWorkerScope)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
882
+ .map((file) => {
883
+ // One corrupt/partially-written worker.json must not blank the whole
884
+ // listing (summarizeWorkers/listWorkerScopes) — skip it with a diagnostic
885
+ // and surface every worker that IS readable.
886
+ try {
887
+ return (0, validation_1.validateWorkerScope)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
888
+ }
889
+ catch (error) {
890
+ process.stderr.write(`cw: skipping unreadable worker scope ${file}: ${error instanceof Error ? error.message : String(error)}\n`);
891
+ return undefined;
892
+ }
893
+ })
894
+ .filter((scope) => scope !== undefined);
875
895
  }
876
896
  function requireWorkerScope(run, workerId) {
877
897
  const scope = getWorkerScope(run, workerId);