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.
Files changed (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +51 -3
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +64 -0
  6. package/apps/architecture-review-fast/workflow.js +153 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/agent-config.js +21 -7
  12. package/dist/candidate-scoring.js +42 -22
  13. package/dist/capability-core.js +132 -17
  14. package/dist/capability-registry.js +138 -168
  15. package/dist/cli.js +97 -98
  16. package/dist/collaboration.js +5 -6
  17. package/dist/commit.js +20 -6
  18. package/dist/compare.js +18 -0
  19. package/dist/coordinator/classify.js +45 -0
  20. package/dist/coordinator/paths.js +42 -0
  21. package/dist/coordinator/util.js +129 -0
  22. package/dist/coordinator.js +127 -300
  23. package/dist/dispatch.js +35 -0
  24. package/dist/drive.js +79 -6
  25. package/dist/error-feedback.js +8 -4
  26. package/dist/evidence-reasoning.js +3 -3
  27. package/dist/execution-backend/agent.js +331 -0
  28. package/dist/execution-backend/probes.js +96 -0
  29. package/dist/execution-backend/util.js +47 -0
  30. package/dist/execution-backend.js +73 -421
  31. package/dist/mcp-server.js +79 -183
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +145 -0
  34. package/dist/multi-agent/paths.js +22 -0
  35. package/dist/multi-agent-eval/format.js +194 -0
  36. package/dist/multi-agent-eval/normalize.js +51 -0
  37. package/dist/multi-agent-eval.js +39 -244
  38. package/dist/multi-agent-host.js +0 -19
  39. package/dist/multi-agent.js +125 -314
  40. package/dist/node-snapshot.js +3 -3
  41. package/dist/observability/format.js +61 -0
  42. package/dist/observability/intake.js +98 -0
  43. package/dist/observability.js +14 -160
  44. package/dist/operator-ux/format.js +364 -0
  45. package/dist/operator-ux.js +22 -363
  46. package/dist/orchestrator/lifecycle-operations.js +2 -1
  47. package/dist/orchestrator/report.js +8 -0
  48. package/dist/orchestrator.js +26 -9
  49. package/dist/reclamation.js +26 -21
  50. package/dist/run-export.js +494 -25
  51. package/dist/run-registry/derive.js +172 -0
  52. package/dist/run-registry/format.js +124 -0
  53. package/dist/run-registry/gc.js +251 -0
  54. package/dist/run-registry/policy.js +16 -0
  55. package/dist/run-registry/queue.js +116 -0
  56. package/dist/run-registry.js +89 -597
  57. package/dist/run-state-schema.js +1 -0
  58. package/dist/sandbox-profile.js +43 -2
  59. package/dist/state-explosion/format.js +159 -0
  60. package/dist/state-explosion/helpers.js +82 -0
  61. package/dist/state-explosion.js +165 -304
  62. package/dist/state-node.js +19 -4
  63. package/dist/telemetry-attestation.js +55 -0
  64. package/dist/telemetry-demo.js +15 -3
  65. package/dist/telemetry-ledger.js +60 -15
  66. package/dist/topology.js +25 -8
  67. package/dist/triggers.js +33 -14
  68. package/dist/trust-audit.js +145 -33
  69. package/dist/version.js +1 -1
  70. package/dist/worker-isolation/helpers.js +51 -0
  71. package/dist/worker-isolation/paths.js +46 -0
  72. package/dist/worker-isolation.js +39 -115
  73. package/docs/agent-delegation-drive.7.md +71 -0
  74. package/docs/canonical-workflow-apps.7.md +37 -0
  75. package/docs/cli-mcp-parity.7.md +16 -0
  76. package/docs/contract-migration-tooling.7.md +6 -0
  77. package/docs/control-plane-scheduling.7.md +6 -0
  78. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  79. package/docs/durable-state-and-locking.7.md +8 -0
  80. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  81. package/docs/execution-backends.7.md +6 -0
  82. package/docs/index.md +2 -0
  83. package/docs/launch/demo.tape +28 -0
  84. package/docs/launch/launch-kit.md +96 -17
  85. package/docs/launch/pre-launch-checklist.md +53 -0
  86. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  87. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  88. package/docs/multi-agent-operator-ux.7.md +6 -0
  89. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  90. package/docs/node-snapshot-diff-replay.7.md +6 -0
  91. package/docs/observability-cost-accounting.7.md +6 -0
  92. package/docs/project-index.md +27 -6
  93. package/docs/real-execution-backends.7.md +6 -0
  94. package/docs/release-and-migration.7.md +8 -0
  95. package/docs/release-tooling.7.md +6 -0
  96. package/docs/routines.md +23 -0
  97. package/docs/run-registry-control-plane.7.md +89 -2
  98. package/docs/run-retention-reclamation.7.md +8 -0
  99. package/docs/source-context-profiles.7.md +119 -0
  100. package/docs/state-explosion-management.7.md +13 -0
  101. package/docs/team-collaboration.7.md +6 -0
  102. package/docs/trust-model.md +267 -0
  103. package/docs/unix-principles.md +49 -1
  104. package/docs/vendor-manifest-loadability.7.md +43 -0
  105. package/docs/web-desktop-workbench.7.md +6 -0
  106. package/manifest/plugin.manifest.json +1 -1
  107. package/manifest/source-context-profiles.json +142 -0
  108. package/package.json +4 -1
  109. package/scripts/agents/builtin-templates.json +7 -0
  110. package/scripts/agents/claude-p-agent.js +129 -43
  111. package/scripts/architecture-review-fast.js +362 -0
  112. package/scripts/bump-version.js +5 -10
  113. package/scripts/canonical-apps-list.js +64 -0
  114. package/scripts/canonical-apps.js +36 -4
  115. package/scripts/coverage-gate.js +211 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/source-context.js +291 -0
  121. package/scripts/version-sync-check.js +5 -7
  122. package/skills/ci-triage/SKILL.md +50 -0
  123. package/skills/ci-triage/agents/openai.yaml +4 -0
  124. package/skills/cool-workflow/SKILL.md +4 -1
  125. package/skills/deploy-check/SKILL.md +55 -0
  126. package/skills/deploy-check/agents/openai.yaml +4 -0
  127. package/skills/design-qa/SKILL.md +49 -0
  128. package/skills/design-qa/agents/openai.yaml +4 -0
  129. package/skills/pr-review/SKILL.md +45 -0
  130. package/skills/pr-review/agents/openai.yaml +4 -0
  131. package/dist/capability-dispatcher.js +0 -86
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LIFECYCLE_STATES = void 0;
7
+ exports.compareRecords = compareRecords;
8
+ exports.compareHistory = compareHistory;
9
+ exports.compareQueue = compareQueue;
10
+ exports.matchesQuery = matchesQuery;
11
+ exports.distinctBackends = distinctBackends;
12
+ exports.digestInputs = digestInputs;
13
+ exports.countRecords = countRecords;
14
+ exports.optionalLower = optionalLower;
15
+ exports.clampInt = clampInt;
16
+ exports.queueId = queueId;
17
+ exports.isRunLifecycleState = isRunLifecycleState;
18
+ exports.loadReclaimedFromDir = loadReclaimedFromDir;
19
+ // Pure, stateless helpers for the run registry — comparison, query matching,
20
+ // input digesting, counting, and small utilities. Carved out of run-registry.ts
21
+ // (FreeBSD-audit R2) so the stateful RunRegistry class no longer bundles the pure
22
+ // derivation layer. Nothing here touches `this`; everything is a pure function of
23
+ // its arguments (queueId is the lone exception — a process-local counter, kept as
24
+ // it was; making ID minting deterministic is a separate tracked item).
25
+ const node_fs_1 = __importDefault(require("node:fs"));
26
+ const node_path_1 = __importDefault(require("node:path"));
27
+ const compare_1 = require("../compare");
28
+ exports.LIFECYCLE_STATES = [
29
+ "queued",
30
+ "running",
31
+ "blocked",
32
+ "completed",
33
+ "failed",
34
+ "archived",
35
+ "reclaimed"
36
+ ];
37
+ function compareRecords(a, b) {
38
+ if (a.createdAt !== b.createdAt)
39
+ return a.createdAt < b.createdAt ? -1 : 1;
40
+ return (0, compare_1.compareBytes)(a.runId, b.runId);
41
+ }
42
+ function compareHistory(a, b) {
43
+ // Newest first.
44
+ if (a.createdAt !== b.createdAt)
45
+ return a.createdAt < b.createdAt ? 1 : -1;
46
+ return (0, compare_1.compareBytes)(a.runId, b.runId);
47
+ }
48
+ function compareQueue(a, b) {
49
+ if (a.priority !== b.priority)
50
+ return a.priority - b.priority;
51
+ if (a.enqueuedAt !== b.enqueuedAt)
52
+ return a.enqueuedAt < b.enqueuedAt ? -1 : 1;
53
+ return (0, compare_1.compareBytes)(a.id, b.id);
54
+ }
55
+ function matchesQuery(record, query) {
56
+ if (query.app && !(record.appId || record.workflowId || "").toLowerCase().includes(query.app))
57
+ return false;
58
+ if (query.status && record.lifecycle !== query.status && record.derivedLifecycle !== query.status)
59
+ return false;
60
+ if (query.repo && node_path_1.default.resolve(record.repo) !== query.repo)
61
+ return false;
62
+ if (query.since && record.createdAt < query.since)
63
+ return false;
64
+ if (query.until && record.createdAt > query.until)
65
+ return false;
66
+ if (query.text) {
67
+ const haystack = [
68
+ record.runId,
69
+ record.appId,
70
+ record.workflowId,
71
+ record.title,
72
+ record.repo,
73
+ record.lifecycle,
74
+ record.loopStage,
75
+ record.inputsDigest
76
+ ]
77
+ .filter(Boolean)
78
+ .join(" ")
79
+ .toLowerCase();
80
+ if (!haystack.includes(query.text))
81
+ return false;
82
+ }
83
+ return true;
84
+ }
85
+ /** Bounded, deterministic stringification of run inputs for free-text search.
86
+ * Descriptive intent keys (question, prompt, ...) come first so they survive
87
+ * truncation; the rest follow alphabetically. Deterministic and compact. */
88
+ const DIGEST_PRIORITY_KEYS = ["question", "prompt", "task", "summary", "title", "objective", "focus", "topic"];
89
+ /** Distinct execution backends used by a run's dispatches/tasks, recomputed from
90
+ * source state. Sorted; empty for pre-v0.1.29 / default-only runs that never
91
+ * recorded a backend. The registry stays backend-agnostic — this is metadata. */
92
+ function distinctBackends(run) {
93
+ const backends = new Set();
94
+ for (const dispatch of run.dispatches || []) {
95
+ if (dispatch.backendId)
96
+ backends.add(dispatch.backendId);
97
+ }
98
+ for (const task of run.tasks || []) {
99
+ if (task.backendId)
100
+ backends.add(task.backendId);
101
+ }
102
+ return [...backends].sort();
103
+ }
104
+ function digestInputs(inputs) {
105
+ if (!inputs || typeof inputs !== "object")
106
+ return undefined;
107
+ const keys = Object.keys(inputs);
108
+ const ordered = [
109
+ ...DIGEST_PRIORITY_KEYS.filter((k) => keys.includes(k)),
110
+ ...keys.filter((k) => !DIGEST_PRIORITY_KEYS.includes(k)).sort()
111
+ ];
112
+ const parts = [];
113
+ for (const key of ordered) {
114
+ const value = inputs[key];
115
+ if (value === undefined || value === null)
116
+ continue;
117
+ const rendered = Array.isArray(value) ? value.join(",") : typeof value === "object" ? JSON.stringify(value) : String(value);
118
+ parts.push(`${key}=${rendered}`);
119
+ }
120
+ const joined = parts.join(" ").replace(/\s+/g, " ").trim();
121
+ return joined.length > 360 ? `${joined.slice(0, 357)}...` : joined;
122
+ }
123
+ function countRecords(records) {
124
+ const counts = {
125
+ total: records.length,
126
+ queued: 0,
127
+ running: 0,
128
+ blocked: 0,
129
+ completed: 0,
130
+ failed: 0,
131
+ archived: 0,
132
+ reclaimed: 0
133
+ };
134
+ for (const record of records) {
135
+ counts[record.lifecycle] = (counts[record.lifecycle] || 0) + 1;
136
+ }
137
+ return counts;
138
+ }
139
+ function optionalLower(value) {
140
+ if (value === undefined || value === null || value === "")
141
+ return undefined;
142
+ return String(value).toLowerCase();
143
+ }
144
+ function clampInt(value, fallback, min) {
145
+ const n = Number(value);
146
+ if (!Number.isFinite(n))
147
+ return fallback;
148
+ return Math.max(min, Math.floor(n));
149
+ }
150
+ let queueCounter = 0;
151
+ function queueId() {
152
+ queueCounter += 1;
153
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
154
+ return `q-${stamp}-${String(queueCounter).padStart(3, "0")}`;
155
+ }
156
+ function isRunLifecycleState(value) {
157
+ return typeof value === "string" && exports.LIFECYCLE_STATES.includes(value);
158
+ }
159
+ /** Read a run dir's `reclaimed.json` overlay (v0.1.39). Fail-closed to an empty
160
+ * chain on absence/corruption — a malformed overlay must never brick the run. */
161
+ function loadReclaimedFromDir(runDir) {
162
+ const file = node_path_1.default.join(runDir, "reclaimed.json");
163
+ if (!node_fs_1.default.existsSync(file))
164
+ return { schemaVersion: 1, runId: "", tombstones: [] };
165
+ try {
166
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
167
+ return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
168
+ }
169
+ catch {
170
+ return { schemaVersion: 1, runId: "", tombstones: [] };
171
+ }
172
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatRegistryReport = formatRegistryReport;
4
+ exports.formatRunSearch = formatRunSearch;
5
+ exports.formatRunShow = formatRunShow;
6
+ exports.formatGcPlan = formatGcPlan;
7
+ exports.formatGcRun = formatGcRun;
8
+ exports.formatGcVerify = formatGcVerify;
9
+ exports.formatResume = formatResume;
10
+ exports.formatHistory = formatHistory;
11
+ exports.formatQueueList = formatQueueList;
12
+ function countsLine(counts) {
13
+ 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}`;
14
+ }
15
+ function recordLine(record) {
16
+ const flags = [record.archived ? "archived" : "", record.provenance?.rerunOf ? `rerunOf=${record.provenance.rerunOf}` : ""].filter(Boolean).join(" ");
17
+ return ` [${record.lifecycle}] ${record.runId} (${record.appId || record.workflowId}) ${record.loopStage}${flags ? ` {${flags}}` : ""}`;
18
+ }
19
+ function formatRegistryReport(report) {
20
+ const lines = [];
21
+ lines.push(`Run Registry (${report.scope}): ${report.root}`);
22
+ lines.push(`Freshness: ${report.freshness.status}${report.freshness.staleRuns.length ? ` (stale: ${report.freshness.staleRuns.join(", ")})` : ""}${report.freshness.missingRuns.length ? ` (missing: ${report.freshness.missingRuns.join(", ")})` : ""}`);
23
+ lines.push(`Repos: ${report.index.repos.length}`);
24
+ lines.push(countsLine(report.counts));
25
+ if (report.freshness.status !== "valid")
26
+ lines.push(`Next Action: ${report.nextAction}`);
27
+ return lines.join("\n");
28
+ }
29
+ function formatRunSearch(result) {
30
+ const lines = [];
31
+ lines.push(`Run Search (${result.scope}): ${result.total} match(es), showing ${result.records.length} [offset ${result.offset}] freshness=${result.freshness}`);
32
+ for (const record of result.records)
33
+ lines.push(recordLine(record));
34
+ if (!result.records.length)
35
+ lines.push(" (no matching runs)");
36
+ return lines.join("\n");
37
+ }
38
+ function formatRunShow(result) {
39
+ if (!result.found) {
40
+ return `Run ${result.runId}: MISSING (source state.json absent — fail closed). Next: ${result.nextAction}`;
41
+ }
42
+ const r = result.record;
43
+ const lines = [
44
+ `Run ${r.runId} [${r.lifecycle}] (derived: ${r.derivedLifecycle})`,
45
+ ` app=${r.appId || r.workflowId} loopStage=${r.loopStage} repo=${r.repo}`,
46
+ ` tasks: total=${r.tasks.total} pending=${r.tasks.pending} running=${r.tasks.running} failed=${r.tasks.failed} completed=${r.tasks.completed}`,
47
+ ` commits=${r.commitCount} (verifier-gated=${r.verifierGatedCommitCount}) openFeedback=${r.openFeedbackCount}`
48
+ ];
49
+ if (r.provenance?.rerunOf)
50
+ lines.push(` provenance: rerunOf=${r.provenance.rerunOf} gen=${r.provenance.generation} origin=${r.provenance.originRunId}`);
51
+ if (r.tier && r.tier !== "live") {
52
+ lines.push(` tier=${r.tier} capability=${r.capability} reason=${r.capabilityReason}${r.reclaimedBytes ? ` bytesFreed=${r.reclaimedBytes}` : ""}${r.tombstoneHash ? ` tombstone=${r.tombstoneHash.slice(0, 19)}` : ""}`);
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+ function formatGcPlan(result) {
57
+ const lines = [
58
+ `GC Plan (${result.scope}): ${result.eligibleCount}/${result.total} eligible, ${result.bytesToFree} byte(s) would be freed [DRY-RUN, frees nothing]`,
59
+ ` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots}`
60
+ ];
61
+ for (const entry of result.entries) {
62
+ if (entry.eligible) {
63
+ const kinds = Object.entries(entry.byKind).map(([k, v]) => `${k}=${v}`).join(" ");
64
+ lines.push(` [eligible] ${entry.runId} -> ${entry.capability} (${entry.capabilityReason}) ${entry.bytesToFree}B {${kinds}}`);
65
+ }
66
+ else {
67
+ lines.push(` [skip:${entry.reason}] ${entry.runId} (tier=${entry.tier})`);
68
+ }
69
+ }
70
+ if (!result.entries.length)
71
+ lines.push(" (no runs in scope)");
72
+ return lines.join("\n");
73
+ }
74
+ function formatGcRun(result) {
75
+ const lines = [`GC Run (${result.scope}): reclaimed ${result.reclaimed.length} run(s), freed ${result.totalBytesFreed} byte(s)`];
76
+ for (const r of result.reclaimed)
77
+ lines.push(` [reclaimed] ${r.runId} -> ${r.capability} (${r.capabilityReason}) ${r.bytesFreed}B tombstone=${r.tombstoneHash.slice(0, 19)}`);
78
+ for (const r of result.refused)
79
+ lines.push(` [refused:${r.code}] ${r.runId}`);
80
+ if (!result.reclaimed.length && !result.refused.length)
81
+ lines.push(" (nothing eligible)");
82
+ return lines.join("\n");
83
+ }
84
+ function formatGcVerify(result) {
85
+ const lines = [
86
+ `GC Verify ${result.runId}: reclaimed=${result.reclaimed} verified=${result.verified} tier=${result.tier} capability=${result.capability}${result.tombstoneHash ? ` tombstone=${result.tombstoneHash.slice(0, 19)}` : ""}`
87
+ ];
88
+ for (const check of result.checks)
89
+ lines.push(` ${check.pass ? "PASS" : "FAIL"} ${check.name}${check.code ? ` [${check.code}]` : ""}${check.detail ? ` (${check.detail})` : ""}`);
90
+ return lines.join("\n");
91
+ }
92
+ function formatResume(result) {
93
+ const lines = [
94
+ `Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
95
+ ` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`
96
+ ];
97
+ for (const action of result.nextActions)
98
+ lines.push(` -> ${action.command}\n ${action.reason}`);
99
+ // Only when --drive/--once continued the run; the default read-only resume text is unchanged.
100
+ if (result.drive) {
101
+ const d = result.drive;
102
+ lines.push(` drive: ${d.status} (${d.completedWorkers}/${d.plannedWorkers} workers${d.commitId ? `, committed ${d.commitId}` : ""})`);
103
+ }
104
+ return lines.join("\n");
105
+ }
106
+ function formatHistory(result) {
107
+ const lines = [];
108
+ lines.push(`Run History (${result.scope}): ${result.total} run(s) across ${result.repos.length} repo(s), freshness=${result.freshness}`);
109
+ for (const entry of result.entries) {
110
+ lines.push(` ${entry.createdAt} [${entry.lifecycle}] ${entry.runId} (${entry.appId || entry.workflowId})${entry.provenance?.rerunOf ? ` rerunOf=${entry.provenance.rerunOf}` : ""}`);
111
+ }
112
+ if (!result.entries.length)
113
+ lines.push(" (no runs)");
114
+ return lines.join("\n");
115
+ }
116
+ function formatQueueList(result) {
117
+ const lines = [`Run Queue: ${result.total} entry(ies) [priority asc]`];
118
+ for (const entry of result.entries) {
119
+ lines.push(` #${entry.priority} ${entry.id} [${entry.status}] ${entry.appId || entry.workflowId || entry.runId || "?"} repo=${entry.repo}${entry.note ? ` note=${entry.note}` : ""}`);
120
+ }
121
+ if (!result.entries.length)
122
+ lines.push(" (queue empty)");
123
+ return lines.join("\n");
124
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reclamationPolicy = reclamationPolicy;
4
+ exports.reclaimEligibility = reclaimEligibility;
5
+ exports.gcPlan = gcPlan;
6
+ exports.gcRun = gcRun;
7
+ exports.gcVerify = gcVerify;
8
+ const reclamation_1 = require("../reclamation");
9
+ const trust_audit_1 = require("../trust-audit");
10
+ const policy_1 = require("./policy");
11
+ /** Resolve the effective reclamation policy (defaults reclaim NOTHING). */
12
+ function reclamationPolicy(overrides = {}) {
13
+ return { ...policy_1.DEFAULT_RUN_REGISTRY_POLICY, ...overrides };
14
+ }
15
+ /** Fail-closed eligibility: terminal AND archived AND no open feedback AND past
16
+ * retention. Returns the matching refusal code, or null when eligible. Reads
17
+ * the live-source-derived record; order yields distinct, stable codes. */
18
+ function reclaimEligibility(record, policy, nowMs) {
19
+ if (record.tier === "reclaimed")
20
+ return "already-reclaimed";
21
+ const terminalStates = policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"];
22
+ if (record.derivedLifecycle !== "completed" && record.derivedLifecycle !== "failed")
23
+ return "non-terminal";
24
+ if (!terminalStates.includes(record.derivedLifecycle))
25
+ return "non-terminal";
26
+ if (record.openFeedbackCount > 0)
27
+ return "open-feedback";
28
+ if (!record.archived)
29
+ return "not-archived";
30
+ const days = policy.reclaimAfterArchiveDays ?? 0;
31
+ if (days > 0) {
32
+ const archivedAtMs = record.archivedAt ? Date.parse(record.archivedAt) : NaN;
33
+ if (!Number.isFinite(archivedAtMs))
34
+ return "within-retention";
35
+ if (archivedAtMs > nowMs - days * 24 * 60 * 60 * 1000)
36
+ return "within-retention";
37
+ }
38
+ return null;
39
+ }
40
+ /** Resolve a single run to a one-element record list via locate() (repo-first),
41
+ * avoiding a full-registry scan for single-run gc plan/run. */
42
+ function recordsForRunId(host, runId, scope) {
43
+ const located = host.locate(runId, scope);
44
+ return located ? [located.record] : [];
45
+ }
46
+ /** Dry-run: compute eligible runs, per-kind bytes that WOULD be freed, and the
47
+ * capability downgrade. Frees NOTHING. */
48
+ function gcPlan(host, options = {}) {
49
+ const scope = options.scope || "home";
50
+ const policy = reclamationPolicy(options.policy);
51
+ const nowIso = options.now || new Date().toISOString();
52
+ const nowMs = Date.parse(nowIso);
53
+ // Fast, deterministic single-run path: resolve just that run via locate()
54
+ // (repo-first) so a home-scope plan never re-scans the whole registry.
55
+ const records = options.runId ? recordsForRunId(host, options.runId, scope) : host.buildIndex(scope).records;
56
+ const entries = [];
57
+ let bytesToFree = 0;
58
+ let eligibleCount = 0;
59
+ for (const record of records) {
60
+ const refusal = reclaimEligibility(record, policy, nowMs);
61
+ let plan;
62
+ try {
63
+ const run = host.loadRun(record.repo, record.runId);
64
+ plan = (0, reclamation_1.planReclamation)(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots });
65
+ }
66
+ catch {
67
+ entries.push({
68
+ runId: record.runId,
69
+ repo: record.repo,
70
+ eligible: false,
71
+ reason: "unreadable",
72
+ tier: record.tier || "live",
73
+ capability: record.capability || "re-runnable",
74
+ capabilityReason: record.capabilityReason || "live-full",
75
+ bytesToFree: 0,
76
+ byKind: {},
77
+ freeable: []
78
+ });
79
+ continue;
80
+ }
81
+ const eligible = refusal === null;
82
+ const entry = {
83
+ runId: record.runId,
84
+ repo: record.repo,
85
+ eligible,
86
+ reason: eligible ? "eligible" : refusal,
87
+ tier: record.tier || "live",
88
+ capability: plan.capability,
89
+ capabilityReason: plan.capabilityReason,
90
+ bytesToFree: eligible ? plan.bytesToFree : 0,
91
+ byKind: eligible ? plan.byKind : {},
92
+ freeable: eligible ? plan.freeable.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes })) : []
93
+ };
94
+ entries.push(entry);
95
+ if (eligible) {
96
+ eligibleCount += 1;
97
+ bytesToFree += plan.bytesToFree;
98
+ }
99
+ }
100
+ return {
101
+ schemaVersion: 1,
102
+ scope,
103
+ generatedAt: nowIso,
104
+ policy: {
105
+ reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays ?? 0,
106
+ keepSnapshots: Boolean(policy.keepSnapshots),
107
+ keepScratch: Boolean(policy.keepScratch),
108
+ reclaimStates: policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"]
109
+ },
110
+ total: entries.length,
111
+ eligibleCount,
112
+ bytesToFree,
113
+ entries,
114
+ nextAction: eligibleCount ? "node scripts/cw.js gc run" : "node scripts/cw.js run search"
115
+ };
116
+ }
117
+ /** Execute the write-ahead reclamation transaction for eligible runs. Bounded
118
+ * (`maxReclaimRuns` / `maxReclaimBytes`), fail-closed on any incomplete
119
+ * skeleton. Produces a tombstone and frees the bulk. */
120
+ function gcRun(host, options = {}) {
121
+ const scope = options.scope || "home";
122
+ const policy = reclamationPolicy(options.policy);
123
+ const nowIso = options.now || new Date().toISOString();
124
+ const nowMs = Date.parse(nowIso);
125
+ const records = options.runId ? recordsForRunId(host, options.runId, scope) : host.buildIndex(scope).records;
126
+ const maxRuns = options.limit ?? (policy.maxReclaimRuns || 0);
127
+ const maxBytes = policy.maxReclaimBytes || 0;
128
+ const reclaimed = [];
129
+ const refused = [];
130
+ let totalBytesFreed = 0;
131
+ for (const record of records) {
132
+ const refusal = reclaimEligibility(record, policy, nowMs);
133
+ if (refusal) {
134
+ refused.push({ runId: record.runId, code: refusal });
135
+ continue;
136
+ }
137
+ if (maxRuns > 0 && reclaimed.length >= maxRuns)
138
+ break;
139
+ let run;
140
+ try {
141
+ run = host.loadRun(record.repo, record.runId);
142
+ }
143
+ catch {
144
+ refused.push({ runId: record.runId, code: "unreadable" });
145
+ continue;
146
+ }
147
+ try {
148
+ const result = (0, reclamation_1.runReclamation)(run, {
149
+ now: nowIso,
150
+ actor: options.actor,
151
+ policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
152
+ reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots }
153
+ });
154
+ // No post-free saveCheckpoint: runReclamation now DURABLY persists the
155
+ // result-node re-point inside the transaction (before any byte is freed),
156
+ // so state.json can never reference a freed path even on a crash here.
157
+ reclaimed.push({
158
+ runId: record.runId,
159
+ bytesFreed: result.bytesFreed,
160
+ tombstoneHash: result.tombstone.tombstoneHash,
161
+ capability: result.tombstone.capability,
162
+ capabilityReason: result.tombstone.capabilityReason
163
+ });
164
+ // Independent reclamation WITNESS in the tamper-evident trust-audit chain:
165
+ // proves this run WAS reclaimed even if reclaimed.json is later deleted — so
166
+ // `gc verify` can tell proof-deletion apart from never-reclaimed.
167
+ (0, trust_audit_1.recordTrustAuditEvent)(run, {
168
+ kind: "run.reclaimed",
169
+ decision: "recorded",
170
+ source: "cw-validated",
171
+ metadata: { tombstoneHash: result.tombstone.tombstoneHash, bytesFreed: result.bytesFreed, capability: result.tombstone.capability }
172
+ });
173
+ totalBytesFreed += result.bytesFreed;
174
+ if (maxBytes > 0 && totalBytesFreed >= maxBytes)
175
+ break;
176
+ }
177
+ catch (error) {
178
+ if (error instanceof reclamation_1.ReclamationError)
179
+ refused.push({ runId: record.runId, code: error.code });
180
+ else
181
+ throw error;
182
+ }
183
+ }
184
+ return {
185
+ schemaVersion: 1,
186
+ scope,
187
+ generatedAt: nowIso,
188
+ dryRun: false,
189
+ reclaimed,
190
+ refused,
191
+ totalBytesFreed,
192
+ nextAction: reclaimed.length ? "node scripts/cw.js gc verify <run-id>" : "node scripts/cw.js gc plan"
193
+ };
194
+ }
195
+ /** Re-prove a reclaimed run: skeleton schema-complete, tombstone chain
196
+ * recomputed-and-untampered, each reconstructable artifact re-derived from its
197
+ * RETAINED inputs to its expectDigest, and eligible-when-reclaimed. */
198
+ function gcVerify(host, runId, options = {}) {
199
+ const scope = options.scope || "home";
200
+ const located = host.locate(runId, scope);
201
+ if (!located) {
202
+ return {
203
+ schemaVersion: 1,
204
+ runId,
205
+ reclaimed: false,
206
+ verified: false,
207
+ tier: "live",
208
+ capability: "re-runnable",
209
+ chainLength: 0,
210
+ checks: [{ name: "located", pass: false, code: "not-reclaimed", detail: "run source not found" }],
211
+ nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
212
+ };
213
+ }
214
+ const run = host.loadRun(located.record.repo, runId);
215
+ const result = (0, reclamation_1.verifyReclamation)(run);
216
+ const checks = result.checks.map((c) => ({ name: c.name, pass: c.pass, code: c.code, detail: c.detail }));
217
+ // Eligible-when-reclaimed: each tombstone must have sealed a terminal verdict.
218
+ let eligibleWhenReclaimed = result.reclaimed;
219
+ for (const tombstone of result.tombstones) {
220
+ const terminal = tombstone.skeleton.finalVerdict?.terminal === true;
221
+ if (!terminal) {
222
+ eligibleWhenReclaimed = false;
223
+ checks.push({ name: `eligible-when-reclaimed:${tombstone.tombstoneId}`, pass: false, code: "ineligible-when-reclaimed", detail: "non-terminal verdict sealed" });
224
+ }
225
+ }
226
+ const last = result.tombstones[result.tombstones.length - 1];
227
+ // Independent witness: a trust-audit "run.reclaimed" event proves this run was
228
+ // reclaimed even if reclaimed.json was deleted. A present witness + missing proof
229
+ // = the proof was deleted/tampered (NOT "never reclaimed") — fail closed so
230
+ // `gc verify <run> && deploy` cannot pass on a wiped reclamation record.
231
+ const witnessed = (0, trust_audit_1.listTrustAuditEvents)(run).some((event) => event.kind === "run.reclaimed");
232
+ const proofDeleted = witnessed && !result.reclaimed;
233
+ if (proofDeleted) {
234
+ checks.push({ name: "reclaim-witness", pass: false, code: "reclaim-proof-deleted", detail: "trust-audit attests reclamation but reclaimed.json is missing/empty" });
235
+ }
236
+ const reclaimed = result.reclaimed || proofDeleted;
237
+ const verified = result.verified && eligibleWhenReclaimed && !proofDeleted;
238
+ return {
239
+ schemaVersion: 1,
240
+ runId,
241
+ reclaimed,
242
+ verified,
243
+ tier: located.record.tier || (reclaimed ? "reclaimed" : "live"),
244
+ capability: located.record.capability || "re-runnable",
245
+ capabilityReason: located.record.capabilityReason,
246
+ tombstoneHash: last?.tombstoneHash,
247
+ chainLength: result.tombstones.length,
248
+ checks,
249
+ nextAction: verified ? "node scripts/cw.js run show " + runId : "node scripts/cw.js gc plan"
250
+ };
251
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_RUN_REGISTRY_POLICY = exports.RUN_REGISTRY_SCHEMA_VERSION = void 0;
4
+ exports.RUN_REGISTRY_SCHEMA_VERSION = 1;
5
+ exports.DEFAULT_RUN_REGISTRY_POLICY = {
6
+ schemaVersion: 1,
7
+ archiveOlderThanDays: 0,
8
+ archiveStates: ["completed", "failed"],
9
+ defaultQueuePriority: 100,
10
+ reclaimAfterArchiveDays: 0,
11
+ reclaimStates: ["completed", "failed"],
12
+ keepSnapshots: false,
13
+ keepScratch: false,
14
+ maxReclaimRuns: 0,
15
+ maxReclaimBytes: 0
16
+ };
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.queueFilePath = queueFilePath;
7
+ exports.loadQueue = loadQueue;
8
+ exports.saveQueue = saveQueue;
9
+ exports.queueAdd = queueAdd;
10
+ exports.queueList = queueList;
11
+ exports.queueShow = queueShow;
12
+ exports.queueDrain = queueDrain;
13
+ // Durable run-queue operations for the run registry (FreeBSD-audit R2 deep).
14
+ // Carved out of run-registry.ts so the RunRegistry class no longer bundles the
15
+ // stateful queue cluster; the class keeps the public methods as thin delegators.
16
+ //
17
+ // BEHAVIOR-PRESERVING — pure code movement, zero logic change. Each function
18
+ // takes a `QueueHost` (the registry, narrowed to exactly the file-access +
19
+ // repo-registration helpers the queue needs) so it stays a function of its
20
+ // inputs, matching the existing router pattern (orchestrator/*-operations.ts,
21
+ // run-registry/derive.ts + format.ts).
22
+ //
23
+ // The queue file lives beside the other home-registry plain files (EXPLICIT,
24
+ // INSPECTABLE STATE): readable, diffable, no hidden database. Cross-process
25
+ // read-modify-write is locked (v0.1.40, P1-D) so a concurrent add/drain can
26
+ // never drop or double-drain an entry.
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_fs_1 = __importDefault(require("node:fs"));
29
+ const state_1 = require("../state");
30
+ const derive_1 = require("./derive");
31
+ function queueFilePath(host) {
32
+ return node_path_1.default.join(host.homeRegistryDir(), "queue.json");
33
+ }
34
+ function loadQueue(host) {
35
+ const file = queueFilePath(host);
36
+ if (!node_fs_1.default.existsSync(file))
37
+ return [];
38
+ try {
39
+ const parsed = (0, state_1.readJson)(file);
40
+ return Array.isArray(parsed.entries) ? parsed.entries : [];
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ function saveQueue(host, entries) {
47
+ (0, state_1.writeJson)(queueFilePath(host), { schemaVersion: 1, entries }, { durable: true });
48
+ }
49
+ function queueAdd(host, options = {}) {
50
+ const repo = options.repo ? node_path_1.default.resolve(options.repo) : host.repoRoot;
51
+ // Cross-process read-modify-write on the home queue: lock so a concurrently
52
+ // added task can never vanish (v0.1.40, P1-D).
53
+ return (0, state_1.withFileLock)(queueFilePath(host), () => {
54
+ const entries = loadQueue(host);
55
+ const entry = {
56
+ schemaVersion: 1,
57
+ id: options.id || (0, derive_1.queueId)(),
58
+ runId: options.runId,
59
+ appId: options.appId,
60
+ workflowId: options.workflowId,
61
+ repo,
62
+ priority: Number.isFinite(options.priority) ? Number(options.priority) : host.defaultQueuePriority,
63
+ enqueuedAt: new Date().toISOString(),
64
+ status: "pending",
65
+ inputs: options.inputs,
66
+ note: options.note
67
+ };
68
+ entries.push(entry);
69
+ host.registerRepo(repo);
70
+ saveQueue(host, entries);
71
+ return entry;
72
+ });
73
+ }
74
+ function queueList(host, options = {}) {
75
+ let entries = loadQueue(host);
76
+ if (options.status)
77
+ entries = entries.filter((e) => e.status === options.status);
78
+ if (options.repo) {
79
+ const repo = node_path_1.default.resolve(options.repo);
80
+ entries = entries.filter((e) => node_path_1.default.resolve(e.repo) === repo);
81
+ }
82
+ entries = [...entries].sort(derive_1.compareQueue);
83
+ return { schemaVersion: 1, total: entries.length, entries };
84
+ }
85
+ function queueShow(host, id) {
86
+ const entry = loadQueue(host).find((e) => e.id === id);
87
+ if (!entry)
88
+ throw new Error(`Queue entry not found: ${id}`);
89
+ return entry;
90
+ }
91
+ /** Drain the next N ready/pending entries in policy order, marking them drained.
92
+ * CW records readiness/order; the HOST still executes the workers. */
93
+ function queueDrain(host, options = {}) {
94
+ const limit = (0, derive_1.clampInt)(options.limit, 1, 1);
95
+ const repoFilter = options.repo ? node_path_1.default.resolve(options.repo) : undefined;
96
+ // Lock the drain RMW so two hosts can never double-drain the same entry
97
+ // (v0.1.40, P1-D — the scheduling kernel's concurrency ceiling now holds
98
+ // across processes, not just within one).
99
+ return (0, state_1.withFileLock)(queueFilePath(host), () => {
100
+ const entries = loadQueue(host);
101
+ const drainable = entries
102
+ .filter((e) => e.status === "pending" || e.status === "ready")
103
+ .filter((e) => !repoFilter || node_path_1.default.resolve(e.repo) === repoFilter)
104
+ .sort(derive_1.compareQueue);
105
+ const drained = [];
106
+ const drainedAt = new Date().toISOString();
107
+ for (const entry of drainable.slice(0, limit)) {
108
+ entry.status = "drained";
109
+ entry.drainedAt = drainedAt;
110
+ drained.push(entry);
111
+ }
112
+ saveQueue(host, entries);
113
+ const remaining = entries.filter((e) => e.status === "pending" || e.status === "ready").length;
114
+ return { schemaVersion: 1, drained, remaining };
115
+ });
116
+ }