harness-everything 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +0 -82
- package/.github/workflows/doc-governance.yml +91 -0
- package/.github/workflows/release.yml +5 -2
- package/CHANGELOG.md +30 -0
- package/ci/mechanism-2ax-doc-lifecycle.test.js +142 -12
- package/ci/mechanism-prb-outline-review.test.js +965 -47
- package/docs/doc-workflow-reconciliation.md +47 -1
- package/docs/document-lifecycle.md +19 -0
- package/docs/repository-contract.md +1 -0
- package/hooks/scripts/lib/doc-reconciliation-provider.js +333 -9
- package/hooks/scripts/state-persist.js +5 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +333 -9
- package/plugins/harness-everything/hooks/scripts/state-persist.js +5 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/doc-governance-guard.js +31 -8
|
@@ -19,7 +19,11 @@ const failures = [];
|
|
|
19
19
|
|
|
20
20
|
function test(name, run) {
|
|
21
21
|
try {
|
|
22
|
-
run();
|
|
22
|
+
const result = run();
|
|
23
|
+
if (result && typeof result.unchecked === 'string') {
|
|
24
|
+
console.log(`UNCHECKED ${name}: ${result.unchecked}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
23
27
|
console.log(`PASS ${name}`);
|
|
24
28
|
} catch (error) {
|
|
25
29
|
failures.push(name);
|
|
@@ -27,6 +31,47 @@ function test(name, run) {
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
function symlinkUnavailable(type) {
|
|
35
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-symlink-probe-'));
|
|
36
|
+
try {
|
|
37
|
+
const target = path.join(directory, 'target');
|
|
38
|
+
if (type === 'dir') fs.mkdirSync(target);
|
|
39
|
+
else fs.writeFileSync(target, '');
|
|
40
|
+
fs.symlinkSync(target, path.join(directory, 'link'), type);
|
|
41
|
+
return null;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (['EPERM', 'EACCES', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP'].includes(error.code)) return error.code;
|
|
44
|
+
throw error;
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function symlinkCitationPrecondition(linkText, expectedText, target, parse = internals.parseOutlineCitation) {
|
|
51
|
+
if (linkText !== expectedText) {
|
|
52
|
+
return { unchecked: `symlink text differs from fixture citation bytes: expected ${JSON.stringify(expectedText)}, actual ${JSON.stringify(linkText)}` };
|
|
53
|
+
}
|
|
54
|
+
assert.strictEqual(parse(Buffer.from(linkText), target).state, 'accepted',
|
|
55
|
+
'citation-looking symlink link text must remain a positive control');
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('explicitly unchecked controls do not also report PASS', () => {
|
|
60
|
+
const output = [];
|
|
61
|
+
const log = console.log;
|
|
62
|
+
try {
|
|
63
|
+
console.log = message => output.push(message);
|
|
64
|
+
test('fixture precondition', () => ({ unchecked: 'EPERM' }));
|
|
65
|
+
} finally { console.log = log; }
|
|
66
|
+
assert.deepStrictEqual(output, ['UNCHECKED fixture precondition: EPERM']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('matching symlink citation bytes fail closed when the parser rejects them', () => {
|
|
70
|
+
const expectedText = outlineText();
|
|
71
|
+
assert.throws(() => symlinkCitationPrecondition(expectedText, expectedText, 'docs/adr/DOCS-001.md', () => ({ state: 'unresolved' })),
|
|
72
|
+
/citation-looking symlink link text must remain a positive control/);
|
|
73
|
+
});
|
|
74
|
+
|
|
30
75
|
function sha(value) {
|
|
31
76
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
32
77
|
}
|
|
@@ -64,7 +109,7 @@ function fixtureGit(root, args) {
|
|
|
64
109
|
}
|
|
65
110
|
}
|
|
66
111
|
|
|
67
|
-
function outlineText({ complete = false, decisionPlaceholder = false, duplicateDecision = false, titledDecision = false,
|
|
112
|
+
function outlineText({ id = null, complete = false, decisionPlaceholder = false, duplicateDecision = false, titledDecision = false,
|
|
68
113
|
indentedDecision = false,
|
|
69
114
|
rowId = '20', target = 'docs/adr/DOCS-001.md', domain = 'DOCS' } = {}) {
|
|
70
115
|
const decision = complete ? '\n## Decision\nThis is already a complete drafted decision.\n'
|
|
@@ -72,11 +117,13 @@ function outlineText({ complete = false, decisionPlaceholder = false, duplicateD
|
|
|
72
117
|
: titledDecision ? '\n## Decision — Final choice\nThis titled section contains a complete decision.\n'
|
|
73
118
|
: indentedDecision ? '\n ## Decision — Indented final choice\nThis indented section contains substantive text.\n'
|
|
74
119
|
: decisionPlaceholder ? '\n## Decision\n- TBD\n' : '';
|
|
75
|
-
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}`;
|
|
120
|
+
return `---\nid: ${id || `${domain}-001`}\nstatus: proposed\n---\n\nInventory row: ${rowId}\nTarget: ${target}\nDomain: ${domain}\n\n## Outline\n- Context: TBD\n- Decision: TBD\n${decision}`;
|
|
76
121
|
}
|
|
77
122
|
|
|
78
123
|
function createOutlineFixture(options = {}) {
|
|
79
|
-
const
|
|
124
|
+
const generatedRoot = fs.mkdtempSync(path.join(options.rootDirectory || os.tmpdir(), 'prb-outline-fixture-'));
|
|
125
|
+
const root = options.preserveRootSpelling ? generatedRoot : canonical(generatedRoot);
|
|
126
|
+
const workspaceRoot = canonical(root);
|
|
80
127
|
const retainedInventory = options.retainedInventory === true;
|
|
81
128
|
const target = options.target || (retainedInventory ? 'docs/adr/EVID-001.md' : 'docs/adr/DOCS-001.md');
|
|
82
129
|
const rowId = options.rowId || (retainedInventory ? '17' : '20');
|
|
@@ -89,57 +136,37 @@ function createOutlineFixture(options = {}) {
|
|
|
89
136
|
? `${root}${path.sep}..${path.sep}${path.basename(root)}` : root;
|
|
90
137
|
const workflowId = 'workflow-prb';
|
|
91
138
|
const workflowRevision = 2;
|
|
92
|
-
const config = JSON.stringify({ governed: [{ path: 'docs/adr/*.md', role: 'adr' }], domains: [domain], derived: [] }, null, 2) + '\n';
|
|
139
|
+
const config = JSON.stringify({ governed: [{ path: options.governedPattern || 'docs/adr/*.md', role: 'adr' }], domains: [domain], derived: [] }, null, 2) + '\n';
|
|
93
140
|
const configDigest = sha(config);
|
|
94
141
|
fs.mkdirSync(path.join(root, '.agents/harness-everything'), { recursive: true });
|
|
95
142
|
fs.mkdirSync(path.dirname(path.join(root, target)), { recursive: true });
|
|
96
143
|
fs.writeFileSync(path.join(root, '.agents/harness-everything/project-docs.json'), config);
|
|
97
144
|
if (options.targetExistsAtB) fs.writeFileSync(path.join(root, target),
|
|
98
145
|
`---\nid: DOCS-001\nstatus: proposed\n---\n\n## Decision\nAn existing B document.\n`);
|
|
146
|
+
if (options.unrelatedFileAtB) fs.writeFileSync(path.join(root, options.unrelatedFileAtB), 'unmodified at B\n');
|
|
99
147
|
fixtureGit(root, ['init', '-q']);
|
|
100
148
|
fixtureGit(root, ['add', '-A']);
|
|
101
149
|
fixtureGit(root, ['commit', '-q', '-m', 'B baseline']);
|
|
102
150
|
const baselineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
103
151
|
const baselineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
152
|
+
if (options.unrelatedFileAtB) fs.writeFileSync(path.join(root, options.unrelatedFileAtB), 'modified after B\n');
|
|
104
153
|
let sessionDir = path.join(stateHome, 'session');
|
|
105
154
|
if (options.realHook) {
|
|
106
155
|
const priorStateHome = process.env.HARNESS_STATE_HOME;
|
|
107
156
|
process.env.HARNESS_STATE_HOME = stateHome;
|
|
108
|
-
try {
|
|
157
|
+
try {
|
|
158
|
+
// Model SessionStart: hook state follows the established root while later payload cwd may keep its alias spelling.
|
|
159
|
+
const workspace = require(path.join(ROOT, 'scripts/lib/workspace.js'));
|
|
160
|
+
writeJson(path.join(stateHome, 'session-workspaces', `${sha(sessionId)}.json`), {
|
|
161
|
+
version: 1, sessionId, workspaceRoot, workspaceKey: workspace.getWorkspaceKey(workspaceRoot),
|
|
162
|
+
});
|
|
163
|
+
sessionDir = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js')).getSessionDir(workspaceRoot, sessionId);
|
|
164
|
+
}
|
|
109
165
|
finally { if (priorStateHome === undefined) delete process.env.HARNESS_STATE_HOME; else process.env.HARNESS_STATE_HOME = priorStateHome; }
|
|
110
166
|
}
|
|
111
167
|
|
|
112
|
-
fs.writeFileSync(path.join(root, target), outlineText({
|
|
113
|
-
complete: options.completeOutline === true,
|
|
114
|
-
decisionPlaceholder: options.decisionPlaceholder === true,
|
|
115
|
-
duplicateDecision: options.duplicateDecision === true,
|
|
116
|
-
indentedDecision: options.indentedDecision === true,
|
|
117
|
-
rowId, target, domain,
|
|
118
|
-
}));
|
|
119
|
-
fixtureGit(root, ['add', '-A']);
|
|
120
|
-
fixtureGit(root, ['commit', '-q', '-m', 'O outline']);
|
|
121
|
-
const outlineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
122
|
-
const outlineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
123
|
-
const outlineBytes = fs.readFileSync(path.join(root, target));
|
|
124
|
-
const outlineBlob = fixtureGit(root, ['rev-parse', `HEAD:${target}`]);
|
|
125
|
-
|
|
126
|
-
const inventoryBytes = retainedInventory
|
|
127
|
-
? fs.readFileSync(path.join(ROOT, 'benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/inventory/2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6.md'))
|
|
128
|
-
: Buffer.from(syntheticInventory);
|
|
129
|
-
const pinDigest = sha(inventoryBytes);
|
|
130
|
-
const expectedPin = retainedInventory ? internals.OUTLINE_INVENTORY_PIN : {
|
|
131
|
-
source: 'synthetic://approved-inventory', sha256: pinDigest, runId: 'synthetic-run', inputSha: 'abcdef012345',
|
|
132
|
-
approvalRef: { issue: 325, commentId: 'synthetic-comment', decision: 1 },
|
|
133
|
-
retainedArtifactPath: `inventory/${pinDigest}.md`,
|
|
134
|
-
};
|
|
135
|
-
if (retainedInventory) assert.strictEqual(pinDigest, expectedPin.sha256, 'retained inventory bytes must match the pinned digest');
|
|
136
|
-
const outlineRoot = path.join(sessionDir, 'doc-reconciliation/outline-confirmation');
|
|
137
|
-
const inventoryDir = path.join(outlineRoot, 'inventory');
|
|
138
|
-
fs.mkdirSync(inventoryDir, { recursive: true });
|
|
139
|
-
fs.writeFileSync(path.join(inventoryDir, `${pinDigest}.md`), inventoryBytes);
|
|
140
|
-
writeJson(path.join(inventoryDir, 'inventory-pin.json'), expectedPin);
|
|
141
168
|
const requirementIds = ['prb'];
|
|
142
|
-
const workspaceId = 'ws-' + internals.digestText(internals.captureJson({ workspaceRoot: root }));
|
|
169
|
+
const workspaceId = 'ws-' + internals.digestText(internals.captureJson({ workspaceRoot: canonical(root) }));
|
|
143
170
|
const taskBinding = { schema: 'harness.doc-reconciliation-task-pin.v1', taskId, requirementIds,
|
|
144
171
|
requirementDigest: sha(JSON.stringify(requirementIds)), baselineCommit, baselineRoot: root,
|
|
145
172
|
configDigest, sessionId, workflowId, workspaceId };
|
|
@@ -149,9 +176,30 @@ function createOutlineFixture(options = {}) {
|
|
|
149
176
|
});
|
|
150
177
|
|
|
151
178
|
let hookPreloader = null;
|
|
152
|
-
|
|
179
|
+
const hookDiagnostics = [];
|
|
180
|
+
function actualCaptureSessionDirs() {
|
|
181
|
+
const workspacesDir = path.join(stateHome, 'workspaces');
|
|
182
|
+
try {
|
|
183
|
+
return fs.readdirSync(workspacesDir).flatMap(workspaceKey => {
|
|
184
|
+
const sessionsDir = path.join(workspacesDir, workspaceKey, 'state', 'sessions');
|
|
185
|
+
try {
|
|
186
|
+
return fs.readdirSync(sessionsDir).map(session => path.join(sessionsDir, session))
|
|
187
|
+
.filter(dir => fs.readdirSync(path.join(dir, 'doc-reconciliation-capture'))
|
|
188
|
+
.some(name => /^\d{8}\.json$/.test(name)));
|
|
189
|
+
} catch { return []; }
|
|
190
|
+
});
|
|
191
|
+
} catch { return []; }
|
|
192
|
+
}
|
|
193
|
+
function logReviewHookDiagnostics() {
|
|
194
|
+
console.error('REVIEW-HOOK-CAPTURE-DIAGNOSTICS ' + JSON.stringify({
|
|
195
|
+
expectedSessionDir: sessionDir,
|
|
196
|
+
writtenSessionDirs: actualCaptureSessionDirs(),
|
|
197
|
+
hooks: hookDiagnostics.filter(item => item.agentType === 'reviewer' || item.toolUseId === 'review-dispatch'),
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
function runHook(payload, pilotEnabled = true, usePreload = true) {
|
|
153
201
|
if (!options.realHook) throw new Error('real state-persist capture was not enabled for this fixture');
|
|
154
|
-
if (!hookPreloader) {
|
|
202
|
+
if (usePreload && !hookPreloader) {
|
|
155
203
|
hookPreloader = path.join(stateHome, 'outline-test-preload.js');
|
|
156
204
|
const providerPath = path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-provider.js');
|
|
157
205
|
fs.writeFileSync(hookPreloader, [
|
|
@@ -176,15 +224,134 @@ function createOutlineFixture(options = {}) {
|
|
|
176
224
|
'}',
|
|
177
225
|
].join('\n'));
|
|
178
226
|
}
|
|
179
|
-
const child = spawnSync(process.execPath, ['--require', hookPreloader, path.join(ROOT, 'hooks/scripts/state-persist.js')], {
|
|
227
|
+
const child = spawnSync(process.execPath, [...(usePreload ? ['--require', hookPreloader] : []), path.join(ROOT, 'hooks/scripts/state-persist.js')], {
|
|
180
228
|
cwd: root, encoding: 'utf8', input: JSON.stringify(payload),
|
|
181
229
|
env: { ...process.env, HARNESS_STATE_HOME: stateHome, HARNESS_WORKSPACE_ROOT: root,
|
|
182
230
|
HARNESS_DOC_RECONCILIATION_PILOT: pilotEnabled ? '1' : '0' },
|
|
183
231
|
});
|
|
184
|
-
|
|
232
|
+
hookDiagnostics.push({
|
|
233
|
+
event: payload.hook_event_name,
|
|
234
|
+
toolUseId: payload.tool_use_id || null,
|
|
235
|
+
agentId: payload.agent_id || null,
|
|
236
|
+
agentType: payload.agent_type || null,
|
|
237
|
+
exitCode: child.status,
|
|
238
|
+
stderr: child.stderr || '',
|
|
239
|
+
});
|
|
240
|
+
if (child.status !== 0) {
|
|
241
|
+
if (payload.agent_type === 'reviewer' || payload.tool_use_id === 'review-dispatch') logReviewHookDiagnostics();
|
|
242
|
+
throw new Error(`state-persist failed open contract: ${child.stderr || child.stdout || child.status}`);
|
|
243
|
+
}
|
|
185
244
|
return provider.readCaptureView(sessionDir).records;
|
|
186
245
|
}
|
|
187
246
|
|
|
247
|
+
const outlineBytes = Buffer.from(outlineText({
|
|
248
|
+
id: options.targetId,
|
|
249
|
+
complete: options.completeOutline === true,
|
|
250
|
+
decisionPlaceholder: options.decisionPlaceholder === true,
|
|
251
|
+
duplicateDecision: options.duplicateDecision === true,
|
|
252
|
+
indentedDecision: options.indentedDecision === true,
|
|
253
|
+
rowId, target, domain,
|
|
254
|
+
}));
|
|
255
|
+
let preOutlineOperation = null;
|
|
256
|
+
if (options.preOutlineOperation) {
|
|
257
|
+
if (!options.realHook) throw new Error('pre-outline operation requires the real state-persist hook');
|
|
258
|
+
const operation = options.preOutlineOperation;
|
|
259
|
+
const toolUseId = 'pre-outline-' + operation + '-' + sessionId;
|
|
260
|
+
const payload = operation === 'write'
|
|
261
|
+
? { session_id: sessionId, cwd: root, hook_event_name: 'PreToolUse', tool_name: 'Write', tool_use_id: toolUseId,
|
|
262
|
+
tool_input: { file_path: path.join(root, target), content: outlineBytes.toString('utf8') } }
|
|
263
|
+
: { session_id: sessionId, cwd: root, hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_use_id: toolUseId,
|
|
264
|
+
tool_input: { command: 'git status --porcelain' } };
|
|
265
|
+
const preRecords = runHook(payload, true, false);
|
|
266
|
+
let output = '';
|
|
267
|
+
if (operation === 'write') fs.writeFileSync(path.join(root, target), outlineBytes);
|
|
268
|
+
else output = execFileSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8' });
|
|
269
|
+
const postPayload = { ...payload, hook_event_name: 'PostToolUse', tool_response: { exitCode: 0, stdout: output } };
|
|
270
|
+
runHook(postPayload, true, false);
|
|
271
|
+
const records = provider.readCaptureView(sessionDir).records;
|
|
272
|
+
const pre = preRecords.find(item => item.event === 'PreToolUse' && item.tool_use_id === toolUseId);
|
|
273
|
+
const post = records.find(item => item.event === 'PostToolUse' && item.tool_use_id === toolUseId);
|
|
274
|
+
assert.ok(pre && post, operation + ' operation has real PreToolUse and successful PostToolUse captures');
|
|
275
|
+
preOutlineOperation = { operation, toolUseId, command: operation === 'status' ? 'git status --porcelain' : null,
|
|
276
|
+
output, preRef: pre.ref, postRef: post.ref };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!preOutlineOperation || preOutlineOperation.operation !== 'write') fs.writeFileSync(path.join(root, target), outlineBytes);
|
|
280
|
+
if (options.symlinkOutlineObject) {
|
|
281
|
+
fs.rmSync(path.join(root, target), { force: true });
|
|
282
|
+
fs.symlinkSync(outlineBytes.toString('utf8'), path.join(root, target));
|
|
283
|
+
}
|
|
284
|
+
let capturedCommit = false;
|
|
285
|
+
let indexAfterCapturedAdd = null;
|
|
286
|
+
let indexAfterOutlineCommit = null;
|
|
287
|
+
if (options.capturedOutlineCommit) {
|
|
288
|
+
// The coordinator's own git add/commit run through the real hook, so the B→O commit is captured Bash evidence.
|
|
289
|
+
if (!options.realHook || !preOutlineOperation || preOutlineOperation.operation !== 'write') {
|
|
290
|
+
throw new Error('captured O commit requires the real hook and a captured pre-O write');
|
|
291
|
+
}
|
|
292
|
+
// extraOutlineFile is an uncaptured write staged into O: only the O commit scope check can reject it.
|
|
293
|
+
if (options.extraOutlineFile) fs.writeFileSync(path.join(root, options.extraOutlineFile), 'uncaptured extra\n');
|
|
294
|
+
const addArgs = options.capturedOutlineAddArgs || ['add', '--', target];
|
|
295
|
+
const commitArgs = options.capturedOutlineCommitArgs || ['commit', '-m', 'O outline'];
|
|
296
|
+
const addCommand = typeof options.capturedOutlineAddCommand === 'function'
|
|
297
|
+
? options.capturedOutlineAddCommand({ root, target }) : options.capturedOutlineAddCommand;
|
|
298
|
+
const gitOperations = [
|
|
299
|
+
['add', addArgs, addCommand || `git add -- ${target}`],
|
|
300
|
+
['commit', commitArgs, options.capturedOutlineCommitCommand || 'git commit -m "O outline"'],
|
|
301
|
+
];
|
|
302
|
+
for (const [name, args, command] of gitOperations) {
|
|
303
|
+
const payload = { session_id: sessionId, cwd: root, hook_event_name: 'PreToolUse', tool_name: 'Bash',
|
|
304
|
+
tool_use_id: `pre-outline-${name}-${sessionId}`, tool_input: { command } };
|
|
305
|
+
runHook(payload, true, false);
|
|
306
|
+
if (name === 'add' && options.extraOutlineFile) fixtureGit(root, ['add', '--', options.extraOutlineFile]);
|
|
307
|
+
if (name === 'commit' && options.stagedUnrelatedWitness) fixtureGit(root, ['reset', '--', options.unrelatedFileAtB]);
|
|
308
|
+
fixtureGit(root, args);
|
|
309
|
+
if (name === 'add' && options.stagedUnrelatedWitness) {
|
|
310
|
+
indexAfterCapturedAdd = fixtureGit(root, ['diff', '--cached', '--name-only']).split('\n').filter(Boolean).sort();
|
|
311
|
+
}
|
|
312
|
+
if (name === 'commit' && options.stagedUnrelatedWitness) {
|
|
313
|
+
fixtureGit(root, ['add', '--', options.unrelatedFileAtB]);
|
|
314
|
+
indexAfterOutlineCommit = fixtureGit(root, ['diff', '--cached', '--name-only']).split('\n').filter(Boolean).sort();
|
|
315
|
+
}
|
|
316
|
+
runHook({ ...payload, hook_event_name: 'PostToolUse', tool_response: { exitCode: 0, stdout: '' } }, true, false);
|
|
317
|
+
}
|
|
318
|
+
capturedCommit = true;
|
|
319
|
+
}
|
|
320
|
+
if (options.stagedUnrelatedWitness) fixtureGit(root, ['reset', '--hard', 'HEAD']);
|
|
321
|
+
if (!capturedCommit) {
|
|
322
|
+
// revertedExtraOutlineCommit: a non-target file committed inside B..O and removed before O. Only per-commit scope sees it.
|
|
323
|
+
if (options.revertedExtraOutlineCommit) {
|
|
324
|
+
const extra = options.revertedExtraOutlineCommit;
|
|
325
|
+
fs.mkdirSync(path.dirname(path.join(root, extra)), { recursive: true });
|
|
326
|
+
fs.writeFileSync(path.join(root, extra), 'scratch content\n');
|
|
327
|
+
fixtureGit(root, ['add', '--', extra]);
|
|
328
|
+
fixtureGit(root, ['commit', '-q', '-m', 'scratch in O range']);
|
|
329
|
+
fs.rmSync(path.join(root, extra));
|
|
330
|
+
}
|
|
331
|
+
fixtureGit(root, ['add', '-A']);
|
|
332
|
+
fixtureGit(root, ['commit', '-q', '-m', 'O outline']);
|
|
333
|
+
}
|
|
334
|
+
let outlineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
335
|
+
let outlineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
336
|
+
let outlineBlob = fixtureGit(root, ['rev-parse', `HEAD:${target}`]);
|
|
337
|
+
|
|
338
|
+
const inventoryBytes = retainedInventory
|
|
339
|
+
? fs.readFileSync(path.join(ROOT, 'benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/inventory/2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6.md'))
|
|
340
|
+
: Buffer.from(syntheticInventory);
|
|
341
|
+
const pinDigest = sha(inventoryBytes);
|
|
342
|
+
const expectedPin = retainedInventory ? internals.OUTLINE_INVENTORY_PIN : {
|
|
343
|
+
source: 'synthetic://approved-inventory', sha256: pinDigest, runId: 'synthetic-run', inputSha: 'abcdef012345',
|
|
344
|
+
approvalRef: { issue: 325, commentId: 'synthetic-comment', decision: 1 },
|
|
345
|
+
retainedArtifactPath: `inventory/${pinDigest}.md`,
|
|
346
|
+
};
|
|
347
|
+
if (retainedInventory) assert.strictEqual(pinDigest, expectedPin.sha256, 'retained inventory bytes must match the pinned digest');
|
|
348
|
+
const outlineRoot = path.join(sessionDir, 'doc-reconciliation/outline-confirmation');
|
|
349
|
+
const inventoryDir = path.join(outlineRoot, 'inventory');
|
|
350
|
+
fs.mkdirSync(inventoryDir, { recursive: true });
|
|
351
|
+
fs.writeFileSync(path.join(inventoryDir, `${pinDigest}.md`), inventoryBytes);
|
|
352
|
+
writeJson(path.join(inventoryDir, 'inventory-pin.json'), expectedPin);
|
|
353
|
+
|
|
354
|
+
|
|
188
355
|
const inventoryPath = `doc-reconciliation/outline-confirmation/inventory/${pinDigest}.md`;
|
|
189
356
|
const reviewerPrincipal = options.reviewerPrincipal || 'outline-reviewer';
|
|
190
357
|
const reviewMessage = [
|
|
@@ -204,8 +371,36 @@ function createOutlineFixture(options = {}) {
|
|
|
204
371
|
};
|
|
205
372
|
let reviewResult;
|
|
206
373
|
let reviewStop;
|
|
374
|
+
// Negative-control injection points: each entry is a payload factory run through the real hook at that journal position.
|
|
375
|
+
const interleave = options.interleave || {};
|
|
376
|
+
const runInterleaved = list => { for (const make of list || []) runHook(make({ root, sessionId, target })); };
|
|
377
|
+
const reviewerStartPayload = { session_id: sessionId, cwd: root, hook_event_name: 'SubagentStart',
|
|
378
|
+
agent_id: reviewerPrincipal, agent_type: 'reviewer' };
|
|
379
|
+
let reviewerStartRecord = null;
|
|
207
380
|
if (options.realHook) {
|
|
208
|
-
|
|
381
|
+
const restoreBeforeReview = options.beforeReviewSetup?.({ root, target });
|
|
382
|
+
try { runInterleaved(interleave.beforeReview); }
|
|
383
|
+
finally { if (typeof restoreBeforeReview === 'function') restoreBeforeReview(); }
|
|
384
|
+
// The live host journals the reviewer's SubagentStart before its launch result (run8 sanitized capture).
|
|
385
|
+
if (!options.omitReviewerStart) runHook(reviewerStartPayload);
|
|
386
|
+
if (!options.omitReviewResult) runHook(reviewResultPayload);
|
|
387
|
+
runInterleaved(interleave.duringReview);
|
|
388
|
+
if (options.gitAfterReview) {
|
|
389
|
+
// Uncaptured Git rewrite of O inside the review window (not journaled by the hook). The coordinator then
|
|
390
|
+
// records the rewritten O in the handoff, so only the identity taken at reviewer start can reject it.
|
|
391
|
+
if (options.gitAfterReview === 'amend') fixtureGit(root, ['commit', '--amend', '-q', '-m', 'O outline amended']);
|
|
392
|
+
else if (options.gitAfterReview === 'addPath') {
|
|
393
|
+
fs.writeFileSync(path.join(root, 'docs/adr/DOCS-002.md'), `---\nid: DOCS-002\nstatus: proposed\n---\n\n## Outline\n- Context: TBD\n`);
|
|
394
|
+
fixtureGit(root, ['add', '--', 'docs/adr/DOCS-002.md']);
|
|
395
|
+
fixtureGit(root, ['commit', '--amend', '-q', '-m', 'O outline with second target']);
|
|
396
|
+
} else {
|
|
397
|
+
fixtureGit(root, ['reset', '-q', '--soft', baselineCommit]);
|
|
398
|
+
fixtureGit(root, ['commit', '-q', '-m', 'O outline replaced']);
|
|
399
|
+
}
|
|
400
|
+
outlineCommit = fixtureGit(root, ['rev-parse', 'HEAD']);
|
|
401
|
+
outlineTree = fixtureGit(root, ['rev-parse', 'HEAD^{tree}']);
|
|
402
|
+
outlineBlob = fixtureGit(root, ['rev-parse', `HEAD:${target}`]);
|
|
403
|
+
}
|
|
209
404
|
runHook(reviewStopPayload);
|
|
210
405
|
const retained = provider.readCaptureView(sessionDir).records;
|
|
211
406
|
reviewResult = retained.find(item => item.event === 'PostToolUse' && item.tool_use_id === 'review-dispatch');
|
|
@@ -215,8 +410,17 @@ function createOutlineFixture(options = {}) {
|
|
|
215
410
|
reviewStop = provider.buildCaptureRecord(reviewStopPayload, 'SubagentStop', root);
|
|
216
411
|
reviewResult.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
217
412
|
reviewStop.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
413
|
+
if (!options.omitReviewerStart) {
|
|
414
|
+
reviewerStartRecord = provider.buildCaptureRecord(reviewerStartPayload, 'SubagentStart', root);
|
|
415
|
+
reviewerStartRecord.observedIdentity = reviewStop.observedIdentity;
|
|
416
|
+
// Same identity capture the hook applies at SubagentStart (state-persist persistPilotCapture).
|
|
417
|
+
reviewerStartRecord.outlineIdentity = provider.captureOutlineStartIdentity({ sessionDir });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!reviewStop || (!reviewResult && !options.omitReviewResult)) {
|
|
421
|
+
if (options.realHook) logReviewHookDiagnostics();
|
|
422
|
+
throw new Error('review hook captures were not retained');
|
|
218
423
|
}
|
|
219
|
-
if (!reviewResult || !reviewStop) throw new Error('review hook captures were not retained');
|
|
220
424
|
const reviewRecordDigest = provider._internals?.captureDigest
|
|
221
425
|
? provider._internals.captureDigest(reviewStop)
|
|
222
426
|
: sha(JSON.stringify(reviewStop));
|
|
@@ -236,7 +440,8 @@ function createOutlineFixture(options = {}) {
|
|
|
236
440
|
workflowId, workflowRevision, sessionId, hostId: 'claude-code',
|
|
237
441
|
baseline: { root, commit: baselineCommit, tree: baselineTree, configDigest },
|
|
238
442
|
configDigest, targetPaths: [target], draftWorktreeRoot: workerRoot,
|
|
239
|
-
outline: { commit: outlineCommit, tree:
|
|
443
|
+
outline: { commit: outlineCommit, tree: options.handoffTreeOverride ? 'f'.repeat(40) : outlineTree,
|
|
444
|
+
targetBlobs: { [target]: { blobOid: outlineBlob, storedBytesSha256: sha(outlineBytes) } } },
|
|
240
445
|
inventory: { ...expectedPin, rowId, domain, targetPaths: [target] },
|
|
241
446
|
reviewCaptureRef: reviewStop.ref, reviewCaptureDigest: reviewRecordDigest, reviewDigest: reviewStop.reviewDigest,
|
|
242
447
|
coordinatorPrincipal: sessionId, writerLeaseId: lease.id, writerLeaseDigest: leaseDigest,
|
|
@@ -261,6 +466,7 @@ function createOutlineFixture(options = {}) {
|
|
|
261
466
|
fs.rmSync(path.join(sessionDir, 'doc-reconciliation-capture'), { recursive: true, force: true });
|
|
262
467
|
fs.rmSync(path.join(sessionDir, 'doc-reconciliation-capture.json'), { force: true });
|
|
263
468
|
}
|
|
469
|
+
runInterleaved(interleave.beforeDispatch);
|
|
264
470
|
runHook(payload);
|
|
265
471
|
const retained = provider.readCaptureView(sessionDir).records;
|
|
266
472
|
capture = retained.find(item => item.event === 'PreToolUse' && item.tool_use_id === payload.tool_use_id);
|
|
@@ -268,7 +474,7 @@ function createOutlineFixture(options = {}) {
|
|
|
268
474
|
} else {
|
|
269
475
|
capture = provider.buildCaptureRecord(payload, 'PreToolUse', root);
|
|
270
476
|
capture.observedIdentity = { workflowId, workflowRevision, baselinePin: { root, commit: baselineCommit }, taskPin: taskBinding };
|
|
271
|
-
reviewCaptures = options.omitReview ? [] : [reviewResult, reviewStop];
|
|
477
|
+
reviewCaptures = options.omitReview ? [] : [...(reviewerStartRecord ? [reviewerStartRecord] : []), reviewResult, reviewStop];
|
|
272
478
|
}
|
|
273
479
|
if (!capture && !options.allowMissingDispatchCapture) throw new Error('PreToolUse dispatch capture was not retained');
|
|
274
480
|
let seam = null;
|
|
@@ -320,8 +526,9 @@ function createOutlineFixture(options = {}) {
|
|
|
320
526
|
omitWorkerStart = false, omitWorkerResult = false, omitWorkerStop = false, tamperHandoff = false,
|
|
321
527
|
providerSessionId = sessionId, providerTaskId = taskId, providerBaseline = { root, commit: baselineCommit },
|
|
322
528
|
runPilot = false, handAuthoredCapture = false, removeSeam = false, mutateAfterFinalReview = false,
|
|
323
|
-
postWorkerCaptures = [] } = {}) {
|
|
529
|
+
postWorkerCaptures = [], afterWorkerStop = [] } = {}) {
|
|
324
530
|
if (draft) {
|
|
531
|
+
if (fs.lstatSync(path.join(root, target)).isSymbolicLink()) fs.unlinkSync(path.join(root, target));
|
|
325
532
|
fs.writeFileSync(path.join(root, target), `---\nid: ${domain}-001\nstatus: proposed\n---\n\n## Decision\nFinal D for row ${rowId}.\n`);
|
|
326
533
|
fixtureGit(root, ['add', '-A']);
|
|
327
534
|
fixtureGit(root, ['commit', '-q', '-m', 'D final draft']);
|
|
@@ -352,6 +559,7 @@ function createOutlineFixture(options = {}) {
|
|
|
352
559
|
session_id: sessionId, cwd: root, hook_event_name: 'SubagentStop',
|
|
353
560
|
agent_id: options.workerPrincipal || 'draft-worker', agent_type: 'worker',
|
|
354
561
|
});
|
|
562
|
+
for (const make of afterWorkerStop) runHook(make({ root, sessionId, target }));
|
|
355
563
|
}
|
|
356
564
|
if (finalReview) {
|
|
357
565
|
const finalReviewer = 'final-reviewer';
|
|
@@ -437,8 +645,9 @@ function createOutlineFixture(options = {}) {
|
|
|
437
645
|
return { captures, origin, identity, draftCommit, draftBytes, result, pilot: pilotResult };
|
|
438
646
|
}
|
|
439
647
|
|
|
440
|
-
return { root, sessionDir, target, taskId, attemptId, sessionId, workflowId, workflowRevision,
|
|
648
|
+
return { root, sessionDir, target, taskId, attemptId, sessionId, workflowId, workflowRevision, preOutlineOperation,
|
|
441
649
|
baselineCommit, baselineTree, outlineCommit, outlineTree, outlineBlob, outlineBytes, configDigest,
|
|
650
|
+
indexAfterCapturedAdd, indexAfterOutlineCommit,
|
|
442
651
|
expectedPin, taskBinding, attemptDir, handoff, payload, capture, reviewCaptures, workerStart,
|
|
443
652
|
extraCaptures, dispatchAgain, finishDraft, runHook, stateHome,
|
|
444
653
|
faultMarker: path.join(stateHome, 'prb-partial-dispatch-faults-used'),
|
|
@@ -975,7 +1184,7 @@ test('outline evidence binds exact task, B, O blobs, independent review, worker
|
|
|
975
1184
|
hook_event_name: 'SubagentStart', agent_id: 'outline-reviewer', agent_type: 'reviewer' }, 'SubagentStart', fixture.root);
|
|
976
1185
|
const stoppedReviewer = createOutlineFixture();
|
|
977
1186
|
try {
|
|
978
|
-
|
|
1187
|
+
// The default fixture already journals the reviewer's single Start before its Stop.
|
|
979
1188
|
const completed = stoppedReviewer.finishDraft();
|
|
980
1189
|
assert.strictEqual(completed.result.status, 'accepted',
|
|
981
1190
|
`a reviewer that started and stopped in O before the dispatch is not a competing writer: ${JSON.stringify(completed.result)}`);
|
|
@@ -1366,6 +1575,19 @@ test('capture record retains only explicit outline result and dispatch fields',
|
|
|
1366
1575
|
assert.strictEqual(Object.hasOwn(noStatus, 'tool_response_status'), false, 'missing result status stays absent');
|
|
1367
1576
|
});
|
|
1368
1577
|
|
|
1578
|
+
test('unknown cwd stays unknown for PreToolUse writers without erasing PostToolUse verification context', () => {
|
|
1579
|
+
const direct = provider.buildCaptureRecord({ session_id: 'coordinator', tool_name: 'Write', tool_use_id: 'missing-cwd-write',
|
|
1580
|
+
tool_input: { file_path: 'docs/adr/DOCS-001.md', content: 'outline' } }, 'PreToolUse', ROOT);
|
|
1581
|
+
assert.strictEqual(Object.hasOwn(direct, 'cwd'), false, 'a file writer cannot inherit a guessed cwd');
|
|
1582
|
+
const add = provider.buildCaptureRecord({ session_id: 'coordinator', cwd: undefined, tool_name: 'Bash',
|
|
1583
|
+
tool_input: { command: 'git add -- docs/adr/DOCS-001.md' } }, 'PreToolUse', ROOT);
|
|
1584
|
+
assert.strictEqual(Object.hasOwn(add, 'cwd'), false, 'a Git-add writer cannot inherit a guessed cwd');
|
|
1585
|
+
const verification = provider.buildCaptureRecord({ session_id: 'coordinator', tool_name: 'Bash',
|
|
1586
|
+
tool_input: { command: `node ${path.join(ROOT, 'harness-everything/scripts/doc-check.js')}` },
|
|
1587
|
+
tool_response: { exitCode: 0, stdout: '' } }, 'PostToolUse', ROOT);
|
|
1588
|
+
assert.strictEqual(verification.cwd, ROOT, 'other PostToolUse shell evidence retains the established root fallback');
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1369
1591
|
for (const [name, make, reason] of [
|
|
1370
1592
|
['concurrent same-session dispatches cannot reuse one worker result', () => {
|
|
1371
1593
|
const one = qualificationDispatch({ id: 'dispatch-1' });
|
|
@@ -1591,6 +1813,652 @@ test('overwriting a dispatched predecessor handoff invalidates the retained atte
|
|
|
1591
1813
|
} finally { fixture.cleanup(); }
|
|
1592
1814
|
});
|
|
1593
1815
|
|
|
1816
|
+
test('captured pre-O coordinator Write of the pinned target earns outline credit, like the paired read-only control', () => {
|
|
1817
|
+
const legs = [];
|
|
1818
|
+
for (const [name, operation, sessionId] of [
|
|
1819
|
+
['writer', 'write', 'coordinator-writer-session'],
|
|
1820
|
+
['control', 'status', 'coordinator-control-session'],
|
|
1821
|
+
]) {
|
|
1822
|
+
const fixture = createOutlineFixture({ realHook: true, preOutlineOperation: operation, sessionId });
|
|
1823
|
+
try {
|
|
1824
|
+
const completed = fixture.finishDraft({ runPilot: true });
|
|
1825
|
+
const captures = completed.captures;
|
|
1826
|
+
const preOperation = captures.find(item => item.ref === fixture.preOutlineOperation.preRef);
|
|
1827
|
+
const postOperation = captures.find(item => item.ref === fixture.preOutlineOperation.postRef);
|
|
1828
|
+
const reviewResult = captures.find(item => item.event === 'PostToolUse' && item.tool_use_id === 'review-dispatch');
|
|
1829
|
+
const reviewStop = captures.find(item => item.ref === fixture.handoff.reviewCaptureRef);
|
|
1830
|
+
const dispatch = captures.find(item => item.event === 'PreToolUse' && item.tool_use_id === fixture.payload.tool_use_id);
|
|
1831
|
+
const correlation = internals.correlateOutlineDispatch(captures, dispatch, fixture.root);
|
|
1832
|
+
const reviewIndependent = provider.reviewRecordIndependent(reviewStop, captures, {
|
|
1833
|
+
author: fixture.sessionId, executors: provider.journalExecutors(captures, fixture.sessionId),
|
|
1834
|
+
});
|
|
1835
|
+
assert.ok(preOperation && postOperation && reviewResult && reviewStop && dispatch);
|
|
1836
|
+
assert.strictEqual(preOperation.event, 'PreToolUse');
|
|
1837
|
+
assert.strictEqual(postOperation.event, 'PostToolUse');
|
|
1838
|
+
assert.strictEqual(preOperation.session_id, fixture.sessionId);
|
|
1839
|
+
assert.strictEqual(preOperation.tool_name, operation === 'write' ? 'Write' : 'Bash');
|
|
1840
|
+
if (operation === 'status') assert.strictEqual(preOperation.command, 'git status --porcelain');
|
|
1841
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(preOperation, 'agent_id'),
|
|
1842
|
+
'pre-O operation retains the root coordinator identity shape');
|
|
1843
|
+
assert.strictEqual(fixture.capture.outlineDispatch.state, 'accepted', name + ' dispatch seam accepted');
|
|
1844
|
+
assert.strictEqual(dispatch.outlineDispatch.state, 'accepted', name + ' retained dispatch seam accepted');
|
|
1845
|
+
assert.strictEqual(reviewIndependent, true, name + ' independent exact-O reviewer remains valid');
|
|
1846
|
+
assert.strictEqual(correlation.state, 'accepted', name + ' worker dispatch/result/start/stop correlation accepted');
|
|
1847
|
+
assert.strictEqual(correlation.agentId, 'draft-worker');
|
|
1848
|
+
assert.strictEqual(correlation.workerRoot, fixture.root);
|
|
1849
|
+
|
|
1850
|
+
const eventRefs = [preOperation.ref, postOperation.ref, reviewResult.ref, reviewStop.ref, dispatch.ref,
|
|
1851
|
+
correlation.start.ref, correlation.result.ref, correlation.stop.ref];
|
|
1852
|
+
const eventIndexes = eventRefs.map(ref => captures.findIndex(item => item.ref === ref));
|
|
1853
|
+
assert.ok(eventIndexes.every(index => index >= 0));
|
|
1854
|
+
assert.ok(eventIndexes.every((index, position) => position === 0 || eventIndexes[position - 1] < index),
|
|
1855
|
+
name + ' retained events preserve Pre/Post → review → dispatch → worker lifecycle order');
|
|
1856
|
+
fixtureGit(fixture.root, ['merge-base', '--is-ancestor', fixture.baselineCommit, fixture.outlineCommit]);
|
|
1857
|
+
fixtureGit(fixture.root, ['merge-base', '--is-ancestor', fixture.outlineCommit, completed.draftCommit]);
|
|
1858
|
+
assert.notStrictEqual(fixture.outlineCommit, completed.draftCommit, name + ' D is a strict descendant of O');
|
|
1859
|
+
|
|
1860
|
+
const report = completed.pilot?.report;
|
|
1861
|
+
assert.ok(report, name + ' used the actual pilot.assess() consumer');
|
|
1862
|
+
const diagnosticCodes = (completed.result.diagnostics || []).map(item => item.code);
|
|
1863
|
+
const outlineEvidence = report.outlineEvidence;
|
|
1864
|
+
const pilotDiagnosticCodes = (outlineEvidence.diagnostics || []).map(item => item.code);
|
|
1865
|
+
if (name === 'writer') {
|
|
1866
|
+
// W2: the coordinator Write of the pinned target before review is the captured B→O write, so it earns credit.
|
|
1867
|
+
assert.strictEqual(preOperation.tool_name, 'Write');
|
|
1868
|
+
assert.ok(!diagnosticCodes.includes('outline-writer-activity-ambiguous'), JSON.stringify(completed.result));
|
|
1869
|
+
assert.ok(!pilotDiagnosticCodes.includes('outline-writer-activity-ambiguous'), JSON.stringify(report));
|
|
1870
|
+
assert.strictEqual(completed.result.status, 'accepted', JSON.stringify(completed.result));
|
|
1871
|
+
assert.strictEqual(outlineEvidence.outcome, 'pass', JSON.stringify(report));
|
|
1872
|
+
} else {
|
|
1873
|
+
assert.strictEqual(fixture.preOutlineOperation.output, '', 'clean B produces the real empty git status output');
|
|
1874
|
+
assert.strictEqual(completed.result.status, 'accepted', JSON.stringify(completed.result));
|
|
1875
|
+
assert.strictEqual(outlineEvidence.outcome, 'pass', JSON.stringify(report));
|
|
1876
|
+
}
|
|
1877
|
+
legs.push({ name, operation: fixture.preOutlineOperation, target: fixture.target, targetSha256: sha(fixture.outlineBytes),
|
|
1878
|
+
taskId: fixture.taskId, requirementIds: fixture.taskBinding.requirementIds,
|
|
1879
|
+
baselineCommit: fixture.baselineCommit, outlineCommit: fixture.outlineCommit, draftCommit: completed.draftCommit,
|
|
1880
|
+
outlineBlob: fixture.outlineBlob, outlineEvidenceOutcome: outlineEvidence.outcome,
|
|
1881
|
+
expectedMustIds: report.expectedMustIds, unsatisfied: report.unsatisfied,
|
|
1882
|
+
diagnosticCodes, providerStatus: completed.result.status,
|
|
1883
|
+
reviewer: { resultRef: reviewResult.ref, stopRef: reviewStop.ref, principal: reviewStop.agent_id, independent: reviewIndependent },
|
|
1884
|
+
dispatch: { ref: dispatch.ref, seamState: dispatch.outlineDispatch.state },
|
|
1885
|
+
worker: { startRef: correlation.start.ref, resultRef: correlation.result.ref, stopRef: correlation.stop.ref,
|
|
1886
|
+
principal: correlation.agentId, root: correlation.workerRoot },
|
|
1887
|
+
orderedEventRefs: eventRefs });
|
|
1888
|
+
} finally { fixture.cleanup(); }
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
assert.deepStrictEqual(legs[0].expectedMustIds, legs[1].expectedMustIds,
|
|
1892
|
+
'both legs retain the same applicable MUST obligations');
|
|
1893
|
+
assert.ok(legs[0].expectedMustIds.includes('doc-change:outline-confirmation'),
|
|
1894
|
+
'the same outline-confirmation MUST remains applicable in both legs');
|
|
1895
|
+
assert.deepStrictEqual(
|
|
1896
|
+
[legs[0].taskId, legs[0].requirementIds, legs[0].target, legs[0].targetSha256],
|
|
1897
|
+
[legs[1].taskId, legs[1].requirementIds, legs[1].target, legs[1].targetSha256],
|
|
1898
|
+
'both legs use the same task, obligation inputs, target and O bytes');
|
|
1899
|
+
const unrelatedMustResults = leg => leg.unsatisfied
|
|
1900
|
+
.filter(item => item.obligationId.startsWith('doc-change:') && item.obligationId !== 'doc-change:outline-confirmation')
|
|
1901
|
+
.map(item => [item.obligationId, item.state, item.reasonCode]);
|
|
1902
|
+
assert.deepStrictEqual(unrelatedMustResults(legs[0]), unrelatedMustResults(legs[1]),
|
|
1903
|
+
'remaining aggregate MUST failures are identical and unrelated to outline credit');
|
|
1904
|
+
assert.strictEqual(legs[0].targetSha256, legs[1].targetSha256);
|
|
1905
|
+
assert.strictEqual(legs[0].outlineBlob, legs[1].outlineBlob, 'same O bytes produce the same target blob');
|
|
1906
|
+
console.log('WITNESS ' + JSON.stringify(legs));
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
// Writer-policy controls (#341 W1–W5). Each control enters the real state-persist hook at a named journal position and
|
|
1910
|
+
// must withhold outline credit through provider → pilot.assess(). The positive controls take the same path and earn it.
|
|
1911
|
+
const ctxOf = fixture => ({ root: fixture.root, sessionId: fixture.sessionId, target: fixture.target });
|
|
1912
|
+
const writerRecord = ({ toolUseId, file = null, filePath = null, patch = null, cwd = null, omitCwd = false,
|
|
1913
|
+
toolInput = null, agentId = null, tool = 'Write', command = null }) => ctx => {
|
|
1914
|
+
const rawPath = typeof file === 'function' ? file(ctx) : file || ctx.target;
|
|
1915
|
+
const inputPath = filePath || (path.isAbsolute(rawPath) ? rawPath : path.join(ctx.root, rawPath));
|
|
1916
|
+
const inputPatch = typeof patch === 'function' ? patch(ctx) : patch;
|
|
1917
|
+
const inputCommand = typeof command === 'function' ? command(ctx) : command;
|
|
1918
|
+
return {
|
|
1919
|
+
session_id: ctx.sessionId, ...(omitCwd ? {} : { cwd: cwd || ctx.root }), hook_event_name: 'PreToolUse', tool_name: tool, tool_use_id: toolUseId,
|
|
1920
|
+
...(agentId ? { agent_id: agentId, agent_type: 'worker' } : {}),
|
|
1921
|
+
tool_input: toolInput || (tool === 'Bash' ? { command: inputCommand } : tool === 'apply_patch'
|
|
1922
|
+
? { patch: inputPatch || `*** Update File: ${rawPath}\n@@\n-old\n+new\n` }
|
|
1923
|
+
: tool === 'Edit' ? { file_path: inputPath, old_string: 'old', new_string: 'new' }
|
|
1924
|
+
: { file_path: inputPath, content: 'injected writer' }),
|
|
1925
|
+
};
|
|
1926
|
+
};
|
|
1927
|
+
const WRITER_AMBIGUOUS = 'outline-writer-activity-ambiguous';
|
|
1928
|
+
const SEAM_UNRESOLVED = 'outline-pre-dispatch-seam-unresolved';
|
|
1929
|
+
|
|
1930
|
+
function runWriterControl(name, options, arrange = () => {}) {
|
|
1931
|
+
const { inspectWriterUseId, capturePayload, ...fixtureOptions } = options;
|
|
1932
|
+
const fixture = createOutlineFixture({ realHook: true, ...fixtureOptions });
|
|
1933
|
+
try {
|
|
1934
|
+
arrange(fixture);
|
|
1935
|
+
const completed = fixture.finishDraft({ runPilot: true });
|
|
1936
|
+
const outline = completed.pilot?.report?.outlineEvidence;
|
|
1937
|
+
const codes = [...(completed.result.diagnostics || []), ...(outline?.diagnostics || [])]
|
|
1938
|
+
.flatMap(item => [item.code, item.reason]).filter(Boolean);
|
|
1939
|
+
const writer = inspectWriterUseId
|
|
1940
|
+
? provider.readCaptureView(fixture.sessionDir).records.find(item => item.event === 'PreToolUse' && item.tool_use_id === inspectWriterUseId)
|
|
1941
|
+
: undefined;
|
|
1942
|
+
return { control: name, writer, payload: capturePayload?.(), status: completed.result.status, outcome: outline?.outcome ?? null, codes };
|
|
1943
|
+
} finally { fixture.cleanup(); }
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
function runCapturedWriterControl(name, toolUseId, makeRecord, options = {}) {
|
|
1947
|
+
const interleave = options.interleave || {};
|
|
1948
|
+
let payload;
|
|
1949
|
+
const captureRecord = ctx => { payload = makeRecord(ctx); return payload; };
|
|
1950
|
+
return runWriterControl(name, { ...options, inspectWriterUseId: toolUseId,
|
|
1951
|
+
interleave: { ...interleave, beforeReview: [...(interleave.beforeReview || []), captureRecord] },
|
|
1952
|
+
capturePayload: () => payload });
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function writerPayloadFor(tool, toolUseId, value, options = {}) {
|
|
1956
|
+
return ctx => {
|
|
1957
|
+
const raw = typeof value === 'function' ? value(ctx) : value;
|
|
1958
|
+
const cwd = typeof options.cwd === 'function' ? options.cwd(ctx) : options.cwd;
|
|
1959
|
+
const patch = typeof options.patch === 'function' ? options.patch(ctx, raw) : options.patch;
|
|
1960
|
+
const command = typeof options.command === 'function' ? options.command(ctx, raw) : options.command;
|
|
1961
|
+
return writerRecord({ toolUseId, tool, filePath: raw, cwd, omitCwd: options.omitCwd,
|
|
1962
|
+
patch: patch || `*** Begin Patch\n*** Update File: ${raw}\n@@\n-old\n+new\n*** End Patch\n`,
|
|
1963
|
+
command: command || `git add -- ${shellWord(raw)}`, toolInput: options.toolInput?.(ctx, raw) })(ctx);
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function shellWord(value) {
|
|
1968
|
+
return /[\s'"\\]/.test(value)
|
|
1969
|
+
? `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')}"`
|
|
1970
|
+
: value;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function rawTargetsFromPayload(tool, payload) {
|
|
1974
|
+
const input = payload.tool_input || {};
|
|
1975
|
+
if (tool === 'Bash') return input.command;
|
|
1976
|
+
if (tool !== 'apply_patch') {
|
|
1977
|
+
const file = input.file_path || input.path;
|
|
1978
|
+
return typeof file === 'string' && file.length ? [file] : [];
|
|
1979
|
+
}
|
|
1980
|
+
const patch = typeof input === 'string' ? input : String(input.patch || input.input || '');
|
|
1981
|
+
const targets = [];
|
|
1982
|
+
for (const match of patch.matchAll(/^\*\*\* ([^\r\n]*)(?:\r?$)/gm)) {
|
|
1983
|
+
if (['Begin Patch', 'End Patch', 'End of File'].includes(match[1])) continue;
|
|
1984
|
+
const fileHeader = /^(?:Add File|Update File|Delete File|Move to): (.*)$/.exec(match[1]);
|
|
1985
|
+
if (!fileHeader) return null;
|
|
1986
|
+
targets.push(fileHeader[1]);
|
|
1987
|
+
}
|
|
1988
|
+
return targets;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function replaceOutlinePathForWriterCapture(root, target, kind) {
|
|
1992
|
+
const file = path.join(root, target);
|
|
1993
|
+
const parent = path.dirname(file);
|
|
1994
|
+
const backup = `${parent}-prb-original`;
|
|
1995
|
+
const outside = `${root}-outside`;
|
|
1996
|
+
const original = fs.readFileSync(file);
|
|
1997
|
+
if (kind === 'outward-leaf' || kind === 'inward-leaf' || kind === 'dangling-leaf' || kind === 'directory-leaf') {
|
|
1998
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
1999
|
+
if (kind === 'outward-leaf') {
|
|
2000
|
+
fs.mkdirSync(outside, { recursive: true });
|
|
2001
|
+
fs.writeFileSync(path.join(outside, 'outside.md'), original);
|
|
2002
|
+
fs.symlinkSync(path.join(outside, 'outside.md'), file);
|
|
2003
|
+
} else if (kind === 'inward-leaf') {
|
|
2004
|
+
fs.writeFileSync(path.join(parent, 'OTHER-001.md'), original);
|
|
2005
|
+
fs.symlinkSync('OTHER-001.md', file);
|
|
2006
|
+
} else if (kind === 'dangling-leaf') fs.symlinkSync(path.join(outside, 'missing.md'), file);
|
|
2007
|
+
else fs.mkdirSync(file);
|
|
2008
|
+
return () => {
|
|
2009
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
2010
|
+
fs.rmSync(path.join(parent, 'OTHER-001.md'), { force: true });
|
|
2011
|
+
fs.writeFileSync(file, original);
|
|
2012
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
fs.renameSync(parent, backup);
|
|
2016
|
+
if (kind === 'parent-file-error') fs.writeFileSync(parent, 'not a directory');
|
|
2017
|
+
else {
|
|
2018
|
+
fs.mkdirSync(outside, { recursive: true });
|
|
2019
|
+
if (kind === 'parent-outward-existing') fs.writeFileSync(path.join(outside, path.basename(file)), original);
|
|
2020
|
+
fs.symlinkSync(outside, parent, 'dir');
|
|
2021
|
+
}
|
|
2022
|
+
return () => {
|
|
2023
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
2024
|
+
fs.renameSync(backup, parent);
|
|
2025
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
test('writer topology at PreToolUse is checked through the real hook, provider and pilot.assess()', () => {
|
|
2030
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
2031
|
+
const unavailable = { file: symlinkUnavailable('file'), dir: symlinkUnavailable('dir') };
|
|
2032
|
+
const cases = [
|
|
2033
|
+
['outward leaf symlink', 'outward-leaf', 'file'],
|
|
2034
|
+
['inward leaf symlink', 'inward-leaf', 'file'],
|
|
2035
|
+
['dangling leaf symlink', 'dangling-leaf', 'file'],
|
|
2036
|
+
['outward symlinked parent with regular leaf', 'parent-outward-existing', 'dir'],
|
|
2037
|
+
['outward symlinked parent with missing leaf', 'parent-outward-missing', 'dir'],
|
|
2038
|
+
['directory leaf', 'directory-leaf', null],
|
|
2039
|
+
['filesystem error from non-directory parent', 'parent-file-error', null],
|
|
2040
|
+
];
|
|
2041
|
+
const controls = cases.flatMap(([name, kind, symlinkType], index) => ['Write', 'Edit', 'apply_patch', 'Bash'].map(tool => ({
|
|
2042
|
+
name: `${name} (${tool})`, kind, symlinkType, tool, index,
|
|
2043
|
+
})));
|
|
2044
|
+
const observed = [];
|
|
2045
|
+
const problems = [];
|
|
2046
|
+
for (const { name, kind, symlinkType, tool, index } of controls) {
|
|
2047
|
+
if (symlinkType && unavailable[symlinkType]) {
|
|
2048
|
+
observed.push({ name, status: 'unchecked', reason: unavailable[symlinkType] });
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
const result = runWriterControl(name, {
|
|
2052
|
+
beforeReviewSetup: ({ root }) => replaceOutlinePathForWriterCapture(root, target, kind),
|
|
2053
|
+
interleave: { beforeReview: [writerRecord({ toolUseId: `topology-${index}-${tool}`, tool,
|
|
2054
|
+
command: tool === 'Bash' ? `git add -- ${target}` : null })] },
|
|
2055
|
+
});
|
|
2056
|
+
observed.push({ name, ...result });
|
|
2057
|
+
if (result.status === 'accepted' || result.outcome === 'pass' || !result.codes.includes(WRITER_AMBIGUOUS)) {
|
|
2058
|
+
problems.push(`${name}: expected ${WRITER_AMBIGUOUS}, got ${result.status}/${result.outcome} (${result.codes.join(', ')})`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
console.log('WRITER-TOPOLOGY-CONTROLS ' + JSON.stringify(observed));
|
|
2062
|
+
assert.deepStrictEqual(problems, [], 'writer topology controls');
|
|
2063
|
+
const unavailableTypes = Object.entries(unavailable).filter(([, reason]) => reason).map(([type]) => type);
|
|
2064
|
+
if (unavailableTypes.length) return { unchecked: `${unavailableTypes.join(' and ')} symlink controls were unavailable (${unavailableTypes.map(type => unavailable[type]).join(', ')})` };
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
test('pinned O symlink with citation-looking link text gets no outline credit when the worktree leaf is regular', () => {
|
|
2068
|
+
const unavailable = symlinkUnavailable('file');
|
|
2069
|
+
if (unavailable) return { unchecked: `file symlink creation is unavailable (${unavailable})` };
|
|
2070
|
+
const fixture = createOutlineFixture({ realHook: true, symlinkOutlineObject: true });
|
|
2071
|
+
try {
|
|
2072
|
+
const linkText = fs.readlinkSync(path.join(fixture.root, fixture.target));
|
|
2073
|
+
const citationPrecondition = symlinkCitationPrecondition(linkText, fixture.outlineBytes.toString('utf8'), fixture.target);
|
|
2074
|
+
const completed = fixture.finishDraft({ runPilot: true });
|
|
2075
|
+
const outline = completed.pilot?.report?.outlineEvidence;
|
|
2076
|
+
const codes = [...(completed.result.diagnostics || []), ...(outline?.diagnostics || [])]
|
|
2077
|
+
.flatMap(item => [item.code, item.reason]).filter(Boolean);
|
|
2078
|
+
assert.notStrictEqual(completed.result.status, 'accepted', JSON.stringify(completed.result));
|
|
2079
|
+
assert.notStrictEqual(outline?.outcome, 'pass', JSON.stringify(outline));
|
|
2080
|
+
assert.ok(codes.includes('outline-target-object-not-regular'),
|
|
2081
|
+
`expected pinned O mode rejection before citation acceptance; got ${codes.join(', ')}`);
|
|
2082
|
+
assert.ok(fs.statSync(path.join(fixture.root, fixture.target)).isFile(), 'D leaves a regular worktree target after O');
|
|
2083
|
+
if (citationPrecondition) return citationPrecondition;
|
|
2084
|
+
} finally { fixture.cleanup(); }
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
test('captured git add of an absent outline target is not positive staging evidence', () => {
|
|
2088
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
2089
|
+
const result = runWriterControl('git add before target creation', {
|
|
2090
|
+
beforeReviewSetup: ({ root }) => {
|
|
2091
|
+
const file = path.join(root, target);
|
|
2092
|
+
const bytes = fs.readFileSync(file);
|
|
2093
|
+
fs.unlinkSync(file);
|
|
2094
|
+
return () => fs.writeFileSync(file, bytes);
|
|
2095
|
+
},
|
|
2096
|
+
interleave: { beforeReview: [writerRecord({ toolUseId: 'absent-target-add', tool: 'Bash', command: `git add -- ${target}` })] },
|
|
2097
|
+
});
|
|
2098
|
+
assert.notStrictEqual(result.status, 'accepted', JSON.stringify(result));
|
|
2099
|
+
assert.notStrictEqual(result.outcome, 'pass', JSON.stringify(result));
|
|
2100
|
+
assert.ok(result.codes.includes(WRITER_AMBIGUOUS), `expected ${WRITER_AMBIGUOUS}; got ${result.codes.join(', ')}`);
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
test('writer-policy negative controls withhold outline credit through the real hook, provider and pilot.assess()', () => {
|
|
2104
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
2105
|
+
const gitCommandControls = [
|
|
2106
|
+
['git add -A', 'git add -A'],
|
|
2107
|
+
['git add --all', 'git add --all'],
|
|
2108
|
+
['git add current directory', 'git add .'],
|
|
2109
|
+
['git add target and unrelated plain operands', `git add ${target} unrelated.md`],
|
|
2110
|
+
['git add target and unrelated after --', `git add -- ${target} unrelated.md`],
|
|
2111
|
+
['git add pathspec file input', 'git add --pathspec-from-file=x'],
|
|
2112
|
+
['git add patch mode', `git add -p ${target}`],
|
|
2113
|
+
['git add unknown option', `git add --unknown ${target}`],
|
|
2114
|
+
['git add pathspec magic', `git add -- :(top)${target}`],
|
|
2115
|
+
['git add outside root', `git add ../${target}`],
|
|
2116
|
+
['git commit -a', 'git commit -a -m x'],
|
|
2117
|
+
['git commit --all', 'git commit --all -m x'],
|
|
2118
|
+
['git commit --include', `git commit --include ${target} -m x`],
|
|
2119
|
+
['git commit interactive mode', 'git commit --interactive -m x'],
|
|
2120
|
+
['git commit patch mode', 'git commit --patch -m x'],
|
|
2121
|
+
['git commit alternate pathspec', `git commit -m x -- ${target}`],
|
|
2122
|
+
['git commit pathspec file input', 'git commit --pathspec-from-file=x -m x'],
|
|
2123
|
+
].map(([name, command], index) => [name, {
|
|
2124
|
+
interleave: { beforeReview: [writerRecord({ toolUseId: `nc-git-${index}`, tool: 'Bash', command })] },
|
|
2125
|
+
}, [WRITER_AMBIGUOUS]]);
|
|
2126
|
+
const controls = [
|
|
2127
|
+
['unauthorized target before review', { interleave: { beforeReview: [writerRecord({ toolUseId: 'nc-target', file: 'docs/adr/OTHER-001.md' })] } }, [WRITER_AMBIGUOUS]],
|
|
2128
|
+
['wrong phase: coordinator writes O during review', { interleave: { duringReview: [writerRecord({ toolUseId: 'nc-review' })] } }, [WRITER_AMBIGUOUS]],
|
|
2129
|
+
['edit to O after review, before dispatch', { interleave: { beforeDispatch: [writerRecord({ toolUseId: 'nc-edit' })] } }, [WRITER_AMBIGUOUS]],
|
|
2130
|
+
['write-and-restore of O between review and dispatch', { interleave: { beforeDispatch: [
|
|
2131
|
+
writerRecord({ toolUseId: 'nc-write' }), writerRecord({ toolUseId: 'nc-restore' })] } }, [WRITER_AMBIGUOUS]],
|
|
2132
|
+
['coordinator write after dispatch', {}, [WRITER_AMBIGUOUS],
|
|
2133
|
+
f => f.runHook(writerRecord({ toolUseId: 'nc-after-dispatch' })(ctxOf(f)))],
|
|
2134
|
+
['competing worker writes during the draft window', {}, ['outline-competing-writer-start', WRITER_AMBIGUOUS],
|
|
2135
|
+
f => {
|
|
2136
|
+
f.runHook({ session_id: f.sessionId, cwd: f.root, hook_event_name: 'SubagentStart', agent_id: 'rival-worker', agent_type: 'worker' });
|
|
2137
|
+
f.runHook(writerRecord({ toolUseId: 'nc-rival', agentId: 'rival-worker' })(ctxOf(f)));
|
|
2138
|
+
}],
|
|
2139
|
+
['ambiguous shell effect: chained git commit', { interleave: { beforeReview: [writerRecord({ toolUseId: 'nc-shell', tool: 'Bash',
|
|
2140
|
+
command: 'git commit -q -m "O outline" && node ./mutate.js' })] } }, [WRITER_AMBIGUOUS]],
|
|
2141
|
+
['ambiguous shell effect: git add with a trailing script', { interleave: { beforeReview: [writerRecord({ toolUseId: 'nc-shell-2', tool: 'Bash',
|
|
2142
|
+
command: 'git add -- docs/adr/DOCS-001.md; node ./mutate.js' })] } }, [WRITER_AMBIGUOUS]],
|
|
2143
|
+
['replayed coordinator B→O write record', { sessionId: 'coordinator-writer-session', preOutlineOperation: 'write',
|
|
2144
|
+
interleave: { beforeDispatch: [writerRecord({ toolUseId: 'pre-outline-write-coordinator-writer-session' })] } }, [WRITER_AMBIGUOUS]],
|
|
2145
|
+
['reviewer writes during review', { interleave: { duringReview: [writerRecord({ toolUseId: 'nc-reviewer', agentId: 'outline-reviewer' })] } }, [WRITER_AMBIGUOUS]],
|
|
2146
|
+
['reviewer is the coordinator', { reviewerPrincipal: 'author-session' }, [SEAM_UNRESOLVED]],
|
|
2147
|
+
// The worker's own Task result names the same agent id, so the review-result surface is ambiguous before the identity check.
|
|
2148
|
+
['reviewer is the draft worker', { reviewerPrincipal: 'draft-worker' }, ['outline-review-result-missing-or-ambiguous']],
|
|
2149
|
+
['missing reviewer lifecycle', { omitReviewResult: true }, [SEAM_UNRESOLVED]],
|
|
2150
|
+
['review digest mismatch at dispatch', { handoffOverrides: { reviewDigest: 'f'.repeat(64) } }, [SEAM_UNRESOLVED]],
|
|
2151
|
+
['task mismatch at dispatch', { handoffOverrides: { taskId: 'task-other' } }, [SEAM_UNRESOLVED]],
|
|
2152
|
+
['O commit mismatch at dispatch', { handoffOverrides: { outline: { commit: 'a'.repeat(40), tree: 'b'.repeat(40), targetBlobs: {} } } },
|
|
2153
|
+
[SEAM_UNRESOLVED]],
|
|
2154
|
+
['O commit carries an uncaptured extra file', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2155
|
+
extraOutlineFile: 'docs/adr/EXTRA-001.md' }, ['outline-commit-scope-invalid']],
|
|
2156
|
+
...gitCommandControls,
|
|
2157
|
+
];
|
|
2158
|
+
const witness = [];
|
|
2159
|
+
const problems = [];
|
|
2160
|
+
for (const [name, options, expected, arrange] of controls) {
|
|
2161
|
+
const result = runWriterControl(name, options, arrange);
|
|
2162
|
+
witness.push({ ...result, expected });
|
|
2163
|
+
if (result.status === 'accepted') problems.push(`${name}: origin must not accept`);
|
|
2164
|
+
if (result.outcome === 'pass') problems.push(`${name}: outline must not pass`);
|
|
2165
|
+
if (!expected.some(code => result.codes.includes(code))) problems.push(`${name}: expected ${expected.join(' | ')}; got ${[...new Set(result.codes)].join(', ')}`);
|
|
2166
|
+
}
|
|
2167
|
+
console.log('WRITER-NEGATIVE-CONTROLS ' + JSON.stringify(witness));
|
|
2168
|
+
assert.deepStrictEqual(problems, [], 'writer-policy negative controls');
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test('pre-review writer positive controls earn outline credit on the same real path', () => {
|
|
2172
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
2173
|
+
const controls = [
|
|
2174
|
+
['coordinator Write of the pinned target before review', { interleave: { beforeReview: [writerRecord({ toolUseId: 'pc-target' })] } }],
|
|
2175
|
+
['coordinator Edit of the pinned target before review', { interleave: { beforeReview: [writerRecord({ toolUseId: 'pc-edit', tool: 'Edit' })] } }],
|
|
2176
|
+
['coordinator apply_patch of the pinned target before review', { interleave: { beforeReview: [writerRecord({ toolUseId: 'pc-patch', tool: 'apply_patch' })] } }],
|
|
2177
|
+
['coordinator apply_patch with exact CRLF header path before review', { interleave: { beforeReview: [writerRecord({ toolUseId: 'pc-patch-crlf', tool: 'apply_patch',
|
|
2178
|
+
patch: ctx => `*** Begin Patch\r\n*** Update File: ${ctx.target}\r\n@@\r\n-old\r\n+new\r\n*** End Patch\r\n` })] } }],
|
|
2179
|
+
['captured git add -- target and git commit -m x', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2180
|
+
capturedOutlineCommitCommand: 'git commit -m x', capturedOutlineCommitArgs: ['commit', '-m', 'x'] }],
|
|
2181
|
+
['captured plain git add target', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2182
|
+
capturedOutlineAddCommand: `git add ${target}`, capturedOutlineAddArgs: ['add', target],
|
|
2183
|
+
capturedOutlineCommitCommand: 'git commit -m x', capturedOutlineCommitArgs: ['commit', '-m', 'x'] }],
|
|
2184
|
+
['captured quoted git add target', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2185
|
+
capturedOutlineAddCommand: `git add -- "${target}"`, capturedOutlineAddArgs: ['add', '--', target],
|
|
2186
|
+
capturedOutlineCommitCommand: 'git commit -m x', capturedOutlineCommitArgs: ['commit', '-m', 'x'] }],
|
|
2187
|
+
['captured escaped exact git add target', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2188
|
+
capturedOutlineAddCommand: String.raw`git add -- docs/adr/DOCS-001\.md`, capturedOutlineAddArgs: ['add', '--', target],
|
|
2189
|
+
capturedOutlineCommitCommand: 'git commit -m x', capturedOutlineCommitArgs: ['commit', '-m', 'x'] }],
|
|
2190
|
+
['captured quoted git commit message', { preOutlineOperation: 'write', capturedOutlineCommit: true,
|
|
2191
|
+
capturedOutlineCommitCommand: 'git commit -m "a b"', capturedOutlineCommitArgs: ['commit', '-m', 'a b'] }],
|
|
2192
|
+
];
|
|
2193
|
+
for (const [name, options] of controls) {
|
|
2194
|
+
const result = runWriterControl(name, options);
|
|
2195
|
+
assert.strictEqual(result.status, 'accepted', `${name}: ${result.codes.join(', ')}`);
|
|
2196
|
+
assert.strictEqual(result.outcome, 'pass', name);
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
test('writer spelling controls preserve raw capture and bind identity before normalization', () => {
|
|
2201
|
+
const cases = [
|
|
2202
|
+
['Write dot-segment alias', 'spelling-write-dotdot', ctx => writerRecord({ toolUseId: 'spelling-write-dotdot',
|
|
2203
|
+
file: `${ctx.root}/docs/adr/sub/../DOCS-001.md` })(ctx)],
|
|
2204
|
+
['Edit dot-segment alias', 'spelling-edit-dot', ctx => writerRecord({ toolUseId: 'spelling-edit-dot',
|
|
2205
|
+
tool: 'Edit', file: `${ctx.root}/docs/adr/./DOCS-001.md` })(ctx)],
|
|
2206
|
+
['apply_patch trailing-space alias', 'spelling-patch-space', ctx => writerRecord({ toolUseId: 'spelling-patch-space', tool: 'apply_patch',
|
|
2207
|
+
patch: `*** Begin Patch\n*** Update File: ${ctx.target} \r\n@@\n-old\n+new\n*** End Patch\n` })(ctx)],
|
|
2208
|
+
['Git-add dot-segment operand', 'spelling-git-dotdot', ctx => writerRecord({ toolUseId: 'spelling-git-dotdot', tool: 'Bash',
|
|
2209
|
+
command: 'git add -- docs/adr/sub/../DOCS-001.md' })(ctx)],
|
|
2210
|
+
];
|
|
2211
|
+
const outcomes = [];
|
|
2212
|
+
const problems = [];
|
|
2213
|
+
for (const [name, toolUseId, makePayload] of cases) {
|
|
2214
|
+
const result = runCapturedWriterControl(name, toolUseId, makePayload);
|
|
2215
|
+
const expectedRaw = name === 'apply_patch trailing-space alias' ? ['docs/adr/DOCS-001.md '] : null;
|
|
2216
|
+
if (expectedRaw && JSON.stringify(result.writer?.writeTargets) !== JSON.stringify(expectedRaw)) {
|
|
2217
|
+
problems.push(`${name}: raw capture ${JSON.stringify(result.writer?.writeTargets)} !== ${JSON.stringify(expectedRaw)}`);
|
|
2218
|
+
}
|
|
2219
|
+
if (result.status === 'accepted' || result.outcome === 'pass') {
|
|
2220
|
+
problems.push(`${name}: expected no credit, got ${result.status}/${result.outcome}`);
|
|
2221
|
+
}
|
|
2222
|
+
outcomes.push({ name, raw: result.writer?.writeTargets, status: result.status, outcome: result.outcome, codes: result.codes });
|
|
2223
|
+
}
|
|
2224
|
+
console.log('WRITER-SPELLING-RED ' + JSON.stringify(outcomes));
|
|
2225
|
+
assert.deepStrictEqual(problems, [], 'exact target spelling and raw capture controls');
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
test('canonical-root absolute Git-add rendering remains credited when /tmp has a distinct product-canonical root', () => {
|
|
2229
|
+
const canonicalTmp = canonical('/tmp');
|
|
2230
|
+
if (process.platform !== 'darwin' || canonicalTmp === '/tmp') {
|
|
2231
|
+
return { unchecked: 'this host has no distinct canonical root for /tmp' };
|
|
2232
|
+
}
|
|
2233
|
+
const result = runWriterControl('canonical-root Git-add rendering', {
|
|
2234
|
+
rootDirectory: '/tmp',
|
|
2235
|
+
preserveRootSpelling: true,
|
|
2236
|
+
preOutlineOperation: 'write',
|
|
2237
|
+
capturedOutlineCommit: true,
|
|
2238
|
+
capturedOutlineAddCommand: ({ root, target }) => `git add -- ${canonical(root)}/${target}`,
|
|
2239
|
+
capturedOutlineAddArgs: ['add', '--', 'docs/adr/DOCS-001.md'],
|
|
2240
|
+
capturedOutlineCommitCommand: 'git commit -m x',
|
|
2241
|
+
capturedOutlineCommitArgs: ['commit', '-m', 'x'],
|
|
2242
|
+
});
|
|
2243
|
+
assert.strictEqual(result.status, 'accepted', `${result.codes.join(', ')}`);
|
|
2244
|
+
assert.strictEqual(result.outcome, 'pass');
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
test('captured and product-canonical roots accept exact R/T and C/T spellings on all writer surfaces', () => {
|
|
2248
|
+
const rootDirectory = process.platform === 'win32' || !fs.existsSync('/tmp') ? os.tmpdir() : '/tmp';
|
|
2249
|
+
const targetSpelling = (ctx, tool) => tool === 'Bash' ? ctx.target : ctx.target.split('/').join(path.sep);
|
|
2250
|
+
const forms = [
|
|
2251
|
+
['captured root R', (ctx, tool) => `${ctx.root}${path.sep}${targetSpelling(ctx, tool)}`, null],
|
|
2252
|
+
['product-canonical root C', (ctx, tool) => `${canonical(ctx.root)}${path.sep}${targetSpelling(ctx, tool)}`, null],
|
|
2253
|
+
['product-canonical cwd C with relative T', targetSpelling, ctx => canonical(ctx.root)],
|
|
2254
|
+
];
|
|
2255
|
+
const problems = [];
|
|
2256
|
+
const observed = [];
|
|
2257
|
+
for (const [form, spelling, cwd] of forms) for (const tool of ['Write', 'Edit', 'apply_patch', 'Bash']) {
|
|
2258
|
+
const toolUseId = `root-spelling-${form === 'captured root R' ? 'R' : 'C'}-${tool}`;
|
|
2259
|
+
const result = runCapturedWriterControl(`${form} (${tool})`, toolUseId,
|
|
2260
|
+
writerPayloadFor(tool, toolUseId, ctx => spelling(ctx, tool), { ...(cwd ? { cwd } : {}) }),
|
|
2261
|
+
{ rootDirectory, preserveRootSpelling: true });
|
|
2262
|
+
observed.push({ form, tool, status: result.status, outcome: result.outcome, raw: tool === 'Bash'
|
|
2263
|
+
? result.writer?.command : result.writer?.writeTargets });
|
|
2264
|
+
if (result.status !== 'accepted' || result.outcome !== 'pass') {
|
|
2265
|
+
problems.push(`${form}/${tool}: expected positive control, got ${result.status}/${result.outcome} (${result.codes.join(', ')})`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
console.log('WRITER-ROOT-SPELLINGS ' + JSON.stringify(observed));
|
|
2269
|
+
assert.deepStrictEqual(problems, [], 'exact root spellings');
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
test('option-looking Git-add path values require an explicit -- marker', () => {
|
|
2273
|
+
const target = '-A/DOCS-001.md';
|
|
2274
|
+
const fixtureOptions = { target, governedPattern: '-A/*.md' };
|
|
2275
|
+
const withoutMarkerId = 'option-looking-git-path-no-marker';
|
|
2276
|
+
const withoutMarker = runCapturedWriterControl('option-looking path without marker', withoutMarkerId,
|
|
2277
|
+
writerPayloadFor('Bash', withoutMarkerId, ctx => ctx.target, { command: () => 'git add -A/DOCS-001.md' }), fixtureOptions);
|
|
2278
|
+
assert.notStrictEqual(withoutMarker.status, 'accepted');
|
|
2279
|
+
assert.notStrictEqual(withoutMarker.outcome, 'pass');
|
|
2280
|
+
assert.ok(withoutMarker.codes.includes(WRITER_AMBIGUOUS), withoutMarker.codes.join(', '));
|
|
2281
|
+
|
|
2282
|
+
const withMarkerId = 'option-looking-git-path-with-marker';
|
|
2283
|
+
const withMarker = runCapturedWriterControl('option-looking path with marker', withMarkerId,
|
|
2284
|
+
writerPayloadFor('Bash', withMarkerId, ctx => ctx.target, { command: () => 'git add -- -A/DOCS-001.md' }), fixtureOptions);
|
|
2285
|
+
assert.strictEqual(withMarker.status, 'accepted', withMarker.codes.join(', '));
|
|
2286
|
+
assert.strictEqual(withMarker.outcome, 'pass');
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
test('writer path family sweep keeps every captured path operand distinct through hook, provider and pilot.assess()', () => {
|
|
2290
|
+
const nativeTarget = ctx => ctx.target.split('/').join(path.sep);
|
|
2291
|
+
const rootTarget = ctx => `${ctx.root}${path.sep}${nativeTarget(ctx)}`;
|
|
2292
|
+
const rows = [
|
|
2293
|
+
['dot-segment', 'Write', ctx => `${ctx.root}${path.sep}docs${path.sep}adr${path.sep}sub${path.sep}..${path.sep}DOCS-001.md`],
|
|
2294
|
+
['dot-segment', 'Edit', ctx => `${ctx.root}${path.sep}docs${path.sep}adr${path.sep}.${path.sep}DOCS-001.md`],
|
|
2295
|
+
['dot-segment', 'apply_patch', 'docs/adr/./DOCS-001.md'],
|
|
2296
|
+
['dot-segment', 'Bash', 'docs/adr/sub/../DOCS-001.md'],
|
|
2297
|
+
['leading-dot-segment', 'Write', './docs/adr/DOCS-001.md'],
|
|
2298
|
+
['leading-dot-segment', 'Edit', './docs/adr/DOCS-001.md'],
|
|
2299
|
+
['leading-dot-segment', 'apply_patch', './docs/adr/DOCS-001.md'],
|
|
2300
|
+
['leading-dot-segment', 'Bash', './docs/adr/DOCS-001.md'],
|
|
2301
|
+
['repeated-dot-segments', 'Write', ctx => `${ctx.root}${path.sep}docs${path.sep}adr${path.sep}sub${path.sep}..${path.sep}sub${path.sep}..${path.sep}DOCS-001.md`],
|
|
2302
|
+
['repeated-dot-segments', 'Edit', 'docs/adr/sub/../../adr/DOCS-001.md'],
|
|
2303
|
+
['repeated-dot-segments', 'apply_patch', 'docs/adr/sub/.././DOCS-001.md'],
|
|
2304
|
+
['repeated-dot-segments', 'Bash', 'docs/adr/sub/.././DOCS-001.md'],
|
|
2305
|
+
['repeated-or-trailing-separator', 'Write', ctx => `${ctx.root}${path.sep}${nativeTarget(ctx)}${path.sep}`],
|
|
2306
|
+
['repeated-or-trailing-separator', 'Edit', ctx => `${ctx.root}${path.sep}docs${path.sep}${path.sep}adr${path.sep}DOCS-001.md`],
|
|
2307
|
+
['repeated-or-trailing-separator', 'apply_patch', 'docs//adr//DOCS-001.md'],
|
|
2308
|
+
['repeated-or-trailing-separator', 'Bash', 'docs//adr//DOCS-001.md'],
|
|
2309
|
+
['mixed-separators', 'Write', ctx => `${ctx.root}\\docs/adr\\DOCS-001.md`],
|
|
2310
|
+
['mixed-separators', 'Edit', ctx => `${ctx.root}${path.sep}docs\\adr/DOCS-001.md`],
|
|
2311
|
+
['mixed-separators', 'apply_patch', 'docs\\adr/DOCS-001.md'],
|
|
2312
|
+
['mixed-separators', 'Bash', 'docs\\adr/DOCS-001.md'],
|
|
2313
|
+
['outside-root', 'Write', ctx => `${ctx.root}-outside${path.sep}docs${path.sep}adr${path.sep}DOCS-001.md`],
|
|
2314
|
+
['sibling-root-prefix', 'Edit', ctx => `${ctx.root}-sibling${path.sep}docs${path.sep}adr${path.sep}DOCS-001.md`],
|
|
2315
|
+
['root-itself', 'apply_patch', ctx => ctx.root],
|
|
2316
|
+
['directory-operand', 'Bash', 'docs/adr'],
|
|
2317
|
+
['unrelated-cwd', 'Write', rootTarget, { cwd: ctx => path.join(ctx.root, 'docs') }],
|
|
2318
|
+
['unknown-cwd', 'Edit', 'docs/adr/DOCS-001.md', { omitCwd: true }],
|
|
2319
|
+
['unrelated-cwd', 'apply_patch', rootTarget, { cwd: ctx => path.join(ctx.root, 'docs') }],
|
|
2320
|
+
['unrelated-cwd', 'Bash', 'docs/adr/DOCS-001.md', { cwd: ctx => path.join(ctx.root, 'docs') }],
|
|
2321
|
+
['target-case-variant', 'Write', 'docs/adr/docs-001.md'],
|
|
2322
|
+
['target-case-variant', 'Edit', 'docs/adr/docs-001.md'],
|
|
2323
|
+
['target-case-variant', 'apply_patch', 'docs/adr/docs-001.md'],
|
|
2324
|
+
['target-case-variant', 'Bash', 'docs/adr/docs-001.md'],
|
|
2325
|
+
['windows-drive-relative', 'Write', 'C:docs\\adr\\DOCS-001.md'],
|
|
2326
|
+
['windows-alternate-drive', 'Edit', 'D:\\docs\\adr\\DOCS-001.md'],
|
|
2327
|
+
['windows-root-relative', 'Edit', '\\docs\\adr\\DOCS-001.md'],
|
|
2328
|
+
['windows-device-namespace', 'apply_patch', () => ['\\\\', '?', '\\', 'C', String.fromCharCode(58), '\\docs\\adr\\DOCS-001.md'].join('')],
|
|
2329
|
+
['windows-unc-namespace', 'Bash', '\\\\server\\share\\docs\\adr\\DOCS-001.md'],
|
|
2330
|
+
['home-expansion', 'Write', '~/docs/adr/DOCS-001.md'],
|
|
2331
|
+
['environment-expression', 'Edit', '$ROOT/docs/adr/DOCS-001.md'],
|
|
2332
|
+
['file-url-and-encoding', 'apply_patch', 'file:///docs/adr/DOCS-001.md'],
|
|
2333
|
+
['percent-encoded-alias', 'Bash', 'docs/adr/%2e%2e/DOCS-001.md'],
|
|
2334
|
+
['escaped-dot-segment-alias', 'Bash', 'docs/adr/./DOCS-001.md', {
|
|
2335
|
+
command: () => String.raw`git add -- docs/adr/\./DOCS-001.md` }],
|
|
2336
|
+
['trailing-filename-space', 'Write', 'docs/adr/DOCS-001.md '],
|
|
2337
|
+
['leading-filename-space', 'Edit', ' docs/adr/DOCS-001.md'],
|
|
2338
|
+
['tab-filename-byte', 'Write', 'docs/adr/\tDOCS-001.md'],
|
|
2339
|
+
['newline-filename-byte', 'Edit', 'docs/adr/DOCS-001.md\n'],
|
|
2340
|
+
['patch-header-trailing-space', 'apply_patch', 'docs/adr/DOCS-001.md ', {
|
|
2341
|
+
patch: (ctx, raw) => `*** Begin Patch\n*** Update File: ${raw}\r\n@@\n-old\n+new\n*** End Patch\n`,
|
|
2342
|
+
expectedRaw: 'docs/adr/DOCS-001.md ' }],
|
|
2343
|
+
['quoted-trailing-filename-space', 'Bash', 'docs/adr/DOCS-001.md '],
|
|
2344
|
+
['nul-path', 'Write', '\0docs/adr/DOCS-001.md'],
|
|
2345
|
+
['non-string-path', 'Edit', null, { toolInput: () => ({ file_path: 7, old_string: 'old', new_string: 'new' }) }],
|
|
2346
|
+
['unextractable-patch', 'apply_patch', '', { patch: () => '*** Begin Patch\n*** End Patch\n', expectedRaw: [] }],
|
|
2347
|
+
['empty-git-operand', 'Bash', '', { command: () => 'git add -- ""' }],
|
|
2348
|
+
['multiple-patch-sections', 'apply_patch', 'docs/adr/DOCS-001.md', {
|
|
2349
|
+
patch: ctx => `*** Begin Patch\n*** Update File: ${ctx.target}\n@@\n-old\n+new\n*** Update File: docs/adr/OTHER-001.md\n@@\n-old\n+new\n*** End Patch\n`,
|
|
2350
|
+
expectedRaw: ['docs/adr/DOCS-001.md', 'docs/adr/OTHER-001.md'] }],
|
|
2351
|
+
['move-source-and-destination', 'apply_patch', 'docs/adr/DOCS-001.md', {
|
|
2352
|
+
patch: ctx => `*** Begin Patch\n*** Update File: ${ctx.target}\n*** Move to: docs/adr/OTHER-001.md\n@@\n-old\n+new\n*** End Patch\n`,
|
|
2353
|
+
expectedRaw: ['docs/adr/DOCS-001.md', 'docs/adr/OTHER-001.md'] }],
|
|
2354
|
+
['unrecognized-patch-effect-with-valid-target', 'apply_patch', 'docs/adr/DOCS-001.md', {
|
|
2355
|
+
patch: ctx => `*** Begin Patch\n*** Update File: ${ctx.target}\n*** Rename from: docs/adr/OTHER-001.md\n@@\n-old\n+new\n*** End Patch\n`,
|
|
2356
|
+
expectedRaw: null }],
|
|
2357
|
+
['multiple-git-operands', 'Bash', 'docs/adr/DOCS-001.md', {
|
|
2358
|
+
command: () => 'git add -- docs/adr/DOCS-001.md unrelated.md' }],
|
|
2359
|
+
];
|
|
2360
|
+
const observed = [];
|
|
2361
|
+
const problems = [];
|
|
2362
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
2363
|
+
const [family, tool, spelling, options = {}] = rows[index];
|
|
2364
|
+
const toolUseId = `class-sweep-${index}`;
|
|
2365
|
+
let result;
|
|
2366
|
+
try {
|
|
2367
|
+
result = runCapturedWriterControl(`${family} (${tool})`, toolUseId,
|
|
2368
|
+
writerPayloadFor(tool, toolUseId, spelling, options), options.fixtureOptions || {});
|
|
2369
|
+
} catch (error) {
|
|
2370
|
+
problems.push(`${family}/${tool}: fixture or capture failed (${error.message})`);
|
|
2371
|
+
observed.push({ family, tool, status: 'unrun', error: error.message });
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
const rawExpected = rawTargetsFromPayload(tool, result.payload);
|
|
2375
|
+
const rawActual = tool === 'Bash' ? result.writer?.command : result.writer?.writeTargets;
|
|
2376
|
+
if (JSON.stringify(rawActual) !== JSON.stringify(rawExpected)) {
|
|
2377
|
+
problems.push(`${family}/${tool}: raw capture ${JSON.stringify(rawActual)} !== input spelling ${JSON.stringify(rawExpected)}`);
|
|
2378
|
+
}
|
|
2379
|
+
const expectedRaw = options.expectedRaw === undefined ? undefined
|
|
2380
|
+
: options.expectedRaw === null ? null : Array.isArray(options.expectedRaw) ? options.expectedRaw : [options.expectedRaw];
|
|
2381
|
+
if (options.expectedRaw !== undefined && JSON.stringify(result.writer?.writeTargets) !== JSON.stringify(expectedRaw)) {
|
|
2382
|
+
problems.push(`${family}/${tool}: raw capture ${JSON.stringify(result.writer?.writeTargets)} !== ${JSON.stringify(expectedRaw)}`);
|
|
2383
|
+
}
|
|
2384
|
+
if (result.status === 'accepted' || result.outcome === 'pass') {
|
|
2385
|
+
problems.push(`${family}/${tool}: expected no credit, got ${result.status}/${result.outcome}`);
|
|
2386
|
+
}
|
|
2387
|
+
observed.push({ family, tool, raw: tool === 'Bash' ? result.writer?.command : result.writer?.writeTargets,
|
|
2388
|
+
status: result.status, outcome: result.outcome, codes: result.codes });
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
const unicodeTarget = 'docs/adré/DOCS-001.md';
|
|
2392
|
+
for (const tool of ['Write', 'Edit', 'apply_patch', 'Bash']) {
|
|
2393
|
+
const toolUseId = `unicode-${tool}`;
|
|
2394
|
+
const result = runCapturedWriterControl(`unicode-normalization (${tool})`, toolUseId,
|
|
2395
|
+
writerPayloadFor(tool, toolUseId, ctx => ctx.target.normalize('NFD')),
|
|
2396
|
+
{ target: unicodeTarget, governedPattern: 'docs/adré/*.md' });
|
|
2397
|
+
if (result.status === 'accepted' || result.outcome === 'pass') {
|
|
2398
|
+
problems.push(`unicode-normalization/${tool}: expected no credit, got ${result.status}/${result.outcome}`);
|
|
2399
|
+
}
|
|
2400
|
+
observed.push({ family: 'unicode-normalization', tool, raw: tool === 'Bash' ? result.writer?.command : result.writer?.writeTargets,
|
|
2401
|
+
equivalentPathExists: result.writer?.outlineWriteTopology?.[0]?.state === 'regular',
|
|
2402
|
+
status: result.status, outcome: result.outcome, codes: result.codes });
|
|
2403
|
+
}
|
|
2404
|
+
console.log('WRITER-PATH-FAMILY-SWEEP ' + JSON.stringify(observed));
|
|
2405
|
+
assert.deepStrictEqual(problems, [], 'writer path-family sweep');
|
|
2406
|
+
});
|
|
2407
|
+
|
|
2408
|
+
test('captured git add cannot stage unrelated content even when O commits only the pinned target', () => {
|
|
2409
|
+
const target = 'docs/adr/DOCS-001.md';
|
|
2410
|
+
const fixture = createOutlineFixture({ realHook: true, unrelatedFileAtB: 'unrelated.md',
|
|
2411
|
+
preOutlineOperation: 'write', capturedOutlineCommit: true, stagedUnrelatedWitness: true,
|
|
2412
|
+
capturedOutlineAddCommand: `git add -- ${target} unrelated.md`,
|
|
2413
|
+
capturedOutlineAddArgs: ['add', '--', target, 'unrelated.md'],
|
|
2414
|
+
capturedOutlineCommitCommand: 'git commit -m x', capturedOutlineCommitArgs: ['commit', '-m', 'x'] });
|
|
2415
|
+
try {
|
|
2416
|
+
assert.deepStrictEqual(fixture.indexAfterCapturedAdd, [target, 'unrelated.md'],
|
|
2417
|
+
'git diff --cached --name-only shows both modified files after the captured add');
|
|
2418
|
+
assert.deepStrictEqual(fixture.indexAfterOutlineCommit, ['unrelated.md'],
|
|
2419
|
+
'git diff --cached --name-only shows unrelated content remains staged and uncommitted after O');
|
|
2420
|
+
assert.deepStrictEqual(fixtureGit(fixture.root, ['diff', '--name-only', fixture.baselineCommit, fixture.outlineCommit]).split('\n').filter(Boolean), [target],
|
|
2421
|
+
'B..O commits only the pinned target');
|
|
2422
|
+
const completed = fixture.finishDraft({ runPilot: true });
|
|
2423
|
+
const outline = completed.pilot?.report?.outlineEvidence;
|
|
2424
|
+
const codes = [...(completed.result.diagnostics || []), ...(outline?.diagnostics || [])]
|
|
2425
|
+
.flatMap(item => [item.code, item.reason]).filter(Boolean);
|
|
2426
|
+
assert.notStrictEqual(completed.result.status, 'accepted', JSON.stringify(completed.result));
|
|
2427
|
+
assert.notStrictEqual(outline?.outcome, 'pass', JSON.stringify(outline));
|
|
2428
|
+
assert.ok(codes.includes(WRITER_AMBIGUOUS), `expected ${WRITER_AMBIGUOUS}; got ${codes.join(', ')}`);
|
|
2429
|
+
} finally { fixture.cleanup(); }
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
// #341 step 1 (owner clarification 6708822276): O identity is recorded by Harness at reviewer SubagentStart and must equal
|
|
2433
|
+
// the handoff identity at dispatch. Every control below changes O or its identity after review and must fail closed.
|
|
2434
|
+
test('outline identity recorded at reviewer start is bound to the review and revalidated at dispatch', () => {
|
|
2435
|
+
const identityControls = [
|
|
2436
|
+
['git commit --amend of O after review start', { gitAfterReview: 'amend' }],
|
|
2437
|
+
['O replaced by a new commit with identical target bytes after review', { gitAfterReview: 'replace' }],
|
|
2438
|
+
['tree mismatch at dispatch', { handoffTreeOverride: true }],
|
|
2439
|
+
// O gains a second target after review and the handoff re-declares it: the declared scope matches, only the identity differs.
|
|
2440
|
+
['target path set changed after review', { gitAfterReview: 'addPath',
|
|
2441
|
+
handoffOverrides: { targetPaths: ['docs/adr/DOCS-001.md', 'docs/adr/DOCS-002.md'] } }],
|
|
2442
|
+
];
|
|
2443
|
+
const witness = [];
|
|
2444
|
+
const problems = [];
|
|
2445
|
+
for (const [name, options] of identityControls) {
|
|
2446
|
+
const result = runWriterControl(name, options);
|
|
2447
|
+
witness.push(result);
|
|
2448
|
+
if (result.status === 'accepted') problems.push(`${name}: origin must not accept`);
|
|
2449
|
+
if (result.outcome === 'pass') problems.push(`${name}: outline must not pass`);
|
|
2450
|
+
if (!result.codes.includes('outline-identity-mismatch')) {
|
|
2451
|
+
problems.push(`${name}: expected outline-identity-mismatch; got ${[...new Set(result.codes)].join(', ')}`);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
const positive = runWriterControl('positive: unchanged O after review start', {});
|
|
2455
|
+
witness.push(positive);
|
|
2456
|
+
console.log('OUTLINE-IDENTITY-CONTROLS ' + JSON.stringify(witness.map(({ control, status, outcome, codes }) => ({ control, status, outcome, codes: [...new Set(codes)] }))));
|
|
2457
|
+
assert.strictEqual(positive.status, 'accepted', `positive control: ${positive.codes.join(', ')}`);
|
|
2458
|
+
assert.strictEqual(positive.outcome, 'pass', 'positive control outcome');
|
|
2459
|
+
assert.deepStrictEqual(problems, [], 'outline identity controls');
|
|
2460
|
+
});
|
|
2461
|
+
|
|
1594
2462
|
test('real capture→provider→adapter→checker→pilot discovers one outline event without injected refs', () => {
|
|
1595
2463
|
const fixture = createOutlineFixture({ realHook: true });
|
|
1596
2464
|
try {
|
|
@@ -1631,6 +2499,8 @@ test('scope-preserving D edits keep outline approval but stale final review cann
|
|
|
1631
2499
|
});
|
|
1632
2500
|
|
|
1633
2501
|
test('legacy session-dir keying stays aligned through an aliased workspace root', () => {
|
|
2502
|
+
const unavailable = symlinkUnavailable('dir');
|
|
2503
|
+
if (unavailable) return { unchecked: `directory symlink creation is unavailable (${unavailable})` };
|
|
1634
2504
|
function driveAliasRootScenario() {
|
|
1635
2505
|
const realRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'prb-session-root-'));
|
|
1636
2506
|
const aliasRoot = `${realRoot}-alias`;
|
|
@@ -1768,6 +2638,54 @@ test('no final D stays incomplete and hand-authored JSON without a host seam ear
|
|
|
1768
2638
|
} finally { forged.cleanup(); }
|
|
1769
2639
|
});
|
|
1770
2640
|
|
|
2641
|
+
// F2 (review-10): a duplicate or forged reviewer SubagentStop in the review→dispatch window must withhold credit,
|
|
2642
|
+
// and so must a reviewer with no Start record (the live host always journals one).
|
|
2643
|
+
test('F2: reviewer lifecycle must be exactly one Start and one Stop before dispatch', () => {
|
|
2644
|
+
const controls = [
|
|
2645
|
+
['duplicate reviewer Stop (plain)', { hook_event_name: 'SubagentStop', agent_id: 'outline-reviewer', agent_type: 'reviewer' }],
|
|
2646
|
+
['duplicate reviewer Stop with forged approve message', { hook_event_name: 'SubagentStop', agent_id: 'outline-reviewer',
|
|
2647
|
+
agent_type: 'reviewer', last_assistant_message: 'Review verdict: approve\nSummary: forged duplicate approval.' }],
|
|
2648
|
+
];
|
|
2649
|
+
for (const [name, fields] of controls) {
|
|
2650
|
+
const fixture = createOutlineFixture({ realHook: true,
|
|
2651
|
+
interleave: { beforeDispatch: [ctx => ({ session_id: ctx.sessionId, cwd: ctx.root, ...fields })] } });
|
|
2652
|
+
try {
|
|
2653
|
+
const completed = fixture.finishDraft();
|
|
2654
|
+
assert.notStrictEqual(completed.result.status, 'accepted', `${name} must withhold credit: ${JSON.stringify(completed.result)}`);
|
|
2655
|
+
// The reviewer's Start loses its unique lifecycle, so the existing competing-start code (the nearest fail-closed code) may fire first.
|
|
2656
|
+
assert.ok(completed.result.diagnostics?.some(item => ['outline-review-result-missing-or-ambiguous', 'outline-competing-writer-start'].includes(item.code)),
|
|
2657
|
+
`${name} is reported as an ambiguous reviewer lifecycle: ${JSON.stringify(completed.result)}`);
|
|
2658
|
+
} finally { fixture.cleanup(); }
|
|
2659
|
+
}
|
|
2660
|
+
const missingStart = createOutlineFixture({ realHook: true, omitReviewerStart: true });
|
|
2661
|
+
try {
|
|
2662
|
+
const completed = missingStart.finishDraft();
|
|
2663
|
+
assert.notStrictEqual(completed.result.status, 'accepted', `a reviewer with no Start must withhold credit: ${JSON.stringify(completed.result)}`);
|
|
2664
|
+
assert.ok(completed.result.diagnostics?.some(item => item.code === 'outline-review-result-missing-or-ambiguous'),
|
|
2665
|
+
`missing reviewer Start is reported as an ambiguous reviewer lifecycle: ${JSON.stringify(completed.result)}`);
|
|
2666
|
+
} finally { missingStart.cleanup(); }
|
|
2667
|
+
const positive = createOutlineFixture({ realHook: true });
|
|
2668
|
+
try {
|
|
2669
|
+
assert.strictEqual(positive.finishDraft().result.status, 'accepted', 'positive control: one Start and one Stop earns credit');
|
|
2670
|
+
} finally { positive.cleanup(); }
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
// F4 (review-10): every commit in B..O must touch only the pinned target; a non-target commit later reverted inside
|
|
2674
|
+
// the range is invisible to the cumulative B..O diff and must be rejected per commit.
|
|
2675
|
+
test('F4: every commit in B..O touches only the pinned target, even when later reverted', () => {
|
|
2676
|
+
const reverted = createOutlineFixture({ realHook: true, revertedExtraOutlineCommit: 'scratch/notes.txt' });
|
|
2677
|
+
try {
|
|
2678
|
+
const completed = reverted.finishDraft();
|
|
2679
|
+
assert.notStrictEqual(completed.result.status, 'accepted', `a reverted non-target commit in B..O must not earn credit: ${JSON.stringify(completed.result)}`);
|
|
2680
|
+
assert.ok(completed.result.diagnostics?.some(item => item.code === 'outline-commit-scope-invalid'),
|
|
2681
|
+
`the reverted range is reported as outline-commit-scope-invalid: ${JSON.stringify(completed.result)}`);
|
|
2682
|
+
} finally { reverted.cleanup(); }
|
|
2683
|
+
const clean = createOutlineFixture({ realHook: true });
|
|
2684
|
+
try {
|
|
2685
|
+
assert.strictEqual(clean.finishDraft().result.status, 'accepted', 'positive control: a target-only O range earns credit');
|
|
2686
|
+
} finally { clean.cleanup(); }
|
|
2687
|
+
});
|
|
2688
|
+
|
|
1771
2689
|
if (failures.length) {
|
|
1772
2690
|
console.error(`\n${failures.length} PR-B RED assertion(s) failed.`);
|
|
1773
2691
|
process.exitCode = 1;
|