harness-everything 0.44.0 → 0.46.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.
Files changed (46) hide show
  1. package/.agents/harness-everything/project-docs.json +3 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +27 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
  6. package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +19 -1
  7. package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
  8. package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
  9. package/ci/mechanism-325-f2-doc-craft.test.js +2 -2
  10. package/ci/mechanism-325-f3-derived-views.test.js +5 -2
  11. package/ci/mechanism-325-f4-projections.test.js +11 -1
  12. package/ci/mechanism-pr-a-conflict-check.test.js +507 -0
  13. package/ci/mechanism-prb-outline-review.test.js +1265 -0
  14. package/docs/adr/DOCS-001-project-docs-config.md +32 -0
  15. package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
  16. package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
  17. package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
  18. package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
  19. package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
  20. package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
  21. package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
  22. package/docs/doc-workflow-reconciliation.md +32 -1
  23. package/docs/governed-index.md +8 -0
  24. package/docs/platform-capabilities.md +6 -0
  25. package/harness-everything/SKILL.md +1 -1
  26. package/harness-everything/scripts/doc-check.js +84 -6
  27. package/harness-everything/scripts/doc-evidence.js +55 -0
  28. package/harness-everything/scripts/doc-lifecycle.js +51 -6
  29. package/hooks/hooks.json +1 -1
  30. package/hooks/scripts/lib/doc-reconciliation-events.js +138 -9
  31. package/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
  32. package/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
  33. package/hooks/scripts/state-persist.js +10 -0
  34. package/opencode-plugin/plugin.json +1 -1
  35. package/package.json +1 -1
  36. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  37. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +138 -9
  38. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +131 -15
  39. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +941 -13
  40. package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
  41. package/plugins/harness-everything/plugin.json +1 -1
  42. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  43. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +84 -6
  44. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +51 -6
  46. package/sbom.cdx.json +7 -7
@@ -0,0 +1,507 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // PR-A RED/GREEN contract for the full-corpus conflict-check producer.
5
+ const assert = require('node:assert/strict');
6
+ const crypto = require('node:crypto');
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+ const { execFileSync, spawnSync } = require('node:child_process');
11
+
12
+ const ROOT = path.resolve(__dirname, '..');
13
+ const DOC_CHECK = path.join(ROOT, 'harness-everything', 'scripts', 'doc-check.js');
14
+ const PROVIDER = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'doc-reconciliation-provider.js'));
15
+ const PILOT = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'doc-reconciliation-pilot.js'));
16
+ const PRODUCER = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'doc-reconciliation-producer.js'));
17
+ const EVENTS = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'doc-reconciliation-events.js'));
18
+ const EVIDENCE = require(path.join(ROOT, 'harness-everything', 'scripts', 'doc-evidence.js'));
19
+ const LIFECYCLE = require(path.join(ROOT, 'harness-everything', 'scripts', 'doc-lifecycle.js'));
20
+ const REQUIREMENTS = [
21
+ { id: 'req-docs', stepType: 'doc-change', summary: 'Update the ADR', acceptance: 'the normative change is reviewed' },
22
+ { id: 'req-verify', stepType: 'verification', summary: 'Verify the ADR', acceptance: 'full-corpus conflict evidence is recorded' },
23
+ ];
24
+ const PILOT_CONFIG_TEXT = JSON.stringify({
25
+ governed: [{ path: 'docs/current/**', role: 'adr' }], legacy: [], domains: ['GRPH'],
26
+ }, null, 2) + '\n';
27
+ const CONFIG = '.agents/harness-everything/project-docs.json';
28
+ const CONFIG_TEXT = JSON.stringify({
29
+ governed: [{ path: 'docs/current/**', role: 'reference' }],
30
+ legacy: ['docs/legacy/**'],
31
+ domains: ['GRPH', 'LEG'],
32
+ }, null, 2) + '\n';
33
+
34
+ const roots = [];
35
+ function write(root, relative, text) {
36
+ const file = path.join(root, relative);
37
+ fs.mkdirSync(path.dirname(file), { recursive: true });
38
+ fs.writeFileSync(file, text);
39
+ }
40
+ function git(root, args) {
41
+ const env = { ...process.env, GIT_CONFIG_NOSYSTEM: '1' };
42
+ for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
43
+ return execFileSync('git', ['-C', root, ...args], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
44
+ }
45
+ function reference(id, extra = '') {
46
+ return `---\nrole: reference\nstatus: active\nid: ${id}\n${extra}---\n\n# ${id}\n\nA governed document.\n`;
47
+ }
48
+ function adr(id, body) {
49
+ return `---\nid: ${id}\nrole: adr\nstatus: accepted\n---\n\n# ${id}\n\n## Decision\n\n${body}\n`;
50
+ }
51
+ function workspace(files, configText = CONFIG_TEXT) {
52
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'pra-conflict-check-')));
53
+ roots.push(root);
54
+ write(root, CONFIG, configText);
55
+ for (const [relative, contents] of Object.entries(files)) write(root, relative, contents);
56
+ git(root, ['init', '-q']);
57
+ git(root, ['add', '-A']);
58
+ git(root, ['-c', 'user.name=Harness Test', '-c', 'user.email=harness@example.invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '-m', 'baseline']);
59
+ return { root, base: git(root, ['rev-parse', 'HEAD']) };
60
+ }
61
+ function runCheck(root, base, changed = ['docs/current/a.md'], extra = []) {
62
+ return spawnSync(process.execPath, [DOC_CHECK, 'check', '--workspace', root, '--config', CONFIG,
63
+ ...changed.flatMap(item => ['--changed-path', item]), '--base', base, ...extra],
64
+ { cwd: root, encoding: 'utf8' });
65
+ }
66
+ function report(result) {
67
+ try { return JSON.parse(result.stdout); } catch { return null; }
68
+ }
69
+ function sha(text) { return crypto.createHash('sha256').update(text).digest('hex'); }
70
+
71
+ function seedPilotScenario() {
72
+ const baselineDoc = adr('GRPH-001', 'Checkout retries once.');
73
+ const { root, base } = workspace({
74
+ 'docs/current/a.md': baselineDoc,
75
+ 'docs/current/b.md': adr('GRPH-002', 'The receipt is durable.'),
76
+ }, PILOT_CONFIG_TEXT);
77
+ write(root, 'docs/current/a.md', adr('GRPH-001', 'Checkout retries twice.'));
78
+ const sessionId = 'pra-session';
79
+ const workflowId = 'pra-workflow';
80
+ const sessionDir = path.join(root, '.harness-session');
81
+ fs.mkdirSync(sessionDir, { recursive: true });
82
+ const plan = { strategySelection: 'selected', tier: 'tier2', strategy: 'iterative-single' };
83
+ const context = {
84
+ sessionId, sessionDir, root,
85
+ workflow: { workflowId, revision: 1, state: 'pending', strategy: 'iterative-single', workflowPlan: plan },
86
+ };
87
+ const obligations = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'workflow-obligations.js'));
88
+ obligations.initializePlanningContract(context, plan);
89
+ obligations.recordPlanning(context, JSON.stringify(REQUIREMENTS), plan, 'iterative-single', 'one bounded documentation verification path');
90
+ obligations.materializeExecutionObligations(context, plan);
91
+ const loaded = obligations.loadObligationSet(context, plan);
92
+ if (!loaded.set) throw new Error(`pilot fixture failed to load obligations: ${loaded.error}`);
93
+ const previousPilot = process.env.HARNESS_DOC_RECONCILIATION_PILOT;
94
+ process.env.HARNESS_DOC_RECONCILIATION_PILOT = '1';
95
+ try { PILOT.captureBaseline(context); }
96
+ finally {
97
+ if (previousPilot === undefined) delete process.env.HARNESS_DOC_RECONCILIATION_PILOT;
98
+ else process.env.HARNESS_DOC_RECONCILIATION_PILOT = previousPilot;
99
+ }
100
+ write(sessionDir, 'workflow-run.json', JSON.stringify({ workflowId, revision: 1, docReconciliationBaseline: context.workflow.docReconciliationBaseline }));
101
+ return { root, base, context, plan, set: loaded.set, sessionId, workflowId, sessionDir };
102
+ }
103
+
104
+ function addHostCapture(scenario, payload, eventName = 'PostToolUse') {
105
+ const capture = PROVIDER.buildCaptureRecord(payload, eventName, scenario.root);
106
+ if (!capture) throw new Error(`capture builder did not accept ${eventName}`);
107
+ capture.observedIdentity = PROVIDER.observedIdentityFor(scenario.sessionDir);
108
+ PROVIDER.writeImmutableEvent(scenario.sessionDir, capture);
109
+ PROVIDER.rebuildJournalView(scenario.sessionDir);
110
+ return capture;
111
+ }
112
+
113
+ function addAssessmentCapture(scenario, changed = ['docs/current/a.md'], beforeCapture = null) {
114
+ const args = [DOC_CHECK, 'check', '--workspace', scenario.root, '--config', CONFIG,
115
+ ...changed.flatMap(item => ['--changed-path', item]), '--base', scenario.base, '--conflict-assessment'];
116
+ const result = spawnSync(process.execPath, args, { cwd: scenario.root, encoding: 'utf8' });
117
+ if (result.status !== 0) throw new Error(`fixture assessment failed with ${result.status}: ${report(result)?.conflictAssessment?.reason || result.stderr}`);
118
+ if (beforeCapture) beforeCapture();
119
+ const command = ['node', ...args].join(' ');
120
+ return { result, capture: addHostCapture(scenario, {
121
+ cwd: scenario.root, session_id: scenario.sessionId, tool_name: 'Bash', tool_use_id: 'check-1',
122
+ tool_input: { command }, tool_response: { stdout: result.stdout, interrupted: false },
123
+ }) };
124
+ }
125
+
126
+ function addReviewCapture(scenario) {
127
+ const reviewer = 'independent-reviewer';
128
+ addHostCapture(scenario, {
129
+ cwd: scenario.root, session_id: scenario.sessionId, tool_name: 'Task', tool_use_id: 'review-task',
130
+ tool_input: { description: 'Read-only ADR review' }, tool_response: { agentId: reviewer, interrupted: false },
131
+ });
132
+ const digest = sha(fs.readFileSync(path.join(scenario.root, 'docs/current/a.md')));
133
+ addHostCapture(scenario, {
134
+ cwd: scenario.root, session_id: scenario.sessionId, tool_use_id: 'review-stop', agent_id: reviewer, agent_type: 'general-purpose',
135
+ last_assistant_message: `Review verdict: approve\nThe updated ADR is clear.\nReviewed artifact: docs/current/a.md sha256:${digest}`,
136
+ }, 'SubagentStop');
137
+ }
138
+
139
+ function testOrigin(scenario, captures = PROVIDER.readCaptureView(scenario.sessionDir).records) {
140
+ return PROVIDER.createClaudeOriginProvider({
141
+ hostId: 'claude-code', sessionId: scenario.sessionId, workspaceRoot: scenario.root,
142
+ workflowId: scenario.workflowId, workflowRevision: 1,
143
+ taskId: `task-${REQUIREMENTS.map(item => item.id).join('+')}`,
144
+ principals: { author: scenario.sessionId, executors: [scenario.sessionId] },
145
+ baseline: scenario.context.workflow.docReconciliationBaseline,
146
+ captures,
147
+ qualification: { source: 'fake-test-fixture', faked: true, ref: 'pra-fixture', toolUseIdPopulated: true, agentIdPopulated: true },
148
+ });
149
+ }
150
+
151
+ function providerIdentity(scenario) {
152
+ return {
153
+ workspaceId: 'pra-workspace', sessionId: scenario.sessionId, workflowId: scenario.workflowId,
154
+ workflowRevision: 1, taskId: `task-${REQUIREMENTS.map(item => item.id).join('+')}`,
155
+ taskScopeDigest: '1'.repeat(64), baselineDigest: '2'.repeat(64), candidateDigest: '3'.repeat(64),
156
+ configDigest: '4'.repeat(64), prdRoot: 'docs/prd/',
157
+ };
158
+ }
159
+
160
+ function produceSnapshot(scenario, origin = testOrigin(scenario)) {
161
+ const result = PRODUCER.produceDocSnapshot({
162
+ workspaceRoot: scenario.root, baseline: scenario.context.workflow.docReconciliationBaseline,
163
+ workflow: { workflowId: scenario.workflowId, workflowRevision: 1, sessionId: scenario.sessionId },
164
+ task: { taskId: `task-${REQUIREMENTS.map(item => item.id).join('+')}`, requirements: REQUIREMENTS },
165
+ scope: { prdRoot: 'docs/prd/' }, activeHost: 'claude-code', prdRoot: 'docs/prd/',
166
+ }, { originProvider: origin });
167
+ if (result.state !== 'ready' || !result.snapshot) throw new Error(`pilot snapshot unavailable: ${JSON.stringify(result.diagnostics)}`);
168
+ return result.snapshot;
169
+ }
170
+
171
+ let passed = 0;
172
+ let failed = 0;
173
+ function check(id, fn) {
174
+ try { fn(); passed += 1; console.log(`PASS ${id}`); }
175
+ catch (error) { failed += 1; console.error(`FAIL ${id} — ${error.message.split('\n')[0]}`); }
176
+ }
177
+
178
+ try {
179
+ check('F1 full clean corpus emits a bound conflict-check projection', () => {
180
+ const docs = {
181
+ 'docs/current/a.md': reference('GRPH-001'),
182
+ 'docs/current/b.md': reference('GRPH-002'),
183
+ 'docs/legacy/LEG-001.md': reference('LEG-001'),
184
+ };
185
+ const { root, base } = workspace(docs);
186
+ const result = runCheck(root, base, ['docs/current/a.md'], ['--conflict-assessment']);
187
+ const out = report(result);
188
+ assert.equal(result.status, 0, `expected clean assessment exit 0, got ${result.status}: ${out?.lint?.error || result.stderr}`);
189
+ assert.equal(out.mode, 'conflict-assessment');
190
+ assert.equal(out.capabilities['conflict-check'].state, 'pass');
191
+ assert.deepEqual(out.capabilities['conflict-check'].checkedPaths, ['docs/current/a.md']);
192
+ assert.deepEqual(out.capabilities['conflict-check'].corpus.paths, Object.keys(docs).sort());
193
+ assert.deepEqual(out.capabilities['conflict-check'].corpus.digests, Object.fromEntries(Object.entries(docs).map(([p, text]) => [p, sha(text)])));
194
+ assert.equal(out.capabilities['conflict-check'].corpus.config.path, CONFIG);
195
+ assert.equal(out.capabilities['conflict-check'].corpus.config.digest, sha(fs.readFileSync(path.join(root, CONFIG))));
196
+ assert.equal(out.capabilities['conflict-check'].corpus.baseCommit, base);
197
+ });
198
+
199
+ check('F2 plain changed-path check cannot produce conflict-check credit for unchanged conflicts', () => {
200
+ const { root, base } = workspace({
201
+ 'docs/current/a.md': reference('GRPH-001', 'conflicts_with: [GRPH-002]\n'),
202
+ 'docs/current/b.md': reference('GRPH-002'),
203
+ 'docs/current/probe.md': reference('GRPH-003'),
204
+ });
205
+ const result = runCheck(root, base, ['docs/current/probe.md']);
206
+ const out = report(result);
207
+ assert.equal(result.status, 0, `unchanged conflict must stay a warning in plain check, got ${result.status}`);
208
+ assert.equal(out.capabilities['doc-lint'].state, 'pass');
209
+ assert.equal(out.capabilities['link-check'].state, 'pass');
210
+ assert.equal(out.capabilities['conflict-check'], undefined);
211
+ const command = `node ${DOC_CHECK} check --workspace ${root} --config ${CONFIG} --changed-path docs/current/probe.md --base ${base}`;
212
+ const invocation = PROVIDER.resolveDocCheckInvocation(command, root);
213
+ assert.equal(invocation.kind, 'accepted');
214
+ assert.notEqual(invocation.conflictAssessment, true);
215
+ });
216
+
217
+ check('F3 full assessment rejects recorded conflicts on unchanged files', () => {
218
+ const { root, base } = workspace({
219
+ 'docs/current/a.md': reference('GRPH-001', 'conflicts_with: [GRPH-002]\n'),
220
+ 'docs/current/b.md': reference('GRPH-002'),
221
+ 'docs/current/probe.md': reference('GRPH-003'),
222
+ });
223
+ const result = runCheck(root, base, ['docs/current/probe.md'], ['--conflict-assessment']);
224
+ const out = report(result);
225
+ assert.equal(result.status, 1, `full lint should reject recorded conflict, got ${result.status}`);
226
+ assert.ok(out.lint.diagnostics.some(item => item.code === 'recorded-conflict'));
227
+ assert.notEqual(out.capabilities?.['conflict-check']?.state, 'pass');
228
+ });
229
+
230
+ check('assessment flag is structurally parsed and accepted by the provider', () => {
231
+ const { root, base } = workspace({ 'docs/current/a.md': reference('GRPH-001') });
232
+ const command = `node ${DOC_CHECK} check --workspace ${root} --config ${CONFIG} --changed-path docs/current/a.md --base ${base} --conflict-assessment`;
233
+ const invocation = PROVIDER.resolveDocCheckInvocation(command, root);
234
+ assert.equal(invocation.kind, 'accepted', `provider rejected structured assessment argv: ${invocation.reason}`);
235
+ assert.equal(invocation.conflictAssessment, true);
236
+ assert.equal(invocation.base, base);
237
+ });
238
+
239
+ check('duplicate assessment flag is rejected as a duplicate', () => {
240
+ const { root, base } = workspace({ 'docs/current/a.md': reference('GRPH-001') });
241
+ const result = runCheck(root, base, ['docs/current/a.md'], ['--conflict-assessment', '--conflict-assessment']);
242
+ const out = report(result);
243
+ assert.equal(result.status, 2);
244
+ assert.match(out.lint.error, /repeated argument: --conflict-assessment/);
245
+ });
246
+
247
+ check('assessment mode requires its baseline pin', () => {
248
+ const { root, base } = workspace({ 'docs/current/a.md': reference('GRPH-001') });
249
+ const args = [DOC_CHECK, 'check', '--workspace', root, '--config', CONFIG, '--changed-path', 'docs/current/a.md', '--conflict-assessment'];
250
+ const result = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8' });
251
+ assert.equal(result.status, 2);
252
+ assert.match(report(result).lint.error, /--conflict-assessment requires --base/);
253
+ const command = `node ${DOC_CHECK} check --workspace ${root} --config ${CONFIG} --changed-path docs/current/a.md --conflict-assessment`;
254
+ assert.equal(PROVIDER.resolveDocCheckInvocation(command, root).reason, 'conflict-assessment-base-required');
255
+ const unpinned = runCheck(root, 'HEAD', ['docs/current/a.md'], ['--conflict-assessment']);
256
+ assert.equal(report(unpinned).conflictAssessment.state, 'unavailable');
257
+ assert.equal(report(unpinned).conflictAssessment.reason, 'base-not-pinned');
258
+ const movingBaseCommand = `node ${DOC_CHECK} check --workspace ${root} --config ${CONFIG} --changed-path docs/current/a.md --base HEAD --conflict-assessment`;
259
+ assert.equal(PROVIDER.resolveDocCheckInvocation(movingBaseCommand, root).reason, 'conflict-assessment-base-unpinned');
260
+ assert.ok(base);
261
+ });
262
+
263
+ check('F4 full graph resolves cross-domain legacy targets and rejects unresolved references', () => {
264
+ const crossDomain = workspace({
265
+ 'docs/current/a.md': reference('GRPH-001', 'conflicts_with: [legacy:LEG-001]\n'),
266
+ 'docs/legacy/LEG-001.md': reference('LEG-001'),
267
+ 'docs/current/probe.md': reference('GRPH-003'),
268
+ });
269
+ const crossResult = runCheck(crossDomain.root, crossDomain.base, ['docs/current/probe.md'], ['--conflict-assessment']);
270
+ const crossOut = report(crossResult);
271
+ assert.equal(crossResult.status, 1);
272
+ assert.ok(crossOut.lint.diagnostics.some(item => item.code === 'recorded-conflict' && item.path === 'docs/current/a.md'), JSON.stringify(crossOut.lint.diagnostics));
273
+ assert.notEqual(crossOut.capabilities?.['conflict-check']?.state, 'pass');
274
+
275
+ const unresolved = workspace({
276
+ 'docs/current/a.md': reference('GRPH-001', 'conflicts_with: [LEG-404]\n'),
277
+ 'docs/current/probe.md': reference('GRPH-003'),
278
+ });
279
+ const unresolvedResult = runCheck(unresolved.root, unresolved.base, ['docs/current/probe.md'], ['--conflict-assessment']);
280
+ const unresolvedOut = report(unresolvedResult);
281
+ assert.equal(unresolvedResult.status, 1);
282
+ assert.ok(unresolvedOut.lint.diagnostics.some(item => item.code === 'unresolved-ref'), JSON.stringify(unresolvedOut.lint.diagnostics));
283
+ assert.notEqual(unresolvedOut.capabilities?.['conflict-check']?.state, 'pass');
284
+ });
285
+
286
+ check('omitted corpus and stale base stay unavailable', () => {
287
+ const config = JSON.stringify({ governed: [
288
+ { path: 'docs/current/a.md', role: 'reference' },
289
+ { path: 'docs/current/missing.md', role: 'reference' },
290
+ ], legacy: [], domains: ['GRPH'] }, null, 2) + '\n';
291
+ const omitted = workspace({ 'docs/current/a.md': reference('GRPH-001') }, config);
292
+ const incomplete = report(runCheck(omitted.root, omitted.base, ['docs/current/a.md'], ['--conflict-assessment']));
293
+ assert.equal(incomplete.conflictAssessment.state, 'unavailable');
294
+ assert.equal(incomplete.conflictAssessment.reason, 'corpus-incomplete');
295
+ assert.notEqual(incomplete.capabilities?.['conflict-check']?.state, 'pass');
296
+
297
+ const clean = workspace({ 'docs/current/a.md': reference('GRPH-001') });
298
+ const stale = report(runCheck(clean.root, 'deadbeef', ['docs/current/a.md'], ['--conflict-assessment']));
299
+ assert.notEqual(stale?.capabilities?.['conflict-check']?.state, 'pass');
300
+ });
301
+
302
+ check('mode-looking argument text is not parsed as an assessment flag', () => {
303
+ const { root, base } = workspace({ 'docs/current/a.md': reference('GRPH-001') });
304
+ const command = `node ${DOC_CHECK} check --workspace ${root} --config ${CONFIG} --changed-path "docs/current/a.md --conflict-assessment" --base ${base}`;
305
+ const invocation = PROVIDER.resolveDocCheckInvocation(command, root);
306
+ assert.equal(invocation.kind, 'accepted');
307
+ assert.notEqual(invocation.conflictAssessment, true);
308
+ });
309
+
310
+ check('end-to-end pilot discovers conflict-check from real capture records', () => {
311
+ const previous = process.env.HARNESS_DOC_RECONCILIATION_PILOT;
312
+ process.env.HARNESS_DOC_RECONCILIATION_PILOT = '1';
313
+ try {
314
+ const scenario = seedPilotScenario();
315
+ const { capture } = addAssessmentCapture(scenario);
316
+ addReviewCapture(scenario);
317
+ const result = PILOT.assess(scenario.context, scenario.set, { originProvider: testOrigin(scenario) });
318
+ assert.equal(result.withhold, false, JSON.stringify(result.report));
319
+ assert.equal(result.report.pilotCredit, true);
320
+ assert.ok(result.report.creditedCapabilities.includes('conflict-check'), JSON.stringify(result.report));
321
+ assert.ok(result.report.acceptedRefs.includes(`${capture.ref}#conflict-check`), JSON.stringify(result.report.acceptedRefs));
322
+ } finally {
323
+ if (previous === undefined) delete process.env.HARNESS_DOC_RECONCILIATION_PILOT;
324
+ else process.env.HARNESS_DOC_RECONCILIATION_PILOT = previous;
325
+ }
326
+ });
327
+
328
+ check('capture rejects corpus or config changes between check and PostToolUse', () => {
329
+ const corpusChanged = seedPilotScenario();
330
+ const changedRun = addAssessmentCapture(corpusChanged, ['docs/current/a.md'], () => {
331
+ write(corpusChanged.root, 'docs/current/b.md', adr('GRPH-002', 'The receipt is now archived.'));
332
+ });
333
+ const corpusOrigin = testOrigin(corpusChanged);
334
+ const corpusResult = corpusOrigin.resolveEvent({
335
+ reference: `${changedRun.capture.ref}#conflict-check`, identity: providerIdentity(corpusChanged),
336
+ });
337
+ assert.equal(corpusResult.status, 'rejected');
338
+ assert.ok(corpusResult.diagnostics.some(item => item.code === 'corpus-changed-after-check'));
339
+
340
+ const configChanged = seedPilotScenario();
341
+ const configRun = addAssessmentCapture(configChanged, ['docs/current/a.md'], () => {
342
+ fs.appendFileSync(path.join(configChanged.root, CONFIG), '\n');
343
+ });
344
+ const configOrigin = testOrigin(configChanged);
345
+ const configResult = configOrigin.resolveEvent({
346
+ reference: `${configRun.capture.ref}#conflict-check`, identity: providerIdentity(configChanged),
347
+ });
348
+ assert.equal(configResult.status, 'rejected');
349
+ assert.ok(configResult.diagnostics.some(item => item.code === 'config-changed-after-check'));
350
+ });
351
+
352
+ check('corpus changes after capture are rejected against the later snapshot', () => {
353
+ const scenario = seedPilotScenario();
354
+ const { capture } = addAssessmentCapture(scenario);
355
+ const origin = testOrigin(scenario);
356
+ write(scenario.root, 'docs/current/b.md', adr('GRPH-002', 'The receipt is now archived.'));
357
+ const snapshot = produceSnapshot(scenario, origin);
358
+ const accepted = EVENTS.acceptDocEvents({ snapshot, eventRefs: [`${capture.ref}#conflict-check`] }, { originProvider: origin });
359
+ assert.equal(accepted.state, 'unresolved');
360
+ assert.ok(accepted.rejected.some(item => item.code === 'corpus-mismatch'));
361
+ });
362
+
363
+ check('symlink skips and unreadable corpus files mark the full collection incomplete', () => {
364
+ const { root } = workspace({ 'docs/current/a.md': reference('GRPH-001') });
365
+ const external = path.join(os.tmpdir(), `pra-outside-${process.pid}.md`);
366
+ write(root, 'docs/current/skipped.md', reference('GRPH-002'));
367
+ fs.writeFileSync(external, reference('GRPH-003'));
368
+ let linked = false;
369
+ try {
370
+ fs.rmSync(path.join(root, 'docs/current/skipped.md'));
371
+ fs.symlinkSync(external, path.join(root, 'docs/current/skipped.md'));
372
+ linked = true;
373
+ } catch (error) {
374
+ if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'ENOSYS'].includes(error.code)) throw error;
375
+ }
376
+ try {
377
+ if (linked) {
378
+ const result = report(runCheck(root, git(root, ['rev-parse', 'HEAD']), ['docs/current/a.md'], ['--conflict-assessment']));
379
+ assert.equal(result.conflictAssessment.state, 'unavailable');
380
+ assert.equal(result.conflictAssessment.reason, 'corpus-incomplete');
381
+ }
382
+ } finally {
383
+ fs.rmSync(external, { force: true });
384
+ }
385
+
386
+ const target = path.join(root, 'docs/current/a.md');
387
+ const originalRead = fs.readFileSync;
388
+ const coverage = {};
389
+ fs.readFileSync = function (file, ...args) {
390
+ if (typeof file === 'string' && path.resolve(file) === target) {
391
+ const error = new Error('fixture unreadable');
392
+ error.code = 'EACCES';
393
+ throw error;
394
+ }
395
+ return originalRead.call(this, file, ...args);
396
+ };
397
+ try {
398
+ LIFECYCLE.collectDocs(root, { governed: [{ path: 'docs/current/**', role: 'reference' }], legacy: [] }, coverage);
399
+ } finally { fs.readFileSync = originalRead; }
400
+ assert.equal(coverage.complete, false);
401
+ assert.ok(coverage.skipped.some(item => item.path === 'docs/current/a.md' && item.reason === 'unreadable-file'));
402
+ });
403
+
404
+ check('provider requires the assessment stdout schema as well as the argv mode', () => {
405
+ const scenario = seedPilotScenario();
406
+ const command = `node ${DOC_CHECK} check --workspace ${scenario.root} --config ${CONFIG} --changed-path docs/current/a.md --base ${scenario.base} --conflict-assessment`;
407
+ const capture = addHostCapture(scenario, {
408
+ cwd: scenario.root, session_id: scenario.sessionId, tool_name: 'Bash', tool_use_id: 'forged-mode',
409
+ tool_input: { command }, tool_response: { interrupted: false, stdout: JSON.stringify({
410
+ schema: 'forged', mode: 'conflict-assessment', lint: { exit: 0 },
411
+ capabilities: { 'conflict-check': { state: 'pass', checkedPaths: ['docs/current/a.md'] } },
412
+ }) },
413
+ });
414
+ const response = testOrigin(scenario).resolveEvent({
415
+ reference: `${capture.ref}#conflict-check`, identity: providerIdentity(scenario),
416
+ });
417
+ assert.equal(response.status, 'rejected');
418
+ assert.ok(response.diagnostics.some(item => item.code === 'check-not-passed'));
419
+ });
420
+
421
+ check('snapshot adapter rejects wrong scope, corpus, config, and base bindings', () => {
422
+ const scenario = seedPilotScenario();
423
+ const { capture } = addAssessmentCapture(scenario);
424
+ const origin = testOrigin(scenario);
425
+ const snapshot = produceSnapshot(scenario, origin);
426
+ const reference = `${capture.ref}#conflict-check`;
427
+ const accept = mutate => EVENTS.acceptDocEvents({ snapshot, eventRefs: [reference] }, {
428
+ originProvider: {
429
+ resolveContext: request => origin.resolveContext(request),
430
+ resolveEvent: request => {
431
+ const response = origin.resolveEvent(request);
432
+ if (response.status !== 'accepted') return response;
433
+ const event = JSON.parse(JSON.stringify(response.event));
434
+ mutate(event);
435
+ return { ...response, event };
436
+ },
437
+ },
438
+ });
439
+
440
+ const good = EVENTS.acceptDocEvents({ snapshot, eventRefs: [reference] }, { originProvider: origin });
441
+ assert.equal(good.state, 'ready', JSON.stringify(good.rejected));
442
+ for (const [name, mutate, expected] of [
443
+ ['scope', event => { event.paths = ['docs/current/b.md']; }, 'scope-mismatch'],
444
+ ['corpus', event => { event.corpusBinding.paths.pop(); }, 'corpus-mismatch'],
445
+ ['config', event => { event.corpusBinding.config.digest = '0'.repeat(64); }, 'config-capture-mismatch'],
446
+ ['base', event => { event.corpusBinding.baseCommit = '0'.repeat(40); }, 'snapshot-changed'],
447
+ ]) {
448
+ const output = accept(mutate);
449
+ assert.equal(output.state, 'unresolved', `${name}: ${JSON.stringify(output.rejected)}`);
450
+ assert.ok(output.rejected.some(item => item.code === expected), `${name}: ${JSON.stringify(output.rejected)}`);
451
+ }
452
+ });
453
+
454
+ check('provider keeps replay, interruption, failure, and digest-pin controls', () => {
455
+ const scenario = seedPilotScenario();
456
+ const { capture } = addAssessmentCapture(scenario);
457
+ const identity = providerIdentity(scenario);
458
+ const duplicate = JSON.parse(JSON.stringify(capture));
459
+ duplicate.ref = 'replayed-capture';
460
+ const replay = testOrigin(scenario, [capture, duplicate]).resolveEvent({
461
+ reference: 'replayed-capture#conflict-check', identity,
462
+ });
463
+ assert.ok(replay.diagnostics.some(item => item.code === 'replayed-record'));
464
+
465
+ const interruptedCapture = { ...capture, interrupted: true };
466
+ const interrupted = testOrigin(scenario, [interruptedCapture]).resolveEvent({
467
+ reference: `${capture.ref}#conflict-check`, identity,
468
+ });
469
+ assert.ok(interrupted.diagnostics.some(item => item.code === 'interrupted-not-credited'));
470
+
471
+ addHostCapture(scenario, {
472
+ cwd: scenario.root, session_id: scenario.sessionId, tool_name: 'Bash', tool_use_id: capture.tool_use_id,
473
+ tool_input: { command: capture.command }, error: 'Exit code 1',
474
+ }, 'PostToolUseFailure');
475
+ const failedInvocation = testOrigin(scenario).resolveEvent({
476
+ reference: `${capture.ref}#conflict-check`, identity,
477
+ });
478
+ assert.ok(failedInvocation.diagnostics.some(item => item.code === 'failure-invalidates-mapping'));
479
+
480
+ const stalePinCapture = { ...capture, implementationDigest: '0'.repeat(64) };
481
+ const stalePin = testOrigin(scenario, [stalePinCapture]).resolveEvent({
482
+ reference: `${capture.ref}#conflict-check`, identity,
483
+ });
484
+ assert.ok(stalePin.diagnostics.some(item => item.code === 'implementation-digest-mismatch'));
485
+ });
486
+
487
+ check('a waiver without a successful complete assessment cannot PASS conflict-check', () => {
488
+ const scope = ['docs/current/a.md'];
489
+ const digest = 'a'.repeat(64);
490
+ const baselineDigest = 'b'.repeat(64);
491
+ const output = EVIDENCE.checkDocObligations({
492
+ obligations: [{ id: 'conflict-check', capability: 'conflict-check', strength: 'MUST', applicability: 'applicable', scope: { paths: scope } }],
493
+ artifacts: { [scope[0]]: { digest } }, baseline: { digest: baselineDigest, artifacts: {} }, executionRecords: [],
494
+ reviewRecords: [{ id: 'waiver-1', obligationId: 'conflict-check', kind: 'waiver', reason: 'assessment was complete',
495
+ citedRecordIds: ['missing-assessment'], paths: scope, artifactDigests: { [scope[0]]: digest }, baseDigest: baselineDigest }],
496
+ });
497
+ const result = output.results.find(item => item.obligationId === 'conflict-check');
498
+ assert.ok(result);
499
+ assert.notEqual(result.outcome, 'pass');
500
+ assert.ok(output.diagnostics.some(item => item.code === 'unsupported-waiver'));
501
+ });
502
+ } finally {
503
+ for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
504
+ }
505
+
506
+ console.log(`\nPR-A conflict-check summary: ${passed}/${passed + failed} passed, ${failed} failed`);
507
+ process.exitCode = failed ? 1 : 0;