cool-workflow 0.1.80 → 0.1.81
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +42 -2
- 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/agent-config.js +21 -7
- package/dist/candidate-scoring.js +42 -22
- package/dist/capability-core.js +94 -17
- package/dist/capability-registry.js +138 -171
- package/dist/cli.js +90 -100
- package/dist/collaboration.js +5 -6
- package/dist/commit.js +20 -6
- package/dist/compare.js +18 -0
- package/dist/coordinator/classify.js +45 -0
- package/dist/coordinator/paths.js +42 -0
- package/dist/coordinator/util.js +129 -0
- package/dist/coordinator.js +127 -300
- package/dist/dispatch.js +35 -0
- package/dist/drive.js +7 -7
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +1 -1
- package/dist/execution-backend/agent.js +331 -0
- package/dist/execution-backend/probes.js +96 -0
- package/dist/execution-backend/util.js +47 -0
- package/dist/execution-backend.js +67 -420
- package/dist/mcp-server.js +34 -173
- package/dist/multi-agent/graph.js +84 -0
- package/dist/multi-agent/helpers.js +145 -0
- package/dist/multi-agent/paths.js +22 -0
- package/dist/multi-agent-eval/format.js +194 -0
- package/dist/multi-agent-eval/normalize.js +51 -0
- package/dist/multi-agent-eval.js +39 -244
- package/dist/multi-agent-host.js +0 -19
- package/dist/multi-agent.js +125 -314
- package/dist/node-snapshot.js +3 -3
- package/dist/observability/format.js +61 -0
- package/dist/observability/intake.js +98 -0
- package/dist/observability.js +14 -160
- package/dist/operator-ux/format.js +364 -0
- package/dist/operator-ux.js +22 -363
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +25 -8
- package/dist/reclamation.js +26 -21
- package/dist/run-export.js +138 -14
- package/dist/run-registry/derive.js +172 -0
- package/dist/run-registry/format.js +124 -0
- package/dist/run-registry/gc.js +251 -0
- package/dist/run-registry/policy.js +16 -0
- package/dist/run-registry/queue.js +116 -0
- package/dist/run-registry.js +78 -593
- package/dist/run-state-schema.js +1 -0
- package/dist/sandbox-profile.js +43 -2
- package/dist/state-explosion/format.js +159 -0
- package/dist/state-explosion/helpers.js +82 -0
- package/dist/state-explosion.js +65 -283
- package/dist/state-node.js +19 -4
- package/dist/telemetry-attestation.js +55 -0
- package/dist/telemetry-demo.js +15 -3
- package/dist/telemetry-ledger.js +60 -15
- package/dist/topology.js +25 -8
- package/dist/triggers.js +33 -14
- package/dist/trust-audit.js +145 -33
- package/dist/version.js +1 -1
- package/dist/worker-isolation/helpers.js +51 -0
- package/dist/worker-isolation/paths.js +46 -0
- package/dist/worker-isolation.js +39 -115
- package/docs/agent-delegation-drive.7.md +13 -0
- package/docs/cli-mcp-parity.7.md +4 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/index.md +1 -0
- package/docs/launch/launch-kit.md +46 -23
- package/docs/launch/pre-launch-checklist.md +14 -14
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -5
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/run-registry-control-plane.7.md +54 -8
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-model.md +267 -0
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +4 -2
- package/scripts/agents/builtin-templates.json +7 -0
- package/scripts/bump-version.js +5 -11
- package/scripts/canonical-apps-list.js +64 -0
- package/scripts/canonical-apps.js +19 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +5 -0
- package/scripts/release-check.js +5 -1
- package/scripts/version-sync-check.js +5 -8
- package/dist/capability-dispatcher.js +0 -86
package/dist/run-registry.js
CHANGED
|
@@ -34,51 +34,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
34
34
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
35
35
|
};
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.RunRegistry = exports.DEFAULT_RUN_REGISTRY_POLICY = exports.
|
|
37
|
+
exports.formatQueueList = exports.formatHistory = exports.formatResume = exports.formatGcVerify = exports.formatGcRun = exports.formatGcPlan = exports.formatRunShow = exports.formatRunSearch = exports.formatRegistryReport = exports.RunRegistry = exports.RUN_REGISTRY_SCHEMA_VERSION = exports.DEFAULT_RUN_REGISTRY_POLICY = exports.isRunLifecycleState = exports.compareQueue = void 0;
|
|
38
38
|
exports.resolveCwHome = resolveCwHome;
|
|
39
39
|
exports.deriveLifecycle = deriveLifecycle;
|
|
40
|
-
exports.compareQueue = compareQueue;
|
|
41
|
-
exports.isRunLifecycleState = isRunLifecycleState;
|
|
42
|
-
exports.formatRegistryReport = formatRegistryReport;
|
|
43
|
-
exports.formatRunSearch = formatRunSearch;
|
|
44
|
-
exports.formatRunShow = formatRunShow;
|
|
45
|
-
exports.formatGcPlan = formatGcPlan;
|
|
46
|
-
exports.formatGcRun = formatGcRun;
|
|
47
|
-
exports.formatGcVerify = formatGcVerify;
|
|
48
|
-
exports.formatResume = formatResume;
|
|
49
|
-
exports.formatHistory = formatHistory;
|
|
50
|
-
exports.formatQueueList = formatQueueList;
|
|
51
40
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
52
41
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
53
42
|
const node_os_1 = __importDefault(require("node:os"));
|
|
54
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
55
44
|
const state_1 = require("./state");
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// POLICY defaults. Configurable; never baked into the index. archiveOlderThanDays
|
|
68
|
-
// = 0 disables retention archiving (explicit selection still works). The v0.1.39
|
|
69
|
-
// reclamation knobs all default to RECLAIM NOTHING (back-compatible, opt-in).
|
|
70
|
-
exports.DEFAULT_RUN_REGISTRY_POLICY = {
|
|
71
|
-
schemaVersion: 1,
|
|
72
|
-
archiveOlderThanDays: 0,
|
|
73
|
-
archiveStates: ["completed", "failed"],
|
|
74
|
-
defaultQueuePriority: 100,
|
|
75
|
-
reclaimAfterArchiveDays: 0,
|
|
76
|
-
reclaimStates: ["completed", "failed"],
|
|
77
|
-
keepSnapshots: false,
|
|
78
|
-
keepScratch: false,
|
|
79
|
-
maxReclaimRuns: 0,
|
|
80
|
-
maxReclaimBytes: 0
|
|
81
|
-
};
|
|
45
|
+
// planReclamation/runReclamation/verifyReclamation/ReclamationError moved with the
|
|
46
|
+
// GC cluster into ./run-registry/gc (FreeBSD-audit R2 deep).
|
|
47
|
+
const compare_1 = require("./compare");
|
|
48
|
+
const derive_1 = require("./run-registry/derive");
|
|
49
|
+
Object.defineProperty(exports, "compareQueue", { enumerable: true, get: function () { return derive_1.compareQueue; } });
|
|
50
|
+
Object.defineProperty(exports, "isRunLifecycleState", { enumerable: true, get: function () { return derive_1.isRunLifecycleState; } });
|
|
51
|
+
const gc_1 = require("./run-registry/gc");
|
|
52
|
+
const queue_1 = require("./run-registry/queue");
|
|
53
|
+
const policy_1 = require("./run-registry/policy");
|
|
54
|
+
Object.defineProperty(exports, "DEFAULT_RUN_REGISTRY_POLICY", { enumerable: true, get: function () { return policy_1.DEFAULT_RUN_REGISTRY_POLICY; } });
|
|
55
|
+
Object.defineProperty(exports, "RUN_REGISTRY_SCHEMA_VERSION", { enumerable: true, get: function () { return policy_1.RUN_REGISTRY_SCHEMA_VERSION; } });
|
|
82
56
|
// ---------------------------------------------------------------------------
|
|
83
57
|
// Home registry location (EXPLICIT, INSPECTABLE STATE)
|
|
84
58
|
// ---------------------------------------------------------------------------
|
|
@@ -109,16 +83,16 @@ function fingerprintRun(run) {
|
|
|
109
83
|
`loopStage:${run.loopStage}`,
|
|
110
84
|
`schema:${run.schemaVersion}`
|
|
111
85
|
];
|
|
112
|
-
for (const task of [...run.tasks].sort((a, b) => a.id
|
|
86
|
+
for (const task of [...run.tasks].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
|
|
113
87
|
parts.push(`task:${task.id}:${task.status}`);
|
|
114
88
|
}
|
|
115
|
-
for (const commit of [...run.commits].sort((a, b) => a.id
|
|
89
|
+
for (const commit of [...run.commits].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
|
|
116
90
|
parts.push(`commit:${commit.id}:${commit.verifierGated ? "gated" : "checkpoint"}`);
|
|
117
91
|
}
|
|
118
|
-
for (const phase of [...run.phases].sort((a, b) => a.id
|
|
92
|
+
for (const phase of [...run.phases].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
|
|
119
93
|
parts.push(`phase:${phase.id}:${phase.status}`);
|
|
120
94
|
}
|
|
121
|
-
for (const fb of [...(run.feedback || [])].sort((a, b) => a.id
|
|
95
|
+
for (const fb of [...(run.feedback || [])].sort((a, b) => (0, compare_1.compareBytes)(a.id, b.id))) {
|
|
122
96
|
parts.push(`feedback:${fb.id}:${fb.status}`);
|
|
123
97
|
}
|
|
124
98
|
return fingerprintStrings(parts);
|
|
@@ -182,6 +156,8 @@ class RunRegistry {
|
|
|
182
156
|
repoRegistryDir(repo) {
|
|
183
157
|
return node_path_1.default.join(repo, ".cw", "registry");
|
|
184
158
|
}
|
|
159
|
+
// Public so the carved queue cluster (run-registry/queue.ts) can resolve the
|
|
160
|
+
// home-registry dir without reaching into private state (QueueHost).
|
|
185
161
|
homeRegistryDir() {
|
|
186
162
|
return node_path_1.default.join(this.homeRoot, "registry");
|
|
187
163
|
}
|
|
@@ -216,6 +192,11 @@ class RunRegistry {
|
|
|
216
192
|
provenance: this.loadProvenanceOverlay(repo)
|
|
217
193
|
};
|
|
218
194
|
}
|
|
195
|
+
/** Default queue priority from POLICY (QueueHost). Exposed so the carved queue
|
|
196
|
+
* cluster never re-derives policy. */
|
|
197
|
+
get defaultQueuePriority() {
|
|
198
|
+
return policy_1.DEFAULT_RUN_REGISTRY_POLICY.defaultQueuePriority;
|
|
199
|
+
}
|
|
219
200
|
// ---- home registry files ------------------------------------------------
|
|
220
201
|
reposFilePath() {
|
|
221
202
|
return node_path_1.default.join(this.homeRegistryDir(), "repos.json");
|
|
@@ -252,28 +233,17 @@ class RunRegistry {
|
|
|
252
233
|
const already = current.repos.some((entry) => node_path_1.default.resolve(entry.root) === resolved);
|
|
253
234
|
if (!already)
|
|
254
235
|
current.repos.push({ root: resolved, addedAt: new Date().toISOString() });
|
|
255
|
-
current.repos.sort((a, b) => a.root
|
|
236
|
+
current.repos.sort((a, b) => (0, compare_1.compareBytes)(a.root, b.root));
|
|
256
237
|
(0, state_1.writeJson)(file, current, { durable: true });
|
|
257
238
|
return { registered: !already, repos: current.repos.map((entry) => entry.root) };
|
|
258
239
|
});
|
|
259
240
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
241
|
+
// Queue file helpers + queueAdd/List/Show/Drain now live in ./run-registry/queue
|
|
242
|
+
// (FreeBSD-audit R2 deep). These remain as thin delegators; `this` satisfies the
|
|
243
|
+
// QueueHost contract structurally (repoRoot, defaultQueuePriority,
|
|
244
|
+
// homeRegistryDir, registerRepo).
|
|
263
245
|
loadQueue() {
|
|
264
|
-
|
|
265
|
-
if (!node_fs_1.default.existsSync(file))
|
|
266
|
-
return [];
|
|
267
|
-
try {
|
|
268
|
-
const parsed = (0, state_1.readJson)(file);
|
|
269
|
-
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
saveQueue(entries) {
|
|
276
|
-
(0, state_1.writeJson)(this.queueFilePath(), { schemaVersion: 1, entries }, { durable: true });
|
|
246
|
+
return (0, queue_1.loadQueue)(this);
|
|
277
247
|
}
|
|
278
248
|
// Public queue accessors for the v0.1.37 control-plane scheduler (it operates ON
|
|
279
249
|
// this queue store via pure functions in scheduling.ts; the queue file is never
|
|
@@ -283,7 +253,7 @@ class RunRegistry {
|
|
|
283
253
|
return this.loadQueue();
|
|
284
254
|
}
|
|
285
255
|
saveQueueEntries(entries) {
|
|
286
|
-
|
|
256
|
+
(0, queue_1.saveQueue)(this, entries);
|
|
287
257
|
}
|
|
288
258
|
schedulingPolicyPath() {
|
|
289
259
|
return node_path_1.default.join(this.homeRegistryDir(), "scheduling-policy.json");
|
|
@@ -313,7 +283,7 @@ class RunRegistry {
|
|
|
313
283
|
// Run Retention & Provable Reclamation (v0.1.39): the per-run reclaimed.json
|
|
314
284
|
// overlay (if any) raises the disk-tier above `archived` and downgrades the
|
|
315
285
|
// capability. Derived from source, never invented.
|
|
316
|
-
const reclaim = loadReclaimedFromDir(runDir);
|
|
286
|
+
const reclaim = (0, derive_1.loadReclaimedFromDir)(runDir);
|
|
317
287
|
const lastTombstone = reclaim.tombstones[reclaim.tombstones.length - 1];
|
|
318
288
|
const tier = lastTombstone ? "reclaimed" : archive ? "archived" : "live";
|
|
319
289
|
const capability = lastTombstone ? lastTombstone.capability : "re-runnable";
|
|
@@ -356,8 +326,8 @@ class RunRegistry {
|
|
|
356
326
|
commitCount: (run.commits || []).length,
|
|
357
327
|
verifierGatedCommitCount: li.verifierGatedCommits,
|
|
358
328
|
openFeedbackCount: li.openFeedback,
|
|
359
|
-
backends: distinctBackends(run),
|
|
360
|
-
inputsDigest: digestInputs(run.inputs),
|
|
329
|
+
backends: (0, derive_1.distinctBackends)(run),
|
|
330
|
+
inputsDigest: (0, derive_1.digestInputs)(run.inputs),
|
|
361
331
|
sourceFingerprint: fingerprintRun(run),
|
|
362
332
|
freshness: "valid",
|
|
363
333
|
provenance
|
|
@@ -379,7 +349,7 @@ class RunRegistry {
|
|
|
379
349
|
if (record)
|
|
380
350
|
records.push(record);
|
|
381
351
|
}
|
|
382
|
-
return records.sort(compareRecords);
|
|
352
|
+
return records.sort(derive_1.compareRecords);
|
|
383
353
|
}
|
|
384
354
|
// ---- index construction (current truth) ---------------------------------
|
|
385
355
|
/** Build the CURRENT index fresh from source for the requested scope. This is
|
|
@@ -389,7 +359,7 @@ class RunRegistry {
|
|
|
389
359
|
const records = [];
|
|
390
360
|
for (const repo of repos)
|
|
391
361
|
records.push(...this.scanRepo(repo));
|
|
392
|
-
records.sort(compareRecords);
|
|
362
|
+
records.sort(derive_1.compareRecords);
|
|
393
363
|
const queue = scope === "home" ? this.loadQueue() : this.loadQueue().filter((q) => node_path_1.default.resolve(q.repo) === this.repoRoot);
|
|
394
364
|
const sourceFingerprint = fingerprintStrings([
|
|
395
365
|
...repos.map((r) => `repo:${r}`),
|
|
@@ -404,7 +374,7 @@ class RunRegistry {
|
|
|
404
374
|
repos,
|
|
405
375
|
records,
|
|
406
376
|
queue,
|
|
407
|
-
counts: countRecords(records)
|
|
377
|
+
counts: (0, derive_1.countRecords)(records)
|
|
408
378
|
};
|
|
409
379
|
}
|
|
410
380
|
persistedIndexPath(scope) {
|
|
@@ -496,20 +466,20 @@ class RunRegistry {
|
|
|
496
466
|
const index = this.buildIndex(scope);
|
|
497
467
|
const report = this.report(scope, index);
|
|
498
468
|
const query = {
|
|
499
|
-
text: optionalLower(raw.text),
|
|
500
|
-
app: optionalLower(raw.app),
|
|
469
|
+
text: (0, derive_1.optionalLower)(raw.text),
|
|
470
|
+
app: (0, derive_1.optionalLower)(raw.app),
|
|
501
471
|
status: raw.status,
|
|
502
472
|
repo: raw.repo ? node_path_1.default.resolve(raw.repo) : undefined,
|
|
503
473
|
since: raw.since,
|
|
504
474
|
until: raw.until,
|
|
505
475
|
includeArchived: raw.includeArchived ?? true,
|
|
506
|
-
offset: clampInt(raw.offset, 0, 0),
|
|
507
|
-
limit: clampInt(raw.limit, 50, 1)
|
|
476
|
+
offset: (0, derive_1.clampInt)(raw.offset, 0, 0),
|
|
477
|
+
limit: (0, derive_1.clampInt)(raw.limit, 50, 1)
|
|
508
478
|
};
|
|
509
|
-
let records = index.records.filter((record) => matchesQuery(record, query));
|
|
479
|
+
let records = index.records.filter((record) => (0, derive_1.matchesQuery)(record, query));
|
|
510
480
|
if (!query.includeArchived)
|
|
511
481
|
records = records.filter((record) => !record.archived);
|
|
512
|
-
records.sort(compareRecords);
|
|
482
|
+
records.sort(derive_1.compareRecords);
|
|
513
483
|
const total = records.length;
|
|
514
484
|
const page = records.slice(query.offset, query.offset + query.limit);
|
|
515
485
|
return {
|
|
@@ -568,6 +538,8 @@ class RunRegistry {
|
|
|
568
538
|
nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
|
|
569
539
|
};
|
|
570
540
|
}
|
|
541
|
+
// Public so the carved gc cluster (run-registry/gc.ts) can resolve a run
|
|
542
|
+
// repo-first without reaching into private state (GcHost).
|
|
571
543
|
locate(runId, scope) {
|
|
572
544
|
// Current repo first (least astonishment: cwd wins).
|
|
573
545
|
const here = this.deriveRecordForRun(this.repoRoot, runId);
|
|
@@ -599,6 +571,8 @@ class RunRegistry {
|
|
|
599
571
|
}
|
|
600
572
|
return undefined;
|
|
601
573
|
}
|
|
574
|
+
// Public so the carved gc cluster (run-registry/gc.ts) can load source state
|
|
575
|
+
// for a resolved run without reaching into private state (GcHost).
|
|
602
576
|
loadRun(repo, runId) {
|
|
603
577
|
const statePath = node_path_1.default.join(this.repoRunsDir(repo), runId, "state.json");
|
|
604
578
|
if (!node_fs_1.default.existsSync(statePath))
|
|
@@ -618,7 +592,7 @@ class RunRegistry {
|
|
|
618
592
|
}
|
|
619
593
|
const record = located.record;
|
|
620
594
|
const run = this.loadRun(record.repo, runId);
|
|
621
|
-
const limit = clampInt(options.limit, 5, 1);
|
|
595
|
+
const limit = (0, derive_1.clampInt)(options.limit, 5, 1);
|
|
622
596
|
const nextTasks = (run.tasks || [])
|
|
623
597
|
.filter((t) => t.status === "pending" || t.status === "running")
|
|
624
598
|
.slice(0, limit)
|
|
@@ -699,7 +673,7 @@ class RunRegistry {
|
|
|
699
673
|
/** Apply a retention POLICY: archive eligible runs older than the window. The
|
|
700
674
|
* window/states are policy inputs, never baked into the index. Returns the set
|
|
701
675
|
* archived; archives are overlay marks, so nothing is destroyed. */
|
|
702
|
-
archiveByPolicy(policy =
|
|
676
|
+
archiveByPolicy(policy = policy_1.DEFAULT_RUN_REGISTRY_POLICY, options = {}) {
|
|
703
677
|
const scope = options.scope || "home";
|
|
704
678
|
if (!policy.archiveOlderThanDays || policy.archiveOlderThanDays <= 0) {
|
|
705
679
|
return { policy, archived: [], eligible: 0 };
|
|
@@ -720,227 +694,29 @@ class RunRegistry {
|
|
|
720
694
|
// dry-run (frees nothing); `gc run` executes the write-ahead reclamation
|
|
721
695
|
// transaction (skeleton → tombstone → fsync → free); `gc verify` re-proves a
|
|
722
696
|
// reclaimed run independently. Eligibility is explicit and fail-closed.
|
|
697
|
+
// Implementations live in ./run-registry/gc (FreeBSD-audit R2 deep); these are
|
|
698
|
+
// thin delegators preserving the public surface. `this` satisfies GcHost
|
|
699
|
+
// (buildIndex, locate, loadRun).
|
|
723
700
|
/** Resolve the effective reclamation policy (defaults reclaim NOTHING). */
|
|
724
701
|
reclamationPolicy(overrides = {}) {
|
|
725
|
-
return
|
|
726
|
-
}
|
|
727
|
-
/** Fail-closed eligibility: terminal AND archived AND no open feedback AND past
|
|
728
|
-
* retention. Returns the matching refusal code, or null when eligible. Reads
|
|
729
|
-
* the live-source-derived record; order yields distinct, stable codes. */
|
|
730
|
-
reclaimEligibility(record, policy, nowMs) {
|
|
731
|
-
if (record.tier === "reclaimed")
|
|
732
|
-
return "already-reclaimed";
|
|
733
|
-
const terminalStates = policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"];
|
|
734
|
-
if (record.derivedLifecycle !== "completed" && record.derivedLifecycle !== "failed")
|
|
735
|
-
return "non-terminal";
|
|
736
|
-
if (!terminalStates.includes(record.derivedLifecycle))
|
|
737
|
-
return "non-terminal";
|
|
738
|
-
if (record.openFeedbackCount > 0)
|
|
739
|
-
return "open-feedback";
|
|
740
|
-
if (!record.archived)
|
|
741
|
-
return "not-archived";
|
|
742
|
-
const days = policy.reclaimAfterArchiveDays ?? 0;
|
|
743
|
-
if (days > 0) {
|
|
744
|
-
const archivedAtMs = record.archivedAt ? Date.parse(record.archivedAt) : NaN;
|
|
745
|
-
if (!Number.isFinite(archivedAtMs))
|
|
746
|
-
return "within-retention";
|
|
747
|
-
if (archivedAtMs > nowMs - days * 24 * 60 * 60 * 1000)
|
|
748
|
-
return "within-retention";
|
|
749
|
-
}
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
/** Resolve a single run to a one-element record list via locate() (repo-first),
|
|
753
|
-
* avoiding a full-registry scan for single-run gc plan/run. */
|
|
754
|
-
recordsForRunId(runId, scope) {
|
|
755
|
-
const located = this.locate(runId, scope);
|
|
756
|
-
return located ? [located.record] : [];
|
|
702
|
+
return (0, gc_1.reclamationPolicy)(overrides);
|
|
757
703
|
}
|
|
758
704
|
/** Dry-run: compute eligible runs, per-kind bytes that WOULD be freed, and the
|
|
759
705
|
* capability downgrade. Frees NOTHING. */
|
|
760
706
|
gcPlan(options = {}) {
|
|
761
|
-
|
|
762
|
-
const policy = this.reclamationPolicy(options.policy);
|
|
763
|
-
const nowIso = options.now || new Date().toISOString();
|
|
764
|
-
const nowMs = Date.parse(nowIso);
|
|
765
|
-
// Fast, deterministic single-run path: resolve just that run via locate()
|
|
766
|
-
// (repo-first) so a home-scope plan never re-scans the whole registry.
|
|
767
|
-
const records = options.runId ? this.recordsForRunId(options.runId, scope) : this.buildIndex(scope).records;
|
|
768
|
-
const entries = [];
|
|
769
|
-
let bytesToFree = 0;
|
|
770
|
-
let eligibleCount = 0;
|
|
771
|
-
for (const record of records) {
|
|
772
|
-
const refusal = this.reclaimEligibility(record, policy, nowMs);
|
|
773
|
-
let plan;
|
|
774
|
-
try {
|
|
775
|
-
const run = this.loadRun(record.repo, record.runId);
|
|
776
|
-
plan = (0, reclamation_1.planReclamation)(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots });
|
|
777
|
-
}
|
|
778
|
-
catch {
|
|
779
|
-
entries.push({
|
|
780
|
-
runId: record.runId,
|
|
781
|
-
repo: record.repo,
|
|
782
|
-
eligible: false,
|
|
783
|
-
reason: "unreadable",
|
|
784
|
-
tier: record.tier || "live",
|
|
785
|
-
capability: record.capability || "re-runnable",
|
|
786
|
-
capabilityReason: record.capabilityReason || "live-full",
|
|
787
|
-
bytesToFree: 0,
|
|
788
|
-
byKind: {},
|
|
789
|
-
freeable: []
|
|
790
|
-
});
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
const eligible = refusal === null;
|
|
794
|
-
const entry = {
|
|
795
|
-
runId: record.runId,
|
|
796
|
-
repo: record.repo,
|
|
797
|
-
eligible,
|
|
798
|
-
reason: eligible ? "eligible" : refusal,
|
|
799
|
-
tier: record.tier || "live",
|
|
800
|
-
capability: plan.capability,
|
|
801
|
-
capabilityReason: plan.capabilityReason,
|
|
802
|
-
bytesToFree: eligible ? plan.bytesToFree : 0,
|
|
803
|
-
byKind: eligible ? plan.byKind : {},
|
|
804
|
-
freeable: eligible ? plan.freeable.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes })) : []
|
|
805
|
-
};
|
|
806
|
-
entries.push(entry);
|
|
807
|
-
if (eligible) {
|
|
808
|
-
eligibleCount += 1;
|
|
809
|
-
bytesToFree += plan.bytesToFree;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
schemaVersion: 1,
|
|
814
|
-
scope,
|
|
815
|
-
generatedAt: nowIso,
|
|
816
|
-
policy: {
|
|
817
|
-
reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays ?? 0,
|
|
818
|
-
keepSnapshots: Boolean(policy.keepSnapshots),
|
|
819
|
-
keepScratch: Boolean(policy.keepScratch),
|
|
820
|
-
reclaimStates: policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"]
|
|
821
|
-
},
|
|
822
|
-
total: entries.length,
|
|
823
|
-
eligibleCount,
|
|
824
|
-
bytesToFree,
|
|
825
|
-
entries,
|
|
826
|
-
nextAction: eligibleCount ? "node scripts/cw.js gc run" : "node scripts/cw.js run search"
|
|
827
|
-
};
|
|
707
|
+
return (0, gc_1.gcPlan)(this, options);
|
|
828
708
|
}
|
|
829
709
|
/** Execute the write-ahead reclamation transaction for eligible runs. Bounded
|
|
830
710
|
* (`maxReclaimRuns` / `maxReclaimBytes`), fail-closed on any incomplete
|
|
831
711
|
* skeleton. Produces a tombstone and frees the bulk. */
|
|
832
712
|
gcRun(options = {}) {
|
|
833
|
-
|
|
834
|
-
const policy = this.reclamationPolicy(options.policy);
|
|
835
|
-
const nowIso = options.now || new Date().toISOString();
|
|
836
|
-
const nowMs = Date.parse(nowIso);
|
|
837
|
-
const records = options.runId ? this.recordsForRunId(options.runId, scope) : this.buildIndex(scope).records;
|
|
838
|
-
const maxRuns = options.limit ?? (policy.maxReclaimRuns || 0);
|
|
839
|
-
const maxBytes = policy.maxReclaimBytes || 0;
|
|
840
|
-
const reclaimed = [];
|
|
841
|
-
const refused = [];
|
|
842
|
-
let totalBytesFreed = 0;
|
|
843
|
-
for (const record of records) {
|
|
844
|
-
const refusal = this.reclaimEligibility(record, policy, nowMs);
|
|
845
|
-
if (refusal) {
|
|
846
|
-
refused.push({ runId: record.runId, code: refusal });
|
|
847
|
-
continue;
|
|
848
|
-
}
|
|
849
|
-
if (maxRuns > 0 && reclaimed.length >= maxRuns)
|
|
850
|
-
break;
|
|
851
|
-
let run;
|
|
852
|
-
try {
|
|
853
|
-
run = this.loadRun(record.repo, record.runId);
|
|
854
|
-
}
|
|
855
|
-
catch {
|
|
856
|
-
refused.push({ runId: record.runId, code: "unreadable" });
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
try {
|
|
860
|
-
const result = (0, reclamation_1.runReclamation)(run, {
|
|
861
|
-
now: nowIso,
|
|
862
|
-
actor: options.actor,
|
|
863
|
-
policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
|
|
864
|
-
reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots }
|
|
865
|
-
});
|
|
866
|
-
// No post-free saveCheckpoint: runReclamation now DURABLY persists the
|
|
867
|
-
// result-node re-point inside the transaction (before any byte is freed),
|
|
868
|
-
// so state.json can never reference a freed path even on a crash here.
|
|
869
|
-
reclaimed.push({
|
|
870
|
-
runId: record.runId,
|
|
871
|
-
bytesFreed: result.bytesFreed,
|
|
872
|
-
tombstoneHash: result.tombstone.tombstoneHash,
|
|
873
|
-
capability: result.tombstone.capability,
|
|
874
|
-
capabilityReason: result.tombstone.capabilityReason
|
|
875
|
-
});
|
|
876
|
-
totalBytesFreed += result.bytesFreed;
|
|
877
|
-
if (maxBytes > 0 && totalBytesFreed >= maxBytes)
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
catch (error) {
|
|
881
|
-
if (error instanceof reclamation_1.ReclamationError)
|
|
882
|
-
refused.push({ runId: record.runId, code: error.code });
|
|
883
|
-
else
|
|
884
|
-
throw error;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return {
|
|
888
|
-
schemaVersion: 1,
|
|
889
|
-
scope,
|
|
890
|
-
generatedAt: nowIso,
|
|
891
|
-
dryRun: false,
|
|
892
|
-
reclaimed,
|
|
893
|
-
refused,
|
|
894
|
-
totalBytesFreed,
|
|
895
|
-
nextAction: reclaimed.length ? "node scripts/cw.js gc verify <run-id>" : "node scripts/cw.js gc plan"
|
|
896
|
-
};
|
|
713
|
+
return (0, gc_1.gcRun)(this, options);
|
|
897
714
|
}
|
|
898
715
|
/** Re-prove a reclaimed run: skeleton schema-complete, tombstone chain
|
|
899
716
|
* recomputed-and-untampered, each reconstructable artifact re-derived from its
|
|
900
717
|
* RETAINED inputs to its expectDigest, and eligible-when-reclaimed. */
|
|
901
718
|
gcVerify(runId, options = {}) {
|
|
902
|
-
|
|
903
|
-
const located = this.locate(runId, scope);
|
|
904
|
-
if (!located) {
|
|
905
|
-
return {
|
|
906
|
-
schemaVersion: 1,
|
|
907
|
-
runId,
|
|
908
|
-
reclaimed: false,
|
|
909
|
-
verified: false,
|
|
910
|
-
tier: "live",
|
|
911
|
-
capability: "re-runnable",
|
|
912
|
-
chainLength: 0,
|
|
913
|
-
checks: [{ name: "located", pass: false, code: "not-reclaimed", detail: "run source not found" }],
|
|
914
|
-
nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
const run = this.loadRun(located.record.repo, runId);
|
|
918
|
-
const result = (0, reclamation_1.verifyReclamation)(run);
|
|
919
|
-
const checks = result.checks.map((c) => ({ name: c.name, pass: c.pass, code: c.code, detail: c.detail }));
|
|
920
|
-
// Eligible-when-reclaimed: each tombstone must have sealed a terminal verdict.
|
|
921
|
-
let eligibleWhenReclaimed = result.reclaimed;
|
|
922
|
-
for (const tombstone of result.tombstones) {
|
|
923
|
-
const terminal = tombstone.skeleton.finalVerdict?.terminal === true;
|
|
924
|
-
if (!terminal) {
|
|
925
|
-
eligibleWhenReclaimed = false;
|
|
926
|
-
checks.push({ name: `eligible-when-reclaimed:${tombstone.tombstoneId}`, pass: false, code: "ineligible-when-reclaimed", detail: "non-terminal verdict sealed" });
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
const last = result.tombstones[result.tombstones.length - 1];
|
|
930
|
-
const verified = result.verified && eligibleWhenReclaimed;
|
|
931
|
-
return {
|
|
932
|
-
schemaVersion: 1,
|
|
933
|
-
runId,
|
|
934
|
-
reclaimed: result.reclaimed,
|
|
935
|
-
verified,
|
|
936
|
-
tier: located.record.tier || (result.reclaimed ? "reclaimed" : "live"),
|
|
937
|
-
capability: located.record.capability || "re-runnable",
|
|
938
|
-
capabilityReason: located.record.capabilityReason,
|
|
939
|
-
tombstoneHash: last?.tombstoneHash,
|
|
940
|
-
chainLength: result.tombstones.length,
|
|
941
|
-
checks,
|
|
942
|
-
nextAction: verified ? "node scripts/cw.js run show " + runId : "node scripts/cw.js gc plan"
|
|
943
|
-
};
|
|
719
|
+
return (0, gc_1.gcVerify)(this, runId, options);
|
|
944
720
|
}
|
|
945
721
|
// ---- rerun (NEW run linked to the original; original preserved) ---------
|
|
946
722
|
rerun(runId, options = {}) {
|
|
@@ -992,88 +768,34 @@ class RunRegistry {
|
|
|
992
768
|
};
|
|
993
769
|
}
|
|
994
770
|
// ---- queue (durable, ordered; drained by the host) ----------------------
|
|
771
|
+
// Implementations live in ./run-registry/queue (FreeBSD-audit R2 deep); these
|
|
772
|
+
// are thin delegators preserving the public surface. `this` satisfies QueueHost.
|
|
995
773
|
queueAdd(options = {}) {
|
|
996
|
-
|
|
997
|
-
// Cross-process read-modify-write on the home queue: lock so a concurrently
|
|
998
|
-
// added task can never vanish (v0.1.40, P1-D).
|
|
999
|
-
return (0, state_1.withFileLock)(this.queueFilePath(), () => {
|
|
1000
|
-
const entries = this.loadQueue();
|
|
1001
|
-
const entry = {
|
|
1002
|
-
schemaVersion: 1,
|
|
1003
|
-
id: options.id || queueId(),
|
|
1004
|
-
runId: options.runId,
|
|
1005
|
-
appId: options.appId,
|
|
1006
|
-
workflowId: options.workflowId,
|
|
1007
|
-
repo,
|
|
1008
|
-
priority: Number.isFinite(options.priority) ? Number(options.priority) : exports.DEFAULT_RUN_REGISTRY_POLICY.defaultQueuePriority,
|
|
1009
|
-
enqueuedAt: new Date().toISOString(),
|
|
1010
|
-
status: "pending",
|
|
1011
|
-
inputs: options.inputs,
|
|
1012
|
-
note: options.note
|
|
1013
|
-
};
|
|
1014
|
-
entries.push(entry);
|
|
1015
|
-
this.registerRepo(repo);
|
|
1016
|
-
this.saveQueue(entries);
|
|
1017
|
-
return entry;
|
|
1018
|
-
});
|
|
774
|
+
return (0, queue_1.queueAdd)(this, options);
|
|
1019
775
|
}
|
|
1020
776
|
queueList(options = {}) {
|
|
1021
|
-
|
|
1022
|
-
if (options.status)
|
|
1023
|
-
entries = entries.filter((e) => e.status === options.status);
|
|
1024
|
-
if (options.repo) {
|
|
1025
|
-
const repo = node_path_1.default.resolve(options.repo);
|
|
1026
|
-
entries = entries.filter((e) => node_path_1.default.resolve(e.repo) === repo);
|
|
1027
|
-
}
|
|
1028
|
-
entries = [...entries].sort(compareQueue);
|
|
1029
|
-
return { schemaVersion: 1, total: entries.length, entries };
|
|
777
|
+
return (0, queue_1.queueList)(this, options);
|
|
1030
778
|
}
|
|
1031
779
|
queueShow(id) {
|
|
1032
|
-
|
|
1033
|
-
if (!entry)
|
|
1034
|
-
throw new Error(`Queue entry not found: ${id}`);
|
|
1035
|
-
return entry;
|
|
780
|
+
return (0, queue_1.queueShow)(this, id);
|
|
1036
781
|
}
|
|
1037
|
-
/** Drain the next N ready/pending entries in policy order, marking them drained.
|
|
1038
|
-
* CW records readiness/order; the HOST still executes the workers. */
|
|
1039
782
|
queueDrain(options = {}) {
|
|
1040
|
-
|
|
1041
|
-
const repoFilter = options.repo ? node_path_1.default.resolve(options.repo) : undefined;
|
|
1042
|
-
// Lock the drain RMW so two hosts can never double-drain the same entry
|
|
1043
|
-
// (v0.1.40, P1-D — the scheduling kernel's concurrency ceiling now holds
|
|
1044
|
-
// across processes, not just within one).
|
|
1045
|
-
return (0, state_1.withFileLock)(this.queueFilePath(), () => {
|
|
1046
|
-
const entries = this.loadQueue();
|
|
1047
|
-
const drainable = entries
|
|
1048
|
-
.filter((e) => e.status === "pending" || e.status === "ready")
|
|
1049
|
-
.filter((e) => !repoFilter || node_path_1.default.resolve(e.repo) === repoFilter)
|
|
1050
|
-
.sort(compareQueue);
|
|
1051
|
-
const drained = [];
|
|
1052
|
-
const drainedAt = new Date().toISOString();
|
|
1053
|
-
for (const entry of drainable.slice(0, limit)) {
|
|
1054
|
-
entry.status = "drained";
|
|
1055
|
-
entry.drainedAt = drainedAt;
|
|
1056
|
-
drained.push(entry);
|
|
1057
|
-
}
|
|
1058
|
-
this.saveQueue(entries);
|
|
1059
|
-
const remaining = entries.filter((e) => e.status === "pending" || e.status === "ready").length;
|
|
1060
|
-
return { schemaVersion: 1, drained, remaining };
|
|
1061
|
-
});
|
|
783
|
+
return (0, queue_1.queueDrain)(this, options);
|
|
1062
784
|
}
|
|
1063
785
|
// ---- cross-repo history (unified timeline) ------------------------------
|
|
1064
786
|
history(options = {}) {
|
|
1065
787
|
const scope = options.scope || "home";
|
|
1066
788
|
const index = this.buildIndex(scope);
|
|
1067
789
|
const report = this.report(scope, index);
|
|
1068
|
-
const app = optionalLower(options.app);
|
|
1069
|
-
const limit = clampInt(options.limit, 50, 1);
|
|
1070
|
-
const offset = clampInt(options.offset, 0, 0);
|
|
790
|
+
const app = (0, derive_1.optionalLower)(options.app);
|
|
791
|
+
const limit = (0, derive_1.clampInt)(options.limit, 50, 1);
|
|
792
|
+
const offset = (0, derive_1.clampInt)(options.offset, 0, 0);
|
|
1071
793
|
let records = index.records;
|
|
1072
794
|
if (app)
|
|
1073
795
|
records = records.filter((r) => (r.appId || r.workflowId || "").toLowerCase().includes(app));
|
|
1074
796
|
if (options.status)
|
|
1075
797
|
records = records.filter((r) => r.lifecycle === options.status || r.derivedLifecycle === options.status);
|
|
1076
|
-
const ordered = [...records].sort(compareHistory);
|
|
798
|
+
const ordered = [...records].sort(derive_1.compareHistory);
|
|
1077
799
|
const total = ordered.length;
|
|
1078
800
|
const page = ordered.slice(offset, offset + limit);
|
|
1079
801
|
const entries = page.map((r) => ({
|
|
@@ -1102,253 +824,16 @@ class RunRegistry {
|
|
|
1102
824
|
}
|
|
1103
825
|
}
|
|
1104
826
|
exports.RunRegistry = RunRegistry;
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
function
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1119
|
-
function compareQueue(a, b) {
|
|
1120
|
-
if (a.priority !== b.priority)
|
|
1121
|
-
return a.priority - b.priority;
|
|
1122
|
-
if (a.enqueuedAt !== b.enqueuedAt)
|
|
1123
|
-
return a.enqueuedAt < b.enqueuedAt ? -1 : 1;
|
|
1124
|
-
return a.id.localeCompare(b.id);
|
|
1125
|
-
}
|
|
1126
|
-
function matchesQuery(record, query) {
|
|
1127
|
-
if (query.app && !(record.appId || record.workflowId || "").toLowerCase().includes(query.app))
|
|
1128
|
-
return false;
|
|
1129
|
-
if (query.status && record.lifecycle !== query.status && record.derivedLifecycle !== query.status)
|
|
1130
|
-
return false;
|
|
1131
|
-
if (query.repo && node_path_1.default.resolve(record.repo) !== query.repo)
|
|
1132
|
-
return false;
|
|
1133
|
-
if (query.since && record.createdAt < query.since)
|
|
1134
|
-
return false;
|
|
1135
|
-
if (query.until && record.createdAt > query.until)
|
|
1136
|
-
return false;
|
|
1137
|
-
if (query.text) {
|
|
1138
|
-
const haystack = [
|
|
1139
|
-
record.runId,
|
|
1140
|
-
record.appId,
|
|
1141
|
-
record.workflowId,
|
|
1142
|
-
record.title,
|
|
1143
|
-
record.repo,
|
|
1144
|
-
record.lifecycle,
|
|
1145
|
-
record.loopStage,
|
|
1146
|
-
record.inputsDigest
|
|
1147
|
-
]
|
|
1148
|
-
.filter(Boolean)
|
|
1149
|
-
.join(" ")
|
|
1150
|
-
.toLowerCase();
|
|
1151
|
-
if (!haystack.includes(query.text))
|
|
1152
|
-
return false;
|
|
1153
|
-
}
|
|
1154
|
-
return true;
|
|
1155
|
-
}
|
|
1156
|
-
/** Bounded, deterministic stringification of run inputs for free-text search.
|
|
1157
|
-
* Descriptive intent keys (question, prompt, ...) come first so they survive
|
|
1158
|
-
* truncation; the rest follow alphabetically. Deterministic and compact. */
|
|
1159
|
-
const DIGEST_PRIORITY_KEYS = ["question", "prompt", "task", "summary", "title", "objective", "focus", "topic"];
|
|
1160
|
-
/** Distinct execution backends used by a run's dispatches/tasks, recomputed from
|
|
1161
|
-
* source state. Sorted; empty for pre-v0.1.29 / default-only runs that never
|
|
1162
|
-
* recorded a backend. The registry stays backend-agnostic — this is metadata. */
|
|
1163
|
-
function distinctBackends(run) {
|
|
1164
|
-
const backends = new Set();
|
|
1165
|
-
for (const dispatch of run.dispatches || []) {
|
|
1166
|
-
if (dispatch.backendId)
|
|
1167
|
-
backends.add(dispatch.backendId);
|
|
1168
|
-
}
|
|
1169
|
-
for (const task of run.tasks || []) {
|
|
1170
|
-
if (task.backendId)
|
|
1171
|
-
backends.add(task.backendId);
|
|
1172
|
-
}
|
|
1173
|
-
return [...backends].sort();
|
|
1174
|
-
}
|
|
1175
|
-
function digestInputs(inputs) {
|
|
1176
|
-
if (!inputs || typeof inputs !== "object")
|
|
1177
|
-
return undefined;
|
|
1178
|
-
const keys = Object.keys(inputs);
|
|
1179
|
-
const ordered = [
|
|
1180
|
-
...DIGEST_PRIORITY_KEYS.filter((k) => keys.includes(k)),
|
|
1181
|
-
...keys.filter((k) => !DIGEST_PRIORITY_KEYS.includes(k)).sort()
|
|
1182
|
-
];
|
|
1183
|
-
const parts = [];
|
|
1184
|
-
for (const key of ordered) {
|
|
1185
|
-
const value = inputs[key];
|
|
1186
|
-
if (value === undefined || value === null)
|
|
1187
|
-
continue;
|
|
1188
|
-
const rendered = Array.isArray(value) ? value.join(",") : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1189
|
-
parts.push(`${key}=${rendered}`);
|
|
1190
|
-
}
|
|
1191
|
-
const joined = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1192
|
-
return joined.length > 360 ? `${joined.slice(0, 357)}...` : joined;
|
|
1193
|
-
}
|
|
1194
|
-
function countRecords(records) {
|
|
1195
|
-
const counts = {
|
|
1196
|
-
total: records.length,
|
|
1197
|
-
queued: 0,
|
|
1198
|
-
running: 0,
|
|
1199
|
-
blocked: 0,
|
|
1200
|
-
completed: 0,
|
|
1201
|
-
failed: 0,
|
|
1202
|
-
archived: 0,
|
|
1203
|
-
reclaimed: 0
|
|
1204
|
-
};
|
|
1205
|
-
for (const record of records) {
|
|
1206
|
-
counts[record.lifecycle] = (counts[record.lifecycle] || 0) + 1;
|
|
1207
|
-
}
|
|
1208
|
-
return counts;
|
|
1209
|
-
}
|
|
1210
|
-
function optionalLower(value) {
|
|
1211
|
-
if (value === undefined || value === null || value === "")
|
|
1212
|
-
return undefined;
|
|
1213
|
-
return String(value).toLowerCase();
|
|
1214
|
-
}
|
|
1215
|
-
function clampInt(value, fallback, min) {
|
|
1216
|
-
const n = Number(value);
|
|
1217
|
-
if (!Number.isFinite(n))
|
|
1218
|
-
return fallback;
|
|
1219
|
-
return Math.max(min, Math.floor(n));
|
|
1220
|
-
}
|
|
1221
|
-
let queueCounter = 0;
|
|
1222
|
-
function queueId() {
|
|
1223
|
-
queueCounter += 1;
|
|
1224
|
-
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
1225
|
-
return `q-${stamp}-${String(queueCounter).padStart(3, "0")}`;
|
|
1226
|
-
}
|
|
1227
|
-
function isRunLifecycleState(value) {
|
|
1228
|
-
return typeof value === "string" && LIFECYCLE_STATES.includes(value);
|
|
1229
|
-
}
|
|
1230
|
-
/** Read a run dir's `reclaimed.json` overlay (v0.1.39). Fail-closed to an empty
|
|
1231
|
-
* chain on absence/corruption — a malformed overlay must never brick the run. */
|
|
1232
|
-
function loadReclaimedFromDir(runDir) {
|
|
1233
|
-
const file = node_path_1.default.join(runDir, "reclaimed.json");
|
|
1234
|
-
if (!node_fs_1.default.existsSync(file))
|
|
1235
|
-
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
1236
|
-
try {
|
|
1237
|
-
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
1238
|
-
return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
|
|
1239
|
-
}
|
|
1240
|
-
catch {
|
|
1241
|
-
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
// ---------------------------------------------------------------------------
|
|
1245
|
-
// Human formatting (CLI-only; never affects --json / MCP payloads)
|
|
1246
|
-
// ---------------------------------------------------------------------------
|
|
1247
|
-
function countsLine(counts) {
|
|
1248
|
-
return `total=${counts.total} queued=${counts.queued} running=${counts.running} blocked=${counts.blocked} completed=${counts.completed} failed=${counts.failed} archived=${counts.archived} reclaimed=${counts.reclaimed}`;
|
|
1249
|
-
}
|
|
1250
|
-
function recordLine(record) {
|
|
1251
|
-
const flags = [record.archived ? "archived" : "", record.provenance?.rerunOf ? `rerunOf=${record.provenance.rerunOf}` : ""].filter(Boolean).join(" ");
|
|
1252
|
-
return ` [${record.lifecycle}] ${record.runId} (${record.appId || record.workflowId}) ${record.loopStage}${flags ? ` {${flags}}` : ""}`;
|
|
1253
|
-
}
|
|
1254
|
-
function formatRegistryReport(report) {
|
|
1255
|
-
const lines = [];
|
|
1256
|
-
lines.push(`Run Registry (${report.scope}): ${report.root}`);
|
|
1257
|
-
lines.push(`Freshness: ${report.freshness.status}${report.freshness.staleRuns.length ? ` (stale: ${report.freshness.staleRuns.join(", ")})` : ""}${report.freshness.missingRuns.length ? ` (missing: ${report.freshness.missingRuns.join(", ")})` : ""}`);
|
|
1258
|
-
lines.push(`Repos: ${report.index.repos.length}`);
|
|
1259
|
-
lines.push(countsLine(report.counts));
|
|
1260
|
-
if (report.freshness.status !== "valid")
|
|
1261
|
-
lines.push(`Next Action: ${report.nextAction}`);
|
|
1262
|
-
return lines.join("\n");
|
|
1263
|
-
}
|
|
1264
|
-
function formatRunSearch(result) {
|
|
1265
|
-
const lines = [];
|
|
1266
|
-
lines.push(`Run Search (${result.scope}): ${result.total} match(es), showing ${result.records.length} [offset ${result.offset}] freshness=${result.freshness}`);
|
|
1267
|
-
for (const record of result.records)
|
|
1268
|
-
lines.push(recordLine(record));
|
|
1269
|
-
if (!result.records.length)
|
|
1270
|
-
lines.push(" (no matching runs)");
|
|
1271
|
-
return lines.join("\n");
|
|
1272
|
-
}
|
|
1273
|
-
function formatRunShow(result) {
|
|
1274
|
-
if (!result.found) {
|
|
1275
|
-
return `Run ${result.runId}: MISSING (source state.json absent — fail closed). Next: ${result.nextAction}`;
|
|
1276
|
-
}
|
|
1277
|
-
const r = result.record;
|
|
1278
|
-
const lines = [
|
|
1279
|
-
`Run ${r.runId} [${r.lifecycle}] (derived: ${r.derivedLifecycle})`,
|
|
1280
|
-
` app=${r.appId || r.workflowId} loopStage=${r.loopStage} repo=${r.repo}`,
|
|
1281
|
-
` tasks: total=${r.tasks.total} pending=${r.tasks.pending} running=${r.tasks.running} failed=${r.tasks.failed} completed=${r.tasks.completed}`,
|
|
1282
|
-
` commits=${r.commitCount} (verifier-gated=${r.verifierGatedCommitCount}) openFeedback=${r.openFeedbackCount}`
|
|
1283
|
-
];
|
|
1284
|
-
if (r.provenance?.rerunOf)
|
|
1285
|
-
lines.push(` provenance: rerunOf=${r.provenance.rerunOf} gen=${r.provenance.generation} origin=${r.provenance.originRunId}`);
|
|
1286
|
-
if (r.tier && r.tier !== "live") {
|
|
1287
|
-
lines.push(` tier=${r.tier} capability=${r.capability} reason=${r.capabilityReason}${r.reclaimedBytes ? ` bytesFreed=${r.reclaimedBytes}` : ""}${r.tombstoneHash ? ` tombstone=${r.tombstoneHash.slice(0, 19)}` : ""}`);
|
|
1288
|
-
}
|
|
1289
|
-
return lines.join("\n");
|
|
1290
|
-
}
|
|
1291
|
-
function formatGcPlan(result) {
|
|
1292
|
-
const lines = [
|
|
1293
|
-
`GC Plan (${result.scope}): ${result.eligibleCount}/${result.total} eligible, ${result.bytesToFree} byte(s) would be freed [DRY-RUN, frees nothing]`,
|
|
1294
|
-
` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots}`
|
|
1295
|
-
];
|
|
1296
|
-
for (const entry of result.entries) {
|
|
1297
|
-
if (entry.eligible) {
|
|
1298
|
-
const kinds = Object.entries(entry.byKind).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1299
|
-
lines.push(` [eligible] ${entry.runId} -> ${entry.capability} (${entry.capabilityReason}) ${entry.bytesToFree}B {${kinds}}`);
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
lines.push(` [skip:${entry.reason}] ${entry.runId} (tier=${entry.tier})`);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
if (!result.entries.length)
|
|
1306
|
-
lines.push(" (no runs in scope)");
|
|
1307
|
-
return lines.join("\n");
|
|
1308
|
-
}
|
|
1309
|
-
function formatGcRun(result) {
|
|
1310
|
-
const lines = [`GC Run (${result.scope}): reclaimed ${result.reclaimed.length} run(s), freed ${result.totalBytesFreed} byte(s)`];
|
|
1311
|
-
for (const r of result.reclaimed)
|
|
1312
|
-
lines.push(` [reclaimed] ${r.runId} -> ${r.capability} (${r.capabilityReason}) ${r.bytesFreed}B tombstone=${r.tombstoneHash.slice(0, 19)}`);
|
|
1313
|
-
for (const r of result.refused)
|
|
1314
|
-
lines.push(` [refused:${r.code}] ${r.runId}`);
|
|
1315
|
-
if (!result.reclaimed.length && !result.refused.length)
|
|
1316
|
-
lines.push(" (nothing eligible)");
|
|
1317
|
-
return lines.join("\n");
|
|
1318
|
-
}
|
|
1319
|
-
function formatGcVerify(result) {
|
|
1320
|
-
const lines = [
|
|
1321
|
-
`GC Verify ${result.runId}: reclaimed=${result.reclaimed} verified=${result.verified} tier=${result.tier} capability=${result.capability}${result.tombstoneHash ? ` tombstone=${result.tombstoneHash.slice(0, 19)}` : ""}`
|
|
1322
|
-
];
|
|
1323
|
-
for (const check of result.checks)
|
|
1324
|
-
lines.push(` ${check.pass ? "PASS" : "FAIL"} ${check.name}${check.code ? ` [${check.code}]` : ""}${check.detail ? ` (${check.detail})` : ""}`);
|
|
1325
|
-
return lines.join("\n");
|
|
1326
|
-
}
|
|
1327
|
-
function formatResume(result) {
|
|
1328
|
-
const lines = [
|
|
1329
|
-
`Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
|
|
1330
|
-
` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`
|
|
1331
|
-
];
|
|
1332
|
-
for (const action of result.nextActions)
|
|
1333
|
-
lines.push(` -> ${action.command}\n ${action.reason}`);
|
|
1334
|
-
return lines.join("\n");
|
|
1335
|
-
}
|
|
1336
|
-
function formatHistory(result) {
|
|
1337
|
-
const lines = [];
|
|
1338
|
-
lines.push(`Run History (${result.scope}): ${result.total} run(s) across ${result.repos.length} repo(s), freshness=${result.freshness}`);
|
|
1339
|
-
for (const entry of result.entries) {
|
|
1340
|
-
lines.push(` ${entry.createdAt} [${entry.lifecycle}] ${entry.runId} (${entry.appId || entry.workflowId})${entry.provenance?.rerunOf ? ` rerunOf=${entry.provenance.rerunOf}` : ""}`);
|
|
1341
|
-
}
|
|
1342
|
-
if (!result.entries.length)
|
|
1343
|
-
lines.push(" (no runs)");
|
|
1344
|
-
return lines.join("\n");
|
|
1345
|
-
}
|
|
1346
|
-
function formatQueueList(result) {
|
|
1347
|
-
const lines = [`Run Queue: ${result.total} entry(ies) [priority asc]`];
|
|
1348
|
-
for (const entry of result.entries) {
|
|
1349
|
-
lines.push(` #${entry.priority} ${entry.id} [${entry.status}] ${entry.appId || entry.workflowId || entry.runId || "?"} repo=${entry.repo}${entry.note ? ` note=${entry.note}` : ""}`);
|
|
1350
|
-
}
|
|
1351
|
-
if (!result.entries.length)
|
|
1352
|
-
lines.push(" (queue empty)");
|
|
1353
|
-
return lines.join("\n");
|
|
1354
|
-
}
|
|
827
|
+
// Human formatting (CLI-only) now lives in ./run-registry/format.ts (FreeBSD-
|
|
828
|
+
// audit R2: rendering carved out of the registry class). Re-exported so that
|
|
829
|
+
// importers of "./run-registry" see an unchanged surface.
|
|
830
|
+
var format_1 = require("./run-registry/format");
|
|
831
|
+
Object.defineProperty(exports, "formatRegistryReport", { enumerable: true, get: function () { return format_1.formatRegistryReport; } });
|
|
832
|
+
Object.defineProperty(exports, "formatRunSearch", { enumerable: true, get: function () { return format_1.formatRunSearch; } });
|
|
833
|
+
Object.defineProperty(exports, "formatRunShow", { enumerable: true, get: function () { return format_1.formatRunShow; } });
|
|
834
|
+
Object.defineProperty(exports, "formatGcPlan", { enumerable: true, get: function () { return format_1.formatGcPlan; } });
|
|
835
|
+
Object.defineProperty(exports, "formatGcRun", { enumerable: true, get: function () { return format_1.formatGcRun; } });
|
|
836
|
+
Object.defineProperty(exports, "formatGcVerify", { enumerable: true, get: function () { return format_1.formatGcVerify; } });
|
|
837
|
+
Object.defineProperty(exports, "formatResume", { enumerable: true, get: function () { return format_1.formatResume; } });
|
|
838
|
+
Object.defineProperty(exports, "formatHistory", { enumerable: true, get: function () { return format_1.formatHistory; } });
|
|
839
|
+
Object.defineProperty(exports, "formatQueueList", { enumerable: true, get: function () { return format_1.formatQueueList; } });
|