harness-everything 0.42.0 → 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.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/benchmarks/results/codex-pr-metadata-2026-10-26/README.md +68 -0
  5. package/benchmarks/results/codex-pr-metadata-2026-10-26/green-final.log +162 -0
  6. package/benchmarks/results/codex-pr-metadata-2026-10-26/green.log +158 -0
  7. package/benchmarks/results/codex-pr-metadata-2026-10-26/live-installed-runtime-rejection.json +31 -0
  8. package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js +143 -0
  9. package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.log +7 -0
  10. package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js +78 -0
  11. package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.log +8 -0
  12. package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-review.md +151 -0
  13. package/benchmarks/results/codex-pr-metadata-2026-10-26/original-rejection.json +31 -0
  14. package/benchmarks/results/codex-pr-metadata-2026-10-26/red.log +158 -0
  15. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-green.log +192 -0
  16. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-red.log +192 -0
  17. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-green.log +206 -0
  18. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-red.log +206 -0
  19. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-green.log +174 -0
  20. package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-red.log +174 -0
  21. package/benchmarks/results/codex-pr-metadata-2026-10-26/verification.md +31 -0
  22. package/benchmarks/results/doc-evidence-e1/README.md +68 -0
  23. package/benchmarks/results/doc-evidence-e1/evidence.json +360 -0
  24. package/benchmarks/results/doc-evidence-e1/pilot-report.json +175 -0
  25. package/benchmarks/results/h1-provider-independence-qualification/README.md +26 -3
  26. package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md +30 -0
  27. package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/evidence.json +128 -0
  28. package/ci/action-gate.test.js +86 -0
  29. package/ci/mechanism-2aa-codex-action-gate-policy.test.js +47 -0
  30. package/ci/mechanism-task-git-authorization.test.js +12 -0
  31. package/docs/codex-action-gate.md +27 -1
  32. package/docs/doc-workflow-reconciliation.md +4 -3
  33. package/docs/document-lifecycle.md +3 -3
  34. package/docs/platform-capabilities.md +8 -2
  35. package/hooks/scripts/action-gate.js +35 -8
  36. package/opencode-plugin/plugin.json +1 -1
  37. package/package.json +1 -1
  38. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  39. package/plugins/harness-everything/hooks/scripts/action-gate.js +35 -8
  40. package/plugins/harness-everything/plugin.json +1 -1
  41. package/sbom.cdx.json +7 -7
@@ -0,0 +1,128 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "clientDate": "2026-10-25",
4
+ "classification": "retained-negative-observations-only",
5
+ "publicReproduction": "LOCAL_ONLY supporting packets, fixtures, and runner are not shipped; this PR does not reproduce full runs",
6
+ "productBaseline": "37c97046cb1e620c4a3585b4da5a2593501a48be",
7
+ "runs": [
8
+ {
9
+ "id": "D1",
10
+ "engine": "OpenCode v2.1.4",
11
+ "requestedModel": "opencode/beam-free#default",
12
+ "fixtureCommit": "f486e57d1ba17d92e585b0b42dd4b9c71b2fda4a",
13
+ "sessionId": "ses_e8de7be3cffeBTghRaz6CN1T01",
14
+ "sessionOutcome": "failed",
15
+ "providerFailureType": "provider.free-limit",
16
+ "directSanitizedExportErrorTypes": [
17
+ "provider.free-limit",
18
+ "provider.free-limit"
19
+ ],
20
+ "process": {
21
+ "shellPipelineExit": 1,
22
+ "opencodeProcessExit": null,
23
+ "opencodeProcessExitStatus": "UNKNOWN: pipeline status captured without PIPESTATUS"
24
+ },
25
+ "sanitizedExport": {
26
+ "exitCode": 0,
27
+ "messageCount": 4,
28
+ "sha256": "91f80a5e8b2c94015a5a5602022aa45436cff2a52295615f7eb022aed91eee2b",
29
+ "source": "LOCAL_ONLY:.worktrees/packets/resume-2026-10-25/D1-current/fixture/trace/native-export.json"
30
+ },
31
+ "fixture": {
32
+ "gitCleanBeforeAndAfter": true,
33
+ "configSha256": "422eb8c6481aaadbf7910d4b415e76b2e60723bd0da4c532a94fad1879c38bcd",
34
+ "adrBeforeSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
35
+ "adrAfterSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
36
+ "taskPromptSha256": "524e58eeb6bd64e33986777b19a7fca8ec0829154fc2eb4b0c4a26b8e13b7b5e"
37
+ },
38
+ "observations": {
39
+ "naturalCompletionObserved": false,
40
+ "observerLoadMarkerObserved": false,
41
+ "observerTracePresent": false,
42
+ "pilotReportPresent": false,
43
+ "innerConsumerCause": "UNCHECKED",
44
+ "globalHarnessAbsence": "UNCHECKED"
45
+ },
46
+ "sourceArtifactSha256": {
47
+ "beforeState": "91018ac09c0e58537b53ffbef6adb1d2ecd5cc5b079cccc60484493aa592d474",
48
+ "afterState": "d2f729d1f5eb29b6a17b552fb06a31833ecbe4ef3eda5096e895ebfab212518c",
49
+ "filteredRunLog": "b1f1b48237c94733a6753440617e52c85ee754d1287a78b94d2513e5fdfa07a5",
50
+ "observerImplementation": "33a3a3c8da4f5dc495f7462952b11ee8ad46c4fa9ba0882152acd0eebf0ed56f",
51
+ "taskLocalPluginEntrypoint": "ad4eef698164e644e52eeb0c6f36bef56ca75a5c66d7cd71345d0091108f8fbe"
52
+ }
53
+ },
54
+ {
55
+ "id": "D2",
56
+ "engine": "OpenCode CLI invocation; session identity UNKNOWN",
57
+ "requestedModel": "nvidia/deepseek-ai/deepseek-v4.1-flash",
58
+ "productBaseline": "37c97046cb1e620c4a3585b4da5a2593501a48be",
59
+ "fixtureCommit": "39aa08987e0da2fc2ff3ea3556b84bd6d37f4e79",
60
+ "nvidiaPreflight": {
61
+ "catalogModel": "nvidia/deepseek-ai/deepseek-v4.1-flash",
62
+ "configuredAuthPresence": true,
63
+ "variant": "UNKNOWN",
64
+ "evidenceLayer": "non-session preflight only",
65
+ "providerRequestMadeDuringPreflight": false
66
+ },
67
+ "process": {
68
+ "exitCode": 1,
69
+ "signal": null,
70
+ "timedOut": false,
71
+ "spawnFailureReason": null,
72
+ "stdoutJsonlRecords": 1,
73
+ "filteredEventType": "error",
74
+ "sessionId": null,
75
+ "sessionExistence": "UNKNOWN",
76
+ "stderrBytes": 0,
77
+ "rawOutputRetained": false
78
+ },
79
+ "errorInterpretation": {
80
+ "errorDiscriminantRetained": false,
81
+ "captureLimit": "allowlist omitted the error discriminant",
82
+ "cause": "UNKNOWN",
83
+ "providerRequestStatus": "UNKNOWN"
84
+ },
85
+ "fixture": {
86
+ "gitCleanBeforeAndAfter": true,
87
+ "configSha256": "422eb8c6481aaadbf7910d4b415e76b2e60723bd0da4c532a94fad1879c38bcd",
88
+ "adrBeforeSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
89
+ "adrAfterSha256": "65c903eedc1be2ca6022638fd9cab1f70c08c278e9e03be5c73ad0b2ecab4d18",
90
+ "taskPromptSha256": "524e58eeb6bd64e33986777b19a7fca8ec0829154fc2eb4b0c4a26b8e13b7b5e",
91
+ "adrChanged": false
92
+ },
93
+ "observations": {
94
+ "naturalCompletionObserved": false,
95
+ "observerTracePresent": false,
96
+ "observerLoad": "NOT_OBSERVED",
97
+ "sanitizedExportAttempted": false,
98
+ "reportCapture": "UNKNOWN: no session ID was available for lookup",
99
+ "globalHarnessAbsence": "UNCHECKED",
100
+ "isolation": "UNCHECKED"
101
+ },
102
+ "sourceArtifactSha256": {
103
+ "exitMetadata": "61f8904e70fa9266f52437d536a43d9f8c34b338fa1c0f55827cf1527b7f7582",
104
+ "filteredRunLog": "b2261fe453f0645fb8d7606979e0c4a8b7c08095a6fc1fa3dbb9a670fbf78806",
105
+ "observerImplementation": "33a3a3c8da4f5dc495f7462952b11ee8ad46c4fa9ba0882152acd0eebf0ed56f",
106
+ "taskLocalPluginEntrypoint": "ad4eef698164e644e52eeb0c6f36bef56ca75a5c66d7cd71345d0091108f8fbe"
107
+ }
108
+ }
109
+ ],
110
+ "localRunnerSelfCheck": {
111
+ "result": "PASS",
112
+ "checks": [
113
+ "child exit code 23",
114
+ "timeout and signal capture",
115
+ "spawn failure maps to null exit plus reason"
116
+ ],
117
+ "evidenceLayer": "deterministic mechanism only; not live-host evidence",
118
+ "runnerSource": "LOCAL_ONLY:.worktrees/packets/resume-2026-10-25/D2-nvidia-current/scripts/d2-runner.mjs"
119
+ },
120
+ "qualification": {
121
+ "e1Cycle": false,
122
+ "e1Go": false,
123
+ "behaviorBenefit": false,
124
+ "qualifies": false,
125
+ "nvidiaOutageClaim": false,
126
+ "prClaim": "retained negative observations only"
127
+ }
128
+ }
@@ -353,6 +353,92 @@ try {
353
353
 
354
354
  check(fs.readFileSync(rulesPath, 'utf8') === fs.readFileSync(pluginRules, 'utf8'), 'canonical and plugin action-gate rule tables are byte-identical');
355
355
  check(fs.existsSync(pluginScript), 'OpenAI plugin packages action-gate runtime');
356
+
357
+ // Codex PR metadata scope: exact operation AND actual argument allowlist.
358
+ const metadataInput = { repository_full_name: 'acme/repo', pr_number: 376, body: 'metadata fixture' };
359
+ const metadataPayload = (input = metadataInput, tool = 'mcp__codex_apps__github__update_pull_request', extra = {}) => ({
360
+ ...payload('', { tool }), tool_input: input, ...extra,
361
+ });
362
+ for (const tool of ['update_pull_request', 'mcp__github__update_pull_request', 'mcp__codex_apps__github__update_pull_request', 'mcp__codex_apps__github_update_pull_request']) {
363
+ for (const fields of [{ body: '' }, { title: 'new title' }, { title: 'new title', body: 'new body' }]) {
364
+ const candidate = metadataPayload({ repository_full_name: 'acme/repo', pr_number: 376, ...fields }, tool);
365
+ const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
366
+ check(result.kind === 'allow' && result.stdout === null && result.exitCode === 0, `${tool} ${Object.keys(fields)} defers without a permission decision`);
367
+ const descriptor = actionGate.normalizeActionDescriptor(candidate);
368
+ check(descriptor.action === 'mutate' && descriptor.classification === 'gated', 'metadata remains a non-safe mutation');
369
+ }
370
+ }
371
+ for (const selector of [{ owner: 'acme', repo: 'repo', pullNumber: 376 }, { owner: 'acme', repo: 'repo', pull_number: 376 }]) {
372
+ check(actionGate.evaluatePreToolUse(metadataPayload({ ...selector, body: 'provider-neutral' }), { host: 'codex', sessionDir, ruleTable: table }).kind === 'allow', 'provider-neutral selectors support metadata updates');
373
+ }
374
+ const ownUnknown = JSON.parse('{"repository_full_name":"acme/repo","pr_number":376,"body":"fixture","__proto__":{"state":"closed"}}');
375
+ check(actionGate.evaluatePreToolUse(metadataPayload(ownUnknown), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'own __proto__ field cannot escape the argument allowlist');
376
+ const explicitDelete = metadataPayload(metadataInput, undefined, { action_descriptor: { action: 'delete' } });
377
+ check(actionGate.evaluatePreToolUse(explicitDelete, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'explicit higher-risk action cannot inherit metadata deferral');
378
+ for (const extra of [
379
+ { state: 'closed' }, { state: 'open' }, { state: null }, { base_branch: 'other' },
380
+ { maintainer_can_modify: false }, { maintainer_can_modify: null }, { unknown: true },
381
+ { title: 'mixed', state: 'closed' }, { draft: true }, { base: 'other' },
382
+ { action_descriptor: { action: 'delete' } },
383
+ ]) {
384
+ const result = actionGate.evaluatePreToolUse(metadataPayload({ ...metadataInput, ...extra }), { host: 'codex', sessionDir, ruleTable: table });
385
+ check(result.kind === 'block' && result.exitCode === 2, `metadata with ${JSON.stringify(extra)} remains gated`);
386
+ }
387
+ for (const input of [null, [], 'body', {}, { repository_full_name: 'acme/repo', pr_number: 376 },
388
+ { ...metadataInput, body: null }, { ...metadataInput, title: 2 }, { ...metadataInput, body: {} }]) {
389
+ check(actionGate.evaluatePreToolUse(metadataPayload(input), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'missing or malformed metadata remains gated');
390
+ }
391
+ for (const tool of ['mcp__demo__update_pull_requests', 'mcp__demo__foo_update_pull_request', 'mcp__demo__update_pull_request_and_delete', 'mcp__codex_apps__github_update_pull_request_extra', 'mcp__demo__merge_pull_request', 'mcp__demo__close_pull_request']) {
392
+ check(actionGate.evaluatePreToolUse(metadataPayload(metadataInput, tool), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', `${tool} receives no metadata exception`);
393
+ }
394
+ for (const host of ['claude', 'opencode', 'unknown']) {
395
+ const result = actionGate.evaluatePreToolUse(metadataPayload(), { host, sessionDir, ruleTable: table });
396
+ check(result.rule && result.rule.id === 'structured-mutate', `${host} retains existing mutation rule`);
397
+ }
398
+ for (const hint of [{ actionGate: { required: true } }, { workflowPlan: { actionGate: { required: true } } }, { harness: { workflowPlan: { actionGate: { required: true } } } }, { routerContract: { workflowPlan: { actionGate: { required: true } } } }]) {
399
+ check(actionGate.evaluatePreToolUse(metadataPayload(metadataInput, undefined, hint), { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'explicit required metadata gate cannot be bypassed');
400
+ }
401
+ for (const action of ['read', 'unknown']) {
402
+ for (const descriptor of [{ action_descriptor: { action } }, { actionDescriptor: { action } }, { harness: { actionDescriptor: { action } } }]) {
403
+ const candidate = metadataPayload({ ...metadataInput, state: 'closed' }, undefined, {
404
+ ...descriptor, workflowPlan: { actionGate: { required: true } },
405
+ });
406
+ const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
407
+ check(result.kind === 'block' && result.exitCode === 2, `known Codex PR mutation cannot be downgraded to ${action}`);
408
+ check(result.record && result.record.actionDescriptor.action === 'mutate', 'downgrade refusal remains an audited mutation');
409
+ }
410
+ const inputDescriptor = metadataPayload({ ...metadataInput, state: 'closed', action_descriptor: { action } }, undefined, { actionGate: { required: true } });
411
+ check(actionGate.evaluatePreToolUse(inputDescriptor, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', `input ${action} descriptor cannot hide a required PR mutation gate`);
412
+ }
413
+ const misleadingProvider = metadataPayload({ ...metadataInput, state: 'closed' }, 'mcp__get_provider__update_pull_request');
414
+ check(actionGate.evaluatePreToolUse(misleadingProvider, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'provider namespace read token cannot downgrade exact PR mutation');
415
+ for (const host of ['claude', 'opencode', 'unknown']) {
416
+ const describedRead = metadataPayload(metadataInput, undefined, { action_descriptor: { action: 'read' } });
417
+ check(actionGate.evaluatePreToolUse(describedRead, { host, sessionDir, ruleTable: table }).kind === 'allow', `${host} explicit descriptor behavior remains unchanged`);
418
+ }
419
+ for (const action of ['read', 'unknown', 'mutate', 'delete', 'publish', 'deploy', 'send']) {
420
+ const candidate = metadataPayload({ ...metadataInput, state: 'closed', action_descriptor: { toolFamily: 'shell', action } }, undefined, { actionGate: { required: true } });
421
+ const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
422
+ check(result.kind === 'block' && result.exitCode === 2, `shell family cannot disguise exact Codex PR ${action}`);
423
+ check(result.record && result.record.actionDescriptor.toolFamily === 'mcp', 'known MCP identity is preserved in audit');
424
+ }
425
+ const gateEnvelopes = gate => [
426
+ { actionGate: gate }, { workflowPlan: { actionGate: gate } },
427
+ { routerContract: { workflowPlan: { actionGate: gate } } },
428
+ { harness: { workflowPlan: { actionGate: gate } } },
429
+ ];
430
+ for (let requiredAt = 0; requiredAt < 4; requiredAt++) {
431
+ const optionalHints = Object.assign({}, ...gateEnvelopes({ required: false }));
432
+ const hints = { ...optionalHints, ...gateEnvelopes({ required: true })[requiredAt] };
433
+ const candidate = metadataPayload(metadataInput, undefined, hints);
434
+ const result = actionGate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
435
+ check(result.kind === 'block' && result.exitCode === 2, `required hint envelope ${requiredAt} wins over optional envelopes`);
436
+ check(result.record && result.record.routerActionGate.required === true, 'audit retains required hint instead of a conflicting optional hint');
437
+ const creation = { ...candidate, tool_name: 'mcp__github__create_pull_request' };
438
+ check(actionGate.evaluatePreToolUse(creation, { host: 'codex', sessionDir, ruleTable: table }).kind === 'block', 'PR creation still honors the documented explicit required gate');
439
+ }
440
+
441
+
356
442
  } catch (err) {
357
443
  console.error(err.stack || err.message);
358
444
  failed++;
@@ -127,6 +127,53 @@ try {
127
127
  assert.strictEqual(attributedBlock.exitCode, 2);
128
128
  assert.ok(attributedBlock.attribution.file && fs.existsSync(attributedBlock.attribution.file), 'destructive hook entry must be attributable before the block result');
129
129
 
130
+ // Execute both canonical and packaged pre-entrypoints with real MCP argument shapes.
131
+ for (const gate of [actionGate, require(path.join(pluginRoot, 'hooks', 'scripts', 'action-gate.js'))]) {
132
+ for (const outcome of ['success', 'failure']) {
133
+ const candidate = {
134
+ ...payload('', { toolUseId: `metadata-${outcome}`, turnId: 'metadata-turn' }),
135
+ tool_name: 'mcp__codex_apps__github__update_pull_request',
136
+ tool_input: { repository_full_name: 'acme/repo', pr_number: 376, body: 'private fixture body' },
137
+ };
138
+ const pre = gate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
139
+ assert.strictEqual(pre.kind, 'allow');
140
+ assert.strictEqual(pre.stdout, null);
141
+ assert.strictEqual(pre.record.disposition, 'deferred-to-host');
142
+ assert.strictEqual(pre.record.host, 'codex');
143
+ assert.strictEqual(pre.record.toolUseId, candidate.tool_use_id);
144
+ assert.strictEqual(pre.record.matchedRule, 'routine-pr-metadata-update');
145
+ const hash = gate.hashExact(gate.exactPayloadText(candidate));
146
+ assert.strictEqual(pre.record.commandHash, hash);
147
+ assert.strictEqual(pre.record.actionDescriptor.payloadHash, hash);
148
+ assert.ok(!JSON.stringify(pre.record).includes('private fixture body'));
149
+ const post = gate.finalizeExecution(candidate, outcome, { sessionDir });
150
+ assert.strictEqual(post.record.disposition, 'executed');
151
+ assert.strictEqual(post.record.executionOutcome, outcome);
152
+ assert.strictEqual(post.record.commandHash, hash);
153
+
154
+ gate.evaluatePreToolUse(candidate, { host: 'codex', sessionDir, ruleTable: table });
155
+ const altered = { ...candidate, tool_input: { ...candidate.tool_input, state: 'closed' } };
156
+ assert.strictEqual(gate.finalizeExecution(altered, 'success', { sessionDir }).record.disposition, 'integrity-violation');
157
+ const denied = gate.evaluatePreToolUse(altered, { host: 'codex', sessionDir, ruleTable: table });
158
+ assert.strictEqual(denied.kind, 'block');
159
+ assert.strictEqual(denied.exitCode, 2);
160
+ assert.strictEqual(denied.stdout, null);
161
+ assert.strictEqual(denied.record.disposition, 'rejected');
162
+ assert.strictEqual(denied.record.rejectionReason, 'host-ask-decision-unverified-or-unsupported');
163
+ assert.notStrictEqual(denied.record.commandHash, hash);
164
+ }
165
+ }
166
+ const metadataEntry = preAdapter.processPayload({
167
+ ...payload('', { toolUseId: 'metadata-entry' }),
168
+ tool_name: 'mcp__codex_apps__github_update_pull_request',
169
+ tool_input: { repository_full_name: 'acme/repo', pr_number: 376, title: 'fixture' },
170
+ }, { host: 'codex', sessionDir, ruleTable: table, attributionDir, runNonce: 'metadata-fixture' });
171
+ assert.strictEqual(metadataEntry.kind, 'allow');
172
+ assert.strictEqual(metadataEntry.attribution.record.payloadHash, metadataEntry.record.commandHash);
173
+ assert.strictEqual(metadataEntry.attribution.record.toolUseId, metadataEntry.record.toolUseId);
174
+ assert.strictEqual(fs.readFileSync(path.join(ROOT, 'hooks/scripts/action-gate.js'), 'utf8'),
175
+ fs.readFileSync(path.join(pluginRoot, 'hooks/scripts/action-gate.js'), 'utf8'));
176
+
130
177
  const blockedParent = path.join(tempRoot, 'not-a-directory');
131
178
  fs.writeFileSync(blockedParent, 'fixture', 'utf8');
132
179
  const failedAttribution = preAdapter.processPayload(payload('git push --force origin main', {
@@ -131,6 +131,18 @@ try {
131
131
  }));
132
132
  assert.strictEqual(neutralResult.status, 0, 'exact create_pull_request operation must be host-deferred on Codex (currently exits 2: RED)');
133
133
 
134
+ for (const tool of ['mcp__codex_apps__github__update_pull_request', 'mcp__codex_apps__github_update_pull_request', 'mcp__github__update_pull_request']) {
135
+ for (const fields of [{ body: 'replacement body' }, { title: 'replacement title' }, { title: 'both', body: '' }]) {
136
+ const result = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, ...fields }));
137
+ assert.strictEqual(result.status, 0, 'metadata-only actual stdin payload must be host-deferred');
138
+ assert.strictEqual(result.stdout.trim(), '', 'metadata deferral must emit no permission decision');
139
+ }
140
+ const denied = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, body: 'mixed', state: 'closed' }));
141
+ assert.strictEqual(denied.status, 2, 'mixed metadata/state actual stdin payload must fail closed');
142
+ assert.strictEqual(denied.stdout.trim(), '');
143
+ assert.match(denied.stderr, /structured-mutate/);
144
+ }
145
+
134
146
  // Genuine mutations remain blocked, but the fixed-format approval protocol is retired.
135
147
  const legacyMutation = run(payload('mcp__demo__update_issue', { issue_id: 1, title: 'changed' }));
136
148
  assert.strictEqual(legacyMutation.status, 2, 'genuine mutation must remain blocked on Codex');
@@ -132,7 +132,8 @@ Harness does not parse, bind, or intercept any fixed approval syntax in user
132
132
  prompts. Native agent task-scope authorization and native permissions remain
133
133
  authoritative.
134
134
 
135
- Routine exact `create_pull_request` operations are host-deferred (below). All
135
+ Routine exact `create_pull_request` operations and Codex title/body-only
136
+ `update_pull_request` operations are host-deferred (below). All
136
137
  other risky actions (merge/delete/deploy/publish/send/force-push and unrelated
137
138
  genuine mutations) remain hard PreToolUse blocks; an explicitly required
138
139
  `actionGate` hint still gates. These are deterministic mechanism changes, not
@@ -153,3 +154,28 @@ risky actions (merge/delete/deploy/publish/send/force-push and unrelated genuine
153
154
  mutations) remain gated; exact operation matching is used, with no substring
154
155
  exemption, so misleading names such as `create_pull_requests` or
155
156
  `foo_create_pull_request` still block.
157
+
158
+
159
+ ## Codex PR title/body updates (host-deferred)
160
+
161
+ The Codex-only metadata exception checks the actual tool input, with all of these conditions:
162
+
163
+ - Exact final `__` operation `update_pull_request`, or exact flattened connector name `mcp__codex_apps__github_update_pull_request`.
164
+ - A non-array object containing at least one string `title` or `body` (empty body is supported).
165
+ - Only `title`, `body`, and selector keys `repository_full_name`, `pr_number`, `owner`, `repo`, `pullNumber`, `pull_number` are present.
166
+ - Exact Codex PR updates cannot be downgraded to `read`/`unknown` by an explicit descriptor or a provider namespace; higher-risk action descriptors remain gated. Their tool family comes from the actual tool identity, so a descriptor cannot disguise the MCP update as shell input.
167
+ - No explicit required `actionGate` hint in any supported envelope (a required hint wins over conflicting optional hints); the action descriptor remains `mutate`/`gated`.
168
+
169
+ Any additional key, including `state`, `base_branch`, `base`, `maintainer_can_modify`, `draft`, or an unknown key, prevents the exception, even when its value is null. Mixed metadata/high-risk inputs and similar operation names retain the existing Gate. Claude, OpenCode and unknown hosts retain their prior mutation rules. PR creation behavior is unchanged.
170
+
171
+ ```mermaid
172
+ flowchart LR
173
+ A["Exact PR update + actual arguments"] --> B{"Codex + title/body only<br/>+ no required Gate?"}
174
+ B -->|Yes| C["Host-deferred mutation<br/>no permission decision"]
175
+ B -->|No| D["Existing structured Gate"]
176
+ C --> E["Native task scope + permissions<br/>+ GitHub authorization"]
177
+ ```
178
+
179
+ Host-deferred does not grant authorization. Harness emits no `permissionDecision` allow/ask; native permissions, task scope, and GitHub access still decide execution. The audit records `routine-pr-metadata-update`, `deferred-to-host`, host/tool provenance and the exact payload hash, without storing title/body. PostToolUse records execution success/failure; Stop rejects an unexecuted deferral. A changed payload cannot reuse the original hash.
180
+
181
+ Evidence: [RED/GREEN and independent review](../benchmarks/results/codex-pr-metadata-2026-10-26/README.md). Package/mechanism verification is separate from live Host verification (#110, #122, #135; affected PR #376).
@@ -6,7 +6,7 @@ status: active
6
6
  # Document workflow reconciliation — Phase 3b S1 runtime
7
7
 
8
8
  **Status: the runtime ships default OFF; opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`.
9
- With the pilot ON, F4 ([#358](https://github.com/dyphn1/Harness-everything/pull/358)) grants limited positive credit for exactly `doc-lint` and `link-check`, from a host-captured run of the installed doc-check on Claude Code 2.1.309 (live runs 4–5). S3 provider integration is pending; E1 has not started.**
9
+ With the pilot ON, F4 ([#358](https://github.com/dyphn1/Harness-everything/pull/358)) grants limited positive credit for exactly `doc-lint` and `link-check`, from a host-captured run of the installed doc-check on Claude Code 2.1.309 (live runs 4–5). S3 provider integration is pending; E1 remains OPEN with insufficient qualifying exposure. The recorded OpenCode [D1/D2 attempts](../benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md) do not qualify as E1 cycles.**
10
10
  This document refines the [document lifecycle contract](document-lifecycle.md#phase-3b--narrow-integration-contract-s1-default-off-f4-limited-credit).
11
11
  The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
12
12
  permits this limited, reversible consumer under the
@@ -336,7 +336,7 @@ Preparation assertion rows do not carry `reportOrigin`; the S1 runtime report ca
336
336
  marker is never presented as a production report.
337
337
  ## S1 — switch, trusted scope and fail-closed no-credit gate
338
338
 
339
- **S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending and E1 has not started.**
339
+ **S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending; E1 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying.**
340
340
  The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
341
341
  permits this limited implementation; the [posted execution plan](https://github.com/dyphn1/Harness-everything/issues/325)
342
342
  orders Docs → RED → GREEN → independent review → green CI for each slice. S1 was the temporary
@@ -1085,7 +1085,8 @@ provider adapter remains pending. Require every expected applicable MUST and
1085
1085
  freshness check before positive credit, plus source/package compatibility, negative cases and
1086
1086
  relevant green CI including Windows. The S1 suites establish their tested mechanism/package
1087
1087
  contracts; they do not establish independent host provenance, live-host loading or architectural
1088
- benefit. E1 has not started and begins only after a creditable S2; E1 GO and H1–H4/paired evidence
1088
+ benefit. E1 observation remains OPEN with insufficient qualifying exposure; it begins only after
1089
+ a creditable S2, and the D1/D2 attempts are non-qualifying. E1 GO and H1–H4/paired evidence
1089
1090
  remain separate gates.
1090
1091
 
1091
1092
  ## F4: credited reconciliation (Claude-only, pilot ON)
@@ -4,7 +4,7 @@ Contract for the documentation family: project docs configuration (D5), document
4
4
 
5
5
  Documentation is a **consumer** of the #320 workflow/obligation abstractions. Nothing here is a second router, a standing maintenance loop or a cognitive lock: every check is a pure function that reports diagnostics.
6
6
 
7
- ## Scope of this slice (status as of 2026-10-20)
7
+ ## Scope of this slice (original 2026-10-20 checkpoint; Phase 3b/3c status updated 2026-10-25)
8
8
 
9
9
  This table distinguishes merged mechanism code from end-to-end workflow integration. The last reviewed `main` commit is `29b1d65b398e2622af97bf067078373b7fa76b26` (v0.36.2). That release commit carries `[skip ci]` and only bumps package/distribution versions on top of `e5d22584e243d5b5e587ee0279625e97a9033160`, whose full-gate `CI` run [39374474269](https://github.com/dyphn1/Harness-everything/actions/runs/39374474269) completed with every job succeeded. The historical Windows System One resident teardown (`S1-RS15`) was observed in attempt 1 of [run 39175240370](https://github.com/dyphn1/Harness-everything/actions/runs/39175240370) on `main@75e04359ae5bf11a41739f8eb04d25a76ee8f3a7`; that run completed successfully on attempt 2 and says nothing about `29b1d65`. Treat the resident teardown as a known platform limitation only when a current run on the exact SHA reproduces it, and require an all-green full-gate run for that SHA before calling any baseline green.
10
10
 
@@ -15,8 +15,8 @@ This table distinguishes merged mechanism code from end-to-end workflow integrat
15
15
  | 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | **Merged** in #327; do not rebuild this lifecycle logic for Phase 3 |
16
16
  | Step-type prerequisite (#326) | **Complete:** PR #328 merged; Harness-owned `doc-change` is explicitly declared, has no implicit TDD binding, and is not auto-inserted by router/System One |
17
17
  | 3a — document facts, obligations and evidence producer | **Merged** in #334: `harness-everything/scripts/doc-obligations.js` and `doc-evidence.js`. H3/H4 have *partial deterministic mechanism* coverage, not integrated workflow evidence. Known limitations: caller-attested envelope; `configDigest` presence-only; `candidateDigest` restricted to `docs/prd/`; missing independent task-scope binding. Deleted and old rename paths are bound to trusted per-path baseline evidence ([doc-evidence-baseline-binding.md](doc-evidence-baseline-binding.md), #335 fixed by #342), with workflow-disposition reconciliation left out of scope |
18
- | 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding. **F4 (#358) adds limited positive credit** for exactly `doc-lint` and `link-check` from a host-captured run of the installed doc-check (Claude Code only; live runs on 2.1.309). See [S1 switch, trusted scope and no-credit contract](doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate). S3 pure advisory provider suggestions shipped in 0.37.0 (not wired); E1 has not started. Accepted #341 E0 permits this limited reversible consumer; the live runs are not E1 GO; preserve existing required-binding compatibility |
19
- | 3c — H1–H4 and paired-task evaluation | **Not started.** Mechanism tests alone are insufficient for architecture promotion; compare frozen baseline and treatment on real bounded tasks, success/failure first, then conditional cost and latency |
18
+ | 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding. **F4 (#358) adds limited positive credit** for exactly `doc-lint` and `link-check` from a host-captured run of the installed doc-check (Claude Code only; live runs on 2.1.309). See [S1 switch, trusted scope and no-credit contract](doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate). S3 pure advisory provider suggestions shipped in 0.37.0 (not wired); E1 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying. Accepted #341 E0 permits this limited reversible consumer; the live runs are not E1 GO; preserve existing required-binding compatibility |
19
+ | 3c — H1–H4 and paired-task evaluation | **H1 preparation only** in [#373](https://github.com/dyphn1/Harness-everything/pull/373) ([retained evidence](../benchmarks/results/h1-provider-independence-qualification/README.md)); full H1–H4 and paired-task evaluation remain pending. Mechanism tests alone are insufficient for architecture promotion; compare frozen baseline and treatment on real bounded tasks, success/failure first, then conditional cost and latency |
20
20
  | 1b — shared documentation craft and family metadata | **Shared craft delivered** in F2 (#356): shared references with thin skill consumers. Family metadata is not established. A small optional logical-family metadata seam may be piloted; physical skill directory moves remain blocked on #320 identity/path separation |
21
21
  | 4 — derived-view generators and `describes:` drift | **Regenerate-equals delivered** in F3 (#357) for authorized generators; `describes:` drift reminders and broader maintenance are not established. Preserve generator authorization, deterministic regenerate-equals and scoped reminder semantics |
22
22
 
@@ -17,6 +17,10 @@ Statuses are intentionally explicit:
17
17
 
18
18
  Codex and OpenCode are `Partial` in `liveHostVerification`. The Codex artifact verifies user-hook compatibility capture for two consecutive turns on one Windows Codex CLI host; native plugin-hook mounting and other host/version combinations remain unverified. OpenCode's retained evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS), not durable hard enforcement. No fresh artifacts exist yet for the other surfaces, managed-workspace import, or public OpenAI approval.
19
19
 
20
+ ### Codex PR metadata action-gate boundary
21
+
22
+ Exact title/body-only `update_pull_request` calls are Codex-only host-deferred mutations after an argument allowlist check; explicit required Gate hints and all additional fields retain the existing Gate. Native task scope, permissions and GitHub authorization remain authoritative. Canonical/package mechanism tests cover hashes, audit provenance, outcomes and negative controls; this does not establish candidate live-host coverage. See [Codex policy](codex-action-gate.md#codex-pr-titlebody-updates-host-deferred).
23
+
20
24
  ### Routing contract vs. host evidence
21
25
 
22
26
  The workflow plan controller rejects present non-array required/optional binding declarations before recording a requirement plan. This input-validation contract is covered by deterministic mechanism tests and does not change live-host evidence status.
@@ -27,7 +31,8 @@ This is a **repository/agent contract**, not an automatic upgrade to any platfor
27
31
 
28
32
  ### Action-gate mechanism vs. host semantic boundary
29
33
 
30
- Routine exact `create_pull_request` operations are host-deferred at the mechanism level:
34
+ Routine exact `create_pull_request` operations and Codex title/body-only
35
+ `update_pull_request` operations are host-deferred at the mechanism level:
31
36
  their descriptor remains a mutation (not read/safe), and no Harness consent or
32
37
  `permissionDecision` is emitted. Native agent task-scope authorization and native
33
38
  permissions stay authoritative; an explicit required `actionGate` hint still gates.
@@ -140,7 +145,8 @@ The public OpenAI Skills-only plugin is narrower again. Its ZIP contains the pac
140
145
  Codex `UserPromptSubmit` prompts pass through to the normal router unchanged;
141
146
  Harness does not parse or intercept a fixed approval syntax. Native agent
142
147
  task-scope authorization and native permissions remain authoritative. Routine
143
- exact `create_pull_request` operations are host-deferred at the mechanism level:
148
+ exact `create_pull_request` operations and Codex title/body-only
149
+ `update_pull_request` operations are host-deferred at the mechanism level:
144
150
  their descriptor remains a mutation (not read/safe), and no Harness consent or
145
151
  `permissionDecision` is emitted. All other risky actions remain hard PreToolUse
146
152
  blocks; an explicitly required `actionGate` hint still gates. These are
@@ -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 toolFamily = inferToolFamily(tool, explicit);
116
- const action = inferStructuredAction(tool, explicit);
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 structuredRule(payload) {
144
- const descriptor = normalizeActionDescriptor(payload);
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.42.0",
4
+ "version": "0.42.1",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.42.0",
3
+ "version": "0.42.1",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.42.0",
3
+ "version": "0.42.1",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",