harness-everything 0.45.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 (38) 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 +17 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
  6. package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
  7. package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
  8. package/ci/mechanism-325-f4-projections.test.js +11 -1
  9. package/ci/mechanism-prb-outline-review.test.js +1265 -0
  10. package/docs/adr/DOCS-001-project-docs-config.md +32 -0
  11. package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
  12. package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
  13. package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
  14. package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
  15. package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
  16. package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
  17. package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
  18. package/docs/doc-workflow-reconciliation.md +30 -0
  19. package/docs/governed-index.md +8 -0
  20. package/docs/platform-capabilities.md +2 -0
  21. package/harness-everything/SKILL.md +1 -1
  22. package/harness-everything/scripts/doc-evidence.js +55 -0
  23. package/hooks/hooks.json +1 -1
  24. package/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  25. package/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  26. package/hooks/scripts/lib/doc-reconciliation-provider.js +861 -6
  27. package/hooks/scripts/state-persist.js +10 -0
  28. package/opencode-plugin/plugin.json +1 -1
  29. package/package.json +1 -1
  30. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  31. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
  32. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
  33. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +861 -6
  34. package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
  35. package/plugins/harness-everything/plugin.json +1 -1
  36. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  37. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
  38. 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,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']);
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_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}$/;
52
71
  const DOC_CHECK_SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh', 'fish']);
53
72
  const DOC_CHECK_LAUNCHERS = new Set(['node', 'nodejs', 'bun', 'deno', 'tsx', 'ts-node']);
54
73
  const DOC_CHECK_LAUNCHER_VALUE_FLAGS = new Set(['-r', '--require', '--import', '--loader', '--experimental-loader', '--inspect-port', '--input-type', '--conditions', '--env-file']);
@@ -599,6 +618,828 @@ function evaluateQualification(input, expectedHost = HOST_ID) {
599
618
  return { open: true, source: 'validated-export' };
600
619
  }
601
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
+
602
1443
  // ---- Shared provider state ------------------------------------------------------
603
1444
 
604
1445
  function supportedHostCheck(input, expectedHost = HOST_ID) {
@@ -703,7 +1544,8 @@ const createClaudeOriginProvider = input => {
703
1544
  const observed = cap.observedIdentity;
704
1545
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
705
1546
  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);
1547
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1548
+ && samePathKey(observed.baselinePin.root, pin.root));
707
1549
  if (!sameWorkflow || !samePin) {
708
1550
  return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
709
1551
  }
@@ -711,6 +1553,13 @@ const createClaudeOriginProvider = input => {
711
1553
  if (capability && !(recordOf(cap.observedIdentity))) {
712
1554
  return rejected('identity-missing', 'a doc capability needs the workflow identity observed at capture time');
713
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
+ }
714
1563
  // Replay: identical host-event content re-presented under a new reference
715
1564
  // cannot become a new accepted event. The first recorded occurrence wins.
716
1565
  const signature = contentSignature(cap);
@@ -1029,7 +1878,8 @@ const createOpencodeOriginProvider = input => {
1029
1878
  const pin = recordOf(safeInput.baseline) ? safeInput.baseline : null;
1030
1879
  if (!recordOf(observed)) return rejected('identity-missing', 'OpenCode capture lacks workflow identity observed at capture time');
1031
1880
  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);
1881
+ const samePin = !pin || (recordOf(observed.baselinePin) && observed.baselinePin.commit === pin.commit
1882
+ && samePathKey(observed.baselinePin.root, pin.root));
1033
1883
  if (!sameWorkflow || !samePin) return rejected('identity-mismatch', 'the observed workflow revision or baseline pin differs from the current one; stale evidence loses credit');
1034
1884
  const signature = opencodeContentSignature(cap);
1035
1885
  const firstOccurrence = captures.findIndex(item => recordOf(item) && opencodeContentSignature(item) === signature);
@@ -1118,6 +1968,7 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
1118
1968
  responseRef: `host-payload/${event}/${captureId}`,
1119
1969
  };
1120
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;
1121
1972
  // The launch tool name separates the Agent/Task result surface and the subagent launch from executed tools (F4/F5).
1122
1973
  if (typeof payload.tool_name === 'string') record.tool_name = payload.tool_name;
1123
1974
  const agentId = payload.agent_id !== undefined ? payload.agent_id : responseAliasAgentId;
@@ -1252,7 +2103,8 @@ function readOpencodeCaptureView(sessionDir) {
1252
2103
  function observedIdentityFor(sessionDir) {
1253
2104
  try {
1254
2105
  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 };
2106
+ return { workflowId: workflow.workflowId, workflowRevision: workflow.revision,
2107
+ baselinePin: workflow.docReconciliationBaseline || null, taskPin: workflow.docReconciliationTask || null };
1256
2108
  } catch {
1257
2109
  return null;
1258
2110
  }
@@ -1276,5 +2128,8 @@ function reviewRecordIndependent(cap, captures, principals) {
1276
2128
  module.exports = { createClaudeOriginProvider, createOpencodeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
1277
2129
  registeredInstalledCheck: resolveDocCheckInvocation, installedDocCheck,
1278
2130
  fileDigest, SHIPPED_DOC_CHECK_DIGEST, trustRisk, journalExecutors, reviewRecordIndependent,
1279
- writeImmutableEvent, rebuildJournalView, readCaptureView, readOpencodeCaptureView, observedIdentityFor,
1280
- _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 } };