harness-everything 0.40.0 → 0.41.0
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/.agents/harness-everything/project-docs.json +9 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +43 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/2-injected-architecture.md +229 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/README.md +54 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-injected-document.md +328 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.input.md +8 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.stdout.json +16 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/README.md +34 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/after.json +248 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/before.json +254 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/probe.js +26 -0
- package/benchmarks/results/f2-doc-craft-baseline/README.md +32 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-report.md +84 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1.diff +19 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-report.md +110 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2.diff +19 -0
- package/benchmarks/results/f3-derived-views-baseline/README.md +34 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-report.md +43 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2.diff +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-report.md +57 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3.diff +21 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/README.md +99 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/capture.sanitized.jsonl +26 -0
- package/ci/archive/325-prep/README.md +71 -0
- package/ci/{prep → archive/325-prep}/doc-reconciliation-red.js +9 -7
- package/ci/archive/325-prep/f4-expected-red.json +34 -0
- package/ci/archive/325-prep/historical-qualification.test.js +143 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +17 -4
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +8 -56
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +2 -2
- package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +17 -5
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/harness.js +415 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/off-handoff-golden.json +16 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-failure.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success-minimal.json +9 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success.json +13 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-start.json +7 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-freetext-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-task-result.json +19 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-capture-only.json +99 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-legacy.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-concurrent-writers.json +62 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-lookalikes-runtime-mutations.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-malformed.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-normal-session.json +74 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-runtime-drift-later-npm-test.json +58 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-valid-direct-quoted-review.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/f4/cases.json +2267 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/cases.json +1110 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/legacy-off-golden.json +674 -0
- package/ci/lib/doc-reconciliation-file-spy.js +23 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +136 -10
- package/ci/lib/doc-reconciliation-qualification-preload.js +37 -0
- package/ci/lib/doc-reconciliation-replay.js +104 -0
- package/ci/lib/doc-reconciliation-staged-tree.js +102 -0
- package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +179 -0
- package/ci/mechanism-325-3b-preparation.test.js +27 -101
- package/ci/mechanism-325-3b-s2b-capture.test.js +561 -0
- package/ci/mechanism-325-3b-s2b-wiring.test.js +668 -0
- package/ci/mechanism-325-f1-doc-check.test.js +674 -0
- package/ci/mechanism-325-f2-doc-craft.test.js +597 -0
- package/ci/mechanism-325-f3-derived-views.test.js +798 -0
- package/ci/mechanism-325-f4-invariants.test.js +606 -0
- package/ci/mechanism-325-f4-projections.test.js +382 -0
- package/ci/mechanism-325-f4-qualification.test.js +228 -0
- package/ci/mechanism-325-f4-trust-boundary.test.js +880 -0
- package/ci/mutation-check.js +26 -3
- package/docs/architecture.md +5 -0
- package/docs/doc-workflow-reconciliation.md +331 -4
- package/docs/document-lifecycle.md +11 -0
- package/docs/governed-index.md +20 -0
- package/docs/platform-capabilities.md +22 -2
- package/grill-with-docs/SKILL.md +1 -1
- package/grill-with-docs/references/session-playbook.md +4 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/doc-check.md +186 -0
- package/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/harness-everything/references/triage-and-tiers.md +4 -0
- package/harness-everything/scripts/doc-check.js +442 -0
- package/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/hooks/hooks.json +46 -0
- package/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/hooks/scripts/state-persist.js +78 -2
- package/hooks/scripts/workflow-stop-gate.js +7 -2
- package/opencode-plugin/README.md +15 -0
- package/opencode-plugin/index.mjs +77 -0
- 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/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +78 -2
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +7 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/references/session-playbook.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/doc-check.md +186 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +442 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/plugins/harness-everything/skills/repo-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/repo-docs/references/process-guide.md +4 -0
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/references/process.md +4 -0
- package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +4 -0
- package/repo-docs/SKILL.md +1 -1
- package/repo-docs/references/process-guide.md +4 -0
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/references/process.md +4 -0
- package/to-tickets/SKILL.md +1 -1
- package/to-tickets/references/publishing-and-templates.md +4 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "harness.doc-reconciliation.qualification.v1",
|
|
3
|
+
"host": "claude-code",
|
|
4
|
+
"hostVersion": "2.1.308",
|
|
5
|
+
"captureRecordVersion": "claude-capture-record/1",
|
|
6
|
+
"run": "Run 2b",
|
|
7
|
+
"payloadKinds": {
|
|
8
|
+
"PostToolUse": [
|
|
9
|
+
"agent_id",
|
|
10
|
+
"interrupted",
|
|
11
|
+
"tool_use_id"
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
"agent_id",
|
|
15
|
+
"tool_use_id"
|
|
16
|
+
],
|
|
17
|
+
"SubagentStart": [
|
|
18
|
+
"agent_id"
|
|
19
|
+
],
|
|
20
|
+
"SubagentStop": [
|
|
21
|
+
"agent_id"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"correlation": {
|
|
25
|
+
"preToPostByToolUseId": true,
|
|
26
|
+
"interruptedExplicit": true,
|
|
27
|
+
"subagentStartStopSameAgent": true,
|
|
28
|
+
"agentResultAliasCorrelated": true
|
|
29
|
+
},
|
|
30
|
+
"sanitizedCapture": {
|
|
31
|
+
"file": "doc-reconciliation-capture.sanitized.jsonl",
|
|
32
|
+
"sha256": "db4d8c85d2ba0fd06f53f455f31b125dd194e9a22cb337687bc0e295a16f02e5"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -673,6 +673,13 @@ function updateStep(context, stepId, disposition, options = {}) {
|
|
|
673
673
|
withholdPilotVerification(context, loaded.set);
|
|
674
674
|
throw new Error('document verification pending: S1 grants no document credit');
|
|
675
675
|
}
|
|
676
|
+
// Review-mode refusal (pilot ON only): a self-authored or non-independent review PASS stays pending.
|
|
677
|
+
if (disposition === 'pass' && step.stepType === 'review') {
|
|
678
|
+
const pilot = require('./doc-reconciliation-pilot');
|
|
679
|
+
if (pilot.reviewRequired(loaded.set) && !pilot.independentReviewAccepted(context)) {
|
|
680
|
+
throw new Error('review pending: a self-authored or non-independent review cannot pass this step; an independent host quality review is required');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
676
683
|
step.status = disposition;
|
|
677
684
|
step.evidence = evidence;
|
|
678
685
|
step.reasonCode = reasonCode;
|
|
@@ -4,6 +4,16 @@ const path = require('path');
|
|
|
4
4
|
const { getWorkspaceRoot, getSessionDir } = require('./lib/harness-state');
|
|
5
5
|
const { cwdOf, classifyShell, isVerificationShell, directMutationInWorkspace } = require('./lib/workflow-isolation');
|
|
6
6
|
const { observeTool } = require('./lib/telemetry');
|
|
7
|
+
|
|
8
|
+
// S2b doc-reconciliation pilot capture (docs/doc-workflow-reconciliation.md
|
|
9
|
+
// § S2b "Activation and pilot-only scope", "Claude capture and origin-provider
|
|
10
|
+
// contract"): pilot-only, fail-open to the host, and gated on the exact
|
|
11
|
+
// switch value `1` before any collector logic runs. OFF must return to the
|
|
12
|
+
// existing legacy path before pilot collection; capture never executes a
|
|
13
|
+
// command, verifier or generator and never blocks a tool or Stop.
|
|
14
|
+
const PILOT_CAPTURE_EVENT_NAMES = ['PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'SubagentStart', 'SubagentStop'];
|
|
15
|
+
// Task/Agent results were never routed here before S2b, so they stay capture-only: the legacy handoff path does not run for them.
|
|
16
|
+
const CAPTURE_ONLY_TOOLS = ['Task', 'Agent'];
|
|
7
17
|
let inputData = '';
|
|
8
18
|
const timeout = setTimeout(() => processState(null), 200);
|
|
9
19
|
process.stdin.on('data', chunk => { inputData += chunk; });
|
|
@@ -11,7 +21,30 @@ process.stdin.on('end', () => { clearTimeout(timeout); try { processState(JSON.p
|
|
|
11
21
|
function processState(payload) {
|
|
12
22
|
try {
|
|
13
23
|
const sessionId = payload && (payload.session_id || payload.sessionId);
|
|
24
|
+
const captureEventName = payload ? (payload.hook_event_name || payload.hookEventName) || '' : '';
|
|
25
|
+
const captureToolName = payload ? (payload.tool_name || payload.tool || '') : '';
|
|
26
|
+
const captureOnly = captureEventName === 'SubagentStart' || captureEventName === 'SubagentStop'
|
|
27
|
+
|| captureEventName === 'PreToolUse'
|
|
28
|
+
|| (captureEventName === 'PostToolUse' && CAPTURE_ONLY_TOOLS.indexOf(captureToolName) !== -1);
|
|
29
|
+
if (captureOnly && process.env.HARNESS_DOC_RECONCILIATION_PILOT !== '1') {
|
|
30
|
+
process.exit(0);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
14
33
|
const root = getWorkspaceRoot(payload);
|
|
34
|
+
// SubagentStart/SubagentStop were never routed to this script before S2b.
|
|
35
|
+
// They are capture-only events: the legacy handoff state path stays
|
|
36
|
+
// untouched for them, so wiring them while OFF adds no writes at all.
|
|
37
|
+
if (captureEventName === 'SubagentStart' || captureEventName === 'SubagentStop') {
|
|
38
|
+
persistPilotCapture(payload, captureEventName, sessionId, root);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Pre-tool launch capture and Task/Agent result capture: capture-only, no legacy handoff state is written.
|
|
43
|
+
if (captureEventName === 'PreToolUse' || (captureEventName === 'PostToolUse' && CAPTURE_ONLY_TOOLS.indexOf(captureToolName) !== -1)) {
|
|
44
|
+
persistPilotCapture(payload, captureEventName, sessionId, root);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
15
48
|
const stateFile = path.join(getSessionDir(root, sessionId), 'handoff-state.json');
|
|
16
49
|
let state = { status: 'idle', timestamp: new Date().toISOString() };
|
|
17
50
|
if (fs.existsSync(stateFile)) { try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch (_) {} }
|
|
@@ -25,7 +58,7 @@ function processState(payload) {
|
|
|
25
58
|
const rawExitCode = toolResponse.exitCode ?? toolResponse.exit_code ?? payload.exitCode;
|
|
26
59
|
const exitCode = typeof rawExitCode === 'number' ? rawExitCode : undefined;
|
|
27
60
|
const stderrSignal = typeof stderr === 'string' && stderr.trim().length > 0;
|
|
28
|
-
const hookEvent =
|
|
61
|
+
const hookEvent = captureEventName;
|
|
29
62
|
const isSuccessEvent = hookEvent === 'PostToolUse';
|
|
30
63
|
const isFailed = hookEvent === 'PostToolUseFailure' || (exitCode !== undefined && exitCode !== 0) || (!isSuccessEvent && exitCode === undefined && stderrSignal);
|
|
31
64
|
const toolName = payload.tool_name || payload.tool || 'command';
|
|
@@ -44,10 +77,53 @@ function processState(payload) {
|
|
|
44
77
|
const commandClass = classifyShell(command);
|
|
45
78
|
if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
|
|
46
79
|
// Verification evidence needs a confirmed success: a PostToolUse event or an explicit zero exit.
|
|
47
|
-
|
|
80
|
+
// Pilot ON only: a registered, argv-validated doc-check that resolves to this install's own runtime is the document
|
|
81
|
+
// verification evidence, and only at the shipped pin (R3-02). Any other path earns no verification evidence. OFF stays legacy.
|
|
82
|
+
const pilotEnabled = process.env.HARNESS_DOC_RECONCILIATION_PILOT === '1';
|
|
83
|
+
const docCheck = pilotEnabled ? require('./lib/doc-reconciliation-provider') : null;
|
|
84
|
+
const installedCheck = docCheck && docCheck.registeredInstalledCheck(command, cwdOf(payload, root));
|
|
85
|
+
const docCheckCandidate = installedCheck && installedCheck.kind !== 'not-candidate';
|
|
86
|
+
const pinCheckable = installedCheck && installedCheck.installed
|
|
87
|
+
&& (installedCheck.kind === 'accepted' || installedCheck.reason === 'implementation-pin-mismatch');
|
|
88
|
+
// A non-candidate that is compound or names the checker is not generic verification (trustRisk); the scanner miss does not clear it.
|
|
89
|
+
const verifies = docCheckCandidate
|
|
90
|
+
? Boolean(pinCheckable) && docCheck.fileDigest(installedCheck.installed) === docCheck.SHIPPED_DOC_CHECK_DIGEST
|
|
91
|
+
: !(pilotEnabled && docCheck.trustRisk(command)) && isVerificationShell(command, { cwd: cwdOf(payload, root), root });
|
|
92
|
+
if (!isFailed && (isSuccessEvent || exitCode === 0) && verifies) {
|
|
93
|
+
const verifiedAt = Date.now();
|
|
94
|
+
const verifiedExitCode = exitCode ?? null;
|
|
95
|
+
state.lastVerifyAt = verifiedAt;
|
|
96
|
+
state.lastVerifyExitCode = verifiedExitCode;
|
|
97
|
+
if (pilotEnabled) {
|
|
98
|
+
const source = docCheckCandidate ? 'Doc' : 'Generic';
|
|
99
|
+
state[`last${source}VerifyAt`] = verifiedAt;
|
|
100
|
+
state[`last${source}VerifyExitCode`] = verifiedExitCode;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
48
103
|
}
|
|
49
104
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
50
105
|
observeTool(payload);
|
|
106
|
+
persistPilotCapture(payload, captureEventName, sessionId, root);
|
|
51
107
|
} catch (_) {}
|
|
52
108
|
process.exit(0);
|
|
53
109
|
}
|
|
110
|
+
|
|
111
|
+
// Pilot-only capture journaling. Every guard is placed before any collector
|
|
112
|
+
// work; any failure is swallowed (fail-open), and the hook output/exit is
|
|
113
|
+
// never changed by capture.
|
|
114
|
+
function persistPilotCapture(payload, eventName, sessionId, root) {
|
|
115
|
+
try {
|
|
116
|
+
if (!payload || process.env.HARNESS_DOC_RECONCILIATION_PILOT !== '1') return;
|
|
117
|
+
if (PILOT_CAPTURE_EVENT_NAMES.indexOf(eventName) === -1) return;
|
|
118
|
+
const provider = require('./lib/doc-reconciliation-provider');
|
|
119
|
+
const entry = provider.buildCaptureRecord(payload, eventName, root);
|
|
120
|
+
if (!entry) return;
|
|
121
|
+
const sessionDir = getSessionDir(root, sessionId);
|
|
122
|
+
// Observation-time identity: the workflow and pin in force when the host event was captured.
|
|
123
|
+
const observed = provider.observedIdentityFor(sessionDir);
|
|
124
|
+
if (observed) entry.observedIdentity = observed;
|
|
125
|
+
// Each capture is one exclusively created event file; the compatibility journal is rebuilt from them (not tamper-proof).
|
|
126
|
+
provider.writeImmutableEvent(sessionDir, entry);
|
|
127
|
+
provider.rebuildJournalView(sessionDir);
|
|
128
|
+
} catch (_) { /* fail-open: capture failures never reach the host */ }
|
|
129
|
+
}
|
|
@@ -16,7 +16,8 @@ function decide(payload) {
|
|
|
16
16
|
const context = loadWorkflow(payload);
|
|
17
17
|
const { workflow, sessionDir } = context;
|
|
18
18
|
if (!workflow || workflow.state === 'deferred') return;
|
|
19
|
-
const
|
|
19
|
+
const handoffFile = path.join(sessionDir, 'handoff-state.json');
|
|
20
|
+
let handoff = readJson(handoffFile);
|
|
20
21
|
const unresolved = [];
|
|
21
22
|
const diagnostics = [];
|
|
22
23
|
let requiredFault = false;
|
|
@@ -36,11 +37,15 @@ function decide(payload) {
|
|
|
36
37
|
|
|
37
38
|
required(() => planningUnresolved(context));
|
|
38
39
|
if (String(workflow.strategy || '').startsWith('fable-')) {
|
|
39
|
-
if (!unresolved.length)
|
|
40
|
+
if (!unresolved.length) {
|
|
41
|
+
require('./lib/doc-reconciliation-pilot').assess(context, null);
|
|
42
|
+
}
|
|
40
43
|
const match = matchingRun(context);
|
|
41
44
|
unresolved.push(...(match ? unresolvedStages(match, workflow, handoff?.lastEditAt || 0) : ['correlated-run-missing']));
|
|
42
45
|
} else {
|
|
43
46
|
required(() => unresolvedObligations(context));
|
|
47
|
+
// Assessment can withdraw stale document verification; read the current aggregate afterward.
|
|
48
|
+
handoff = readJson(handoffFile);
|
|
44
49
|
const lastEdit = Math.max(workflow.lastMutationAt || 0, handoff?.lastEditAt || 0);
|
|
45
50
|
if (lastEdit && (handoff?.lastVerifyAt || 0) < lastEdit) unresolved.push('verification-after-edit-missing');
|
|
46
51
|
}
|
|
@@ -104,6 +104,21 @@ new breaker failure. A reflection artifact itself does not count as a code edit.
|
|
|
104
104
|
|
|
105
105
|
## Installation
|
|
106
106
|
|
|
107
|
+
**OpenCode 2.x (and newer 1.18.29+):** the module default-exports
|
|
108
|
+
`{ id: "harness.enforcement", setup(ctx) }` and registers `ctx.tool.hook("execute.before|after")`
|
|
109
|
+
plus `ctx.event.subscribe()` through the OpenCode 2 plugin context. OpenCode 2
|
|
110
|
+
never loads a named-export-only plugin: it fails with `Plugin must export a
|
|
111
|
+
default definition with an id and an effect or setup function`
|
|
112
|
+
(SchemaError at `["default"]`). The named `HarnessEnforcement` export stays
|
|
113
|
+
available for OpenCode 1.x hosts: it receives the V1 context
|
|
114
|
+
(`{client, directory}`) and returns a V1 hooks object
|
|
115
|
+
(`tool.execute.before`, `tool.execute.after`, `event`); the V2 `setup`
|
|
116
|
+
adapter wraps that factory and maps the V2 hook events onto it, so the
|
|
117
|
+
enforcement logic is shared, not duplicated.
|
|
118
|
+
|
|
119
|
+
Direction:
|
|
120
|
+
named factory (V1 hosts/tests) → default-export setup adapter → V2 domains.
|
|
121
|
+
|
|
107
122
|
Copy `index.mjs` into opencode's plugin directory **under a `.js` filename** -
|
|
108
123
|
it is self-contained, no sibling files required:
|
|
109
124
|
|
|
@@ -932,3 +932,80 @@ export const HarnessEnforcement = async ({ client, directory }) => {
|
|
|
932
932
|
},
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
|
+
|
|
936
|
+
// ---------------------------------------------------------------------------
|
|
937
|
+
// OpenCode 2.x entrypoint.
|
|
938
|
+
//
|
|
939
|
+
// OpenCode 2 changed the plugin contract: a plugin default-exports
|
|
940
|
+
// { id, setup(ctx) } and registers hooks through domains (ctx.tool.hook,
|
|
941
|
+
// ctx.event.subscribe, ctx.session.*) instead of returning a hooks object from
|
|
942
|
+
// a named factory. A named-export-only module fails to load with
|
|
943
|
+
// "Plugin must export a default definition with an id and an effect or setup
|
|
944
|
+
// function" (SchemaError at ["default"]) - this is what tripped the global
|
|
945
|
+
// install on this machine's OpenCode 2.1.3.
|
|
946
|
+
//
|
|
947
|
+
// The named `HarnessEnforcement` factory above keeps the hooks-object shape
|
|
948
|
+
// used by the repository mechanism tests (and by OpenCode 1.x hosts); this
|
|
949
|
+
// default export adapts it to the V2 hooks. Tool hook events are verified
|
|
950
|
+
// OpenCode 2.1.3 shapes: execute.before is {tool, sessionID, agent, messageID,
|
|
951
|
+
// id, input} with `input` carrying the tool args; execute.after adds
|
|
952
|
+
// {status, result | error}.
|
|
953
|
+
export default {
|
|
954
|
+
id: "harness.enforcement",
|
|
955
|
+
async setup(ctx) {
|
|
956
|
+
const hooks = await HarnessEnforcement({
|
|
957
|
+
client: {
|
|
958
|
+
session: {
|
|
959
|
+
// V1 shutdown path calls client.session.prompt({path, body}) twice;
|
|
960
|
+
// map it onto the V2 session API instead of duplicating the code.
|
|
961
|
+
prompt: ({ path, body }) =>
|
|
962
|
+
ctx.session.prompt({
|
|
963
|
+
sessionID: path && path.id,
|
|
964
|
+
text: (body && body.parts || []).map((part) => part && part.text || "").join(""),
|
|
965
|
+
}),
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
directory: (ctx.location && ctx.location.directory) || ctx.directory,
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
await ctx.tool.hook("execute.before", (event) =>
|
|
972
|
+
hooks["tool.execute.before"](toolEventInput(event), { args: event.input }))
|
|
973
|
+
await ctx.tool.hook("execute.after", (event) =>
|
|
974
|
+
hooks["tool.execute.after"](toolEventInput(event), { args: event.input }))
|
|
975
|
+
|
|
976
|
+
const events = new AbortController()
|
|
977
|
+
void (async () => {
|
|
978
|
+
for await (const raw of ctx.event.subscribe({ signal: events.signal })) {
|
|
979
|
+
try {
|
|
980
|
+
await hooks.event({
|
|
981
|
+
event: {
|
|
982
|
+
type: raw.type,
|
|
983
|
+
// V1 event handlers read {type, properties}; V2 events are flat
|
|
984
|
+
// objects, so pass both shapes.
|
|
985
|
+
properties: raw.properties ?? raw,
|
|
986
|
+
},
|
|
987
|
+
})
|
|
988
|
+
} catch { // fail open, same as the hooks themselves
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
})()
|
|
992
|
+
return () => events.abort()
|
|
993
|
+
},
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Build the V1-shaped (input, output) pair the legacy hooks expect from the
|
|
997
|
+
// V2 tool-hook event: legacy telemetry reads callID/agentID/sessionID and
|
|
998
|
+
// `.args`, and edit detection reads output.args, which is the same input args
|
|
999
|
+
// object in the V2 API.
|
|
1000
|
+
function toolEventInput(event) {
|
|
1001
|
+
const input = event.input && typeof event.input === "object" ? event.input : {}
|
|
1002
|
+
return {
|
|
1003
|
+
...input,
|
|
1004
|
+
args: input,
|
|
1005
|
+
tool: event.tool,
|
|
1006
|
+
sessionID: event.sessionID,
|
|
1007
|
+
agentID: event.agent,
|
|
1008
|
+
callID: event.id,
|
|
1009
|
+
callId: event.id,
|
|
1010
|
+
}
|
|
1011
|
+
}
|
package/package.json
CHANGED
package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{"run":"1","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-1>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-1>","session_id":"<session-1>","prompt_id":"<prompt-1>","tool_input_command":"node <scratch>/plugin/harness-everything/scripts/doc-check.js check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
2
|
+
{"run":"2a","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-2>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-2>","session_id":"<session-2>","prompt_id":"<prompt-2>","tool_input_command":"node <scratch>/plugin/harness-everything/scripts/doc-check.js check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
3
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-3>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-3>","session_id":"<session-3>","prompt_id":"<prompt-3>","tool_input_command":"node \"<scratch>/plugin/harness-everything/scripts/doc-check.js\" check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
4
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PostToolUse","event":"PostToolUse","tool_use_id":"<tool-use-3>","interrupted":false,"payload":{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_use_id":"<tool-use-3>","session_id":"<session-3>","prompt_id":"<prompt-3>","tool_input_command":"node \"<scratch>/plugin/harness-everything/scripts/doc-check.js\" check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>","tool_response":{"keys":["interrupted","isImage","noOutputExpected","stderr","stdout"],"interrupted":false,"digest":"sha256:22cd89eb079e65f996432f65cfd9bed1a29366def8f3f8af9af94c26c60bd1f5"}}}
|
|
5
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-4>","payload":{"hook_event_name":"PreToolUse","tool_name":"Agent","tool_use_id":"<tool-use-4>","session_id":"<session-3>","prompt_id":"<prompt-3>","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
6
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"SubagentStart","event":"SubagentStart","agent_id":"<agent-1>","payload":{"hook_event_name":"SubagentStart","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
7
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-5>","agent_id":"<agent-1>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-5>","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","tool_input_command":"cat docs/a.md && git diff docs/a.md && shasum -a 256 docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
8
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-6>","agent_id":"<agent-1>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-6>","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","tool_input_command":"git diff docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
9
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PostToolUse","event":"PostToolUse","tool_use_id":"<tool-use-6>","agent_id":"<agent-1>","interrupted":false,"payload":{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_use_id":"<tool-use-6>","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","tool_input_command":"git diff docs/a.md","cwd":"<scratch>/project","transcript_path":"<home-path>","tool_response":{"keys":["interrupted","isImage","noOutputExpected","stderr","stdout"],"interrupted":false,"digest":"sha256:5ff449b9366059bd45c22c411728c93b8c4b5d5a083f1243c7882279b8b2e656"}}}
|
|
10
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PreToolUse","event":"PreToolUse","tool_use_id":"<tool-use-7>","agent_id":"<agent-1>","payload":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"<tool-use-7>","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","tool_input_command":"ls docs; grep -rni retr . --include=*.md","cwd":"<scratch>/project","transcript_path":"<home-path>"}}
|
|
11
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PostToolUse","event":"PostToolUse","tool_use_id":"<tool-use-7>","agent_id":"<agent-1>","interrupted":false,"payload":{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_use_id":"<tool-use-7>","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","tool_input_command":"ls docs; grep -rni retr . --include=*.md","cwd":"<scratch>/project","transcript_path":"<home-path>","tool_response":{"keys":["interrupted","isImage","noOutputExpected","returnCodeInterpretation","stderr","stdout"],"interrupted":false,"digest":"sha256:f4f1baf50b0770a29763b15493f7c7f315321f5c2c92b5288b18d20c3480f97a"}}}
|
|
12
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"SubagentStop","event":"SubagentStop","agent_id":"<agent-1>","payload":{"hook_event_name":"SubagentStop","session_id":"<session-3>","prompt_id":"<prompt-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","cwd":"<scratch>/project","transcript_path":"<home-path>","agent_transcript_path":"<home-path>","last_assistant_message":"Review verdict: approve\n\nSummary: The only change to `docs/a.md` is \"retries once\" becoming \"retries twice\". The front matter is intact, and the link to `b.md` resolves because that file exists. Nothing else in the repo mentions retries, so I found no contradiction. I could not check the \"twice\" claim against the code, since this repo has no code or other source for it. I also couldn't compute a SHA-256, because the shell blocked `shasum`.\n\nReviewed artifact digest: `<scratch>/project/docs/a.md`, the working-tree version, uncommitted on top of baseline 8648a71. The identifying content is the diff hunk `@@ -3,4 +3,4 @@`, with the blob change `0936062..3bea475`. The body line is \"The checkout retries twice. See [b](b.md).\""}}
|
|
13
|
+
{"run":"2b","source":"wrapper-stdin","wrapperLabel":"PostToolUse","event":"PostToolUse","tool_use_id":"<tool-use-4>","payload":{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_use_id":"<tool-use-4>","session_id":"<session-3>","prompt_id":"<prompt-3>","cwd":"<scratch>/project","transcript_path":"<home-path>","tool_response":{"keys":["agentId","agentType","canContinueAgent","content","harnessNoteCount","harnessSectionHash","harnessTailCount","prompt","resolvedModel","status","toolStats","totalDurationMs","totalTokens","totalToolUseCount","usage"],"status":"completed","agentId":"<agent-1>","digest":"sha256:f035f2ed002f3dc6827f3109a8fdbc99ae439472bc504fc4d460b5bf0ac4204e"}}}
|
|
14
|
+
{"run":"1","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-1>","record":{"captureId":"<capture-1>","event":"PreToolUse","tool_use_id":"<tool-use-1>","session_id":"<session-1>","command":"node <scratch>/plugin/harness-everything/scripts/doc-check.js check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
15
|
+
{"run":"1","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-1>","record":{"captureId":"<capture-2>","event":"PreToolUse","tool_use_id":"<tool-use-1>","session_id":"<session-1>","command":"node <scratch>/plugin/harness-everything/scripts/doc-check.js check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
16
|
+
{"run":"2b","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-3>","record":{"captureId":"<capture-3>","event":"PreToolUse","tool_use_id":"<tool-use-3>","session_id":"<session-3>","command":"node \"<scratch>/plugin/harness-everything/scripts/doc-check.js\" check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
17
|
+
{"run":"2b","source":"journal-record","event":"PostToolUse","tool_use_id":"<tool-use-3>","interrupted":false,"record":{"captureId":"<capture-4>","event":"PostToolUse","tool_use_id":"<tool-use-3>","session_id":"<session-3>","command":"node \"<scratch>/plugin/harness-everything/scripts/doc-check.js\" check --workspace <scratch>/project --config .agents/harness-everything/project-docs.json --changed-path docs/a.md","cwd":"<scratch>/project","interrupted":false,"responseDigest":"80ccfae628fb7127a06592ddb8439b864093a34d1b11007d5b137fbc6f95fcef"}}
|
|
18
|
+
{"run":"2b","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-4>","record":{"captureId":"<capture-5>","event":"PreToolUse","tool_use_id":"<tool-use-4>","session_id":"<session-3>","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
19
|
+
{"run":"2b","source":"journal-record","event":"SubagentStart","agent_id":"<agent-1>","record":{"captureId":"<capture-6>","event":"SubagentStart","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
20
|
+
{"run":"2b","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-5>","agent_id":"<agent-1>","record":{"captureId":"<capture-7>","event":"PreToolUse","tool_use_id":"<tool-use-5>","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","command":"cat docs/a.md && git diff docs/a.md && shasum -a 256 docs/a.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
21
|
+
{"run":"2b","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-6>","agent_id":"<agent-1>","record":{"captureId":"<capture-8>","event":"PreToolUse","tool_use_id":"<tool-use-6>","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","command":"git diff docs/a.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
22
|
+
{"run":"2b","source":"journal-record","event":"PostToolUse","tool_use_id":"<tool-use-6>","agent_id":"<agent-1>","interrupted":false,"record":{"captureId":"<capture-9>","event":"PostToolUse","tool_use_id":"<tool-use-6>","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","command":"git diff docs/a.md","cwd":"<scratch>/project","interrupted":false,"responseDigest":"f8770735e00cf9d10b07d43d91379c17ab2ca5444610dd3d5944b52912d13369"}}
|
|
23
|
+
{"run":"2b","source":"journal-record","event":"PreToolUse","tool_use_id":"<tool-use-7>","agent_id":"<agent-1>","record":{"captureId":"<capture-10>","event":"PreToolUse","tool_use_id":"<tool-use-7>","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","command":"ls docs; grep -rni retr . --include=*.md","cwd":"<scratch>/project","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
24
|
+
{"run":"2b","source":"journal-record","event":"PostToolUse","tool_use_id":"<tool-use-7>","agent_id":"<agent-1>","interrupted":false,"record":{"captureId":"<capture-11>","event":"PostToolUse","tool_use_id":"<tool-use-7>","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","command":"ls docs; grep -rni retr . --include=*.md","cwd":"<scratch>/project","interrupted":false,"responseDigest":"4a1d1e23f6b3c88bbc86f5960e2492acc61d65201a9cea4863610c068beb907c"}}
|
|
25
|
+
{"run":"2b","source":"journal-record","event":"SubagentStop","agent_id":"<agent-1>","record":{"captureId":"<capture-12>","event":"SubagentStop","session_id":"<session-3>","agent_id":"<agent-1>","agent_type":"doc-reviewer","cwd":"<scratch>/project","reviewVerdict":"approve","reviewSummary":"Summary: The only change to `docs/a.md` is \"retries once\" becoming \"retries twice\". The front matter is intact, and the link to `b.md` resolves because that file exists. Nothing else in the repo mentions retries, so I found no contradiction. I could not check the \"twice\" claim against the code, since this repo has no code or other source for it. I also couldn't compute a SHA-256, because the shell blocked `shasum`.\n\n\nReviewed artifact digest: `<scratch>/project/docs/a.md`, the working-tree version, uncommitted on top of baseline 8648a71. The identifying content is the diff hunk `@@ -3,4 +3,4 @@`, with the blob change `0936062..3bea475`. The body line is \"The checkout retries twice. See [b](b.md).\"","responseDigest":"eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c"}}
|
|
26
|
+
{"run":"2b","source":"journal-record","event":"PostToolUse","tool_use_id":"<tool-use-4>","agent_id":"<agent-1>","record":{"captureId":"<capture-13>","event":"PostToolUse","tool_use_id":"<tool-use-4>","session_id":"<session-3>","agent_id":"<agent-1>","cwd":"<scratch>/project","responseDigest":"51be05fc9c852f14829a9e01ccd0b2d9d743e0d16c237de69db752d100fad0dc"}}
|
|
@@ -111,10 +111,157 @@ function collect(context) {
|
|
|
111
111
|
return deriveDocObligations(facts).filter(item => item.strength === 'MUST' && item.applicability !== 'N/A');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// S2b credit chain (F4): produce the snapshot, accept the origin-provider events, and check them against the expected
|
|
115
|
+
// MUSTs. Credit only when the core marks the evidence eligible. Production has no qualification, so it stays withheld.
|
|
116
|
+
function creditChain(context, set, dependencies = {}) {
|
|
117
|
+
if (require('./doc-reconciliation-provider').readCaptureView(context.sessionDir).tampered) {
|
|
118
|
+
return { eligible: false, diagnostics: [{ code: 'journal-tampered', message: 'the capture journal no longer matches its immutable event records' }] };
|
|
119
|
+
}
|
|
120
|
+
const { produceDocSnapshot } = require('./doc-reconciliation-producer');
|
|
121
|
+
const { acceptDocEvents } = require('./doc-reconciliation-events');
|
|
122
|
+
const { checkAcceptedDocEvidence } = require('./doc-reconciliation-core');
|
|
123
|
+
const baseline = context.workflow.docReconciliationBaseline;
|
|
124
|
+
const requirements = Array.isArray(set?.requirements) ? set.requirements : [];
|
|
125
|
+
const taskId = `task-${requirements.map(item => item.id).join('+') || 'none'}`;
|
|
126
|
+
const sessionId = context.sessionId;
|
|
127
|
+
const { createClaudeOriginProvider, journalExecutors } = require('./doc-reconciliation-provider');
|
|
128
|
+
const captures = readCaptures(context);
|
|
129
|
+
const originProvider = dependencies.originProvider || createClaudeOriginProvider({
|
|
130
|
+
hostId: 'claude-code',
|
|
131
|
+
sessionId,
|
|
132
|
+
workspaceRoot: context.root,
|
|
133
|
+
workflowId: context.workflow.workflowId,
|
|
134
|
+
workflowRevision: context.workflow.revision,
|
|
135
|
+
taskId,
|
|
136
|
+
principals: { author: sessionId, executors: journalExecutors(captures, sessionId) },
|
|
137
|
+
baseline,
|
|
138
|
+
captures,
|
|
139
|
+
});
|
|
140
|
+
const produced = produceDocSnapshot({
|
|
141
|
+
workspaceRoot: context.root,
|
|
142
|
+
baseline,
|
|
143
|
+
workflow: { workflowId: context.workflow.workflowId, sessionId, workflowRevision: context.workflow.revision },
|
|
144
|
+
task: { taskId, requirements },
|
|
145
|
+
scope: { prdRoot: 'docs/prd/' },
|
|
146
|
+
activeHost: 'claude-code',
|
|
147
|
+
prdRoot: 'docs/prd/',
|
|
148
|
+
}, { originProvider });
|
|
149
|
+
if (produced.state !== 'ready' || !produced.snapshot) return { eligible: false, diagnostics: produced.diagnostics, unsatisfied: [] };
|
|
150
|
+
const eventRefs = captureReferences(context);
|
|
151
|
+
const accepted = acceptDocEvents({ snapshot: produced.snapshot, eventRefs }, { originProvider });
|
|
152
|
+
const checked = checkAcceptedDocEvidence({ snapshot: produced.snapshot, accepted, mode: 'verification' });
|
|
153
|
+
// Credit covers only the expected MUSTs that were both accepted and satisfied (passing, observed). N/A never counts.
|
|
154
|
+
const passing = new Set((checked.results || []).filter(r => r.observation === 'observed' && r.outcome === 'pass').map(r => r.obligationId));
|
|
155
|
+
const expected = new Set(checked.checkedMustIds || []);
|
|
156
|
+
const satisfied = produced.snapshot.obligations.filter(o => o.strength === 'MUST' && o.applicability !== 'N/A' && expected.has(o.id) && passing.has(o.id));
|
|
157
|
+
return {
|
|
158
|
+
eligible: checked.eligible === true,
|
|
159
|
+
capabilities: checked.eligible === true ? [...new Set(satisfied.map(o => o.capability))] : [],
|
|
160
|
+
satisfiedObligationIds: satisfied.map(o => o.id),
|
|
161
|
+
unsatisfied: checked.unsatisfied || [],
|
|
162
|
+
acceptedRefs: Object.keys(accepted.acceptedIndex || {}),
|
|
163
|
+
diagnostics: [...(accepted.diagnostics || []), ...(checked.diagnostics || [])],
|
|
164
|
+
rejected: accepted.rejected || [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Report diagnostics are objects with the obligation they concern (F12); a global fault has obligationId null.
|
|
169
|
+
function diagnosticOf(item) {
|
|
170
|
+
if (item && typeof item === 'object') return { ...item, code: item.code || 'unknown', obligationId: item.obligationId ?? null };
|
|
171
|
+
return { code: String(item), obligationId: null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Review-mode refusal (F4 I2a): with the pilot ON in a supported (non-Fable) workflow, a review step PASS needs an
|
|
175
|
+
// independent quality-review record. The provider's own independence rules decide it: a host agent_id that differs from
|
|
176
|
+
// the author session, an explicit approve verdict, a non-empty summary and the bound final-artifact digest.
|
|
177
|
+
function reviewRequired(set) {
|
|
178
|
+
return enabled() && set?.stepBindingsEnabled === true && !String(set?.strategy || '').startsWith('fable-');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The review step is independent of credit qualification: the step records host-observed independence, while document
|
|
182
|
+
// credit itself still needs the validated qualification (assess).
|
|
183
|
+
function independentReviewAccepted(context) {
|
|
184
|
+
const { reviewRecordIndependent, journalExecutors } = require('./doc-reconciliation-provider');
|
|
185
|
+
const view = require('./doc-reconciliation-provider').readCaptureView(context.sessionDir);
|
|
186
|
+
if (view.tampered) return false;
|
|
187
|
+
const captures = view.records;
|
|
188
|
+
const principals = { author: context.sessionId, executors: journalExecutors(captures, context.sessionId) };
|
|
189
|
+
const pin = context.workflow.docReconciliationBaseline;
|
|
190
|
+
return captures.some(item => recordOf(item) && item.session_id === context.sessionId
|
|
191
|
+
&& recordOf(item.observedIdentity) && item.observedIdentity.workflowId === context.workflow.workflowId
|
|
192
|
+
&& item.observedIdentity.workflowRevision === context.workflow.revision
|
|
193
|
+
&& (!pin || (recordOf(item.observedIdentity.baselinePin) && item.observedIdentity.baselinePin.commit === pin.commit))
|
|
194
|
+
&& reviewRecordIndependent(item, captures, principals));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recordOf(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
198
|
+
|
|
199
|
+
// The session capture records are the only host-event source. A missing journal means no events; a journal that no longer
|
|
200
|
+
// equals its immutable event files is tampered and yields no events, so the chain withholds.
|
|
201
|
+
function readCaptures(context) {
|
|
202
|
+
return require('./doc-reconciliation-provider').readCaptureView(context.sessionDir).records;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Only explicit PostToolUse doc-check candidates are offered; ordinary Edit or Agent results are not (R3-01).
|
|
206
|
+
const docCheckCandidate = item => item.event === 'PostToolUse'
|
|
207
|
+
&& require('./doc-reconciliation-provider').resolveDocCheckInvocation(item.command, item.cwd).kind !== 'not-candidate';
|
|
208
|
+
|
|
209
|
+
function captureReferences(context) {
|
|
210
|
+
const captures = readCaptures(context);
|
|
211
|
+
const candidates = captures
|
|
212
|
+
.filter(item => item && typeof item.ref === 'string' && (item.event === 'SubagentStop' || docCheckCandidate(item)))
|
|
213
|
+
.filter(item => item.event !== 'SubagentStop');
|
|
214
|
+
const reviews = captures.filter(item => item && typeof item.ref === 'string' && item.event === 'SubagentStop');
|
|
215
|
+
const references = [];
|
|
216
|
+
for (const [index, capability] of ['doc-lint', 'link-check'].entries()) {
|
|
217
|
+
const ordered = index ? candidates.slice(1).concat(candidates.slice(0, 1)) : candidates;
|
|
218
|
+
references.push(...ordered.map(item => `${item.ref}#${capability}`));
|
|
219
|
+
}
|
|
220
|
+
// The adapter accepts one event per capability; rotating candidate order lets separate valid runs fill each capability.
|
|
221
|
+
return [...references, ...reviews.map(item => `${item.ref}#quality-review`)];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// R3-02: verification recorded before an installed-runtime change is withdrawn once the live doc-check leaves the pin.
|
|
225
|
+
// Capture time sees the pristine runtime, so only assessment can catch a later change. Scoped to sessions that ran a doc-check.
|
|
226
|
+
function withdrawChangedRuntimeVerification(context) {
|
|
227
|
+
try {
|
|
228
|
+
const provider = require('./doc-reconciliation-provider');
|
|
229
|
+
const installed = provider.installedDocCheck();
|
|
230
|
+
if (!installed || provider.fileDigest(installed) === provider.SHIPPED_DOC_CHECK_DIGEST) return;
|
|
231
|
+
if (!readCaptures(context).some(item => {
|
|
232
|
+
if (!item || item.event !== 'PostToolUse') return false;
|
|
233
|
+
const invocation = provider.resolveDocCheckInvocation(item.command, item.cwd);
|
|
234
|
+
return invocation.kind === 'accepted' || invocation.reason === 'implementation-pin-mismatch';
|
|
235
|
+
})) return;
|
|
236
|
+
const file = path.join(context.sessionDir, 'handoff-state.json');
|
|
237
|
+
const handoff = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
238
|
+
const genericAt = handoff.lastGenericVerifyAt;
|
|
239
|
+
const hasGeneric = typeof genericAt === 'number' && Number.isFinite(genericAt);
|
|
240
|
+
const hasDoc = typeof handoff.lastDocVerifyAt === 'number' && Number.isFinite(handoff.lastDocVerifyAt);
|
|
241
|
+
if (!hasGeneric && !hasDoc && handoff.lastVerifyAt === undefined) return;
|
|
242
|
+
delete handoff.lastDocVerifyAt;
|
|
243
|
+
delete handoff.lastDocVerifyExitCode;
|
|
244
|
+
if (hasGeneric) {
|
|
245
|
+
handoff.lastVerifyAt = genericAt;
|
|
246
|
+
if (Object.prototype.hasOwnProperty.call(handoff, 'lastGenericVerifyExitCode')) {
|
|
247
|
+
handoff.lastVerifyExitCode = handoff.lastGenericVerifyExitCode;
|
|
248
|
+
} else {
|
|
249
|
+
delete handoff.lastVerifyExitCode;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
// Older aggregate-only state has unknown ownership; do not preserve it as unaffected evidence.
|
|
253
|
+
delete handoff.lastVerifyAt;
|
|
254
|
+
delete handoff.lastVerifyExitCode;
|
|
255
|
+
}
|
|
256
|
+
atomicWriteJson(file, handoff);
|
|
257
|
+
} catch { /* fail-open: no handoff state yet, or nothing to withdraw */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
114
260
|
// Assessment is deliberately negative-credit only in S1. Unsupported topologies never enter
|
|
115
261
|
// generic step reconciliation, and failures (including report persistence) never escape to Stop.
|
|
116
|
-
function assess(context, set) {
|
|
262
|
+
function assess(context, set, dependencies = {}) {
|
|
117
263
|
if (!enabled()) return { withhold: false };
|
|
264
|
+
withdrawChangedRuntimeVerification(context);
|
|
118
265
|
const report = {
|
|
119
266
|
schemaVersion: 1, reportOrigin: 'runtime', pilotStatus: 'unavailable', pilotCredit: false,
|
|
120
267
|
identity: null, expectedMustIds: [], results: [], unsatisfied: [], diagnostics: [],
|
|
@@ -133,6 +280,27 @@ function assess(context, set) {
|
|
|
133
280
|
reasonCode: item.applicability === 'unknown' ? 'unknown-applicability' : 'producer-unavailable',
|
|
134
281
|
}));
|
|
135
282
|
if (!withhold) report.diagnostics.push('trusted-non-document-scope');
|
|
283
|
+
if (withhold) {
|
|
284
|
+
let chain;
|
|
285
|
+
try { chain = creditChain(context, set, dependencies); }
|
|
286
|
+
catch (error) { chain = { eligible: false, diagnostics: [{ code: 'collector-unavailable', message: String(error.message) }] }; }
|
|
287
|
+
// The unsatisfied list is rewritten from the chain: a MUST that the chain did not satisfy stays listed (F3).
|
|
288
|
+
const satisfied = new Set(chain.satisfiedObligationIds || []);
|
|
289
|
+
report.unsatisfied = [
|
|
290
|
+
...musts.filter(item => !satisfied.has(item.id)).map(item => ({
|
|
291
|
+
obligationId: item.id, state: item.applicability === 'unknown' ? 'unknown' : 'pending',
|
|
292
|
+
reasonCode: item.applicability === 'unknown' ? 'unknown-applicability' : 'producer-unavailable',
|
|
293
|
+
})),
|
|
294
|
+
...(chain.unsatisfied || []),
|
|
295
|
+
];
|
|
296
|
+
report.creditedCapabilities = chain.eligible === true ? chain.capabilities : [];
|
|
297
|
+
report.acceptedRefs = chain.acceptedRefs || [];
|
|
298
|
+
report.diagnostics.push(...(chain.diagnostics || []));
|
|
299
|
+
// Credit needs an empty unsatisfied list across both sources and at least one satisfied capability.
|
|
300
|
+
withhold = !(chain.eligible === true && report.unsatisfied.length === 0 && report.creditedCapabilities.length > 0);
|
|
301
|
+
report.pilotCredit = !withhold;
|
|
302
|
+
report.pilotStatus = report.pilotCredit ? 'credited' : 'unresolved';
|
|
303
|
+
}
|
|
136
304
|
} catch (error) {
|
|
137
305
|
withhold = true;
|
|
138
306
|
const reason = ['baseline-unavailable', 'config-unavailable', 'scope-unavailable', 'config-scope-changed'].includes(error.message)
|
|
@@ -141,6 +309,7 @@ function assess(context, set) {
|
|
|
141
309
|
report.unsatisfied.push({ obligationId: 'doc-change:scope', state: 'unavailable', reasonCode: reason });
|
|
142
310
|
}
|
|
143
311
|
}
|
|
312
|
+
report.diagnostics = report.diagnostics.map(diagnosticOf);
|
|
144
313
|
try { atomicWriteJson(path.join(context.sessionDir, 'doc-reconciliation-pilot.json'), report); }
|
|
145
314
|
catch {
|
|
146
315
|
if (supported) withhold = true;
|
|
@@ -149,4 +318,4 @@ function assess(context, set) {
|
|
|
149
318
|
return { withhold, report };
|
|
150
319
|
}
|
|
151
320
|
|
|
152
|
-
module.exports = { enabled, captureBaseline, assess };
|
|
321
|
+
module.exports = { enabled, captureBaseline, assess, reviewRequired, independentReviewAccepted };
|