ruvnet-brain 4.3.8 → 4.3.10

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 (85) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +11 -2
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +472 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  36. package/scripts/build-bundle.mjs +8 -0
  37. package/scripts/build-primer.mjs +2 -3
  38. package/scripts/candidate-host-evidence.mjs +59 -41
  39. package/scripts/ci/build-fixture-kb.mjs +59 -2
  40. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  41. package/scripts/ci/stranger-scenario.mjs +13 -28
  42. package/scripts/claims-verify.mjs +219 -21
  43. package/scripts/console-engine.mjs +4 -18
  44. package/scripts/console-runtime-identity.mjs +6 -0
  45. package/scripts/development-maintenance.mjs +47 -0
  46. package/scripts/development-push-check.mjs +35 -0
  47. package/scripts/distill-project.mjs +24 -47
  48. package/scripts/doc-currency.mjs +91 -19
  49. package/scripts/git-hooks/pre-push +4 -162
  50. package/scripts/health-repair.mjs +22 -4
  51. package/scripts/hook-retirement-check.mjs +22 -0
  52. package/scripts/host-install-matrix.mjs +125 -61
  53. package/scripts/integration-evidence.mjs +11 -6
  54. package/scripts/learning-replay-fixture.mjs +23 -2
  55. package/scripts/nightly-two-run-proof.mjs +415 -0
  56. package/scripts/nightly-watchdog.mjs +17 -2
  57. package/scripts/npm-invocation.mjs +20 -0
  58. package/scripts/prepublication-evidence.mjs +42 -6
  59. package/scripts/primer-grounding.mjs +20 -0
  60. package/scripts/product-integrity-contract.mjs +22 -2
  61. package/scripts/public-verification-abandon.mjs +101 -0
  62. package/scripts/public-verification-aggregate.mjs +53 -19
  63. package/scripts/public-verification-finalizer.mjs +8 -2
  64. package/scripts/public-verification-lane.mjs +108 -8
  65. package/scripts/publication-receipt.mjs +173 -66
  66. package/scripts/published-surface-probe.mjs +10 -2
  67. package/scripts/qa-contract.mjs +57 -0
  68. package/scripts/qa-lanes.mjs +40 -0
  69. package/scripts/qa-runner.mjs +54 -64
  70. package/scripts/qe/ux-suite.mjs +10 -34
  71. package/scripts/qualified-candidate-check.mjs +124 -0
  72. package/scripts/release-abort-stale.mjs +2 -2
  73. package/scripts/release-projection.mjs +20 -1
  74. package/scripts/release-publication-handoff.mjs +101 -0
  75. package/scripts/release-qualification-contract.mjs +87 -0
  76. package/scripts/release-qualification.mjs +135 -0
  77. package/scripts/release-transaction.mjs +81 -1
  78. package/scripts/release.mjs +10 -1
  79. package/scripts/remedy-registry.mjs +6 -13
  80. package/scripts/retrieval-canary.mjs +91 -18
  81. package/scripts/selfcheck.mjs +12 -4
  82. package/scripts/snapshot-freshness.mjs +58 -0
  83. package/scripts/stack-sync.mjs +41 -43
  84. package/scripts/staged-host-verifier.mjs +63 -14
  85. package/scripts/wired-check.mjs +31 -2
@@ -43,7 +43,7 @@ const make = (id, statement, owner, contributors, implementation, tests, receipt
43
43
  export const PRODUCT_INTEGRITY_OBLIGATIONS = Object.freeze([
44
44
  make('S-1', 'One complete public corpus', 'CorpusGeneration', ['SourceCoverage'], ['scripts/source-coverage.mjs', 'scripts/corpus-reconcile.mjs', 'scripts/corpus-candidate.mjs'], ['tests/unit/source-coverage.test.mjs', 'tests/unit/corpus-reconcile.test.mjs'], ['ruvnet-brain-source-observation', 'ruvnet-brain-corpus-candidate']),
45
45
  make('S-2', 'One immutable public release projection', 'ReleaseProjection', ['CorpusGeneration'], ['scripts/build-bundle.mjs', 'scripts/coverage-integrity.mjs', 'scripts/public-inventory.mjs'], ['tests/integration/build-bundle-fence.test.mjs', 'tests/unit/public-inventory.test.mjs'], ['ruvnet-brain-release-coverage']),
46
- make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/packed-retrieval-canary.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/packed-retrieval-canary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
46
+ make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/candidate-host-evidence.mjs', 'scripts/prepublication-evidence.mjs', 'scripts/host-install-matrix.mjs', 'kb/retrieval-result.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/candidate-host-evidence.test.mjs', 'tests/unit/candidate-retrieval-matrix.test.mjs', 'tests/unit/retrieval-result-boundary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
47
47
  make('S-4', 'Native nightly runs are ordered, idempotent, and retained within budget', 'RefreshLifecycle', [], ['bin/nightly-refresh.mjs', 'plugin/scripts/nightly-scheduler.mjs', 'scripts/nightly-two-run-proof.mjs', 'kb/refresh-run.mjs'], ['tests/unit/nightly-scheduler.test.mjs', 'tests/unit/nightly-two-run-proof.test.mjs'], ['ruvnet-brain-refresh-run', 'ruvnet-brain-native-two-run-nightly-proof']),
48
48
  make('S-5', 'One active corpus preserves private stores and bounded recovery', 'RefreshLifecycle', ['CorpusGeneration'], ['kb/update-storage-transaction.mjs', 'kb/forge-update.mjs', 'kb/brain-profile.mjs', 'bin/install.mjs'], ['tests/unit/update-storage-transaction.test.mjs', 'tests/unit/brain-profile.test.mjs'], ['ruvnet-brain-update-storage-transaction', 'ruvnet-brain-installed-profile']),
49
49
  make('S-6', 'Every supported OS and host loader converges on exact public bytes', 'HostConvergence', ['PublicVerification'], ['scripts/host-registry.mjs', 'scripts/host-install-matrix.mjs', 'scripts/public-verification-aggregate.mjs'], ['tests/unit/host-registry.test.mjs', 'tests/integration/dual-host-install.test.mjs'], ['ruvnet-brain-host-registry', 'ruvnet-brain-public-verification-leaf']),
@@ -141,9 +141,29 @@ export function renderProductIntegrityTraceMarkdown(contract = validateProductIn
141
141
  }
142
142
  const valueAfter = (argv, flag) => { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : null; };
143
143
  export function runProductIntegrityCli(argv = process.argv.slice(2), io = { stdout: process.stdout }) {
144
- const known = new Set(['--markdown', '--check-markdown', '--trace', '--source-sha', '--out', '--verify-trace']);
144
+ const known = new Set(['--markdown', '--check-markdown', '--check-source', '--trace', '--source-sha', '--out', '--verify-trace']);
145
145
  if (argv.some((arg) => arg.startsWith('--') && !known.has(arg))) throw new Error('unknown product integrity argument');
146
146
  if (!argv.length) { io.stdout.write(`${canonicalJson(validateProductIntegrityContract())}\n`); return 0; }
147
+ if (argv.length === 1 && argv[0] === '--check-source') {
148
+ const root = fs.realpathSync(io.root || fileURLToPath(new URL('..', import.meta.url)));
149
+ const missing = [], invalid = [];
150
+ const files = productIntegrityGovernedPaths();
151
+ for (const file of files) {
152
+ const absolute = path.join(root, file);
153
+ try {
154
+ const stat = fs.lstatSync(absolute);
155
+ const relative = path.relative(root, fs.realpathSync(absolute));
156
+ if (!stat.isFile() || stat.isSymbolicLink() || relative.startsWith(`..${path.sep}`)
157
+ || relative === '..' || path.isAbsolute(relative)) invalid.push(file);
158
+ } catch (error) {
159
+ (error.code === 'ENOENT' ? missing : invalid).push(file);
160
+ }
161
+ }
162
+ const verdict = missing.length || invalid.length ? 'FAIL' : 'PASS';
163
+ io.stdout.write(`${canonicalJson({ verdict, scope: 'source-presence', behaviorVerified: false,
164
+ checked: files.length, missing, invalid })}\n`);
165
+ return verdict === 'PASS' ? 0 : 1;
166
+ }
147
167
  if (argv.length === 1 && argv[0] === '--markdown') { io.stdout.write(renderProductIntegrityTraceMarkdown()); return 0; }
148
168
  const check = valueAfter(argv, '--check-markdown');
149
169
  if (check) { if (fs.readFileSync(path.resolve(check), 'utf8') !== renderProductIntegrityTraceMarkdown()) throw new Error('generated traceability Markdown differs'); io.stdout.write('product integrity Markdown: PASS\n'); return 0; }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { extractZip } from '../kb/zip-extract.mjs';
9
+ import { abandonPublicVerificationTransaction } from './release-transaction.mjs';
10
+ import { liveReleaseProvider } from './release-transaction-provider.mjs';
11
+
12
+ function regularJson(file, max = 131072) {
13
+ const stat = fs.lstatSync(file);
14
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > max) throw new Error('untrusted or oversized closure input');
15
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ }
17
+ const api = (endpoint) => JSON.parse(execFileSync('gh', ['api', endpoint], { encoding: 'utf8', timeout: 30000 }));
18
+
19
+ export function closureProvenance({ event, env, identity, failure, run, artifact, workflow }) {
20
+ const input = event.client_payload;
21
+ if (env.GITHUB_EVENT_NAME !== 'repository_dispatch' || event.action !== 'abandon-public-verification'
22
+ || event.sender?.login !== env.GITHUB_ACTOR || event.repository?.full_name !== identity.repository
23
+ || env.GITHUB_REPOSITORY !== identity.repository || input?.authorization !== 'abandon-published-not-verified') {
24
+ throw new Error('closure requires the explicitly authorized abandonment dispatch');
25
+ }
26
+ if (run.event !== 'repository_dispatch' || !Number.isSafeInteger(run.repository?.id)
27
+ || run.repository.id !== event.repository.id || run.head_repository?.id !== run.repository.id
28
+ || run.head_repository?.full_name !== identity.repository
29
+ || !Number.isSafeInteger(workflow?.id) || run.workflow_id !== workflow.id || workflow.path !== run.path
30
+ || run.id !== input.recovery_run_id || run.status !== 'completed' || run.conclusion !== 'failure'
31
+ || run.path !== '.github/workflows/recover-public-verification.yml'
32
+ || run.repository?.full_name !== identity.repository || !/^[a-f0-9]{40}$/.test(run.head_sha || '')
33
+ || artifact.id !== input.failure_artifact_id || artifact.expired !== false
34
+ || artifact.workflow_run?.id !== run.id || artifact.workflow_run?.head_sha !== run.head_sha
35
+ || artifact.name !== `recovered-public-verification-${failure.os}-${identity.candidateSha}-${run.id}`) {
36
+ throw new Error('GitHub failed-run artifact provenance mismatch');
37
+ }
38
+ return {
39
+ expected: input.expected,
40
+ reason: input.reason,
41
+ authorization: { actor: env.GITHUB_ACTOR, reference: input.authorization_reference,
42
+ event: env.GITHUB_EVENT_NAME, action: event.action,
43
+ dispatchRunId: Number(env.GITHUB_RUN_ID), verifierSha: env.GITHUB_SHA },
44
+ recovery: { repository: identity.repository, workflow: run.path, workflowId: workflow.id, repositoryId: run.repository.id, event: run.event, runId: run.id,
45
+ verifierSha: run.head_sha, conclusion: run.conclusion, artifactId: artifact.id, artifactName: artifact.name },
46
+ };
47
+ }
48
+
49
+ // Download the named GitHub artifact independently; a local JSON lookalike is not provenance.
50
+ async function failureFromArtifact(repository, artifactId) {
51
+ if (!Number.isSafeInteger(artifactId) || artifactId <= 0) throw new Error('invalid failed artifact ID');
52
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'abandon-failed-artifact-'));
53
+ try {
54
+ const zip = path.join(temp, 'failure.zip');
55
+ fs.writeFileSync(zip, execFileSync('gh', ['api', `repos/${repository}/actions/artifacts/${artifactId}/zip`],
56
+ { encoding: null, timeout: 60000, maxBuffer: 2 * 1024 * 1024 }), { flag: 'wx' });
57
+ const output = path.join(temp, 'contents');
58
+ const extracted = await extractZip(zip, output);
59
+ if (extracted.entryNames.length !== 1 || !/^(linux|macos|windows)\.json$/.test(extracted.entryNames[0])) {
60
+ throw new Error('failure artifact must contain exactly one OS failure receipt');
61
+ }
62
+ return regularJson(path.join(output, extracted.entryNames[0]));
63
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
64
+ }
65
+
66
+ export async function abandonPublicVerification({ identityFile, outputFile, env = process.env,
67
+ readApi = api, readFailure = failureFromArtifact, adapter = liveReleaseProvider({ root: process.cwd() }),
68
+ publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem' } = {}) {
69
+ if (!identityFile || !outputFile || !env.GITHUB_EVENT_PATH || !env.RUVNET_SIGNING_KEY) {
70
+ throw new Error('identity, output, dispatch event, and signing key are required');
71
+ }
72
+ if (fs.existsSync(outputFile)) throw new Error('refusing to overwrite closure output');
73
+ const identity = regularJson(identityFile);
74
+ if (identity.repository !== 'stuinfla/ruvnet-brain' || identity.package !== 'ruvnet-brain') throw new Error('unexpected release repository');
75
+ const event = regularJson(env.GITHUB_EVENT_PATH);
76
+ const input = event.client_payload;
77
+ if (![input?.recovery_run_id, input?.failure_artifact_id].every((id) => Number.isSafeInteger(id) && id > 0)) {
78
+ throw new Error('explicit recovery run and failed artifact IDs are required');
79
+ }
80
+ const run = await readApi(`repos/${identity.repository}/actions/runs/${input.recovery_run_id}`);
81
+ const artifact = await readApi(`repos/${identity.repository}/actions/artifacts/${input.failure_artifact_id}`);
82
+ const workflow = await readApi(`repos/${identity.repository}/actions/workflows/recover-public-verification.yml`);
83
+ const failure = await readFailure(identity.repository, input.failure_artifact_id);
84
+ const provenance = closureProvenance({ event, env, identity, failure, run, artifact, workflow });
85
+ const receipt = await abandonPublicVerificationTransaction({ identity, failure, ...provenance, adapter,
86
+ privateKey: crypto.createPrivateKey(env.RUVNET_SIGNING_KEY),
87
+ publicKey: crypto.createPublicKey(fs.readFileSync(publicKeyFile, 'utf8')) });
88
+ fs.mkdirSync(path.dirname(path.resolve(outputFile)), { recursive: true });
89
+ fs.writeFileSync(outputFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
90
+ return receipt;
91
+ }
92
+
93
+ const direct = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
94
+ if (direct) {
95
+ const arg = (name) => { const index = process.argv.indexOf(name); return index < 0 ? null : process.argv[index + 1]; };
96
+ try {
97
+ const receipt = await abandonPublicVerification({ identityFile: arg('--identity'), outputFile: arg('--out') });
98
+ console.log(JSON.stringify({ state: receipt.state, verdict: receipt.observation.verdict,
99
+ transactionId: receipt.transactionId, sequence: receipt.sequence, receiptDigest: receipt.receiptDigest }));
100
+ } catch (error) { console.error(`public-verification-abandon: ${error.message}`); process.exitCode = 1; }
101
+ }
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { canonicalJson, digest } from './coverage-integrity.mjs';
7
7
  import { retrievalOracleExpectationFromPlan, validateRetrievalOracleReview } from './independent-review-receipt.mjs';
8
8
  import { validateRetrievalCanaryPlan, validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
9
+ import { validateNightlyProofReceipt } from './nightly-two-run-proof.mjs';
9
10
 
10
11
  export const PUBLIC_VERIFICATION_OS = Object.freeze(['linux', 'macos', 'windows']);
11
12
  export const PUBLIC_VERIFICATION_MODES = Object.freeze(['claude', 'codex', 'dual']);
@@ -19,7 +20,10 @@ function unsignedLeaf(leaf) {
19
20
  return payload;
20
21
  }
21
22
 
22
- export function validatePublicVerificationLeaf(leaf) {
23
+ export function validatePublicVerificationLeaf(leaf, { publicKey } = {}) {
24
+ if (Object.hasOwn(leaf || {}, 'verifierSha') && !HEX40.test(String(leaf.verifierSha))) {
25
+ throw new Error('public verification leaf verifier SHA is invalid');
26
+ }
23
27
  if (leaf?.schemaVersion !== 1 || leaf?.kind !== 'ruvnet-brain-public-verification-leaf'
24
28
  || !PUBLIC_VERIFICATION_OS.includes(leaf.os) || !PUBLIC_VERIFICATION_MODES.includes(leaf.mode)
25
29
  || !HEX40.test(String(leaf.sourceSha || '')) || !HEX64.test(String(leaf.artifactSha256 || ''))
@@ -49,12 +53,23 @@ export function validatePublicVerificationLeaf(leaf) {
49
53
  || retrieval.coverageGeneration !== leaf.coverageGeneration || retrieval.planSha256 !== leaf.canaryPlanSha256) {
50
54
  throw new Error(`${leaf.os}/${leaf.mode} retrieval identity differs`);
51
55
  }
56
+ if (leaf.mode === 'dual') {
57
+ if (!/^[1-9][0-9]*$/.test(String(leaf.workflowRunId || ''))) throw new Error('native nightly workflow run identity is missing');
58
+ const native = validateNightlyProofReceipt(leaf.nativeNightly, {
59
+ platform: { linux: 'linux', macos: 'darwin', windows: 'win32' }[leaf.os],
60
+ version: leaf.version, sourceSha: leaf.sourceSha, workflowRunId: leaf.workflowRunId,
61
+ packageSha256: leaf.artifactSha256, bundleSha256: leaf.bundleSha256, publicKey,
62
+ });
63
+ if (!native.ok) throw new Error(`${leaf.os}/dual native nightly proof failed: ${native.failures.join('; ')}`);
64
+ } else if (leaf.nativeNightly !== undefined) {
65
+ throw new Error('native nightly proof belongs only to the dual-host leaf');
66
+ }
52
67
  return leaf;
53
68
  }
54
69
 
55
- export function createPublicVerificationLeaf(input) {
70
+ export function createPublicVerificationLeaf(input, options = {}) {
56
71
  const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-leaf', ...input };
57
- return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) });
72
+ return validatePublicVerificationLeaf({ ...payload, leafSha256: digest(payload) }, options);
58
73
  }
59
74
 
60
75
  function aggregatePayload(aggregate) {
@@ -122,6 +137,7 @@ function validateReviewOracleAgainstPlan(review, retrievalPlan) {
122
137
 
123
138
  const identityOf = (leaf) => ({
124
139
  sourceSha: leaf.sourceSha,
140
+ ...(Object.hasOwn(leaf, 'verifierSha') ? { verifierSha: leaf.verifierSha } : {}),
125
141
  version: leaf.version,
126
142
  tag: leaf.tag,
127
143
  artifactSha256: leaf.artifactSha256,
@@ -133,11 +149,13 @@ const identityOf = (leaf) => ({
133
149
  releaseTransactionId: leaf.releaseTransactionId,
134
150
  });
135
151
 
136
- export function buildPublicVerificationAggregate(leaves) {
152
+ export function buildPublicVerificationAggregate(leaves, options = {}) {
137
153
  if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
138
154
  throw new Error('public verification requires exactly nine leaves');
139
155
  }
140
- leaves.forEach(validatePublicVerificationLeaf);
156
+ leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
157
+ const nativeRunIds = new Set(leaves.filter((leaf) => leaf.mode === 'dual').map((leaf) => String(leaf.workflowRunId)));
158
+ if (nativeRunIds.size !== 1) throw new Error('native nightly proofs span different workflow runs');
141
159
  const byLane = new Map(leaves.map((leaf) => [`${leaf.os}/${leaf.mode}`, leaf]));
142
160
  const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
143
161
  if (byLane.size !== required.length || required.some((lane) => !byLane.has(lane))) {
@@ -178,6 +196,7 @@ export function buildPublicVerificationAggregate(leaves) {
178
196
  schemaVersion: 1,
179
197
  kind: 'ruvnet-brain-public-verification-aggregate',
180
198
  identity,
199
+ workflowRunId: [...nativeRunIds][0],
181
200
  coverage: first.coverage,
182
201
  lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
183
202
  evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
@@ -193,7 +212,7 @@ export function buildPublicVerificationAggregate(leaves) {
193
212
  }
194
213
 
195
214
  export function signPublicVerificationAggregate({ leaves }, privateKey) {
196
- const payload = buildPublicVerificationAggregate(leaves);
215
+ const payload = buildPublicVerificationAggregate(leaves, { publicKey: crypto.createPublicKey(privateKey) });
197
216
  const aggregateSha256 = digest(payload);
198
217
  const signed = { ...payload, aggregateSha256 };
199
218
  return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
@@ -210,14 +229,14 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
210
229
  if (!Array.isArray(aggregate.evidence?.leaves)) {
211
230
  throw new Error('public verification aggregate lacks raw leaf evidence');
212
231
  }
213
- const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves);
214
- if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
215
- throw new Error('public verification aggregate differs from rebuilt raw evidence');
216
- }
217
232
  const signed = { ...payload, aggregateSha256: aggregate.aggregateSha256 };
218
233
  if (!crypto.verify(null, Buffer.from(canonicalJson(signed)), publicKey, Buffer.from(aggregate.signature, 'base64'))) {
219
234
  throw new Error('public verification aggregate signature mismatch');
220
235
  }
236
+ const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves, { publicKey });
237
+ if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
238
+ throw new Error('public verification aggregate differs from rebuilt raw evidence');
239
+ }
221
240
  const required = PUBLIC_VERIFICATION_OS.flatMap((os) => PUBLIC_VERIFICATION_MODES.map((mode) => `${os}/${mode}`));
222
241
  if (aggregate.lanes?.length !== required.length || new Set(aggregate.lanes.map(({ lane }) => lane)).size !== required.length
223
242
  || required.some((lane) => !aggregate.lanes.some((row) => row.lane === lane && HEX64.test(String(row.leafSha256 || ''))))) {
@@ -234,7 +253,11 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
234
253
  || aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
235
254
  throw new Error('public verification aggregate retrieval oracle identity is incomplete');
236
255
  }
237
- if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedIdentity)) {
256
+ if (expectedIdentity?.workflowRunId !== undefined && String(expectedIdentity.workflowRunId) !== aggregate.workflowRunId) {
257
+ throw new Error('public verification aggregate workflow run differs from the expected run');
258
+ }
259
+ const { workflowRunId: _expectedRun, ...expectedReleaseIdentity } = expectedIdentity || {};
260
+ if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedReleaseIdentity)) {
238
261
  throw new Error('public verification aggregate identity differs from the release transaction');
239
262
  }
240
263
  return aggregate;
@@ -245,13 +268,13 @@ function parseCliArgs(argv) {
245
268
  for (let index = 0; index < argv.length; index += 2) {
246
269
  const flag = argv[index];
247
270
  const value = argv[index + 1];
248
- if (!['--lanes', '--out'].includes(flag) || !value) {
249
- throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
271
+ if (!['--lanes', '--out', '--verifier-sha', '--workflow-run-id'].includes(flag) || !value) {
272
+ throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
250
273
  }
251
274
  parsed[flag.slice(2)] = value;
252
275
  }
253
- if (!parsed.lanes || !parsed.out || Object.keys(parsed).length !== 2) {
254
- throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
276
+ if (!parsed.lanes || !parsed.out || !/^[1-9][0-9]*$/.test(parsed['workflow-run-id'] || '')) {
277
+ throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file> --workflow-run-id <id>');
255
278
  }
256
279
  return parsed;
257
280
  }
@@ -265,20 +288,24 @@ function readExactJsonFiles(directory, count, label) {
265
288
  .map((entry) => JSON.parse(fs.readFileSync(path.join(directory, entry.name), 'utf8')));
266
289
  }
267
290
 
268
- function readLaneLeaves(directory) {
291
+ function readLaneLeaves(directory, options = {}) {
269
292
  const wrappers = readExactJsonFiles(directory, PUBLIC_VERIFICATION_OS.length, 'lane');
270
293
  const byOs = new Map();
271
294
  for (const wrapper of wrappers) {
272
295
  const payload = { schemaVersion: wrapper?.schemaVersion, kind: wrapper?.kind, os: wrapper?.os,
296
+ ...(Object.hasOwn(wrapper || {}, 'verifierSha') ? { verifierSha: wrapper.verifierSha, sourceSha: wrapper.sourceSha } : {}),
273
297
  leaves: wrapper?.leaves };
274
298
  if (payload.schemaVersion !== 1 || payload.kind !== 'ruvnet-brain-public-verification-os-lane'
275
299
  || !PUBLIC_VERIFICATION_OS.includes(payload.os) || byOs.has(payload.os)
276
300
  || !Array.isArray(payload.leaves) || payload.leaves.length !== PUBLIC_VERIFICATION_MODES.length
277
301
  || payload.leaves.some((leaf) => leaf?.os !== payload.os)
302
+ || (Object.hasOwn(payload, 'verifierSha') && (!HEX40.test(String(payload.verifierSha))
303
+ || !HEX40.test(String(payload.sourceSha))
304
+ || payload.leaves.some((leaf) => leaf.verifierSha !== payload.verifierSha || leaf.sourceSha !== payload.sourceSha)))
278
305
  || digest(payload) !== wrapper.laneSha256) {
279
306
  throw new Error('public verification OS lane wrapper is malformed, duplicated, or tampered');
280
307
  }
281
- payload.leaves.forEach(validatePublicVerificationLeaf);
308
+ payload.leaves.forEach((leaf) => validatePublicVerificationLeaf(leaf, options));
282
309
  byOs.set(payload.os, payload.leaves);
283
310
  }
284
311
  if (PUBLIC_VERIFICATION_OS.some((osName) => !byOs.has(osName))) {
@@ -287,11 +314,16 @@ function readLaneLeaves(directory) {
287
314
  return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
288
315
  }
289
316
 
290
- export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
317
+ export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey, verifierSha, workflowRunId }) {
318
+ if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('expected workflow run ID is required');
291
319
  if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
292
320
  if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
293
- const leaves = readLaneLeaves(lanesDirectory);
321
+ const leaves = readLaneLeaves(lanesDirectory, { publicKey: crypto.createPublicKey(privateKey) });
294
322
  const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
323
+ if (aggregate.workflowRunId !== String(workflowRunId)) throw new Error('public verification aggregate workflow run differs from the expected run');
324
+ if (verifierSha !== undefined && (!HEX40.test(String(verifierSha)) || aggregate.identity.verifierSha !== verifierSha)) {
325
+ throw new Error('public verification aggregate verifier SHA differs from the expected verifier');
326
+ }
295
327
  fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
296
328
  return aggregate;
297
329
  }
@@ -301,6 +333,8 @@ async function main() {
301
333
  const aggregate = generatePublicVerificationAggregate({
302
334
  lanesDirectory: options.lanes,
303
335
  outputFile: options.out,
336
+ verifierSha: options['verifier-sha'],
337
+ workflowRunId: options['workflow-run-id'],
304
338
  privateKey: process.env.RUVNET_SIGNING_KEY,
305
339
  });
306
340
  process.stdout.write(`${JSON.stringify({ ok: true, aggregateSha256: aggregate.aggregateSha256,
@@ -2,6 +2,7 @@
2
2
  import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { finalizeReleaseTransaction } from './release-transaction.mjs';
6
7
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
7
8
 
@@ -22,6 +23,8 @@ export async function finalizePublicVerification({
22
23
  identityFile,
23
24
  aggregateFile,
24
25
  outputFile,
26
+ verifierSha,
27
+ workflowRunId,
25
28
  privatePem = process.env.RUVNET_SIGNING_KEY,
26
29
  publicKeyFile = 'keys/ruvnet-brain-signing.pub.pem',
27
30
  adapter = liveReleaseProvider({ root: process.cwd() }),
@@ -33,7 +36,7 @@ export async function finalizePublicVerification({
33
36
  const identity = regularJson(identityFile, 'release identity');
34
37
  const aggregate = regularJson(aggregateFile, 'public verification aggregate');
35
38
  const publicKey = crypto.createPublicKey(fs.readFileSync(path.resolve(publicKeyFile), 'utf8'));
36
- const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter,
39
+ const receipt = await finalizeReleaseTransaction({ identity, aggregate, adapter, verifierSha, workflowRunId,
37
40
  privateKey: crypto.createPrivateKey(privatePem), publicKey, aggregatePublicKey: publicKey });
38
41
  fs.mkdirSync(path.dirname(output), { recursive: true });
39
42
  fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
@@ -46,6 +49,8 @@ export async function main(args = process.argv.slice(2)) {
46
49
  identityFile: argument(args, '--identity'),
47
50
  aggregateFile: argument(args, '--aggregate'),
48
51
  outputFile: argument(args, '--out'),
52
+ verifierSha: argument(args, '--verifier-sha') ?? undefined,
53
+ workflowRunId: argument(args, '--workflow-run-id'),
49
54
  });
50
55
  process.stdout.write(`${JSON.stringify({ state: receipt.state, transactionId: receipt.transactionId,
51
56
  receiptDigest: receipt.receiptDigest }, null, 2)}\n`);
@@ -56,4 +61,5 @@ export async function main(args = process.argv.slice(2)) {
56
61
  }
57
62
  }
58
63
 
59
- if (path.resolve(process.argv[1] || '') === path.resolve(new URL(import.meta.url).pathname)) process.exitCode = await main();
64
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
65
+ if (invokedDirectly) process.exitCode = await main();
@@ -3,11 +3,12 @@ import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import osModule from 'node:os';
5
5
  import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { canonicalJson, digest, validateCoverageLedger } from './coverage-integrity.mjs';
8
9
  import { validateHostRegistry } from './host-registry.mjs';
9
10
  import { RECEIPT_MODE_NAMES } from './host-install-matrix.mjs';
10
- import { generatePublicationReceipt, livePublicationAdapter } from './publication-receipt.mjs';
11
+ import { generatePublicationReceipt, livePublicationAdapter, validateCandidateSource } from './publication-receipt.mjs';
11
12
  import {
12
13
  createPublicVerificationLeaf,
13
14
  PUBLIC_VERIFICATION_MODES,
@@ -24,6 +25,33 @@ const HEX40 = /^[a-f0-9]{40}$/;
24
25
  const HEX64 = /^[a-f0-9]{64}$/;
25
26
  const normalizedDigest = (value) => String(value || '').replace(/^sha256:/, '');
26
27
 
28
+ function failedCanaryCases(retrieval) {
29
+ return retrieval.cases.filter(({ cohort, status, retrievalHit, citationResolved }) =>
30
+ status !== 'COMPLETED' || retrievalHit !== true || (cohort === 'delta' && citationResolved !== true));
31
+ }
32
+
33
+ function publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves }) {
34
+ const payload = {
35
+ schemaVersion: 1,
36
+ kind: 'ruvnet-brain-public-verification-failure',
37
+ os,
38
+ sourceSha: identity.candidateSha,
39
+ ...(verifierSha === undefined ? {} : { verifierSha }),
40
+ artifactSha256: identity.packageSha256,
41
+ bundleSha256: identity.bundleSha256,
42
+ failures: failures.map(({ mode, reason, retrieval }) => ({
43
+ mode,
44
+ reason,
45
+ metrics: retrieval.metrics,
46
+ failedCaseIds: failedCanaryCases(retrieval).map(({ id }) => id),
47
+ failedCases: failedCanaryCases(retrieval),
48
+ retrievalReceiptSha256: retrieval.receiptSha256,
49
+ })),
50
+ completedLeaves: completedLeaves.map(({ mode, leafSha256 }) => ({ mode, leafSha256 })),
51
+ };
52
+ return { ...payload, failureSha256: digest(payload) };
53
+ }
54
+
27
55
  function validateReleaseIdentity(identity) {
28
56
  if (identity?.repository !== 'stuinfla/ruvnet-brain' || identity?.package !== 'ruvnet-brain'
29
57
  || !HEX40.test(String(identity.candidateSha || '')) || !HEX64.test(String(identity.payloadId || ''))
@@ -76,6 +104,9 @@ function coverageProjection(coverage) {
76
104
 
77
105
  export async function createPublicVerificationLane({
78
106
  os,
107
+ verifierSha,
108
+ workflowRunId,
109
+ publicKey,
79
110
  candidate,
80
111
  publication,
81
112
  identity,
@@ -86,6 +117,8 @@ export async function createPublicVerificationLane({
86
117
  adapter,
87
118
  } = {}) {
88
119
  if (!PUBLIC_VERIFICATION_OS.includes(os)) throw new Error(`unsupported public verification OS: ${os || '(missing)'}`);
120
+ if (verifierSha !== undefined && !HEX40.test(String(verifierSha))) throw new Error('verifier SHA must be a full commit identity');
121
+ if (!/^[1-9][0-9]*$/.test(String(workflowRunId || ''))) throw new Error('workflow run ID is required');
89
122
  validateReleaseIdentity(identity);
90
123
  validatePublicEvidence({ candidate, publication, identity });
91
124
  const registry = validateHostRegistry(hostRegistry);
@@ -106,7 +139,9 @@ export async function createPublicVerificationLane({
106
139
  throw new Error('public verification lane adapter is incomplete');
107
140
  }
108
141
  const common = {
142
+ workflowRunId: String(workflowRunId),
109
143
  sourceSha: identity.candidateSha,
144
+ ...(verifierSha === undefined ? {} : { verifierSha }),
110
145
  version: identity.version,
111
146
  tag: identity.tag,
112
147
  artifactSha256: identity.packageSha256,
@@ -118,6 +153,7 @@ export async function createPublicVerificationLane({
118
153
  releaseTransactionId: transactionIdFor(identity),
119
154
  };
120
155
  const leaves = [];
156
+ const failures = [];
121
157
  for (const mode of PUBLIC_VERIFICATION_MODES) {
122
158
  const installed = publication.installed?.[RECEIPT_MODE_NAMES[mode]];
123
159
  if (installed?.status !== 'PASS' || installed.doctorExit !== 0 || installed.version !== identity.version
@@ -132,12 +168,24 @@ export async function createPublicVerificationLane({
132
168
  search: ({ query, k }) => adapter.searchInstalled({ mode, query, k }),
133
169
  citationResolver: (matched, expected) => adapter.resolveInstalledCitation({ mode, matched, expected }),
134
170
  });
135
- const verifiedRetrieval = validateRetrievalCanaryReceipt(
136
- retrieval,
137
- { plan: retrievalPlan, requireAcceptance: true },
138
- );
171
+ let verifiedRetrieval;
172
+ try {
173
+ verifiedRetrieval = validateRetrievalCanaryReceipt(
174
+ retrieval,
175
+ { plan: retrievalPlan, requireAcceptance: true },
176
+ );
177
+ } catch (error) {
178
+ failures.push({ mode, reason: error.message, retrieval });
179
+ continue;
180
+ }
181
+ let nativeNightly;
182
+ if (mode === 'dual') {
183
+ if (typeof adapter.runNativeNightly !== 'function') throw new Error('native nightly proof adapter is required');
184
+ nativeNightly = await adapter.runNativeNightly({ identity, workflowRunId: String(workflowRunId) });
185
+ }
139
186
  leaves.push(createPublicVerificationLeaf({
140
187
  ...common,
188
+ ...(nativeNightly === undefined ? {} : { nativeNightly }),
141
189
  os,
142
190
  mode,
143
191
  status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
@@ -150,7 +198,12 @@ export async function createPublicVerificationLane({
150
198
  untested: [],
151
199
  skipped: 0,
152
200
  unknown: 0,
153
- }));
201
+ }, { publicKey }));
202
+ }
203
+ if (failures.length) {
204
+ const error = new Error(`retrieval canary acceptance failed for ${failures.map(({ mode }) => mode).join(', ')}`);
205
+ error.publicVerificationFailure = publicVerificationFailure({ os, identity, verifierSha, failures, completedLeaves: leaves });
206
+ throw error;
154
207
  }
155
208
  if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
156
209
  throw new Error('public verification lane modes are incomplete');
@@ -168,8 +221,22 @@ function regularJson(file, label) {
168
221
  return { absolute, bytes: fs.readFileSync(absolute), value: JSON.parse(fs.readFileSync(absolute, 'utf8')) };
169
222
  }
170
223
 
224
+ export function resolveVerifierSha(root, expected) {
225
+ const options = { cwd: root, encoding: 'utf8' };
226
+ const actual = execFileSync('git', ['rev-parse', 'HEAD'], options).trim();
227
+ if (!HEX40.test(actual) || (expected !== undefined && expected !== actual)) {
228
+ throw new Error('verifier SHA differs from the executing checkout');
229
+ }
230
+ if (execFileSync('git', ['status', '--porcelain', '--untracked-files=no'], options).trim()) {
231
+ throw new Error('verifier checkout has tracked changes');
232
+ }
233
+ return actual;
234
+ }
235
+
171
236
  export async function generatePublicVerificationLane({
172
237
  os,
238
+ verifierSha: expectedVerifierSha,
239
+ workflowRunId,
173
240
  candidatePath,
174
241
  identityPath,
175
242
  coveragePath,
@@ -177,10 +244,19 @@ export async function generatePublicVerificationLane({
177
244
  registryPath,
178
245
  outPath,
179
246
  root = process.cwd(),
180
- adapter = livePublicationAdapter({ root }),
247
+ candidateRoot = root,
248
+ adapter = livePublicationAdapter({ root, candidateRoot }),
181
249
  } = {}) {
182
250
  if (os !== hostOperatingSystem()) throw new Error(`runner OS ${hostOperatingSystem() || process.platform} cannot produce ${os || '(missing)'}`);
251
+ const rootDirectory = fs.statSync(root, { bigint: true });
252
+ const scriptDirectory = fs.statSync(fileURLToPath(new URL('..', import.meta.url)), { bigint: true });
253
+ if (!rootDirectory.isDirectory() || rootDirectory.ino <= 0n
254
+ || rootDirectory.dev !== scriptDirectory.dev || rootDirectory.ino !== scriptDirectory.ino) {
255
+ throw new Error('verifier root differs from the executing script checkout');
256
+ }
257
+ const verifierSha = resolveVerifierSha(root, expectedVerifierSha);
183
258
  const candidate = regularJson(candidatePath, 'candidate receipt');
259
+ validateCandidateSource(candidateRoot, candidate.value);
184
260
  const identity = regularJson(identityPath, 'release identity');
185
261
  const releaseCoverage = regularJson(coveragePath, 'release coverage');
186
262
  const retrievalPlan = regularJson(planPath, 'retrieval plan');
@@ -196,6 +272,8 @@ export async function generatePublicVerificationLane({
196
272
  const publication = JSON.parse(fs.readFileSync(publicationPath, 'utf8'));
197
273
  const leaves = await createPublicVerificationLane({
198
274
  os,
275
+ verifierSha,
276
+ workflowRunId,
199
277
  candidate: candidate.value,
200
278
  publication,
201
279
  identity: identity.value,
@@ -206,11 +284,30 @@ export async function generatePublicVerificationLane({
206
284
  hostRegistry: hostRegistry.value,
207
285
  adapter,
208
286
  });
209
- const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-os-lane', os, leaves };
287
+ resolveVerifierSha(root, verifierSha);
288
+ validateCandidateSource(candidateRoot, candidate.value);
289
+ const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-os-lane', os,
290
+ verifierSha, sourceSha: identity.value.candidateSha, leaves };
210
291
  const receipt = { ...payload, laneSha256: digest(payload) };
211
292
  fs.mkdirSync(path.dirname(output), { recursive: true });
212
293
  fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
213
294
  return receipt;
295
+ } catch (error) {
296
+ if (!fs.existsSync(output)) {
297
+ const failure = error.publicVerificationFailure || (() => {
298
+ const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-failure',
299
+ os, verifierSha, sourceSha: identity.value.candidateSha,
300
+ artifactSha256: identity.value.packageSha256, bundleSha256: identity.value.bundleSha256,
301
+ workflowRunId: String(workflowRunId || ''),
302
+ failures: [{ stage: 'public-install-and-native-update', reason: String(error.message).slice(0, 2000) }],
303
+ completedLeaves: [] };
304
+ return { ...payload, failureSha256: digest(payload) };
305
+ })();
306
+ fs.mkdirSync(path.dirname(output), { recursive: true });
307
+ fs.writeFileSync(output, `${JSON.stringify(failure, null, 2)}\n`,
308
+ { flag: 'wx', mode: 0o600 });
309
+ }
310
+ throw error;
214
311
  } finally {
215
312
  await adapter.dispose?.();
216
313
  fs.rmSync(temp, { recursive: true, force: true });
@@ -226,6 +323,9 @@ export async function main(args = process.argv.slice(2)) {
226
323
  try {
227
324
  const receipt = await generatePublicVerificationLane({
228
325
  os: argument(args, '--os'),
326
+ verifierSha: argument(args, '--verifier-sha') ?? undefined,
327
+ workflowRunId: argument(args, '--workflow-run-id'),
328
+ candidateRoot: argument(args, '--candidate-root') ?? undefined,
229
329
  candidatePath: argument(args, '--candidate'),
230
330
  identityPath: argument(args, '--identity'),
231
331
  coveragePath: argument(args, '--coverage'),