cool-workflow 0.1.80 → 0.1.82
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 +62 -48
- package/dist/capability-core.js +152 -96
- package/dist/capability-registry.js +160 -174
- package/dist/cli.js +90 -100
- package/dist/collaboration.js +5 -6
- package/dist/commit.js +232 -209
- 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 +126 -0
- package/dist/coordinator.js +127 -300
- package/dist/dispatch.js +46 -3
- package/dist/drive.js +7 -7
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +5 -2
- package/dist/execution-backend/agent.js +294 -0
- package/dist/execution-backend/probes.js +96 -0
- package/dist/execution-backend/util.js +47 -0
- package/dist/execution-backend.js +77 -450
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +34 -173
- package/dist/multi-agent/graph.js +84 -0
- package/dist/multi-agent/helpers.js +141 -0
- package/dist/multi-agent/ids.js +20 -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 +63 -242
- package/dist/multi-agent-host.js +53 -40
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/multi-agent.js +125 -314
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +11 -21
- 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 +22 -1
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +41 -10
- package/dist/reclamation.js +32 -55
- 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/scheduler.js +34 -4
- 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 +50 -12
- package/dist/triggers.js +33 -14
- package/dist/trust-audit.js +215 -71
- package/dist/validation.js +328 -0
- 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 +182 -173
- package/docs/agent-delegation-drive.7.md +14 -0
- package/docs/cli-mcp-parity.7.md +5 -0
- package/docs/contract-migration-tooling.7.md +3 -0
- package/docs/control-plane-scheduling.7.md +3 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +5 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +3 -0
- package/docs/execution-backends.7.md +3 -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 +5 -0
- package/docs/multi-agent-eval-replay-harness.7.md +3 -0
- package/docs/multi-agent-operator-ux.7.md +3 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +3 -0
- package/docs/observability-cost-accounting.7.md +3 -0
- package/docs/project-index.md +23 -6
- package/docs/real-execution-backends.7.md +3 -0
- package/docs/release-and-migration.7.md +5 -0
- package/docs/release-tooling.7.md +35 -2
- package/docs/run-registry-control-plane.7.md +55 -8
- package/docs/run-retention-reclamation.7.md +5 -0
- package/docs/state-explosion-management.7.md +3 -0
- package/docs/team-collaboration.7.md +3 -0
- package/docs/trust-model.md +267 -0
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +3 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.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/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -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/release-flow.js +181 -5
- package/scripts/version-sync-check.js +5 -8
- package/dist/capability-dispatcher.js +0 -86
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
3
|
"description": "Auditable workflow control-plane and orchestration runtime: TypeScript dispatch, evidence-gated verification, state commits, scheduling, routines, multi-agent coordination, and MCP. Delegates execution to external agents — never runs models.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.82",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.82",
|
|
4
4
|
"description": "Auditable workflow control-plane and orchestration runtime: TypeScript dispatch, evidence-gated verification, state commits, scheduling, routines, multi-agent coordination, and MCP. Delegates execution to external agents — never runs models.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
package/README.md
CHANGED
|
@@ -263,6 +263,40 @@ write paths, command execution, network access, and environment exposure. CW
|
|
|
263
263
|
stores and validates the policy, while the agent host enforces OS/process
|
|
264
264
|
runtime controls. See [docs/sandbox-profiles.7.md](docs/sandbox-profiles.7.md).
|
|
265
265
|
|
|
266
|
+
## Quickstart
|
|
267
|
+
|
|
268
|
+
**30-second proof, no install** — see that a recorded telemetry verdict can't be forged:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npx cool-workflow demo tamper
|
|
272
|
+
# builds a signed ed25519 ledger, forges it 2 ways, both caught offline
|
|
273
|
+
# -> VERDICT: tamper-evidence holds ✓
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Try a real run** — no clone needed; drive an architecture review with your own agent:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npx cool-workflow quickstart architecture-review --repo /path/to/repo \
|
|
280
|
+
--question "Is this architecture sound?" --agent-command builtin:claude
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
CW DELEGATES worker execution to your own agent. With no `--agent-command` (or
|
|
284
|
+
`CW_AGENT_COMMAND`) the drive fails closed (status `blocked`) — it never fabricates a
|
|
285
|
+
result. `--agent-command builtin:claude` resolves to a bundled read-only `claude -p`
|
|
286
|
+
wrapper (needs `claude` on your PATH).
|
|
287
|
+
|
|
288
|
+
**Re-prove a finished run, offline** (`cw` is the installed bin; or `npx cool-workflow <cmd>`):
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
cw telemetry verify <run-id> # re-checks the hash-chained ledger
|
|
292
|
+
cw telemetry verify <run-id> --pubkey pub.pem # also re-runs ed25519 signature checks
|
|
293
|
+
cw audit verify <run-id> # re-proves the trust-audit hash chain
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
More: `cw quickstart <app> --preview` (read-only dry run), `cw run resume <run-id> --drive`
|
|
297
|
+
(continue an interrupted run), `cw run inspect-archive <archive>` (integrity-check a
|
|
298
|
+
portable run archive without importing it).
|
|
299
|
+
|
|
266
300
|
## Structure
|
|
267
301
|
|
|
268
302
|
```text
|
|
@@ -300,6 +334,10 @@ cool-workflow
|
|
|
300
334
|
|
|
301
335
|
## Commands
|
|
302
336
|
|
|
337
|
+
Installed via npm, the bin is `cw` (alias `cool-workflow`): e.g. `cw list`,
|
|
338
|
+
`cw quickstart …`. From a cloned source checkout, before `npm run build`, use the
|
|
339
|
+
equivalent `node scripts/cw.js <cmd>` form shown in the examples below.
|
|
340
|
+
|
|
303
341
|
List bundled workflows:
|
|
304
342
|
|
|
305
343
|
```bash
|
|
@@ -619,7 +657,7 @@ Replaces the linear migration chain with a BFS graph path resolver (`findMigrati
|
|
|
619
657
|
|
|
620
658
|
## Vendor-Adapter Registry (v0.1.47)
|
|
621
659
|
|
|
622
|
-
Data-driven manifest generation: vendor JSON shapes extracted from `gen-manifests.js` into declarative templates in `plugin.manifest.json`. A `_resolveTemplate()` engine resolves `{{path.to.field}}` markers. Adding a new AI platform is pure data.
|
|
660
|
+
Data-driven manifest generation: vendor JSON shapes extracted from `gen-manifests.js` into declarative templates in `plugin.manifest.json`. A `_resolveTemplate()` engine resolves `{{path.to.field}}` markers. Adding a new AI platform is pure data. Cross-vendor is proven by boot, not just by generation: `npm run manifest:load-check` (`node test/vendor-manifest-load-smoke.js`) loads every generated manifest (claude, codex, agents, gemini, opencode) and asserts each exposes the full tool surface (184 tools).
|
|
623
661
|
|
|
624
662
|
## P2 Fixes (v0.1.48)
|
|
625
663
|
|
|
@@ -651,7 +689,7 @@ The orchestration vision landed in one release, all reviewer-gated:
|
|
|
651
689
|
|
|
652
690
|
## Tamper-evidence demo (v0.1.79)
|
|
653
691
|
|
|
654
|
-
`cw demo tamper` — a hermetic, one-command proof that a recorded telemetry verdict cannot be forged undetected: it builds a real ed25519-signed ledger, forges it at the ledger layer (verdict flip + recomputed local hash → the chain still breaks) and the signature layer (inflated tokens, reused signature → ed25519 rejects), all verified offline with only the public key. `cw telemetry verify <run>` is the operator-facing
|
|
692
|
+
`cw demo tamper` — a hermetic, one-command proof that a recorded telemetry verdict cannot be forged undetected: it builds a real ed25519-signed ledger, forges it at the ledger layer (verdict flip + recomputed local hash → the chain still breaks) and the signature layer (inflated tokens, reused signature → ed25519 rejects), all verified offline with only the public key. `cw telemetry verify <run>` (`cw_telemetry_verify` on MCP) is the operator-facing re-proof: by default it recomputes the hash chain on disk so any later edit to a recorded verdict or usage digest is caught; add `--pubkey <pem-or-path>` to re-run each `attested` hop's ed25519 signature check against the stored raw usage too. What this does and does **not** prove — including the single-keyholder ceiling — is documented honestly in [Trust Model & Limitations](docs/trust-model.md); read it before relying on a green verdict.
|
|
655
693
|
|
|
656
694
|
## Opt-in live agent output during a drive (on main, ships next)
|
|
657
695
|
|
|
@@ -662,3 +700,5 @@ v0.1.79
|
|
|
662
700
|
## Fast Architecture Review (v0.1.80)
|
|
663
701
|
|
|
664
702
|
Adds the opt-in fast architecture-review lane: scoped JSONL source contexts, diff-aware exports, reusable Map and Assess results, measurable wrapper metrics, actionable background full-review handoff, and userland model policy flags for routing fast/strong workers without changing the full review contract.
|
|
703
|
+
|
|
704
|
+
_This documentation tracks Cool Workflow v0.1.82. See [CHANGELOG](../../CHANGELOG.md) for the release notes._
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review",
|
|
4
4
|
"title": "Architecture Review",
|
|
5
5
|
"summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.82",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review-fast",
|
|
4
4
|
"title": "Architecture Review Fast",
|
|
5
5
|
"summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.82",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.82",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.82",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.82",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
package/dist/agent-config.js
CHANGED
|
@@ -139,18 +139,30 @@ function agentConfigFromArgs(args) {
|
|
|
139
139
|
// npx/global install, where $(pwd)-relative paths don't exist) can configure a
|
|
140
140
|
// WORKING agent without knowing where the package landed on disk:
|
|
141
141
|
// --agent-command builtin:claude (or CW_AGENT_COMMAND=builtin:claude)
|
|
142
|
-
// resolves to the packaged
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
// resolves to the packaged wrapper invocation. Still pure config — the template
|
|
143
|
+
// is an out-of-process delegation script; CW never calls a model API.
|
|
144
|
+
//
|
|
145
|
+
// The builtin set is DATA, not a kernel TS literal (FreeBSD-audit L15): it lives
|
|
146
|
+
// in scripts/agents/builtin-templates.json (vendor name -> wrapper script name).
|
|
147
|
+
// Adding a vendor is a content/distribution step (drop a wrapper + a JSON line),
|
|
148
|
+
// not a kernel edit — keeping CW vendor-agnostic at the source level.
|
|
149
|
+
function builtinAgentTemplates() {
|
|
150
|
+
const agentsDir = node_path_1.default.join(__dirname, "..", "scripts", "agents");
|
|
151
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(agentsDir, "builtin-templates.json"), "utf8"));
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [name, script] of Object.entries(manifest.templates || {})) {
|
|
154
|
+
out[name] = `node ${node_path_1.default.join(agentsDir, script)} {{input}} {{result}}`;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
147
158
|
function expandBuiltinAgentCommand(command) {
|
|
148
159
|
if (!command || !command.startsWith("builtin:"))
|
|
149
160
|
return command;
|
|
150
161
|
const name = command.slice("builtin:".length).trim();
|
|
151
|
-
const
|
|
162
|
+
const templates = builtinAgentTemplates();
|
|
163
|
+
const template = templates[name];
|
|
152
164
|
if (!template) {
|
|
153
|
-
throw new Error(`Unknown builtin agent template "${name}" — available: ${Object.keys(
|
|
165
|
+
throw new Error(`Unknown builtin agent template "${name}" — available: ${Object.keys(templates).join(", ")}`);
|
|
154
166
|
}
|
|
155
167
|
return template;
|
|
156
168
|
}
|
|
@@ -200,6 +212,8 @@ function setAgentConfigFile(patch, env = process.env) {
|
|
|
200
212
|
endpoint: firstDefined(incoming.endpoint, current.endpoint),
|
|
201
213
|
model: firstDefined(incoming.model, current.model),
|
|
202
214
|
timeoutMs: firstDefined(incoming.timeoutMs, current.timeoutMs),
|
|
215
|
+
attestPublicKey: firstDefined(incoming.attestPublicKey, current.attestPublicKey),
|
|
216
|
+
requireAttestedTelemetry: firstDefined(incoming.requireAttestedTelemetry, current.requireAttestedTelemetry),
|
|
203
217
|
source: "file"
|
|
204
218
|
};
|
|
205
219
|
const stored = redacted(merged);
|
|
@@ -20,7 +20,16 @@ const state_1 = require("./state");
|
|
|
20
20
|
const state_node_1 = require("./state-node");
|
|
21
21
|
const trust_audit_1 = require("./trust-audit");
|
|
22
22
|
const collaboration_1 = require("./collaboration");
|
|
23
|
+
const compare_1 = require("./compare");
|
|
24
|
+
const gates_1 = require("./gates");
|
|
25
|
+
const validation_1 = require("./validation");
|
|
23
26
|
exports.CANDIDATE_SCHEMA_VERSION = 1;
|
|
27
|
+
/** Verdict thresholds on a score's normalized value [0,1], declared once so the
|
|
28
|
+
* numbers carry intent instead of being buried as literals in verdictFor(). A
|
|
29
|
+
* normalized score at-or-above PASS is "pass"; at-or-above WARN (but below
|
|
30
|
+
* PASS) is "warn"; anything lower is "fail". Same numbers as before. */
|
|
31
|
+
const VERDICT_PASS_THRESHOLD = 0.7;
|
|
32
|
+
const VERDICT_WARN_THRESHOLD = 0.4;
|
|
24
33
|
function createCandidateScoring(options = {}) {
|
|
25
34
|
return {
|
|
26
35
|
registerCandidate: (run, input) => registerCandidate(run, input, options),
|
|
@@ -39,7 +48,7 @@ function registerCandidate(run, input, options = {}) {
|
|
|
39
48
|
if (existing)
|
|
40
49
|
return existing;
|
|
41
50
|
const now = new Date().toISOString();
|
|
42
|
-
const id = input.id || createCandidateId(input.kind || "manual", input.workerId || input.taskId || input.resultNodeId);
|
|
51
|
+
const id = input.id || createCandidateId(run, input.kind || "manual", input.workerId || input.taskId || input.resultNodeId);
|
|
43
52
|
const candidate = {
|
|
44
53
|
schemaVersion: exports.CANDIDATE_SCHEMA_VERSION,
|
|
45
54
|
id,
|
|
@@ -103,13 +112,16 @@ function getCandidate(run, candidateId) {
|
|
|
103
112
|
const file = candidateFile(run, candidateId);
|
|
104
113
|
if (!node_fs_1.default.existsSync(file))
|
|
105
114
|
return undefined;
|
|
106
|
-
|
|
115
|
+
// Fail-closed integrity boundary (F4/F5): validate the parsed record against
|
|
116
|
+
// its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
|
|
117
|
+
// forged candidate.json must throw here rather than flow into the run.
|
|
118
|
+
const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
107
119
|
upsertCandidate(run, candidate);
|
|
108
120
|
return candidate;
|
|
109
121
|
}
|
|
110
122
|
function scoreCandidate(run, candidateId, input, options = {}) {
|
|
111
123
|
const candidate = requireCandidate(run, candidateId);
|
|
112
|
-
const scoreId = input.id || createScoreId(
|
|
124
|
+
const scoreId = input.id || createScoreId(candidate);
|
|
113
125
|
const evidence = (0, trust_audit_1.normalizeEvidence)(run, input.evidence || [], {
|
|
114
126
|
source: "operator-recorded",
|
|
115
127
|
candidateId,
|
|
@@ -238,10 +250,11 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
|
|
|
238
250
|
else if (!verifierNode.evidence.length) {
|
|
239
251
|
failures.push(error("candidate-selection-missing-evidence", `Candidate ${candidateId} verifier node has no evidence`));
|
|
240
252
|
}
|
|
241
|
-
else if (emptyCaptureWarning(run, verifierNode)) {
|
|
242
|
-
// HARD no-false-green gate (v0.1.43) —
|
|
243
|
-
// (
|
|
244
|
-
//
|
|
253
|
+
else if ((0, gates_1.emptyCaptureWarning)(run, verifierNode)) {
|
|
254
|
+
// HARD no-false-green gate (v0.1.43) — selection and the commit gate now
|
|
255
|
+
// share ONE emptyCaptureWarning (src/gates.ts), so they CANNOT drift: a
|
|
256
|
+
// verifier node whose backing result was an empty-capture is unselectable
|
|
257
|
+
// here for the same reason it is uncommittable, by construction.
|
|
245
258
|
failures.push(error("candidate-selection-empty-capture", `Candidate ${candidateId} verifier node has no real evidence (empty-capture result)`));
|
|
246
259
|
}
|
|
247
260
|
}
|
|
@@ -279,7 +292,7 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
|
|
|
279
292
|
const now = new Date().toISOString();
|
|
280
293
|
const selection = {
|
|
281
294
|
schemaVersion: exports.CANDIDATE_SCHEMA_VERSION,
|
|
282
|
-
id: createSelectionId(candidateId),
|
|
295
|
+
id: createSelectionId(run, candidateId),
|
|
283
296
|
runId: run.id,
|
|
284
297
|
candidateId,
|
|
285
298
|
selectedAt: now,
|
|
@@ -305,7 +318,7 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
|
|
|
305
318
|
scoreCriteria: bestScore?.criteria,
|
|
306
319
|
verifierNodeId: candidate.verifierNodeId,
|
|
307
320
|
evidenceCount: mergeEvidence(candidate.evidence, verifierNode?.evidence || []).length,
|
|
308
|
-
sandboxProfileId: sandboxProfileForCandidate(run, candidate),
|
|
321
|
+
sandboxProfileId: (0, gates_1.sandboxProfileForCandidate)(run, candidate),
|
|
309
322
|
workerId: candidate.workerId,
|
|
310
323
|
commitGateResult: "passed"
|
|
311
324
|
}),
|
|
@@ -521,7 +534,10 @@ function loadCandidatesFromDisk(run) {
|
|
|
521
534
|
.filter((entry) => entry.isDirectory() && entry.name !== "selections")
|
|
522
535
|
.map((entry) => node_path_1.default.join(candidateRoot(run), entry.name, "candidate.json"))
|
|
523
536
|
.filter((file) => node_fs_1.default.existsSync(file))
|
|
524
|
-
|
|
537
|
+
// Fail-closed integrity boundary (F4/F5): each candidate.json is validated
|
|
538
|
+
// against CandidateRecord before it merges into the run; a corrupt record
|
|
539
|
+
// throws rather than entering the candidate set as a trusted cast.
|
|
540
|
+
.map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
|
|
525
541
|
}
|
|
526
542
|
function readScores(run, candidateId) {
|
|
527
543
|
const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
|
|
@@ -531,7 +547,10 @@ function readScores(run, candidateId) {
|
|
|
531
547
|
.readdirSync(dir)
|
|
532
548
|
.filter((file) => file.endsWith(".json"))
|
|
533
549
|
.sort()
|
|
534
|
-
|
|
550
|
+
// Fail-closed integrity boundary (F4/F5): a score file is validated against
|
|
551
|
+
// CandidateScore before it can feed ranking/selection. A corrupt score must
|
|
552
|
+
// throw, not silently widen the normalized/verdict surface the gate reads.
|
|
553
|
+
.map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
|
|
535
554
|
}
|
|
536
555
|
function candidateArtifacts(run, candidate) {
|
|
537
556
|
return [
|
|
@@ -558,16 +577,16 @@ function inferCandidateKind(input) {
|
|
|
558
577
|
return "manual";
|
|
559
578
|
}
|
|
560
579
|
function bestScore(scores) {
|
|
561
|
-
return [...scores].sort((left, right) => right.normalized - left.normalized || left.createdAt
|
|
580
|
+
return [...scores].sort((left, right) => right.normalized - left.normalized || (0, compare_1.compareBytes)(left.createdAt, right.createdAt))[0];
|
|
562
581
|
}
|
|
563
582
|
function compareRows(left, right, policy) {
|
|
564
583
|
const byScore = right.normalized - left.normalized;
|
|
565
584
|
if (byScore !== 0)
|
|
566
585
|
return byScore;
|
|
567
586
|
if (policy.tieBreaker === "candidateId")
|
|
568
|
-
return left.candidate.id
|
|
569
|
-
const byCreated = left.candidate.createdAt
|
|
570
|
-
return byCreated || left.candidate.id
|
|
587
|
+
return (0, compare_1.compareBytes)(left.candidate.id, right.candidate.id);
|
|
588
|
+
const byCreated = (0, compare_1.compareBytes)(left.candidate.createdAt, right.candidate.createdAt);
|
|
589
|
+
return byCreated || (0, compare_1.compareBytes)(left.candidate.id, right.candidate.id);
|
|
571
590
|
}
|
|
572
591
|
function detectTies(candidates) {
|
|
573
592
|
const groups = new Map();
|
|
@@ -578,10 +597,15 @@ function detectTies(candidates) {
|
|
|
578
597
|
return Array.from(groups.values()).filter((group) => group.length > 1);
|
|
579
598
|
}
|
|
580
599
|
function mergePolicy(policy = {}) {
|
|
600
|
+
// NOTE: `policy.criteria` (string[]) is intentionally NOT carried here. A
|
|
601
|
+
// whole-repo grep shows it has no read points — scoring reads each score's
|
|
602
|
+
// own `input.criteria` (Record<string, number>), not this list. Emitting a
|
|
603
|
+
// default `criteria: []` advertised a guarantee the code never honored and
|
|
604
|
+
// could silently drift, so it is dropped. The field stays OPTIONAL on
|
|
605
|
+
// CandidateScoringPolicy / CandidateRanking.policy for forward-compat input.
|
|
581
606
|
return {
|
|
582
607
|
id: policy.id || "cw.candidate.default",
|
|
583
608
|
title: policy.title || "Default Candidate Scoring",
|
|
584
|
-
criteria: policy.criteria || [],
|
|
585
609
|
requireEvidence: policy.requireEvidence ?? true,
|
|
586
610
|
requireVerifierGate: policy.requireVerifierGate ?? true,
|
|
587
611
|
minNormalized: policy.minNormalized,
|
|
@@ -591,9 +615,9 @@ function mergePolicy(policy = {}) {
|
|
|
591
615
|
function verdictFor(normalized, policy) {
|
|
592
616
|
if (policy.minNormalized !== undefined && normalized < policy.minNormalized)
|
|
593
617
|
return "fail";
|
|
594
|
-
if (normalized >=
|
|
618
|
+
if (normalized >= VERDICT_PASS_THRESHOLD)
|
|
595
619
|
return "pass";
|
|
596
|
-
if (normalized >=
|
|
620
|
+
if (normalized >= VERDICT_WARN_THRESHOLD)
|
|
597
621
|
return "warn";
|
|
598
622
|
return "fail";
|
|
599
623
|
}
|
|
@@ -616,18 +640,26 @@ function indexPath(run) {
|
|
|
616
640
|
function rankingPath(run) {
|
|
617
641
|
return node_path_1.default.join(candidateRoot(run), "ranking.json");
|
|
618
642
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
643
|
+
// Deterministic candidate id (FreeBSD-audit L12/L13): the candidate's POSITION in
|
|
644
|
+
// the run's candidate set, qualified by kind + seed (a stable worker/task/result
|
|
645
|
+
// id) for readability. No wall-clock stamp, no PRNG suffix — re-running the same
|
|
646
|
+
// workflow mints byte-identical candidate ids, keeping fingerprints replay-stable.
|
|
647
|
+
function createCandidateId(run, kind, seed) {
|
|
648
|
+
const seq = (run.candidates || []).length + 1;
|
|
649
|
+
return `candidate-${(0, state_1.safeFileName)(kind)}-${seed ? `${(0, state_1.safeFileName)(seed)}-` : ""}${String(seq).padStart(4, "0")}`;
|
|
650
|
+
}
|
|
651
|
+
// Deterministic score id (FreeBSD-audit L12/L13): the score's POSITION within its
|
|
652
|
+
// candidate's score list. Scores only ever append, so the sequence is unique per
|
|
653
|
+
// candidate and stable across replays.
|
|
654
|
+
function createScoreId(candidate) {
|
|
655
|
+
const seq = (candidate.scores || []).length + 1;
|
|
656
|
+
return `score-${(0, state_1.safeFileName)(candidate.id)}-${String(seq).padStart(4, "0")}`;
|
|
657
|
+
}
|
|
658
|
+
// Deterministic selection id (FreeBSD-audit L12/L13): the selection's POSITION in
|
|
659
|
+
// the run's append-only selection log. No clock, no PRNG.
|
|
660
|
+
function createSelectionId(run, candidateId) {
|
|
661
|
+
const seq = (run.candidateSelections || []).length + 1;
|
|
662
|
+
return `selection-${(0, state_1.safeFileName)(candidateId)}-${String(seq).padStart(4, "0")}`;
|
|
631
663
|
}
|
|
632
664
|
function shouldPersist(options) {
|
|
633
665
|
return options.persist !== false;
|
|
@@ -641,17 +673,6 @@ function error(code, message, options = {}) {
|
|
|
641
673
|
details: options.details
|
|
642
674
|
};
|
|
643
675
|
}
|
|
644
|
-
/** HARD no-false-green gate (v0.1.43) — kept in SYNC with commit.ts. Traces a
|
|
645
|
-
* verifier node back to its source result node and returns the empty-capture
|
|
646
|
-
* marker (set at ingest via isEmptyCapture) when present. Reads ONLY persisted
|
|
647
|
-
* state, so selection replays deterministically. */
|
|
648
|
-
function emptyCaptureWarning(run, verifierNode) {
|
|
649
|
-
const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
|
|
650
|
-
verifierNode.parents[0];
|
|
651
|
-
const resultNode = resultNodeId ? run.nodes?.find((node) => node.id === resultNodeId) : undefined;
|
|
652
|
-
const warning = resultNode?.metadata?.captureWarning;
|
|
653
|
-
return typeof warning === "string" && warning ? warning : undefined;
|
|
654
|
-
}
|
|
655
676
|
function mergeCandidates(left, right) {
|
|
656
677
|
const merged = [...left];
|
|
657
678
|
for (const candidate of right) {
|
|
@@ -688,13 +709,6 @@ function mergeEvidence(left, right) {
|
|
|
688
709
|
}
|
|
689
710
|
return merged;
|
|
690
711
|
}
|
|
691
|
-
function sandboxProfileForCandidate(run, candidate) {
|
|
692
|
-
const worker = candidate.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
|
|
693
|
-
if (worker?.sandboxProfileId)
|
|
694
|
-
return worker.sandboxProfileId;
|
|
695
|
-
const task = candidate.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
|
|
696
|
-
return task?.sandboxProfileId;
|
|
697
|
-
}
|
|
698
712
|
function unique(values) {
|
|
699
713
|
return Array.from(new Set(values.filter(Boolean)));
|
|
700
714
|
}
|