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.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +124 -120
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/capability-core.js +16 -8
- package/dist/capability-registry.js +8 -0
- package/dist/cli.js +12 -1
- package/dist/commit.js +5 -1
- package/dist/doctor.js +153 -0
- package/dist/mcp-server.js +11 -0
- package/dist/orchestrator.js +13 -0
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +25 -78
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +78 -53
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +22 -2
- package/docs/agent-delegation-drive.7.md +90 -86
- package/docs/agent-framework.md +33 -32
- package/docs/candidate-scoring.7.md +26 -24
- package/docs/canonical-workflow-apps.7.md +40 -40
- package/docs/capability-topology-registry.7.md +24 -24
- package/docs/cli-mcp-parity.7.md +226 -154
- package/docs/contract-migration-tooling.7.md +48 -41
- package/docs/control-plane-scheduling.7.md +45 -41
- package/docs/coordinator-blackboard.7.md +30 -30
- package/docs/dogfood-one-real-repo.7.md +44 -44
- package/docs/durable-state-and-locking.7.md +34 -30
- package/docs/end-to-end-golden-path.7.md +29 -29
- package/docs/error-feedback.7.md +27 -27
- package/docs/evidence-adoption-reasoning-chain.7.md +62 -58
- package/docs/execution-backends.7.md +84 -80
- package/docs/getting-started.md +35 -18
- package/docs/index.md +3 -3
- package/docs/mcp-app-surface.7.md +64 -64
- package/docs/multi-agent-cli-mcp-surface.7.md +82 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -55
- package/docs/multi-agent-operator-ux.7.md +69 -65
- package/docs/multi-agent-runtime-core.7.md +39 -39
- package/docs/multi-agent-topologies.7.md +24 -24
- package/docs/multi-agent-trust-policy-audit.7.md +38 -38
- package/docs/node-snapshot-diff-replay.7.md +26 -22
- package/docs/observability-cost-accounting.7.md +49 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +10 -5
- package/docs/real-execution-backends.7.md +47 -43
- package/docs/release-and-migration.7.md +42 -38
- package/docs/release-tooling.7.md +53 -49
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -116
- package/docs/run-retention-reclamation.7.md +45 -41
- package/docs/sandbox-profiles.7.md +32 -32
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +29 -29
- package/docs/source-context-profiles.7.md +28 -28
- package/docs/state-explosion-management.7.md +63 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +62 -58
- package/docs/trust-model.md +126 -126
- package/docs/unix-principles.md +80 -80
- package/docs/vendor-manifest-loadability.7.md +20 -20
- package/docs/verifier-gated-commit.7.md +16 -16
- package/docs/web-desktop-workbench.7.md +69 -65
- package/docs/worker-isolation.7.md +34 -37
- package/docs/workflow-app-framework.7.md +38 -38
- package/manifest/plugin.manifest.json +4 -4
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- 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
|
-
|
|
39
|
-
|
|
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 });
|
package/dist/run-registry.js
CHANGED
|
@@ -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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
(
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
task.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
task.nextRunAt
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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;
|
package/dist/worker-isolation.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
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);
|