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.
- 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 +17 -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 +1265 -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 +30 -0
- 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 +861 -6
- 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 +861 -6
- 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,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
|
|
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
|
|
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,
|
|
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 } };
|