cool-workflow 0.1.82 → 0.1.84
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 +128 -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 +270 -0
- package/dist/cli/command-surface.js +1320 -0
- package/dist/cli.js +2 -1307
- package/dist/commit.js +5 -1
- package/dist/doctor.js +153 -0
- package/dist/mcp-server.js +15 -1451
- package/dist/mcp-surface.js +1441 -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-accept/acceptance.js +114 -0
- package/dist/worker-accept/blackboard-fanout.js +80 -0
- package/dist/worker-accept/blackboard-linkage.js +19 -0
- package/dist/worker-accept/context.js +2 -0
- package/dist/worker-accept/telemetry-ledger.js +116 -0
- package/dist/worker-accept/validation.js +77 -0
- package/dist/worker-accept/verifier-completion.js +73 -0
- package/dist/worker-isolation.js +41 -446
- package/docs/agent-delegation-drive.7.md +94 -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 +230 -154
- package/docs/contract-migration-tooling.7.md +52 -41
- package/docs/control-plane-scheduling.7.md +49 -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 +38 -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 +66 -58
- package/docs/execution-backends.7.md +88 -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 +86 -77
- package/docs/multi-agent-eval-replay-harness.7.md +63 -55
- package/docs/multi-agent-operator-ux.7.md +73 -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 +30 -22
- package/docs/observability-cost-accounting.7.md +53 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +16 -5
- package/docs/real-execution-backends.7.md +51 -43
- package/docs/release-and-migration.7.md +46 -38
- package/docs/release-tooling.7.md +67 -50
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +124 -116
- package/docs/run-retention-reclamation.7.md +49 -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 +67 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +66 -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 +73 -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/bump-version.js +9 -1
- 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/scripts/parity-check.js +27 -57
- package/scripts/release-flow.js +7 -6
- package/scripts/sync-project-index.js +1 -1
- package/dist/verifier-registry.js +0 -46
package/dist/orchestrator.js
CHANGED
|
@@ -821,6 +821,7 @@ function formatHelp() {
|
|
|
821
821
|
"",
|
|
822
822
|
"Commands:",
|
|
823
823
|
" list",
|
|
824
|
+
" doctor [--json] (check your setup: Node, agent backend, git, writable state)",
|
|
824
825
|
" init <workflow-id> [--title TEXT] [--output PATH]",
|
|
825
826
|
" quickstart [app-id] [--repo PATH] [--question TEXT] [--agent-command CMD] [--once] [--preview]",
|
|
826
827
|
" plan <workflow-id> [--repo PATH] [--question TEXT] [--invariant TEXT]",
|
|
@@ -873,6 +874,18 @@ function formatHelp() {
|
|
|
873
874
|
" run search|list|show|resume|archive|rerun|export|import|verify-import [run-id|archive] [--scope repo|home] [--json]",
|
|
874
875
|
" queue add|list|drain|show [queue-id] [--repo PATH] [--priority N]",
|
|
875
876
|
" history [--scope repo|home] [--app ID] [--status STATE] [--json]",
|
|
877
|
+
" audit-run <app-id> [--repo PATH] [--question TEXT] [--agent-command CMD]",
|
|
878
|
+
" metrics show|summary <run-id> [--scope repo|home] [--json]",
|
|
879
|
+
" telemetry verify <run-id> [--pubkey PEM|PATH] [--json]",
|
|
880
|
+
" gc plan|run|verify [run-id] [--json]",
|
|
881
|
+
" sched plan|lease|release|complete|reclaim|reset|policy [--json]",
|
|
882
|
+
" migration list|check|prove [target] [--json]",
|
|
883
|
+
" operator status|report <run-id> [--json]",
|
|
884
|
+
" review status|policy <run-id> [--json]",
|
|
885
|
+
" approve|reject|comment <kind> <run-id> <target-id> [--reason TEXT]",
|
|
886
|
+
" handoff <kind> <run-id> <target-id> [--to ROLE]",
|
|
887
|
+
" loop --prompt TEXT [--interval-minutes N]",
|
|
888
|
+
" demo tamper",
|
|
876
889
|
" workbench view <run-id> [--json]",
|
|
877
890
|
" workbench serve [--port N] [--scope repo|home] [--once|--json]",
|
|
878
891
|
""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sha256OfString = sha256OfString;
|
|
7
|
+
exports.sha256OfFile = sha256OfFile;
|
|
8
|
+
exports.dirBytes = dirBytes;
|
|
9
|
+
exports.contentDigest = contentDigest;
|
|
10
|
+
// Content addressing + byte measurement for run reclamation (NO `du` — in-process
|
|
11
|
+
// only). Carved out of reclamation.ts (FreeBSD-audit god-module carve) so the pure
|
|
12
|
+
// content-addressing leaf no longer sits inside the write-ahead reclamation
|
|
13
|
+
// transaction. These are pure functions of their path/string inputs — no run
|
|
14
|
+
// state, no module-level mutable state.
|
|
15
|
+
//
|
|
16
|
+
// BEHAVIOR-PRESERVING — pure code movement, zero logic change. reclamation.ts
|
|
17
|
+
// re-exports the public symbols (sha256OfString/sha256OfFile/dirBytes) so the
|
|
18
|
+
// module's surface stays byte-identical.
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const compare_1 = require("../compare");
|
|
23
|
+
function sha256Hex(value) {
|
|
24
|
+
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function sha256OfString(value) {
|
|
27
|
+
return `sha256:${sha256Hex(value)}`;
|
|
28
|
+
}
|
|
29
|
+
function sha256OfFile(file) {
|
|
30
|
+
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
31
|
+
}
|
|
32
|
+
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
33
|
+
* file returns its own size; a dir returns the recursive sum. */
|
|
34
|
+
function dirBytes(p) {
|
|
35
|
+
let total = 0;
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = node_fs_1.default.statSync(p);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (stat.isFile())
|
|
44
|
+
return stat.size;
|
|
45
|
+
if (!stat.isDirectory())
|
|
46
|
+
return 0;
|
|
47
|
+
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
48
|
+
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
53
|
+
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
54
|
+
* single sha per freed dir. */
|
|
55
|
+
function contentDigest(p) {
|
|
56
|
+
const stat = node_fs_1.default.statSync(p);
|
|
57
|
+
if (stat.isFile())
|
|
58
|
+
return sha256OfFile(p);
|
|
59
|
+
const parts = [];
|
|
60
|
+
const walk = (dir, rel) => {
|
|
61
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
62
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
63
|
+
const r = node_path_1.default.join(rel, entry.name);
|
|
64
|
+
if (entry.isDirectory())
|
|
65
|
+
walk(abs, r);
|
|
66
|
+
else
|
|
67
|
+
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(p, "");
|
|
71
|
+
return sha256OfString(parts.join("\n"));
|
|
72
|
+
}
|
package/dist/reclamation.js
CHANGED
|
@@ -29,10 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
29
29
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
30
|
};
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
33
|
-
exports.sha256OfString = sha256OfString;
|
|
34
|
-
exports.sha256OfFile = sha256OfFile;
|
|
35
|
-
exports.dirBytes = dirBytes;
|
|
32
|
+
exports.dirBytes = exports.sha256OfFile = exports.sha256OfString = exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
36
33
|
exports.reclaimedLogPath = reclaimedLogPath;
|
|
37
34
|
exports.loadReclamationLog = loadReclamationLog;
|
|
38
35
|
exports.extractSkeleton = extractSkeleton;
|
|
@@ -49,9 +46,12 @@ exports.runReclamation = runReclamation;
|
|
|
49
46
|
exports.reconstructArtifact = reconstructArtifact;
|
|
50
47
|
exports.verifyReclamation = verifyReclamation;
|
|
51
48
|
exports.dominantFailureCode = dominantFailureCode;
|
|
52
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
53
49
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
50
|
const node_path_1 = __importDefault(require("node:path"));
|
|
51
|
+
const hash_1 = require("./reclamation/hash");
|
|
52
|
+
Object.defineProperty(exports, "dirBytes", { enumerable: true, get: function () { return hash_1.dirBytes; } });
|
|
53
|
+
Object.defineProperty(exports, "sha256OfFile", { enumerable: true, get: function () { return hash_1.sha256OfFile; } });
|
|
54
|
+
Object.defineProperty(exports, "sha256OfString", { enumerable: true, get: function () { return hash_1.sha256OfString; } });
|
|
55
55
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
56
56
|
const node_projection_1 = require("./node-projection");
|
|
57
57
|
const node_snapshot_1 = require("./node-snapshot");
|
|
@@ -95,59 +95,6 @@ class ReclamationError extends Error {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
exports.ReclamationError = ReclamationError;
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Content addressing + byte measurement (NO `du` — in-process only).
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
function sha256Hex(value) {
|
|
102
|
-
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
103
|
-
}
|
|
104
|
-
function sha256OfString(value) {
|
|
105
|
-
return `sha256:${sha256Hex(value)}`;
|
|
106
|
-
}
|
|
107
|
-
function sha256OfFile(file) {
|
|
108
|
-
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
109
|
-
}
|
|
110
|
-
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
111
|
-
* file returns its own size; a dir returns the recursive sum. */
|
|
112
|
-
function dirBytes(p) {
|
|
113
|
-
let total = 0;
|
|
114
|
-
let stat;
|
|
115
|
-
try {
|
|
116
|
-
stat = node_fs_1.default.statSync(p);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
if (stat.isFile())
|
|
122
|
-
return stat.size;
|
|
123
|
-
if (!stat.isDirectory())
|
|
124
|
-
return 0;
|
|
125
|
-
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
126
|
-
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
127
|
-
}
|
|
128
|
-
return total;
|
|
129
|
-
}
|
|
130
|
-
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
131
|
-
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
132
|
-
* single sha per freed dir. */
|
|
133
|
-
function contentDigest(p) {
|
|
134
|
-
const stat = node_fs_1.default.statSync(p);
|
|
135
|
-
if (stat.isFile())
|
|
136
|
-
return sha256OfFile(p);
|
|
137
|
-
const parts = [];
|
|
138
|
-
const walk = (dir, rel) => {
|
|
139
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
140
|
-
const abs = node_path_1.default.join(dir, entry.name);
|
|
141
|
-
const r = node_path_1.default.join(rel, entry.name);
|
|
142
|
-
if (entry.isDirectory())
|
|
143
|
-
walk(abs, r);
|
|
144
|
-
else
|
|
145
|
-
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
walk(p, "");
|
|
149
|
-
return sha256OfString(parts.join("\n"));
|
|
150
|
-
}
|
|
151
98
|
/** Persist a run's authoritative state.json DURABLY (atomic temp → fsync →
|
|
152
99
|
* rename). The re-point that scratch reclamation depends on MUST be persisted
|
|
153
100
|
* this way BEFORE any byte is freed — see prepareFree(). */
|
|
@@ -220,13 +167,13 @@ function digestEvidenceEntry(entry) {
|
|
|
220
167
|
try {
|
|
221
168
|
const stat = node_fs_1.default.statSync(candidatePath);
|
|
222
169
|
if (stat.isFile())
|
|
223
|
-
return { ref, digest: sha256OfFile(candidatePath) };
|
|
170
|
+
return { ref, digest: (0, hash_1.sha256OfFile)(candidatePath) };
|
|
224
171
|
}
|
|
225
172
|
catch {
|
|
226
173
|
/* fall through to locator digest */
|
|
227
174
|
}
|
|
228
175
|
}
|
|
229
|
-
return { ref, digest: sha256OfString(ref) };
|
|
176
|
+
return { ref, digest: (0, hash_1.sha256OfString)(ref) };
|
|
230
177
|
}
|
|
231
178
|
/** STEP 1: extract + seal the skeleton. Pure read over the run; never mutates. */
|
|
232
179
|
function extractSkeleton(run) {
|
|
@@ -264,7 +211,7 @@ function extractSkeleton(run) {
|
|
|
264
211
|
.map(([ref, digest]) => ({ ref, digest }))
|
|
265
212
|
.sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
|
|
266
213
|
const eventLog = auditEventLogPath(run);
|
|
267
|
-
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
|
|
214
|
+
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? (0, hash_1.sha256OfFile)(eventLog) : (0, hash_1.sha256OfString)("");
|
|
268
215
|
const events = node_fs_1.default.existsSync(eventLog)
|
|
269
216
|
? node_fs_1.default
|
|
270
217
|
.readFileSync(eventLog, "utf8")
|
|
@@ -284,18 +231,18 @@ function extractSkeleton(run) {
|
|
|
284
231
|
const metricsReport = node_path_1.default.join(run.paths.runDir, "metrics", "metrics-report.json");
|
|
285
232
|
const costRecord = {
|
|
286
233
|
tasks: (run.tasks || []).map((task) => ({ taskId: task.id, model: task.usage?.model, source: task.usage?.source })),
|
|
287
|
-
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? sha256OfFile(metricsReport) : undefined
|
|
234
|
+
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? (0, hash_1.sha256OfFile)(metricsReport) : undefined
|
|
288
235
|
};
|
|
289
236
|
const collaboration = run.collaboration;
|
|
290
237
|
const collaborationLog = {
|
|
291
|
-
digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
238
|
+
digest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
292
239
|
approvals: collaboration?.approvals?.length || 0,
|
|
293
240
|
comments: collaboration?.comments?.length || 0,
|
|
294
241
|
handoffs: collaboration?.handoffs?.length || 0
|
|
295
242
|
};
|
|
296
243
|
// Empty (not a hash-of-empty) when state.json is absent, so the skeleton fails
|
|
297
244
|
// closed — you cannot seal a run whose authoritative state is gone.
|
|
298
|
-
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? sha256OfFile(run.paths.state) : "";
|
|
245
|
+
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? (0, hash_1.sha256OfFile)(run.paths.state) : "";
|
|
299
246
|
return {
|
|
300
247
|
schemaVersion: 1,
|
|
301
248
|
runId: run.id,
|
|
@@ -402,13 +349,13 @@ function buildReferenceGraph(run) {
|
|
|
402
349
|
* node-projection field set (node-projection.ts) so reconstruction matches
|
|
403
350
|
* node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
|
|
404
351
|
function snapshotProjectionDigest(node) {
|
|
405
|
-
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
352
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
406
353
|
}
|
|
407
354
|
/** Body digest of the RETAINED node (lives in state.json). The reconstruction
|
|
408
355
|
* verifier re-derives the projection from this retained input. Same shared field
|
|
409
356
|
* set / canonical bytes as snapshotProjectionDigest. */
|
|
410
357
|
function nodeBodyDigest(node) {
|
|
411
|
-
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
358
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
412
359
|
}
|
|
413
360
|
/** Build the retention plan: which paths are freeable under `policy`, of what
|
|
414
361
|
* kind, how many bytes, and the resulting capability downgrade. */
|
|
@@ -431,7 +378,7 @@ function planReclamation(run, policy = {}) {
|
|
|
431
378
|
const resultsCopy = task?.resultPath;
|
|
432
379
|
if (!resultNodeId || !resultsCopy || !node_fs_1.default.existsSync(resultsCopy))
|
|
433
380
|
continue;
|
|
434
|
-
const bytes = dirBytes(workerDir);
|
|
381
|
+
const bytes = (0, hash_1.dirBytes)(workerDir);
|
|
435
382
|
if (bytes <= 0)
|
|
436
383
|
continue;
|
|
437
384
|
freeable.push({
|
|
@@ -478,14 +425,14 @@ function planReclamation(run, policy = {}) {
|
|
|
478
425
|
continue; // source node gone → cannot reconstruct → retain
|
|
479
426
|
if (repointNodeIds.has(node.id))
|
|
480
427
|
continue; // body will be re-pointed → retain
|
|
481
|
-
const bytes = dirBytes(snapFile);
|
|
428
|
+
const bytes = (0, hash_1.dirBytes)(snapFile);
|
|
482
429
|
if (bytes <= 0)
|
|
483
430
|
continue;
|
|
484
431
|
const inputDigest = nodeBodyDigest(node);
|
|
485
432
|
const recipe = {
|
|
486
433
|
recipeKind: "node-snapshot-projection",
|
|
487
434
|
inputDigests: [inputDigest],
|
|
488
|
-
inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
435
|
+
inputsDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
489
436
|
expectDigest: snapshotProjectionDigest(node),
|
|
490
437
|
sourceRef: node.id
|
|
491
438
|
};
|
|
@@ -534,11 +481,11 @@ function planReclamation(run, policy = {}) {
|
|
|
534
481
|
return { freeable, bytesToFree, byKind, capability, capabilityReason };
|
|
535
482
|
}
|
|
536
483
|
function policyDigestOf(policy) {
|
|
537
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
484
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
538
485
|
}
|
|
539
486
|
/** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
|
|
540
487
|
function genesisPrevHash(skeleton) {
|
|
541
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
488
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
542
489
|
}
|
|
543
490
|
/** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
|
|
544
491
|
* prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
|
|
@@ -551,14 +498,14 @@ function tombstoneHashInput(t) {
|
|
|
551
498
|
policyDigest: t.policyDigest,
|
|
552
499
|
freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
|
|
553
500
|
bytesFreed: t.bytesFreed,
|
|
554
|
-
skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
501
|
+
skeletonDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
555
502
|
capability: t.capability,
|
|
556
503
|
capabilityReason: t.capabilityReason,
|
|
557
504
|
prevTombstoneHash: t.prevTombstoneHash
|
|
558
505
|
});
|
|
559
506
|
}
|
|
560
507
|
function computeTombstoneHash(t) {
|
|
561
|
-
return sha256OfString(tombstoneHashInput(t));
|
|
508
|
+
return (0, hash_1.sha256OfString)(tombstoneHashInput(t));
|
|
562
509
|
}
|
|
563
510
|
function tombstoneId(seq) {
|
|
564
511
|
// Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
|
|
@@ -576,7 +523,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
|
|
|
576
523
|
path: entry.path,
|
|
577
524
|
kind: entry.kind,
|
|
578
525
|
bytes: entry.bytes,
|
|
579
|
-
sha256: contentDigest(entry.absPath),
|
|
526
|
+
sha256: (0, hash_1.contentDigest)(entry.absPath),
|
|
580
527
|
recipe: entry.recipe
|
|
581
528
|
}));
|
|
582
529
|
const base = {
|
|
@@ -685,7 +632,7 @@ function freeBulk(run, tombstone) {
|
|
|
685
632
|
let freedBytes = 0;
|
|
686
633
|
for (const entry of tombstone.freed) {
|
|
687
634
|
const abs = node_path_1.default.join(runDir, entry.path);
|
|
688
|
-
const before = dirBytes(abs);
|
|
635
|
+
const before = (0, hash_1.dirBytes)(abs);
|
|
689
636
|
node_fs_1.default.rmSync(abs, { recursive: true, force: true });
|
|
690
637
|
freedBytes += before;
|
|
691
638
|
}
|
|
@@ -774,15 +721,15 @@ function reconstructArtifact(run, recipe) {
|
|
|
774
721
|
if (recipe.recipeKind === "node-snapshot-projection") {
|
|
775
722
|
const node = (run.nodes || []).find((n) => n.id === recipe.sourceRef);
|
|
776
723
|
if (!node) {
|
|
777
|
-
return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
|
|
724
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("absent"), expectDigest: (0, hash_1.sha256OfString)("absent") };
|
|
778
725
|
}
|
|
779
726
|
const inputDigest = nodeBodyDigest(node);
|
|
780
|
-
const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
727
|
+
const inputsDigest = (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
781
728
|
const expectDigest = snapshotProjectionDigest(node);
|
|
782
729
|
return { inputsDigest, expectDigest };
|
|
783
730
|
}
|
|
784
731
|
// Unknown recipe kind → fail closed (digest can't match expectDigest).
|
|
785
|
-
return { inputsDigest: sha256OfString("unknown-recipe"), expectDigest: sha256OfString("unknown-recipe") };
|
|
732
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("unknown-recipe"), expectDigest: (0, hash_1.sha256OfString)("unknown-recipe") };
|
|
786
733
|
}
|
|
787
734
|
/** Re-prove the whole reclamation chain for a run: skeleton schema-complete,
|
|
788
735
|
* tombstoneHash/prevTombstoneHash chain recomputed-and-untampered, and each
|
|
@@ -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.84";
|
|
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;
|