harness-everything 0.45.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) 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 +24 -0
  5. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/README.md +75 -0
  6. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/capture.sanitized.jsonl +32 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
  8. package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
  9. package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
  10. package/ci/mechanism-325-f4-projections.test.js +11 -1
  11. package/ci/mechanism-prb-outline-review.test.js +1449 -0
  12. package/docs/adr/DOCS-001-project-docs-config.md +32 -0
  13. package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
  14. package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
  15. package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
  16. package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
  17. package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
  18. package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
  19. package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
  20. package/docs/doc-workflow-reconciliation.md +47 -1
  21. package/docs/governed-index.md +8 -0
  22. package/docs/platform-capabilities.md +2 -0
  23. package/harness-everything/SKILL.md +1 -1
  24. package/harness-everything/scripts/doc-evidence.js +55 -0
  25. package/hooks/hooks.json +1 -1
  26. package/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  27. package/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  28. package/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
  29. package/hooks/scripts/state-persist.js +10 -0
  30. package/opencode-plugin/plugin.json +1 -1
  31. package/package.json +1 -1
  32. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  33. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  34. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  35. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
  36. package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
  37. package/plugins/harness-everything/plugin.json +1 -1
  38. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  39. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
  40. 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,12 +45,42 @@ 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']);
49
51
  const DOC_CHECK_BOOLEAN_FLAGS = new Set(['--conflict-assessment']);
50
52
  const DOC_CHECK_SCHEMA = 'harness.doc-check.check.v1';
51
53
  const DOC_CHECK_CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
54
+ const OUTLINE_DISPATCH_SCHEMA = 'harness.outline-dispatch.v1';
55
+ const OUTLINE_CAPTURE_MAPPING = Object.freeze({
56
+ version: 'claude-outline-dispatch-mapping/2',
57
+ promptField: 'tool_input.prompt',
58
+ dispatchKey: 'session_id+tool_use_id',
59
+ runInBackgroundField: 'tool_input.run_in_background',
60
+ isolationPresenceField: 'tool_input.isolation',
61
+ resultAgentIdField: 'tool_response.agentId',
62
+ resultStatusField: 'tool_response.status',
63
+ workerStartEvent: 'SubagentStart',
64
+ workerStopEvent: 'SubagentStop',
65
+ workerIdentity: 'session_id+agent_id',
66
+ workerRootField: 'cwd',
67
+ });
68
+ const OUTLINE_SEAM_SCHEMA = 'harness.outline-dispatch-seam.v1';
69
+ const OUTLINE_HANDOFF_SCHEMA = 'harness.outline-handoff.v1';
70
+ const OUTLINE_LEASE_SCHEMA = 'harness.outline-writer-lease.v1';
71
+ const OUTLINE_INVENTORY_SOURCE = '/Users/daniel.chang/Desktop/Skills/Harness-everything/.worktrees/packets/adr-e1-2026-10-27/report-inv.md';
72
+ const OUTLINE_INVENTORY_SHA256 = '2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6';
73
+ const OUTLINE_INVENTORY_PIN = Object.freeze({
74
+ source: OUTLINE_INVENTORY_SOURCE,
75
+ sha256: OUTLINE_INVENTORY_SHA256,
76
+ runId: 'e1-inv-1',
77
+ inputSha: '0ef0541c6cae550e8269ba712e03797e0e987dfb',
78
+ approvalRef: { issue: 325, commentId: '6436207729', decision: 1 },
79
+ retainedArtifactPath: `inventory/${OUTLINE_INVENTORY_SHA256}.md`,
80
+ });
81
+ const OUTLINE_MARKER = 'HARNESS_OUTLINE_DISPATCH_V1';
82
+ const OUTLINE_TASK_ID = /^task-[A-Za-z0-9][A-Za-z0-9+._-]{0,127}$/;
83
+ const OUTLINE_ATTEMPT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
52
84
  const DOC_CHECK_SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh', 'fish']);
53
85
  const DOC_CHECK_LAUNCHERS = new Set(['node', 'nodejs', 'bun', 'deno', 'tsx', 'ts-node']);
54
86
  const DOC_CHECK_LAUNCHER_VALUE_FLAGS = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--inspect-port', '--input-type', '--conditions', '--env-file']);
@@ -599,6 +631,915 @@ function evaluateQualification(input, expectedHost = HOST_ID) {
599
631
  return { open: true, source: 'validated-export' };
600
632
  }
601
633
 
634
+ function outlineSha(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
635
+
636
+ function parseOutlineDispatchPrompt(prompt) {
637
+ if (typeof prompt !== 'string') return null;
638
+ const occurrences = prompt.match(new RegExp(OUTLINE_MARKER, 'g')) || [];
639
+ if (occurrences.length !== 1) return null;
640
+ const line = prompt.split(/\r?\n/).find(item => item.includes(OUTLINE_MARKER));
641
+ const match = /^(HARNESS_OUTLINE_DISPATCH_V1) taskId=([^\s]+) attemptId=([^\s]+) token=([^\s]+) workerRoot=(.+)$/.exec(line || '');
642
+ if (!match || !OUTLINE_TASK_ID.test(match[2]) || !OUTLINE_ATTEMPT_ID.test(match[3])
643
+ || !textOf(match[4]) || !path.isAbsolute(match[5]) || match[5].trim() !== match[5]) return null;
644
+ const block = match[0];
645
+ const tokenDigest = outlineSha(match[4]);
646
+ const promptDigest = outlineSha(Buffer.from(prompt, 'utf8'));
647
+ return {
648
+ taskId: match[2], attemptId: match[3], token: match[4], tokenDigest,
649
+ workerRoot: match[5], blockDigest: outlineSha(Buffer.from(block, 'utf8')),
650
+ promptDigest,
651
+ dispatchInputDigest: outlineSha(captureJson({ taskId: match[2], attemptId: match[3], tokenDigest,
652
+ workerRoot: match[5], promptDigest })),
653
+ };
654
+ }
655
+
656
+ function parseInventoryPinRows(bytes) {
657
+ const text = Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes || '');
658
+ const identity = /run id `([^`]+)`[^\n]*input SHA `([a-f0-9]+)`/i.exec(text);
659
+ if (!identity) return { state: 'unresolved', reason: 'inventory-identity-missing' };
660
+ const lines = text.split(/\r?\n/);
661
+ const mechanismIndex = lines.findIndex(line => /Mechanism inventory/i.test(line));
662
+ const areaIndex = lines.findIndex(line => /Area grouping \+ candidate domain prefixes/i.test(line));
663
+ if (mechanismIndex < 0 || areaIndex < 0 || areaIndex <= mechanismIndex) return { state: 'unresolved', reason: 'inventory-tables-missing' };
664
+ const rows = new Map();
665
+ for (const line of lines.slice(mechanismIndex, areaIndex)) {
666
+ const match = /^\|\s*(\d{1,2})\s*\|/.exec(line);
667
+ if (!match) continue;
668
+ rows.set(match[1], (rows.get(match[1]) || 0) + 1);
669
+ }
670
+ const domains = new Map();
671
+ for (const line of lines.slice(areaIndex)) {
672
+ const cells = line.split('|').map(cell => cell.trim());
673
+ if (cells.length < 5 || !cells[1] || !cells[2] || !cells[3]) continue;
674
+ const prefix = /^`([A-Z][A-Z0-9]{1,9})`$/.exec(cells[3]);
675
+ if (!prefix) continue;
676
+ for (const item of cells[2].split(',')) {
677
+ const range = /^(\d{1,2})\s*[–-]\s*(\d{1,2})$/.exec(item.trim());
678
+ if (range) {
679
+ for (let id = Number(range[1]); id <= Number(range[2]); id += 1) {
680
+ const key = String(id).padStart(2, '0');
681
+ const entries = domains.get(key) || [];
682
+ entries.push(prefix[1]);
683
+ domains.set(key, entries);
684
+ }
685
+ } else if (/^\d{1,2}$/.test(item.trim())) {
686
+ const key = item.trim().padStart(2, '0');
687
+ const entries = domains.get(key) || [];
688
+ entries.push(prefix[1]);
689
+ domains.set(key, entries);
690
+ }
691
+ }
692
+ }
693
+ return { state: 'parsed', runId: identity[1], inputSha: identity[2], rows, domains };
694
+ }
695
+
696
+ function validateOutlineInventoryPin({ bytes, pin, expectedPin = OUTLINE_INVENTORY_PIN } = {}) {
697
+ if (!Buffer.isBuffer(bytes) || !recordOf(pin) || !recordOf(expectedPin)) return { state: 'unresolved', reason: 'inventory-unavailable' };
698
+ const fields = ['source', 'sha256', 'runId', 'inputSha', 'approvalRef', 'retainedArtifactPath'];
699
+ const actualPin = Object.fromEntries(fields.map(field => [field, pin[field]]));
700
+ const trustedPin = Object.fromEntries(fields.map(field => [field, expectedPin[field]]));
701
+ if (captureJson(actualPin) !== captureJson(trustedPin)) return { state: 'unresolved', reason: 'inventory-pin-mismatch' };
702
+ const digest = outlineSha(bytes);
703
+ if (digest !== expectedPin.sha256 || expectedPin.retainedArtifactPath !== `inventory/${expectedPin.sha256}.md`) {
704
+ return { state: 'unresolved', reason: 'inventory-digest-mismatch' };
705
+ }
706
+ const parsed = parseInventoryPinRows(bytes);
707
+ if (parsed.state !== 'parsed' || parsed.runId !== expectedPin.runId || parsed.inputSha !== expectedPin.inputSha) {
708
+ return { state: 'unresolved', reason: parsed.reason || 'inventory-revision-mismatch' };
709
+ }
710
+ return { state: 'accepted', pin: trustedPin, pinDigest: outlineSha(Buffer.from(captureJson(trustedPin))),
711
+ artifactDigest: digest, parsed };
712
+ }
713
+
714
+ function resolveInventoryRow(inventory, rowId) {
715
+ const parsed = inventory && inventory.parsed;
716
+ if (!parsed || parsed.state !== 'parsed' || !textOf(rowId)) return { state: 'unresolved', reason: 'inventory-row-missing' };
717
+ const matches = [...parsed.rows.entries()].filter(([id]) => id === rowId);
718
+ const domains = parsed.domains.get(rowId) || [];
719
+ if (matches.length !== 1 || matches[0][1] !== 1 || domains.length !== 1) {
720
+ return { state: 'unresolved', reason: 'inventory-row-ambiguous' };
721
+ }
722
+ return { state: 'accepted', row: { rowId, domain: domains[0] } };
723
+ }
724
+
725
+ function hasSubstantiveDecision(text) {
726
+ const lines = String(text || '').split(/\r?\n/);
727
+ let decisionLines = null;
728
+ const substantive = linesToCheck => linesToCheck.some(line => {
729
+ const value = line.trim().replace(/^[-*]\s*/, '');
730
+ return value && !/^<!--|^-->$|^\[[^\]]*\]$|^<[^>]+>$|^TBD$|^TODO$/i.test(value);
731
+ });
732
+ for (const line of lines) {
733
+ const heading = /^ {0,3}##\s+(.+?)\s*#*\s*$/.exec(line);
734
+ const headingText = heading?.[1]?.trim() || '';
735
+ if (/^Decision\b/i.test(headingText)) {
736
+ if (decisionLines && substantive(decisionLines)) return true;
737
+ decisionLines = [];
738
+ continue;
739
+ }
740
+ if (decisionLines && heading) {
741
+ if (substantive(decisionLines)) return true;
742
+ decisionLines = null;
743
+ continue;
744
+ }
745
+ if (decisionLines) decisionLines.push(line);
746
+ }
747
+ return Boolean(decisionLines && substantive(decisionLines));
748
+ }
749
+
750
+ function parseOutlineCitation(bytes, targetPath) {
751
+ const text = Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes || '');
752
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
753
+ if (!frontmatter) return { state: 'unresolved', reason: 'outline-frontmatter-missing' };
754
+ const field = name => new RegExp(`^${name}:\\s*([^\\r\\n]+)\\s*$`, 'm').exec(frontmatter[1])?.[1]?.trim() || '';
755
+ const id = field('id');
756
+ const status = field('status');
757
+ const rowMatches = [...text.matchAll(/^Inventory row:\s*([0-9]{1,2})\s*$/gmi)];
758
+ const targetMatches = [...text.matchAll(/^Target:\s*(\S+)\s*$/gmi)];
759
+ const domainMatches = [...text.matchAll(/^Domain:\s*([A-Z][A-Z0-9]{1,9})\s*$/gmi)];
760
+ if (!/^([A-Z][A-Z0-9]{1,9})-\d{3,}$/.test(id) || status !== 'proposed'
761
+ || rowMatches.length !== 1 || targetMatches.length !== 1 || domainMatches.length !== 1
762
+ || targetMatches[0][1] !== targetPath) return { state: 'unresolved', reason: 'outline-citation-invalid' };
763
+ if (hasSubstantiveDecision(text)) return { state: 'unresolved', reason: 'outline-already-draft' };
764
+ return { state: 'accepted', id, status, rowId: rowMatches[0][1], targetPath,
765
+ domain: domainMatches[0][1], bytesDigest: outlineSha(Buffer.from(text, 'utf8')) };
766
+ }
767
+
768
+ function outlineGit(root, args, binary = false) {
769
+ try {
770
+ const env = { ...process.env };
771
+ for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
772
+ return execFileSync('git', ['-C', root, ...args], { env, encoding: binary ? null : 'utf8',
773
+ timeout: 10000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
774
+ } catch (error) {
775
+ throw new Error('outline-git-unavailable', { cause: error });
776
+ }
777
+ }
778
+
779
+ function canonicalOutlineRoot(value) {
780
+ if (typeof value !== 'string' || !path.isAbsolute(value)) throw new Error('outline-root-invalid');
781
+ try { fs.statSync(value); } catch { throw new Error('outline-root-not-canonical'); }
782
+ return canonical(value);
783
+ }
784
+
785
+ function outlineWorkspaceId(root) { return 'ws-' + digestText(captureJson({ workspaceRoot: canonical(root) })); }
786
+
787
+ function readOutlineJson(file) {
788
+ try {
789
+ const bytes = fs.readFileSync(file);
790
+ const value = JSON.parse(bytes.toString('utf8'));
791
+ if (!recordOf(value)) return { state: 'unresolved', reason: 'outline-record-invalid' };
792
+ return { state: 'accepted', value, bytes, digest: outlineSha(bytes) };
793
+ } catch {
794
+ return { state: 'unresolved', reason: 'outline-record-unavailable' };
795
+ }
796
+ }
797
+
798
+ function outlineTaskState(sessionDir) {
799
+ const workflowFile = path.join(sessionDir, 'workflow-run.json');
800
+ const loaded = readOutlineJson(workflowFile);
801
+ if (loaded.state !== 'accepted') return { state: 'unresolved', reason: 'workflow-pin-unavailable' };
802
+ const workflow = loaded.value;
803
+ const taskBinding = workflow.docReconciliationTask;
804
+ const baseline = workflow.docReconciliationBaseline;
805
+ if (!recordOf(taskBinding) || !OUTLINE_TASK_ID.test(String(taskBinding.taskId || ''))
806
+ || !Array.isArray(taskBinding.requirementIds) || !taskBinding.requirementIds.length
807
+ || !HEX64.test(String(taskBinding.requirementDigest || ''))
808
+ || !recordOf(baseline) || !textOf(baseline.root) || !/^[0-9a-f]{40,64}$/.test(String(baseline.commit || ''))
809
+ || outlineSha(Buffer.from(captureJson(taskBinding.requirementIds))) !== taskBinding.requirementDigest
810
+ || taskBinding.taskId !== `task-${taskBinding.requirementIds.join('+')}`
811
+ || taskBinding.baselineCommit !== baseline.commit || !sameCanonicalPath(taskBinding.baselineRoot, baseline.root)
812
+ || taskBinding.workflowId !== workflow.workflowId || taskBinding.workspaceId !== outlineWorkspaceId(baseline.root)) {
813
+ return { state: 'unresolved', reason: 'workflow-task-binding-invalid' };
814
+ }
815
+ return { state: 'accepted', workflow, taskBinding, baseline };
816
+ }
817
+
818
+ function resolveOutlineAttempt({ sessionDir, taskId, attemptId } = {}) {
819
+ if (!textOf(sessionDir) || !OUTLINE_TASK_ID.test(String(taskId || '')) || !OUTLINE_ATTEMPT_ID.test(String(attemptId || ''))) {
820
+ return { state: 'unresolved', reason: 'attempt-identity-invalid' };
821
+ }
822
+ const taskState = outlineTaskState(sessionDir);
823
+ if (taskState.state !== 'accepted' || taskState.taskBinding.taskId !== taskId) {
824
+ return { state: 'unresolved', reason: taskState.reason || 'task-binding-mismatch' };
825
+ }
826
+ const attemptsRoot = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId, 'attempts');
827
+ let names;
828
+ try { names = fs.readdirSync(attemptsRoot, { withFileTypes: true }).filter(item => item.isDirectory()).map(item => item.name).sort(compareKeys); }
829
+ catch { return { state: 'unresolved', reason: 'attempt-chain-unavailable' }; }
830
+ if (!names.length) return { state: 'unresolved', reason: 'attempt-chain-empty' };
831
+ const attempts = new Map();
832
+ for (const name of names) {
833
+ if (!OUTLINE_ATTEMPT_ID.test(name)) return { state: 'unresolved', reason: 'attempt-id-invalid' };
834
+ const loaded = readOutlineJson(path.join(attemptsRoot, name, 'handoff.json'));
835
+ if (loaded.state !== 'accepted') return { state: 'unresolved', reason: 'handoff-unavailable' };
836
+ const handoff = loaded.value;
837
+ if (handoff.schema !== OUTLINE_HANDOFF_SCHEMA || handoff.taskId !== taskId || handoff.attemptId !== name
838
+ || !textOf(handoff.handoffId) || !textOf(handoff.draftDispatchToken)
839
+ || !recordOf(handoff.baseline) || handoff.baseline.commit !== taskState.baseline.commit
840
+ || !sameCanonicalPath(handoff.baseline.root, taskState.baseline.root)
841
+ || handoff.configDigest !== taskState.taskBinding.configDigest) {
842
+ return { state: 'unresolved', reason: 'handoff-identity-or-baseline-mismatch' };
843
+ }
844
+ const attemptDir = path.join(attemptsRoot, name);
845
+ const seamDir = path.join(attemptDir, 'seams');
846
+ const tokenDir = path.join(attemptDir, 'tokens');
847
+ const filesIn = dir => {
848
+ try { return fs.readdirSync(dir, { withFileTypes: true })
849
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json')).map(entry => path.join(dir, entry.name)); }
850
+ catch { return []; }
851
+ };
852
+ const seamFiles = filesIn(seamDir);
853
+ const tokenFiles = filesIn(tokenDir);
854
+ const evidenceFiles = [...seamFiles, ...tokenFiles];
855
+ const sealLoaded = readOutlineJson(path.join(attemptDir, 'handoff-seal.json'));
856
+ const sealExists = fs.existsSync(path.join(attemptDir, 'handoff-seal.json'));
857
+ if (sealExists && (sealLoaded.state !== 'accepted' || sealLoaded.value.schema !== 'harness.outline-handoff-seal.v1'
858
+ || sealLoaded.value.taskId !== taskId || sealLoaded.value.attemptId !== name
859
+ || sealLoaded.value.handoffDigest !== loaded.digest)) {
860
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
861
+ }
862
+ if (evidenceFiles.length && !sealExists) {
863
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
864
+ }
865
+ for (const evidenceFile of evidenceFiles) {
866
+ const evidenceLoaded = readOutlineJson(evidenceFile);
867
+ if (evidenceLoaded.state !== 'accepted' || evidenceLoaded.value.taskId !== taskId
868
+ || evidenceLoaded.value.attemptId !== name || evidenceLoaded.value.handoffDigest !== loaded.digest) {
869
+ return { state: 'unresolved', reason: 'handoff-overwritten-after-dispatch' };
870
+ }
871
+ }
872
+ let interrupted = false;
873
+ if (sealExists && (tokenFiles.length !== 1 || seamFiles.length !== 1)) interrupted = true;
874
+ if (sealExists && tokenFiles.length === 1 && seamFiles.length === 1) {
875
+ const token = readOutlineJson(tokenFiles[0]).value;
876
+ const seam = readOutlineJson(seamFiles[0]).value;
877
+ if (token.schema !== 'harness.outline-token.v1' || seam.schema !== OUTLINE_SEAM_SCHEMA
878
+ || token.tokenDigest !== seam.tokenDigest || token.toolUseId !== seam.toolUseId
879
+ || token.dispatchInputDigest !== seam.dispatchInputDigest) {
880
+ return { state: 'unresolved', reason: 'handoff-dispatch-evidence-mismatch' };
881
+ }
882
+ }
883
+ attempts.set(name, { handoff, handoffDigest: loaded.digest, attemptDir, interrupted });
884
+ }
885
+ const parents = new Map();
886
+ const children = new Map(names.map(name => [name, 0]));
887
+ const tokens = new Set();
888
+ for (const [name, item] of attempts) {
889
+ const { handoff } = item;
890
+ if (tokens.has(handoff.draftDispatchToken)) return { state: 'unresolved', reason: 'attempt-token-reused' };
891
+ tokens.add(handoff.draftDispatchToken);
892
+ const parent = handoff.supersedesAttemptId;
893
+ if (parent === null || parent === undefined) { parents.set(name, null); continue; }
894
+ if (!attempts.has(parent) || parent === name || children.get(parent) === undefined) {
895
+ return { state: 'unresolved', reason: 'attempt-predecessor-missing' };
896
+ }
897
+ children.set(parent, children.get(parent) + 1);
898
+ if (children.get(parent) > 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
899
+ parents.set(name, parent);
900
+ }
901
+ const roots = [...parents.values()].filter(parent => parent === null).length;
902
+ if (roots !== 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
903
+ for (const name of names) {
904
+ const seen = new Set();
905
+ let current = name;
906
+ while (current !== null) {
907
+ if (seen.has(current)) return { state: 'unresolved', reason: 'attempt-chain-cyclic' };
908
+ seen.add(current);
909
+ current = parents.get(current);
910
+ if (current === undefined) return { state: 'unresolved', reason: 'attempt-predecessor-missing' };
911
+ }
912
+ }
913
+ const leaves = names.filter(name => children.get(name) === 0);
914
+ if (leaves.length !== 1) return { state: 'unresolved', reason: 'attempt-chain-ambiguous' };
915
+ if (leaves[0] !== attemptId) return { state: 'unresolved', reason: 'attempt-superseded' };
916
+ const currentAttempt = attempts.get(attemptId);
917
+ if (currentAttempt.interrupted) return { state: 'unresolved', reason: 'attempt-interrupted' };
918
+ return { state: 'accepted', taskBinding: taskState.taskBinding, baseline: taskState.baseline,
919
+ handoff: currentAttempt.handoff, handoffDigest: currentAttempt.handoffDigest,
920
+ attemptDir: currentAttempt.attemptDir, predecessors: [...parents.keys()].filter(id => id !== attemptId) };
921
+ }
922
+
923
+ function outlineHandoffDirectory(sessionDir, taskId) {
924
+ return path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId);
925
+ }
926
+
927
+ function validateOutlineReview({ handoff, attemptDir, captures, targetPaths, outlineDigests, inventory, sessionId } = {}) {
928
+ const refs = Array.isArray(captures) ? captures : [];
929
+ const matches = refs.filter(item => recordOf(item) && item.ref === handoff.reviewCaptureRef && item.event === 'SubagentStop');
930
+ if (matches.length !== 1) return { state: 'unresolved', reason: 'outline-review-capture-missing-or-ambiguous' };
931
+ const review = matches[0];
932
+ const reviewer = review.agent_id;
933
+ if (!textOf(reviewer) || reviewer === sessionId || review.reviewVerdict !== 'approve'
934
+ || !textOf(review.reviewSummary) || !HEX64.test(String(review.reviewDigest || ''))
935
+ || review.reviewDigest !== handoff.reviewDigest) return { state: 'unresolved', reason: 'outline-review-invalid' };
936
+ const capturedDigest = captureDigest(review);
937
+ if (capturedDigest !== handoff.reviewCaptureDigest) return { state: 'unresolved', reason: 'outline-review-capture-digest-mismatch' };
938
+ const principals = { author: sessionId, executors: journalExecutors(refs, sessionId) };
939
+ if (!reviewRecordIndependent(review, refs, principals)) return { state: 'unresolved', reason: 'outline-review-not-independent' };
940
+ const inventoryPath = path.posix.join('doc-reconciliation/outline-confirmation/inventory', path.posix.basename(inventory.pin.retainedArtifactPath));
941
+ const expectedReviewed = { ...outlineDigests, [inventoryPath]: inventory.artifactDigest };
942
+ if (captureJson(review.reviewedArtifactDigests) !== captureJson(expectedReviewed)) {
943
+ return { state: 'unresolved', reason: 'outline-review-artifact-binding-mismatch' };
944
+ }
945
+ const summary = String(review.reviewSummary);
946
+ if (!summary.includes(`inventory row ${handoff.inventory.rowId}`) || !summary.includes(targetPaths[0])
947
+ || !summary.includes(handoff.inventory.domain)) return { state: 'unresolved', reason: 'outline-review-scope-mismatch' };
948
+ const result = refs.filter(item => recordOf(item) && item.event === 'PostToolUse'
949
+ && (item.tool_name === 'Agent' || item.tool_name === 'Task') && item.agent_id === reviewer);
950
+ if (result.length !== 1) return { state: 'unresolved', reason: 'outline-review-result-missing-or-ambiguous' };
951
+ return { state: 'accepted', review, reviewerPrincipal: reviewer, captureDigest: capturedDigest,
952
+ reviewDigest: review.reviewDigest, inventoryPath };
953
+ }
954
+
955
+ function validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspaceRoot, captures, expectedPin } = {}) {
956
+ const taskState = outlineTaskState(sessionDir);
957
+ if (taskState.state !== 'accepted' || parsed.taskId !== taskState.taskBinding.taskId) {
958
+ return { state: 'unresolved', reason: taskState.reason || 'task-binding-mismatch' };
959
+ }
960
+ const attempt = resolveOutlineAttempt({ sessionDir, taskId: parsed.taskId, attemptId: parsed.attemptId });
961
+ if (attempt.state !== 'accepted') return attempt;
962
+ const { handoff, handoffDigest, attemptDir, baseline, taskBinding } = attempt;
963
+ const workflow = taskState.workflow;
964
+ if (handoff.schema !== OUTLINE_HANDOFF_SCHEMA || handoff.workspaceId !== outlineWorkspaceId(baseline.root)
965
+ || handoff.workflowId !== workflow.workflowId || handoff.workflowRevision !== workflow.revision
966
+ || handoff.sessionId !== payload.session_id || handoff.hostId !== HOST_ID
967
+ || handoff.coordinatorPrincipal !== payload.session_id
968
+ || handoff.draftDispatchToken !== parsed.token
969
+ || !sameCanonicalPath(handoff.baseline.root, baseline.root) || handoff.baseline.commit !== baseline.commit
970
+ || handoff.baseline.tree !== baseline.tree || handoff.configDigest !== taskBinding.configDigest) {
971
+ return { state: 'unresolved', reason: 'handoff-identity-mismatch' };
972
+ }
973
+ const markerRoot = canonicalOutlineRoot(parsed.workerRoot);
974
+ const declaredRoot = canonicalOutlineRoot(handoff.draftWorktreeRoot);
975
+ if (!sameCanonicalPath(markerRoot, declaredRoot)) return { state: 'unresolved', reason: 'worker-root-mismatch' };
976
+ const gitRoot = outlineGit(markerRoot, ['rev-parse', '--show-toplevel']).trim();
977
+ if (!sameCanonicalPath(canonicalOutlineRoot(gitRoot), markerRoot)) return { state: 'unresolved', reason: 'worker-root-mismatch' };
978
+ const head = outlineGit(markerRoot, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
979
+ const tree = outlineGit(markerRoot, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
980
+ const statusBytes = outlineGit(markerRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all'], true);
981
+ const statusDigest = outlineSha(statusBytes);
982
+ if (head !== handoff.outline.commit || tree !== handoff.outline.tree || statusBytes.length !== 0) {
983
+ return { state: 'unresolved', reason: statusBytes.length ? 'worker-worktree-not-clean-at-O' : 'worker-worktree-not-at-O' };
984
+ }
985
+ if (outlineGit(markerRoot, ['rev-parse', '--verify', `${baseline.commit}^{commit}`]).trim() !== baseline.commit
986
+ || outlineGit(markerRoot, ['rev-parse', '--verify', `${handoff.outline.commit}^{tree}`]).trim() !== handoff.outline.tree
987
+ || outlineGit(markerRoot, ['merge-base', '--is-ancestor', baseline.commit, handoff.outline.commit]) !== '') {
988
+ return { state: 'unresolved', reason: 'B-to-O-git-binding-invalid' };
989
+ }
990
+ const targetPaths = Array.isArray(handoff.targetPaths) ? [...handoff.targetPaths] : [];
991
+ if (!targetPaths.length || targetPaths.some(item => typeof item !== 'string' || path.isAbsolute(item)
992
+ || item.includes('\\') || item.split('/').some(part => !part || part === '.' || part === '..'))
993
+ || captureJson([...new Set(targetPaths)].sort(compareKeys)) !== captureJson(targetPaths)) {
994
+ return { state: 'unresolved', reason: 'outline-target-scope-invalid' };
995
+ }
996
+ const pinDir = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', 'inventory');
997
+ const trustedPin = expectedPin || OUTLINE_INVENTORY_PIN;
998
+ const pinFile = path.join(pinDir, 'inventory-pin.json');
999
+ const pinLoaded = readOutlineJson(pinFile);
1000
+ if (pinLoaded.state !== 'accepted') return { state: 'unresolved', reason: 'inventory-pin-unavailable' };
1001
+ const inventoryFile = path.join(pinDir, trustedPin.retainedArtifactPath.split('/').pop());
1002
+ let inventoryBytes;
1003
+ try { inventoryBytes = fs.readFileSync(inventoryFile); }
1004
+ catch { return { state: 'unresolved', reason: 'inventory-artifact-unavailable' }; }
1005
+ const inventory = validateOutlineInventoryPin({ bytes: inventoryBytes, pin: pinLoaded.value, expectedPin: trustedPin });
1006
+ if (inventory.state !== 'accepted') return inventory;
1007
+ if (outlineSha(pinLoaded.bytes) !== handoff.inventoryPinDigest && handoff.inventoryPinDigest !== undefined) {
1008
+ return { state: 'unresolved', reason: 'inventory-pin-digest-mismatch' };
1009
+ }
1010
+ const outlineBlobs = recordOf(handoff.outline?.targetBlobs) ? handoff.outline.targetBlobs : {};
1011
+ const outlineDigests = {};
1012
+ let citedRow = null;
1013
+ for (const target of targetPaths) {
1014
+ const atBaseline = outlineGit(baseline.root, ['ls-tree', '-r', '--name-only', baseline.commit, '--', target]).trim();
1015
+ if (atBaseline.split('\n').filter(Boolean).includes(target)) return { state: 'unresolved', reason: 'outline-target-existed-at-B' };
1016
+ const bytes = outlineGit(markerRoot, ['show', `${handoff.outline.commit}:${target}`], true);
1017
+ const blobOid = outlineGit(markerRoot, ['rev-parse', `${handoff.outline.commit}:${target}`]).trim();
1018
+ const expected = outlineBlobs[target];
1019
+ if (!recordOf(expected) || expected.blobOid !== blobOid || expected.storedBytesSha256 !== outlineSha(bytes)) {
1020
+ return { state: 'unresolved', reason: 'outline-object-binding-mismatch' };
1021
+ }
1022
+ const citation = parseOutlineCitation(bytes, target);
1023
+ if (citation.state !== 'accepted') return citation;
1024
+ const row = resolveInventoryRow(inventory, citation.rowId);
1025
+ if (row.state !== 'accepted' || citation.domain !== row.row.domain || citation.domain !== handoff.inventory.domain
1026
+ || handoff.inventory.rowId !== citation.rowId || !Array.isArray(handoff.inventory.targetPaths)
1027
+ || captureJson(handoff.inventory.targetPaths) !== captureJson(targetPaths)) {
1028
+ return { state: 'unresolved', reason: row.reason || 'outline-inventory-binding-mismatch' };
1029
+ }
1030
+ const expectedId = path.posix.basename(target, path.posix.extname(target));
1031
+ if (citation.id !== expectedId || !citation.id.startsWith(`${row.row.domain}-`)) {
1032
+ return { state: 'unresolved', reason: 'outline-domain-target-mismatch' };
1033
+ }
1034
+ if (citedRow && captureJson(citedRow) !== captureJson(row.row)) return { state: 'unresolved', reason: 'outline-row-ambiguous' };
1035
+ citedRow = row.row;
1036
+ outlineDigests[target] = outlineSha(bytes);
1037
+ }
1038
+ const leaseFile = path.join(attemptDir, 'writer-lease.json');
1039
+ const leaseLoaded = readOutlineJson(leaseFile);
1040
+ if (leaseLoaded.state !== 'accepted') return { state: 'unresolved', reason: 'writer-lease-unavailable' };
1041
+ const lease = leaseLoaded.value;
1042
+ if (lease.schema !== OUTLINE_LEASE_SCHEMA || lease.id !== handoff.writerLeaseId
1043
+ || lease.principalId !== payload.session_id || !sameCanonicalPath(lease.root, markerRoot) || lease.exclusive !== true
1044
+ || lease.active !== true || (Array.isArray(lease.competingWriters) && lease.competingWriters.length)
1045
+ || leaseLoaded.digest !== handoff.writerLeaseDigest) {
1046
+ return { state: 'unresolved', reason: 'writer-lease-invalid' };
1047
+ }
1048
+ const review = validateOutlineReview({ handoff, attemptDir, captures, targetPaths, outlineDigests, inventory,
1049
+ sessionId: payload.session_id });
1050
+ if (review.state !== 'accepted') return review;
1051
+ return { state: 'accepted', taskBinding, baseline, handoff, handoffDigest, attemptDir, markerRoot,
1052
+ worktree: { root: markerRoot, head, tree, statusDigest, clean: true },
1053
+ inventory, inventoryPinDigest: outlineSha(pinLoaded.bytes), row: citedRow, targetPaths, outlineDigests, review,
1054
+ leaseDigest: leaseLoaded.digest, leaseId: lease.id };
1055
+ }
1056
+
1057
+ function outlineDispatchUnresolved(parsed, reason) {
1058
+ return { schema: OUTLINE_DISPATCH_SCHEMA, state: 'unresolved', reasons: [reason],
1059
+ ...(parsed ? { taskId: parsed.taskId, attemptId: parsed.attemptId, tokenDigest: parsed.tokenDigest,
1060
+ promptDigest: parsed.promptDigest, blockDigest: parsed.blockDigest, dispatchInputDigest: parsed.dispatchInputDigest,
1061
+ workerRoot: parsed.workerRoot } : {}) };
1062
+ }
1063
+
1064
+ function captureOutlineDispatch(input = {}) {
1065
+ let parsed = null;
1066
+ try {
1067
+ const { payload, capture, sessionDir, workspaceRoot, captures = [], fixtureExpectedPin } = input;
1068
+ if (!recordOf(payload) || !recordOf(capture) || payload.hook_event_name !== 'PreToolUse'
1069
+ || !['Agent', 'Task'].includes(payload.tool_name) || !recordOf(payload.tool_input)) {
1070
+ return outlineDispatchUnresolved(null, 'dispatch-event-invalid');
1071
+ }
1072
+ parsed = parseOutlineDispatchPrompt(payload.tool_input.prompt);
1073
+ if (!parsed) return outlineDispatchUnresolved(null, 'dispatch-marker-invalid');
1074
+ if (capture.tool_use_id !== payload.tool_use_id || !textOf(payload.tool_use_id)
1075
+ || capture.session_id !== payload.session_id || !textOf(payload.session_id)) {
1076
+ return outlineDispatchUnresolved(parsed, 'dispatch-correlation-missing');
1077
+ }
1078
+ const failedPredecessor = (Array.isArray(captures) ? captures : []).some(item => recordOf(item)
1079
+ && item.event === 'PreToolUse' && item.session_id === payload.session_id
1080
+ && item.outlineDispatch?.state === 'unresolved'
1081
+ && (item.tool_use_id === payload.tool_use_id
1082
+ || (item.outlineDispatch.taskId === parsed.taskId && item.outlineDispatch.attemptId === parsed.attemptId
1083
+ && item.outlineDispatch.tokenDigest === parsed.tokenDigest)));
1084
+ if (failedPredecessor) return outlineDispatchUnresolved(parsed, 'attempt-already-unresolved');
1085
+ const validated = validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspaceRoot,
1086
+ captures, expectedPin: fixtureExpectedPin });
1087
+ if (validated.state !== 'accepted') return outlineDispatchUnresolved(parsed, validated.reason || 'dispatch-validation-unresolved');
1088
+ const seamDir = path.join(validated.attemptDir, 'seams');
1089
+ const tokenDir = path.join(validated.attemptDir, 'tokens');
1090
+ const seamName = `${outlineSha(String(payload.tool_use_id))}.json`;
1091
+ const seamFile = path.join(seamDir, seamName);
1092
+ const tokenFile = path.join(tokenDir, `${parsed.tokenDigest}.json`);
1093
+ const priorSeam = readOutlineJson(seamFile);
1094
+ if (priorSeam.state === 'accepted') {
1095
+ const prior = priorSeam.value;
1096
+ if (prior.toolUseId === payload.tool_use_id && prior.dispatchInputDigest === parsed.dispatchInputDigest
1097
+ && prior.taskId === parsed.taskId && prior.attemptId === parsed.attemptId) {
1098
+ return { ...prior, schema: OUTLINE_DISPATCH_SCHEMA, duplicate: true };
1099
+ }
1100
+ return outlineDispatchUnresolved(parsed, 'dispatch-input-changed');
1101
+ }
1102
+ const priorToken = readOutlineJson(tokenFile);
1103
+ if (priorToken.state === 'accepted') {
1104
+ const token = priorToken.value;
1105
+ if (token.toolUseId !== payload.tool_use_id) return outlineDispatchUnresolved(parsed, 'draft-token-reused');
1106
+ return outlineDispatchUnresolved(parsed, 'seam-missing-after-token-consumption');
1107
+ }
1108
+ const seam = {
1109
+ schema: OUTLINE_SEAM_SCHEMA, state: 'accepted', captureRef: capture.ref, toolUseId: payload.tool_use_id,
1110
+ taskId: parsed.taskId, attemptId: parsed.attemptId, workerRoot: validated.markerRoot,
1111
+ tokenDigest: parsed.tokenDigest, promptDigest: parsed.promptDigest, blockDigest: parsed.blockDigest,
1112
+ dispatchInputDigest: parsed.dispatchInputDigest, handoffDigest: validated.handoffDigest,
1113
+ reviewCaptureRef: validated.review.review.ref, reviewCaptureDigest: validated.review.captureDigest,
1114
+ reviewDigest: validated.review.reviewDigest, inventoryPinDigest: validated.inventoryPinDigest,
1115
+ writerLeaseDigest: validated.leaseDigest, writerLeaseId: validated.leaseId,
1116
+ coordinatorPrincipal: payload.session_id, reviewerPrincipal: validated.review.reviewerPrincipal,
1117
+ worktree: validated.worktree, validationReasons: [],
1118
+ };
1119
+ const sealFile = path.join(validated.attemptDir, 'handoff-seal.json');
1120
+ const existingSeal = readOutlineJson(sealFile);
1121
+ if (existingSeal.state === 'accepted') {
1122
+ if (existingSeal.value.taskId !== parsed.taskId || existingSeal.value.attemptId !== parsed.attemptId
1123
+ || existingSeal.value.handoffDigest !== validated.handoffDigest) {
1124
+ return outlineDispatchUnresolved(parsed, 'handoff-overwritten-after-dispatch');
1125
+ }
1126
+ return outlineDispatchUnresolved(parsed, 'handoff-seal-already-consumed');
1127
+ }
1128
+ const seal = { schema: 'harness.outline-handoff-seal.v1', taskId: parsed.taskId,
1129
+ attemptId: parsed.attemptId, handoffDigest: validated.handoffDigest };
1130
+ try { fs.writeFileSync(sealFile, JSON.stringify(seal, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' }); }
1131
+ catch { return outlineDispatchUnresolved(parsed, 'handoff-seal-persistence-failed'); }
1132
+ fs.mkdirSync(tokenDir, { recursive: true });
1133
+ fs.mkdirSync(seamDir, { recursive: true });
1134
+ const token = { schema: 'harness.outline-token.v1', taskId: parsed.taskId, attemptId: parsed.attemptId,
1135
+ tokenDigest: parsed.tokenDigest, toolUseId: payload.tool_use_id, dispatchInputDigest: parsed.dispatchInputDigest,
1136
+ handoffDigest: validated.handoffDigest };
1137
+ try {
1138
+ fs.writeFileSync(tokenFile, JSON.stringify(token, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
1139
+ } catch (error) {
1140
+ if (error.code === 'EEXIST') return outlineDispatchUnresolved(parsed, 'draft-token-reused');
1141
+ return outlineDispatchUnresolved(parsed, 'token-consumption-failed');
1142
+ }
1143
+ try {
1144
+ fs.writeFileSync(seamFile, JSON.stringify(seam, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
1145
+ } catch {
1146
+ return outlineDispatchUnresolved(parsed, 'seam-persistence-failed');
1147
+ }
1148
+ return { ...seam, schema: OUTLINE_DISPATCH_SCHEMA,
1149
+ seamRef: path.relative(sessionDir, seamFile).split(path.sep).join('/') };
1150
+ } catch (error) {
1151
+ return outlineDispatchUnresolved(parsed, error.message === 'outline-git-unavailable' ? error.message : 'dispatch-validation-failed');
1152
+ }
1153
+ }
1154
+
1155
+ function validateOutlineCaptureQualification(input, expectedHost = HOST_ID) {
1156
+ if (!recordOf(input) || input.hostId !== expectedHost || expectedHost !== HOST_ID) return { open: false, reason: 'unsupported-host' };
1157
+ const supplied = input.qualification;
1158
+ if (recordOf(supplied) && supplied.faked === true && supplied.source === 'fake-test-fixture'
1159
+ && supplied.toolUseIdPopulated === true && supplied.agentIdPopulated === true) {
1160
+ return supplied.outlineDispatchCaptureVerified === true
1161
+ ? { open: true, source: 'dependency-injection' }
1162
+ : { open: false, reason: 'outline-live-capture-unqualified' };
1163
+ }
1164
+ try {
1165
+ const qualificationError = validateQualificationExport(expectedHost);
1166
+ if (qualificationError) return { open: false, reason: qualificationError };
1167
+ const exported = JSON.parse(fs.readFileSync(path.join(__dirname, EXPORT_FILE), 'utf8'));
1168
+ const mapping = exported.outlineDispatchCapture;
1169
+ const source = exported.sanitizedCapture;
1170
+ const records = fs.readFileSync(path.join(__dirname, source.file), 'utf8').split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
1171
+ if (!outlineCaptureMappingValid(mapping, records)) return { open: false, reason: 'outline-live-capture-unqualified' };
1172
+ return { open: true, source: 'validated-export', captureFile: source.file };
1173
+ } catch (error) {
1174
+ return { open: false, reason: `outline-live-capture-unqualified:${error.code || 'invalid'}` };
1175
+ }
1176
+ }
1177
+
1178
+ function outlineCaptureMappingValid(mapping, records) {
1179
+ if (!recordOf(mapping) || captureJson(mapping) !== captureJson(OUTLINE_CAPTURE_MAPPING) || !Array.isArray(records)) return false;
1180
+ const pres = records.filter(item => recordOf(item) && item.event === 'PreToolUse'
1181
+ && ['Task', 'Agent'].includes(item.tool_name) && recordOf(item.tool_input)
1182
+ && typeof item.tool_input.prompt === 'string' && textOf(item.session_id) && textOf(item.tool_use_id));
1183
+ return pres.some(pre => correlateOutlineDispatch(records, pre).state === 'accepted');
1184
+ }
1185
+
1186
+ function outlineCorrelationFailure(reason) { return { state: 'unresolved', reason }; }
1187
+
1188
+ function outlineDispatchKeyMatches(item, pre) {
1189
+ return recordOf(item) && item.session_id === pre.session_id && item.tool_use_id === pre.tool_use_id;
1190
+ }
1191
+
1192
+ function outlineResultAgentId(result) {
1193
+ if (recordOf(result.tool_response)) {
1194
+ const observed = result.tool_response.agentId;
1195
+ if (textOf(result.agent_id) && textOf(observed) && result.agent_id !== observed) {
1196
+ return { state: 'unresolved', reason: 'outline-worker-result-agent-id-conflict' };
1197
+ }
1198
+ return { state: textOf(observed) ? 'accepted' : 'unresolved', agentId: observed,
1199
+ ...(textOf(observed) ? {} : { reason: 'outline-worker-result-agent-id-missing' }) };
1200
+ }
1201
+ return textOf(result.agent_id)
1202
+ ? { state: 'accepted', agentId: result.agent_id }
1203
+ : { state: 'unresolved', reason: 'outline-worker-result-agent-id-missing' };
1204
+ }
1205
+
1206
+ function outlineResultStatus(result) {
1207
+ if (recordOf(result.tool_response)) return Object.prototype.hasOwnProperty.call(result.tool_response, 'status')
1208
+ ? { present: true, value: result.tool_response.status } : { present: false };
1209
+ return Object.prototype.hasOwnProperty.call(result, 'tool_response_status')
1210
+ ? { present: true, value: result.tool_response_status } : { present: false };
1211
+ }
1212
+
1213
+ function correlateOutlineDispatch(records, anchor, expectedWorkerRoot) {
1214
+ if (!Array.isArray(records)) return outlineCorrelationFailure('outline-capture-records-missing');
1215
+ if (!recordOf(anchor) || anchor.event !== 'PreToolUse' || !['Task', 'Agent'].includes(anchor.tool_name)) {
1216
+ return outlineCorrelationFailure('outline-dispatch-anchor-invalid');
1217
+ }
1218
+ if (!textOf(anchor.session_id) || !textOf(anchor.tool_use_id)) return outlineCorrelationFailure('outline-dispatch-key-missing');
1219
+ const pres = records.filter(item => recordOf(item) && item.event === 'PreToolUse'
1220
+ && ['Task', 'Agent'].includes(item.tool_name) && outlineDispatchKeyMatches(item, anchor));
1221
+ if (pres.length !== 1) return outlineCorrelationFailure('outline-dispatch-anchor-missing-or-ambiguous');
1222
+ const pre = pres[0];
1223
+ let workerRoot = expectedWorkerRoot;
1224
+ if (recordOf(pre.tool_input)) {
1225
+ const marker = parseOutlineDispatchPrompt(pre.tool_input.prompt);
1226
+ if (!marker) return outlineCorrelationFailure('outline-dispatch-marker-invalid');
1227
+ if (workerRoot !== undefined && !samePathKey(marker.workerRoot, workerRoot)) {
1228
+ return outlineCorrelationFailure('outline-dispatch-worker-root-mismatch');
1229
+ }
1230
+ workerRoot = marker.workerRoot;
1231
+ if (!Object.prototype.hasOwnProperty.call(pre.tool_input, 'run_in_background')) {
1232
+ return outlineCorrelationFailure('outline-dispatch-background-unobserved');
1233
+ }
1234
+ if (pre.tool_input.run_in_background !== false) {
1235
+ return outlineCorrelationFailure('outline-dispatch-background-enabled');
1236
+ }
1237
+ if (Object.prototype.hasOwnProperty.call(pre.tool_input, 'isolation')) {
1238
+ return outlineCorrelationFailure('outline-dispatch-isolation-unsupported');
1239
+ }
1240
+ } else {
1241
+ if (!recordOf(pre.outlineDispatch) || pre.outlineDispatch.state !== 'accepted' || !textOf(pre.outlineDispatch.workerRoot)) {
1242
+ return outlineCorrelationFailure('outline-dispatch-marker-unobserved');
1243
+ }
1244
+ if (workerRoot !== undefined && !samePathKey(pre.outlineDispatch.workerRoot, workerRoot)) {
1245
+ return outlineCorrelationFailure('outline-dispatch-worker-root-mismatch');
1246
+ }
1247
+ workerRoot = pre.outlineDispatch.workerRoot;
1248
+ if (!Object.prototype.hasOwnProperty.call(pre, 'run_in_background')) {
1249
+ return outlineCorrelationFailure('outline-dispatch-background-unobserved');
1250
+ }
1251
+ if (pre.run_in_background !== false) return outlineCorrelationFailure('outline-dispatch-background-enabled');
1252
+ if (!Object.prototype.hasOwnProperty.call(pre, 'isolationSet')) {
1253
+ return outlineCorrelationFailure('outline-dispatch-isolation-unobserved');
1254
+ }
1255
+ if (pre.isolationSet !== false) return outlineCorrelationFailure('outline-dispatch-isolation-unsupported');
1256
+ }
1257
+ if (!textOf(workerRoot) || !path.isAbsolute(workerRoot)) return outlineCorrelationFailure('outline-dispatch-worker-root-unobserved');
1258
+
1259
+ const failures = records.filter(item => outlineDispatchKeyMatches(item, pre) && item.event === 'PostToolUseFailure');
1260
+ if (failures.length) return outlineCorrelationFailure('outline-worker-result-failed');
1261
+ const results = records.filter(item => outlineDispatchKeyMatches(item, pre) && item.event === 'PostToolUse'
1262
+ && ['Task', 'Agent'].includes(item.tool_name));
1263
+ if (results.length !== 1) return outlineCorrelationFailure('outline-worker-result-missing-or-ambiguous');
1264
+ const result = results[0];
1265
+ const status = outlineResultStatus(result);
1266
+ if (!status.present || status.value === undefined) return outlineCorrelationFailure('outline-worker-result-status-missing');
1267
+ if (status.value === 'async_launched') return outlineCorrelationFailure('outline-worker-result-async');
1268
+ if (status.value !== 'completed') return outlineCorrelationFailure('outline-worker-result-failed');
1269
+ const observedAgent = outlineResultAgentId(result);
1270
+ if (observedAgent.state !== 'accepted') return outlineCorrelationFailure(observedAgent.reason);
1271
+ const agentId = observedAgent.agentId;
1272
+ const sameAgentResults = records.filter(item => recordOf(item) && item.event === 'PostToolUse'
1273
+ && ['Task', 'Agent'].includes(item.tool_name) && item.session_id === pre.session_id
1274
+ && outlineResultAgentId(item).state === 'accepted' && outlineResultAgentId(item).agentId === agentId);
1275
+ if (sameAgentResults.length !== 1) return outlineCorrelationFailure('outline-worker-agent-id-replayed');
1276
+
1277
+ const starts = records.filter(item => recordOf(item) && item.event === 'SubagentStart'
1278
+ && item.session_id === pre.session_id && item.agent_id === agentId);
1279
+ if (starts.length !== 1) return outlineCorrelationFailure('outline-worker-start-missing-or-ambiguous');
1280
+ const stops = records.filter(item => recordOf(item) && item.event === 'SubagentStop'
1281
+ && item.session_id === pre.session_id && item.agent_id === agentId);
1282
+ if (stops.length !== 1) return outlineCorrelationFailure('outline-worker-stop-missing-or-ambiguous');
1283
+ const start = starts[0];
1284
+ const stop = stops[0];
1285
+ const preIndex = records.indexOf(pre);
1286
+ const startIndex = records.indexOf(start);
1287
+ const stopIndex = records.indexOf(stop);
1288
+ if (preIndex < 0 || startIndex < 0 || stopIndex < 0) return outlineCorrelationFailure('outline-worker-journal-position-unavailable');
1289
+ if (startIndex <= preIndex) return outlineCorrelationFailure('outline-worker-start-before-dispatch');
1290
+ if (stopIndex <= startIndex) return outlineCorrelationFailure('outline-worker-stop-before-start');
1291
+ for (const workerEvent of [start, stop]) {
1292
+ if (!textOf(workerEvent.cwd) || !path.isAbsolute(workerEvent.cwd)) {
1293
+ return outlineCorrelationFailure('outline-worker-root-unobserved');
1294
+ }
1295
+ if (!samePathKey(workerEvent.cwd, workerRoot)) return outlineCorrelationFailure('outline-worker-root-mismatch');
1296
+ }
1297
+ return { state: 'accepted', pre, result, start, stop, workerRoot, agentId };
1298
+ }
1299
+
1300
+ function verifyOutlineMergeCorrespondence({ root, outlineCommit, draftCommit, mergedCommit, targetPaths, record } = {}) {
1301
+ if (!recordOf(record) || !textOf(root) || !Array.isArray(targetPaths) || !targetPaths.length
1302
+ || record.outlineCommit !== outlineCommit || record.draftCommit !== draftCommit || record.mergedCommit !== mergedCommit) {
1303
+ return { state: 'unresolved', reason: 'merge-correspondence-missing-or-mismatched' };
1304
+ }
1305
+ try {
1306
+ for (const commit of [outlineCommit, draftCommit, mergedCommit]) {
1307
+ if (outlineGit(root, ['rev-parse', '--verify', `${commit}^{commit}`]).trim() !== commit) {
1308
+ return { state: 'unresolved', reason: 'merge-object-missing' };
1309
+ }
1310
+ }
1311
+ if (outlineCommit === draftCommit || outlineGit(root, ['merge-base', '--is-ancestor', outlineCommit, draftCommit]) !== '') {
1312
+ return { state: 'unresolved', reason: 'merge-pre-draft-ancestry-invalid' };
1313
+ }
1314
+ const recorded = record.targetBlobs;
1315
+ if (!recordOf(recorded) || captureJson(Object.keys(recorded).sort(compareKeys)) !== captureJson([...targetPaths].sort(compareKeys))) {
1316
+ return { state: 'unresolved', reason: 'merge-target-correspondence-invalid' };
1317
+ }
1318
+ let changed = false;
1319
+ for (const target of targetPaths) {
1320
+ const draft = outlineGit(root, ['rev-parse', `${draftCommit}:${target}`]).trim();
1321
+ const merged = outlineGit(root, ['rev-parse', `${mergedCommit}:${target}`]).trim();
1322
+ if (recorded[target]?.draft !== draft || recorded[target]?.merged !== merged) {
1323
+ return { state: 'unresolved', reason: 'merge-target-correspondence-invalid' };
1324
+ }
1325
+ changed ||= draft !== merged;
1326
+ }
1327
+ return { state: changed ? 'changed' : 'accepted', draftCommit, mergedCommit, targetPaths: [...targetPaths], targetBlobs: recorded };
1328
+ } catch (error) {
1329
+ return { state: 'unresolved', reason: error.message || 'merge-object-missing' };
1330
+ }
1331
+ }
1332
+
1333
+ function outlineAncestry(root, ancestor, descendant) {
1334
+ try {
1335
+ outlineGit(root, ['merge-base', '--is-ancestor', ancestor, descendant]);
1336
+ return true;
1337
+ } catch { return false; }
1338
+ }
1339
+
1340
+ function possibleWriterCapture(item) {
1341
+ if (!recordOf(item)) return false;
1342
+ if (['Write', 'Edit', 'apply_patch'].includes(item.tool_name)) return true;
1343
+ if (!['Bash', 'PowerShell', 'exec_command'].includes(item.tool_name)) return false;
1344
+ return !['read-only', 'worktree-setup'].includes(classifyShell(item.command));
1345
+ }
1346
+
1347
+ function sameCanonicalPath(left, right) {
1348
+ try {
1349
+ fs.statSync(left);
1350
+ fs.statSync(right);
1351
+ return key(left) === key(right);
1352
+ }
1353
+ catch { return false; }
1354
+ }
1355
+
1356
+ function samePathKey(left, right) {
1357
+ try { return key(left) === key(right); }
1358
+ catch { return false; }
1359
+ }
1360
+
1361
+ function sameOutlineTaskBinding(left, right) {
1362
+ if (!recordOf(left) || !recordOf(right)) return false;
1363
+ const { baselineRoot: leftRoot, ...leftIdentity } = left;
1364
+ const { baselineRoot: rightRoot, ...rightIdentity } = right;
1365
+ return captureJson(leftIdentity) === captureJson(rightIdentity) && sameCanonicalPath(leftRoot, rightRoot);
1366
+ }
1367
+
1368
+ function resolveOutlineEvidence({ safeInput, capture, captures, request, originRef } = {}) {
1369
+ const fail = (reason, status = 'rejected') => ({ status, diagnostics: [diagnostic(reason, { reason })] });
1370
+ try {
1371
+ const cap = capture;
1372
+ const sessionDir = safeInput.sessionDir;
1373
+ const dispatch = cap?.outlineDispatch;
1374
+ if (!recordOf(dispatch) || dispatch.schema !== OUTLINE_DISPATCH_SCHEMA || dispatch.state !== 'accepted'
1375
+ || !textOf(dispatch.seamRef) || dispatch.captureRef !== cap.ref || dispatch.toolUseId !== cap.tool_use_id) {
1376
+ return fail('outline-pre-dispatch-seam-unresolved');
1377
+ }
1378
+ if (cap.event !== 'PreToolUse' || !['Task', 'Agent'].includes(cap.tool_name)
1379
+ || cap.session_id !== safeInput.sessionId || cap.observedIdentity?.taskPin?.taskId !== safeInput.taskId) {
1380
+ return fail('outline-dispatch-identity-mismatch');
1381
+ }
1382
+ const taskState = outlineTaskState(sessionDir);
1383
+ if (taskState.state !== 'accepted' || taskState.taskBinding.taskId !== safeInput.taskId
1384
+ || (recordOf(safeInput.taskBinding) && !sameOutlineTaskBinding(taskState.taskBinding, safeInput.taskBinding))
1385
+ || !sameOutlineTaskBinding(cap.observedIdentity.taskPin, taskState.taskBinding)
1386
+ || cap.observedIdentity.baselinePin?.commit !== taskState.baseline.commit
1387
+ || !sameCanonicalPath(cap.observedIdentity.baselinePin?.root, taskState.baseline.root)) {
1388
+ return fail('outline-task-binding-mismatch');
1389
+ }
1390
+ const attempt = resolveOutlineAttempt({ sessionDir, taskId: dispatch.taskId, attemptId: dispatch.attemptId });
1391
+ if (attempt.state !== 'accepted') return fail(attempt.reason || 'outline-attempt-unresolved');
1392
+ if (attempt.handoffDigest !== dispatch.handoffDigest) return fail('outline-handoff-changed-after-dispatch');
1393
+ const seamRef = String(dispatch.seamRef).replace(/\\/g, '/');
1394
+ if (path.isAbsolute(seamRef) || seamRef.split('/').some(part => !part || part === '.' || part === '..')) {
1395
+ return fail('outline-seam-reference-invalid');
1396
+ }
1397
+ const seamFile = path.join(sessionDir, ...seamRef.split('/'));
1398
+ const seamLoaded = readOutlineJson(seamFile);
1399
+ if (seamLoaded.state !== 'accepted') return fail('outline-seam-missing');
1400
+ const seam = seamLoaded.value;
1401
+ if (seam.schema !== OUTLINE_SEAM_SCHEMA || seam.state !== 'accepted' || seam.captureRef !== cap.ref
1402
+ || seam.toolUseId !== cap.tool_use_id || seam.taskId !== dispatch.taskId || seam.attemptId !== dispatch.attemptId
1403
+ || seam.dispatchInputDigest !== dispatch.dispatchInputDigest || seam.handoffDigest !== attempt.handoffDigest
1404
+ || seam.tokenDigest !== dispatch.tokenDigest) return fail('outline-seam-mismatch');
1405
+ const sameAttempt = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1406
+ && item.event === 'PreToolUse' && item.outlineDispatch?.taskId === dispatch.taskId
1407
+ && item.outlineDispatch?.attemptId === dispatch.attemptId && item.outlineDispatch?.tokenDigest === dispatch.tokenDigest);
1408
+ for (const other of sameAttempt) {
1409
+ const evidence = other.outlineDispatch;
1410
+ if (evidence.state !== 'accepted' || evidence.dispatchInputDigest !== dispatch.dispatchInputDigest
1411
+ || other.tool_use_id !== cap.tool_use_id) return fail('outline-dispatch-replay-or-conflict');
1412
+ }
1413
+ const sameToolUse = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1414
+ && item.event === 'PreToolUse' && item.tool_use_id === cap.tool_use_id);
1415
+ if (sameToolUse.some(item => item.ref !== cap.ref && (item.outlineDispatch?.state !== 'accepted'
1416
+ || item.outlineDispatch.dispatchInputDigest !== dispatch.dispatchInputDigest))) return fail('outline-tool-use-id-replayed');
1417
+ const handoff = attempt.handoff;
1418
+ const targetPaths = Array.isArray(handoff.targetPaths) ? handoff.targetPaths : [];
1419
+ const expectedPin = recordOf(safeInput.qualification) && safeInput.qualification.faked === true
1420
+ && recordOf(safeInput.outlineFixture?.expectedPin) ? safeInput.outlineFixture.expectedPin : OUTLINE_INVENTORY_PIN;
1421
+ const inventoryRoot = path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', 'inventory');
1422
+ const pinFile = path.join(inventoryRoot, 'inventory-pin.json');
1423
+ const pinLoaded = readOutlineJson(pinFile);
1424
+ if (pinLoaded.state !== 'accepted' || outlineSha(pinLoaded.bytes) !== seam.inventoryPinDigest) return fail('outline-inventory-pin-changed');
1425
+ let inventoryBytes;
1426
+ try { inventoryBytes = fs.readFileSync(path.join(inventoryRoot, expectedPin.retainedArtifactPath.split('/').pop())); }
1427
+ catch { return fail('outline-inventory-bytes-missing'); }
1428
+ const inventory = validateOutlineInventoryPin({ bytes: inventoryBytes, pin: pinLoaded.value, expectedPin });
1429
+ if (inventory.state !== 'accepted') return fail(inventory.reason || 'outline-inventory-unresolved');
1430
+ const outlineDigests = {};
1431
+ const reviewRows = {};
1432
+ for (const target of targetPaths) {
1433
+ const bytes = outlineGit(seam.worktree.root, ['show', `${handoff.outline.commit}:${target}`], true);
1434
+ outlineDigests[target] = outlineSha(bytes);
1435
+ const citation = parseOutlineCitation(bytes, target);
1436
+ if (citation.state !== 'accepted') return fail(citation.reason || 'outline-citation-invalid');
1437
+ const row = resolveInventoryRow(inventory, citation.rowId);
1438
+ if (row.state !== 'accepted' || row.row.domain !== citation.domain || citation.domain !== handoff.inventory?.domain
1439
+ || citation.rowId !== handoff.inventory?.rowId) return fail(row.reason || 'outline-inventory-binding-mismatch');
1440
+ reviewRows[target] = row.row;
1441
+ }
1442
+ const review = validateOutlineReview({ handoff, attemptDir: attempt.attemptDir, captures, targetPaths,
1443
+ outlineDigests, inventory, sessionId: safeInput.sessionId });
1444
+ if (review.state !== 'accepted' || review.captureDigest !== seam.reviewCaptureDigest
1445
+ || review.reviewDigest !== seam.reviewDigest || review.reviewerPrincipal !== seam.reviewerPrincipal) {
1446
+ return fail(review.reason || 'outline-review-changed-after-dispatch');
1447
+ }
1448
+ const correlation = correlateOutlineDispatch(captures, cap, seam.workerRoot);
1449
+ if (correlation.state !== 'accepted') return fail(correlation.reason);
1450
+ const worker = correlation.start;
1451
+ const workerRoot = correlation.workerRoot;
1452
+ if (review.reviewerPrincipal === worker.agent_id) return fail('outline-reviewer-is-draft-worker');
1453
+ const principals = { author: safeInput.sessionId, executors: journalExecutors(captures, safeInput.sessionId) };
1454
+ if (!reviewRecordIndependent(review.review, captures, principals)) return fail('outline-review-not-independent');
1455
+ const competingStarts = (Array.isArray(captures) ? captures : []).filter(item => recordOf(item)
1456
+ && item.event === 'SubagentStart' && item.ref !== worker.ref && sameCanonicalPath(item.cwd, workerRoot));
1457
+ if (competingStarts.length) return fail('outline-competing-writer-start');
1458
+ const unattributedWriters = (Array.isArray(captures) ? captures : []).filter(item => possibleWriterCapture(item)
1459
+ && (item.agent_id !== worker.agent_id || !sameCanonicalPath(item.cwd, workerRoot)));
1460
+ if (unattributedWriters.length) return fail('outline-writer-activity-ambiguous');
1461
+
1462
+ let candidateRoot;
1463
+ try { candidateRoot = canonicalOutlineRoot(safeInput.workspaceRoot); }
1464
+ catch { return fail('outline-candidate-root-unavailable'); }
1465
+ const baseline = taskState.baseline;
1466
+ const outlineCommit = handoff.outline.commit;
1467
+ let finalCommit;
1468
+ let finalTree;
1469
+ try {
1470
+ finalCommit = outlineGit(candidateRoot, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
1471
+ finalTree = outlineGit(candidateRoot, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
1472
+ outlineGit(candidateRoot, ['rev-parse', '--verify', `${outlineCommit}^{commit}`]);
1473
+ outlineGit(candidateRoot, ['rev-parse', '--verify', `${baseline.commit}^{commit}`]);
1474
+ } catch { return fail('outline-final-git-object-unavailable'); }
1475
+ if (finalCommit === outlineCommit) return fail('outline-final-draft-missing', 'unavailable');
1476
+ if (!outlineAncestry(candidateRoot, baseline.commit, outlineCommit)) return fail('outline-B-to-O-ancestry-invalid');
1477
+ let merge = { state: 'accepted', draftCommit: finalCommit, mergedCommit: finalCommit, targetPaths };
1478
+ if (!outlineAncestry(candidateRoot, outlineCommit, finalCommit)) {
1479
+ const mergeFile = path.join(attempt.attemptDir, 'merge-correspondence.json');
1480
+ const mergeLoaded = readOutlineJson(mergeFile);
1481
+ if (mergeLoaded.state !== 'accepted') return fail('outline-merge-correspondence-unavailable', 'unavailable');
1482
+ const correspondence = mergeLoaded.value;
1483
+ if (correspondence.taskId !== dispatch.taskId || correspondence.attemptId !== dispatch.attemptId
1484
+ || correspondence.baselineCommit !== baseline.commit) return fail('outline-merge-correspondence-mismatch');
1485
+ merge = verifyOutlineMergeCorrespondence({ root: candidateRoot, outlineCommit,
1486
+ draftCommit: correspondence.draftCommit, mergedCommit: finalCommit, targetPaths, record: correspondence });
1487
+ if (!['accepted', 'changed'].includes(merge.state)) return fail(merge.reason || 'outline-merge-correspondence-invalid', 'unavailable');
1488
+ }
1489
+ const draftArtifactDigests = {};
1490
+ const draftBlobOids = {};
1491
+ for (const target of targetPaths) {
1492
+ const atBaseline = outlineGit(candidateRoot, ['ls-tree', '-r', '--name-only', baseline.commit, '--', target]).trim();
1493
+ if (atBaseline.split('\n').filter(Boolean).includes(target)) return fail('outline-target-existed-at-B');
1494
+ let candidateBytes;
1495
+ let commitBytes;
1496
+ try {
1497
+ candidateBytes = fs.readFileSync(path.join(candidateRoot, ...target.split('/')));
1498
+ commitBytes = outlineGit(candidateRoot, ['show', `${finalCommit}:${target}`], true);
1499
+ } catch { return fail('outline-final-target-missing'); }
1500
+ if (!Buffer.from(candidateBytes).equals(commitBytes)) return fail('outline-final-working-tree-not-D');
1501
+ draftArtifactDigests[target] = outlineSha(commitBytes);
1502
+ draftBlobOids[target] = outlineGit(candidateRoot, ['rev-parse', `${finalCommit}:${target}`]).trim();
1503
+ }
1504
+ const independentOf = [...new Set([safeInput.sessionId, ...principals.executors])].filter(value => textOf(value) && value !== review.reviewerPrincipal);
1505
+ const event = {
1506
+ eventId: 'claude-outline-' + digestText(captureJson({ handoffDigest: attempt.handoffDigest,
1507
+ seamDigest: seamLoadedDigest(seamFile), draftCommit: finalCommit })),
1508
+ producerId: 'claude-code-origin-provider', originRef, principalId: review.reviewerPrincipal,
1509
+ provenance: `claude-code-outline-capture:${cap.captureId}`, capability: 'outline-confirmation', kind: 'review',
1510
+ verdict: 'approve', summary: review.review.reviewSummary, independentOf,
1511
+ identity: recordOf(request.identity) ? JSON.parse(JSON.stringify(request.identity)) : null,
1512
+ paths: targetPaths, reviewedArtifactDigests: { ...outlineDigests,
1513
+ [review.inventoryPath]: inventory.artifactDigest }, draftArtifactDigests,
1514
+ outlineEvidence: {
1515
+ schema: 'harness.outline-confirmation.v1', state: 'validated', taskId: dispatch.taskId,
1516
+ attemptId: dispatch.attemptId, inventoryRowId: handoff.inventory.rowId, domain: handoff.inventory.domain,
1517
+ inventoryPinDigest: seam.inventoryPinDigest, inventoryArtifactDigest: inventory.artifactDigest,
1518
+ baselineCommit: baseline.commit, outlineCommit, outlineTree: handoff.outline.tree,
1519
+ outlineBlobOids: Object.fromEntries(targetPaths.map(target => [target, handoff.outline.targetBlobs[target].blobOid])),
1520
+ outlineArtifactDigests: outlineDigests, draftCommit: merge.draftCommit, mergedCommit: finalCommit,
1521
+ draftTree: finalTree, draftBlobOids, draftArtifactDigests, handoffDigest: attempt.handoffDigest,
1522
+ reviewCaptureRef: review.review.ref, reviewCaptureDigest: review.captureDigest,
1523
+ reviewDigest: review.reviewDigest, seamDigest: seamLoadedDigest(seamFile), dispatchInputDigest: seam.dispatchInputDigest,
1524
+ dispatchToolUseId: cap.tool_use_id, coordinatorPrincipal: safeInput.sessionId,
1525
+ reviewerPrincipal: review.reviewerPrincipal, workerPrincipal: worker.agent_id,
1526
+ workerRoot, writerLeaseDigest: seam.writerLeaseDigest,
1527
+ ancestry: 'strict-descendant', mergeCorrespondence: merge.state === 'accepted' && merge.draftCommit === finalCommit ? null : merge,
1528
+ targetPaths,
1529
+ },
1530
+ captured: { captureRef: cap.ref, toolUseId: cap.tool_use_id, seamDigest: seamLoadedDigest(seamFile) },
1531
+ };
1532
+ return { status: 'accepted', originRef, event,
1533
+ correlation: { toolCorrelationId: cap.tool_use_id, resultRef: cap.responseRef } };
1534
+ } catch (error) {
1535
+ return fail(error.message === 'outline-git-unavailable' ? 'outline-git-unavailable' : 'outline-evidence-unresolved', 'unavailable');
1536
+ }
1537
+ }
1538
+
1539
+ function seamLoadedDigest(file) {
1540
+ try { return outlineSha(fs.readFileSync(file)); } catch { return ''; }
1541
+ }
1542
+
602
1543
  // ---- Shared provider state ------------------------------------------------------
603
1544
 
604
1545
  function supportedHostCheck(input, expectedHost = HOST_ID) {
@@ -703,7 +1644,8 @@ const createClaudeOriginProvider = input => {
703
1644
  const observed = cap.observedIdentity;
704
1645
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
705
1646
  const sameWorkflow = observed.workflowId === safeInput.workflowId && observed.workflowRevision === safeInput.workflowRevision;
706
- const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit && observed.baselinePin.root === pin.root);
1647
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1648
+ && samePathKey(observed.baselinePin.root, pin.root));
707
1649
  if (!sameWorkflow || !samePin) {
708
1650
  return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
709
1651
  }
@@ -711,6 +1653,13 @@ const createClaudeOriginProvider = input => {
711
1653
  if (capability && !(recordOf(cap.observedIdentity))) {
712
1654
  return rejected('identity-missing', 'a doc capability needs the workflow identity observed at capture time');
713
1655
  }
1656
+ if (capability === 'outline-confirmation') {
1657
+ const outlineQualification = validateOutlineCaptureQualification(safeInput);
1658
+ if (!outlineQualification.open) {
1659
+ 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})`);
1660
+ }
1661
+ return resolveOutlineEvidence({ safeInput, capture: cap, captures, request, originRef });
1662
+ }
714
1663
  // Replay: identical host-event content re-presented under a new reference
715
1664
  // cannot become a new accepted event. The first recorded occurrence wins.
716
1665
  const signature = contentSignature(cap);
@@ -1029,7 +1978,8 @@ const createOpencodeOriginProvider = input => {
1029
1978
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
1030
1979
  if (!recordOf(observed)) return rejected('identity-missing', 'OpenCode capture lacks workflow identity observed at capture time');
1031
1980
  const sameWorkflow = observed.workflowId === safeInput.workflowId && observed.workflowRevision === safeInput.workflowRevision;
1032
- const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit && observed.baselinePin.root === pin.root);
1981
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1982
+ && samePathKey(observed.baselinePin.root, pin.root));
1033
1983
  if (!sameWorkflow || !samePin) return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
1034
1984
  const signature = opencodeContentSignature(cap);
1035
1985
  const firstOccurrence = captures.findIndex(item => recordOf(item) && opencodeContentSignature(item) === signature);
@@ -1099,6 +2049,8 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1099
2049
  if (event === 'PreToolUse' && PRE_CAPTURE_TOOLS.indexOf(payload.tool_name) === -1) return null;
1100
2050
  const toolInput = recordOf(payload.tool_input) ? payload.tool_input : {};
1101
2051
  const toolResponse = payload.tool_response;
2052
+ const outlinePre = event === 'PreToolUse' && ['Task', 'Agent'].includes(payload.tool_name);
2053
+ const outlineResult = event === 'PostToolUse' && ['Task', 'Agent'].includes(payload.tool_name);
1102
2054
  // Demonstrated alias mapping (doc: "Host result aliases such as agentId
1103
2055
  // require a demonstrated capture mapping to the same subagent identity"):
1104
2056
  // the PostToolUse Task/Agent result surface reports the subagent id as
@@ -1111,16 +2063,28 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1111
2063
  ref: captureId,
1112
2064
  event,
1113
2065
  command: typeof toolInput.command === 'string' ? toolInput.command : '',
1114
- cwd: typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : (typeof workspaceRoot === 'string' && workspaceRoot ? workspaceRoot : ''),
1115
2066
  tool_use_id: typeof payload.tool_use_id === 'string' ? payload.tool_use_id : '',
1116
2067
  session_id: payload.session_id === undefined && payload.sessionId === undefined ? '' : String(payload.session_id !== undefined ? payload.session_id : payload.sessionId),
1117
2068
  responseDigest: captureDigest(toolResponse),
1118
2069
  responseRef: `host-payload/${event}/${captureId}`,
1119
2070
  };
2071
+ if (typeof payload.cwd === 'string' && payload.cwd) record.cwd = payload.cwd;
2072
+ else if (!['SubagentStart', 'SubagentStop'].includes(event) && typeof workspaceRoot === 'string' && workspaceRoot) record.cwd = workspaceRoot;
1120
2073
  if (payload.prompt_id !== undefined) record.prompt_id = payload.prompt_id;
2074
+ if (payload.parent_tool_use_id !== undefined) record.parent_tool_use_id = payload.parent_tool_use_id;
1121
2075
  // The launch tool name separates the Agent/Task result surface and the subagent launch from executed tools (F4/F5).
1122
2076
  if (typeof payload.tool_name === 'string') record.tool_name = payload.tool_name;
1123
- const agentId = payload.agent_id !== undefined ? payload.agent_id : responseAliasAgentId;
2077
+ if (outlinePre) {
2078
+ if (Object.prototype.hasOwnProperty.call(toolInput, 'run_in_background')) record.run_in_background = toolInput.run_in_background;
2079
+ record.isolationSet = Object.prototype.hasOwnProperty.call(toolInput, 'isolation');
2080
+ }
2081
+ if (outlineResult && recordOf(toolResponse) && Object.prototype.hasOwnProperty.call(toolResponse, 'status')) {
2082
+ record.tool_response_status = toolResponse.status;
2083
+ }
2084
+ const resultAgentConflict = outlineResult && textOf(payload.agent_id) && textOf(responseAliasAgentId)
2085
+ && payload.agent_id !== responseAliasAgentId;
2086
+ const agentId = outlineResult ? (resultAgentConflict ? undefined : responseAliasAgentId)
2087
+ : payload.agent_id !== undefined ? payload.agent_id : responseAliasAgentId;
1124
2088
  if (agentId !== undefined) {
1125
2089
  record.agent_id = agentId;
1126
2090
  if (payload.agent_type !== undefined) record.agent_type = payload.agent_type;
@@ -1252,7 +2216,8 @@ function readOpencodeCaptureView(sessionDir) {
1252
2216
  function observedIdentityFor(sessionDir) {
1253
2217
  try {
1254
2218
  const workflow = JSON.parse(fs.readFileSync(path.join(sessionDir, 'workflow-run.json'), 'utf8'));
1255
- return { workflowId: workflow.workflowId, workflowRevision: workflow.revision, baselinePin: workflow.docReconciliationBaseline || null };
2219
+ return { workflowId: workflow.workflowId, workflowRevision: workflow.revision,
2220
+ baselinePin: workflow.docReconciliationBaseline || null, taskPin: workflow.docReconciliationTask || null };
1256
2221
  } catch {
1257
2222
  return null;
1258
2223
  }
@@ -1276,5 +2241,8 @@ function reviewRecordIndependent(cap, captures, principals) {
1276
2241
  module.exports = { createClaudeOriginProvider, createOpencodeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
1277
2242
  registeredInstalledCheck: resolveDocCheckInvocation, installedDocCheck,
1278
2243
  fileDigest, SHIPPED_DOC_CHECK_DIGEST, trustRisk, journalExecutors, reviewRecordIndependent,
1279
- writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
1280
- _internals: { captureJson, captureDigest, digestText, canonicalCapture } };
2244
+ captureOutlineDispatch, writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
2245
+ _internals: { captureJson, captureDigest, digestText, canonicalCapture, parseOutlineDispatchPrompt,
2246
+ parseInventoryPinRows, validateOutlineInventoryPin, resolveInventoryRow, parseOutlineCitation, resolveOutlineAttempt,
2247
+ captureOutlineDispatch, resolveOutlineEvidence, verifyOutlineMergeCorrespondence, validateOutlineCaptureQualification,
2248
+ outlineCaptureMappingValid, correlateOutlineDispatch, OUTLINE_CAPTURE_MAPPING, OUTLINE_INVENTORY_PIN } };