harness-everything 0.41.3 → 0.42.1
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/README.md +68 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green-final.log +162 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/live-installed-runtime-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js +143 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.log +7 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js +78 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.log +8 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-review.md +151 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/original-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/red.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-green.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-red.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-green.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-red.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-green.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-red.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/verification.md +31 -0
- package/benchmarks/results/doc-evidence-e1/README.md +68 -0
- package/benchmarks/results/doc-evidence-e1/evidence.json +360 -0
- package/benchmarks/results/doc-evidence-e1/pilot-report.json +175 -0
- package/benchmarks/results/h1-provider-independence-qualification/README.md +95 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md +30 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/evidence.json +128 -0
- package/ci/action-gate.test.js +86 -0
- package/ci/mechanism-2aa-codex-action-gate-policy.test.js +47 -0
- package/ci/mechanism-325-3b-h1-qualification.test.js +206 -0
- package/ci/mechanism-341-opencode-lane.test.js +465 -0
- package/ci/mechanism-task-git-authorization.test.js +12 -0
- package/docs/codex-action-gate.md +27 -1
- package/docs/doc-workflow-reconciliation.md +4 -3
- package/docs/document-lifecycle.md +3 -3
- package/docs/platform-capabilities.md +8 -2
- package/hooks/scripts/action-gate.js +35 -8
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +40 -10
- package/hooks/scripts/lib/doc-reconciliation-provider.js +159 -11
- package/hooks/scripts/workflow-stop-gate.js +4 -1
- package/opencode-plugin/index.mjs +77 -5
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/action-gate.js +35 -8
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +40 -10
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +159 -11
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +4 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -108,12 +108,14 @@ function safeResource(input, explicit) {
|
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function normalizeActionDescriptor(payload) {
|
|
111
|
+
function normalizeActionDescriptor(payload, options = {}) {
|
|
112
112
|
const tool = toolNameOf(payload);
|
|
113
113
|
const input = toolInputOf(payload);
|
|
114
114
|
const explicit = explicitActionDescriptor(payload);
|
|
115
|
-
const
|
|
116
|
-
const
|
|
115
|
+
const codexPrUpdate = detectHost(payload, options.host) === 'codex' && isPullRequestUpdateTool(tool);
|
|
116
|
+
const toolFamily = inferToolFamily(tool, codexPrUpdate ? null : explicit);
|
|
117
|
+
const inferred = inferStructuredAction(tool, explicit);
|
|
118
|
+
const action = codexPrUpdate && !STRUCTURED_GATED_ACTIONS.has(inferred) ? 'mutate' : inferred;
|
|
117
119
|
const classification = STRUCTURED_GATED_ACTIONS.has(action)
|
|
118
120
|
? 'gated'
|
|
119
121
|
: (STRUCTURED_SAFE_ACTIONS.has(action) ? 'safe' : 'unknown');
|
|
@@ -140,8 +142,23 @@ function isRoutinePullRequestCreation(toolName) {
|
|
|
140
142
|
return (last === 'create_pull_request' || name === 'create_pull_request');
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
+
function isPullRequestUpdateTool(name) {
|
|
146
|
+
return name.split('__').pop() === 'update_pull_request' || name === 'mcp__codex_apps__github_update_pull_request';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isPullRequestMetadataUpdate(payload) {
|
|
150
|
+
if (!isPullRequestUpdateTool(toolNameOf(payload))) return false;
|
|
151
|
+
const input = toolInputOf(payload);
|
|
152
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false;
|
|
153
|
+
const keys = Object.keys(input);
|
|
154
|
+
const allowed = new Set(['repository_full_name', 'pr_number', 'owner', 'repo', 'pullNumber', 'pull_number', 'title', 'body']);
|
|
155
|
+
return keys.every(key => allowed.has(key))
|
|
156
|
+
&& keys.some(key => key === 'title' || key === 'body')
|
|
157
|
+
&& keys.filter(key => key === 'title' || key === 'body').every(key => typeof input[key] === 'string');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function structuredRule(payload, options = {}) {
|
|
161
|
+
const descriptor = normalizeActionDescriptor(payload, options);
|
|
145
162
|
if (descriptor.toolFamily === 'shell') return { descriptor, rule: null };
|
|
146
163
|
if (descriptor.classification === 'safe') return { descriptor, rule: null };
|
|
147
164
|
// Routine PR creation is a host-deferred mutation, not a Harness-gated one:
|
|
@@ -152,6 +169,10 @@ function structuredRule(payload) {
|
|
|
152
169
|
if (isRoutinePullRequestCreation(descriptor.toolName) && !(hint && hint.required)) {
|
|
153
170
|
return { descriptor, rule: null };
|
|
154
171
|
}
|
|
172
|
+
if (detectHost(payload, options.host) === 'codex' && descriptor.action === 'mutate'
|
|
173
|
+
&& isPullRequestMetadataUpdate(payload) && !(hint && hint.required)) {
|
|
174
|
+
return { descriptor, rule: null, hostDeferred: true };
|
|
175
|
+
}
|
|
155
176
|
if (descriptor.classification === 'gated') {
|
|
156
177
|
return {
|
|
157
178
|
descriptor,
|
|
@@ -367,7 +388,8 @@ function routerActionGateHint(payload) {
|
|
|
367
388
|
payload && payload.routerContract && payload.routerContract.workflowPlan && payload.routerContract.workflowPlan.actionGate,
|
|
368
389
|
payload && payload.harness && payload.harness.workflowPlan && payload.harness.workflowPlan.actionGate,
|
|
369
390
|
];
|
|
370
|
-
const gate = candidates.find(candidate => candidate && typeof candidate === 'object')
|
|
391
|
+
const gate = candidates.find(candidate => candidate && typeof candidate === 'object' && candidate.required)
|
|
392
|
+
|| candidates.find(candidate => candidate && typeof candidate === 'object');
|
|
371
393
|
if (!gate) return null;
|
|
372
394
|
return {
|
|
373
395
|
required: Boolean(gate.required),
|
|
@@ -435,7 +457,7 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
435
457
|
const exact = exactPayloadText(payload);
|
|
436
458
|
const commandHash = hashExact(exact);
|
|
437
459
|
const now = new Date().toISOString();
|
|
438
|
-
const descriptor = normalizeActionDescriptor(payload);
|
|
460
|
+
const descriptor = normalizeActionDescriptor(payload, { host });
|
|
439
461
|
const record = {
|
|
440
462
|
schemaVersion: 1,
|
|
441
463
|
toolName: toolNameOf(payload),
|
|
@@ -467,11 +489,15 @@ function recordDecision(payload, rule, table, host, disposition, options = {}, e
|
|
|
467
489
|
function evaluatePreToolUse(payload, options = {}) {
|
|
468
490
|
const context = sessionContext(payload, options.sessionDir);
|
|
469
491
|
const table = options.ruleTable || loadRuleTable(options.rulePath || RULES_PATH);
|
|
470
|
-
const structured = structuredRule(payload);
|
|
492
|
+
const structured = structuredRule(payload, options);
|
|
471
493
|
const rule = structured.rule || classify(payload, table.rules, context.sessionDir);
|
|
472
494
|
const host = detectHost(payload, options.host);
|
|
473
495
|
|
|
474
496
|
if (!rule) {
|
|
497
|
+
const record = structured.hostDeferred ? recordDecision(payload, {
|
|
498
|
+
id: 'routine-pr-metadata-update',
|
|
499
|
+
reason: 'Codex PR title/body metadata update defers to native task scope and permissions.',
|
|
500
|
+
}, table, host, 'deferred-to-host', { ...options, sessionDir: context.sessionDir }) : null;
|
|
475
501
|
return {
|
|
476
502
|
kind: 'allow',
|
|
477
503
|
exitCode: 0,
|
|
@@ -480,6 +506,7 @@ function evaluatePreToolUse(payload, options = {}) {
|
|
|
480
506
|
rule: null,
|
|
481
507
|
table,
|
|
482
508
|
descriptor: structured.descriptor,
|
|
509
|
+
...(record ? { record } : {}),
|
|
483
510
|
};
|
|
484
511
|
}
|
|
485
512
|
|
|
@@ -114,9 +114,17 @@ function collect(context) {
|
|
|
114
114
|
// S2b credit chain (F4): produce the snapshot, accept the origin-provider events, and check them against the expected
|
|
115
115
|
// MUSTs. Credit only when the core marks the evidence eligible. Production has no qualification, so it stays withheld.
|
|
116
116
|
function creditChain(context, set, dependencies = {}) {
|
|
117
|
-
|
|
117
|
+
const hostId = activeHostFor(context);
|
|
118
|
+
const providerModule = require('./doc-reconciliation-provider');
|
|
119
|
+
const captureView = hostId === 'opencode' ? providerModule.readOpencodeCaptureView(context.sessionDir)
|
|
120
|
+
: hostId === 'claude-code' ? providerModule.readCaptureView(context.sessionDir)
|
|
121
|
+
: { records: [], tampered: false };
|
|
122
|
+
if (captureView.tampered) {
|
|
118
123
|
return { eligible: false, diagnostics: [{ code: 'journal-tampered', message: 'the capture journal no longer matches its immutable event records' }] };
|
|
119
124
|
}
|
|
125
|
+
if (!['claude-code', 'opencode'].includes(hostId)) {
|
|
126
|
+
return { eligible: false, diagnostics: [{ code: 'unsupported-host', message: `no document origin provider is registered for ${String(hostId)}` }] };
|
|
127
|
+
}
|
|
120
128
|
const { produceDocSnapshot } = require('./doc-reconciliation-producer');
|
|
121
129
|
const { acceptDocEvents } = require('./doc-reconciliation-events');
|
|
122
130
|
const { checkAcceptedDocEvidence } = require('./doc-reconciliation-core');
|
|
@@ -124,29 +132,38 @@ function creditChain(context, set, dependencies = {}) {
|
|
|
124
132
|
const requirements = Array.isArray(set?.requirements) ? set.requirements : [];
|
|
125
133
|
const taskId = `task-${requirements.map(item => item.id).join('+') || 'none'}`;
|
|
126
134
|
const sessionId = context.sessionId;
|
|
127
|
-
const { createClaudeOriginProvider, journalExecutors } =
|
|
128
|
-
const captures =
|
|
129
|
-
const
|
|
130
|
-
|
|
135
|
+
const { createClaudeOriginProvider, createOpencodeOriginProvider, journalExecutors } = providerModule;
|
|
136
|
+
const captures = captureView.records;
|
|
137
|
+
const principals = hostId === 'opencode'
|
|
138
|
+
? { author: sessionId, executors: [sessionId] }
|
|
139
|
+
: { author: sessionId, executors: journalExecutors(captures, sessionId) };
|
|
140
|
+
const createOriginProvider = hostId === 'opencode' ? createOpencodeOriginProvider : createClaudeOriginProvider;
|
|
141
|
+
const providerInput = {
|
|
142
|
+
hostId,
|
|
131
143
|
sessionId,
|
|
132
144
|
workspaceRoot: context.root,
|
|
133
145
|
workflowId: context.workflow.workflowId,
|
|
134
146
|
workflowRevision: context.workflow.revision,
|
|
135
147
|
taskId,
|
|
136
|
-
principals
|
|
148
|
+
principals,
|
|
137
149
|
baseline,
|
|
138
150
|
captures,
|
|
139
|
-
}
|
|
151
|
+
};
|
|
152
|
+
const originProvider = dependencies.originProvider || createOriginProvider(providerInput);
|
|
140
153
|
const produced = produceDocSnapshot({
|
|
141
154
|
workspaceRoot: context.root,
|
|
142
155
|
baseline,
|
|
143
156
|
workflow: { workflowId: context.workflow.workflowId, sessionId, workflowRevision: context.workflow.revision },
|
|
144
157
|
task: { taskId, requirements },
|
|
145
158
|
scope: { prdRoot: 'docs/prd/' },
|
|
146
|
-
activeHost:
|
|
159
|
+
activeHost: hostId,
|
|
147
160
|
prdRoot: 'docs/prd/',
|
|
148
161
|
}, { originProvider });
|
|
149
|
-
if (produced.state !== 'ready' || !produced.snapshot) return {
|
|
162
|
+
if (produced.state !== 'ready' || !produced.snapshot) return {
|
|
163
|
+
eligible: false,
|
|
164
|
+
diagnostics: produced.diagnostics || [],
|
|
165
|
+
unsatisfied: [],
|
|
166
|
+
};
|
|
150
167
|
const eventRefs = captureReferences(context);
|
|
151
168
|
const accepted = acceptDocEvents({ snapshot: produced.snapshot, eventRefs }, { originProvider });
|
|
152
169
|
const checked = checkAcceptedDocEvidence({ snapshot: produced.snapshot, accepted, mode: 'verification' });
|
|
@@ -181,6 +198,7 @@ function reviewRequired(set) {
|
|
|
181
198
|
// The review step is independent of credit qualification: the step records host-observed independence, while document
|
|
182
199
|
// credit itself still needs the validated qualification (assess).
|
|
183
200
|
function independentReviewAccepted(context) {
|
|
201
|
+
if (activeHostFor(context) !== 'claude-code') return false;
|
|
184
202
|
const { reviewRecordIndependent, journalExecutors } = require('./doc-reconciliation-provider');
|
|
185
203
|
const view = require('./doc-reconciliation-provider').readCaptureView(context.sessionDir);
|
|
186
204
|
if (view.tampered) return false;
|
|
@@ -196,10 +214,18 @@ function independentReviewAccepted(context) {
|
|
|
196
214
|
|
|
197
215
|
function recordOf(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
198
216
|
|
|
217
|
+
function activeHostFor(context) {
|
|
218
|
+
return context.activeHost === undefined || context.activeHost === null ? 'claude-code' : context.activeHost;
|
|
219
|
+
}
|
|
220
|
+
|
|
199
221
|
// The session capture records are the only host-event source. A missing journal means no events; a journal that no longer
|
|
200
222
|
// equals its immutable event files is tampered and yields no events, so the chain withholds.
|
|
201
223
|
function readCaptures(context) {
|
|
202
|
-
|
|
224
|
+
const provider = require('./doc-reconciliation-provider');
|
|
225
|
+
const hostId = activeHostFor(context);
|
|
226
|
+
if (hostId === 'opencode') return provider.readOpencodeCaptureView(context.sessionDir).records;
|
|
227
|
+
if (hostId === 'claude-code') return provider.readCaptureView(context.sessionDir).records;
|
|
228
|
+
return [];
|
|
203
229
|
}
|
|
204
230
|
|
|
205
231
|
// Only explicit PostToolUse doc-check candidates are offered; ordinary Edit or Agent results are not (R3-01).
|
|
@@ -208,6 +234,10 @@ const docCheckCandidate = item => item.event === 'PostToolUse'
|
|
|
208
234
|
|
|
209
235
|
function captureReferences(context) {
|
|
210
236
|
const captures = readCaptures(context);
|
|
237
|
+
if (activeHostFor(context) === 'opencode') {
|
|
238
|
+
return captures.filter(item => typeof item?.call_id === 'string' && item.call_id.trim())
|
|
239
|
+
.flatMap(item => ['doc-lint', 'link-check'].map(capability => `${item.call_id}#${capability}`));
|
|
240
|
+
}
|
|
211
241
|
const candidates = captures
|
|
212
242
|
.filter(item => item && typeof item.ref === 'string' && (item.event === 'SubagentStop' || docCheckCandidate(item)))
|
|
213
243
|
.filter(item => item.event !== 'SubagentStop');
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// qualification object ({ source: 'fake-test-fixture', faked: true }) is a
|
|
16
16
|
// test-only construct for mechanism tests; production passes no
|
|
17
17
|
// qualification and stays gated.
|
|
18
|
-
// -
|
|
19
|
-
//
|
|
18
|
+
// - The Claude provider accepts only Claude. OpenCode has a separate
|
|
19
|
+
// fail-closed mapping and never borrows Claude's exit or reviewer rules.
|
|
20
20
|
// - Exit mapping (owner decision row 1): for a given `tool_use_id`, a captured
|
|
21
21
|
// `PostToolUse` with `interrupted === false` and no `PostToolUseFailure`
|
|
22
22
|
// arrival maps to exit 0. This is a named mapping of the host's own
|
|
@@ -37,6 +37,10 @@ const path = require('path');
|
|
|
37
37
|
|
|
38
38
|
const HOST_ID = 'claude-code';
|
|
39
39
|
const CAPTURE_RECORD_VERSION = 'claude-capture-record/1';
|
|
40
|
+
const OPENCODE_HOST_ID = 'opencode';
|
|
41
|
+
const OPENCODE_CAPTURE_RECORD_VERSION = 'opencode-capture-record/1';
|
|
42
|
+
const OPENCODE_CAPTURE_FILE = 'opencode-captures.jsonl';
|
|
43
|
+
const OPENCODE_CAPTURE_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
40
44
|
const QUALIFICATION_EXPORT_SCHEMA = 'harness.doc-reconciliation.qualification.v1';
|
|
41
45
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
42
46
|
const PRE_CAPTURE_TOOLS = ['Bash', 'PowerShell', 'Task', 'Agent'];
|
|
@@ -522,10 +526,14 @@ function observedCorrelation(records) {
|
|
|
522
526
|
|
|
523
527
|
// Validates the qualification export against the sanitized capture it names: schema, host, capture-record version, the
|
|
524
528
|
// capture's digest, and every claimed kind, field and correlation. Returns null when valid, else the reason.
|
|
525
|
-
function validateQualificationExport() {
|
|
529
|
+
function validateQualificationExport(expectedHost = HOST_ID) {
|
|
526
530
|
const exported = JSON.parse(fs.readFileSync(path.join(__dirname, EXPORT_FILE), 'utf8'));
|
|
527
531
|
if (!recordOf(exported) || exported.schema !== QUALIFICATION_EXPORT_SCHEMA) return 'qualification export schema is not recognized';
|
|
528
|
-
if (
|
|
532
|
+
if (!textOf(exported.hostVersion)) return 'qualification export host or host version is missing';
|
|
533
|
+
if (exported.host !== expectedHost) {
|
|
534
|
+
if (expectedHost === HOST_ID) return 'qualification export host or host version is missing';
|
|
535
|
+
return 'qualification export host does not match the selected provider';
|
|
536
|
+
}
|
|
529
537
|
if (exported.captureRecordVersion !== CAPTURE_RECORD_VERSION) return 'qualification export capture record version is not supported';
|
|
530
538
|
const source = recordOf(exported.sanitizedCapture) ? exported.sanitizedCapture : {};
|
|
531
539
|
// The sanitized capture is a sibling of the export, so source and packaged copies resolve it the same way.
|
|
@@ -547,7 +555,7 @@ function validateQualificationExport() {
|
|
|
547
555
|
return null;
|
|
548
556
|
}
|
|
549
557
|
|
|
550
|
-
function evaluateQualification(input) {
|
|
558
|
+
function evaluateQualification(input, expectedHost = HOST_ID) {
|
|
551
559
|
if (recordOf(input) && input.qualification !== undefined && input.qualification !== null) {
|
|
552
560
|
const supplied = input.qualification;
|
|
553
561
|
// Test-only DI path: an explicitly fake qualification object. Anything else
|
|
@@ -567,7 +575,7 @@ function evaluateQualification(input) {
|
|
|
567
575
|
// against the sanitized capture it names. Nothing is read from the environment, capture flags or ancestor directories.
|
|
568
576
|
let reason = null;
|
|
569
577
|
try {
|
|
570
|
-
reason = validateQualificationExport();
|
|
578
|
+
reason = validateQualificationExport(expectedHost);
|
|
571
579
|
} catch (error) {
|
|
572
580
|
reason = `qualification evidence could not be read (${error.code || 'error'})`;
|
|
573
581
|
}
|
|
@@ -582,11 +590,11 @@ function evaluateQualification(input) {
|
|
|
582
590
|
|
|
583
591
|
// ---- Shared provider state ------------------------------------------------------
|
|
584
592
|
|
|
585
|
-
function supportedHostCheck(input) {
|
|
586
|
-
if (recordOf(input) && input.hostId ===
|
|
593
|
+
function supportedHostCheck(input, expectedHost = HOST_ID) {
|
|
594
|
+
if ([HOST_ID, OPENCODE_HOST_ID].includes(expectedHost) && recordOf(input) && input.hostId === expectedHost) return null;
|
|
587
595
|
const host = recordOf(input) && typeof input.hostId === 'string' ? input.hostId : 'unknown';
|
|
588
596
|
return diagnostic('unsupported-host', {
|
|
589
|
-
reason: `unsupported host ${host} has no
|
|
597
|
+
reason: `unsupported host ${host} has no matching capture implementation; hosts must not borrow another host's exit mapping`,
|
|
590
598
|
});
|
|
591
599
|
}
|
|
592
600
|
|
|
@@ -892,6 +900,127 @@ const createClaudeOriginProvider = input => {
|
|
|
892
900
|
return { resolveContext, resolveEvent };
|
|
893
901
|
};
|
|
894
902
|
|
|
903
|
+
function opencodeContentSignature(cap) {
|
|
904
|
+
return captureJson({
|
|
905
|
+
hostId: cap.hostId,
|
|
906
|
+
tool: cap.tool === undefined ? null : cap.tool,
|
|
907
|
+
command: cap.command === undefined ? null : cap.command,
|
|
908
|
+
cwd: cap.cwd === undefined ? null : cap.cwd,
|
|
909
|
+
status: cap.status === undefined ? null : cap.status,
|
|
910
|
+
exit: cap.exit === undefined ? null : cap.exit,
|
|
911
|
+
resultDigest: cap.result_digest === undefined ? null : cap.result_digest,
|
|
912
|
+
error: cap.error === undefined ? null : cap.error,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const createOpencodeOriginProvider = input => {
|
|
917
|
+
const safeInput = recordOf(input) ? input : {};
|
|
918
|
+
|
|
919
|
+
function resolveContext(request) {
|
|
920
|
+
const hostFail = supportedHostCheck(safeInput, OPENCODE_HOST_ID);
|
|
921
|
+
if (hostFail) return { status: 'unavailable', diagnostics: [hostFail] };
|
|
922
|
+
const qualification = evaluateQualification(safeInput, OPENCODE_HOST_ID);
|
|
923
|
+
if (!qualification.open) return { status: 'unavailable', diagnostics: [qualification.fail] };
|
|
924
|
+
const workspaceRoot = textOf(safeInput.workspaceRoot) ? safeInput.workspaceRoot : (recordOf(request) && textOf(request.workspaceRoot) ? request.workspaceRoot : '');
|
|
925
|
+
const sessionId = textOf(safeInput.sessionId) ? safeInput.sessionId : '';
|
|
926
|
+
const workflowId = textOf(safeInput.workflowId) ? safeInput.workflowId : '';
|
|
927
|
+
const workflowRevision = Number.isInteger(safeInput.workflowRevision) ? safeInput.workflowRevision : null;
|
|
928
|
+
const taskId = textOf(safeInput.taskId) ? safeInput.taskId : '';
|
|
929
|
+
if (!workspaceRoot || !sessionId || !workflowId || workflowRevision === null || !taskId) {
|
|
930
|
+
return { status: 'unavailable', diagnostics: [diagnostic('invalid-input', { reason: 'OpenCode origin requires the recorded workspace, session, workflow, revision and task identity' })] };
|
|
931
|
+
}
|
|
932
|
+
const suppliedPrincipals = recordOf(safeInput.principals) ? safeInput.principals : {};
|
|
933
|
+
const author = textOf(suppliedPrincipals.author) ? suppliedPrincipals.author : sessionId;
|
|
934
|
+
const executors = Array.isArray(suppliedPrincipals.executors) ? suppliedPrincipals.executors.filter(textOf) : [sessionId];
|
|
935
|
+
const originRef = 'origin://opencode/' + digestText(captureJson({
|
|
936
|
+
hostId: OPENCODE_HOST_ID, sessionId, workspaceRoot, taskId, workflowId, workflowRevision,
|
|
937
|
+
author, executors, qualificationSource: qualification.source,
|
|
938
|
+
}));
|
|
939
|
+
return {
|
|
940
|
+
status: 'accepted', originRef,
|
|
941
|
+
context: {
|
|
942
|
+
workspaceId: 'ws-' + digestText(captureJson({ workspaceRoot })), sessionId, workflowId, workflowRevision, taskId,
|
|
943
|
+
principals: { author, executors: executors.slice() }, activeHost: OPENCODE_HOST_ID,
|
|
944
|
+
baselineReference: `retained-live-capture:opencode-341:${digestText(originRef)}`,
|
|
945
|
+
...(recordOf(safeInput.baseline) && textOf(safeInput.baseline.commit) && textOf(safeInput.baseline.root)
|
|
946
|
+
? { baseline: { commit: safeInput.baseline.commit, root: safeInput.baseline.root } } : {}),
|
|
947
|
+
workspaceRoot,
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function resolveEvent(request) {
|
|
953
|
+
if (!recordOf(request) || !textOf(request.reference)) return unavailable('invalid-input', 'resolveEvent requires a nonempty event reference');
|
|
954
|
+
const hostFail = supportedHostCheck(safeInput, OPENCODE_HOST_ID);
|
|
955
|
+
if (hostFail) return { status: 'unavailable', diagnostics: [hostFail] };
|
|
956
|
+
const qualification = evaluateQualification(safeInput, OPENCODE_HOST_ID);
|
|
957
|
+
if (!qualification.open) return { status: 'unavailable', diagnostics: [qualification.fail] };
|
|
958
|
+
const [captureRef, capability] = String(request.reference).split('#');
|
|
959
|
+
const captures = Array.isArray(safeInput.captures) ? safeInput.captures : [];
|
|
960
|
+
const matches = captures.map((item, index) => ({ item, index })).filter(({ item }) => recordOf(item) && item.call_id === captureRef);
|
|
961
|
+
if (!matches.length) return unavailable('origin-unavailable', `no captured OpenCode event is retained for reference ${request.reference}`);
|
|
962
|
+
if (matches.length > 1) return rejected('replayed-record', 'multiple OpenCode records share this call id');
|
|
963
|
+
const { item: cap, index } = matches[0];
|
|
964
|
+
if (cap.hostId !== OPENCODE_HOST_ID || cap.schema !== OPENCODE_CAPTURE_RECORD_VERSION
|
|
965
|
+
|| cap.session_id !== safeInput.sessionId) {
|
|
966
|
+
return rejected('record-origin-mismatch', 'the capture host and session must match this OpenCode provider invocation');
|
|
967
|
+
}
|
|
968
|
+
const observed = cap.observedIdentity;
|
|
969
|
+
const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
|
|
970
|
+
if (!recordOf(observed)) return rejected('identity-missing', 'OpenCode capture lacks workflow identity observed at capture time');
|
|
971
|
+
const sameWorkflow = observed.workflowId === safeInput.workflowId && observed.workflowRevision === safeInput.workflowRevision;
|
|
972
|
+
const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit && observed.baselinePin.root === pin.root);
|
|
973
|
+
if (!sameWorkflow || !samePin) return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
|
|
974
|
+
const signature = opencodeContentSignature(cap);
|
|
975
|
+
const firstOccurrence = captures.findIndex(item => recordOf(item) && opencodeContentSignature(item) === signature);
|
|
976
|
+
if (firstOccurrence !== index) return rejected('replayed-record', `identical OpenCode tool content already recorded (first occurrence index ${firstOccurrence})`);
|
|
977
|
+
if (cap.error !== undefined && cap.error !== null) return rejected('failure-not-credited', 'the native tool event carries an error');
|
|
978
|
+
if (typeof cap.status !== 'string' || !cap.status) return rejected('outcome-ambiguous', 'the native tool status is missing');
|
|
979
|
+
if (cap.status !== 'completed') return rejected(cap.status === 'error' ? 'failure-not-credited' : 'outcome-ambiguous', `native tool status ${cap.status} does not prove completion`);
|
|
980
|
+
const processTool = ['bash', 'shell', 'powershell', 'cmd'].includes(String(cap.tool).toLowerCase());
|
|
981
|
+
if (processTool && !Number.isInteger(cap.exit)) return rejected('outcome-ambiguous', 'a process result without a numeric native exit is ambiguous');
|
|
982
|
+
if (processTool && cap.exit !== 0) return rejected('failure-not-credited', `the native process exit was ${cap.exit}`);
|
|
983
|
+
if (!/^[0-9a-f]{64}$/.test(String(cap.result_digest || '')) || !Number.isInteger(cap.result_bytes) || cap.result_bytes < 0) {
|
|
984
|
+
return rejected('stdout-withheld', 'the native result digest and byte count are absent');
|
|
985
|
+
}
|
|
986
|
+
const author = recordOf(safeInput.principals) && textOf(safeInput.principals.author) ? safeInput.principals.author : safeInput.sessionId;
|
|
987
|
+
if (capability === 'quality-review') {
|
|
988
|
+
if (!textOf(cap.agent_id)) return rejected('reviewer-principal-missing', 'OpenCode supplied no host-issued reviewer identity');
|
|
989
|
+
if (cap.agent_id === author) return rejected('reviewer-not-independent', 'the reviewer identity equals the author session principal');
|
|
990
|
+
return rejected('reviewer-principal-missing', 'OpenCode supplied no host-issued independent-review record');
|
|
991
|
+
}
|
|
992
|
+
if (capability) {
|
|
993
|
+
if (!['doc-lint', 'link-check'].includes(capability)) return rejected('capability-mismatch', `OpenCode cannot supply capability ${capability}`);
|
|
994
|
+
if (!textOf(cap.command)) return rejected('command-missing', 'the native record has no command for a registered doc-check');
|
|
995
|
+
if (!textOf(cap.cwd)) return rejected('unregistered-command', 'the host omitted per-tool cwd, so the doc-check invocation cannot be validated');
|
|
996
|
+
const registered = resolveDocCheckInvocation(cap.command, cap.cwd);
|
|
997
|
+
if (registered.kind !== 'accepted') return rejected('unregistered-command', `doc-check invocation rejected (${registered.reason}); no credit`);
|
|
998
|
+
return rejected('stdout-withheld', 'the OpenCode result body is retained only as a digest and cannot prove doc-check output');
|
|
999
|
+
}
|
|
1000
|
+
if (!textOf(cap.command)) return rejected('command-missing', 'captured OpenCode record has no retained process command');
|
|
1001
|
+
if (!textOf(cap.cwd)) return rejected('outcome-ambiguous', 'the host omitted per-tool cwd');
|
|
1002
|
+
if (!processTool) return rejected('capture-event-unsupported', `tool ${String(cap.tool)} has no OpenCode process mapping`);
|
|
1003
|
+
if (!recordOf(request.identity)) return rejected('identity-missing', 'execution evidence needs a snapshot identity');
|
|
1004
|
+
const contextResponse = resolveContext(request);
|
|
1005
|
+
if (contextResponse.status !== 'accepted') return unavailable('origin-unavailable', 'origin context did not resolve');
|
|
1006
|
+
const identity = JSON.parse(JSON.stringify(request.identity));
|
|
1007
|
+
const resultRef = `opencode-result/${cap.call_id}/${cap.result_digest}`;
|
|
1008
|
+
const event = {
|
|
1009
|
+
eventId: 'opencode-exec-' + digestText(signature + ':execution'),
|
|
1010
|
+
producerId: 'opencode-origin-provider', originRef: contextResponse.originRef,
|
|
1011
|
+
principalId: cap.session_id, provenance: 'opencode-capture:' + cap.call_id,
|
|
1012
|
+
capability: 'shell-command', kind: 'execution', command: cap.command, argv: [cap.command], cwd: cap.cwd,
|
|
1013
|
+
exitCode: cap.exit, signal: null, toolCorrelationId: cap.call_id, resultRef,
|
|
1014
|
+
identity, startIdentity: identity, endIdentity: identity,
|
|
1015
|
+
paths: [], artifactDigests: {}, captured: { responseRef: resultRef, responseDigest: cap.result_digest },
|
|
1016
|
+
};
|
|
1017
|
+
return { status: 'accepted', originRef: contextResponse.originRef, event,
|
|
1018
|
+
correlation: { toolCorrelationId: cap.call_id, resultRef } };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return { resolveContext, resolveEvent };
|
|
1022
|
+
};
|
|
1023
|
+
|
|
895
1024
|
// ---- Capture record builder (consumed by state-persist.js) ----------------------
|
|
896
1025
|
|
|
897
1026
|
// Builds the retained capture record for a Claude hook payload. The field names
|
|
@@ -1028,6 +1157,24 @@ function readCaptureView(sessionDir) {
|
|
|
1028
1157
|
return { records: view, tampered: false };
|
|
1029
1158
|
}
|
|
1030
1159
|
|
|
1160
|
+
function readOpencodeCaptureView(sessionDir) {
|
|
1161
|
+
const file = path.join(sessionDir, OPENCODE_CAPTURE_FILE);
|
|
1162
|
+
try {
|
|
1163
|
+
if (fs.statSync(file).size > OPENCODE_CAPTURE_LIMIT_BYTES) return { records: [], tampered: true };
|
|
1164
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
1165
|
+
const records = text.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
|
|
1166
|
+
if (!records.every(item => recordOf(item) && item.schema === OPENCODE_CAPTURE_RECORD_VERSION
|
|
1167
|
+
&& item.hostId === OPENCODE_HOST_ID && textOf(item.session_id) && textOf(item.call_id)
|
|
1168
|
+
&& textOf(item.tool) && Object.prototype.hasOwnProperty.call(item, 'status'))) {
|
|
1169
|
+
return { records: [], tampered: true };
|
|
1170
|
+
}
|
|
1171
|
+
return { records, tampered: false };
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (error.code === 'ENOENT') return { records: [], tampered: false };
|
|
1174
|
+
return { records: [], tampered: true };
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1031
1178
|
// Observation-time identity: the workflow and pin the host event was captured under. Absent before a workflow starts.
|
|
1032
1179
|
function observedIdentityFor(sessionDir) {
|
|
1033
1180
|
try {
|
|
@@ -1053,7 +1200,8 @@ function reviewRecordIndependent(cap, captures, principals) {
|
|
|
1053
1200
|
}
|
|
1054
1201
|
|
|
1055
1202
|
// Keep the old state-persist seam as an alias; it points at the single resolver above.
|
|
1056
|
-
module.exports = { createClaudeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
|
|
1203
|
+
module.exports = { createClaudeOriginProvider, createOpencodeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
|
|
1057
1204
|
registeredInstalledCheck: resolveDocCheckInvocation, installedDocCheck,
|
|
1058
1205
|
fileDigest, SHIPPED_DOC_CHECK_DIGEST, trustRisk, journalExecutors, reviewRecordIndependent,
|
|
1059
|
-
writeImmutableEvent, rebuildJournalView, readCaptureView,
|
|
1206
|
+
writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
|
|
1207
|
+
_internals: { captureJson, captureDigest, digestText, canonicalCapture } };
|
|
@@ -14,6 +14,7 @@ function decide(payload) {
|
|
|
14
14
|
try {
|
|
15
15
|
const { planningUnresolved, unresolvedObligations } = require('./lib/workflow-obligations');
|
|
16
16
|
const context = loadWorkflow(payload);
|
|
17
|
+
context.activeHost = payload && typeof payload.host_id === 'string' ? payload.host_id : undefined;
|
|
17
18
|
const { workflow, sessionDir } = context;
|
|
18
19
|
if (!workflow || workflow.state === 'deferred') return;
|
|
19
20
|
const handoffFile = path.join(sessionDir, 'handoff-state.json');
|
|
@@ -93,4 +94,6 @@ function decide(payload) {
|
|
|
93
94
|
} catch (error) { warn(error && error.message ? error.message : String(error)); }
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
readHookInput(decide);
|
|
97
|
+
if (require.main === module) readHookInput(decide);
|
|
98
|
+
|
|
99
|
+
module.exports = { decide };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.42.1",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
package/sbom.cdx.json
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
"$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.7",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e3ce04bb-3f54-5090-bffc-a7fca1d360db",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-26T06:57:43.886Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.42.1"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.42.1",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.
|
|
20
|
+
"version": "0.42.1",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.42.1"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -2033,7 +2033,7 @@
|
|
|
2033
2033
|
]
|
|
2034
2034
|
},
|
|
2035
2035
|
{
|
|
2036
|
-
"ref": "pkg:npm/harness-everything@0.
|
|
2036
|
+
"ref": "pkg:npm/harness-everything@0.42.1",
|
|
2037
2037
|
"dependsOn": [
|
|
2038
2038
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2039
2039
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|