harness-everything 0.48.0 → 0.49.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +20 -0
- package/ci/mechanism-prb-outline-review.test.js +965 -47
- package/docs/doc-workflow-reconciliation.md +47 -1
- package/hooks/scripts/lib/doc-reconciliation-provider.js +333 -9
- package/hooks/scripts/state-persist.js +5 -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-provider.js +333 -9
- package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -68,6 +68,7 @@ const OUTLINE_CAPTURE_MAPPING = Object.freeze({
|
|
|
68
68
|
const OUTLINE_SEAM_SCHEMA = 'harness.outline-dispatch-seam.v1';
|
|
69
69
|
const OUTLINE_HANDOFF_SCHEMA = 'harness.outline-handoff.v1';
|
|
70
70
|
const OUTLINE_LEASE_SCHEMA = 'harness.outline-writer-lease.v1';
|
|
71
|
+
const OUTLINE_WRITE_TOOLS = ['Write', 'Edit', 'apply_patch'];
|
|
71
72
|
const OUTLINE_INVENTORY_SOURCE = '/Users/daniel.chang/Desktop/Skills/Harness-everything/.worktrees/packets/adr-e1-2026-10-27/report-inv.md';
|
|
72
73
|
const OUTLINE_INVENTORY_SHA256 = '2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6';
|
|
73
74
|
const OUTLINE_INVENTORY_PIN = Object.freeze({
|
|
@@ -960,6 +961,47 @@ function outlineHandoffDirectory(sessionDir, taskId) {
|
|
|
960
961
|
return path.join(sessionDir, 'doc-reconciliation', 'outline-confirmation', taskId);
|
|
961
962
|
}
|
|
962
963
|
|
|
964
|
+
// #341 step 1: Harness records O's identity at each SubagentStart inside an outline task. A reviewer's launch record predates
|
|
965
|
+
// its review, so the identity it carries is the O the reviewer evaluated; the record is not taken from the reviewer's prose.
|
|
966
|
+
function captureOutlineStartIdentity({ sessionDir } = {}) {
|
|
967
|
+
const taskState = outlineTaskState(sessionDir);
|
|
968
|
+
if (taskState.state !== 'accepted') return null;
|
|
969
|
+
const root = taskState.baseline.root;
|
|
970
|
+
try {
|
|
971
|
+
const commit = outlineGit(root, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
|
|
972
|
+
const tree = outlineGit(root, ['rev-parse', '--verify', 'HEAD^{tree}']).trim();
|
|
973
|
+
const targetPaths = outlineGit(root, ['diff', '--name-only', '--no-renames', '-z', taskState.baseline.commit, commit])
|
|
974
|
+
.split('\0').filter(Boolean).sort(compareKeys);
|
|
975
|
+
const targetBlobs = {};
|
|
976
|
+
for (const target of targetPaths) {
|
|
977
|
+
targetBlobs[target] = { blobOid: outlineGit(root, ['rev-parse', `${commit}:${target}`]).trim(),
|
|
978
|
+
storedBytesSha256: outlineSha(outlineGit(root, ['show', `${commit}:${target}`], true)) };
|
|
979
|
+
}
|
|
980
|
+
return { state: 'accepted', identity: { baselineCommit: taskState.baseline.commit, outlineCommit: commit,
|
|
981
|
+
outlineTree: tree, targetPaths, targetBlobs } };
|
|
982
|
+
} catch {
|
|
983
|
+
return { state: 'unresolved', reason: 'outline-identity-unavailable' };
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// The reviewer has exactly one launch record, it precedes the review Stop, and its identity equals the handoff's O identity.
|
|
988
|
+
function validateOutlineIdentity({ handoff, captures } = {}) {
|
|
989
|
+
const refs = (Array.isArray(captures) ? captures : []).filter(recordOf);
|
|
990
|
+
const stop = refs.find(item => item.ref === handoff.reviewCaptureRef && item.event === 'SubagentStop');
|
|
991
|
+
if (!stop || !textOf(stop.agent_id)) return { state: 'unresolved', reason: 'outline-review-capture-missing-or-ambiguous' };
|
|
992
|
+
const starts = refs.filter(item => item.event === 'SubagentStart' && item.agent_id === stop.agent_id);
|
|
993
|
+
if (starts.length !== 1) return { state: 'unresolved', reason: 'outline-review-result-missing-or-ambiguous' };
|
|
994
|
+
if (refs.indexOf(starts[0]) > refs.indexOf(stop)) return { state: 'unresolved', reason: 'outline-identity-mismatch' };
|
|
995
|
+
const recorded = starts[0].outlineIdentity;
|
|
996
|
+
if (!recordOf(recorded) || recorded.state !== 'accepted' || !recordOf(recorded.identity)) {
|
|
997
|
+
return { state: 'unresolved', reason: 'outline-identity-unavailable' };
|
|
998
|
+
}
|
|
999
|
+
const current = { baselineCommit: handoff.baseline?.commit, outlineCommit: handoff.outline?.commit,
|
|
1000
|
+
outlineTree: handoff.outline?.tree, targetPaths: handoff.targetPaths, targetBlobs: handoff.outline?.targetBlobs };
|
|
1001
|
+
if (captureJson(recorded.identity) !== captureJson(current)) return { state: 'unresolved', reason: 'outline-identity-mismatch' };
|
|
1002
|
+
return { state: 'accepted', identityDigest: outlineSha(Buffer.from(captureJson(recorded.identity))) };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
963
1005
|
function validateOutlineReview({ handoff, attemptDir, captures, targetPaths, outlineDigests, inventory, sessionId } = {}) {
|
|
964
1006
|
const refs = Array.isArray(captures) ? captures : [];
|
|
965
1007
|
const matches = refs.filter(item => recordOf(item) && item.ref === handoff.reviewCaptureRef && item.event === 'SubagentStop');
|
|
@@ -1006,6 +1048,15 @@ function validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspa
|
|
|
1006
1048
|
|| handoff.baseline.tree !== baseline.tree || handoff.configDigest !== taskBinding.configDigest) {
|
|
1007
1049
|
return { state: 'unresolved', reason: 'handoff-identity-mismatch' };
|
|
1008
1050
|
}
|
|
1051
|
+
// O must change exactly the declared targets. Checked before the identity so an extra file reports its scope reason.
|
|
1052
|
+
const changedAtO = outlineGit(baseline.root, ['diff', '--name-only', '--no-renames', '-z', baseline.commit, handoff.outline.commit])
|
|
1053
|
+
.split('\0').filter(Boolean).sort(compareKeys);
|
|
1054
|
+
if (captureJson(changedAtO) !== captureJson(Array.isArray(handoff.targetPaths) ? [...handoff.targetPaths].sort(compareKeys) : null)) {
|
|
1055
|
+
return { state: 'unresolved', reason: 'outline-commit-scope-invalid' };
|
|
1056
|
+
}
|
|
1057
|
+
// Dispatch must still name the O the reviewer recorded: checked before the worktree, lease and review checks.
|
|
1058
|
+
const recordedIdentity = validateOutlineIdentity({ handoff, captures });
|
|
1059
|
+
if (recordedIdentity.state !== 'accepted') return recordedIdentity;
|
|
1009
1060
|
const markerRoot = canonicalOutlineRoot(parsed.workerRoot);
|
|
1010
1061
|
const declaredRoot = canonicalOutlineRoot(handoff.draftWorktreeRoot);
|
|
1011
1062
|
if (!sameCanonicalPath(markerRoot, declaredRoot)) return { state: 'unresolved', reason: 'worker-root-mismatch' };
|
|
@@ -1049,6 +1100,9 @@ function validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspa
|
|
|
1049
1100
|
for (const target of targetPaths) {
|
|
1050
1101
|
const atBaseline = outlineGit(baseline.root, ['ls-tree', '-r', '--name-only', baseline.commit, '--', target]).trim();
|
|
1051
1102
|
if (atBaseline.split('\n').filter(Boolean).includes(target)) return { state: 'unresolved', reason: 'outline-target-existed-at-B' };
|
|
1103
|
+
if (!outlineGitTreeHasRegularBlob(markerRoot, handoff.outline.tree, target)) {
|
|
1104
|
+
return { state: 'unresolved', reason: 'outline-target-object-not-regular' };
|
|
1105
|
+
}
|
|
1052
1106
|
const bytes = outlineGit(markerRoot, ['show', `${handoff.outline.commit}:${target}`], true);
|
|
1053
1107
|
const blobOid = outlineGit(markerRoot, ['rev-parse', `${handoff.outline.commit}:${target}`]).trim();
|
|
1054
1108
|
const expected = outlineBlobs[target];
|
|
@@ -1087,7 +1141,7 @@ function validateOutlineHandoffAtDispatch({ payload, parsed, sessionDir, workspa
|
|
|
1087
1141
|
return { state: 'accepted', taskBinding, baseline, handoff, handoffDigest, attemptDir, markerRoot,
|
|
1088
1142
|
worktree: { root: markerRoot, head, tree, statusDigest, clean: true },
|
|
1089
1143
|
inventory, inventoryPinDigest: outlineSha(pinLoaded.bytes), row: citedRow, targetPaths, outlineDigests, review,
|
|
1090
|
-
leaseDigest: leaseLoaded.digest, leaseId: lease.id };
|
|
1144
|
+
leaseDigest: leaseLoaded.digest, leaseId: lease.id, identityDigest: recordedIdentity.identityDigest };
|
|
1091
1145
|
}
|
|
1092
1146
|
|
|
1093
1147
|
function outlineDispatchUnresolved(parsed, reason) {
|
|
@@ -1147,7 +1201,8 @@ function captureOutlineDispatch(input = {}) {
|
|
|
1147
1201
|
tokenDigest: parsed.tokenDigest, promptDigest: parsed.promptDigest, blockDigest: parsed.blockDigest,
|
|
1148
1202
|
dispatchInputDigest: parsed.dispatchInputDigest, handoffDigest: validated.handoffDigest,
|
|
1149
1203
|
reviewCaptureRef: validated.review.review.ref, reviewCaptureDigest: validated.review.captureDigest,
|
|
1150
|
-
reviewDigest: validated.review.reviewDigest,
|
|
1204
|
+
reviewDigest: validated.review.reviewDigest, outlineIdentityDigest: validated.identityDigest,
|
|
1205
|
+
inventoryPinDigest: validated.inventoryPinDigest,
|
|
1151
1206
|
writerLeaseDigest: validated.leaseDigest, writerLeaseId: validated.leaseId,
|
|
1152
1207
|
coordinatorPrincipal: payload.session_id, reviewerPrincipal: validated.review.reviewerPrincipal,
|
|
1153
1208
|
worktree: validated.worktree, validationReasons: [],
|
|
@@ -1384,11 +1439,208 @@ function outlineAncestry(root, ancestor, descendant) {
|
|
|
1384
1439
|
|
|
1385
1440
|
function possibleWriterCapture(item) {
|
|
1386
1441
|
if (!recordOf(item)) return false;
|
|
1387
|
-
if (
|
|
1442
|
+
if (OUTLINE_WRITE_TOOLS.includes(item.tool_name)) return true;
|
|
1388
1443
|
if (!['Bash', 'PowerShell', 'exec_command'].includes(item.tool_name)) return false;
|
|
1389
1444
|
return !['read-only', 'worktree-setup'].includes(classifyShell(item.command));
|
|
1390
1445
|
}
|
|
1391
1446
|
|
|
1447
|
+
function writeTargetsOf(toolName, toolInput) {
|
|
1448
|
+
if (toolName === 'apply_patch') {
|
|
1449
|
+
const patch = typeof toolInput === 'string' ? toolInput : (recordOf(toolInput) ? String(toolInput.patch || toolInput.input || '') : '');
|
|
1450
|
+
const targets = [];
|
|
1451
|
+
for (const match of patch.matchAll(/^\*\*\* ([^\r\n]*)(?:\r?$)/gm)) {
|
|
1452
|
+
const header = match[1];
|
|
1453
|
+
if (['Begin Patch', 'End Patch', 'End of File'].includes(header)) continue;
|
|
1454
|
+
const fileHeader = /^(?:Add File|Update File|Delete File|Move to): (.*)$/.exec(header);
|
|
1455
|
+
if (!fileHeader) return null;
|
|
1456
|
+
targets.push(fileHeader[1]);
|
|
1457
|
+
}
|
|
1458
|
+
return targets;
|
|
1459
|
+
}
|
|
1460
|
+
const file = recordOf(toolInput) ? toolInput.file_path || toolInput.path : undefined;
|
|
1461
|
+
return typeof file === 'string' && file.length ? [file] : [];
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function validOutlineTarget(target) {
|
|
1465
|
+
return typeof target === 'string' && target.length > 0 && !target.includes('\0') && !target.includes('\\')
|
|
1466
|
+
&& !target.includes(':') && !path.posix.isAbsolute(target)
|
|
1467
|
+
&& target.split('/').every(part => part && part !== '.' && part !== '..');
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function outlineRootSpellings(root) {
|
|
1471
|
+
try {
|
|
1472
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) return [];
|
|
1473
|
+
return [...new Set([root, canonicalOutlineRoot(root)])];
|
|
1474
|
+
} catch { return []; }
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function outlineCwdMatchesRoot(cwd, root, allowVerifiedAlias = false) {
|
|
1478
|
+
if (typeof cwd !== 'string' || !path.isAbsolute(cwd)) return false;
|
|
1479
|
+
if (outlineRootSpellings(root).includes(cwd)) return true;
|
|
1480
|
+
if (!allowVerifiedAlias) return false;
|
|
1481
|
+
try { return canonicalOutlineRoot(cwd) === canonicalOutlineRoot(root); }
|
|
1482
|
+
catch { return false; }
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function outlineCaptureRootSpellings(root, cwd) {
|
|
1486
|
+
const roots = outlineRootSpellings(root);
|
|
1487
|
+
// Hook setup may retain the canonical root while the captured cwd keeps its original spelling; this affects topology only.
|
|
1488
|
+
if (outlineCwdMatchesRoot(cwd, root, true)) roots.push(cwd);
|
|
1489
|
+
return [...new Set(roots)];
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function outlineRelativeSpellingKey(spelling, toolName) {
|
|
1493
|
+
if (validOutlineTarget(spelling)) return spelling;
|
|
1494
|
+
if (process.platform !== 'win32' || !OUTLINE_WRITE_TOOLS.includes(toolName)
|
|
1495
|
+
|| !spelling.includes(path.sep) || spelling.includes('/')) return null;
|
|
1496
|
+
const candidate = spelling.split(path.sep).join('/');
|
|
1497
|
+
return validOutlineTarget(candidate) ? candidate : null;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function outlineSpellingKey(spelling, { root, cwd, toolName } = {}) {
|
|
1501
|
+
if (typeof spelling !== 'string' || !spelling.length || spelling.includes('\0')
|
|
1502
|
+
|| !outlineCwdMatchesRoot(cwd, root, true)) return null;
|
|
1503
|
+
const relative = outlineRelativeSpellingKey(spelling, toolName);
|
|
1504
|
+
if (relative) return relative;
|
|
1505
|
+
for (const rootSpelling of outlineCaptureRootSpellings(root, cwd)) {
|
|
1506
|
+
const prefix = rootSpelling.endsWith(path.sep) ? rootSpelling : `${rootSpelling}${path.sep}`;
|
|
1507
|
+
if (!spelling.startsWith(prefix)) continue;
|
|
1508
|
+
return outlineRelativeSpellingKey(spelling.slice(prefix.length), toolName);
|
|
1509
|
+
}
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function outlineTargetSpellings(target, { root, toolName } = {}) {
|
|
1514
|
+
if (!validOutlineTarget(target)) return [];
|
|
1515
|
+
const targetSpellings = [target];
|
|
1516
|
+
if (process.platform === 'win32' && OUTLINE_WRITE_TOOLS.includes(toolName)) {
|
|
1517
|
+
targetSpellings.push(target.split('/').join(path.sep));
|
|
1518
|
+
}
|
|
1519
|
+
const spellings = [...targetSpellings];
|
|
1520
|
+
for (const rootSpelling of outlineRootSpellings(root)) {
|
|
1521
|
+
const prefix = rootSpelling.endsWith(path.sep) ? rootSpelling : `${rootSpelling}${path.sep}`;
|
|
1522
|
+
for (const renderedTarget of targetSpellings) spellings.push(prefix + renderedTarget);
|
|
1523
|
+
}
|
|
1524
|
+
return spellings;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Capture can prove a lexical path/topology before the later handoff supplies the pin. Its candidate is non-authorizing;
|
|
1528
|
+
// the assessment call below must match the same raw spelling against the validated pinned target set.
|
|
1529
|
+
function recognizeOutlineTarget(spelling, { root, cwd, targets, toolName } = {}) {
|
|
1530
|
+
const hasPinnedTargets = Array.isArray(targets);
|
|
1531
|
+
if (typeof spelling !== 'string' || !spelling.length || spelling.includes('\0')
|
|
1532
|
+
|| !outlineCwdMatchesRoot(cwd, root, !hasPinnedTargets)) return null;
|
|
1533
|
+
if (Array.isArray(targets)) {
|
|
1534
|
+
const target = targets.find(item => outlineTargetSpellings(item, { root, toolName }).includes(spelling));
|
|
1535
|
+
return target ? { candidate: target, target } : null;
|
|
1536
|
+
}
|
|
1537
|
+
const candidate = outlineSpellingKey(spelling, { root, cwd, toolName });
|
|
1538
|
+
if (!candidate) return null;
|
|
1539
|
+
return { candidate, target: null };
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function outlineBToOGitAllowed(command, { cwd, root, targets, topology }) {
|
|
1543
|
+
const parsed = safeTokenize(String(command || '').trim());
|
|
1544
|
+
if (!parsed.ok || parsed.tokens[0] !== 'git' || parsed.tokens.length < 2) return false;
|
|
1545
|
+
const [operation, ...args] = parsed.tokens.slice(1);
|
|
1546
|
+
if (operation === 'add') {
|
|
1547
|
+
const hasOperandMarker = args[0] === '--';
|
|
1548
|
+
const operands = hasOperandMarker ? args.slice(1) : args;
|
|
1549
|
+
if (!operands.length || !Array.isArray(topology) || topology.length !== operands.length) return false;
|
|
1550
|
+
if (!hasOperandMarker && operands.some(operand => operand.startsWith('-'))) return false;
|
|
1551
|
+
return operands.every((operand, index) => {
|
|
1552
|
+
if (!operand || operand.startsWith(':') || /[*?\[\]{}]/.test(operand) || /[\\/]$/.test(operand)) return false;
|
|
1553
|
+
const target = recognizeOutlineTarget(operand, { root, cwd, targets, toolName: 'Bash' })?.target;
|
|
1554
|
+
const proof = topology[index];
|
|
1555
|
+
return Boolean(target && proof?.path === target && proof.state === 'regular'
|
|
1556
|
+
&& outlineFilesystemProof(root, root, target, false).state === 'regular');
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
return operation === 'commit' && args.length === 2 && args[0] === '-m' && args[1] !== '';
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
function outlineFilesystemProof(root, cwd, target, allowMissing) {
|
|
1563
|
+
let relativePath;
|
|
1564
|
+
try {
|
|
1565
|
+
const canonicalRoot = canonicalOutlineRoot(root);
|
|
1566
|
+
const base = canonicalOutlineRoot(cwd || root);
|
|
1567
|
+
if (base !== canonicalRoot) return { path: null, state: 'unresolved' };
|
|
1568
|
+
const relative = path.relative(canonicalRoot, path.resolve(canonicalRoot, target));
|
|
1569
|
+
if (!relative || path.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path.sep}`)) {
|
|
1570
|
+
return { path: null, state: 'unresolved' };
|
|
1571
|
+
}
|
|
1572
|
+
const absolute = path.resolve(canonicalRoot, relative);
|
|
1573
|
+
relativePath = path.relative(canonicalRoot, absolute);
|
|
1574
|
+
if (!relativePath || path.isAbsolute(relativePath) || relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {
|
|
1575
|
+
return { path: null, state: 'unresolved' };
|
|
1576
|
+
}
|
|
1577
|
+
const relativeKey = relativePath.split(path.sep).join('/');
|
|
1578
|
+
let current = canonicalRoot;
|
|
1579
|
+
const parts = relativePath.split(path.sep);
|
|
1580
|
+
for (let index = 0; index < parts.length; index++) {
|
|
1581
|
+
current = path.join(current, parts[index]);
|
|
1582
|
+
let stat;
|
|
1583
|
+
try { stat = fs.lstatSync(current); }
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
return error.code === 'ENOENT' && allowMissing
|
|
1586
|
+
? { path: relativeKey, state: 'absent' } : { path: relativeKey, state: 'unresolved' };
|
|
1587
|
+
}
|
|
1588
|
+
if (stat.isSymbolicLink() || (index < parts.length - 1 && !stat.isDirectory())) {
|
|
1589
|
+
return { path: relativeKey, state: 'unresolved' };
|
|
1590
|
+
}
|
|
1591
|
+
if (index === parts.length - 1) return { path: relativeKey, state: stat.isFile() ? 'regular' : 'unresolved' };
|
|
1592
|
+
}
|
|
1593
|
+
} catch { /* filesystem state is evidence only when every component can be checked */ }
|
|
1594
|
+
return { path: relativePath ? relativePath.split(path.sep).join('/') : null, state: 'unresolved' };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function outlineGitTreeHasRegularBlob(root, tree, target) {
|
|
1598
|
+
try {
|
|
1599
|
+
const entries = outlineGit(root, ['--literal-pathspecs', 'ls-tree', '-z', tree, '--', target], true)
|
|
1600
|
+
.toString('utf8').split('\0').filter(Boolean);
|
|
1601
|
+
if (entries.length !== 1) return false;
|
|
1602
|
+
const tab = entries[0].indexOf('\t');
|
|
1603
|
+
if (tab < 0 || entries[0].slice(tab + 1) !== target) return false;
|
|
1604
|
+
const [mode, type, oid] = entries[0].slice(0, tab).split(/\s+/);
|
|
1605
|
+
return type === 'blob' && ['100644', '100755'].includes(mode) && /^[0-9a-f]+$/i.test(oid);
|
|
1606
|
+
} catch { return false; }
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Phase-aware writer decision (W1–W3). Principal, phase and operation-time path topology come from captured records.
|
|
1610
|
+
// Allowed: coordinator Write/Edit/apply_patch of exactly a pinned target before the reviewer lifecycle starts; coordinator
|
|
1611
|
+
// target-bound git add or simple git commit before review; the correlated draft worker
|
|
1612
|
+
// between dispatch and its Stop. Everything else, including any coordinator state write after review, stays ambiguous.
|
|
1613
|
+
function outlineWriterAllowed(item, { index, sessionId, root, targets, worker, workerRoot, reviewStartIndex, reviewStopIndex,
|
|
1614
|
+
dispatchIndex, workerStopIndex, captures }) {
|
|
1615
|
+
const phase = index < reviewStartIndex ? 'b-to-o' : index <= reviewStopIndex ? 'review'
|
|
1616
|
+
: index < dispatchIndex ? 'review-to-dispatch' : index <= workerStopIndex ? 'dispatch-to-draft' : 'after-draft';
|
|
1617
|
+
if (phase === 'dispatch-to-draft') return item.agent_id === worker.agent_id && sameCanonicalPath(item.cwd, workerRoot);
|
|
1618
|
+
if (phase !== 'b-to-o' || textOf(item.agent_id) || item.session_id !== sessionId) return false;
|
|
1619
|
+
if (item.event !== 'PreToolUse') {
|
|
1620
|
+
const pre = (Array.isArray(captures) ? captures : []).find(record => record.event === 'PreToolUse'
|
|
1621
|
+
&& record.session_id === item.session_id && record.tool_use_id === item.tool_use_id
|
|
1622
|
+
&& record.tool_name === item.tool_name && record.command === item.command
|
|
1623
|
+
&& captureJson(record.writeTargets || null) === captureJson(item.writeTargets || null)
|
|
1624
|
+
&& samePathKey(record.cwd || root, item.cwd || root));
|
|
1625
|
+
return Boolean(pre && outlineWriterAllowed(pre, { index: captures.indexOf(pre), sessionId, root, targets, worker, workerRoot,
|
|
1626
|
+
reviewStartIndex, reviewStopIndex, dispatchIndex, workerStopIndex, captures }));
|
|
1627
|
+
}
|
|
1628
|
+
if (OUTLINE_WRITE_TOOLS.includes(item.tool_name)) {
|
|
1629
|
+
const topology = item.outlineWriteTopology;
|
|
1630
|
+
return Array.isArray(item.writeTargets) && item.writeTargets.length > 0
|
|
1631
|
+
&& Array.isArray(topology) && topology.length === item.writeTargets.length
|
|
1632
|
+
&& item.writeTargets.every((file, index) => {
|
|
1633
|
+
const target = recognizeOutlineTarget(file, { root, cwd: item.cwd, targets, toolName: item.tool_name })?.target;
|
|
1634
|
+
const proof = topology[index];
|
|
1635
|
+
return Boolean(target && proof?.path === target && ['absent', 'regular'].includes(proof.state)
|
|
1636
|
+
&& outlineFilesystemProof(root, root, target, false).state === 'regular');
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
const cwd = item.cwd;
|
|
1640
|
+
return item.tool_name === 'Bash' && outlineCwdMatchesRoot(cwd, root)
|
|
1641
|
+
&& outlineBToOGitAllowed(item.command, { cwd, root, targets, topology: item.outlineWriteTopology });
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1392
1644
|
function sameCanonicalPath(left, right) {
|
|
1393
1645
|
try {
|
|
1394
1646
|
fs.statSync(left);
|
|
@@ -1418,7 +1670,10 @@ function resolveOutlineEvidence({ safeInput, capture, captures, request, originR
|
|
|
1418
1670
|
const dispatch = cap?.outlineDispatch;
|
|
1419
1671
|
if (!recordOf(dispatch) || dispatch.schema !== OUTLINE_DISPATCH_SCHEMA || dispatch.state !== 'accepted'
|
|
1420
1672
|
|| !textOf(dispatch.seamRef) || dispatch.captureRef !== cap.ref || dispatch.toolUseId !== cap.tool_use_id) {
|
|
1421
|
-
|
|
1673
|
+
// The dispatch-time reason (for example outline-identity-mismatch) is kept beside the wrapper code, so the rejection names its cause.
|
|
1674
|
+
const reasons = recordOf(dispatch) && Array.isArray(dispatch.reasons) ? dispatch.reasons.filter(reason => /^[a-z0-9-]+$/.test(String(reason))) : [];
|
|
1675
|
+
return { status: 'rejected', diagnostics: [diagnostic('outline-pre-dispatch-seam-unresolved', { reason: 'outline-pre-dispatch-seam-unresolved' }),
|
|
1676
|
+
...reasons.map(reason => diagnostic(reason, { reason }))] };
|
|
1422
1677
|
}
|
|
1423
1678
|
if (cap.event !== 'PreToolUse' || !['Task', 'Agent'].includes(cap.tool_name)
|
|
1424
1679
|
|| cap.session_id !== safeInput.sessionId || cap.observedIdentity?.taskPin?.taskId !== safeInput.taskId) {
|
|
@@ -1475,6 +1730,9 @@ function resolveOutlineEvidence({ safeInput, capture, captures, request, originR
|
|
|
1475
1730
|
const outlineDigests = {};
|
|
1476
1731
|
const reviewRows = {};
|
|
1477
1732
|
for (const target of targetPaths) {
|
|
1733
|
+
if (!outlineGitTreeHasRegularBlob(seam.worktree.root, handoff.outline.tree, target)) {
|
|
1734
|
+
return fail('outline-target-object-not-regular');
|
|
1735
|
+
}
|
|
1478
1736
|
const bytes = outlineGit(seam.worktree.root, ['show', `${handoff.outline.commit}:${target}`], true);
|
|
1479
1737
|
outlineDigests[target] = outlineSha(bytes);
|
|
1480
1738
|
const citation = parseOutlineCitation(bytes, target);
|
|
@@ -1527,9 +1785,44 @@ function resolveOutlineEvidence({ safeInput, capture, captures, request, originR
|
|
|
1527
1785
|
&& item.event === 'SubagentStart' && item.ref !== worker.ref && sameCanonicalPath(item.cwd, workerRoot)
|
|
1528
1786
|
&& !stoppedBeforeDispatch(item) && !startedAfterWorkerStop(item));
|
|
1529
1787
|
if (competingStarts.length) return fail('outline-competing-writer-start');
|
|
1530
|
-
|
|
1531
|
-
|
|
1788
|
+
// Reviewer lifecycle is exactly one Start and one Stop for its identity; a duplicate or missing record withholds credit.
|
|
1789
|
+
const reviewerLifecycle = event => journal.filter(item => recordOf(item) && item.event === event
|
|
1790
|
+
&& item.agent_id === review.reviewerPrincipal).length;
|
|
1791
|
+
if (reviewerLifecycle('SubagentStart') !== 1 || reviewerLifecycle('SubagentStop') !== 1) {
|
|
1792
|
+
return fail('outline-review-result-missing-or-ambiguous');
|
|
1793
|
+
}
|
|
1794
|
+
// The O the reviewer recorded at launch must still be the O dispatch and the handoff name (#341 step 1).
|
|
1795
|
+
const identityNow = validateOutlineIdentity({ handoff, captures });
|
|
1796
|
+
if (identityNow.state !== 'accepted' || identityNow.identityDigest !== seam.outlineIdentityDigest) {
|
|
1797
|
+
return fail(identityNow.reason || 'outline-identity-mismatch');
|
|
1798
|
+
}
|
|
1799
|
+
// Review window: the reviewer's first record (or its launch result) through its SubagentStop.
|
|
1800
|
+
const reviewLaunchIds = new Set(journal.filter(item => recordOf(item) && item.event === 'PostToolUse'
|
|
1801
|
+
&& ['Task', 'Agent'].includes(item.tool_name) && item.agent_id === review.reviewerPrincipal && textOf(item.tool_use_id))
|
|
1802
|
+
.map(item => item.tool_use_id));
|
|
1803
|
+
const writerWindow = {
|
|
1804
|
+
sessionId: safeInput.sessionId, root: taskState.baseline.root, targets: targetPaths, worker, workerRoot,
|
|
1805
|
+
captures: journal,
|
|
1806
|
+
reviewStartIndex: journal.findIndex(item => recordOf(item) && (item.agent_id === review.reviewerPrincipal
|
|
1807
|
+
|| (textOf(item.tool_use_id) && reviewLaunchIds.has(item.tool_use_id)))),
|
|
1808
|
+
reviewStopIndex: journal.indexOf(review.review), dispatchIndex, workerStopIndex: journal.indexOf(correlation.stop),
|
|
1809
|
+
};
|
|
1810
|
+
const unattributedWriters = journal.filter((item, index) => possibleWriterCapture(item)
|
|
1811
|
+
&& !outlineWriterAllowed(item, { ...writerWindow, index }));
|
|
1532
1812
|
if (unattributedWriters.length) return fail('outline-writer-activity-ambiguous');
|
|
1813
|
+
// The B→O commit is accepted only if O changes exactly the pinned target(s) relative to B.
|
|
1814
|
+
const oChanged = outlineGit(seam.worktree.root, ['diff', '--name-only', '--no-renames', '-z', taskState.baseline.commit,
|
|
1815
|
+
handoff.outline.commit]).split('\0').filter(Boolean).sort(compareKeys);
|
|
1816
|
+
if (captureJson(oChanged) !== captureJson([...targetPaths].sort(compareKeys))) return fail('outline-commit-scope-invalid');
|
|
1817
|
+
// The cumulative diff hides content committed then reverted inside B..O, so every commit (merges too, per parent) must touch only the targets.
|
|
1818
|
+
const targetSet = new Set(targetPaths);
|
|
1819
|
+
const rangeCommits = outlineGit(seam.worktree.root, ['rev-list', `${taskState.baseline.commit}..${handoff.outline.commit}`])
|
|
1820
|
+
.split('\n').filter(Boolean);
|
|
1821
|
+
for (const commit of rangeCommits) {
|
|
1822
|
+
const touched = outlineGit(seam.worktree.root, ['diff-tree', '-r', '--root', '-m', '--no-commit-id', '--name-only',
|
|
1823
|
+
'--no-renames', '-z', commit]).split('\0').filter(Boolean);
|
|
1824
|
+
if (touched.some(file => !targetSet.has(file))) return fail('outline-commit-scope-invalid');
|
|
1825
|
+
}
|
|
1533
1826
|
|
|
1534
1827
|
let candidateRoot;
|
|
1535
1828
|
try { candidateRoot = canonicalOutlineRoot(safeInput.workspaceRoot); }
|
|
@@ -2227,12 +2520,40 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
|
|
|
2227
2520
|
responseDigest: captureDigest(toolResponse),
|
|
2228
2521
|
responseRef: `host-payload/${event}/${captureId}`,
|
|
2229
2522
|
};
|
|
2523
|
+
const parsedWriterCommand = event === 'PreToolUse' && payload.tool_name === 'Bash'
|
|
2524
|
+
? safeTokenize(record.command.trim()) : null;
|
|
2525
|
+
const writerMayNeedCwd = OUTLINE_WRITE_TOOLS.includes(payload.tool_name)
|
|
2526
|
+
|| (parsedWriterCommand?.ok && parsedWriterCommand.tokens[0] === 'git'
|
|
2527
|
+
&& ['add', 'commit'].includes(parsedWriterCommand.tokens[1]));
|
|
2230
2528
|
if (typeof payload.cwd === 'string' && payload.cwd) record.cwd = payload.cwd;
|
|
2231
|
-
else if (!['SubagentStart', 'SubagentStop'].includes(event)
|
|
2529
|
+
else if (!writerMayNeedCwd && !['SubagentStart', 'SubagentStop'].includes(event)
|
|
2530
|
+
&& typeof workspaceRoot === 'string' && workspaceRoot) record.cwd = workspaceRoot;
|
|
2232
2531
|
if (payload.prompt_id !== undefined) record.prompt_id = payload.prompt_id;
|
|
2233
2532
|
if (payload.parent_tool_use_id !== undefined) record.parent_tool_use_id = payload.parent_tool_use_id;
|
|
2234
2533
|
// The launch tool name separates the Agent/Task result surface and the subagent launch from executed tools (F4/F5).
|
|
2235
2534
|
if (typeof payload.tool_name === 'string') record.tool_name = payload.tool_name;
|
|
2535
|
+
// The write target and its PreToolUse topology snapshot are path evidence for W1/W2; absent for other tools.
|
|
2536
|
+
if (OUTLINE_WRITE_TOOLS.includes(payload.tool_name)) record.writeTargets = writeTargetsOf(payload.tool_name, payload.tool_input);
|
|
2537
|
+
// shortcut: this snapshot cannot close capture-to-operation races; upgrade if the host can bind path proof atomically.
|
|
2538
|
+
if (event === 'PreToolUse') {
|
|
2539
|
+
let topologyTargets = OUTLINE_WRITE_TOOLS.includes(payload.tool_name) ? record.writeTargets : null;
|
|
2540
|
+
let allowMissing = OUTLINE_WRITE_TOOLS.includes(payload.tool_name);
|
|
2541
|
+
if (payload.tool_name === 'Bash') {
|
|
2542
|
+
const parsed = safeTokenize(record.command.trim());
|
|
2543
|
+
if (parsed.ok && parsed.tokens[0] === 'git' && parsed.tokens[1] === 'add') {
|
|
2544
|
+
const args = parsed.tokens.slice(2);
|
|
2545
|
+
topologyTargets = args[0] === '--' ? args.slice(1) : args;
|
|
2546
|
+
allowMissing = false;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (Array.isArray(topologyTargets)) {
|
|
2550
|
+
record.outlineWriteTopology = topologyTargets.map(target => {
|
|
2551
|
+
const recognized = recognizeOutlineTarget(target, { root: workspaceRoot, cwd: record.cwd, toolName: payload.tool_name });
|
|
2552
|
+
return recognized ? outlineFilesystemProof(workspaceRoot, record.cwd, recognized.candidate, allowMissing)
|
|
2553
|
+
: { path: null, state: 'unresolved' };
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2236
2557
|
if (outlinePre) {
|
|
2237
2558
|
if (Object.prototype.hasOwnProperty.call(toolInput, 'run_in_background')) record.run_in_background = toolInput.run_in_background;
|
|
2238
2559
|
record.isolationSet = Object.prototype.hasOwnProperty.call(toolInput, 'isolation');
|
|
@@ -2317,6 +2638,8 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
|
|
|
2317
2638
|
// Each capture is one event file, created exclusively and never rewritten by this code. The compatibility journal
|
|
2318
2639
|
// `doc-reconciliation-capture.json` is rebuilt from those files, and readers compare the two. This detects accidental and
|
|
2319
2640
|
// journal-only edits. It does not stop the local agent from rewriting event files, which the owner's bound accepts (F13).
|
|
2641
|
+
// F13 includes rewriting the reviewer Start event's W2 `outlineIdentity`; W2 revalidation assumes authentic captured
|
|
2642
|
+
// events and does not authenticate locally rewritten capture state.
|
|
2320
2643
|
const EVENT_DIR = 'doc-reconciliation-capture';
|
|
2321
2644
|
const JOURNAL_FILE = 'doc-reconciliation-capture.json';
|
|
2322
2645
|
const EVENT_NAME = /^\d{8}\.json$/;
|
|
@@ -2410,8 +2733,9 @@ function reviewRecordIndependent(cap, captures, principals) {
|
|
|
2410
2733
|
module.exports = { createClaudeOriginProvider, createOpencodeOriginProvider, buildCaptureRecord, resolveDocCheckInvocation,
|
|
2411
2734
|
registeredInstalledCheck: resolveDocCheckInvocation, installedDocCheck,
|
|
2412
2735
|
fileDigest, SHIPPED_DOC_CHECK_DIGEST, trustRisk, journalExecutors, reviewRecordIndependent,
|
|
2413
|
-
captureOutlineDispatch, writeImmutableEvent, rebuildJournalView, readCaptureView,
|
|
2414
|
-
|
|
2736
|
+
captureOutlineDispatch, captureOutlineStartIdentity, writeImmutableEvent, rebuildJournalView, readCaptureView,
|
|
2737
|
+
readOpencodeCaptureView, observedIdentityFor,
|
|
2738
|
+
_internals: { validateOutlineIdentity, captureJson, captureDigest, digestText, canonicalCapture, parseOutlineDispatchPrompt,
|
|
2415
2739
|
parseInventoryPinRows, validateOutlineInventoryPin, resolveInventoryRow, parseOutlineCitation, resolveOutlineAttempt,
|
|
2416
2740
|
captureOutlineDispatch, resolveOutlineEvidence, verifyOutlineMergeCorrespondence, validateOutlineCaptureQualification,
|
|
2417
2741
|
outlineCaptureMappingValid, correlateOutlineDispatch, OUTLINE_CAPTURE_MAPPING, OUTLINE_INVENTORY_PIN } };
|
|
@@ -148,6 +148,11 @@ function persistPilotCapture(payload, eventName, sessionId, root) {
|
|
|
148
148
|
// Observation-time identity: the workflow and pin in force when the host event was captured.
|
|
149
149
|
const observed = provider.observedIdentityFor(sessionDir);
|
|
150
150
|
if (observed) entry.observedIdentity = observed;
|
|
151
|
+
// #341 step 1: O's identity is taken at launch records, so a reviewer's identity predates its review.
|
|
152
|
+
if (eventName === 'SubagentStart') {
|
|
153
|
+
const outlineIdentity = provider.captureOutlineStartIdentity({ sessionDir });
|
|
154
|
+
if (outlineIdentity) entry.outlineIdentity = outlineIdentity;
|
|
155
|
+
}
|
|
151
156
|
if (eventName === 'PreToolUse' && (entry.tool_name === 'Agent' || entry.tool_name === 'Task')) {
|
|
152
157
|
try {
|
|
153
158
|
const existing = provider.readCaptureView(sessionDir);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.49.0",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
package/sbom.cdx.json
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
"$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.7",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:962b7322-7ab3-5bae-8399-c8e93c5d6717",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-11-
|
|
8
|
+
"timestamp": "2026-11-08T20:01:04.125Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.49.0"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.49.0",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.
|
|
20
|
+
"version": "0.49.0",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.49.0"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -2033,7 +2033,7 @@
|
|
|
2033
2033
|
]
|
|
2034
2034
|
},
|
|
2035
2035
|
{
|
|
2036
|
-
"ref": "pkg:npm/harness-everything@0.
|
|
2036
|
+
"ref": "pkg:npm/harness-everything@0.49.0",
|
|
2037
2037
|
"dependsOn": [
|
|
2038
2038
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2039
2039
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|