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
@@ -34,6 +34,8 @@
34
34
  const crypto = require('crypto');
35
35
  const fs = require('fs');
36
36
  const path = require('path');
37
+ const { execFileSync } = require('child_process');
38
+ const { canonical, key, classifyShell } = require('./workflow-isolation');
37
39
 
38
40
  const HOST_ID = 'claude-code';
39
41
  const CAPTURE_RECORD_VERSION = 'claude-capture-record/1';
@@ -43,10 +45,29 @@ const OPENCODE_CAPTURE_FILE = 'opencode-captures.jsonl';
43
45
  const OPENCODE_CAPTURE_LIMIT_BYTES = 4 * 1024 * 1024;
44
46
  const QUALIFICATION_EXPORT_SCHEMA = 'harness.doc-reconciliation.qualification.v1';
45
47
  const HEX64 = /^[0-9a-f]{64}$/;
46
- const PRE_CAPTURE_TOOLS = ['Bash', 'PowerShell', 'Task', 'Agent'];
48
+ const PRE_CAPTURE_TOOLS = ['Bash', 'PowerShell', 'Task', 'Agent', 'Write', 'Edit', 'apply_patch'];
47
49
  const STDOUT_CAP_BYTES = 1024 * 1024;
48
50
  const DOC_CHECK_FLAGS = new Set(['--workspace', '--config', '--changed-path', '--base']);
51
+ const DOC_CHECK_BOOLEAN_FLAGS = new Set(['--conflict-assessment']);
49
52
  const DOC_CHECK_SCHEMA = 'harness.doc-check.check.v1';
53
+ const DOC_CHECK_CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
54
+ const OUTLINE_DISPATCH_SCHEMA = 'harness.outline-dispatch.v1';
55
+ const OUTLINE_SEAM_SCHEMA = 'harness.outline-dispatch-seam.v1';
56
+ const OUTLINE_HANDOFF_SCHEMA = 'harness.outline-handoff.v1';
57
+ const OUTLINE_LEASE_SCHEMA = 'harness.outline-writer-lease.v1';
58
+ const OUTLINE_INVENTORY_SOURCE = '/Users/daniel.chang/Desktop/Skills/Harness-everything/.worktrees/packets/adr-e1-2026-10-27/report-inv.md';
59
+ const OUTLINE_INVENTORY_SHA256 = '2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6';
60
+ const OUTLINE_INVENTORY_PIN = Object.freeze({
61
+ source: OUTLINE_INVENTORY_SOURCE,
62
+ sha256: OUTLINE_INVENTORY_SHA256,
63
+ runId: 'e1-inv-1',
64
+ inputSha: '0ef0541c6cae550e8269ba712e03797e0e987dfb',
65
+ approvalRef: { issue: 325, commentId: '6436207729', decision: 1 },
66
+ retainedArtifactPath: `inventory/${OUTLINE_INVENTORY_SHA256}.md`,
67
+ });
68
+ const OUTLINE_MARKER = 'HARNESS_OUTLINE_DISPATCH_V1';
69
+ const OUTLINE_TASK_ID = /^task-[A-Za-z0-9][A-Za-z0-9+._-]{0,127}$/;
70
+ const OUTLINE_ATTEMPT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
50
71
  const DOC_CHECK_SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh', 'fish']);
51
72
  const DOC_CHECK_LAUNCHERS = new Set(['node', 'nodejs', 'bun', 'deno', 'tsx', 'ts-node']);
52
73
  const DOC_CHECK_LAUNCHER_VALUE_FLAGS = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--inspect-port', '--input-type', '--conditions', '--env-file']);
@@ -65,7 +86,7 @@ function isUncPathPrefix(token, command, index) {
65
86
  return token === '' && command[index] === '\\' && command[index + 1] === '\\';
66
87
  }
67
88
  // Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
68
- const SHIPPED_DOC_CHECK_DIGEST = 'da5e51a763eda6680afb63ee781b566253df9d358eec1e65436786c9ecebbbdb';
89
+ const SHIPPED_DOC_CHECK_DIGEST = '1afd94d66b356c94206d2441529ec0b7fafa1bcb453b0f1e2cf6994b1e4858e5';
69
90
  // This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
70
91
  const DOC_CHECK_INSTALL_LAYOUTS = [
71
92
  ['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
@@ -357,10 +378,16 @@ function resolveDocCheckInvocation(command, cwd) {
357
378
 
358
379
  const values = new Map();
359
380
  const changedPaths = [];
360
- for (let index = 3; index < tokens.length; index += 2) {
361
- const flag = tokens[index];
362
- const value = tokens[index + 1];
381
+ let conflictAssessment = false;
382
+ for (let index = 3; index < tokens.length;) {
383
+ const flag = tokens[index++];
384
+ if (DOC_CHECK_BOOLEAN_FLAGS.has(flag)) {
385
+ if (conflictAssessment) return rejected('duplicate-flag');
386
+ conflictAssessment = true;
387
+ continue;
388
+ }
363
389
  if (!DOC_CHECK_FLAGS.has(flag)) return rejected('unknown-flag');
390
+ const value = tokens[index++];
364
391
  if (value === undefined || !value || value.startsWith('--')) return rejected('flag-value-missing');
365
392
  if (flag === '--changed-path') changedPaths.push(value);
366
393
  else {
@@ -371,6 +398,8 @@ function resolveDocCheckInvocation(command, cwd) {
371
398
  if (!values.has('--workspace') || !values.has('--config') || !changedPaths.length) {
372
399
  return rejected('required-flag-missing');
373
400
  }
401
+ if (conflictAssessment && !values.has('--base')) return rejected('conflict-assessment-base-required');
402
+ if (conflictAssessment && !/^[0-9a-f]{40,64}$/.test(values.get('--base'))) return rejected('conflict-assessment-base-unpinned');
374
403
 
375
404
  installed = installedDocCheck();
376
405
  if (!installed) return rejected('implementation-not-installed');
@@ -382,7 +411,8 @@ function resolveDocCheckInvocation(command, cwd) {
382
411
  if (fileDigest(registered.installed) !== SHIPPED_DOC_CHECK_DIGEST) return rejected('implementation-pin-mismatch', 'the installed doc-check is not the shipped script (canonical digest differs from SHIPPED_DOC_CHECK_DIGEST); no credit');
383
412
  }
384
413
  }
385
- return { kind: 'accepted', reason: null, installed: registered.installed, digest, argv };
414
+ return { kind: 'accepted', reason: null, installed: registered.installed, digest, argv,
415
+ conflictAssessment, base: values.get('--base') || null, changedPaths };
386
416
  }
387
417
 
388
418
  // Observation-time digests of the checked paths, read from the workspace when the check ran. Absent paths are omitted.
@@ -588,6 +618,828 @@ function evaluateQualification(input, expectedHost = HOST_ID) {
588
618
  return { open: true, source: 'validated-export' };
589
619
  }
590
620
 
621
+ function outlineSha(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
622
+
623
+ function parseOutlineDispatchPrompt(prompt) {
624
+ if (typeof prompt !== 'string') return null;
625
+ const occurrences = prompt.match(new RegExp(OUTLINE_MARKER, 'g')) || [];
626
+ if (occurrences.length !== 1) return null;
627
+ const line = prompt.split(/\r?\n/).find(item => item.includes(OUTLINE_MARKER));
628
+ const match = /^(HARNESS_OUTLINE_DISPATCH_V1) taskId=([^\s]+) attemptId=([^\s]+) token=([^\s]+) workerRoot=(.+)$/.exec(line || '');
629
+ if (!match || !OUTLINE_TASK_ID.test(match[2]) || !OUTLINE_ATTEMPT_ID.test(match[3])
630
+ || !textOf(match[4]) || !path.isAbsolute(match[5]) || match[5].trim() !== match[5]) return null;
631
+ const block = match[0];
632
+ const tokenDigest = outlineSha(match[4]);
633
+ const promptDigest = outlineSha(Buffer.from(prompt, 'utf8'));
634
+ return {
635
+ taskId: match[2], attemptId: match[3], token: match[4], tokenDigest,
636
+ workerRoot: match[5], blockDigest: outlineSha(Buffer.from(block, 'utf8')),
637
+ promptDigest,
638
+ dispatchInputDigest: outlineSha(captureJson({ taskId: match[2], attemptId: match[3], tokenDigest,
639
+ workerRoot: match[5], promptDigest })),
640
+ };
641
+ }
642
+
643
+ function parseInventoryPinRows(bytes) {
644
+ const text = Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes || '');
645
+ const identity = /run id `([^`]+)`[^\n]*input SHA `([a-f0-9]+)`/i.exec(text);
646
+ if (!identity) return { state: 'unresolved', reason: 'inventory-identity-missing' };
647
+ const lines = text.split(/\r?\n/);
648
+ const mechanismIndex = lines.findIndex(line => /Mechanism inventory/i.test(line));
649
+ const areaIndex = lines.findIndex(line => /Area grouping \+ candidate domain prefixes/i.test(line));
650
+ if (mechanismIndex < 0 || areaIndex < 0 || areaIndex <= mechanismIndex) return { state: 'unresolved', reason: 'inventory-tables-missing' };
651
+ const rows = new Map();
652
+ for (const line of lines.slice(mechanismIndex, areaIndex)) {
653
+ const match = /^\|\s*(\d{1,2})\s*\|/.exec(line);
654
+ if (!match) continue;
655
+ rows.set(match[1], (rows.get(match[1]) || 0) + 1);
656
+ }
657
+ const domains = new Map();
658
+ for (const line of lines.slice(areaIndex)) {
659
+ const cells = line.split('|').map(cell => cell.trim());
660
+ if (cells.length < 5 || !cells[1] || !cells[2] || !cells[3]) continue;
661
+ const prefix = /^`([A-Z][A-Z0-9]{1,9})`$/.exec(cells[3]);
662
+ if (!prefix) continue;
663
+ for (const item of cells[2].split(',')) {
664
+ const range = /^(\d{1,2})\s*[–-]\s*(\d{1,2})$/.exec(item.trim());
665
+ if (range) {
666
+ for (let id = Number(range[1]); id <= Number(range[2]); id += 1) {
667
+ const key = String(id).padStart(2, '0');
668
+ const entries = domains.get(key) || [];
669
+ entries.push(prefix[1]);
670
+ domains.set(key, entries);
671
+ }
672
+ } else if (/^\d{1,2}$/.test(item.trim())) {
673
+ const key = item.trim().padStart(2, '0');
674
+ const entries = domains.get(key) || [];
675
+ entries.push(prefix[1]);
676
+ domains.set(key, entries);
677
+ }
678
+ }
679
+ }
680
+ return { state: 'parsed', runId: identity[1], inputSha: identity[2], rows, domains };
681
+ }
682
+
683
+ function validateOutlineInventoryPin({ bytes, pin, expectedPin = OUTLINE_INVENTORY_PIN } = {}) {
684
+ if (!Buffer.isBuffer(bytes) || !recordOf(pin) || !recordOf(expectedPin)) return { state: 'unresolved', reason: 'inventory-unavailable' };
685
+ const fields = ['source', 'sha256', 'runId', 'inputSha', 'approvalRef', 'retainedArtifactPath'];
686
+ const actualPin = Object.fromEntries(fields.map(field => [field, pin[field]]));
687
+ const trustedPin = Object.fromEntries(fields.map(field => [field, expectedPin[field]]));
688
+ if (captureJson(actualPin) !== captureJson(trustedPin)) return { state: 'unresolved', reason: 'inventory-pin-mismatch' };
689
+ const digest = outlineSha(bytes);
690
+ if (digest !== expectedPin.sha256 || expectedPin.retainedArtifactPath !== `inventory/${expectedPin.sha256}.md`) {
691
+ return { state: 'unresolved', reason: 'inventory-digest-mismatch' };
692
+ }
693
+ const parsed = parseInventoryPinRows(bytes);
694
+ if (parsed.state !== 'parsed' || parsed.runId !== expectedPin.runId || parsed.inputSha !== expectedPin.inputSha) {
695
+ return { state: 'unresolved', reason: parsed.reason || 'inventory-revision-mismatch' };
696
+ }
697
+ return { state: 'accepted', pin: trustedPin, pinDigest: outlineSha(Buffer.from(captureJson(trustedPin))),
698
+ artifactDigest: digest, parsed };
699
+ }
700
+
701
+ function resolveInventoryRow(inventory, rowId) {
702
+ const parsed = inventory && inventory.parsed;
703
+ if (!parsed || parsed.state !== 'parsed' || !textOf(rowId)) return { state: 'unresolved', reason: 'inventory-row-missing' };
704
+ const matches = [...parsed.rows.entries()].filter(([id]) => id === rowId);
705
+ const domains = parsed.domains.get(rowId) || [];
706
+ if (matches.length !== 1 || matches[0][1] !== 1 || domains.length !== 1) {
707
+ return { state: 'unresolved', reason: 'inventory-row-ambiguous' };
708
+ }
709
+ return { state: 'accepted', row: { rowId, domain: domains[0] } };
710
+ }
711
+
712
+ function hasSubstantiveDecision(text) {
713
+ const lines = String(text || '').split(/\r?\n/);
714
+ let decisionLines = null;
715
+ const substantive = linesToCheck => linesToCheck.some(line => {
716
+ const value = line.trim().replace(/^[-*]\s*/, '');
717
+ return value && !/^<!--|^-->$|^\[[^\]]*\]$|^<[^>]+>$|^TBD$|^TODO$/i.test(value);
718
+ });
719
+ for (const line of lines) {
720
+ const heading = /^ {0,3}##\s+(.+?)\s*#*\s*$/.exec(line);
721
+ const headingText = heading?.[1]?.trim() || '';
722
+ if (/^Decision\b/i.test(headingText)) {
723
+ if (decisionLines && substantive(decisionLines)) return true;
724
+ decisionLines = [];
725
+ continue;
726
+ }
727
+ if (decisionLines && heading) {
728
+ if (substantive(decisionLines)) return true;
729
+ decisionLines = null;
730
+ continue;
731
+ }
732
+ if (decisionLines) decisionLines.push(line);
733
+ }
734
+ return Boolean(decisionLines && substantive(decisionLines));
735
+ }
736
+
737
+ function parseOutlineCitation(bytes, targetPath) {
738
+ const text = Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes || '');
739
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
740
+ if (!frontmatter) return { state: 'unresolved', reason: 'outline-frontmatter-missing' };
741
+ const field = name => new RegExp(`^${name}:\\s*([^\\r\\n]+)\\s*$`, 'm').exec(frontmatter[1])?.[1]?.trim() || '';
742
+ const id = field('id');
743
+ const status = field('status');
744
+ const rowMatches = [...text.matchAll(/^Inventory row:\s*([0-9]{1,2})\s*$/gmi)];
745
+ const targetMatches = [...text.matchAll(/^Target:\s*(\S+)\s*$/gmi)];
746
+ const domainMatches = [...text.matchAll(/^Domain:\s*([A-Z][A-Z0-9]{1,9})\s*$/gmi)];
747
+ if (!/^([A-Z][A-Z0-9]{1,9})-\d{3,}$/.test(id) || status !== 'proposed'
748
+ || rowMatches.length !== 1 || targetMatches.length !== 1 || domainMatches.length !== 1
749
+ || targetMatches[0][1] !== targetPath) return { state: 'unresolved', reason: 'outline-citation-invalid' };
750
+ if (hasSubstantiveDecision(text)) return { state: 'unresolved', reason: 'outline-already-draft' };
751
+ return { state: 'accepted', id, status, rowId: rowMatches[0][1], targetPath,
752
+ domain: domainMatches[0][1], bytesDigest: outlineSha(Buffer.from(text, 'utf8')) };
753
+ }
754
+
755
+ function outlineGit(root, args, binary = false) {
756
+ try {
757
+ const env = { ...process.env };
758
+ for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
759
+ return execFileSync('git', ['-C', root, ...args], { env, encoding: binary ? null : 'utf8',
760
+ timeout: 10000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
761
+ } catch (error) {
762
+ throw new Error('outline-git-unavailable', { cause: error });
763
+ }
764
+ }
765
+
766
+ function canonicalOutlineRoot(value) {
767
+ if (typeof value !== 'string' || !path.isAbsolute(value)) throw new Error('outline-root-invalid');
768
+ try { fs.statSync(value); } catch { throw new Error('outline-root-not-canonical'); }
769
+ return canonical(value);
770
+ }
771
+
772
+ function outlineWorkspaceId(root) { return 'ws-' + digestText(captureJson({ workspaceRoot: canonical(root) })); }
773
+
774
+ function readOutlineJson(file) {
775
+ try {
776
+ const bytes = fs.readFileSync(file);
777
+ const value = JSON.parse(bytes.toString('utf8'));
778
+ if (!recordOf(value)) return { state: 'unresolved', reason: 'outline-record-invalid' };
779
+ return { state: 'accepted', value, bytes, digest: outlineSha(bytes) };
780
+ } catch {
781
+ return { state: 'unresolved', reason: 'outline-record-unavailable' };
782
+ }
783
+ }
784
+
785
+ function outlineTaskState(sessionDir) {
786
+ const workflowFile = path.join(sessionDir, 'workflow-run.json');
787
+ const loaded = readOutlineJson(workflowFile);
788
+ if (loaded.state !== 'accepted') return { state: 'unresolved', reason: 'workflow-pin-unavailable' };
789
+ const workflow = loaded.value;
790
+ const taskBinding = workflow.docReconciliationTask;
791
+ const baseline = workflow.docReconciliationBaseline;
792
+ if (!recordOf(taskBinding) || !OUTLINE_TASK_ID.test(String(taskBinding.taskId || ''))
793
+ || !Array.isArray(taskBinding.requirementIds) || !taskBinding.requirementIds.length
794
+ || !HEX64.test(String(taskBinding.requirementDigest || ''))
795
+ || !recordOf(baseline) || !textOf(baseline.root) || !/^[0-9a-f]{40,64}$/.test(String(baseline.commit || ''))
796
+ || outlineSha(Buffer.from(captureJson(taskBinding.requirementIds))) !== taskBinding.requirementDigest
797
+ || taskBinding.taskId !== `task-${taskBinding.requirementIds.join('+')}`
798
+ || taskBinding.baselineCommit !== baseline.commit || !sameCanonicalPath(taskBinding.baselineRoot, baseline.root)
799
+ || taskBinding.workflowId !== workflow.workflowId || taskBinding.workspaceId !== outlineWorkspaceId(baseline.root)) {
800
+ return { state: 'unresolved', reason: 'workflow-task-binding-invalid' };
801
+ }
802
+ return { state: 'accepted', workflow, taskBinding, baseline };
803
+ }
804
+
805
+ function resolveOutlineAttempt({ sessionDir, taskId, attemptId } = {}) {
806
+ if (!textOf(sessionDir) || !OUTLINE_TASK_ID.test(String(taskId || '')) || !OUTLINE_ATTEMPT_ID.test(String(attemptId || ''))) {
807
+ return { state: 'unresolved', reason: 'attempt-identity-invalid' };
808
+ }
809
+ const taskState = outlineTaskState(sessionDir);
810
+ if (taskState.state !== 'accepted' || taskState.taskBinding.taskId !== taskId) {
811
+ return { state: 'unresolved', reason: taskState.reason || 'task-binding-mismatch' };
812
+ }
813
+ const attemptsRoot = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId, 'attempts');
814
+ let names;
815
+ try { names = fs.readdirSync(attemptsRoot, { withFileTypes: true }).filter(item => item.isDirectory()).map(item => item.name).sort(compareKeys); }
816
+ catch { return { state: 'unresolved', reason: 'attempt-chain-unavailable' }; }
817
+ if (!names.length) return { state: 'unresolved', reason: 'attempt-chain-empty' };
818
+ const attempts = new Map();
819
+ for (const name of names) {
820
+ if (!OUTLINE_ATTEMPT_ID.test(name)) return { state: 'unresolved', reason: 'attempt-id-invalid' };
821
+ const loaded = readOutlineJson(path.join(attemptsRoot, name, 'handoff.json'));
822
+ if (loaded.state !== 'accepted') return { state: 'unresolved', reason: 'handoff-unavailable' };
823
+ const handoff = loaded.value;
824
+ if (handoff.schema !== OUTLINE_HANDOFF_SCHEMA || handoff.taskId !== taskId || handoff.attemptId !== name
825
+ || !textOf(handoff.handoffId) || !textOf(handoff.draftDispatchToken)
826
+ || !recordOf(handoff.baseline) || handoff.baseline.commit !== taskState.baseline.commit
827
+ || !sameCanonicalPath(handoff.baseline.root, taskState.baseline.root)
828
+ || handoff.configDigest !== taskState.taskBinding.configDigest) {
829
+ return { state: 'unresolved', reason: 'handoff-identity-or-baseline-mismatch' };
830
+ }
831
+ const attemptDir = path.join(attemptsRoot, name);
832
+ const seamDir = path.join(attemptDir, 'seams');
833
+ const tokenDir = path.join(attemptDir, 'tokens');
834
+ const filesIn = dir => {
835
+ try { return fs.readdirSync(dir, { withFileTypes: true })
836
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json')).map(entry => path.join(dir, entry.name)); }
837
+ catch { return []; }
838
+ };
839
+ const seamFiles = filesIn(seamDir);
840
+ const tokenFiles = filesIn(tokenDir);
841
+ const evidenceFiles = [...seamFiles, ...tokenFiles];
842
+ const sealLoaded = readOutlineJson(path.join(attemptDir, 'handoff-seal.json'));
843
+ const sealExists = fs.existsSync(path.join(attemptDir, 'handoff-seal.json'));
844
+ if (sealExists && (sealLoaded.state !== 'accepted' || sealLoaded.value.schema !== 'harness.outline-handoff-seal.v1'
845
+ || sealLoaded.value.taskId !== taskId || sealLoaded.value.attemptId !== name
846
+ || sealLoaded.value.handoffDigest !== loaded.digest)) {
847
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
848
+ }
849
+ if (evidenceFiles.length && !sealExists) {
850
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
851
+ }
852
+ for (const evidenceFile of evidenceFiles) {
853
+ const evidenceLoaded = readOutlineJson(evidenceFile);
854
+ if (evidenceLoaded.state !== 'accepted' || evidenceLoaded.value.taskId !== taskId
855
+ || evidenceLoaded.value.attemptId !== name || evidenceLoaded.value.handoffDigest !== loaded.digest) {
856
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
857
+ }
858
+ }
859
+ let interrupted = false;
860
+ if (sealExists && (tokenFiles.length !== 1 || seamFiles.length !== 1)) interrupted = true;
861
+ if (sealExists && tokenFiles.length === 1 && seamFiles.length === 1) {
862
+ const token = readOutlineJson(tokenFiles[0]).value;
863
+ const seam = readOutlineJson(seamFiles[0]).value;
864
+ if (token.schema !== 'harness.outline-token.v1' || seam.schema !== OUTLINE_SEAM_SCHEMA
865
+ || token.tokenDigest !== seam.tokenDigest || token.toolUseId !== seam.toolUseId
866
+ || token.dispatchInputDigest !== seam.dispatchInputDigest) {
867
+ return { state: 'unresolved', reason: 'handoff-dispatch-evidence-mismatch' };
868
+ }
869
+ }
870
+ attempts.set(name, { handoff, handoffDigest: loaded.digest, attemptDir, interrupted });
871
+ }
872
+ const parents = new Map();
873
+ const children = new Map(names.map(name => [name, 0]));
874
+ const tokens = new Set();
875
+ for (const [name, item] of attempts) {
876
+ const { handoff } = item;
877
+ if (tokens.has(handoff.draftDispatchToken)) return { state: 'unresolved', reason: 'attempt-token-reused' };
878
+ tokens.add(handoff.draftDispatchToken);
879
+ const parent = handoff.supersedesAttemptId;
880
+ if (parent === null || parent === undefined) { parents.set(name, null); continue; }
881
+ if (!attempts.has(parent) || parent === name || children.get(parent) === undefined) {
882
+ return { state: 'unresolved', reason: 'attempt-predecessor-missing' };
883
+ }
884
+ children.set(parent, children.get(parent) + 1);
885
+ if (children.get(parent) > 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
886
+ parents.set(name, parent);
887
+ }
888
+ const roots = [...parents.values()].filter(parent => parent === null).length;
889
+ if (roots !== 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
890
+ for (const name of names) {
891
+ const seen = new Set();
892
+ let current = name;
893
+ while (current !== null) {
894
+ if (seen.has(current)) return { state: 'unresolved', reason: 'attempt-chain-cyclic' };
895
+ seen.add(current);
896
+ current = parents.get(current);
897
+ if (current === undefined) return { state: 'unresolved', reason: 'attempt-predecessor-missing' };
898
+ }
899
+ }
900
+ const leaves = names.filter(name => children.get(name) === 0);
901
+ if (leaves.length !== 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
902
+ if (leaves[0] !== attemptId) return { state: 'unresolved', reason: 'attempt-superseded' };
903
+ const currentAttempt = attempts.get(attemptId);
904
+ if (currentAttempt.interrupted) return { state: 'unresolved', reason: 'attempt-interrupted' };
905
+ return { state: 'accepted', taskBinding: taskState.taskBinding, baseline: taskState.baseline,
906
+ handoff: currentAttempt.handoff, handoffDigest: currentAttempt.handoffDigest,
907
+ attemptDir: currentAttempt.attemptDir, predecessors: [...parents.keys()].filter(id => id !== attemptId) };
908
+ }
909
+
910
+ function outlineHandoffDirectory(sessionDir, taskId) {
911
+ return path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId);
912
+ }
913
+
914
+ function validateOutlineReview({ handoff, attemptDir, captures, targetPaths, outlineDigests, inventory, sessionId } = {}) {
915
+ const refs = Array.isArray(captures) ? captures : [];
916
+ const matches = refs.filter(item => recordOf(item) && item.ref === handoff.reviewCaptureRef && item.event === 'SubagentStop');
917
+ if (matches.length !== 1) return { state: 'unresolved', reason: 'outline-review-capture-missing-or-ambiguous' };
918
+ const review = matches[0];
919
+ const reviewer = review.agent_id;
920
+ if (!textOf(reviewer) || reviewer === sessionId || review.reviewVerdict !== 'approve'
921
+ || !textOf(review.reviewSummary) || !HEX64.test(String(review.reviewDigest || ''))
922
+ || review.reviewDigest !== handoff.reviewDigest) return { state: 'unresolved', reason: 'outline-review-invalid' };
923
+ const capturedDigest = captureDigest(review);
924
+ if (capturedDigest !== handoff.reviewCaptureDigest) return { state: 'unresolved', reason: 'outline-review-capture-digest-mismatch' };
925
+ const principals = { author: sessionId, executors: journalExecutors(refs, sessionId) };
926
+ if (!reviewRecordIndependent(review, refs, principals)) return { state: 'unresolved', reason: 'outline-review-not-independent' };
927
+ const inventoryPath = path.posix.join('doc-reconciliation/outline-confirmation/inventory', path.posix.basename(inventory.pin.retainedArtifactPath));
928
+ const expectedReviewed = { ...outlineDigests, [inventoryPath]: inventory.artifactDigest };
929
+ if (captureJson(review.reviewedArtifactDigests) !== captureJson(expectedReviewed)) {
930
+ return { state: 'unresolved', reason: 'outline-review-artifact-binding-mismatch' };
931
+ }
932
+ const summary = String(review.reviewSummary);
933
+ if (!summary.includes(`inventory row ${handoff.inventory.rowId}`) || !summary.includes(targetPaths[0])
934
+ || !summary.includes(handoff.inventory.domain)) return { state: 'unresolved', reason: 'outline-review-scope-mismatch' };
935
+ const result = refs.filter(item => recordOf(item) && item.event === 'PostToolUse'
936
+ && (item.tool_name === 'Agent' || item.tool_name === 'Task') && item.agent_id === reviewer);
937
+ if (result.length !== 1) return { state: 'unresolved', reason: 'outline-review-result-missing-or-ambiguous' };
938
+ return { state: 'accepted', review, reviewerPrincipal: reviewer, captureDigest: capturedDigest,
939
+ reviewDigest: review.reviewDigest, inventoryPath };
940
+ }
941
+
942
+ function validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspaceRoot, captures, expectedPin } = {}) {
943
+ const taskState = outlineTaskState(sessionDir);
944
+ if (taskState.state !== 'accepted' || parsed.taskId !== taskState.taskBinding.taskId) {
945
+ return { state: 'unresolved', reason: taskState.reason || 'task-binding-mismatch' };
946
+ }
947
+ const attempt = resolveOutlineAttempt({ sessionDir, taskId: parsed.taskId, attemptId: parsed.attemptId });
948
+ if (attempt.state !== 'accepted') return attempt;
949
+ const { handoff, handoffDigest, attemptDir, baseline, taskBinding } = attempt;
950
+ const workflow = taskState.workflow;
951
+ if (handoff.schema !== OUTLINE_HANDOFF_SCHEMA || handoff.workspaceId !== outlineWorkspaceId(baseline.root)
952
+ || handoff.workflowId !== workflow.workflowId || handoff.workflowRevision !== workflow.revision
953
+ || handoff.sessionId !== payload.session_id || handoff.hostId !== HOST_ID
954
+ || handoff.coordinatorPrincipal !== payload.session_id
955
+ || handoff.draftDispatchToken !== parsed.token
956
+ || !sameCanonicalPath(handoff.baseline.root, baseline.root) || handoff.baseline.commit !== baseline.commit
957
+ || handoff.baseline.tree !== baseline.tree || handoff.configDigest !== taskBinding.configDigest) {
958
+ return { state: 'unresolved', reason: 'handoff-identity-mismatch' };
959
+ }
960
+ const markerRoot = canonicalOutlineRoot(parsed.workerRoot);
961
+ const declaredRoot = canonicalOutlineRoot(handoff.draftWorktreeRoot);
962
+ if (!sameCanonicalPath(markerRoot, declaredRoot)) return { state: 'unresolved', reason: 'worker-root-mismatch' };
963
+ const gitRoot = outlineGit(markerRoot, ['rev-parse', '--show-toplevel']).trim();
964
+ if (!sameCanonicalPath(canonicalOutlineRoot(gitRoot), markerRoot)) return { state: 'unresolved', reason: 'worker-root-mismatch' };
965
+ const head = outlineGit(markerRoot, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
966
+ const tree = outlineGit(markerRoot, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
967
+ const statusBytes = outlineGit(markerRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all'], true);
968
+ const statusDigest = outlineSha(statusBytes);
969
+ if (head !== handoff.outline.commit || tree !== handoff.outline.tree || statusBytes.length !== 0) {
970
+ return { state: 'unresolved', reason: statusBytes.length ? 'worker-worktree-not-clean-at-O' : 'worker-worktree-not-at-O' };
971
+ }
972
+ if (outlineGit(markerRoot, ['rev-parse', '--verify', `${baseline.commit}^{commit}`]).trim() !== baseline.commit
973
+ || outlineGit(markerRoot, ['rev-parse', '--verify', `${handoff.outline.commit}^{tree}`]).trim() !== handoff.outline.tree
974
+ || outlineGit(markerRoot, ['merge-base', '--is-ancestor', baseline.commit, handoff.outline.commit]) !== '') {
975
+ return { state: 'unresolved', reason: 'B-to-O-git-binding-invalid' };
976
+ }
977
+ const targetPaths = Array.isArray(handoff.targetPaths) ? [...handoff.targetPaths] : [];
978
+ if (!targetPaths.length || targetPaths.some(item => typeof item !== 'string' || path.isAbsolute(item)
979
+ || item.includes('\\') || item.split('/').some(part => !part || part === '.' || part === '..'))
980
+ || captureJson([...new Set(targetPaths)].sort(compareKeys)) !== captureJson(targetPaths)) {
981
+ return { state: 'unresolved', reason: 'outline-target-scope-invalid' };
982
+ }
983
+ const pinDir = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', 'inventory');
984
+ const trustedPin = expectedPin || OUTLINE_INVENTORY_PIN;
985
+ const pinFile = path.join(pinDir, 'inventory-pin.json');
986
+ const pinLoaded = readOutlineJson(pinFile);
987
+ if (pinLoaded.state !== 'accepted') return { state: 'unresolved', reason: 'inventory-pin-unavailable' };
988
+ const inventoryFile = path.join(pinDir, trustedPin.retainedArtifactPath.split('/').pop());
989
+ let inventoryBytes;
990
+ try { inventoryBytes = fs.readFileSync(inventoryFile); }
991
+ catch { return { state: 'unresolved', reason: 'inventory-artifact-unavailable' }; }
992
+ const inventory = validateOutlineInventoryPin({ bytes: inventoryBytes, pin: pinLoaded.value, expectedPin: trustedPin });
993
+ if (inventory.state !== 'accepted') return inventory;
994
+ if (outlineSha(pinLoaded.bytes) !== handoff.inventoryPinDigest && handoff.inventoryPinDigest !== undefined) {
995
+ return { state: 'unresolved', reason: 'inventory-pin-digest-mismatch' };
996
+ }
997
+ const outlineBlobs = recordOf(handoff.outline?.targetBlobs) ? handoff.outline.targetBlobs : {};
998
+ const outlineDigests = {};
999
+ let citedRow = null;
1000
+ for (const target of targetPaths) {
1001
+ const atBaseline = outlineGit(baseline.root, ['ls-tree', '-r', '--name-only', baseline.commit, '--', target]).trim();
1002
+ if (atBaseline.split('\n').filter(Boolean).includes(target)) return { state: 'unresolved', reason: 'outline-target-existed-at-B' };
1003
+ const bytes = outlineGit(markerRoot, ['show', `${handoff.outline.commit}:${target}`], true);
1004
+ const blobOid = outlineGit(markerRoot, ['rev-parse', `${handoff.outline.commit}:${target}`]).trim();
1005
+ const expected = outlineBlobs[target];
1006
+ if (!recordOf(expected) || expected.blobOid !== blobOid || expected.storedBytesSha256 !== outlineSha(bytes)) {
1007
+ return { state: 'unresolved', reason: 'outline-object-binding-mismatch' };
1008
+ }
1009
+ const citation = parseOutlineCitation(bytes, target);
1010
+ if (citation.state !== 'accepted') return citation;
1011
+ const row = resolveInventoryRow(inventory, citation.rowId);
1012
+ if (row.state !== 'accepted' || citation.domain !== row.row.domain || citation.domain !== handoff.inventory.domain
1013
+ || handoff.inventory.rowId !== citation.rowId || !Array.isArray(handoff.inventory.targetPaths)
1014
+ || captureJson(handoff.inventory.targetPaths) !== captureJson(targetPaths)) {
1015
+ return { state: 'unresolved', reason: row.reason || 'outline-inventory-binding-mismatch' };
1016
+ }
1017
+ const expectedId = path.posix.basename(target, path.posix.extname(target));
1018
+ if (citation.id !== expectedId || !citation.id.startsWith(`${row.row.domain}-`)) {
1019
+ return { state: 'unresolved', reason: 'outline-domain-target-mismatch' };
1020
+ }
1021
+ if (citedRow && captureJson(citedRow) !== captureJson(row.row)) return { state: 'unresolved', reason: 'outline-row-ambiguous' };
1022
+ citedRow = row.row;
1023
+ outlineDigests[target] = outlineSha(bytes);
1024
+ }
1025
+ const leaseFile = path.join(attemptDir, 'writer-lease.json');
1026
+ const leaseLoaded = readOutlineJson(leaseFile);
1027
+ if (leaseLoaded.state !== 'accepted') return { state: 'unresolved', reason: 'writer-lease-unavailable' };
1028
+ const lease = leaseLoaded.value;
1029
+ if (lease.schema !== OUTLINE_LEASE_SCHEMA || lease.id !== handoff.writerLeaseId
1030
+ || lease.principalId !== payload.session_id || !sameCanonicalPath(lease.root, markerRoot) || lease.exclusive !== true
1031
+ || lease.active !== true || (Array.isArray(lease.competingWriters) && lease.competingWriters.length)
1032
+ || leaseLoaded.digest !== handoff.writerLeaseDigest) {
1033
+ return { state: 'unresolved', reason: 'writer-lease-invalid' };
1034
+ }
1035
+ const review = validateOutlineReview({ handoff, attemptDir, captures, targetPaths, outlineDigests, inventory,
1036
+ sessionId: payload.session_id });
1037
+ if (review.state !== 'accepted') return review;
1038
+ return { state: 'accepted', taskBinding, baseline, handoff, handoffDigest, attemptDir, markerRoot,
1039
+ worktree: { root: markerRoot, head, tree, statusDigest, clean: true },
1040
+ inventory, inventoryPinDigest: outlineSha(pinLoaded.bytes), row: citedRow, targetPaths, outlineDigests, review,
1041
+ leaseDigest: leaseLoaded.digest, leaseId: lease.id };
1042
+ }
1043
+
1044
+ function outlineDispatchUnresolved(parsed, reason) {
1045
+ return { schema: OUTLINE_DISPATCH_SCHEMA, state: 'unresolved', reasons: [reason],
1046
+ ...(parsed ? { taskId: parsed.taskId, attemptId: parsed.attemptId, tokenDigest: parsed.tokenDigest,
1047
+ promptDigest: parsed.promptDigest, blockDigest: parsed.blockDigest, dispatchInputDigest: parsed.dispatchInputDigest,
1048
+ workerRoot: parsed.workerRoot } : {}) };
1049
+ }
1050
+
1051
+ function captureOutlineDispatch(input = {}) {
1052
+ let parsed = null;
1053
+ try {
1054
+ const { payload, capture, sessionDir, workspaceRoot, captures = [], fixtureExpectedPin } = input;
1055
+ if (!recordOf(payload) || !recordOf(capture) || payload.hook_event_name !== 'PreToolUse'
1056
+ || !['Agent', 'Task'].includes(payload.tool_name) || !recordOf(payload.tool_input)) {
1057
+ return outlineDispatchUnresolved(null, 'dispatch-event-invalid');
1058
+ }
1059
+ parsed = parseOutlineDispatchPrompt(payload.tool_input.prompt);
1060
+ if (!parsed) return outlineDispatchUnresolved(null, 'dispatch-marker-invalid');
1061
+ if (capture.tool_use_id !== payload.tool_use_id || !textOf(payload.tool_use_id)
1062
+ || capture.session_id !== payload.session_id || !textOf(payload.session_id)) {
1063
+ return outlineDispatchUnresolved(parsed, 'dispatch-correlation-missing');
1064
+ }
1065
+ const failedPredecessor = (Array.isArray(captures) ? captures : []).some(item => recordOf(item)
1066
+ && item.event === 'PreToolUse' && item.session_id === payload.session_id
1067
+ && item.outlineDispatch?.state === 'unresolved'
1068
+ && (item.tool_use_id === payload.tool_use_id
1069
+ || (item.outlineDispatch.taskId === parsed.taskId && item.outlineDispatch.attemptId === parsed.attemptId
1070
+ && item.outlineDispatch.tokenDigest === parsed.tokenDigest)));
1071
+ if (failedPredecessor) return outlineDispatchUnresolved(parsed, 'attempt-already-unresolved');
1072
+ const validated = validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspaceRoot,
1073
+ captures, expectedPin: fixtureExpectedPin });
1074
+ if (validated.state !== 'accepted') return outlineDispatchUnresolved(parsed, validated.reason || 'dispatch-validation-unresolved');
1075
+ const seamDir = path.join(validated.attemptDir, 'seams');
1076
+ const tokenDir = path.join(validated.attemptDir, 'tokens');
1077
+ const seamName = `${outlineSha(String(payload.tool_use_id))}.json`;
1078
+ const seamFile = path.join(seamDir, seamName);
1079
+ const tokenFile = path.join(tokenDir, `${parsed.tokenDigest}.json`);
1080
+ const priorSeam = readOutlineJson(seamFile);
1081
+ if (priorSeam.state === 'accepted') {
1082
+ const prior = priorSeam.value;
1083
+ if (prior.toolUseId === payload.tool_use_id && prior.dispatchInputDigest === parsed.dispatchInputDigest
1084
+ && prior.taskId === parsed.taskId && prior.attemptId === parsed.attemptId) {
1085
+ return { ...prior, schema: OUTLINE_DISPATCH_SCHEMA, duplicate: true };
1086
+ }
1087
+ return outlineDispatchUnresolved(parsed, 'dispatch-input-changed');
1088
+ }
1089
+ const priorToken = readOutlineJson(tokenFile);
1090
+ if (priorToken.state === 'accepted') {
1091
+ const token = priorToken.value;
1092
+ if (token.toolUseId !== payload.tool_use_id) return outlineDispatchUnresolved(parsed, 'draft-token-reused');
1093
+ return outlineDispatchUnresolved(parsed, 'seam-missing-after-token-consumption');
1094
+ }
1095
+ const seam = {
1096
+ schema: OUTLINE_SEAM_SCHEMA, state: 'accepted', captureRef: capture.ref, toolUseId: payload.tool_use_id,
1097
+ taskId: parsed.taskId, attemptId: parsed.attemptId, workerRoot: validated.markerRoot,
1098
+ tokenDigest: parsed.tokenDigest, promptDigest: parsed.promptDigest, blockDigest: parsed.blockDigest,
1099
+ dispatchInputDigest: parsed.dispatchInputDigest, handoffDigest: validated.handoffDigest,
1100
+ reviewCaptureRef: validated.review.review.ref, reviewCaptureDigest: validated.review.captureDigest,
1101
+ reviewDigest: validated.review.reviewDigest, inventoryPinDigest: validated.inventoryPinDigest,
1102
+ writerLeaseDigest: validated.leaseDigest, writerLeaseId: validated.leaseId,
1103
+ coordinatorPrincipal: payload.session_id, reviewerPrincipal: validated.review.reviewerPrincipal,
1104
+ worktree: validated.worktree, validationReasons: [],
1105
+ };
1106
+ const sealFile = path.join(validated.attemptDir, 'handoff-seal.json');
1107
+ const existingSeal = readOutlineJson(sealFile);
1108
+ if (existingSeal.state === 'accepted') {
1109
+ if (existingSeal.value.taskId !== parsed.taskId || existingSeal.value.attemptId !== parsed.attemptId
1110
+ || existingSeal.value.handoffDigest !== validated.handoffDigest) {
1111
+ return outlineDispatchUnresolved(parsed, 'handoff-overwritten-after-dispatch');
1112
+ }
1113
+ return outlineDispatchUnresolved(parsed, 'handoff-seal-already-consumed');
1114
+ }
1115
+ const seal = { schema: 'harness.outline-handoff-seal.v1', taskId: parsed.taskId,
1116
+ attemptId: parsed.attemptId, handoffDigest: validated.handoffDigest };
1117
+ try { fs.writeFileSync(sealFile, JSON.stringify(seal, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' }); }
1118
+ catch { return outlineDispatchUnresolved(parsed, 'handoff-seal-persistence-failed'); }
1119
+ fs.mkdirSync(tokenDir, { recursive: true });
1120
+ fs.mkdirSync(seamDir, { recursive: true });
1121
+ const token = { schema: 'harness.outline-token.v1', taskId: parsed.taskId, attemptId: parsed.attemptId,
1122
+ tokenDigest: parsed.tokenDigest, toolUseId: payload.tool_use_id, dispatchInputDigest: parsed.dispatchInputDigest,
1123
+ handoffDigest: validated.handoffDigest };
1124
+ try {
1125
+ fs.writeFileSync(tokenFile, JSON.stringify(token, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
1126
+ } catch (error) {
1127
+ if (error.code === 'EEXIST') return outlineDispatchUnresolved(parsed, 'draft-token-reused');
1128
+ return outlineDispatchUnresolved(parsed, 'token-consumption-failed');
1129
+ }
1130
+ try {
1131
+ fs.writeFileSync(seamFile, JSON.stringify(seam, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
1132
+ } catch {
1133
+ return outlineDispatchUnresolved(parsed, 'seam-persistence-failed');
1134
+ }
1135
+ return { ...seam, schema: OUTLINE_DISPATCH_SCHEMA,
1136
+ seamRef: path.relative(sessionDir, seamFile).split(path.sep).join('/') };
1137
+ } catch (error) {
1138
+ return outlineDispatchUnresolved(parsed, error.message === 'outline-git-unavailable' ? error.message : 'dispatch-validation-failed');
1139
+ }
1140
+ }
1141
+
1142
+ function validateOutlineCaptureQualification(input, expectedHost = HOST_ID) {
1143
+ if (!recordOf(input) || input.hostId !== expectedHost || expectedHost !== HOST_ID) return { open: false, reason: 'unsupported-host' };
1144
+ const supplied = input.qualification;
1145
+ if (recordOf(supplied) && supplied.faked === true && supplied.source === 'fake-test-fixture'
1146
+ && supplied.toolUseIdPopulated === true && supplied.agentIdPopulated === true) {
1147
+ return supplied.outlineDispatchCaptureVerified === true
1148
+ ? { open: true, source: 'dependency-injection' }
1149
+ : { open: false, reason: 'outline-live-capture-unqualified' };
1150
+ }
1151
+ try {
1152
+ const qualificationError = validateQualificationExport(expectedHost);
1153
+ if (qualificationError) return { open: false, reason: qualificationError };
1154
+ const exported = JSON.parse(fs.readFileSync(path.join(__dirname, EXPORT_FILE), 'utf8'));
1155
+ const mapping = exported.outlineDispatchCapture;
1156
+ const source = exported.sanitizedCapture;
1157
+ const records = fs.readFileSync(path.join(__dirname, source.file), 'utf8').split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
1158
+ if (!outlineCaptureMappingValid(mapping, records)) return { open: false, reason: 'outline-live-capture-unqualified' };
1159
+ return { open: true, source: 'validated-export', captureFile: source.file };
1160
+ } catch (error) {
1161
+ return { open: false, reason: `outline-live-capture-unqualified:${error.code || 'invalid'}` };
1162
+ }
1163
+ }
1164
+
1165
+ function outlineCaptureMappingValid(mapping, records) {
1166
+ if (!recordOf(mapping) || mapping.promptField !== 'tool_input.prompt'
1167
+ || mapping.coordinatorPrincipalField !== 'session_id' || mapping.synchronousPreToolUse !== true
1168
+ || mapping.workerStartDispatchField !== 'parent_tool_use_id' || mapping.workerStartRootField !== 'cwd'
1169
+ || mapping.taskResultDispatchField !== 'tool_use_id' || !Array.isArray(records)) return false;
1170
+ const pres = records.filter(item => recordOf(item) && item.event === 'PreToolUse'
1171
+ && ['Task', 'Agent'].includes(item.tool_name) && recordOf(item.tool_input)
1172
+ && typeof item.tool_input.prompt === 'string' && textOf(item.session_id) && textOf(item.tool_use_id));
1173
+ return pres.some(pre => {
1174
+ const marker = parseOutlineDispatchPrompt(pre.tool_input.prompt);
1175
+ if (!marker) return false;
1176
+ const starts = records.filter(item => recordOf(item) && item.event === 'SubagentStart'
1177
+ && item.parent_tool_use_id === pre.tool_use_id && item.session_id === pre.session_id
1178
+ && textOf(item.cwd) && textOf(marker.workerRoot) && key(item.cwd) === key(marker.workerRoot)
1179
+ && textOf(item.agent_id));
1180
+ if (starts.length !== 1) return false;
1181
+ const worker = starts[0];
1182
+ const results = records.filter(item => recordOf(item) && item.event === 'PostToolUse'
1183
+ && ['Task', 'Agent'].includes(item.tool_name) && item.tool_use_id === pre.tool_use_id
1184
+ && item.session_id === pre.session_id && item.agent_id === worker.agent_id
1185
+ && item.interrupted === false);
1186
+ return results.length === 1;
1187
+ });
1188
+ }
1189
+
1190
+ function verifyOutlineMergeCorrespondence({ root, outlineCommit, draftCommit, mergedCommit, targetPaths, record } = {}) {
1191
+ if (!recordOf(record) || !textOf(root) || !Array.isArray(targetPaths) || !targetPaths.length
1192
+ || record.outlineCommit !== outlineCommit || record.draftCommit !== draftCommit || record.mergedCommit !== mergedCommit) {
1193
+ return { state: 'unresolved', reason: 'merge-correspondence-missing-or-mismatched' };
1194
+ }
1195
+ try {
1196
+ for (const commit of [outlineCommit, draftCommit, mergedCommit]) {
1197
+ if (outlineGit(root, ['rev-parse', '--verify', `${commit}^{commit}`]).trim() !== commit) {
1198
+ return { state: 'unresolved', reason: 'merge-object-missing' };
1199
+ }
1200
+ }
1201
+ if (outlineCommit === draftCommit || outlineGit(root, ['merge-base', '--is-ancestor', outlineCommit, draftCommit]) !== '') {
1202
+ return { state: 'unresolved', reason: 'merge-pre-draft-ancestry-invalid' };
1203
+ }
1204
+ const recorded = record.targetBlobs;
1205
+ if (!recordOf(recorded) || captureJson(Object.keys(recorded).sort(compareKeys)) !== captureJson([...targetPaths].sort(compareKeys))) {
1206
+ return { state: 'unresolved', reason: 'merge-target-correspondence-invalid' };
1207
+ }
1208
+ let changed = false;
1209
+ for (const target of targetPaths) {
1210
+ const draft = outlineGit(root, ['rev-parse', `${draftCommit}:${target}`]).trim();
1211
+ const merged = outlineGit(root, ['rev-parse', `${mergedCommit}:${target}`]).trim();
1212
+ if (recorded[target]?.draft !== draft || recorded[target]?.merged !== merged) {
1213
+ return { state: 'unresolved', reason: 'merge-target-correspondence-invalid' };
1214
+ }
1215
+ changed ||= draft !== merged;
1216
+ }
1217
+ return { state: changed ? 'changed' : 'accepted', draftCommit, mergedCommit, targetPaths: [...targetPaths], targetBlobs: recorded };
1218
+ } catch (error) {
1219
+ return { state: 'unresolved', reason: error.message || 'merge-object-missing' };
1220
+ }
1221
+ }
1222
+
1223
+ function outlineAncestry(root, ancestor, descendant) {
1224
+ try {
1225
+ outlineGit(root, ['merge-base', '--is-ancestor', ancestor, descendant]);
1226
+ return true;
1227
+ } catch { return false; }
1228
+ }
1229
+
1230
+ function possibleWriterCapture(item) {
1231
+ if (!recordOf(item)) return false;
1232
+ if (['Write', 'Edit', 'apply_patch'].includes(item.tool_name)) return true;
1233
+ if (!['Bash', 'PowerShell', 'exec_command'].includes(item.tool_name)) return false;
1234
+ return !['read-only', 'worktree-setup'].includes(classifyShell(item.command));
1235
+ }
1236
+
1237
+ function sameCanonicalPath(left, right) {
1238
+ try {
1239
+ fs.statSync(left);
1240
+ fs.statSync(right);
1241
+ return key(left) === key(right);
1242
+ }
1243
+ catch { return false; }
1244
+ }
1245
+
1246
+ function samePathKey(left, right) {
1247
+ try { return key(left) === key(right); }
1248
+ catch { return false; }
1249
+ }
1250
+
1251
+ function sameOutlineTaskBinding(left, right) {
1252
+ if (!recordOf(left) || !recordOf(right)) return false;
1253
+ const { baselineRoot: leftRoot, ...leftIdentity } = left;
1254
+ const { baselineRoot: rightRoot, ...rightIdentity } = right;
1255
+ return captureJson(leftIdentity) === captureJson(rightIdentity) && sameCanonicalPath(leftRoot, rightRoot);
1256
+ }
1257
+
1258
+ function resolveOutlineEvidence({ safeInput, capture, captures, request, originRef } = {}) {
1259
+ const fail = (reason, status = 'rejected') => ({ status, diagnostics: [diagnostic(reason, { reason })] });
1260
+ try {
1261
+ const cap = capture;
1262
+ const sessionDir = safeInput.sessionDir;
1263
+ const dispatch = cap?.outlineDispatch;
1264
+ if (!recordOf(dispatch) || dispatch.schema !== OUTLINE_DISPATCH_SCHEMA || dispatch.state !== 'accepted'
1265
+ || !textOf(dispatch.seamRef) || dispatch.captureRef !== cap.ref || dispatch.toolUseId !== cap.tool_use_id) {
1266
+ return fail('outline-pre-dispatch-seam-unresolved');
1267
+ }
1268
+ if (cap.event !== 'PreToolUse' || !['Task', 'Agent'].includes(cap.tool_name)
1269
+ || cap.session_id !== safeInput.sessionId || cap.observedIdentity?.taskPin?.taskId !== safeInput.taskId) {
1270
+ return fail('outline-dispatch-identity-mismatch');
1271
+ }
1272
+ const taskState = outlineTaskState(sessionDir);
1273
+ if (taskState.state !== 'accepted' || taskState.taskBinding.taskId !== safeInput.taskId
1274
+ || (recordOf(safeInput.taskBinding) && !sameOutlineTaskBinding(taskState.taskBinding, safeInput.taskBinding))
1275
+ || !sameOutlineTaskBinding(cap.observedIdentity.taskPin, taskState.taskBinding)
1276
+ || cap.observedIdentity.baselinePin?.commit !== taskState.baseline.commit
1277
+ || !sameCanonicalPath(cap.observedIdentity.baselinePin?.root, taskState.baseline.root)) {
1278
+ return fail('outline-task-binding-mismatch');
1279
+ }
1280
+ const attempt = resolveOutlineAttempt({ sessionDir, taskId: dispatch.taskId, attemptId: dispatch.attemptId });
1281
+ if (attempt.state !== 'accepted') return fail(attempt.reason || 'outline-attempt-unresolved');
1282
+ if (attempt.handoffDigest !== dispatch.handoffDigest) return fail('outline-handoff-changed-after-dispatch');
1283
+ const seamRef = String(dispatch.seamRef).replace(/\\/g, '/');
1284
+ if (path.isAbsolute(seamRef) || seamRef.split('/').some(part => !part || part === '.' || part === '..')) {
1285
+ return fail('outline-seam-reference-invalid');
1286
+ }
1287
+ const seamFile = path.join(sessionDir, ...seamRef.split('/'));
1288
+ const seamLoaded = readOutlineJson(seamFile);
1289
+ if (seamLoaded.state !== 'accepted') return fail('outline-seam-missing');
1290
+ const seam = seamLoaded.value;
1291
+ if (seam.schema !== OUTLINE_SEAM_SCHEMA || seam.state !== 'accepted' || seam.captureRef !== cap.ref
1292
+ || seam.toolUseId !== cap.tool_use_id || seam.taskId !== dispatch.taskId || seam.attemptId !== dispatch.attemptId
1293
+ || seam.dispatchInputDigest !== dispatch.dispatchInputDigest || seam.handoffDigest !== attempt.handoffDigest
1294
+ || seam.tokenDigest !== dispatch.tokenDigest) return fail('outline-seam-mismatch');
1295
+ const sameAttempt = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1296
+ && item.event === 'PreToolUse' && item.outlineDispatch?.taskId === dispatch.taskId
1297
+ && item.outlineDispatch?.attemptId === dispatch.attemptId && item.outlineDispatch?.tokenDigest === dispatch.tokenDigest);
1298
+ for (const other of sameAttempt) {
1299
+ const evidence = other.outlineDispatch;
1300
+ if (evidence.state !== 'accepted' || evidence.dispatchInputDigest !== dispatch.dispatchInputDigest
1301
+ || other.tool_use_id !== cap.tool_use_id) return fail('outline-dispatch-replay-or-conflict');
1302
+ }
1303
+ const sameToolUse = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1304
+ && item.event === 'PreToolUse' && item.tool_use_id === cap.tool_use_id);
1305
+ if (sameToolUse.some(item => item.ref !== cap.ref && (item.outlineDispatch?.state !== 'accepted'
1306
+ || item.outlineDispatch.dispatchInputDigest !== dispatch.dispatchInputDigest))) return fail('outline-tool-use-id-replayed');
1307
+ const handoff = attempt.handoff;
1308
+ const targetPaths = Array.isArray(handoff.targetPaths) ? handoff.targetPaths : [];
1309
+ const expectedPin = recordOf(safeInput.qualification) && safeInput.qualification.faked === true
1310
+ && recordOf(safeInput.outlineFixture?.expectedPin) ? safeInput.outlineFixture.expectedPin : OUTLINE_INVENTORY_PIN;
1311
+ const inventoryRoot = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', 'inventory');
1312
+ const pinFile = path.join(inventoryRoot, 'inventory-pin.json');
1313
+ const pinLoaded = readOutlineJson(pinFile);
1314
+ if (pinLoaded.state !== 'accepted' || outlineSha(pinLoaded.bytes) !== seam.inventoryPinDigest) return fail('outline-inventory-pin-changed');
1315
+ let inventoryBytes;
1316
+ try { inventoryBytes = fs.readFileSync(path.join(inventoryRoot, expectedPin.retainedArtifactPath.split('/').pop())); }
1317
+ catch { return fail('outline-inventory-bytes-missing'); }
1318
+ const inventory = validateOutlineInventoryPin({ bytes: inventoryBytes, pin: pinLoaded.value, expectedPin });
1319
+ if (inventory.state !== 'accepted') return fail(inventory.reason || 'outline-inventory-unresolved');
1320
+ const outlineDigests = {};
1321
+ const reviewRows = {};
1322
+ for (const target of targetPaths) {
1323
+ const bytes = outlineGit(seam.worktree.root, ['show', `${handoff.outline.commit}:${target}`], true);
1324
+ outlineDigests[target] = outlineSha(bytes);
1325
+ const citation = parseOutlineCitation(bytes, target);
1326
+ if (citation.state !== 'accepted') return fail(citation.reason || 'outline-citation-invalid');
1327
+ const row = resolveInventoryRow(inventory, citation.rowId);
1328
+ if (row.state !== 'accepted' || row.row.domain !== citation.domain || citation.domain !== handoff.inventory?.domain
1329
+ || citation.rowId !== handoff.inventory?.rowId) return fail(row.reason || 'outline-inventory-binding-mismatch');
1330
+ reviewRows[target] = row.row;
1331
+ }
1332
+ const review = validateOutlineReview({ handoff, attemptDir: attempt.attemptDir, captures, targetPaths,
1333
+ outlineDigests, inventory, sessionId: safeInput.sessionId });
1334
+ if (review.state !== 'accepted' || review.captureDigest !== seam.reviewCaptureDigest
1335
+ || review.reviewDigest !== seam.reviewDigest || review.reviewerPrincipal !== seam.reviewerPrincipal) {
1336
+ return fail(review.reason || 'outline-review-changed-after-dispatch');
1337
+ }
1338
+ const startEvents = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1339
+ && item.event === 'SubagentStart' && item.parent_tool_use_id === cap.tool_use_id && item.session_id === cap.session_id);
1340
+ if (startEvents.length !== 1 || !textOf(startEvents[0].agent_id)) return fail('outline-worker-start-missing-or-ambiguous');
1341
+ const worker = startEvents[0];
1342
+ let workerRoot;
1343
+ try { workerRoot = canonicalOutlineRoot(worker.cwd); }
1344
+ catch { return fail('outline-worker-root-unobserved'); }
1345
+ if (!sameCanonicalPath(workerRoot, seam.workerRoot)) return fail('outline-worker-root-mismatch');
1346
+ const results = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1347
+ && item.event === 'PostToolUse' && ['Task', 'Agent'].includes(item.tool_name)
1348
+ && item.tool_use_id === cap.tool_use_id && item.session_id === cap.session_id);
1349
+ if (results.length !== 1 || results[0].agent_id !== worker.agent_id || results[0].interrupted !== false) {
1350
+ return fail('outline-worker-result-missing-or-mismatched');
1351
+ }
1352
+ if (review.reviewerPrincipal === worker.agent_id) return fail('outline-reviewer-is-draft-worker');
1353
+ const principals = { author: safeInput.sessionId, executors: journalExecutors(captures, safeInput.sessionId) };
1354
+ if (!reviewRecordIndependent(review.review, captures, principals)) return fail('outline-review-not-independent');
1355
+ const competingStarts = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1356
+ && item.event === 'SubagentStart' && item.ref !== worker.ref && sameCanonicalPath(item.cwd, workerRoot));
1357
+ if (competingStarts.length) return fail('outline-competing-writer-start');
1358
+ const unattributedWriters = (Array.isArray(captures) ? captures : []).filter(item => possibleWriterCapture(item)
1359
+ && (item.agent_id !== worker.agent_id || !sameCanonicalPath(item.cwd, workerRoot)));
1360
+ if (unattributedWriters.length) return fail('outline-writer-activity-ambiguous');
1361
+
1362
+ let candidateRoot;
1363
+ try { candidateRoot = canonicalOutlineRoot(safeInput.workspaceRoot); }
1364
+ catch { return fail('outline-candidate-root-unavailable'); }
1365
+ const baseline = taskState.baseline;
1366
+ const outlineCommit = handoff.outline.commit;
1367
+ let finalCommit;
1368
+ let finalTree;
1369
+ try {
1370
+ finalCommit = outlineGit(candidateRoot, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
1371
+ finalTree = outlineGit(candidateRoot, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
1372
+ outlineGit(candidateRoot, ['rev-parse', '--verify', `${outlineCommit}^{commit}`]);
1373
+ outlineGit(candidateRoot, ['rev-parse', '--verify', `${baseline.commit}^{commit}`]);
1374
+ } catch { return fail('outline-final-git-object-unavailable'); }
1375
+ if (finalCommit === outlineCommit) return fail('outline-final-draft-missing', 'unavailable');
1376
+ if (!outlineAncestry(candidateRoot, baseline.commit, outlineCommit)) return fail('outline-B-to-O-ancestry-invalid');
1377
+ let merge = { state: 'accepted', draftCommit: finalCommit, mergedCommit: finalCommit, targetPaths };
1378
+ if (!outlineAncestry(candidateRoot, outlineCommit, finalCommit)) {
1379
+ const mergeFile = path.join(attempt.attemptDir, 'merge-correspondence.json');
1380
+ const mergeLoaded = readOutlineJson(mergeFile);
1381
+ if (mergeLoaded.state !== 'accepted') return fail('outline-merge-correspondence-unavailable', 'unavailable');
1382
+ const correspondence = mergeLoaded.value;
1383
+ if (correspondence.taskId !== dispatch.taskId || correspondence.attemptId !== dispatch.attemptId
1384
+ || correspondence.baselineCommit !== baseline.commit) return fail('outline-merge-correspondence-mismatch');
1385
+ merge = verifyOutlineMergeCorrespondence({ root: candidateRoot, outlineCommit,
1386
+ draftCommit: correspondence.draftCommit, mergedCommit: finalCommit, targetPaths, record: correspondence });
1387
+ if (!['accepted', 'changed'].includes(merge.state)) return fail(merge.reason || 'outline-merge-correspondence-invalid', 'unavailable');
1388
+ }
1389
+ const draftArtifactDigests = {};
1390
+ const draftBlobOids = {};
1391
+ for (const target of targetPaths) {
1392
+ const atBaseline = outlineGit(candidateRoot, ['ls-tree', '-r', '--name-only', baseline.commit, '--', target]).trim();
1393
+ if (atBaseline.split('\n').filter(Boolean).includes(target)) return fail('outline-target-existed-at-B');
1394
+ let candidateBytes;
1395
+ let commitBytes;
1396
+ try {
1397
+ candidateBytes = fs.readFileSync(path.join(candidateRoot, ...target.split('/')));
1398
+ commitBytes = outlineGit(candidateRoot, ['show', `${finalCommit}:${target}`], true);
1399
+ } catch { return fail('outline-final-target-missing'); }
1400
+ if (!Buffer.from(candidateBytes).equals(commitBytes)) return fail('outline-final-working-tree-not-D');
1401
+ draftArtifactDigests[target] = outlineSha(commitBytes);
1402
+ draftBlobOids[target] = outlineGit(candidateRoot, ['rev-parse', `${finalCommit}:${target}`]).trim();
1403
+ }
1404
+ const independentOf = [...new Set([safeInput.sessionId, ...principals.executors])].filter(value => textOf(value) && value !== review.reviewerPrincipal);
1405
+ const event = {
1406
+ eventId: 'claude-outline-' + digestText(captureJson({ handoffDigest: attempt.handoffDigest,
1407
+ seamDigest: seamLoadedDigest(seamFile), draftCommit: finalCommit })),
1408
+ producerId: 'claude-code-origin-provider', originRef, principalId: review.reviewerPrincipal,
1409
+ provenance: `claude-code-outline-capture:${cap.captureId}`, capability: 'outline-confirmation', kind: 'review',
1410
+ verdict: 'approve', summary: review.review.reviewSummary, independentOf,
1411
+ identity: recordOf(request.identity) ? JSON.parse(JSON.stringify(request.identity)) : null,
1412
+ paths: targetPaths, reviewedArtifactDigests: { ...outlineDigests,
1413
+ [review.inventoryPath]: inventory.artifactDigest }, draftArtifactDigests,
1414
+ outlineEvidence: {
1415
+ schema: 'harness.outline-confirmation.v1', state: 'validated', taskId: dispatch.taskId,
1416
+ attemptId: dispatch.attemptId, inventoryRowId: handoff.inventory.rowId, domain: handoff.inventory.domain,
1417
+ inventoryPinDigest: seam.inventoryPinDigest, inventoryArtifactDigest: inventory.artifactDigest,
1418
+ baselineCommit: baseline.commit, outlineCommit, outlineTree: handoff.outline.tree,
1419
+ outlineBlobOids: Object.fromEntries(targetPaths.map(target => [target, handoff.outline.targetBlobs[target].blobOid])),
1420
+ outlineArtifactDigests: outlineDigests, draftCommit: merge.draftCommit, mergedCommit: finalCommit,
1421
+ draftTree: finalTree, draftBlobOids, draftArtifactDigests, handoffDigest: attempt.handoffDigest,
1422
+ reviewCaptureRef: review.review.ref, reviewCaptureDigest: review.captureDigest,
1423
+ reviewDigest: review.reviewDigest, seamDigest: seamLoadedDigest(seamFile), dispatchInputDigest: seam.dispatchInputDigest,
1424
+ dispatchToolUseId: cap.tool_use_id, coordinatorPrincipal: safeInput.sessionId,
1425
+ reviewerPrincipal: review.reviewerPrincipal, workerPrincipal: worker.agent_id,
1426
+ workerRoot, writerLeaseDigest: seam.writerLeaseDigest,
1427
+ ancestry: 'strict-descendant', mergeCorrespondence: merge.state === 'accepted' && merge.draftCommit === finalCommit ? null : merge,
1428
+ targetPaths,
1429
+ },
1430
+ captured: { captureRef: cap.ref, toolUseId: cap.tool_use_id, seamDigest: seamLoadedDigest(seamFile) },
1431
+ };
1432
+ return { status: 'accepted', originRef, event,
1433
+ correlation: { toolCorrelationId: cap.tool_use_id, resultRef: cap.responseRef } };
1434
+ } catch (error) {
1435
+ return fail(error.message === 'outline-git-unavailable' ? 'outline-git-unavailable' : 'outline-evidence-unresolved', 'unavailable');
1436
+ }
1437
+ }
1438
+
1439
+ function seamLoadedDigest(file) {
1440
+ try { return outlineSha(fs.readFileSync(file)); } catch { return ''; }
1441
+ }
1442
+
591
1443
  // ---- Shared provider state ------------------------------------------------------
592
1444
 
593
1445
  function supportedHostCheck(input, expectedHost = HOST_ID) {
@@ -692,7 +1544,8 @@ const createClaudeOriginProvider = input => {
692
1544
  const observed = cap.observedIdentity;
693
1545
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
694
1546
  const sameWorkflow = observed.workflowId === safeInput.workflowId && observed.workflowRevision === safeInput.workflowRevision;
695
- const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit && observed.baselinePin.root === pin.root);
1547
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1548
+ && samePathKey(observed.baselinePin.root, pin.root));
696
1549
  if (!sameWorkflow || !samePin) {
697
1550
  return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
698
1551
  }
@@ -700,6 +1553,13 @@ const createClaudeOriginProvider = input => {
700
1553
  if (capability && !(recordOf(cap.observedIdentity))) {
701
1554
  return rejected('identity-missing', 'a doc capability needs the workflow identity observed at capture time');
702
1555
  }
1556
+ if (capability === 'outline-confirmation') {
1557
+ const outlineQualification = validateOutlineCaptureQualification(safeInput);
1558
+ if (!outlineQualification.open) {
1559
+ return unavailable('outline-live-gate-unmet', `positive outline credit is disabled until retained Claude prompt, coordinator, synchronous-boundary and worker-root capture is qualified (${outlineQualification.reason})`);
1560
+ }
1561
+ return resolveOutlineEvidence({ safeInput, capture: cap, captures, request, originRef });
1562
+ }
703
1563
  // Replay: identical host-event content re-presented under a new reference
704
1564
  // cannot become a new accepted event. The first recorded occurrence wins.
705
1565
  const signature = contentSignature(cap);
@@ -714,6 +1574,9 @@ const createClaudeOriginProvider = input => {
714
1574
  if (cap.event !== 'PostToolUse') return rejected('capability-mismatch', 'only a PostToolUse check invocation can cover a doc capability');
715
1575
  const registered = resolveDocCheckInvocation(cap.command, cap.cwd);
716
1576
  if (registered.kind === 'not-candidate') return rejected('unregistered-command', 'the command is not a registered doc-check invocation; an arbitrary successful shell command never covers a doc capability');
1577
+ if (capability === 'conflict-check' && registered.conflictAssessment !== true) {
1578
+ return rejected('capability-mismatch', 'conflict-check requires the structurally parsed --conflict-assessment mode');
1579
+ }
717
1580
  if (registered.kind === 'rejected' && registered.reason !== 'implementation-pin-mismatch') {
718
1581
  if (registered.reason === 'implementation-not-installed') return rejected('implementation-not-installed', 'the registered check does not resolve to this install\'s own doc-check runtime; a different path earns no credit');
719
1582
  return rejected('unregistered-command', `doc-check invocation rejected (${registered.reason}); no credit`);
@@ -744,11 +1607,55 @@ const createClaudeOriginProvider = input => {
744
1607
  }
745
1608
  const paths = Array.isArray(projection.checkedPaths) ? projection.checkedPaths.slice() : [];
746
1609
  if (!paths.length) return rejected('scope-empty', `${capability} checked no governed paths`);
1610
+ let corpusBinding;
1611
+ let baselineArtifactDigests;
1612
+ if (capability === 'conflict-check') {
1613
+ const assessment = report.conflictAssessment;
1614
+ const corpus = assessment && assessment.corpus;
1615
+ const config = corpus && corpus.config;
1616
+ if (registered.conflictAssessment !== true || report.mode !== 'conflict-assessment'
1617
+ || assessment?.schema !== DOC_CHECK_CONFLICT_SCHEMA || assessment.state !== 'pass'
1618
+ || projection.corpus?.complete !== true || corpus?.complete !== true
1619
+ || !Array.isArray(corpus.paths) || !corpus.paths.length || !recordOf(corpus.digests)
1620
+ || !recordOf(config) || !textOf(config.path) || path.isAbsolute(config.path)
1621
+ || config.path.split(/[\\/]/).includes('..') || !HEX64.test(String(config.digest || ''))
1622
+ || !/^[0-9a-f]{40,64}$/.test(String(corpus.baseCommit || ''))
1623
+ || JSON.stringify([...new Set(corpus.paths)].sort(compareKeys)) !== JSON.stringify(corpus.paths)
1624
+ || JSON.stringify(Object.keys(corpus.digests).sort(compareKeys)) !== JSON.stringify(corpus.paths)
1625
+ || corpus.paths.some(relative => !textOf(relative) || path.isAbsolute(relative)
1626
+ || relative.split(/[\\/]/).includes('..') || !HEX64.test(String(corpus.digests[relative] || '')))) {
1627
+ return rejected('corpus-binding-invalid', 'conflict-check stdout does not carry a complete, structured full-corpus assessment');
1628
+ }
1629
+ const observedCorpus = recordOf(cap.observedCorpusDigests) ? cap.observedCorpusDigests : {};
1630
+ if (corpus.paths.some(relative => observedCorpus[relative] !== corpus.digests[relative])) {
1631
+ return rejected('corpus-changed-after-check', 'a full-corpus path is missing or changed between assessment and capture');
1632
+ }
1633
+ if (cap.observedConfigDigest !== config.digest) {
1634
+ return rejected('config-changed-after-check', 'the project-docs config changed between assessment and capture');
1635
+ }
1636
+ if (!recordOf(request.identity) || !HEX64.test(String(request.identity.baselineDigest || ''))
1637
+ || !HEX64.test(String(request.identity.candidateDigest || '')) || !HEX64.test(String(request.identity.configDigest || ''))) {
1638
+ return rejected('identity-missing', 'conflict-check needs the complete snapshot identity');
1639
+ }
1640
+ corpusBinding = {
1641
+ complete: true,
1642
+ paths: corpus.paths.slice(),
1643
+ digests: { ...corpus.digests },
1644
+ config: { ...config },
1645
+ baseCommit: corpus.baseCommit,
1646
+ baselineDigest: request.identity.baselineDigest,
1647
+ candidateDigest: request.identity.candidateDigest,
1648
+ configIdentityDigest: request.identity.configDigest,
1649
+ };
1650
+ baselineArtifactDigests = recordOf(assessment.baselineArtifacts) ? assessment.baselineArtifacts : {};
1651
+ }
747
1652
  const observed = recordOf(cap.observedDigests) ? cap.observedDigests : {};
748
1653
  const artifactDigests = {};
1654
+ const baselineArtifacts = {};
749
1655
  for (const relative of paths) {
750
- if (typeof observed[relative] !== 'string') return rejected('artifact-unobserved', `no observation-time digest for ${relative}`);
751
- artifactDigests[relative] = observed[relative];
1656
+ if (typeof observed[relative] === 'string') artifactDigests[relative] = observed[relative];
1657
+ else if (capability === 'conflict-check' && HEX64.test(String(baselineArtifactDigests[relative] || ''))) baselineArtifacts[relative] = baselineArtifactDigests[relative];
1658
+ else return rejected('artifact-unobserved', `no observation-time digest for ${relative}`);
752
1659
  }
753
1660
  const argv = ['node', ...registered.argv];
754
1661
  const command = argv.join(' ');
@@ -772,6 +1679,8 @@ const createClaudeOriginProvider = input => {
772
1679
  endIdentity: recordOf(request.identity) ? JSON.parse(JSON.stringify(request.identity)) : null,
773
1680
  paths,
774
1681
  artifactDigests,
1682
+ ...(Object.keys(baselineArtifacts).length ? { baselineArtifacts } : {}),
1683
+ ...(corpusBinding ? { corpusBinding } : {}),
775
1684
  captured: { responseRef: cap.responseRef, responseDigest: cap.responseDigest },
776
1685
  };
777
1686
  return { status: 'accepted', originRef, event, correlation: { toolCorrelationId: cap.tool_use_id, resultRef: cap.responseRef } };
@@ -969,7 +1878,8 @@ const createOpencodeOriginProvider = input => {
969
1878
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
970
1879
  if (!recordOf(observed)) return rejected('identity-missing', 'OpenCode capture lacks workflow identity observed at capture time');
971
1880
  const sameWorkflow = observed.workflowId === safeInput.workflowId && observed.workflowRevision === safeInput.workflowRevision;
972
- const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit && observed.baselinePin.root === pin.root);
1881
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1882
+ && samePathKey(observed.baselinePin.root, pin.root));
973
1883
  if (!sameWorkflow || !samePin) return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
974
1884
  const signature = opencodeContentSignature(cap);
975
1885
  const firstOccurrence = captures.findIndex(item => recordOf(item) && opencodeContentSignature(item) === signature);
@@ -1058,6 +1968,7 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1058
1968
  responseRef: `host-payload/${event}/${captureId}`,
1059
1969
  };
1060
1970
  if (payload.prompt_id !== undefined) record.prompt_id = payload.prompt_id;
1971
+ if (payload.parent_tool_use_id !== undefined) record.parent_tool_use_id = payload.parent_tool_use_id;
1061
1972
  // The launch tool name separates the Agent/Task result surface and the subagent launch from executed tools (F4/F5).
1062
1973
  if (typeof payload.tool_name === 'string') record.tool_name = payload.tool_name;
1063
1974
  const agentId = payload.agent_id !== undefined ? payload.agent_id : responseAliasAgentId;
@@ -1086,12 +1997,25 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1086
1997
  record.toolResponse = toolResponse;
1087
1998
  } else { record.stdout = null; record.stdoutTruncated = true; }
1088
1999
  let checkedPaths = [];
2000
+ let conflictCorpus = null;
1089
2001
  try {
1090
2002
  const report = JSON.parse(stdout);
1091
2003
  checkedPaths = ['doc-lint', 'link-check'].flatMap(capability => (recordOf(report.capabilities) && recordOf(report.capabilities[capability])
1092
2004
  && Array.isArray(report.capabilities[capability].checkedPaths) ? report.capabilities[capability].checkedPaths : []));
2005
+ const conflict = recordOf(report.capabilities) && report.capabilities['conflict-check'];
2006
+ if (installedCheck.conflictAssessment === true && report.mode === 'conflict-assessment'
2007
+ && recordOf(conflict) && conflict.state === 'pass' && recordOf(conflict.corpus)) {
2008
+ checkedPaths.push(...(Array.isArray(conflict.checkedPaths) ? conflict.checkedPaths : []));
2009
+ conflictCorpus = report.conflictAssessment && report.conflictAssessment.corpus;
2010
+ }
1093
2011
  } catch { /* not JSON: no paths observed, the event will be withheld */ }
1094
2012
  record.observedDigests = typeof workspaceRoot === 'string' && workspaceRoot ? observedDigestsFor(workspaceRoot, [...new Set(checkedPaths)]) : {};
2013
+ if (recordOf(conflictCorpus) && Array.isArray(conflictCorpus.paths) && recordOf(conflictCorpus.config)) {
2014
+ record.observedCorpusDigests = typeof workspaceRoot === 'string' && workspaceRoot
2015
+ ? observedDigestsFor(workspaceRoot, conflictCorpus.paths) : {};
2016
+ record.observedConfigDigest = typeof workspaceRoot === 'string' && workspaceRoot
2017
+ ? observedDigestsFor(workspaceRoot, [conflictCorpus.config.path])[conflictCorpus.config.path] || null : null;
2018
+ }
1095
2019
  }
1096
2020
  } else if (event === 'PostToolUseFailure') {
1097
2021
  record.is_interrupt = Boolean(payload.is_interrupt);
@@ -1179,7 +2103,8 @@ function readOpencodeCaptureView(sessionDir) {
1179
2103
  function observedIdentityFor(sessionDir) {
1180
2104
  try {
1181
2105
  const workflow = JSON.parse(fs.readFileSync(path.join(sessionDir, 'workflow-run.json'), 'utf8'));
1182
- return { workflowId: workflow.workflowId, workflowRevision: workflow.revision, baselinePin: workflow.docReconciliationBaseline || null };
2106
+ return { workflowId: workflow.workflowId, workflowRevision: workflow.revision,
2107
+ baselinePin: workflow.docReconciliationBaseline || null, taskPin: workflow.docReconciliationTask || null };
1183
2108
  } catch {
1184
2109
  return null;
1185
2110
  }
@@ -1203,5 +2128,8 @@ function reviewRecordIndependent(cap, captures, principals) {
1203
2128
  module.exports = { createClaudeOriginProvider, createOpencodeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
1204
2129
  registeredInstalledCheck: resolveDocCheckInvocation, installedDocCheck,
1205
2130
  fileDigest, SHIPPED_DOC_CHECK_DIGEST, trustRisk, journalExecutors, reviewRecordIndependent,
1206
- writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
1207
- _internals: { captureJson, captureDigest, digestText, canonicalCapture } };
2131
+ captureOutlineDispatch, writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
2132
+ _internals: { captureJson, captureDigest, digestText, canonicalCapture, parseOutlineDispatchPrompt,
2133
+ parseInventoryPinRows, validateOutlineInventoryPin, resolveInventoryRow, parseOutlineCitation, resolveOutlineAttempt,
2134
+ captureOutlineDispatch, resolveOutlineEvidence, verifyOutlineMergeCorrespondence, validateOutlineCaptureQualification,
2135
+ outlineCaptureMappingValid, OUTLINE_INVENTORY_PIN } };