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.
- package/.agents/harness-everything/project-docs.json +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/README.md +75 -0
- package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/capture.sanitized.jsonl +32 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +2 -2
- package/ci/mechanism-2ax-doc-lifecycle.test.js +2 -1
- package/ci/mechanism-325-3b-s2b-capture.test.js +6 -1
- package/ci/mechanism-325-f4-projections.test.js +11 -1
- package/ci/mechanism-prb-outline-review.test.js +1449 -0
- package/docs/adr/DOCS-001-project-docs-config.md +32 -0
- package/docs/adr/DOCS-002-adr-identity-supersession-index.md +34 -0
- package/docs/adr/DOCS-003-prd-revision-contract.md +29 -0
- package/docs/adr/DOCS-004-doc-change-step-type.md +28 -0
- package/docs/adr/DOCS-005-doc-obligations-evidence-producer.md +29 -0
- package/docs/adr/DOCS-006-reconciliation-pilot.md +50 -0
- package/docs/adr/DOCS-007-shared-doc-craft-derived-views.md +31 -0
- package/docs/adr/DOCS-008-obligation-abstraction-bounded-consumer.md +30 -0
- package/docs/doc-workflow-reconciliation.md +47 -1
- package/docs/governed-index.md +8 -0
- package/docs/platform-capabilities.md +2 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +55 -0
- package/hooks/hooks.json +1 -1
- package/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
- package/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
- package/hooks/scripts/state-persist.js +10 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +121 -9
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +128 -15
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +976 -8
- package/plugins/harness-everything/hooks/scripts/state-persist.js +10 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +55 -0
- 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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 } };
|