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