cool-workflow 0.1.79 → 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 +51 -3
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +64 -0
- package/apps/architecture-review-fast/workflow.js +153 -0
- 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 +132 -17
- package/dist/capability-registry.js +138 -168
- package/dist/cli.js +97 -98
- 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 +79 -6
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +3 -3
- 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 +73 -421
- package/dist/mcp-server.js +79 -183
- 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/lifecycle-operations.js +2 -1
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +26 -9
- package/dist/reclamation.js +26 -21
- package/dist/run-export.js +494 -25
- 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 +89 -597
- 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 +165 -304
- 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 +71 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +16 -0
- package/docs/contract-migration-tooling.7.md +6 -0
- package/docs/control-plane-scheduling.7.md +6 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +8 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
- package/docs/execution-backends.7.md +6 -0
- package/docs/index.md +2 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +96 -17
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
- package/docs/multi-agent-eval-replay-harness.7.md +6 -0
- package/docs/multi-agent-operator-ux.7.md +6 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +6 -0
- package/docs/observability-cost-accounting.7.md +6 -0
- package/docs/project-index.md +27 -6
- package/docs/real-execution-backends.7.md +6 -0
- package/docs/release-and-migration.7.md +8 -0
- package/docs/release-tooling.7.md +6 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +89 -2
- package/docs/run-retention-reclamation.7.md +8 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +13 -0
- package/docs/team-collaboration.7.md +6 -0
- package/docs/trust-model.md +267 -0
- package/docs/unix-principles.md +49 -1
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +6 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +142 -0
- package/package.json +4 -1
- package/scripts/agents/builtin-templates.json +7 -0
- package/scripts/agents/claude-p-agent.js +129 -43
- package/scripts/architecture-review-fast.js +362 -0
- package/scripts/bump-version.js +5 -10
- package/scripts/canonical-apps-list.js +64 -0
- package/scripts/canonical-apps.js +36 -4
- package/scripts/coverage-gate.js +211 -0
- 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/source-context.js +291 -0
- package/scripts/version-sync-check.js +5 -7
- package/skills/ci-triage/SKILL.md +50 -0
- package/skills/ci-triage/agents/openai.yaml +4 -0
- package/skills/cool-workflow/SKILL.md +4 -1
- package/skills/deploy-check/SKILL.md +55 -0
- package/skills/deploy-check/agents/openai.yaml +4 -0
- package/skills/design-qa/SKILL.md +49 -0
- package/skills/design-qa/agents/openai.yaml +4 -0
- package/skills/pr-review/SKILL.md +45 -0
- package/skills/pr-review/agents/openai.yaml +4 -0
- 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
|
}
|
|
@@ -210,6 +186,17 @@ class RunRegistry {
|
|
|
210
186
|
return { schemaVersion: 1, links: {} };
|
|
211
187
|
}
|
|
212
188
|
}
|
|
189
|
+
loadRepoOverlays(repo) {
|
|
190
|
+
return {
|
|
191
|
+
archive: this.loadArchiveOverlay(repo),
|
|
192
|
+
provenance: this.loadProvenanceOverlay(repo)
|
|
193
|
+
};
|
|
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
|
+
}
|
|
213
200
|
// ---- home registry files ------------------------------------------------
|
|
214
201
|
reposFilePath() {
|
|
215
202
|
return node_path_1.default.join(this.homeRegistryDir(), "repos.json");
|
|
@@ -246,28 +233,17 @@ class RunRegistry {
|
|
|
246
233
|
const already = current.repos.some((entry) => node_path_1.default.resolve(entry.root) === resolved);
|
|
247
234
|
if (!already)
|
|
248
235
|
current.repos.push({ root: resolved, addedAt: new Date().toISOString() });
|
|
249
|
-
current.repos.sort((a, b) => a.root
|
|
236
|
+
current.repos.sort((a, b) => (0, compare_1.compareBytes)(a.root, b.root));
|
|
250
237
|
(0, state_1.writeJson)(file, current, { durable: true });
|
|
251
238
|
return { registered: !already, repos: current.repos.map((entry) => entry.root) };
|
|
252
239
|
});
|
|
253
240
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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).
|
|
257
245
|
loadQueue() {
|
|
258
|
-
|
|
259
|
-
if (!node_fs_1.default.existsSync(file))
|
|
260
|
-
return [];
|
|
261
|
-
try {
|
|
262
|
-
const parsed = (0, state_1.readJson)(file);
|
|
263
|
-
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
saveQueue(entries) {
|
|
270
|
-
(0, state_1.writeJson)(this.queueFilePath(), { schemaVersion: 1, entries }, { durable: true });
|
|
246
|
+
return (0, queue_1.loadQueue)(this);
|
|
271
247
|
}
|
|
272
248
|
// Public queue accessors for the v0.1.37 control-plane scheduler (it operates ON
|
|
273
249
|
// this queue store via pure functions in scheduling.ts; the queue file is never
|
|
@@ -277,7 +253,7 @@ class RunRegistry {
|
|
|
277
253
|
return this.loadQueue();
|
|
278
254
|
}
|
|
279
255
|
saveQueueEntries(entries) {
|
|
280
|
-
|
|
256
|
+
(0, queue_1.saveQueue)(this, entries);
|
|
281
257
|
}
|
|
282
258
|
schedulingPolicyPath() {
|
|
283
259
|
return node_path_1.default.join(this.homeRegistryDir(), "scheduling-policy.json");
|
|
@@ -286,7 +262,7 @@ class RunRegistry {
|
|
|
286
262
|
/** Derive a RunRecord from a run directory's source state.json. Returns the
|
|
287
263
|
* record, or null when source is unreadable/unsupported (caller decides how to
|
|
288
264
|
* surface `missing` — we never fabricate a status). */
|
|
289
|
-
deriveRecord(repo, runDir) {
|
|
265
|
+
deriveRecord(repo, runDir, overlays = this.loadRepoOverlays(repo)) {
|
|
290
266
|
const statePath = node_path_1.default.join(runDir, "state.json");
|
|
291
267
|
if (!node_fs_1.default.existsSync(statePath))
|
|
292
268
|
return null;
|
|
@@ -302,12 +278,12 @@ class RunRegistry {
|
|
|
302
278
|
}
|
|
303
279
|
const li = lifecycleInputs(run);
|
|
304
280
|
const derived = deriveLifecycle(li);
|
|
305
|
-
const archive =
|
|
306
|
-
const provenance =
|
|
281
|
+
const archive = overlays.archive.archived[run.id];
|
|
282
|
+
const provenance = overlays.provenance.links[run.id];
|
|
307
283
|
// Run Retention & Provable Reclamation (v0.1.39): the per-run reclaimed.json
|
|
308
284
|
// overlay (if any) raises the disk-tier above `archived` and downgrades the
|
|
309
285
|
// capability. Derived from source, never invented.
|
|
310
|
-
const reclaim = loadReclaimedFromDir(runDir);
|
|
286
|
+
const reclaim = (0, derive_1.loadReclaimedFromDir)(runDir);
|
|
311
287
|
const lastTombstone = reclaim.tombstones[reclaim.tombstones.length - 1];
|
|
312
288
|
const tier = lastTombstone ? "reclaimed" : archive ? "archived" : "live";
|
|
313
289
|
const capability = lastTombstone ? lastTombstone.capability : "re-runnable";
|
|
@@ -350,8 +326,8 @@ class RunRegistry {
|
|
|
350
326
|
commitCount: (run.commits || []).length,
|
|
351
327
|
verifierGatedCommitCount: li.verifierGatedCommits,
|
|
352
328
|
openFeedbackCount: li.openFeedback,
|
|
353
|
-
backends: distinctBackends(run),
|
|
354
|
-
inputsDigest: digestInputs(run.inputs),
|
|
329
|
+
backends: (0, derive_1.distinctBackends)(run),
|
|
330
|
+
inputsDigest: (0, derive_1.digestInputs)(run.inputs),
|
|
355
331
|
sourceFingerprint: fingerprintRun(run),
|
|
356
332
|
freshness: "valid",
|
|
357
333
|
provenance
|
|
@@ -364,15 +340,16 @@ class RunRegistry {
|
|
|
364
340
|
const runsDir = this.repoRunsDir(repo);
|
|
365
341
|
if (!node_fs_1.default.existsSync(runsDir))
|
|
366
342
|
return [];
|
|
343
|
+
const overlays = this.loadRepoOverlays(repo);
|
|
367
344
|
const records = [];
|
|
368
345
|
for (const entry of node_fs_1.default.readdirSync(runsDir, { withFileTypes: true })) {
|
|
369
346
|
if (!entry.isDirectory())
|
|
370
347
|
continue;
|
|
371
|
-
const record = this.deriveRecord(repo, node_path_1.default.join(runsDir, entry.name));
|
|
348
|
+
const record = this.deriveRecord(repo, node_path_1.default.join(runsDir, entry.name), overlays);
|
|
372
349
|
if (record)
|
|
373
350
|
records.push(record);
|
|
374
351
|
}
|
|
375
|
-
return records.sort(compareRecords);
|
|
352
|
+
return records.sort(derive_1.compareRecords);
|
|
376
353
|
}
|
|
377
354
|
// ---- index construction (current truth) ---------------------------------
|
|
378
355
|
/** Build the CURRENT index fresh from source for the requested scope. This is
|
|
@@ -382,7 +359,7 @@ class RunRegistry {
|
|
|
382
359
|
const records = [];
|
|
383
360
|
for (const repo of repos)
|
|
384
361
|
records.push(...this.scanRepo(repo));
|
|
385
|
-
records.sort(compareRecords);
|
|
362
|
+
records.sort(derive_1.compareRecords);
|
|
386
363
|
const queue = scope === "home" ? this.loadQueue() : this.loadQueue().filter((q) => node_path_1.default.resolve(q.repo) === this.repoRoot);
|
|
387
364
|
const sourceFingerprint = fingerprintStrings([
|
|
388
365
|
...repos.map((r) => `repo:${r}`),
|
|
@@ -397,7 +374,7 @@ class RunRegistry {
|
|
|
397
374
|
repos,
|
|
398
375
|
records,
|
|
399
376
|
queue,
|
|
400
|
-
counts: countRecords(records)
|
|
377
|
+
counts: (0, derive_1.countRecords)(records)
|
|
401
378
|
};
|
|
402
379
|
}
|
|
403
380
|
persistedIndexPath(scope) {
|
|
@@ -489,20 +466,20 @@ class RunRegistry {
|
|
|
489
466
|
const index = this.buildIndex(scope);
|
|
490
467
|
const report = this.report(scope, index);
|
|
491
468
|
const query = {
|
|
492
|
-
text: optionalLower(raw.text),
|
|
493
|
-
app: optionalLower(raw.app),
|
|
469
|
+
text: (0, derive_1.optionalLower)(raw.text),
|
|
470
|
+
app: (0, derive_1.optionalLower)(raw.app),
|
|
494
471
|
status: raw.status,
|
|
495
472
|
repo: raw.repo ? node_path_1.default.resolve(raw.repo) : undefined,
|
|
496
473
|
since: raw.since,
|
|
497
474
|
until: raw.until,
|
|
498
475
|
includeArchived: raw.includeArchived ?? true,
|
|
499
|
-
offset: clampInt(raw.offset, 0, 0),
|
|
500
|
-
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)
|
|
501
478
|
};
|
|
502
|
-
let records = index.records.filter((record) => matchesQuery(record, query));
|
|
479
|
+
let records = index.records.filter((record) => (0, derive_1.matchesQuery)(record, query));
|
|
503
480
|
if (!query.includeArchived)
|
|
504
481
|
records = records.filter((record) => !record.archived);
|
|
505
|
-
records.sort(compareRecords);
|
|
482
|
+
records.sort(derive_1.compareRecords);
|
|
506
483
|
const total = records.length;
|
|
507
484
|
const page = records.slice(query.offset, query.offset + query.limit);
|
|
508
485
|
return {
|
|
@@ -561,6 +538,8 @@ class RunRegistry {
|
|
|
561
538
|
nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
|
|
562
539
|
};
|
|
563
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).
|
|
564
543
|
locate(runId, scope) {
|
|
565
544
|
// Current repo first (least astonishment: cwd wins).
|
|
566
545
|
const here = this.deriveRecordForRun(this.repoRoot, runId);
|
|
@@ -592,6 +571,8 @@ class RunRegistry {
|
|
|
592
571
|
}
|
|
593
572
|
return undefined;
|
|
594
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).
|
|
595
576
|
loadRun(repo, runId) {
|
|
596
577
|
const statePath = node_path_1.default.join(this.repoRunsDir(repo), runId, "state.json");
|
|
597
578
|
if (!node_fs_1.default.existsSync(statePath))
|
|
@@ -611,7 +592,7 @@ class RunRegistry {
|
|
|
611
592
|
}
|
|
612
593
|
const record = located.record;
|
|
613
594
|
const run = this.loadRun(record.repo, runId);
|
|
614
|
-
const limit = clampInt(options.limit, 5, 1);
|
|
595
|
+
const limit = (0, derive_1.clampInt)(options.limit, 5, 1);
|
|
615
596
|
const nextTasks = (run.tasks || [])
|
|
616
597
|
.filter((t) => t.status === "pending" || t.status === "running")
|
|
617
598
|
.slice(0, limit)
|
|
@@ -692,7 +673,7 @@ class RunRegistry {
|
|
|
692
673
|
/** Apply a retention POLICY: archive eligible runs older than the window. The
|
|
693
674
|
* window/states are policy inputs, never baked into the index. Returns the set
|
|
694
675
|
* archived; archives are overlay marks, so nothing is destroyed. */
|
|
695
|
-
archiveByPolicy(policy =
|
|
676
|
+
archiveByPolicy(policy = policy_1.DEFAULT_RUN_REGISTRY_POLICY, options = {}) {
|
|
696
677
|
const scope = options.scope || "home";
|
|
697
678
|
if (!policy.archiveOlderThanDays || policy.archiveOlderThanDays <= 0) {
|
|
698
679
|
return { policy, archived: [], eligible: 0 };
|
|
@@ -713,227 +694,29 @@ class RunRegistry {
|
|
|
713
694
|
// dry-run (frees nothing); `gc run` executes the write-ahead reclamation
|
|
714
695
|
// transaction (skeleton → tombstone → fsync → free); `gc verify` re-proves a
|
|
715
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).
|
|
716
700
|
/** Resolve the effective reclamation policy (defaults reclaim NOTHING). */
|
|
717
701
|
reclamationPolicy(overrides = {}) {
|
|
718
|
-
return
|
|
719
|
-
}
|
|
720
|
-
/** Fail-closed eligibility: terminal AND archived AND no open feedback AND past
|
|
721
|
-
* retention. Returns the matching refusal code, or null when eligible. Reads
|
|
722
|
-
* the live-source-derived record; order yields distinct, stable codes. */
|
|
723
|
-
reclaimEligibility(record, policy, nowMs) {
|
|
724
|
-
if (record.tier === "reclaimed")
|
|
725
|
-
return "already-reclaimed";
|
|
726
|
-
const terminalStates = policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"];
|
|
727
|
-
if (record.derivedLifecycle !== "completed" && record.derivedLifecycle !== "failed")
|
|
728
|
-
return "non-terminal";
|
|
729
|
-
if (!terminalStates.includes(record.derivedLifecycle))
|
|
730
|
-
return "non-terminal";
|
|
731
|
-
if (record.openFeedbackCount > 0)
|
|
732
|
-
return "open-feedback";
|
|
733
|
-
if (!record.archived)
|
|
734
|
-
return "not-archived";
|
|
735
|
-
const days = policy.reclaimAfterArchiveDays ?? 0;
|
|
736
|
-
if (days > 0) {
|
|
737
|
-
const archivedAtMs = record.archivedAt ? Date.parse(record.archivedAt) : NaN;
|
|
738
|
-
if (!Number.isFinite(archivedAtMs))
|
|
739
|
-
return "within-retention";
|
|
740
|
-
if (archivedAtMs > nowMs - days * 24 * 60 * 60 * 1000)
|
|
741
|
-
return "within-retention";
|
|
742
|
-
}
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
/** Resolve a single run to a one-element record list via locate() (repo-first),
|
|
746
|
-
* avoiding a full-registry scan for single-run gc plan/run. */
|
|
747
|
-
recordsForRunId(runId, scope) {
|
|
748
|
-
const located = this.locate(runId, scope);
|
|
749
|
-
return located ? [located.record] : [];
|
|
702
|
+
return (0, gc_1.reclamationPolicy)(overrides);
|
|
750
703
|
}
|
|
751
704
|
/** Dry-run: compute eligible runs, per-kind bytes that WOULD be freed, and the
|
|
752
705
|
* capability downgrade. Frees NOTHING. */
|
|
753
706
|
gcPlan(options = {}) {
|
|
754
|
-
|
|
755
|
-
const policy = this.reclamationPolicy(options.policy);
|
|
756
|
-
const nowIso = options.now || new Date().toISOString();
|
|
757
|
-
const nowMs = Date.parse(nowIso);
|
|
758
|
-
// Fast, deterministic single-run path: resolve just that run via locate()
|
|
759
|
-
// (repo-first) so a home-scope plan never re-scans the whole registry.
|
|
760
|
-
const records = options.runId ? this.recordsForRunId(options.runId, scope) : this.buildIndex(scope).records;
|
|
761
|
-
const entries = [];
|
|
762
|
-
let bytesToFree = 0;
|
|
763
|
-
let eligibleCount = 0;
|
|
764
|
-
for (const record of records) {
|
|
765
|
-
const refusal = this.reclaimEligibility(record, policy, nowMs);
|
|
766
|
-
let plan;
|
|
767
|
-
try {
|
|
768
|
-
const run = this.loadRun(record.repo, record.runId);
|
|
769
|
-
plan = (0, reclamation_1.planReclamation)(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots });
|
|
770
|
-
}
|
|
771
|
-
catch {
|
|
772
|
-
entries.push({
|
|
773
|
-
runId: record.runId,
|
|
774
|
-
repo: record.repo,
|
|
775
|
-
eligible: false,
|
|
776
|
-
reason: "unreadable",
|
|
777
|
-
tier: record.tier || "live",
|
|
778
|
-
capability: record.capability || "re-runnable",
|
|
779
|
-
capabilityReason: record.capabilityReason || "live-full",
|
|
780
|
-
bytesToFree: 0,
|
|
781
|
-
byKind: {},
|
|
782
|
-
freeable: []
|
|
783
|
-
});
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
|
-
const eligible = refusal === null;
|
|
787
|
-
const entry = {
|
|
788
|
-
runId: record.runId,
|
|
789
|
-
repo: record.repo,
|
|
790
|
-
eligible,
|
|
791
|
-
reason: eligible ? "eligible" : refusal,
|
|
792
|
-
tier: record.tier || "live",
|
|
793
|
-
capability: plan.capability,
|
|
794
|
-
capabilityReason: plan.capabilityReason,
|
|
795
|
-
bytesToFree: eligible ? plan.bytesToFree : 0,
|
|
796
|
-
byKind: eligible ? plan.byKind : {},
|
|
797
|
-
freeable: eligible ? plan.freeable.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes })) : []
|
|
798
|
-
};
|
|
799
|
-
entries.push(entry);
|
|
800
|
-
if (eligible) {
|
|
801
|
-
eligibleCount += 1;
|
|
802
|
-
bytesToFree += plan.bytesToFree;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
return {
|
|
806
|
-
schemaVersion: 1,
|
|
807
|
-
scope,
|
|
808
|
-
generatedAt: nowIso,
|
|
809
|
-
policy: {
|
|
810
|
-
reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays ?? 0,
|
|
811
|
-
keepSnapshots: Boolean(policy.keepSnapshots),
|
|
812
|
-
keepScratch: Boolean(policy.keepScratch),
|
|
813
|
-
reclaimStates: policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"]
|
|
814
|
-
},
|
|
815
|
-
total: entries.length,
|
|
816
|
-
eligibleCount,
|
|
817
|
-
bytesToFree,
|
|
818
|
-
entries,
|
|
819
|
-
nextAction: eligibleCount ? "node scripts/cw.js gc run" : "node scripts/cw.js run search"
|
|
820
|
-
};
|
|
707
|
+
return (0, gc_1.gcPlan)(this, options);
|
|
821
708
|
}
|
|
822
709
|
/** Execute the write-ahead reclamation transaction for eligible runs. Bounded
|
|
823
710
|
* (`maxReclaimRuns` / `maxReclaimBytes`), fail-closed on any incomplete
|
|
824
711
|
* skeleton. Produces a tombstone and frees the bulk. */
|
|
825
712
|
gcRun(options = {}) {
|
|
826
|
-
|
|
827
|
-
const policy = this.reclamationPolicy(options.policy);
|
|
828
|
-
const nowIso = options.now || new Date().toISOString();
|
|
829
|
-
const nowMs = Date.parse(nowIso);
|
|
830
|
-
const records = options.runId ? this.recordsForRunId(options.runId, scope) : this.buildIndex(scope).records;
|
|
831
|
-
const maxRuns = options.limit ?? (policy.maxReclaimRuns || 0);
|
|
832
|
-
const maxBytes = policy.maxReclaimBytes || 0;
|
|
833
|
-
const reclaimed = [];
|
|
834
|
-
const refused = [];
|
|
835
|
-
let totalBytesFreed = 0;
|
|
836
|
-
for (const record of records) {
|
|
837
|
-
const refusal = this.reclaimEligibility(record, policy, nowMs);
|
|
838
|
-
if (refusal) {
|
|
839
|
-
refused.push({ runId: record.runId, code: refusal });
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
if (maxRuns > 0 && reclaimed.length >= maxRuns)
|
|
843
|
-
break;
|
|
844
|
-
let run;
|
|
845
|
-
try {
|
|
846
|
-
run = this.loadRun(record.repo, record.runId);
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
refused.push({ runId: record.runId, code: "unreadable" });
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
const result = (0, reclamation_1.runReclamation)(run, {
|
|
854
|
-
now: nowIso,
|
|
855
|
-
actor: options.actor,
|
|
856
|
-
policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
|
|
857
|
-
reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots }
|
|
858
|
-
});
|
|
859
|
-
// No post-free saveCheckpoint: runReclamation now DURABLY persists the
|
|
860
|
-
// result-node re-point inside the transaction (before any byte is freed),
|
|
861
|
-
// so state.json can never reference a freed path even on a crash here.
|
|
862
|
-
reclaimed.push({
|
|
863
|
-
runId: record.runId,
|
|
864
|
-
bytesFreed: result.bytesFreed,
|
|
865
|
-
tombstoneHash: result.tombstone.tombstoneHash,
|
|
866
|
-
capability: result.tombstone.capability,
|
|
867
|
-
capabilityReason: result.tombstone.capabilityReason
|
|
868
|
-
});
|
|
869
|
-
totalBytesFreed += result.bytesFreed;
|
|
870
|
-
if (maxBytes > 0 && totalBytesFreed >= maxBytes)
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
catch (error) {
|
|
874
|
-
if (error instanceof reclamation_1.ReclamationError)
|
|
875
|
-
refused.push({ runId: record.runId, code: error.code });
|
|
876
|
-
else
|
|
877
|
-
throw error;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
return {
|
|
881
|
-
schemaVersion: 1,
|
|
882
|
-
scope,
|
|
883
|
-
generatedAt: nowIso,
|
|
884
|
-
dryRun: false,
|
|
885
|
-
reclaimed,
|
|
886
|
-
refused,
|
|
887
|
-
totalBytesFreed,
|
|
888
|
-
nextAction: reclaimed.length ? "node scripts/cw.js gc verify <run-id>" : "node scripts/cw.js gc plan"
|
|
889
|
-
};
|
|
713
|
+
return (0, gc_1.gcRun)(this, options);
|
|
890
714
|
}
|
|
891
715
|
/** Re-prove a reclaimed run: skeleton schema-complete, tombstone chain
|
|
892
716
|
* recomputed-and-untampered, each reconstructable artifact re-derived from its
|
|
893
717
|
* RETAINED inputs to its expectDigest, and eligible-when-reclaimed. */
|
|
894
718
|
gcVerify(runId, options = {}) {
|
|
895
|
-
|
|
896
|
-
const located = this.locate(runId, scope);
|
|
897
|
-
if (!located) {
|
|
898
|
-
return {
|
|
899
|
-
schemaVersion: 1,
|
|
900
|
-
runId,
|
|
901
|
-
reclaimed: false,
|
|
902
|
-
verified: false,
|
|
903
|
-
tier: "live",
|
|
904
|
-
capability: "re-runnable",
|
|
905
|
-
chainLength: 0,
|
|
906
|
-
checks: [{ name: "located", pass: false, code: "not-reclaimed", detail: "run source not found" }],
|
|
907
|
-
nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
const run = this.loadRun(located.record.repo, runId);
|
|
911
|
-
const result = (0, reclamation_1.verifyReclamation)(run);
|
|
912
|
-
const checks = result.checks.map((c) => ({ name: c.name, pass: c.pass, code: c.code, detail: c.detail }));
|
|
913
|
-
// Eligible-when-reclaimed: each tombstone must have sealed a terminal verdict.
|
|
914
|
-
let eligibleWhenReclaimed = result.reclaimed;
|
|
915
|
-
for (const tombstone of result.tombstones) {
|
|
916
|
-
const terminal = tombstone.skeleton.finalVerdict?.terminal === true;
|
|
917
|
-
if (!terminal) {
|
|
918
|
-
eligibleWhenReclaimed = false;
|
|
919
|
-
checks.push({ name: `eligible-when-reclaimed:${tombstone.tombstoneId}`, pass: false, code: "ineligible-when-reclaimed", detail: "non-terminal verdict sealed" });
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
const last = result.tombstones[result.tombstones.length - 1];
|
|
923
|
-
const verified = result.verified && eligibleWhenReclaimed;
|
|
924
|
-
return {
|
|
925
|
-
schemaVersion: 1,
|
|
926
|
-
runId,
|
|
927
|
-
reclaimed: result.reclaimed,
|
|
928
|
-
verified,
|
|
929
|
-
tier: located.record.tier || (result.reclaimed ? "reclaimed" : "live"),
|
|
930
|
-
capability: located.record.capability || "re-runnable",
|
|
931
|
-
capabilityReason: located.record.capabilityReason,
|
|
932
|
-
tombstoneHash: last?.tombstoneHash,
|
|
933
|
-
chainLength: result.tombstones.length,
|
|
934
|
-
checks,
|
|
935
|
-
nextAction: verified ? "node scripts/cw.js run show " + runId : "node scripts/cw.js gc plan"
|
|
936
|
-
};
|
|
719
|
+
return (0, gc_1.gcVerify)(this, runId, options);
|
|
937
720
|
}
|
|
938
721
|
// ---- rerun (NEW run linked to the original; original preserved) ---------
|
|
939
722
|
rerun(runId, options = {}) {
|
|
@@ -985,88 +768,34 @@ class RunRegistry {
|
|
|
985
768
|
};
|
|
986
769
|
}
|
|
987
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.
|
|
988
773
|
queueAdd(options = {}) {
|
|
989
|
-
|
|
990
|
-
// Cross-process read-modify-write on the home queue: lock so a concurrently
|
|
991
|
-
// added task can never vanish (v0.1.40, P1-D).
|
|
992
|
-
return (0, state_1.withFileLock)(this.queueFilePath(), () => {
|
|
993
|
-
const entries = this.loadQueue();
|
|
994
|
-
const entry = {
|
|
995
|
-
schemaVersion: 1,
|
|
996
|
-
id: options.id || queueId(),
|
|
997
|
-
runId: options.runId,
|
|
998
|
-
appId: options.appId,
|
|
999
|
-
workflowId: options.workflowId,
|
|
1000
|
-
repo,
|
|
1001
|
-
priority: Number.isFinite(options.priority) ? Number(options.priority) : exports.DEFAULT_RUN_REGISTRY_POLICY.defaultQueuePriority,
|
|
1002
|
-
enqueuedAt: new Date().toISOString(),
|
|
1003
|
-
status: "pending",
|
|
1004
|
-
inputs: options.inputs,
|
|
1005
|
-
note: options.note
|
|
1006
|
-
};
|
|
1007
|
-
entries.push(entry);
|
|
1008
|
-
this.registerRepo(repo);
|
|
1009
|
-
this.saveQueue(entries);
|
|
1010
|
-
return entry;
|
|
1011
|
-
});
|
|
774
|
+
return (0, queue_1.queueAdd)(this, options);
|
|
1012
775
|
}
|
|
1013
776
|
queueList(options = {}) {
|
|
1014
|
-
|
|
1015
|
-
if (options.status)
|
|
1016
|
-
entries = entries.filter((e) => e.status === options.status);
|
|
1017
|
-
if (options.repo) {
|
|
1018
|
-
const repo = node_path_1.default.resolve(options.repo);
|
|
1019
|
-
entries = entries.filter((e) => node_path_1.default.resolve(e.repo) === repo);
|
|
1020
|
-
}
|
|
1021
|
-
entries = [...entries].sort(compareQueue);
|
|
1022
|
-
return { schemaVersion: 1, total: entries.length, entries };
|
|
777
|
+
return (0, queue_1.queueList)(this, options);
|
|
1023
778
|
}
|
|
1024
779
|
queueShow(id) {
|
|
1025
|
-
|
|
1026
|
-
if (!entry)
|
|
1027
|
-
throw new Error(`Queue entry not found: ${id}`);
|
|
1028
|
-
return entry;
|
|
780
|
+
return (0, queue_1.queueShow)(this, id);
|
|
1029
781
|
}
|
|
1030
|
-
/** Drain the next N ready/pending entries in policy order, marking them drained.
|
|
1031
|
-
* CW records readiness/order; the HOST still executes the workers. */
|
|
1032
782
|
queueDrain(options = {}) {
|
|
1033
|
-
|
|
1034
|
-
const repoFilter = options.repo ? node_path_1.default.resolve(options.repo) : undefined;
|
|
1035
|
-
// Lock the drain RMW so two hosts can never double-drain the same entry
|
|
1036
|
-
// (v0.1.40, P1-D — the scheduling kernel's concurrency ceiling now holds
|
|
1037
|
-
// across processes, not just within one).
|
|
1038
|
-
return (0, state_1.withFileLock)(this.queueFilePath(), () => {
|
|
1039
|
-
const entries = this.loadQueue();
|
|
1040
|
-
const drainable = entries
|
|
1041
|
-
.filter((e) => e.status === "pending" || e.status === "ready")
|
|
1042
|
-
.filter((e) => !repoFilter || node_path_1.default.resolve(e.repo) === repoFilter)
|
|
1043
|
-
.sort(compareQueue);
|
|
1044
|
-
const drained = [];
|
|
1045
|
-
const drainedAt = new Date().toISOString();
|
|
1046
|
-
for (const entry of drainable.slice(0, limit)) {
|
|
1047
|
-
entry.status = "drained";
|
|
1048
|
-
entry.drainedAt = drainedAt;
|
|
1049
|
-
drained.push(entry);
|
|
1050
|
-
}
|
|
1051
|
-
this.saveQueue(entries);
|
|
1052
|
-
const remaining = entries.filter((e) => e.status === "pending" || e.status === "ready").length;
|
|
1053
|
-
return { schemaVersion: 1, drained, remaining };
|
|
1054
|
-
});
|
|
783
|
+
return (0, queue_1.queueDrain)(this, options);
|
|
1055
784
|
}
|
|
1056
785
|
// ---- cross-repo history (unified timeline) ------------------------------
|
|
1057
786
|
history(options = {}) {
|
|
1058
787
|
const scope = options.scope || "home";
|
|
1059
788
|
const index = this.buildIndex(scope);
|
|
1060
789
|
const report = this.report(scope, index);
|
|
1061
|
-
const app = optionalLower(options.app);
|
|
1062
|
-
const limit = clampInt(options.limit, 50, 1);
|
|
1063
|
-
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);
|
|
1064
793
|
let records = index.records;
|
|
1065
794
|
if (app)
|
|
1066
795
|
records = records.filter((r) => (r.appId || r.workflowId || "").toLowerCase().includes(app));
|
|
1067
796
|
if (options.status)
|
|
1068
797
|
records = records.filter((r) => r.lifecycle === options.status || r.derivedLifecycle === options.status);
|
|
1069
|
-
const ordered = [...records].sort(compareHistory);
|
|
798
|
+
const ordered = [...records].sort(derive_1.compareHistory);
|
|
1070
799
|
const total = ordered.length;
|
|
1071
800
|
const page = ordered.slice(offset, offset + limit);
|
|
1072
801
|
const entries = page.map((r) => ({
|
|
@@ -1095,253 +824,16 @@ class RunRegistry {
|
|
|
1095
824
|
}
|
|
1096
825
|
}
|
|
1097
826
|
exports.RunRegistry = RunRegistry;
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
}
|
|
1106
|
-
function
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
function compareQueue(a, b) {
|
|
1113
|
-
if (a.priority !== b.priority)
|
|
1114
|
-
return a.priority - b.priority;
|
|
1115
|
-
if (a.enqueuedAt !== b.enqueuedAt)
|
|
1116
|
-
return a.enqueuedAt < b.enqueuedAt ? -1 : 1;
|
|
1117
|
-
return a.id.localeCompare(b.id);
|
|
1118
|
-
}
|
|
1119
|
-
function matchesQuery(record, query) {
|
|
1120
|
-
if (query.app && !(record.appId || record.workflowId || "").toLowerCase().includes(query.app))
|
|
1121
|
-
return false;
|
|
1122
|
-
if (query.status && record.lifecycle !== query.status && record.derivedLifecycle !== query.status)
|
|
1123
|
-
return false;
|
|
1124
|
-
if (query.repo && node_path_1.default.resolve(record.repo) !== query.repo)
|
|
1125
|
-
return false;
|
|
1126
|
-
if (query.since && record.createdAt < query.since)
|
|
1127
|
-
return false;
|
|
1128
|
-
if (query.until && record.createdAt > query.until)
|
|
1129
|
-
return false;
|
|
1130
|
-
if (query.text) {
|
|
1131
|
-
const haystack = [
|
|
1132
|
-
record.runId,
|
|
1133
|
-
record.appId,
|
|
1134
|
-
record.workflowId,
|
|
1135
|
-
record.title,
|
|
1136
|
-
record.repo,
|
|
1137
|
-
record.lifecycle,
|
|
1138
|
-
record.loopStage,
|
|
1139
|
-
record.inputsDigest
|
|
1140
|
-
]
|
|
1141
|
-
.filter(Boolean)
|
|
1142
|
-
.join(" ")
|
|
1143
|
-
.toLowerCase();
|
|
1144
|
-
if (!haystack.includes(query.text))
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
return true;
|
|
1148
|
-
}
|
|
1149
|
-
/** Bounded, deterministic stringification of run inputs for free-text search.
|
|
1150
|
-
* Descriptive intent keys (question, prompt, ...) come first so they survive
|
|
1151
|
-
* truncation; the rest follow alphabetically. Deterministic and compact. */
|
|
1152
|
-
const DIGEST_PRIORITY_KEYS = ["question", "prompt", "task", "summary", "title", "objective", "focus", "topic"];
|
|
1153
|
-
/** Distinct execution backends used by a run's dispatches/tasks, recomputed from
|
|
1154
|
-
* source state. Sorted; empty for pre-v0.1.29 / default-only runs that never
|
|
1155
|
-
* recorded a backend. The registry stays backend-agnostic — this is metadata. */
|
|
1156
|
-
function distinctBackends(run) {
|
|
1157
|
-
const backends = new Set();
|
|
1158
|
-
for (const dispatch of run.dispatches || []) {
|
|
1159
|
-
if (dispatch.backendId)
|
|
1160
|
-
backends.add(dispatch.backendId);
|
|
1161
|
-
}
|
|
1162
|
-
for (const task of run.tasks || []) {
|
|
1163
|
-
if (task.backendId)
|
|
1164
|
-
backends.add(task.backendId);
|
|
1165
|
-
}
|
|
1166
|
-
return [...backends].sort();
|
|
1167
|
-
}
|
|
1168
|
-
function digestInputs(inputs) {
|
|
1169
|
-
if (!inputs || typeof inputs !== "object")
|
|
1170
|
-
return undefined;
|
|
1171
|
-
const keys = Object.keys(inputs);
|
|
1172
|
-
const ordered = [
|
|
1173
|
-
...DIGEST_PRIORITY_KEYS.filter((k) => keys.includes(k)),
|
|
1174
|
-
...keys.filter((k) => !DIGEST_PRIORITY_KEYS.includes(k)).sort()
|
|
1175
|
-
];
|
|
1176
|
-
const parts = [];
|
|
1177
|
-
for (const key of ordered) {
|
|
1178
|
-
const value = inputs[key];
|
|
1179
|
-
if (value === undefined || value === null)
|
|
1180
|
-
continue;
|
|
1181
|
-
const rendered = Array.isArray(value) ? value.join(",") : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1182
|
-
parts.push(`${key}=${rendered}`);
|
|
1183
|
-
}
|
|
1184
|
-
const joined = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1185
|
-
return joined.length > 360 ? `${joined.slice(0, 357)}...` : joined;
|
|
1186
|
-
}
|
|
1187
|
-
function countRecords(records) {
|
|
1188
|
-
const counts = {
|
|
1189
|
-
total: records.length,
|
|
1190
|
-
queued: 0,
|
|
1191
|
-
running: 0,
|
|
1192
|
-
blocked: 0,
|
|
1193
|
-
completed: 0,
|
|
1194
|
-
failed: 0,
|
|
1195
|
-
archived: 0,
|
|
1196
|
-
reclaimed: 0
|
|
1197
|
-
};
|
|
1198
|
-
for (const record of records) {
|
|
1199
|
-
counts[record.lifecycle] = (counts[record.lifecycle] || 0) + 1;
|
|
1200
|
-
}
|
|
1201
|
-
return counts;
|
|
1202
|
-
}
|
|
1203
|
-
function optionalLower(value) {
|
|
1204
|
-
if (value === undefined || value === null || value === "")
|
|
1205
|
-
return undefined;
|
|
1206
|
-
return String(value).toLowerCase();
|
|
1207
|
-
}
|
|
1208
|
-
function clampInt(value, fallback, min) {
|
|
1209
|
-
const n = Number(value);
|
|
1210
|
-
if (!Number.isFinite(n))
|
|
1211
|
-
return fallback;
|
|
1212
|
-
return Math.max(min, Math.floor(n));
|
|
1213
|
-
}
|
|
1214
|
-
let queueCounter = 0;
|
|
1215
|
-
function queueId() {
|
|
1216
|
-
queueCounter += 1;
|
|
1217
|
-
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
1218
|
-
return `q-${stamp}-${String(queueCounter).padStart(3, "0")}`;
|
|
1219
|
-
}
|
|
1220
|
-
function isRunLifecycleState(value) {
|
|
1221
|
-
return typeof value === "string" && LIFECYCLE_STATES.includes(value);
|
|
1222
|
-
}
|
|
1223
|
-
/** Read a run dir's `reclaimed.json` overlay (v0.1.39). Fail-closed to an empty
|
|
1224
|
-
* chain on absence/corruption — a malformed overlay must never brick the run. */
|
|
1225
|
-
function loadReclaimedFromDir(runDir) {
|
|
1226
|
-
const file = node_path_1.default.join(runDir, "reclaimed.json");
|
|
1227
|
-
if (!node_fs_1.default.existsSync(file))
|
|
1228
|
-
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
1229
|
-
try {
|
|
1230
|
-
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
1231
|
-
return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
|
|
1232
|
-
}
|
|
1233
|
-
catch {
|
|
1234
|
-
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// ---------------------------------------------------------------------------
|
|
1238
|
-
// Human formatting (CLI-only; never affects --json / MCP payloads)
|
|
1239
|
-
// ---------------------------------------------------------------------------
|
|
1240
|
-
function countsLine(counts) {
|
|
1241
|
-
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}`;
|
|
1242
|
-
}
|
|
1243
|
-
function recordLine(record) {
|
|
1244
|
-
const flags = [record.archived ? "archived" : "", record.provenance?.rerunOf ? `rerunOf=${record.provenance.rerunOf}` : ""].filter(Boolean).join(" ");
|
|
1245
|
-
return ` [${record.lifecycle}] ${record.runId} (${record.appId || record.workflowId}) ${record.loopStage}${flags ? ` {${flags}}` : ""}`;
|
|
1246
|
-
}
|
|
1247
|
-
function formatRegistryReport(report) {
|
|
1248
|
-
const lines = [];
|
|
1249
|
-
lines.push(`Run Registry (${report.scope}): ${report.root}`);
|
|
1250
|
-
lines.push(`Freshness: ${report.freshness.status}${report.freshness.staleRuns.length ? ` (stale: ${report.freshness.staleRuns.join(", ")})` : ""}${report.freshness.missingRuns.length ? ` (missing: ${report.freshness.missingRuns.join(", ")})` : ""}`);
|
|
1251
|
-
lines.push(`Repos: ${report.index.repos.length}`);
|
|
1252
|
-
lines.push(countsLine(report.counts));
|
|
1253
|
-
if (report.freshness.status !== "valid")
|
|
1254
|
-
lines.push(`Next Action: ${report.nextAction}`);
|
|
1255
|
-
return lines.join("\n");
|
|
1256
|
-
}
|
|
1257
|
-
function formatRunSearch(result) {
|
|
1258
|
-
const lines = [];
|
|
1259
|
-
lines.push(`Run Search (${result.scope}): ${result.total} match(es), showing ${result.records.length} [offset ${result.offset}] freshness=${result.freshness}`);
|
|
1260
|
-
for (const record of result.records)
|
|
1261
|
-
lines.push(recordLine(record));
|
|
1262
|
-
if (!result.records.length)
|
|
1263
|
-
lines.push(" (no matching runs)");
|
|
1264
|
-
return lines.join("\n");
|
|
1265
|
-
}
|
|
1266
|
-
function formatRunShow(result) {
|
|
1267
|
-
if (!result.found) {
|
|
1268
|
-
return `Run ${result.runId}: MISSING (source state.json absent — fail closed). Next: ${result.nextAction}`;
|
|
1269
|
-
}
|
|
1270
|
-
const r = result.record;
|
|
1271
|
-
const lines = [
|
|
1272
|
-
`Run ${r.runId} [${r.lifecycle}] (derived: ${r.derivedLifecycle})`,
|
|
1273
|
-
` app=${r.appId || r.workflowId} loopStage=${r.loopStage} repo=${r.repo}`,
|
|
1274
|
-
` tasks: total=${r.tasks.total} pending=${r.tasks.pending} running=${r.tasks.running} failed=${r.tasks.failed} completed=${r.tasks.completed}`,
|
|
1275
|
-
` commits=${r.commitCount} (verifier-gated=${r.verifierGatedCommitCount}) openFeedback=${r.openFeedbackCount}`
|
|
1276
|
-
];
|
|
1277
|
-
if (r.provenance?.rerunOf)
|
|
1278
|
-
lines.push(` provenance: rerunOf=${r.provenance.rerunOf} gen=${r.provenance.generation} origin=${r.provenance.originRunId}`);
|
|
1279
|
-
if (r.tier && r.tier !== "live") {
|
|
1280
|
-
lines.push(` tier=${r.tier} capability=${r.capability} reason=${r.capabilityReason}${r.reclaimedBytes ? ` bytesFreed=${r.reclaimedBytes}` : ""}${r.tombstoneHash ? ` tombstone=${r.tombstoneHash.slice(0, 19)}` : ""}`);
|
|
1281
|
-
}
|
|
1282
|
-
return lines.join("\n");
|
|
1283
|
-
}
|
|
1284
|
-
function formatGcPlan(result) {
|
|
1285
|
-
const lines = [
|
|
1286
|
-
`GC Plan (${result.scope}): ${result.eligibleCount}/${result.total} eligible, ${result.bytesToFree} byte(s) would be freed [DRY-RUN, frees nothing]`,
|
|
1287
|
-
` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots}`
|
|
1288
|
-
];
|
|
1289
|
-
for (const entry of result.entries) {
|
|
1290
|
-
if (entry.eligible) {
|
|
1291
|
-
const kinds = Object.entries(entry.byKind).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1292
|
-
lines.push(` [eligible] ${entry.runId} -> ${entry.capability} (${entry.capabilityReason}) ${entry.bytesToFree}B {${kinds}}`);
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
lines.push(` [skip:${entry.reason}] ${entry.runId} (tier=${entry.tier})`);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
if (!result.entries.length)
|
|
1299
|
-
lines.push(" (no runs in scope)");
|
|
1300
|
-
return lines.join("\n");
|
|
1301
|
-
}
|
|
1302
|
-
function formatGcRun(result) {
|
|
1303
|
-
const lines = [`GC Run (${result.scope}): reclaimed ${result.reclaimed.length} run(s), freed ${result.totalBytesFreed} byte(s)`];
|
|
1304
|
-
for (const r of result.reclaimed)
|
|
1305
|
-
lines.push(` [reclaimed] ${r.runId} -> ${r.capability} (${r.capabilityReason}) ${r.bytesFreed}B tombstone=${r.tombstoneHash.slice(0, 19)}`);
|
|
1306
|
-
for (const r of result.refused)
|
|
1307
|
-
lines.push(` [refused:${r.code}] ${r.runId}`);
|
|
1308
|
-
if (!result.reclaimed.length && !result.refused.length)
|
|
1309
|
-
lines.push(" (nothing eligible)");
|
|
1310
|
-
return lines.join("\n");
|
|
1311
|
-
}
|
|
1312
|
-
function formatGcVerify(result) {
|
|
1313
|
-
const lines = [
|
|
1314
|
-
`GC Verify ${result.runId}: reclaimed=${result.reclaimed} verified=${result.verified} tier=${result.tier} capability=${result.capability}${result.tombstoneHash ? ` tombstone=${result.tombstoneHash.slice(0, 19)}` : ""}`
|
|
1315
|
-
];
|
|
1316
|
-
for (const check of result.checks)
|
|
1317
|
-
lines.push(` ${check.pass ? "PASS" : "FAIL"} ${check.name}${check.code ? ` [${check.code}]` : ""}${check.detail ? ` (${check.detail})` : ""}`);
|
|
1318
|
-
return lines.join("\n");
|
|
1319
|
-
}
|
|
1320
|
-
function formatResume(result) {
|
|
1321
|
-
const lines = [
|
|
1322
|
-
`Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
|
|
1323
|
-
` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`
|
|
1324
|
-
];
|
|
1325
|
-
for (const action of result.nextActions)
|
|
1326
|
-
lines.push(` -> ${action.command}\n ${action.reason}`);
|
|
1327
|
-
return lines.join("\n");
|
|
1328
|
-
}
|
|
1329
|
-
function formatHistory(result) {
|
|
1330
|
-
const lines = [];
|
|
1331
|
-
lines.push(`Run History (${result.scope}): ${result.total} run(s) across ${result.repos.length} repo(s), freshness=${result.freshness}`);
|
|
1332
|
-
for (const entry of result.entries) {
|
|
1333
|
-
lines.push(` ${entry.createdAt} [${entry.lifecycle}] ${entry.runId} (${entry.appId || entry.workflowId})${entry.provenance?.rerunOf ? ` rerunOf=${entry.provenance.rerunOf}` : ""}`);
|
|
1334
|
-
}
|
|
1335
|
-
if (!result.entries.length)
|
|
1336
|
-
lines.push(" (no runs)");
|
|
1337
|
-
return lines.join("\n");
|
|
1338
|
-
}
|
|
1339
|
-
function formatQueueList(result) {
|
|
1340
|
-
const lines = [`Run Queue: ${result.total} entry(ies) [priority asc]`];
|
|
1341
|
-
for (const entry of result.entries) {
|
|
1342
|
-
lines.push(` #${entry.priority} ${entry.id} [${entry.status}] ${entry.appId || entry.workflowId || entry.runId || "?"} repo=${entry.repo}${entry.note ? ` note=${entry.note}` : ""}`);
|
|
1343
|
-
}
|
|
1344
|
-
if (!result.entries.length)
|
|
1345
|
-
lines.push(" (queue empty)");
|
|
1346
|
-
return lines.join("\n");
|
|
1347
|
-
}
|
|
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; } });
|