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
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
const { canonical } = require(path.resolve(__dirname, '../hooks/scripts/lib/workflow-isolation.js'));
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
13
|
+
const provider = require(path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-provider.js'));
|
|
14
|
+
const pilot = require(path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-pilot.js'));
|
|
15
|
+
const { checkDocObligations } = require(path.join(ROOT, 'harness-everything/scripts/doc-evidence.js'));
|
|
16
|
+
const internals = provider._internals || {};
|
|
17
|
+
const failures = [];
|
|
18
|
+
|
|
19
|
+
function test(name, run) {
|
|
20
|
+
try {
|
|
21
|
+
run();
|
|
22
|
+
console.log(`PASS ${name}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
failures.push(name);
|
|
25
|
+
console.error(`FAIL ${name}: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha(value) {
|
|
30
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJson(file, value) {
|
|
34
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dispatchLine = 'HARNESS_OUTLINE_DISPATCH_V1 taskId=task-prb attemptId=attempt-1 token=token-1 workerRoot=/tmp/prb-worker';
|
|
39
|
+
|
|
40
|
+
const syntheticInventory = [
|
|
41
|
+
'# Synthetic inventory',
|
|
42
|
+
'run id `synthetic-run` · input SHA `abcdef012345`',
|
|
43
|
+
'',
|
|
44
|
+
'## Mechanism inventory',
|
|
45
|
+
'| # | Mechanism | Evidence |',
|
|
46
|
+
'|---|---|---|',
|
|
47
|
+
'| 20 | ADR identity | mechanism evidence |',
|
|
48
|
+
'',
|
|
49
|
+
'## Area grouping + candidate domain prefixes',
|
|
50
|
+
'| Area | Rows | Candidate prefix | Why |',
|
|
51
|
+
'|---|---|---|---|',
|
|
52
|
+
'| Documents | 20–22 | `DOCS` | Synthetic fixture |',
|
|
53
|
+
].join('\n') + '\n';
|
|
54
|
+
|
|
55
|
+
function fixtureGit(root, args) {
|
|
56
|
+
const env = { ...process.env, GIT_AUTHOR_NAME: 'outline-fixture', GIT_AUTHOR_EMAIL: 'outline-fixture@invalid',
|
|
57
|
+
GIT_COMMITTER_NAME: 'outline-fixture', GIT_COMMITTER_EMAIL: 'outline-fixture@invalid' };
|
|
58
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_') && !/^GIT_(AUTHOR|COMMITTER)_/.test(key)) delete env[key];
|
|
59
|
+
try {
|
|
60
|
+
return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`${error.message}: ${String(error.stderr || '').trim()}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function outlineText({ complete = false, decisionPlaceholder = false, duplicateDecision = false, titledDecision = false,
|
|
67
|
+
indentedDecision = false,
|
|
68
|
+
rowId = '20', target = 'docs/adr/DOCS-001.md', domain = 'DOCS' } = {}) {
|
|
69
|
+
const decision = complete ? '\n## Decision\nThis is already a complete drafted decision.\n'
|
|
70
|
+
: duplicateDecision ? '\n## Decision\n- TBD\n\n## Decision\nThis second Decision section contains substantive text.\n'
|
|
71
|
+
: titledDecision ? '\n## Decision — Final choice\nThis titled section contains a complete decision.\n'
|
|
72
|
+
: indentedDecision ? '\n ## Decision — Indented final choice\nThis indented section contains substantive text.\n'
|
|
73
|
+
: decisionPlaceholder ? '\n## Decision\n- TBD\n' : '';
|
|
74
|
+
return `---\nid: ${domain}-001\nstatus: proposed\n---\n\nInventory row: ${rowId}\nTarget: ${target}\nDomain: ${domain}\n\n## Outline\n- Context: TBD\n- Decision: TBD\n${decision}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createOutlineFixture(options = {}) {
|
|
78
|
+
const root = canonical(fs.mkdtempSync(path.join(os.tmpdir(), 'prb-outline-fixture-')));
|
|
79
|
+
const target = options.target || 'docs/adr/DOCS-001.md';
|
|
80
|
+
const taskId = options.taskId || 'task-prb';
|
|
81
|
+
const attemptId = options.attemptId || 'attempt-1';
|
|
82
|
+
const sessionId = options.sessionId || 'author-session';
|
|
83
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-outline-state-'));
|
|
84
|
+
const workerRoot = options.workerRootAlias
|
|
85
|
+
? `${root}${path.sep}..${path.sep}${path.basename(root)}` : root;
|
|
86
|
+
const workflowId = 'workflow-prb';
|
|
87
|
+
const workflowRevision = 2;
|
|
88
|
+
const config = JSON.stringify({ governed: [{ path: 'docs/adr/*.md', role: 'adr' }], domains: ['DOCS'], derived: [] }, null, 2) + '\n';
|
|
89
|
+
const configDigest = sha(config);
|
|
90
|
+
fs.mkdirSync(path.join(root, '.agents/harness-everything'), { recursive: true });
|
|
91
|
+
fs.mkdirSync(path.dirname(path.join(root, target)), { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(root, '.agents/harness-everything/project-docs.json'), config);
|
|
93
|
+
if (options.targetExistsAtB) fs.writeFileSync(path.join(root, target),
|
|
94
|
+
`---\nid: DOCS-001\nstatus: proposed\n---\n\n## Decision\nAn existing B document.\n`);
|
|
95
|
+
fixtureGit(root, ['init', '-q']);
|
|
96
|
+
fixtureGit(root, ['add', '-A']);
|
|
97
|
+
fixtureGit(root, ['commit', '-q', '-m', 'B baseline']);
|
|
98
|
+
const baselineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
99
|
+
const baselineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
100
|
+
let sessionDir = path.join(stateHome, 'session');
|
|
101
|
+
if (options.realHook) {
|
|
102
|
+
const priorStateHome = process.env.HARNESS_STATE_HOME;
|
|
103
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
104
|
+
try { sessionDir = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js')).getSessionDir(root, sessionId); }
|
|
105
|
+
finally { if (priorStateHome === undefined) delete process.env.HARNESS_STATE_HOME; else process.env.HARNESS_STATE_HOME = priorStateHome; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(path.join(root, target), outlineText({
|
|
109
|
+
complete: options.completeOutline === true,
|
|
110
|
+
decisionPlaceholder: options.decisionPlaceholder === true,
|
|
111
|
+
duplicateDecision: options.duplicateDecision === true,
|
|
112
|
+
indentedDecision: options.indentedDecision === true,
|
|
113
|
+
rowId: options.rowId || '20', target, domain: options.domain || 'DOCS',
|
|
114
|
+
}));
|
|
115
|
+
fixtureGit(root, ['add', '-A']);
|
|
116
|
+
fixtureGit(root, ['commit', '-q', '-m', 'O outline']);
|
|
117
|
+
const outlineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
118
|
+
const outlineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
119
|
+
const outlineBytes = fs.readFileSync(path.join(root, target));
|
|
120
|
+
const outlineBlob = fixtureGit(root, ['rev-parse', `HEAD:${target}`]);
|
|
121
|
+
|
|
122
|
+
const pinDigest = sha(syntheticInventory);
|
|
123
|
+
const expectedPin = {
|
|
124
|
+
source: 'synthetic://approved-inventory', sha256: pinDigest, runId: 'synthetic-run', inputSha: 'abcdef012345',
|
|
125
|
+
approvalRef: { issue: 325, commentId: 'synthetic-comment', decision: 1 },
|
|
126
|
+
retainedArtifactPath: `inventory/${pinDigest}.md`,
|
|
127
|
+
};
|
|
128
|
+
const outlineRoot = path.join(sessionDir, 'doc-reconciliation/outline-confirmation');
|
|
129
|
+
const inventoryDir = path.join(outlineRoot, 'inventory');
|
|
130
|
+
fs.mkdirSync(inventoryDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(inventoryDir, `${pinDigest}.md`), syntheticInventory);
|
|
132
|
+
writeJson(path.join(inventoryDir, 'inventory-pin.json'), expectedPin);
|
|
133
|
+
const requirementIds = ['prb'];
|
|
134
|
+
const workspaceId = 'ws-' + internals.digestText(internals.captureJson({ workspaceRoot: root }));
|
|
135
|
+
const taskBinding = { schema: 'harness.doc-reconciliation-task-pin.v1', taskId, requirementIds,
|
|
136
|
+
requirementDigest: sha(JSON.stringify(requirementIds)), baselineCommit, baselineRoot: root,
|
|
137
|
+
configDigest, sessionId, workflowId, workspaceId };
|
|
138
|
+
writeJson(path.join(sessionDir, 'workflow-run.json'), {
|
|
139
|
+
workflowId, revision: workflowRevision, docReconciliationBaseline: { root, commit: baselineCommit, tree: baselineTree },
|
|
140
|
+
docReconciliationTask: taskBinding,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let hookPreloader = null;
|
|
144
|
+
function runHook(payload, pilotEnabled = true) {
|
|
145
|
+
if (!options.realHook) throw new Error('real state-persist capture was not enabled for this fixture');
|
|
146
|
+
if (!hookPreloader) {
|
|
147
|
+
hookPreloader = path.join(stateHome, 'outline-test-preload.js');
|
|
148
|
+
const providerPath = path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-provider.js');
|
|
149
|
+
fs.writeFileSync(hookPreloader, [
|
|
150
|
+
...(options.failPartialDispatchStorage ? [
|
|
151
|
+
"const fs=require('fs');",
|
|
152
|
+
"const path=require('path');",
|
|
153
|
+
"const marker=path.join(process.env.HARNESS_STATE_HOME,'prb-partial-dispatch-faults-used');",
|
|
154
|
+
'const write=fs.writeFileSync;',
|
|
155
|
+
'let tokenFailure=false;',
|
|
156
|
+
'fs.writeFileSync=function(file,data){',
|
|
157
|
+
"const target=String(file).replace(/\\\\/g,'/');",
|
|
158
|
+
"if(!fs.existsSync(marker)&&/\\/tokens\\/[^/]+\\.json$/.test(target)){tokenFailure=true;const error=new Error('injected token write failure');error.code='EIO';throw error;}",
|
|
159
|
+
"if(tokenFailure&&!fs.existsSync(marker)&&/\\/doc-reconciliation-capture\\/\\d{8}\\.json$/.test(target)&&String(data).includes('\\\"state\\\": \\\"unresolved\\\"')){write(marker,'injected');const error=new Error('injected unresolved capture write failure');error.code='EIO';throw error;}",
|
|
160
|
+
'return write.apply(this,arguments);',
|
|
161
|
+
'};',
|
|
162
|
+
] : []),
|
|
163
|
+
`const p=require(${JSON.stringify(providerPath)});`,
|
|
164
|
+
"if(typeof p.captureOutlineDispatch==='function'){",
|
|
165
|
+
'const original=p.captureOutlineDispatch;',
|
|
166
|
+
`const pin=${JSON.stringify(expectedPin)};`,
|
|
167
|
+
'p.captureOutlineDispatch=(input)=>original({...input,fixtureExpectedPin:pin});',
|
|
168
|
+
'}',
|
|
169
|
+
].join('\n'));
|
|
170
|
+
}
|
|
171
|
+
const child = spawnSync(process.execPath, ['--require', hookPreloader, path.join(ROOT, 'hooks/scripts/state-persist.js')], {
|
|
172
|
+
cwd: root, encoding: 'utf8', input: JSON.stringify(payload),
|
|
173
|
+
env: { ...process.env, HARNESS_STATE_HOME: stateHome, HARNESS_WORKSPACE_ROOT: root,
|
|
174
|
+
HARNESS_DOC_RECONCILIATION_PILOT: pilotEnabled ? '1' : '0' },
|
|
175
|
+
});
|
|
176
|
+
if (child.status !== 0) throw new Error(`state-persist failed open contract: ${child.stderr || child.stdout || child.status}`);
|
|
177
|
+
return provider.readCaptureView(sessionDir).records;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const inventoryPath = `doc-reconciliation/outline-confirmation/inventory/${pinDigest}.md`;
|
|
181
|
+
const reviewerPrincipal = options.reviewerPrincipal || 'outline-reviewer';
|
|
182
|
+
const reviewMessage = [
|
|
183
|
+
'Review verdict: approve',
|
|
184
|
+
`Reviewed artifact: ${target} sha256:${sha(outlineBytes)}`,
|
|
185
|
+
`Reviewed artifact: ${inventoryPath} sha256:${pinDigest}`,
|
|
186
|
+
`Summary: Approved inventory row 20 for ${target} in domain DOCS.`,
|
|
187
|
+
].join('\n');
|
|
188
|
+
const reviewResultPayload = {
|
|
189
|
+
session_id: sessionId, cwd: root, hook_event_name: 'PostToolUse', tool_name: 'Task',
|
|
190
|
+
tool_use_id: 'review-dispatch', tool_input: { command: 'Task' },
|
|
191
|
+
tool_response: { agentId: reviewerPrincipal, interrupted: false },
|
|
192
|
+
};
|
|
193
|
+
const reviewStopPayload = {
|
|
194
|
+
session_id: sessionId, cwd: root, hook_event_name: 'SubagentStop', agent_id: reviewerPrincipal,
|
|
195
|
+
agent_type: 'reviewer', last_assistant_message: reviewMessage,
|
|
196
|
+
};
|
|
197
|
+
let reviewResult;
|
|
198
|
+
let reviewStop;
|
|
199
|
+
if (options.realHook) {
|
|
200
|
+
runHook(reviewResultPayload);
|
|
201
|
+
runHook(reviewStopPayload);
|
|
202
|
+
const retained = provider.readCaptureView(sessionDir).records;
|
|
203
|
+
reviewResult = retained.find(item => item.event === 'PostToolUse' && item.tool_use_id === 'review-dispatch');
|
|
204
|
+
reviewStop = retained.find(item => item.event === 'SubagentStop' && item.agent_id === reviewerPrincipal);
|
|
205
|
+
} else {
|
|
206
|
+
reviewResult = provider.buildCaptureRecord(reviewResultPayload, 'PostToolUse', root);
|
|
207
|
+
reviewStop = provider.buildCaptureRecord(reviewStopPayload, 'SubagentStop', root);
|
|
208
|
+
reviewResult.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
209
|
+
reviewStop.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
210
|
+
}
|
|
211
|
+
if (!reviewResult || !reviewStop) throw new Error('review hook captures were not retained');
|
|
212
|
+
const reviewRecordDigest = provider._internals?.captureDigest
|
|
213
|
+
? provider._internals.captureDigest(reviewStop)
|
|
214
|
+
: sha(JSON.stringify(reviewStop));
|
|
215
|
+
|
|
216
|
+
const attemptDir = path.join(outlineRoot, taskId, 'attempts', attemptId);
|
|
217
|
+
fs.mkdirSync(attemptDir, { recursive: true });
|
|
218
|
+
const lease = { schema: 'harness.outline-writer-lease.v1', id: 'lease-1', taskId, attemptId,
|
|
219
|
+
principalId: sessionId, root: workerRoot, exclusive: true, active: true,
|
|
220
|
+
...(options.competingWriters ? { competingWriters: ['unknown-writer'] } : {}) };
|
|
221
|
+
writeJson(path.join(attemptDir, 'writer-lease.json'), lease);
|
|
222
|
+
const leaseDigest = sha(fs.readFileSync(path.join(attemptDir, 'writer-lease.json')));
|
|
223
|
+
const marker = `HARNESS_OUTLINE_DISPATCH_V1 taskId=${taskId} attemptId=${attemptId} token=${options.token || 'token-1'} workerRoot=${options.markerRoot || workerRoot}`;
|
|
224
|
+
const handoff = {
|
|
225
|
+
schema: 'harness.outline-handoff.v1', handoffId: 'handoff-1', taskId, attemptId,
|
|
226
|
+
supersedesAttemptId: options.supersedesAttemptId || null,
|
|
227
|
+
workspaceId,
|
|
228
|
+
workflowId, workflowRevision, sessionId, hostId: 'claude-code',
|
|
229
|
+
baseline: { root, commit: baselineCommit, tree: baselineTree, configDigest },
|
|
230
|
+
configDigest, targetPaths: [target], draftWorktreeRoot: workerRoot,
|
|
231
|
+
outline: { commit: outlineCommit, tree: outlineTree, targetBlobs: { [target]: { blobOid: outlineBlob, storedBytesSha256: sha(outlineBytes) } } },
|
|
232
|
+
inventory: { ...expectedPin, rowId: options.rowId || '20', domain: options.domain || 'DOCS', targetPaths: [target] },
|
|
233
|
+
reviewCaptureRef: reviewStop.ref, reviewCaptureDigest: reviewRecordDigest, reviewDigest: reviewStop.reviewDigest,
|
|
234
|
+
coordinatorPrincipal: sessionId, writerLeaseId: lease.id, writerLeaseDigest: leaseDigest,
|
|
235
|
+
draftDispatchToken: options.token || 'token-1',
|
|
236
|
+
...(options.handoffOverrides || {}),
|
|
237
|
+
};
|
|
238
|
+
if (!options.omitHandoff) writeJson(path.join(attemptDir, 'handoff.json'), handoff);
|
|
239
|
+
|
|
240
|
+
const payload = {
|
|
241
|
+
session_id: sessionId, cwd: root, hook_event_name: 'PreToolUse', tool_name: 'Task',
|
|
242
|
+
tool_use_id: options.toolUseId || 'dispatch-1', tool_input: { prompt: marker },
|
|
243
|
+
};
|
|
244
|
+
if (options.blockSeamStorage) {
|
|
245
|
+
fs.mkdirSync(path.join(attemptDir, 'seams', `${sha(String(payload.tool_use_id))}.json`), { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
if (options.blockTokenStorage) fs.writeFileSync(path.join(attemptDir, 'tokens'), 'blocked token directory');
|
|
248
|
+
if (options.dirtyBeforeDispatch) fs.writeFileSync(path.join(root, 'untracked-before-dispatch.txt'), 'dirty');
|
|
249
|
+
let capture;
|
|
250
|
+
let reviewCaptures;
|
|
251
|
+
if (options.realHook) {
|
|
252
|
+
if (options.omitReview) {
|
|
253
|
+
fs.rmSync(path.join(sessionDir, 'doc-reconciliation-capture'), { recursive: true, force: true });
|
|
254
|
+
fs.rmSync(path.join(sessionDir, 'doc-reconciliation-capture.json'), { force: true });
|
|
255
|
+
}
|
|
256
|
+
runHook(payload);
|
|
257
|
+
const retained = provider.readCaptureView(sessionDir).records;
|
|
258
|
+
capture = retained.find(item => item.event === 'PreToolUse' && item.tool_use_id === payload.tool_use_id);
|
|
259
|
+
reviewCaptures = retained.filter(item => item !== capture);
|
|
260
|
+
} else {
|
|
261
|
+
capture = provider.buildCaptureRecord(payload, 'PreToolUse', root);
|
|
262
|
+
capture.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
263
|
+
reviewCaptures = options.omitReview ? [] : [reviewResult, reviewStop];
|
|
264
|
+
}
|
|
265
|
+
if (!capture && !options.allowMissingDispatchCapture) throw new Error('PreToolUse dispatch capture was not retained');
|
|
266
|
+
let seam = null;
|
|
267
|
+
if (options.realHook) {
|
|
268
|
+
seam = capture?.outlineDispatch || null;
|
|
269
|
+
} else if (options.captureBeforeReview) {
|
|
270
|
+
const noReview = internals.captureOutlineDispatch && internals.captureOutlineDispatch({
|
|
271
|
+
payload, capture, sessionDir, workspaceRoot: root, captures: [], fixtureExpectedPin: expectedPin,
|
|
272
|
+
});
|
|
273
|
+
seam = noReview;
|
|
274
|
+
// A later handoff/review cannot repair a dispatch-time miss.
|
|
275
|
+
writeJson(path.join(attemptDir, 'handoff.json'), handoff);
|
|
276
|
+
} else if (internals.captureOutlineDispatch) {
|
|
277
|
+
seam = internals.captureOutlineDispatch({
|
|
278
|
+
payload, capture, sessionDir, workspaceRoot: root, captures: reviewCaptures, fixtureExpectedPin: expectedPin,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (seam) capture.outlineDispatch = seam;
|
|
282
|
+
const workerStartPayload = {
|
|
283
|
+
session_id: sessionId, cwd: options.workerStartRoot || root, hook_event_name: 'SubagentStart',
|
|
284
|
+
agent_id: options.workerPrincipal || 'draft-worker', agent_type: 'worker', parent_tool_use_id: capture?.tool_use_id || payload.tool_use_id,
|
|
285
|
+
};
|
|
286
|
+
let workerStart;
|
|
287
|
+
if (options.realHook) {
|
|
288
|
+
runHook(workerStartPayload);
|
|
289
|
+
workerStart = provider.readCaptureView(sessionDir).records.find(item => item.event === 'SubagentStart'
|
|
290
|
+
&& item.agent_id === workerStartPayload.agent_id);
|
|
291
|
+
} else {
|
|
292
|
+
workerStart = provider.buildCaptureRecord(workerStartPayload, 'SubagentStart', root);
|
|
293
|
+
workerStart.observedIdentity = capture.observedIdentity;
|
|
294
|
+
}
|
|
295
|
+
const extraCaptures = [];
|
|
296
|
+
|
|
297
|
+
function dispatchAgain({ toolUseId = capture.tool_use_id, prompt = payload.tool_input.prompt } = {}) {
|
|
298
|
+
const extraPayload = { ...payload, tool_use_id: toolUseId, tool_input: { prompt } };
|
|
299
|
+
const extra = provider.buildCaptureRecord(extraPayload, 'PreToolUse', root);
|
|
300
|
+
extra.observedIdentity = capture.observedIdentity;
|
|
301
|
+
if (internals.captureOutlineDispatch) {
|
|
302
|
+
extra.outlineDispatch = internals.captureOutlineDispatch({
|
|
303
|
+
payload: extraPayload, capture: extra, sessionDir, workspaceRoot: root,
|
|
304
|
+
captures: reviewCaptures, fixtureExpectedPin: expectedPin,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
extraCaptures.push(extra);
|
|
308
|
+
return extra;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function finishDraft({ draft = true, nonDescendant = false, finalReview = false, baselineRoot = root,
|
|
312
|
+
omitWorkerStart = false, omitWorkerResult = false, tamperHandoff = false,
|
|
313
|
+
providerSessionId = sessionId, providerTaskId = taskId, providerBaseline = { root, commit: baselineCommit },
|
|
314
|
+
runPilot = false, handAuthoredCapture = false, removeSeam = false, mutateAfterFinalReview = false } = {}) {
|
|
315
|
+
if (draft) {
|
|
316
|
+
fs.writeFileSync(path.join(root, target), `---\nid: DOCS-001\nstatus: proposed\n---\n\n## Decision\nFinal D for row 20.\n`);
|
|
317
|
+
fixtureGit(root, ['add', '-A']);
|
|
318
|
+
fixtureGit(root, ['commit', '-q', '-m', 'D final draft']);
|
|
319
|
+
}
|
|
320
|
+
if (nonDescendant) {
|
|
321
|
+
fixtureGit(root, ['checkout', '--orphan', 'unrelated']);
|
|
322
|
+
fixtureGit(root, ['rm', '-rf', '.']);
|
|
323
|
+
fs.mkdirSync(path.dirname(path.join(root, target)), { recursive: true });
|
|
324
|
+
fs.writeFileSync(path.join(root, target), `---\nid: DOCS-001\nstatus: proposed\n---\n\n## Decision\nUnrelated D.\n`);
|
|
325
|
+
fixtureGit(root, ['add', '-A']);
|
|
326
|
+
fixtureGit(root, ['-c', 'user.name=outline-fixture', '-c', 'user.email=outline-fixture@invalid', 'commit', '-q', '-m', 'unrelated D']);
|
|
327
|
+
}
|
|
328
|
+
let draftCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
329
|
+
let draftBytes = fs.readFileSync(path.join(root, target));
|
|
330
|
+
if (tamperHandoff) {
|
|
331
|
+
const handoffFile = path.join(attemptDir, 'handoff.json');
|
|
332
|
+
const changed = JSON.parse(fs.readFileSync(handoffFile, 'utf8'));
|
|
333
|
+
changed.outline.tree = 'f'.repeat(40);
|
|
334
|
+
writeJson(handoffFile, changed);
|
|
335
|
+
}
|
|
336
|
+
if (options.realHook) {
|
|
337
|
+
if (!omitWorkerResult) runHook({
|
|
338
|
+
session_id: sessionId, cwd: root, hook_event_name: 'PostToolUse', tool_name: 'Task',
|
|
339
|
+
tool_use_id: capture.tool_use_id, tool_input: { command: 'Task' },
|
|
340
|
+
tool_response: { agentId: options.workerPrincipal || 'draft-worker', interrupted: false },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (finalReview) {
|
|
344
|
+
const finalReviewer = 'final-reviewer';
|
|
345
|
+
const finalResultPayload = {
|
|
346
|
+
session_id: sessionId, cwd: root, hook_event_name: 'PostToolUse', tool_name: 'Task',
|
|
347
|
+
tool_use_id: 'final-review-dispatch', tool_input: { command: 'Task' },
|
|
348
|
+
tool_response: { agentId: finalReviewer, interrupted: false },
|
|
349
|
+
};
|
|
350
|
+
const finalStopPayload = {
|
|
351
|
+
session_id: sessionId, cwd: root, hook_event_name: 'SubagentStop', agent_id: finalReviewer,
|
|
352
|
+
last_assistant_message: `Review verdict: approve\nReviewed artifact: ${target} sha256:${sha(draftBytes)}\nSummary: Final candidate reviewed.`,
|
|
353
|
+
};
|
|
354
|
+
if (options.realHook) {
|
|
355
|
+
runHook(finalResultPayload);
|
|
356
|
+
runHook(finalStopPayload);
|
|
357
|
+
} else {
|
|
358
|
+
const finalResult = provider.buildCaptureRecord(finalResultPayload, 'PostToolUse', root);
|
|
359
|
+
const finalStop = provider.buildCaptureRecord(finalStopPayload, 'SubagentStop', root);
|
|
360
|
+
finalResult.observedIdentity = capture.observedIdentity;
|
|
361
|
+
finalStop.observedIdentity = capture.observedIdentity;
|
|
362
|
+
extraCaptures.push(finalResult, finalStop);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (mutateAfterFinalReview) {
|
|
366
|
+
fs.writeFileSync(path.join(root, target), `---\nid: DOCS-001\nstatus: proposed\n---\n\n## Decision\nChanged after final review.\n`);
|
|
367
|
+
fixtureGit(root, ['add', '-A']);
|
|
368
|
+
fixtureGit(root, ['commit', '-q', '-m', 'D changed after review']);
|
|
369
|
+
draftCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
370
|
+
draftBytes = fs.readFileSync(path.join(root, target));
|
|
371
|
+
}
|
|
372
|
+
const captures = options.realHook
|
|
373
|
+
? provider.readCaptureView(sessionDir).records
|
|
374
|
+
: [...reviewCaptures, capture, ...extraCaptures, ...(!omitWorkerStart ? [workerStart] : []), ...(!omitWorkerResult ? [provider.buildCaptureRecord({
|
|
375
|
+
session_id: sessionId, cwd: root, hook_event_name: 'PostToolUse', tool_name: 'Task',
|
|
376
|
+
tool_use_id: capture.tool_use_id, tool_input: { command: 'Task' },
|
|
377
|
+
tool_response: { agentId: options.workerPrincipal || 'draft-worker', interrupted: false },
|
|
378
|
+
}, 'PostToolUse', root)] : [])];
|
|
379
|
+
const qualification = { source: 'fake-test-fixture', faked: true, toolUseIdPopulated: true,
|
|
380
|
+
agentIdPopulated: true, outlineDispatchCaptureVerified: true };
|
|
381
|
+
if (removeSeam && capture.outlineDispatch?.seamRef) {
|
|
382
|
+
fs.rmSync(path.join(sessionDir, capture.outlineDispatch.seamRef), { force: true });
|
|
383
|
+
}
|
|
384
|
+
if (handAuthoredCapture) {
|
|
385
|
+
const forged = { ...capture, outlineDispatch: { schema: 'harness.outline-dispatch.v1', state: 'accepted',
|
|
386
|
+
taskId, attemptId, seamRef: 'doc-reconciliation/outline-confirmation/fake-seam.json',
|
|
387
|
+
dispatchInputDigest: sha('candidate-authored') } };
|
|
388
|
+
writeJson(path.join(sessionDir, 'doc-reconciliation-capture.json'), { version: 1, records: [forged] });
|
|
389
|
+
}
|
|
390
|
+
const origin = provider.createClaudeOriginProvider({
|
|
391
|
+
hostId: 'claude-code', sessionId: providerSessionId, workspaceRoot: root, workflowId, workflowRevision, taskId: providerTaskId,
|
|
392
|
+
principals: { author: providerSessionId, executors: [] }, baseline: providerBaseline,
|
|
393
|
+
captures, sessionDir, taskBinding, qualification, outlineFixture: { expectedPin },
|
|
394
|
+
});
|
|
395
|
+
const identity = {
|
|
396
|
+
workspaceId: origin.resolveContext({}).context.workspaceId, sessionId: providerSessionId, workflowId, workflowRevision, taskId: providerTaskId,
|
|
397
|
+
taskScopeDigest: sha('task-scope'), baselineDigest: sha('baseline-snapshot'),
|
|
398
|
+
candidateDigest: sha('candidate-snapshot'), configDigest, prdRoot: 'docs/prd/',
|
|
399
|
+
};
|
|
400
|
+
const result = origin.resolveEvent({ reference: `${capture.ref}#outline-confirmation`, identity });
|
|
401
|
+
let pilotResult = null;
|
|
402
|
+
if (runPilot) {
|
|
403
|
+
const previousSwitch = process.env.HARNESS_DOC_RECONCILIATION_PILOT;
|
|
404
|
+
const previousFactory = provider.createClaudeOriginProvider;
|
|
405
|
+
provider.createClaudeOriginProvider = input => previousFactory({ ...input, qualification,
|
|
406
|
+
outlineFixture: { expectedPin }, taskBinding });
|
|
407
|
+
process.env.HARNESS_DOC_RECONCILIATION_PILOT = '1';
|
|
408
|
+
const context = { root, sessionId, sessionDir, activeHost: 'claude-code', workflow: {
|
|
409
|
+
workflowId, revision: workflowRevision, strategy: 'iterative-single', state: 'running',
|
|
410
|
+
docReconciliationBaseline: { root: baselineRoot, commit: baselineCommit, tree: baselineTree }, docReconciliationTask: taskBinding,
|
|
411
|
+
} };
|
|
412
|
+
try {
|
|
413
|
+
pilotResult = pilot.assess(context, { strategy: 'iterative-single', stepBindingsEnabled: true,
|
|
414
|
+
requirements: [{ id: 'prb', summary: 'produce an ADR', acceptance: 'outline review precedes drafting' }] });
|
|
415
|
+
} finally {
|
|
416
|
+
provider.createClaudeOriginProvider = previousFactory;
|
|
417
|
+
if (previousSwitch === undefined) delete process.env.HARNESS_DOC_RECONCILIATION_PILOT;
|
|
418
|
+
else process.env.HARNESS_DOC_RECONCILIATION_PILOT = previousSwitch;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { captures, origin, identity, draftCommit, draftBytes, result, pilot: pilotResult };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { root, sessionDir, target, taskId, attemptId, sessionId, workflowId, workflowRevision,
|
|
425
|
+
baselineCommit, baselineTree, outlineCommit, outlineTree, outlineBlob, outlineBytes, configDigest,
|
|
426
|
+
expectedPin, taskBinding, attemptDir, handoff, payload, capture, reviewCaptures, workerStart,
|
|
427
|
+
extraCaptures, dispatchAgain, finishDraft, runHook, stateHome,
|
|
428
|
+
faultMarker: path.join(stateHome, 'prb-partial-dispatch-faults-used'),
|
|
429
|
+
cleanup: () => {
|
|
430
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
431
|
+
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
432
|
+
} };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
test('strict dispatch parser accepts one exact block and rejects duplicate/free-form/alternate-field tokens', () => {
|
|
436
|
+
assert.strictEqual(typeof internals.parseOutlineDispatchPrompt, 'function', 'missing strict tool_input.prompt parser');
|
|
437
|
+
const parse = internals.parseOutlineDispatchPrompt;
|
|
438
|
+
const valid = parse(`Review complete.\n${dispatchLine}\nStart drafting after the outline review.`);
|
|
439
|
+
assert.strictEqual(valid.taskId, 'task-prb');
|
|
440
|
+
assert.strictEqual(valid.attemptId, 'attempt-1');
|
|
441
|
+
assert.strictEqual(valid.token, 'token-1');
|
|
442
|
+
assert.strictEqual(valid.workerRoot, '/tmp/prb-worker');
|
|
443
|
+
assert.strictEqual(parse(`${dispatchLine}\n${dispatchLine}`), null, 'two marker blocks are ambiguous');
|
|
444
|
+
assert.strictEqual(parse(`token-1\n${dispatchLine.replace('HARNESS_OUTLINE_DISPATCH_V1 ', '')}`), null,
|
|
445
|
+
'a free-form token or alternate-field marker cannot dispatch');
|
|
446
|
+
assert.strictEqual(parse({ prompt: dispatchLine }), null, 'only tool_input.prompt text is parsed');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('outline classifier checks every Decision section before awarding pre-draft status', () => {
|
|
450
|
+
assert.strictEqual(typeof internals.parseOutlineCitation, 'function', 'missing all-section outline completeness check');
|
|
451
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
452
|
+
const placeholder = internals.parseOutlineCitation(Buffer.from(outlineText({ decisionPlaceholder: true })), target);
|
|
453
|
+
assert.strictEqual(placeholder.state, 'accepted', 'one placeholder Decision section is an outline');
|
|
454
|
+
const duplicate = internals.parseOutlineCitation(Buffer.from(outlineText({ duplicateDecision: true })), target);
|
|
455
|
+
assert.strictEqual(duplicate.state, 'unresolved');
|
|
456
|
+
assert.strictEqual(duplicate.reason, 'outline-already-draft', 'later substantive Decision text cannot hide behind a placeholder');
|
|
457
|
+
const titled = internals.parseOutlineCitation(Buffer.from(outlineText({ titledDecision: true })), target);
|
|
458
|
+
assert.strictEqual(titled.state, 'unresolved');
|
|
459
|
+
assert.strictEqual(titled.reason, 'outline-already-draft', 'a titled Decision heading cannot evade draft detection');
|
|
460
|
+
const indented = internals.parseOutlineCitation(Buffer.from(outlineText({ indentedDecision: true })), target);
|
|
461
|
+
assert.strictEqual(indented.state, 'unresolved');
|
|
462
|
+
assert.strictEqual(indented.reason, 'outline-already-draft', 'a valid indented Markdown Decision heading cannot evade draft detection');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('coordinator-seeded inventory pin requires exact bytes, all fixed fields, and one row/domain match', () => {
|
|
466
|
+
assert.strictEqual(typeof internals.validateOutlineInventoryPin, 'function', 'missing pinned-inventory validator');
|
|
467
|
+
const content = [
|
|
468
|
+
'# Synthetic inventory',
|
|
469
|
+
'run id `synthetic-run` · input SHA `abcdef012345`',
|
|
470
|
+
'',
|
|
471
|
+
'## Mechanism inventory',
|
|
472
|
+
'| # | Mechanism | Evidence |',
|
|
473
|
+
'|---|---|---|',
|
|
474
|
+
'| 20 | ADR identity | mechanism evidence |',
|
|
475
|
+
'',
|
|
476
|
+
'## Area grouping + candidate domain prefixes',
|
|
477
|
+
'| Area | Rows | Candidate prefix | Why |',
|
|
478
|
+
'|---|---|---|---|',
|
|
479
|
+
'| Documents | 20–22 | `DOCS` | Synthetic fixture |',
|
|
480
|
+
].join('\n') + '\n';
|
|
481
|
+
const pin = {
|
|
482
|
+
source: 'synthetic://approved-inventory',
|
|
483
|
+
sha256: sha(content),
|
|
484
|
+
runId: 'synthetic-run',
|
|
485
|
+
inputSha: 'abcdef012345',
|
|
486
|
+
approvalRef: { issue: 325, commentId: 'synthetic-comment', decision: 1 },
|
|
487
|
+
retainedArtifactPath: 'inventory/' + sha(content) + '.md',
|
|
488
|
+
};
|
|
489
|
+
const valid = internals.validateOutlineInventoryPin({ bytes: Buffer.from(content), pin, expectedPin: pin });
|
|
490
|
+
assert.strictEqual(valid.state, 'accepted');
|
|
491
|
+
assert.deepStrictEqual(internals.resolveInventoryRow(valid, '20').row, { rowId: '20', domain: 'DOCS' });
|
|
492
|
+
const changed = internals.validateOutlineInventoryPin({ bytes: Buffer.from(content + '\nchanged\n'), pin, expectedPin: pin });
|
|
493
|
+
assert.strictEqual(changed.state, 'unresolved', 'a self-consistent-looking replacement cannot match the pinned bytes');
|
|
494
|
+
const stale = internals.validateOutlineInventoryPin({ bytes: Buffer.from(content), pin: { ...pin, runId: 'stale-run' }, expectedPin: pin });
|
|
495
|
+
assert.strictEqual(stale.state, 'unresolved', 'stale metadata cannot be repaired by matching rows');
|
|
496
|
+
const staleInput = internals.validateOutlineInventoryPin({ bytes: Buffer.from(content), pin: { ...pin, inputSha: 'feedface' }, expectedPin: pin });
|
|
497
|
+
assert.strictEqual(staleInput.state, 'unresolved', 'stale input revision is not a fallback candidate');
|
|
498
|
+
const unavailableLocator = internals.validateOutlineInventoryPin({ bytes: Buffer.from(content),
|
|
499
|
+
pin: { ...pin, retainedArtifactPath: 'repo-local-copy.md' }, expectedPin: pin });
|
|
500
|
+
assert.strictEqual(unavailableLocator.state, 'unresolved', 'an unavailable locator has no candidate-selected fallback');
|
|
501
|
+
const missing = internals.validateOutlineInventoryPin({ bytes: null, pin, expectedPin: pin });
|
|
502
|
+
assert.strictEqual(missing.state, 'unresolved', 'unavailable source has no fallback');
|
|
503
|
+
const duplicate = content.replace('| 20 | ADR identity', '| 20 | duplicate\n| 20 | ADR identity');
|
|
504
|
+
const duplicatePin = { ...pin, sha256: sha(duplicate), retainedArtifactPath: 'inventory/' + sha(duplicate) + '.md' };
|
|
505
|
+
const duplicateResult = internals.validateOutlineInventoryPin({ bytes: Buffer.from(duplicate), pin: duplicatePin, expectedPin: duplicatePin });
|
|
506
|
+
assert.strictEqual(internals.resolveInventoryRow(duplicateResult, '20').state, 'unresolved', 'duplicate row ids are ambiguous');
|
|
507
|
+
const multiDomain = content.replace('| Documents | 20–22 | `DOCS` | Synthetic fixture |',
|
|
508
|
+
'| Documents | 20–22 | `DOCS` | Synthetic fixture |\n| Other documents | 20 | `ENFC` | Ambiguous mapping |');
|
|
509
|
+
const multiDomainPin = { ...pin, sha256: sha(multiDomain), retainedArtifactPath: 'inventory/' + sha(multiDomain) + '.md' };
|
|
510
|
+
const multiDomainResult = internals.validateOutlineInventoryPin({ bytes: Buffer.from(multiDomain),
|
|
511
|
+
pin: multiDomainPin, expectedPin: multiDomainPin });
|
|
512
|
+
assert.strictEqual(internals.resolveInventoryRow(multiDomainResult, '20').state, 'unresolved', 'ambiguous domain mapping is unresolved');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('generic successful execution or waiver cannot pass outline-confirmation', () => {
|
|
516
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
517
|
+
const digest = sha('final D bytes');
|
|
518
|
+
const baselineDigest = sha('original B');
|
|
519
|
+
const obligation = {
|
|
520
|
+
id: 'outline-confirmation', capability: 'outline-confirmation', strength: 'MUST',
|
|
521
|
+
applicability: 'applicable', scope: { paths: [target] },
|
|
522
|
+
};
|
|
523
|
+
const genericExecution = {
|
|
524
|
+
id: 'generic-exec', obligationId: 'outline-confirmation', kind: 'execution',
|
|
525
|
+
command: 'echo done', exitCode: 0, paths: [target], artifactDigests: { [target]: digest },
|
|
526
|
+
baseDigest: baselineDigest, provenance: 'ordinary shell command',
|
|
527
|
+
};
|
|
528
|
+
const genericWaiver = {
|
|
529
|
+
id: 'generic-waiver', obligationId: 'outline-confirmation', kind: 'waiver', reason: 'approved',
|
|
530
|
+
citedRecordIds: ['generic-exec'], paths: [target], artifactDigests: { [target]: digest }, baseDigest: baselineDigest,
|
|
531
|
+
};
|
|
532
|
+
const output = checkDocObligations({
|
|
533
|
+
obligations: [obligation], artifacts: { [target]: { digest } }, baseline: { digest: baselineDigest },
|
|
534
|
+
executionRecords: [genericExecution], reviewRecords: [genericWaiver],
|
|
535
|
+
});
|
|
536
|
+
assert.notStrictEqual(output.results[0].outcome, 'pass', 'outline-confirmation must bypass generic execution and waiver lanes');
|
|
537
|
+
assert.strictEqual(output.results[0].observation, 'unresolved');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('outline checker reports a validated O/review/handoff/seam and strict O-to-D as a separate result', () => {
|
|
541
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
542
|
+
const digest = sha('final D bytes');
|
|
543
|
+
const baselineDigest = sha('original B');
|
|
544
|
+
const evidence = {
|
|
545
|
+
schema: 'harness.outline-confirmation.v1', state: 'validated',
|
|
546
|
+
taskId: 'task-prb', attemptId: 'attempt-1', inventoryRowId: '20', domain: 'DOCS',
|
|
547
|
+
inventoryPinDigest: sha('pin'), inventoryArtifactDigest: sha('inventory'),
|
|
548
|
+
baselineCommit: 'a'.repeat(40), outlineCommit: 'b'.repeat(40), draftCommit: 'c'.repeat(40), mergedCommit: 'c'.repeat(40),
|
|
549
|
+
handoffDigest: sha('handoff'), reviewDigest: sha('review'), seamDigest: sha('seam'),
|
|
550
|
+
reviewCaptureDigest: sha('review-capture'), dispatchInputDigest: sha('dispatch'), writerLeaseDigest: sha('lease'),
|
|
551
|
+
dispatchToolUseId: 'dispatch-1',
|
|
552
|
+
ancestry: 'strict-descendant', targetPaths: [target],
|
|
553
|
+
mergeCorrespondence: null,
|
|
554
|
+
outlineArtifactDigests: { [target]: sha('outline O bytes') },
|
|
555
|
+
draftArtifactDigests: { [target]: digest },
|
|
556
|
+
reviewerPrincipal: 'reviewer-agent', coordinatorPrincipal: 'author-session', workerPrincipal: 'worker-agent',
|
|
557
|
+
};
|
|
558
|
+
const output = checkDocObligations({
|
|
559
|
+
obligations: [{ id: 'outline-confirmation', capability: 'outline-confirmation', strength: 'MUST',
|
|
560
|
+
applicability: 'applicable', scope: { paths: [target] } }],
|
|
561
|
+
artifacts: { [target]: { digest } }, baseline: { digest: baselineDigest }, executionRecords: [],
|
|
562
|
+
reviewRecords: [{ id: 'outline-event', obligationId: 'outline-confirmation', kind: 'outline-confirmation',
|
|
563
|
+
provenance: 'validated pre-draft outline evidence', paths: [target], baseDigest: baselineDigest,
|
|
564
|
+
artifactDigests: { [target]: digest }, outlineEvidence: evidence }],
|
|
565
|
+
});
|
|
566
|
+
assert.strictEqual(output.results[0].outcome, 'pass', JSON.stringify({ result: output.results[0], diagnostics: output.diagnostics }));
|
|
567
|
+
assert.strictEqual(output.results[0].observation, 'observed');
|
|
568
|
+
const incomplete = checkDocObligations({
|
|
569
|
+
obligations: [{ id: 'outline-confirmation', capability: 'outline-confirmation', strength: 'MUST',
|
|
570
|
+
applicability: 'applicable', scope: { paths: [target] } }],
|
|
571
|
+
artifacts: { [target]: { digest } }, baseline: { digest: baselineDigest }, executionRecords: [], reviewRecords: [],
|
|
572
|
+
});
|
|
573
|
+
assert.strictEqual(incomplete.results[0].outcome, 'none');
|
|
574
|
+
assert.strictEqual(incomplete.results[0].observation, 'unresolved', 'missing D remains incomplete');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('PreToolUse capture stores only narrow dispatch evidence and remains fail-open when handoff is absent', () => {
|
|
578
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-outline-capture-'));
|
|
579
|
+
const stateHome = path.join(root, 'state');
|
|
580
|
+
const workspace = path.join(root, 'workspace');
|
|
581
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
582
|
+
try {
|
|
583
|
+
const payload = {
|
|
584
|
+
session_id: 'outline-author', cwd: workspace, hook_event_name: 'PreToolUse',
|
|
585
|
+
tool_name: 'Task', tool_use_id: 'dispatch-1', tool_input: { prompt: dispatchLine, alternatePrompt: dispatchLine },
|
|
586
|
+
};
|
|
587
|
+
const driven = spawnSync(process.execPath, [path.join(ROOT, 'hooks/scripts/state-persist.js')], {
|
|
588
|
+
cwd: workspace, encoding: 'utf8', input: JSON.stringify(payload),
|
|
589
|
+
env: { ...process.env, HARNESS_STATE_HOME: stateHome, HARNESS_WORKSPACE_ROOT: workspace, HARNESS_DOC_RECONCILIATION_PILOT: '1' },
|
|
590
|
+
});
|
|
591
|
+
assert.strictEqual(driven.status, 0, 'capture failure remains fail-open to the host');
|
|
592
|
+
const journals = [];
|
|
593
|
+
const walk = dir => {
|
|
594
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
595
|
+
const full = path.join(dir, entry.name);
|
|
596
|
+
if (entry.isDirectory()) walk(full);
|
|
597
|
+
else if (entry.name === 'doc-reconciliation-capture.json') journals.push(full);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
walk(stateHome);
|
|
601
|
+
assert.strictEqual(journals.length, 1, 'real state-persist hook captured the dispatch');
|
|
602
|
+
const capture = JSON.parse(fs.readFileSync(journals[0], 'utf8')).records[0];
|
|
603
|
+
assert.strictEqual(capture.outlineDispatch.state, 'unresolved', 'missing handoff cannot earn later credit');
|
|
604
|
+
assert.ok(!Object.hasOwn(capture, 'prompt'), 'raw prompt text is not retained');
|
|
605
|
+
assert.ok(/^[a-f0-9]{64}$/.test(capture.outlineDispatch.promptDigest));
|
|
606
|
+
} finally {
|
|
607
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('seam persistence failure consumes the token, keeps tools fail-open, and cannot be repaired on retry', () => {
|
|
612
|
+
const fixture = createOutlineFixture({ realHook: true, blockSeamStorage: true });
|
|
613
|
+
try {
|
|
614
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'unresolved');
|
|
615
|
+
const tokenDigest = sha('token-1');
|
|
616
|
+
const tokenFile = path.join(fixture.attemptDir, 'tokens', `${tokenDigest}.json`);
|
|
617
|
+
assert.ok(fs.existsSync(tokenFile), 'token is consumed before the seam write can fail');
|
|
618
|
+
const replay = fixture.runHook(fixture.payload);
|
|
619
|
+
const last = replay.filter(item => item.event === 'PreToolUse').at(-1);
|
|
620
|
+
assert.strictEqual(last.outlineDispatch.state, 'unresolved', 'retry cannot recreate a missing seam');
|
|
621
|
+
assert.ok(fs.existsSync(tokenFile));
|
|
622
|
+
} finally { fixture.cleanup(); }
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('failed token persistence invalidates the attempt; same-attempt retry requires a fresh token/attempt', () => {
|
|
626
|
+
const fixture = createOutlineFixture({ realHook: true, blockTokenStorage: true });
|
|
627
|
+
try {
|
|
628
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'unresolved');
|
|
629
|
+
const blocked = path.join(fixture.attemptDir, 'tokens');
|
|
630
|
+
assert.ok(fs.existsSync(blocked));
|
|
631
|
+
fs.rmSync(blocked, { force: true });
|
|
632
|
+
const retried = fixture.runHook(fixture.payload);
|
|
633
|
+
const latest = retried.filter(item => item.event === 'PreToolUse').at(-1);
|
|
634
|
+
assert.strictEqual(latest.outlineDispatch.state, 'unresolved', 'an earlier failed token write invalidates this attempt');
|
|
635
|
+
assert.strictEqual(fs.existsSync(path.join(fixture.attemptDir, 'seams', `${sha(String(fixture.capture.tool_use_id))}.json`)), false);
|
|
636
|
+
} finally { fixture.cleanup(); }
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test('configured PreToolUse capture observes competing file writers before completion callbacks', () => {
|
|
640
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'hooks/hooks.json'), 'utf8'));
|
|
641
|
+
const captureHook = manifest.hooks?.PreToolUse?.find(item => item.id === 'harness:pre:pilot-capture');
|
|
642
|
+
const matcher = captureHook?.matcher || '';
|
|
643
|
+
const results = [];
|
|
644
|
+
for (const tool of ['Write', 'Edit', 'apply_patch']) {
|
|
645
|
+
const fixture = createOutlineFixture({ realHook: true });
|
|
646
|
+
try {
|
|
647
|
+
const toolUseId = `competing-${tool}`;
|
|
648
|
+
const input = tool === 'Write' ? { file_path: fixture.target, content: 'competing writer' }
|
|
649
|
+
: tool === 'Edit' ? { file_path: fixture.target, old_string: 'before', new_string: 'after' }
|
|
650
|
+
: { patch: '*** Begin Patch\n*** Update File: docs/adr/DOCS-001.md\n*** End Patch\n' };
|
|
651
|
+
const captures = fixture.runHook({ session_id: fixture.sessionId, cwd: fixture.root,
|
|
652
|
+
hook_event_name: 'PreToolUse', tool_name: tool, tool_use_id: toolUseId,
|
|
653
|
+
agent_id: `competing-principal-${tool}`, agent_type: 'worker', tool_input: input });
|
|
654
|
+
const start = captures.find(item => item.event === 'PreToolUse' && item.tool_use_id === toolUseId);
|
|
655
|
+
const completed = fixture.finishDraft({ runPilot: true });
|
|
656
|
+
results.push({ tool, manifestRouted: matcher.split('|').includes(tool), captured: Boolean(start),
|
|
657
|
+
competingIdentityRetained: start?.agent_id === `competing-principal-${tool}`,
|
|
658
|
+
classifierRejected: JSON.stringify(completed.result).includes('outline-writer-activity-ambiguous'),
|
|
659
|
+
noOutlinePass: completed.pilot?.report?.outlineEvidence?.outcome !== 'pass' });
|
|
660
|
+
} finally { fixture.cleanup(); }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const positive = createOutlineFixture({ realHook: true });
|
|
664
|
+
let positiveResult;
|
|
665
|
+
try {
|
|
666
|
+
const captures = positive.runHook({ session_id: positive.sessionId, cwd: positive.root,
|
|
667
|
+
hook_event_name: 'PreToolUse', tool_name: 'Write', tool_use_id: 'worker-write',
|
|
668
|
+
agent_id: 'draft-worker', agent_type: 'worker',
|
|
669
|
+
tool_input: { file_path: positive.target, content: 'the correlated worker writes' } });
|
|
670
|
+
const start = captures.find(item => item.event === 'PreToolUse' && item.tool_use_id === 'worker-write');
|
|
671
|
+
const completed = positive.finishDraft({ runPilot: true });
|
|
672
|
+
positiveResult = { manifestRouted: matcher.split('|').includes('Write'), captured: Boolean(start),
|
|
673
|
+
eligible: completed.pilot?.report?.outlineEvidence?.outcome === 'pass' };
|
|
674
|
+
} finally { positive.cleanup(); }
|
|
675
|
+
|
|
676
|
+
const off = createOutlineFixture({ realHook: true });
|
|
677
|
+
let offRemainsWriteFree;
|
|
678
|
+
try {
|
|
679
|
+
const before = provider.readCaptureView(off.sessionDir).records.length;
|
|
680
|
+
off.runHook({ session_id: off.sessionId, cwd: off.root, hook_event_name: 'PreToolUse',
|
|
681
|
+
tool_name: 'Write', tool_use_id: 'pilot-off-write', tool_input: { file_path: off.target, content: 'off' } }, false);
|
|
682
|
+
offRemainsWriteFree = provider.readCaptureView(off.sessionDir).records.length === before;
|
|
683
|
+
} finally { off.cleanup(); }
|
|
684
|
+
|
|
685
|
+
assert.strictEqual(offRemainsWriteFree, true, 'pilot OFF must remain capture-write-free for file tools');
|
|
686
|
+
assert.strictEqual(positiveResult.eligible, true,
|
|
687
|
+
`the correlated worker remains eligible as a positive control: ${JSON.stringify(positiveResult)}`);
|
|
688
|
+
assert.ok(results.every(item => item.manifestRouted && item.captured && item.competingIdentityRetained
|
|
689
|
+
&& item.classifierRejected && item.noOutlinePass)
|
|
690
|
+
&& positiveResult.manifestRouted && positiveResult.captured,
|
|
691
|
+
`each configured file-writer start must reach the classifier without a completion callback: ${JSON.stringify({ results, positiveResult })}`);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('partial seal without token, seam, or retained failure cannot be reused; a fresh successor can recover', () => {
|
|
695
|
+
function faultedFixture() {
|
|
696
|
+
return createOutlineFixture({ realHook: true, failPartialDispatchStorage: true,
|
|
697
|
+
allowMissingDispatchCapture: true });
|
|
698
|
+
}
|
|
699
|
+
function firstFailureIsUnrecorded(fixture) {
|
|
700
|
+
const token = path.join(fixture.attemptDir, 'tokens', `${sha('token-1')}.json`);
|
|
701
|
+
const seam = path.join(fixture.attemptDir, 'seams', `${sha('dispatch-1')}.json`);
|
|
702
|
+
return fs.existsSync(path.join(fixture.attemptDir, 'handoff-seal.json'))
|
|
703
|
+
&& fs.existsSync(fixture.faultMarker) && !fs.existsSync(token) && !fs.existsSync(seam)
|
|
704
|
+
&& !provider.readCaptureView(fixture.sessionDir).records.some(item => item.tool_use_id === 'dispatch-1');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const sameId = faultedFixture();
|
|
708
|
+
let sameIdRetry;
|
|
709
|
+
let successor;
|
|
710
|
+
try {
|
|
711
|
+
const initialState = firstFailureIsUnrecorded(sameId);
|
|
712
|
+
const sameIdRecords = sameId.runHook(sameId.payload);
|
|
713
|
+
const sameIdCapture = sameIdRecords.filter(item => item.event === 'PreToolUse'
|
|
714
|
+
&& item.tool_use_id === sameId.payload.tool_use_id).at(-1);
|
|
715
|
+
sameIdRetry = { retry: sameIdCapture?.outlineDispatch || null, retryCaptured: Boolean(sameIdCapture), initialState };
|
|
716
|
+
const secondAttemptId = 'attempt-2';
|
|
717
|
+
const secondAttemptDir = path.join(path.dirname(sameId.attemptDir), secondAttemptId);
|
|
718
|
+
const lease = { schema: 'harness.outline-writer-lease.v1', id: 'lease-2', taskId: sameId.taskId,
|
|
719
|
+
attemptId: secondAttemptId, principalId: sameId.sessionId, root: sameId.root, exclusive: true, active: true };
|
|
720
|
+
const leaseFile = path.join(secondAttemptDir, 'writer-lease.json');
|
|
721
|
+
writeJson(leaseFile, lease);
|
|
722
|
+
writeJson(path.join(secondAttemptDir, 'handoff.json'), { ...sameId.handoff,
|
|
723
|
+
attemptId: secondAttemptId, handoffId: 'handoff-2', supersedesAttemptId: sameId.attemptId,
|
|
724
|
+
draftDispatchToken: 'token-2', writerLeaseId: lease.id,
|
|
725
|
+
writerLeaseDigest: sha(fs.readFileSync(leaseFile)) });
|
|
726
|
+
const successorPayload = { ...sameId.payload, tool_use_id: 'dispatch-successor', tool_input: {
|
|
727
|
+
prompt: `HARNESS_OUTLINE_DISPATCH_V1 taskId=${sameId.taskId} attemptId=${secondAttemptId} token=token-2 workerRoot=${sameId.root}` } };
|
|
728
|
+
const successorCapture = sameId.runHook(successorPayload).find(item => item.event === 'PreToolUse'
|
|
729
|
+
&& item.tool_use_id === successorPayload.tool_use_id);
|
|
730
|
+
successor = successorCapture?.outlineDispatch || null;
|
|
731
|
+
sameIdRetry = { ...sameIdRetry, successor };
|
|
732
|
+
} finally { sameId.cleanup(); }
|
|
733
|
+
|
|
734
|
+
const differentId = faultedFixture();
|
|
735
|
+
let differentIdRetry;
|
|
736
|
+
try {
|
|
737
|
+
const initialState = firstFailureIsUnrecorded(differentId);
|
|
738
|
+
const retryPayload = { ...differentId.payload, tool_use_id: 'dispatch-different-id' };
|
|
739
|
+
const retryCapture = differentId.runHook(retryPayload).find(item => item.event === 'PreToolUse'
|
|
740
|
+
&& item.tool_use_id === retryPayload.tool_use_id);
|
|
741
|
+
differentIdRetry = { initialState, retry: retryCapture?.outlineDispatch || null, retryCaptured: Boolean(retryCapture),
|
|
742
|
+
tokenCreated: fs.existsSync(path.join(differentId.attemptDir, 'tokens', `${sha('token-1')}.json`)),
|
|
743
|
+
seamCreated: fs.existsSync(path.join(differentId.attemptDir, 'seams', `${sha(retryPayload.tool_use_id)}.json`)) };
|
|
744
|
+
} finally { differentId.cleanup(); }
|
|
745
|
+
|
|
746
|
+
assert.ok(sameIdRetry.initialState && differentIdRetry.initialState,
|
|
747
|
+
`fault injection must leave only each seal, losing both token and failure capture: ${JSON.stringify({ sameIdRetry, differentIdRetry })}`);
|
|
748
|
+
assert.strictEqual(sameIdRetry.successor?.state, 'accepted',
|
|
749
|
+
`a fresh successor attempt can recover while retaining its interrupted predecessor: ${JSON.stringify(sameIdRetry.successor)}`);
|
|
750
|
+
assert.ok(sameIdRetry.retry?.state === 'unresolved' && differentIdRetry.retry?.state === 'unresolved'
|
|
751
|
+
&& !differentIdRetry.tokenCreated && !differentIdRetry.seamCreated,
|
|
752
|
+
`neither same-ID nor different-ID retry may reconstruct the interrupted seam: ${JSON.stringify({ sameIdRetry, differentIdRetry })}`);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('pilot exposes a real-capture-only outline reference resolver', () => {
|
|
756
|
+
assert.strictEqual(typeof pilot._internals?.outlineDispatchReferences, 'function',
|
|
757
|
+
'missing pilot reference discovery from retained capture records');
|
|
758
|
+
const resolve = pilot._internals.outlineDispatchReferences;
|
|
759
|
+
const captured = [{
|
|
760
|
+
ref: 'PreToolUse:fixture', event: 'PreToolUse', tool_name: 'Task', tool_use_id: 'dispatch-1',
|
|
761
|
+
outlineDispatch: { state: 'accepted', taskId: 'task-prb', attemptId: 'attempt-1' },
|
|
762
|
+
}];
|
|
763
|
+
assert.deepStrictEqual(resolve(captured, 'task-prb'), ['PreToolUse:fixture#outline-confirmation']);
|
|
764
|
+
assert.deepStrictEqual(resolve([], 'task-prb'), [], 'hand-authored JSON with no host capture creates no event ref');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test('pre-dispatch seam requires review, exact worker root, clean O tree, lease, pin, and immutable handoff', () => {
|
|
768
|
+
assert.strictEqual(typeof internals.captureOutlineDispatch, 'function', 'missing synchronous pre-dispatch seam');
|
|
769
|
+
const differentWorkerRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-outline-different-worker-'));
|
|
770
|
+
try {
|
|
771
|
+
for (const [name, options, reason] of [
|
|
772
|
+
['dispatch before review cannot be repaired by later review/handoff', { captureBeforeReview: true }, 'review was absent at dispatch'],
|
|
773
|
+
['draft worktree mutation after handoff is caught before dispatch', { dirtyBeforeDispatch: true }, 'worker root was not clean at O'],
|
|
774
|
+
['missing writer identity or lease stays unresolved', { omitHandoff: true }, 'handoff/lease absent'],
|
|
775
|
+
['a no-force-push policy is not evidence of pre-draft ordering', { omitHandoff: true }, 'no force-push cannot replace a handoff'],
|
|
776
|
+
['prompt token must exactly equal the coordinator handoff token', { handoffOverrides: { draftDispatchToken: 'coordinator-token' } }, 'prompt token is not handoff-issued'],
|
|
777
|
+
['an outline already containing a complete decision is not pre-draft evidence', { completeOutline: true }, 'O is already a draft'],
|
|
778
|
+
['a second substantive Decision heading cannot hide after a placeholder section', { duplicateDecision: true }, 'later Decision text makes O a draft'],
|
|
779
|
+
['a one-to-three-space indented Markdown Decision heading is still substantive', { indentedDecision: true }, 'indented Decision text makes O a draft'],
|
|
780
|
+
['target present at B cannot be treated as a new document', { targetExistsAtB: true }, 'target existed at original B'],
|
|
781
|
+
['same author and reviewer is not independent', { reviewerPrincipal: 'author-session' }, 'reviewer is the author'],
|
|
782
|
+
['wrong row or domain mapping does not resolve inventory scope', { rowId: '99' }, 'row is absent from pinned inventory'],
|
|
783
|
+
]) {
|
|
784
|
+
const fixture = createOutlineFixture(options);
|
|
785
|
+
try {
|
|
786
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'unresolved', reason);
|
|
787
|
+
const later = fixture.finishDraft().result;
|
|
788
|
+
assert.notStrictEqual(later.status, 'accepted', `${name}: later evidence cannot repair the seam`);
|
|
789
|
+
} finally { fixture.cleanup(); }
|
|
790
|
+
}
|
|
791
|
+
const wrongRoot = createOutlineFixture({ markerRoot: differentWorkerRoot });
|
|
792
|
+
try {
|
|
793
|
+
assert.notStrictEqual(canonical(wrongRoot.root), canonical(differentWorkerRoot), 'negative uses an existing, distinct directory');
|
|
794
|
+
assert.strictEqual(wrongRoot.capture.outlineDispatch.state, 'unresolved', 'a different existing root stays unresolved');
|
|
795
|
+
assert.notStrictEqual(wrongRoot.finishDraft().result.status, 'accepted', 'later evidence cannot repair the root mismatch');
|
|
796
|
+
} finally { wrongRoot.cleanup(); }
|
|
797
|
+
} finally { fs.rmSync(differentWorkerRoot, { recursive: true, force: true }); }
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('canonical root identity accepts an alias across marker, handoff, lease, and worker capture', () => {
|
|
801
|
+
const fixture = createOutlineFixture({ workerRootAlias: true });
|
|
802
|
+
try {
|
|
803
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'accepted', JSON.stringify(fixture.capture.outlineDispatch));
|
|
804
|
+
assert.strictEqual(fixture.finishDraft().result.status, 'accepted', 'worker CWD and aliased root identify one directory');
|
|
805
|
+
} finally { fixture.cleanup(); }
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test('one-use token and tool correlation allow only an identical same-ID duplicate', () => {
|
|
809
|
+
assert.strictEqual(typeof internals.captureOutlineDispatch, 'function', 'missing token-consuming seam');
|
|
810
|
+
const duplicate = createOutlineFixture();
|
|
811
|
+
try {
|
|
812
|
+
const same = duplicate.dispatchAgain();
|
|
813
|
+
assert.strictEqual(same.outlineDispatch.state, 'accepted', JSON.stringify(same.outlineDispatch));
|
|
814
|
+
assert.strictEqual(same.outlineDispatch.duplicate, true);
|
|
815
|
+
assert.strictEqual(duplicate.finishDraft().result.status, 'accepted', 'same ID and same input deduplicate to one seam');
|
|
816
|
+
} finally { duplicate.cleanup(); }
|
|
817
|
+
|
|
818
|
+
for (const [name, replay] of [
|
|
819
|
+
['same token on a different tool_use_id', { toolUseId: 'dispatch-2' }],
|
|
820
|
+
['same tool_use_id with changed dispatch input', { prompt: dispatchLine + '\nchanged input' }],
|
|
821
|
+
]) {
|
|
822
|
+
const fixture = createOutlineFixture();
|
|
823
|
+
try {
|
|
824
|
+
const replayed = fixture.dispatchAgain(replay);
|
|
825
|
+
assert.strictEqual(replayed.outlineDispatch.state, 'unresolved', name);
|
|
826
|
+
const completed = fixture.finishDraft();
|
|
827
|
+
assert.notStrictEqual(completed.result.status, 'accepted', `${name} cannot earn later credit: ${JSON.stringify(completed.captures.filter(item => item.event === 'PreToolUse').map(item => ({ id: item.tool_use_id, dispatch: item.outlineDispatch })))}`);
|
|
828
|
+
} finally { fixture.cleanup(); }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const consumed = createOutlineFixture();
|
|
832
|
+
try {
|
|
833
|
+
const seamFile = path.join(consumed.attemptDir, 'seams', `${sha(consumed.capture.tool_use_id)}.json`);
|
|
834
|
+
fs.rmSync(seamFile, { force: true });
|
|
835
|
+
const retry = consumed.dispatchAgain();
|
|
836
|
+
assert.strictEqual(retry.outlineDispatch.state, 'unresolved', 'a consumed token with a missing seam is never recreated');
|
|
837
|
+
} finally { consumed.cleanup(); }
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test('outline evidence binds exact task, B, O blobs, independent review, worker start/result, and strict O-to-D', () => {
|
|
841
|
+
assert.strictEqual(typeof internals.captureOutlineDispatch, 'function', 'missing pre-dispatch evidence');
|
|
842
|
+
const valid = createOutlineFixture();
|
|
843
|
+
try {
|
|
844
|
+
const completed = valid.finishDraft({ finalReview: true });
|
|
845
|
+
assert.strictEqual(valid.capture.outlineDispatch.state, 'accepted');
|
|
846
|
+
assert.strictEqual(completed.result.status, 'accepted', JSON.stringify(completed.result));
|
|
847
|
+
assert.strictEqual(completed.result.event.capability, 'outline-confirmation');
|
|
848
|
+
} finally { valid.cleanup(); }
|
|
849
|
+
|
|
850
|
+
const placeholder = createOutlineFixture({ decisionPlaceholder: true });
|
|
851
|
+
try {
|
|
852
|
+
assert.strictEqual(placeholder.capture.outlineDispatch.state, 'accepted',
|
|
853
|
+
'one placeholder Decision heading is still a valid outline');
|
|
854
|
+
assert.strictEqual(placeholder.finishDraft().result.status, 'accepted');
|
|
855
|
+
} finally { placeholder.cleanup(); }
|
|
856
|
+
|
|
857
|
+
const delayedWriter = createOutlineFixture();
|
|
858
|
+
try {
|
|
859
|
+
delayedWriter.reviewCaptures.unshift(provider.buildCaptureRecord({
|
|
860
|
+
session_id: delayedWriter.sessionId, cwd: delayedWriter.root, hook_event_name: 'PostToolUse',
|
|
861
|
+
tool_name: 'Edit', tool_use_id: 'delayed-edit', agent_id: delayedWriter.sessionId,
|
|
862
|
+
tool_input: { file_path: delayedWriter.target }, tool_response: { interrupted: false },
|
|
863
|
+
}, 'PostToolUse', delayedWriter.root));
|
|
864
|
+
assert.notStrictEqual(delayedWriter.finishDraft().result.status, 'accepted',
|
|
865
|
+
'a delayed write/edit callback from the coordinator remains visible and blocks ambiguous writer history');
|
|
866
|
+
} finally { delayedWriter.cleanup(); }
|
|
867
|
+
|
|
868
|
+
const unqualified = createOutlineFixture();
|
|
869
|
+
try {
|
|
870
|
+
const completed = unqualified.finishDraft();
|
|
871
|
+
const noRetainedMapping = provider.createClaudeOriginProvider({
|
|
872
|
+
hostId: 'claude-code', sessionId: unqualified.sessionId, workspaceRoot: unqualified.root,
|
|
873
|
+
workflowId: unqualified.workflowId, workflowRevision: unqualified.workflowRevision, taskId: unqualified.taskId,
|
|
874
|
+
principals: { author: unqualified.sessionId, executors: [] },
|
|
875
|
+
baseline: { root: unqualified.root, commit: unqualified.baselineCommit },
|
|
876
|
+
captures: completed.captures, sessionDir: unqualified.sessionDir, taskBinding: unqualified.taskBinding,
|
|
877
|
+
qualification: { source: 'fake-test-fixture', faked: true, toolUseIdPopulated: true, agentIdPopulated: true },
|
|
878
|
+
outlineFixture: { expectedPin: unqualified.expectedPin },
|
|
879
|
+
});
|
|
880
|
+
assert.notStrictEqual(noRetainedMapping.resolveEvent({ reference: `${unqualified.capture.ref}#outline-confirmation`,
|
|
881
|
+
identity: completed.identity }).status, 'accepted', 'unqualified live prompt/root mapping never earns positive credit');
|
|
882
|
+
} finally { unqualified.cleanup(); }
|
|
883
|
+
|
|
884
|
+
for (const [name, fixtureOptions, finishOptions] of [
|
|
885
|
+
['wrong task', {}, { providerTaskId: 'task-other' }],
|
|
886
|
+
['wrong session after restart', {}, { providerSessionId: 'different-session' }],
|
|
887
|
+
['wrong baseline/config/O tree/blob binding', { handoffOverrides: { configDigest: 'f'.repeat(64) } }, {}],
|
|
888
|
+
['missing O Git object', { handoffOverrides: { outline: { commit: 'f'.repeat(40), tree: 'e'.repeat(40), targetBlobs: {} } } }, {}],
|
|
889
|
+
['wrong target path binding', { handoffOverrides: { targetPaths: ['docs/adr/DOCS-999.md'] } }, {}],
|
|
890
|
+
['wrong O tree/blob binding', { handoffOverrides: { outline: { commit: 'a'.repeat(40), tree: 'f'.repeat(40), targetBlobs: {} } } }, {}],
|
|
891
|
+
['missing Git object', {}, { draft: false }],
|
|
892
|
+
['D is not a strict descendant of O', {}, { nonDescendant: true }],
|
|
893
|
+
['no final D', {}, { draft: false }],
|
|
894
|
+
['worker start/root/principal absent', {}, { omitWorkerStart: true }],
|
|
895
|
+
['eventual Task result absent', {}, { omitWorkerResult: true }],
|
|
896
|
+
['outline reviewer is also the draft worker', { reviewerPrincipal: 'draft-worker' }, {}],
|
|
897
|
+
['unknown competing writer has no positive lease evidence', { competingWriters: true }, {}],
|
|
898
|
+
]) {
|
|
899
|
+
const fixture = createOutlineFixture(fixtureOptions);
|
|
900
|
+
try {
|
|
901
|
+
const completed = fixture.finishDraft(finishOptions);
|
|
902
|
+
assert.notStrictEqual(completed.result.status, 'accepted', `${name} is rejected or unresolved`);
|
|
903
|
+
} finally { fixture.cleanup(); }
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test('baseline integrity, callback order, delayed result, stash/restore, and unobservable writer channels stay unresolved', () => {
|
|
908
|
+
assert.strictEqual(typeof internals.captureOutlineDispatch, 'function', 'missing pre-dispatch seam validation');
|
|
909
|
+
for (const [name, fixtureOptions] of [
|
|
910
|
+
['a write before review', { dirtyBeforeDispatch: true }],
|
|
911
|
+
['a missing pre-dispatch handoff', { omitHandoff: true }],
|
|
912
|
+
['a target deleted and re-added after it existed at B', { targetExistsAtB: true }],
|
|
913
|
+
['a complete draft merely labelled as an outline', { completeOutline: true }],
|
|
914
|
+
]) {
|
|
915
|
+
const fixture = createOutlineFixture(fixtureOptions);
|
|
916
|
+
try {
|
|
917
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'unresolved', name);
|
|
918
|
+
const delayed = fixture.finishDraft({ omitWorkerResult: true });
|
|
919
|
+
assert.notStrictEqual(delayed.result.status, 'accepted', `${name}; delayed callback/result cannot repair the seam`);
|
|
920
|
+
} finally { fixture.cleanup(); }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const movedBaseline = createOutlineFixture({
|
|
924
|
+
handoffOverrides: { baseline: { root: '/tmp/other-root', commit: 'd'.repeat(40), tree: 'e'.repeat(40), configDigest: 'f'.repeat(64) } },
|
|
925
|
+
});
|
|
926
|
+
try {
|
|
927
|
+
assert.strictEqual(movedBaseline.capture.outlineDispatch.state, 'unresolved', 'B cannot be reset to O or another root');
|
|
928
|
+
} finally { movedBaseline.cleanup(); }
|
|
929
|
+
|
|
930
|
+
const changedAfterReview = createOutlineFixture();
|
|
931
|
+
try {
|
|
932
|
+
fs.writeFileSync(path.join(changedAfterReview.root, changedAfterReview.target), 'changed after O review');
|
|
933
|
+
assert.strictEqual(internals.captureOutlineDispatch({
|
|
934
|
+
payload: changedAfterReview.payload, capture: changedAfterReview.capture, sessionDir: changedAfterReview.sessionDir,
|
|
935
|
+
workspaceRoot: changedAfterReview.root, captures: changedAfterReview.reviewCaptures,
|
|
936
|
+
fixtureExpectedPin: changedAfterReview.expectedPin,
|
|
937
|
+
}).state, 'unresolved', 'stash/restore or any draft bytes before dispatch make the worker root dirty');
|
|
938
|
+
} finally { changedAfterReview.cleanup(); }
|
|
939
|
+
|
|
940
|
+
const shellWriter = createOutlineFixture();
|
|
941
|
+
try {
|
|
942
|
+
shellWriter.reviewCaptures.unshift(provider.buildCaptureRecord({
|
|
943
|
+
session_id: shellWriter.sessionId, cwd: shellWriter.root, hook_event_name: 'PostToolUse',
|
|
944
|
+
tool_name: 'Bash', tool_use_id: 'shell-writer-1', agent_id: shellWriter.sessionId,
|
|
945
|
+
tool_input: { command: 'node ./mutate.js' }, tool_response: { interrupted: false },
|
|
946
|
+
}, 'PostToolUse', shellWriter.root));
|
|
947
|
+
assert.notStrictEqual(shellWriter.finishDraft().result.status, 'accepted',
|
|
948
|
+
'an unclassified script-capable shell from a non-worker remains unresolved');
|
|
949
|
+
} finally { shellWriter.cleanup(); }
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test('live qualification requires same-session dispatch, worker-start root, and result correlation', () => {
|
|
953
|
+
assert.strictEqual(typeof internals.outlineCaptureMappingValid, 'function', 'missing qualification correlation validator');
|
|
954
|
+
const mapping = { promptField: 'tool_input.prompt', coordinatorPrincipalField: 'session_id', synchronousPreToolUse: true,
|
|
955
|
+
workerStartDispatchField: 'parent_tool_use_id', workerStartRootField: 'cwd', taskResultDispatchField: 'tool_use_id' };
|
|
956
|
+
const records = [
|
|
957
|
+
{ event: 'PreToolUse', tool_name: 'Task', tool_use_id: 'dispatch-1', session_id: 'coordinator',
|
|
958
|
+
tool_input: { prompt: 'HARNESS_OUTLINE_DISPATCH_V1 taskId=task-prb attemptId=attempt-1 token=token-1 workerRoot=/tmp/worker' } },
|
|
959
|
+
{ event: 'SubagentStart', parent_tool_use_id: 'dispatch-1', agent_id: 'worker', session_id: 'coordinator', cwd: '/tmp/worker' },
|
|
960
|
+
{ event: 'PostToolUse', tool_name: 'Task', tool_use_id: 'dispatch-1', agent_id: 'worker', session_id: 'coordinator', interrupted: false },
|
|
961
|
+
];
|
|
962
|
+
assert.strictEqual(internals.outlineCaptureMappingValid(mapping, records), true);
|
|
963
|
+
const crossSession = records.map(item => ({ ...item }));
|
|
964
|
+
crossSession[2].session_id = 'different-session';
|
|
965
|
+
assert.strictEqual(internals.outlineCaptureMappingValid(mapping, crossSession), false,
|
|
966
|
+
'matching tool IDs across different coordinator sessions do not qualify the live mapping');
|
|
967
|
+
const wrongRoot = records.map(item => ({ ...item }));
|
|
968
|
+
wrongRoot[1].cwd = '/tmp/other-worker';
|
|
969
|
+
assert.strictEqual(internals.outlineCaptureMappingValid(mapping, wrongRoot), false);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test('original B and immutable attempt chain survive reconfirmation; stale attempts/tokens never cover new scope', () => {
|
|
973
|
+
assert.strictEqual(typeof internals.resolveOutlineAttempt, 'function', 'missing immutable attempt-chain resolver');
|
|
974
|
+
const fixture = createOutlineFixture();
|
|
975
|
+
try {
|
|
976
|
+
const resolved = internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: fixture.attemptId });
|
|
977
|
+
assert.strictEqual(resolved.state, 'accepted');
|
|
978
|
+
const originalBaseline = fixture.baselineCommit;
|
|
979
|
+
const secondId = 'attempt-2';
|
|
980
|
+
const secondDir = path.join(fixture.sessionDir, 'doc-reconciliation/outline-confirmation', fixture.taskId, 'attempts', secondId);
|
|
981
|
+
fs.mkdirSync(secondDir, { recursive: true });
|
|
982
|
+
const lease2 = { schema: 'harness.outline-writer-lease.v1', id: 'lease-2', taskId: fixture.taskId,
|
|
983
|
+
attemptId: secondId, principalId: fixture.sessionId, root: fixture.root, exclusive: true, active: true };
|
|
984
|
+
writeJson(path.join(secondDir, 'writer-lease.json'), lease2);
|
|
985
|
+
const second = { ...fixture.handoff, attemptId: secondId, handoffId: 'handoff-2', supersedesAttemptId: fixture.attemptId,
|
|
986
|
+
draftDispatchToken: 'token-2', writerLeaseId: lease2.id,
|
|
987
|
+
writerLeaseDigest: sha(fs.readFileSync(path.join(secondDir, 'writer-lease.json'))) };
|
|
988
|
+
writeJson(path.join(secondDir, 'handoff.json'), second);
|
|
989
|
+
const secondResolved = internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: secondId });
|
|
990
|
+
assert.strictEqual(secondResolved.state, 'accepted', 'current attempt links to one retained predecessor');
|
|
991
|
+
assert.strictEqual(secondResolved.handoff.baseline.commit, originalBaseline, 'B never moves to O');
|
|
992
|
+
assert.notStrictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: fixture.attemptId }).state,
|
|
993
|
+
'accepted', 'superseded dispatch cannot cover the new scope');
|
|
994
|
+
const reusedToken = { ...second, draftDispatchToken: fixture.handoff.draftDispatchToken };
|
|
995
|
+
writeJson(path.join(secondDir, 'handoff.json'), reusedToken);
|
|
996
|
+
assert.strictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: secondId }).state,
|
|
997
|
+
'unresolved', 'a token consumed by a predecessor cannot be reused by a retry');
|
|
998
|
+
const movedB = { ...second, baseline: { ...second.baseline, commit: fixture.outlineCommit } };
|
|
999
|
+
writeJson(path.join(secondDir, 'handoff.json'), movedB);
|
|
1000
|
+
assert.strictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: secondId }).state,
|
|
1001
|
+
'unresolved', 'the task baseline cannot move from B to O');
|
|
1002
|
+
const changedScope = { ...second, targetPaths: ['docs/adr/DOCS-002.md'],
|
|
1003
|
+
inventory: { ...second.inventory, targetPaths: ['docs/adr/DOCS-002.md'] } };
|
|
1004
|
+
writeJson(path.join(secondDir, 'handoff.json'), changedScope);
|
|
1005
|
+
const changedPrompt = `HARNESS_OUTLINE_DISPATCH_V1 taskId=${fixture.taskId} attemptId=${secondId} token=token-2 workerRoot=${fixture.root}`;
|
|
1006
|
+
const changedPayload = { ...fixture.payload, tool_use_id: 'dispatch-scope-change', tool_input: { prompt: changedPrompt } };
|
|
1007
|
+
const changedCapture = provider.buildCaptureRecord(changedPayload, 'PreToolUse', fixture.root);
|
|
1008
|
+
changedCapture.observedIdentity = fixture.capture.observedIdentity;
|
|
1009
|
+
assert.strictEqual(internals.captureOutlineDispatch({ payload: changedPayload, capture: changedCapture,
|
|
1010
|
+
sessionDir: fixture.sessionDir, workspaceRoot: fixture.root, captures: fixture.reviewCaptures,
|
|
1011
|
+
fixtureExpectedPin: fixture.expectedPin }).state, 'unresolved',
|
|
1012
|
+
'changed attempt scope cannot reuse an old O review/handoff');
|
|
1013
|
+
second.supersedesAttemptId = 'missing-predecessor';
|
|
1014
|
+
writeJson(path.join(secondDir, 'handoff.json'), second);
|
|
1015
|
+
assert.strictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir, taskId: fixture.taskId, attemptId: secondId }).state,
|
|
1016
|
+
'unresolved', 'broken attempt chains stay unresolved');
|
|
1017
|
+
} finally { fixture.cleanup(); }
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test('squash correspondence retains only identical O/D target blobs and requires available Git objects', () => {
|
|
1021
|
+
assert.strictEqual(typeof internals.verifyOutlineMergeCorrespondence, 'function', 'missing squash correspondence validator');
|
|
1022
|
+
const fixture = createOutlineFixture();
|
|
1023
|
+
try {
|
|
1024
|
+
const completed = fixture.finishDraft();
|
|
1025
|
+
const draftBlob = fixtureGit(fixture.root, ['rev-parse', `${completed.draftCommit}:${fixture.target}`]);
|
|
1026
|
+
fs.writeFileSync(path.join(fixture.root, 'merge-marker.txt'), 'squash metadata');
|
|
1027
|
+
fixtureGit(fixture.root, ['add', '-A']);
|
|
1028
|
+
fixtureGit(fixture.root, ['commit', '-q', '-m', 'identical squash target']);
|
|
1029
|
+
const mergedCommit = fixtureGit(fixture.root, ['rev-parse', 'HEAD']);
|
|
1030
|
+
const identical = internals.verifyOutlineMergeCorrespondence({
|
|
1031
|
+
root: fixture.root, outlineCommit: fixture.outlineCommit, draftCommit: completed.draftCommit,
|
|
1032
|
+
mergedCommit, targetPaths: [fixture.target], record: { outlineCommit: fixture.outlineCommit,
|
|
1033
|
+
draftCommit: completed.draftCommit, mergedCommit, targetBlobs: { [fixture.target]: { draft: draftBlob, merged: draftBlob } } },
|
|
1034
|
+
});
|
|
1035
|
+
assert.strictEqual(identical.state, 'accepted');
|
|
1036
|
+
fs.writeFileSync(path.join(fixture.root, fixture.target), 'changed during merge');
|
|
1037
|
+
fixtureGit(fixture.root, ['add', '-A']);
|
|
1038
|
+
fixtureGit(fixture.root, ['commit', '-q', '-m', 'changed squash target']);
|
|
1039
|
+
const changedMerge = fixtureGit(fixture.root, ['rev-parse', 'HEAD']);
|
|
1040
|
+
const changedBlob = fixtureGit(fixture.root, ['rev-parse', `${changedMerge}:${fixture.target}`]);
|
|
1041
|
+
const changed = internals.verifyOutlineMergeCorrespondence({
|
|
1042
|
+
root: fixture.root, outlineCommit: fixture.outlineCommit, draftCommit: completed.draftCommit,
|
|
1043
|
+
mergedCommit: changedMerge, targetPaths: [fixture.target], record: { outlineCommit: fixture.outlineCommit,
|
|
1044
|
+
draftCommit: completed.draftCommit, mergedCommit: changedMerge,
|
|
1045
|
+
targetBlobs: { [fixture.target]: { draft: draftBlob, merged: changedBlob } } },
|
|
1046
|
+
});
|
|
1047
|
+
assert.strictEqual(changed.state, 'changed', 'changed final bytes do not inherit the old final review');
|
|
1048
|
+
const missing = internals.verifyOutlineMergeCorrespondence({ root: fixture.root,
|
|
1049
|
+
outlineCommit: fixture.outlineCommit, draftCommit: completed.draftCommit, mergedCommit,
|
|
1050
|
+
targetPaths: [fixture.target], record: null });
|
|
1051
|
+
assert.strictEqual(missing.state, 'unresolved');
|
|
1052
|
+
} finally { fixture.cleanup(); }
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test('overwriting a dispatched predecessor handoff invalidates the retained attempt chain', () => {
|
|
1056
|
+
assert.strictEqual(typeof internals.resolveOutlineAttempt, 'function');
|
|
1057
|
+
const fixture = createOutlineFixture();
|
|
1058
|
+
try {
|
|
1059
|
+
const secondId = 'attempt-2';
|
|
1060
|
+
const secondDir = path.join(fixture.sessionDir, 'doc-reconciliation/outline-confirmation', fixture.taskId, 'attempts', secondId);
|
|
1061
|
+
fs.mkdirSync(secondDir, { recursive: true });
|
|
1062
|
+
const lease = { schema: 'harness.outline-writer-lease.v1', id: 'lease-2', taskId: fixture.taskId,
|
|
1063
|
+
attemptId: secondId, principalId: fixture.sessionId, root: fixture.root, exclusive: true, active: true };
|
|
1064
|
+
writeJson(path.join(secondDir, 'writer-lease.json'), lease);
|
|
1065
|
+
const second = { ...fixture.handoff, attemptId: secondId, handoffId: 'handoff-2',
|
|
1066
|
+
supersedesAttemptId: fixture.attemptId, draftDispatchToken: 'token-2', writerLeaseId: lease.id,
|
|
1067
|
+
writerLeaseDigest: sha(fs.readFileSync(path.join(secondDir, 'writer-lease.json'))) };
|
|
1068
|
+
writeJson(path.join(secondDir, 'handoff.json'), second);
|
|
1069
|
+
assert.strictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir,
|
|
1070
|
+
taskId: fixture.taskId, attemptId: secondId }).state, 'accepted');
|
|
1071
|
+
const predecessorFile = path.join(fixture.attemptDir, 'handoff.json');
|
|
1072
|
+
fs.rmSync(path.join(fixture.attemptDir, 'tokens'), { recursive: true, force: true });
|
|
1073
|
+
fs.rmSync(path.join(fixture.attemptDir, 'seams'), { recursive: true, force: true });
|
|
1074
|
+
const overwritten = JSON.parse(fs.readFileSync(predecessorFile, 'utf8'));
|
|
1075
|
+
overwritten.handoffId = 'hand-off-overwritten-after-dispatch';
|
|
1076
|
+
writeJson(predecessorFile, overwritten);
|
|
1077
|
+
assert.strictEqual(internals.resolveOutlineAttempt({ sessionDir: fixture.sessionDir,
|
|
1078
|
+
taskId: fixture.taskId, attemptId: secondId }).state, 'unresolved',
|
|
1079
|
+
'the retained handoff seal still detects overwrites after token/seam files are removed');
|
|
1080
|
+
} finally { fixture.cleanup(); }
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test('real capture→provider→adapter→checker→pilot discovers one outline event without injected refs', () => {
|
|
1084
|
+
const fixture = createOutlineFixture({ realHook: true });
|
|
1085
|
+
try {
|
|
1086
|
+
const completed = fixture.finishDraft({ finalReview: true, runPilot: true });
|
|
1087
|
+
const report = completed.pilot?.report;
|
|
1088
|
+
assert.ok(report, 'pilot produced its runtime report');
|
|
1089
|
+
assert.strictEqual(report.outlineEvidence?.outcome, 'pass', JSON.stringify(report));
|
|
1090
|
+
assert.ok(report.acceptedRefs.some(ref => ref.endsWith('#outline-confirmation')),
|
|
1091
|
+
'pilot discovered the pre-tool capture reference itself');
|
|
1092
|
+
assert.ok(report.acceptedRefs.some(ref => ref.endsWith('#quality-review')),
|
|
1093
|
+
'the existing final exact-D quality-review lane remains independently discoverable');
|
|
1094
|
+
assert.strictEqual(report.pilotCredit, false, 'other B→D MUSTs remain independently unsatisfied');
|
|
1095
|
+
} finally { fixture.cleanup(); }
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test('pilot compares the current Git root with an aliased pinned baseline root', () => {
|
|
1099
|
+
const fixture = createOutlineFixture({ realHook: true });
|
|
1100
|
+
try {
|
|
1101
|
+
const baselineRoot = `${fixture.root}${path.sep}..${path.sep}${path.basename(fixture.root)}`;
|
|
1102
|
+
const completed = fixture.finishDraft({ finalReview: true, runPilot: true, baselineRoot });
|
|
1103
|
+
assert.strictEqual(completed.pilot.report.outlineEvidence?.outcome, 'pass', JSON.stringify(completed.pilot.report));
|
|
1104
|
+
assert.ok(!completed.pilot.report.diagnostics.some(item => item.code === 'baseline-unavailable'),
|
|
1105
|
+
'equivalent root spellings do not make the baseline unavailable');
|
|
1106
|
+
} finally { fixture.cleanup(); }
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
test('scope-preserving D edits keep outline approval but stale final review cannot transfer to new bytes', () => {
|
|
1110
|
+
const fixture = createOutlineFixture({ realHook: true });
|
|
1111
|
+
try {
|
|
1112
|
+
const completed = fixture.finishDraft({ finalReview: true, mutateAfterFinalReview: true, runPilot: true });
|
|
1113
|
+
const report = completed.pilot.report;
|
|
1114
|
+
assert.strictEqual(report.outlineEvidence?.outcome, 'pass');
|
|
1115
|
+
assert.ok(report.acceptedRefs.some(ref => ref.endsWith('#outline-confirmation')));
|
|
1116
|
+
assert.ok(!report.acceptedRefs.some(ref => ref.endsWith('#quality-review')),
|
|
1117
|
+
'the prior review digest does not bind changed final D bytes');
|
|
1118
|
+
assert.strictEqual(report.pilotCredit, false);
|
|
1119
|
+
} finally { fixture.cleanup(); }
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
test('legacy session-dir keying stays aligned through an aliased workspace root', () => {
|
|
1123
|
+
function driveAliasRootScenario() {
|
|
1124
|
+
const realRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-session-root-'));
|
|
1125
|
+
const aliasRoot = `${realRoot}-alias`;
|
|
1126
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-session-state-'));
|
|
1127
|
+
const sessionId = 'prb-alias-session';
|
|
1128
|
+
const preload = path.join(stateHome, 'realpath-alias.js');
|
|
1129
|
+
fs.symlinkSync(realRoot, aliasRoot, 'dir');
|
|
1130
|
+
writeJson(path.join(stateHome, 'session-workspaces', `${sha(sessionId)}.json`), {
|
|
1131
|
+
version: 1, sessionId, workspaceRoot: aliasRoot,
|
|
1132
|
+
});
|
|
1133
|
+
fs.writeFileSync(preload, [
|
|
1134
|
+
"const fs=require('fs');",
|
|
1135
|
+
"const path=require('path');",
|
|
1136
|
+
'const original=fs.realpathSync;',
|
|
1137
|
+
'const alias=path.resolve(process.env.PRB_TEST_ALIAS_ROOT);',
|
|
1138
|
+
'function realpathSync(target,...args){',
|
|
1139
|
+
' if(typeof target==="string"&&path.resolve(target)===alias)return alias;',
|
|
1140
|
+
' return original.call(fs,target,...args);',
|
|
1141
|
+
'}',
|
|
1142
|
+
'realpathSync.native=original.native;',
|
|
1143
|
+
'fs.realpathSync=realpathSync;',
|
|
1144
|
+
].join('\n'));
|
|
1145
|
+
|
|
1146
|
+
const previousStateHome = process.env.HARNESS_STATE_HOME;
|
|
1147
|
+
const originalRealpathSync = fs.realpathSync;
|
|
1148
|
+
let aliasSessionDir;
|
|
1149
|
+
let canonicalSessionDir;
|
|
1150
|
+
try {
|
|
1151
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
1152
|
+
fs.realpathSync = function realpathSync(target, ...args) {
|
|
1153
|
+
if (typeof target === 'string' && path.resolve(target) === path.resolve(aliasRoot)) return path.resolve(aliasRoot);
|
|
1154
|
+
return originalRealpathSync.call(fs, target, ...args);
|
|
1155
|
+
};
|
|
1156
|
+
fs.realpathSync.native = originalRealpathSync.native;
|
|
1157
|
+
const { getSessionDir } = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js'));
|
|
1158
|
+
aliasSessionDir = getSessionDir(aliasRoot, sessionId);
|
|
1159
|
+
canonicalSessionDir = getSessionDir(realRoot, sessionId);
|
|
1160
|
+
} finally {
|
|
1161
|
+
fs.realpathSync = originalRealpathSync;
|
|
1162
|
+
if (previousStateHome === undefined) delete process.env.HARNESS_STATE_HOME;
|
|
1163
|
+
else process.env.HARNESS_STATE_HOME = previousStateHome;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const env = {
|
|
1167
|
+
...process.env,
|
|
1168
|
+
HARNESS_STATE_HOME: stateHome,
|
|
1169
|
+
HARNESS_WORKSPACE_ROOT: aliasRoot,
|
|
1170
|
+
HARNESS_DOC_RECONCILIATION_PILOT: '0',
|
|
1171
|
+
PRB_TEST_ALIAS_ROOT: aliasRoot,
|
|
1172
|
+
};
|
|
1173
|
+
const runHook = (relative, payload, pilotEnabled = false) => spawnSync(process.execPath,
|
|
1174
|
+
['--require', preload, path.join(ROOT, relative)], {
|
|
1175
|
+
cwd: aliasRoot,
|
|
1176
|
+
input: JSON.stringify(payload),
|
|
1177
|
+
encoding: 'utf8',
|
|
1178
|
+
env: { ...env, HARNESS_DOC_RECONCILIATION_PILOT: pilotEnabled ? '1' : '0' },
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
const legacy = runHook('hooks/scripts/state-persist.js', {
|
|
1183
|
+
hook_event_name: 'PostToolUse', session_id: sessionId, cwd: aliasRoot,
|
|
1184
|
+
tool_name: 'Bash', tool_input: { command: 'echo legacy-state-probe' },
|
|
1185
|
+
tool_response: { exitCode: 0, stdout: '', stderr: '' },
|
|
1186
|
+
});
|
|
1187
|
+
const pilotOffCaptureCount = provider.readCaptureView(aliasSessionDir).records.length
|
|
1188
|
+
+ provider.readCaptureView(canonicalSessionDir).records.length;
|
|
1189
|
+
const pilot = runHook('hooks/scripts/state-persist.js', {
|
|
1190
|
+
hook_event_name: 'PreToolUse', session_id: sessionId, cwd: aliasRoot,
|
|
1191
|
+
tool_name: 'Agent', tool_use_id: 'pilot-alias-capture', agent_type: 'worker',
|
|
1192
|
+
tool_input: { prompt: 'alias capture probe' },
|
|
1193
|
+
}, true);
|
|
1194
|
+
writeJson(path.join(aliasSessionDir, 'rule-of-3-state.json'), {
|
|
1195
|
+
count: 3, threshold: 3, lastHash: 'alias-keying-control', lastFailureAt: 0,
|
|
1196
|
+
});
|
|
1197
|
+
const breaker = runHook('hooks/scripts/rule-of-3.js', {
|
|
1198
|
+
hook_event_name: 'PreToolUse', session_id: sessionId, cwd: aliasRoot,
|
|
1199
|
+
tool_name: 'apply_patch', tool_input: { patch: '*** Update File: unrelated.txt\n@@\n-old\n+new\n' },
|
|
1200
|
+
});
|
|
1201
|
+
assert.strictEqual(legacy.status, 0, `pilot-OFF state-persist must remain fail-open: ${legacy.stderr}`);
|
|
1202
|
+
assert.strictEqual(pilot.status, 0, `pilot capture must remain fail-open: ${pilot.stderr}`);
|
|
1203
|
+
assert.strictEqual(breaker.status, 2, `rule-of-3 must find the tripped alias session: ${breaker.stderr}`);
|
|
1204
|
+
assert.match(breaker.stderr, /RULE OF 3 CIRCUIT BREAKER TRIGGERED/);
|
|
1205
|
+
console.log('PASS rule-of-3 alias-root positive control blocks at exit 2');
|
|
1206
|
+
return {
|
|
1207
|
+
legacyExit: legacy.status,
|
|
1208
|
+
pilotOffCaptureCount,
|
|
1209
|
+
handoffInAliasSession: fs.existsSync(path.join(aliasSessionDir, 'handoff-state.json')),
|
|
1210
|
+
handoffInCanonicalSession: fs.existsSync(path.join(canonicalSessionDir, 'handoff-state.json')),
|
|
1211
|
+
pilotCaptureInAliasSession: provider.readCaptureView(aliasSessionDir).records.some(item => item.tool_use_id === 'pilot-alias-capture'),
|
|
1212
|
+
pilotCaptureInCanonicalSession: provider.readCaptureView(canonicalSessionDir).records.some(item => item.tool_use_id === 'pilot-alias-capture'),
|
|
1213
|
+
breakerExit: breaker.status,
|
|
1214
|
+
};
|
|
1215
|
+
} finally {
|
|
1216
|
+
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
1217
|
+
fs.rmSync(aliasRoot, { force: true });
|
|
1218
|
+
fs.rmSync(realRoot, { recursive: true, force: true });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const first = driveAliasRootScenario();
|
|
1223
|
+
const second = driveAliasRootScenario();
|
|
1224
|
+
assert.deepStrictEqual(first, second, 'identical alias-root inputs must produce the same normalized state and exits');
|
|
1225
|
+
assert.strictEqual(first.legacyExit, 0, 'pilot OFF remains fail-open');
|
|
1226
|
+
assert.strictEqual(first.pilotOffCaptureCount, 0, 'pilot OFF writes no capture record');
|
|
1227
|
+
assert.strictEqual(first.breakerExit, 2, 'the aliased rule-of-3 positive control blocks');
|
|
1228
|
+
assert.deepStrictEqual({
|
|
1229
|
+
handoffInAliasSession: first.handoffInAliasSession,
|
|
1230
|
+
handoffInCanonicalSession: first.handoffInCanonicalSession,
|
|
1231
|
+
pilotCaptureInAliasSession: first.pilotCaptureInAliasSession,
|
|
1232
|
+
pilotCaptureInCanonicalSession: first.pilotCaptureInCanonicalSession,
|
|
1233
|
+
}, {
|
|
1234
|
+
handoffInAliasSession: true,
|
|
1235
|
+
handoffInCanonicalSession: false,
|
|
1236
|
+
pilotCaptureInAliasSession: true,
|
|
1237
|
+
pilotCaptureInCanonicalSession: false,
|
|
1238
|
+
}, `legacy handoff and pilot capture must stay in rule-of-3's original session directory: ${JSON.stringify(first)}`);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
test('no final D stays incomplete and hand-authored JSON without a host seam earns no outline credit', () => {
|
|
1242
|
+
const noDraft = createOutlineFixture({ realHook: true });
|
|
1243
|
+
try {
|
|
1244
|
+
const result = noDraft.finishDraft({ draft: false, runPilot: true });
|
|
1245
|
+
assert.strictEqual(result.pilot.report.outlineEvidence?.observation, 'unresolved');
|
|
1246
|
+
assert.strictEqual(result.pilot.report.outlineEvidence?.outcome, 'none');
|
|
1247
|
+
} finally { noDraft.cleanup(); }
|
|
1248
|
+
|
|
1249
|
+
const forged = createOutlineFixture();
|
|
1250
|
+
try {
|
|
1251
|
+
const result = forged.finishDraft({ handAuthoredCapture: true, removeSeam: true, runPilot: true });
|
|
1252
|
+
assert.notStrictEqual(result.pilot.report.outlineEvidence?.outcome, 'pass',
|
|
1253
|
+
'candidate-authored JSON and a missing host seam are unresolved');
|
|
1254
|
+
const opencode = provider.createOpencodeOriginProvider({ hostId: 'opencode', sessionId: forged.sessionId });
|
|
1255
|
+
assert.notStrictEqual(opencode.resolveEvent({ reference: 'fake#outline-confirmation' }).status, 'accepted',
|
|
1256
|
+
'OpenCode remains unsupported for positive outline credit');
|
|
1257
|
+
} finally { forged.cleanup(); }
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
if (failures.length) {
|
|
1261
|
+
console.error(`\n${failures.length} PR-B RED assertion(s) failed.`);
|
|
1262
|
+
process.exitCode = 1;
|
|
1263
|
+
} else {
|
|
1264
|
+
console.log('\nAll PR-B outline mechanism checks passed.');
|
|
1265
|
+
}
|