@withtruing/companion 0.13.2 → 0.14.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/README.md +25 -26
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +837 -207
- package/dist/codex-binary.js +96 -0
- package/dist/codex-cli-caller.js +90 -16
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +95 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
package/dist/git-range.js
CHANGED
|
@@ -28,8 +28,10 @@ export function gitRangeEnvironment(parent = process.env) {
|
|
|
28
28
|
delete environment.GIT_CONFIG_NOSYSTEM;
|
|
29
29
|
return environment;
|
|
30
30
|
}
|
|
31
|
-
function runGit(repoPath, args, outputLimit, input, environment) {
|
|
31
|
+
function runGit(repoPath, args, outputLimit, input, environment, execution) {
|
|
32
32
|
return new Promise((resolve, reject) => {
|
|
33
|
+
if (execution?.signal.aborted)
|
|
34
|
+
return reject(new GitCommandFailure('resource'));
|
|
33
35
|
const child = spawn('git', ['--no-replace-objects', ...args], {
|
|
34
36
|
cwd: repoPath,
|
|
35
37
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -41,19 +43,35 @@ function runGit(repoPath, args, outputLimit, input, environment) {
|
|
|
41
43
|
let stdoutBytes = 0;
|
|
42
44
|
let stderrBytes = 0;
|
|
43
45
|
let settled = false;
|
|
46
|
+
let closeProcess;
|
|
47
|
+
const processClosed = new Promise((done) => { closeProcess = done; });
|
|
48
|
+
const unregister = execution?.registerReader({
|
|
49
|
+
cancel: async () => {
|
|
50
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
51
|
+
child.kill('SIGKILL');
|
|
52
|
+
await processClosed;
|
|
53
|
+
},
|
|
54
|
+
}) ?? (() => undefined);
|
|
55
|
+
const abort = () => {
|
|
56
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
57
|
+
child.kill('SIGKILL');
|
|
58
|
+
};
|
|
59
|
+
execution?.signal.addEventListener('abort', abort, { once: true });
|
|
44
60
|
const finish = (error, value) => {
|
|
45
61
|
if (settled)
|
|
46
62
|
return;
|
|
47
63
|
settled = true;
|
|
48
64
|
clearTimeout(timer);
|
|
65
|
+
execution?.signal.removeEventListener('abort', abort);
|
|
66
|
+
unregister();
|
|
49
67
|
if (error)
|
|
50
68
|
reject(error);
|
|
51
69
|
else
|
|
52
70
|
resolve(value);
|
|
53
71
|
};
|
|
54
72
|
const overflow = () => {
|
|
55
|
-
child.
|
|
56
|
-
|
|
73
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
74
|
+
child.kill('SIGKILL');
|
|
57
75
|
};
|
|
58
76
|
child.stdout?.on('data', (chunk) => {
|
|
59
77
|
stdoutBytes += chunk.length;
|
|
@@ -68,15 +86,19 @@ function runGit(repoPath, args, outputLimit, input, environment) {
|
|
|
68
86
|
if (stdoutBytes + stderrBytes > outputLimit)
|
|
69
87
|
overflow();
|
|
70
88
|
});
|
|
71
|
-
child.on('error', () =>
|
|
89
|
+
child.on('error', () => {
|
|
90
|
+
closeProcess();
|
|
91
|
+
finish(new GitCommandFailure('resource'));
|
|
92
|
+
});
|
|
72
93
|
child.on('close', (code, signal) => {
|
|
94
|
+
closeProcess();
|
|
73
95
|
if (signal || code === null)
|
|
74
96
|
return finish(new GitCommandFailure('resource'));
|
|
75
97
|
finish(undefined, { stdout: Buffer.concat(stdout), exitCode: code, stderr: Buffer.concat(stderr).toString('utf8') });
|
|
76
98
|
});
|
|
77
99
|
const timer = setTimeout(() => {
|
|
78
|
-
child.
|
|
79
|
-
|
|
100
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
101
|
+
child.kill('SIGKILL');
|
|
80
102
|
}, GIT_RANGE_COMMAND_TIMEOUT_MS);
|
|
81
103
|
if (input)
|
|
82
104
|
child.stdin?.end(input);
|
|
@@ -166,7 +188,7 @@ export function fitsGitRangeTransport(result) {
|
|
|
166
188
|
return Buffer.byteLength(JSON.stringify({ result, filesRead: [] }), 'utf8') <= GIT_RANGE_MAX_HTTP_BYTES;
|
|
167
189
|
}
|
|
168
190
|
const failure = (code) => ({ ok: false, code });
|
|
169
|
-
export async function runGitRangeTask(task, repoPath, connection) {
|
|
191
|
+
export async function runGitRangeTask(task, repoPath, connection, acquired) {
|
|
170
192
|
const payload = task.payload;
|
|
171
193
|
const branch = payload.branch;
|
|
172
194
|
const base = payload.base;
|
|
@@ -178,40 +200,54 @@ export async function runGitRangeTask(task, repoPath, connection) {
|
|
|
178
200
|
|| (payload.schemaProbe !== undefined && !validSchemaProbe(payload.schemaProbe))) {
|
|
179
201
|
return { result: failure('history_failed'), filesRead: [] };
|
|
180
202
|
}
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
if (acquired && (acquired.base !== base || !SHA.test(acquired.head))) {
|
|
204
|
+
return { result: failure('history_failed'), filesRead: [] };
|
|
205
|
+
}
|
|
206
|
+
const executeGit = (args, outputLimit, input, environment = acquired?.environment) => runGit(repoPath, args, outputLimit, input, environment, acquired);
|
|
207
|
+
const branchCheck = await executeGit(['check-ref-format', '--branch', branch], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
208
|
+
const remoteList = acquired ? null : await executeGit(['remote'], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
183
209
|
if (branchCheck.exitCode !== 0)
|
|
184
210
|
return { result: failure('branch_missing'), filesRead: [] };
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
let head;
|
|
212
|
+
let historyShallow;
|
|
213
|
+
if (acquired) {
|
|
214
|
+
head = acquired.head;
|
|
215
|
+
historyShallow = acquired.historyShallow;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const remotes = exactLine(Buffer.concat([remoteList.stdout,
|
|
219
|
+
remoteList.stdout.length === 0 ? Buffer.from('\n') : Buffer.alloc(0)]));
|
|
220
|
+
if (remoteList.exitCode !== 0 || !remotes.split('\n').includes(remote)) {
|
|
221
|
+
return { result: failure('branch_missing'), filesRead: [] };
|
|
222
|
+
}
|
|
223
|
+
destination = `refs/product-console/git-range/${task.id}/${randomUUID()}/head`;
|
|
224
|
+
const destinationCheck = await executeGit(['check-ref-format', destination], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
225
|
+
const sourceCheck = await executeGit(['check-ref-format', `refs/heads/${branch}`], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
226
|
+
if (destinationCheck.exitCode !== 0 || sourceCheck.exitCode !== 0)
|
|
227
|
+
return { result: failure('branch_missing'), filesRead: [] };
|
|
228
|
+
const fetchEnvironment = await managedFetchEnvironment(repoPath);
|
|
229
|
+
delete fetchEnvironment.GIT_CONFIG_NOSYSTEM;
|
|
230
|
+
const fetched = await executeGit([
|
|
231
|
+
'fetch', '--no-tags', '--refmap=', '--no-write-fetch-head', '--no-recurse-submodules', '--',
|
|
232
|
+
remote, `+refs/heads/${branch}:${destination}`,
|
|
233
|
+
], GIT_RANGE_METADATA_OUTPUT_BYTES, undefined, fetchEnvironment);
|
|
234
|
+
reportBackgroundFetch(repoPath, { ok: fetched.exitCode === 0, stderr: fetched.stderr });
|
|
235
|
+
if (fetched.exitCode !== 0)
|
|
236
|
+
return { result: failure('fetch_failed'), filesRead: [] };
|
|
237
|
+
const resolved = await executeGit(['rev-parse', '--verify', `${destination}^{commit}`], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
238
|
+
if (resolved.exitCode !== 0)
|
|
239
|
+
return { result: failure('history_failed'), filesRead: [] };
|
|
240
|
+
head = exactLine(resolved.stdout);
|
|
241
|
+
if (!SHA.test(head))
|
|
242
|
+
return { result: failure('history_failed'), filesRead: [] };
|
|
243
|
+
const shallowOutput = await executeGit(['rev-parse', '--is-shallow-repository'], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
244
|
+
if (shallowOutput.exitCode !== 0)
|
|
245
|
+
return { result: failure('history_failed'), filesRead: [] };
|
|
246
|
+
const shallowText = exactLine(shallowOutput.stdout);
|
|
247
|
+
if (shallowText !== 'true' && shallowText !== 'false')
|
|
248
|
+
return { result: failure('history_failed'), filesRead: [] };
|
|
249
|
+
historyShallow = shallowText === 'true';
|
|
250
|
+
}
|
|
215
251
|
if (base === null) {
|
|
216
252
|
const result = { ok: true, mode: 'head_only', head, base, commits: [], changedPaths: [], totalCommits: null, baseUnavailable: false, truncatedFiles: false, historyShallow };
|
|
217
253
|
return { result: fitsGitRangeTransport(result) ? result : failure('history_failed'), filesRead: [] };
|
|
@@ -220,7 +256,12 @@ export async function runGitRangeTask(task, repoPath, connection) {
|
|
|
220
256
|
const result = { ok: true, mode: 'range', head, base, commits: [], changedPaths: [], totalCommits: 0, baseUnavailable: false, truncatedFiles: false, historyShallow };
|
|
221
257
|
return { result: fitsGitRangeTransport(result) ? result : failure('history_failed'), filesRead: [] };
|
|
222
258
|
}
|
|
223
|
-
|
|
259
|
+
if (acquired?.baseUnavailableReason) {
|
|
260
|
+
return { result: { ok: true, mode: 'base_unavailable', head, base, commits: [], changedPaths: [],
|
|
261
|
+
totalCommits: null, baseUnavailable: true,
|
|
262
|
+
baseUnavailableReason: acquired.baseUnavailableReason, truncatedFiles: false, historyShallow }, filesRead: [] };
|
|
263
|
+
}
|
|
264
|
+
const baseCheck = await executeGit(['cat-file', '--batch-check'], GIT_RANGE_METADATA_OUTPUT_BYTES, Buffer.from(`${base}\n`));
|
|
224
265
|
if (baseCheck.exitCode !== 0)
|
|
225
266
|
return { result: failure('history_failed'), filesRead: [] };
|
|
226
267
|
const baseDescription = exactLine(baseCheck.stdout);
|
|
@@ -229,19 +270,19 @@ export async function runGitRangeTask(task, repoPath, connection) {
|
|
|
229
270
|
}
|
|
230
271
|
if (!new RegExp(`^${base} commit [0-9]+$`).test(baseDescription))
|
|
231
272
|
return { result: failure('history_failed'), filesRead: [] };
|
|
232
|
-
if (historyShallow) {
|
|
273
|
+
if (historyShallow && !acquired) {
|
|
233
274
|
return { result: { ok: true, mode: 'base_unavailable', head, base, commits: [], changedPaths: [], totalCommits: null, baseUnavailable: true, baseUnavailableReason: 'shallow_history', truncatedFiles: false, historyShallow }, filesRead: [] };
|
|
234
275
|
}
|
|
235
|
-
const ancestor = await
|
|
276
|
+
const ancestor = await executeGit(['merge-base', '--is-ancestor', base, head], GIT_RANGE_METADATA_OUTPUT_BYTES);
|
|
236
277
|
if (ancestor.exitCode === 1) {
|
|
237
278
|
return { result: { ok: true, mode: 'base_unavailable', head, base, commits: [], changedPaths: [], totalCommits: null, baseUnavailable: true, baseUnavailableReason: 'not_ancestor', truncatedFiles: false, historyShallow }, filesRead: [] };
|
|
238
279
|
}
|
|
239
280
|
if (ancestor.exitCode !== 0)
|
|
240
281
|
return { result: failure('history_failed'), filesRead: [] };
|
|
241
282
|
const [enumerated, counted, diff] = await Promise.all([
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
283
|
+
executeGit(['rev-list', '--topo-order', '--reverse', `${base}..${head}`, '--'], GIT_RANGE_ENUMERATION_OUTPUT_BYTES),
|
|
284
|
+
executeGit(['rev-list', '--count', `${base}..${head}`, '--'], GIT_RANGE_METADATA_OUTPUT_BYTES),
|
|
285
|
+
executeGit(['diff', '--no-color', '--name-status', '-z', '-M', base, head], GIT_RANGE_PATH_OUTPUT_BYTES),
|
|
245
286
|
]);
|
|
246
287
|
if (enumerated.exitCode !== 0 || counted.exitCode !== 0 || diff.exitCode !== 0)
|
|
247
288
|
return { result: failure('history_failed'), filesRead: [] };
|
|
@@ -259,7 +300,7 @@ export async function runGitRangeTask(task, repoPath, connection) {
|
|
|
259
300
|
return { result: failure('history_failed'), filesRead: [] };
|
|
260
301
|
}
|
|
261
302
|
const selected = oids.slice(0, GIT_RANGE_MAX_COMMITS);
|
|
262
|
-
const objects = await
|
|
303
|
+
const objects = await executeGit(['cat-file', '--batch'], GIT_RANGE_OBJECT_OUTPUT_BYTES, Buffer.from(`${selected.join('\n')}\n`));
|
|
263
304
|
if (objects.exitCode !== 0)
|
|
264
305
|
return { result: failure('history_failed'), filesRead: [] };
|
|
265
306
|
const commits = parseCommitObjects(objects.stdout, selected);
|
|
@@ -272,6 +313,7 @@ export async function runGitRangeTask(task, repoPath, connection) {
|
|
|
272
313
|
head,
|
|
273
314
|
changedPaths: paths.changedPaths,
|
|
274
315
|
probe: payload.schemaProbe,
|
|
316
|
+
environment: acquired?.environment,
|
|
275
317
|
});
|
|
276
318
|
const result = {
|
|
277
319
|
ok: true, mode: 'range', head, base, commits, changedPaths: paths.changedPaths,
|
package/dist/http-client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* fetch (Node 20+, no dependency needed) — see package.json's engines field. */
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { manifestHash } from './surface-mapper.js';
|
|
5
|
+
import { WHOLE_READ_COMPANION_CAPABILITIES } from './whole-read-claims.js';
|
|
5
6
|
const WHOAMI_TIMEOUT_MS = 15_000;
|
|
6
7
|
const CLAIM_TIMEOUT_MS = 10_000;
|
|
7
8
|
const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
@@ -9,6 +10,7 @@ const RESULT_TIMEOUT_MS = 60_000;
|
|
|
9
10
|
const ERROR_REPORT_TIMEOUT_MS = 30_000;
|
|
10
11
|
const PAIRING_TIMEOUT_MS = 15_000;
|
|
11
12
|
const READINESS_TIMEOUT_MS = 10_000;
|
|
13
|
+
const BUILD_RESULT_TIMEOUT_MS = 60_000;
|
|
12
14
|
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
13
15
|
if (typeof packageVersion !== 'string' || !packageVersion) {
|
|
14
16
|
throw new Error('companion/package.json does not contain a version');
|
|
@@ -23,9 +25,15 @@ export const COMPANION_CAPABILITIES = [
|
|
|
23
25
|
'schema-entities@1',
|
|
24
26
|
'verify-automatic-surface@1',
|
|
25
27
|
'map-module-expansion@1',
|
|
28
|
+
...WHOLE_READ_COMPANION_CAPABILITIES,
|
|
26
29
|
];
|
|
27
|
-
const
|
|
28
|
-
|
|
30
|
+
export const CLAUDE_CLI_FAST_MODEL = 'claude-haiku-5-5';
|
|
31
|
+
export const CODEX_FAST_MODEL = 'gpt-5.6-luna';
|
|
32
|
+
export const WHOLE_READ_FAST_MODEL_CAPABILITY = 'whole-read-fast-model@';
|
|
33
|
+
const capabilityHeaders = (fastModel) => ({
|
|
34
|
+
'X-Companion-Capabilities': [...COMPANION_CAPABILITIES,
|
|
35
|
+
...(fastModel && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(fastModel)
|
|
36
|
+
? [`${WHOLE_READ_FAST_MODEL_CAPABILITY}${fastModel}`] : [])].join(','),
|
|
29
37
|
});
|
|
30
38
|
export class CompanionHttpError extends Error {
|
|
31
39
|
status;
|
|
@@ -52,9 +60,14 @@ export class PairingHttpError extends Error {
|
|
|
52
60
|
export class CompanionClient {
|
|
53
61
|
url;
|
|
54
62
|
token;
|
|
55
|
-
|
|
63
|
+
fastModelsByConnection;
|
|
64
|
+
constructor(url, token, fastModelsByConnection = {}) {
|
|
56
65
|
this.url = url;
|
|
57
66
|
this.token = token;
|
|
67
|
+
this.fastModelsByConnection = fastModelsByConnection;
|
|
68
|
+
}
|
|
69
|
+
capabilityHeaders(connectionId) {
|
|
70
|
+
return capabilityHeaders(connectionId ? this.fastModelsByConnection[connectionId] : undefined);
|
|
58
71
|
}
|
|
59
72
|
request(path, timeoutMs, init) {
|
|
60
73
|
return fetch(`${this.url}${path}`, {
|
|
@@ -82,6 +95,68 @@ export class CompanionClient {
|
|
|
82
95
|
whoami() {
|
|
83
96
|
return this.call('/api/companion/whoami', WHOAMI_TIMEOUT_MS);
|
|
84
97
|
}
|
|
98
|
+
listBuildAssignments() {
|
|
99
|
+
return this.call('/api/companion/builds/assignments', CLAIM_TIMEOUT_MS, {
|
|
100
|
+
headers: this.capabilityHeaders(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
acceptBuildAssignment(assignment, report) {
|
|
104
|
+
return this.call(`/api/companion/builds/assignments/${encodeURIComponent(assignment.id)}/accept`, BUILD_RESULT_TIMEOUT_MS, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: this.capabilityHeaders(assignment.connectionId),
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
assignmentRevision: assignment.assignmentRevision,
|
|
109
|
+
canonicalRepository: assignment.canonicalRepository,
|
|
110
|
+
snapshotSha: assignment.snapshotSha,
|
|
111
|
+
...report,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
claimBuildUnit(assignment, input) {
|
|
116
|
+
return this.call(`/api/companion/builds/${encodeURIComponent(assignment.buildId)}/claim`, CLAIM_TIMEOUT_MS, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: this.capabilityHeaders(assignment.connectionId),
|
|
119
|
+
body: JSON.stringify({ assignmentId: assignment.id,
|
|
120
|
+
assignmentRevision: assignment.assignmentRevision, ...input }),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
buildAttemptPath(claim) {
|
|
124
|
+
return `/api/companion/builds/${encodeURIComponent(claim.buildId)}`
|
|
125
|
+
+ `/assignments/${encodeURIComponent(claim.assignmentId)}`
|
|
126
|
+
+ `/units/${encodeURIComponent(claim.unitId)}/attempts/${encodeURIComponent(claim.attemptId)}`;
|
|
127
|
+
}
|
|
128
|
+
putBuildResultPage(claim, page) {
|
|
129
|
+
return this.call(`${this.buildAttemptPath(claim)}/pages`, BUILD_RESULT_TIMEOUT_MS, {
|
|
130
|
+
method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({
|
|
131
|
+
claimToken: claim.claimToken, controlRevision: claim.controlRevision,
|
|
132
|
+
assignmentRevision: claim.assignmentRevision, page,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
sealBuildResult(claim, value) {
|
|
137
|
+
return this.call(`${this.buildAttemptPath(claim)}/seal`, BUILD_RESULT_TIMEOUT_MS, {
|
|
138
|
+
method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({
|
|
139
|
+
claimToken: claim.claimToken, controlRevision: claim.controlRevision,
|
|
140
|
+
assignmentRevision: claim.assignmentRevision, ...value,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
acceptBuildResult(claim, value) {
|
|
145
|
+
return this.call(`${this.buildAttemptPath(claim)}/result`, BUILD_RESULT_TIMEOUT_MS, {
|
|
146
|
+
method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({
|
|
147
|
+
claimToken: claim.claimToken, controlRevision: claim.controlRevision,
|
|
148
|
+
assignmentRevision: claim.assignmentRevision, ...value,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
reportBuildError(claim, failure, usage) {
|
|
153
|
+
return this.call(`${this.buildAttemptPath(claim)}/error`, ERROR_REPORT_TIMEOUT_MS, {
|
|
154
|
+
method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({
|
|
155
|
+
claimToken: claim.claimToken, controlRevision: claim.controlRevision,
|
|
156
|
+
assignmentRevision: claim.assignmentRevision, failure, ...(usage === undefined ? {} : { usage }),
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
85
160
|
async githubToken(repo) {
|
|
86
161
|
try {
|
|
87
162
|
const response = await this.call('/api/companion/github-token', CLAIM_TIMEOUT_MS, { method: 'POST', body: JSON.stringify({ repo }) });
|
|
@@ -101,7 +176,7 @@ export class CompanionClient {
|
|
|
101
176
|
claimNext(connectionId) {
|
|
102
177
|
return this.call(connectionId
|
|
103
178
|
? `/api/companion/tasks/next?connectionId=${encodeURIComponent(connectionId)}`
|
|
104
|
-
: '/api/companion/tasks/next', CLAIM_TIMEOUT_MS, { headers: capabilityHeaders() });
|
|
179
|
+
: '/api/companion/tasks/next', CLAIM_TIMEOUT_MS, { headers: this.capabilityHeaders(connectionId) });
|
|
105
180
|
}
|
|
106
181
|
async claimNextInvestigation() {
|
|
107
182
|
const response = await this.request('/api/companion/investigations/next', CLAIM_TIMEOUT_MS, {
|
|
@@ -182,14 +257,24 @@ export class CompanionClient {
|
|
|
182
257
|
}),
|
|
183
258
|
});
|
|
184
259
|
}
|
|
185
|
-
|
|
186
|
-
return this.call(`/api/companion/
|
|
260
|
+
heartbeatSurfaceMapping(job) {
|
|
261
|
+
return this.call(`/api/companion/surface-mappings/${encodeURIComponent(job.jobId)}/heartbeat`, HEARTBEAT_TIMEOUT_MS, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: {
|
|
264
|
+
'X-Companion-Version': COMPANION_VERSION,
|
|
265
|
+
...capabilityHeaders(),
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ attempt: job.attempt, claimToken: job.claimToken }),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
heartbeat(taskId, claimToken) {
|
|
271
|
+
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/heartbeat`, HEARTBEAT_TIMEOUT_MS, { method: 'POST', ...(claimToken ? { body: JSON.stringify({ claimToken }) } : {}) });
|
|
187
272
|
}
|
|
188
|
-
reportResult(taskId, result, filesRead, snapshot) {
|
|
273
|
+
reportResult(taskId, result, filesRead, snapshot, claimToken) {
|
|
189
274
|
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/result`, RESULT_TIMEOUT_MS, {
|
|
190
275
|
method: 'POST',
|
|
191
276
|
headers: capabilityHeaders(),
|
|
192
|
-
body: JSON.stringify({ result, filesRead, ...(snapshot ? { snapshot } : {}) }),
|
|
277
|
+
body: JSON.stringify({ result, filesRead, ...(snapshot ? { snapshot } : {}), ...(claimToken ? { claimToken } : {}) }),
|
|
193
278
|
});
|
|
194
279
|
}
|
|
195
280
|
reportGitRangeResult(taskId, result) {
|
|
@@ -199,8 +284,8 @@ export class CompanionClient {
|
|
|
199
284
|
}
|
|
200
285
|
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/result`, RESULT_TIMEOUT_MS, { method: 'POST', headers: capabilityHeaders(), body });
|
|
201
286
|
}
|
|
202
|
-
reportError(taskId, error) {
|
|
203
|
-
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/error`, ERROR_REPORT_TIMEOUT_MS, { method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({ error }) });
|
|
287
|
+
reportError(taskId, error, claimToken) {
|
|
288
|
+
return this.call(`/api/companion/tasks/${encodeURIComponent(taskId)}/error`, ERROR_REPORT_TIMEOUT_MS, { method: 'POST', headers: capabilityHeaders(), body: JSON.stringify({ error, ...(claimToken ? { claimToken } : {}) }) });
|
|
204
289
|
}
|
|
205
290
|
reportInvestigationResult(investigationId, result) {
|
|
206
291
|
return this.call(`/api/companion/investigations/${encodeURIComponent(investigationId)}/result`, RESULT_TIMEOUT_MS, { method: 'POST', headers: capabilityHeaders(), body: JSON.stringify(result) });
|
|
@@ -4,7 +4,7 @@ import { readFile, lstat, stat } from 'node:fs/promises';
|
|
|
4
4
|
import { resolve, sep } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { listRepoFiles } from './local-scan.js';
|
|
7
|
-
import { obtainRequestedSources } from './observed-sources.js';
|
|
7
|
+
import { obtainRequestedSources, } from './observed-sources.js';
|
|
8
8
|
import { scopeCovers, validateReaderResult } from './reader-result-contract.js';
|
|
9
9
|
import { gitErrorMessage } from './sandbox.js';
|
|
10
10
|
import { isUsageLimitError } from './usage-limit.js';
|
|
@@ -1152,7 +1152,7 @@ async function enumerateTrees(request, obtained) {
|
|
|
1152
1152
|
}
|
|
1153
1153
|
export async function runInvestigation(request, connections, callModel, reportDiagnostic = (diagnostic) => {
|
|
1154
1154
|
console.error(formatInvestigationReaderDiagnostic(diagnostic));
|
|
1155
|
-
}, execution) {
|
|
1155
|
+
}, execution, sourceAcquisition) {
|
|
1156
1156
|
if (request.policyVersion === 'policy-4' && !execution) {
|
|
1157
1157
|
throw new MissingPolicy4ExecutionError();
|
|
1158
1158
|
}
|
|
@@ -1160,7 +1160,7 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1160
1160
|
let observedSources = [];
|
|
1161
1161
|
let evidence = [];
|
|
1162
1162
|
try {
|
|
1163
|
-
obtained = await obtainRequestedSources(request, connections);
|
|
1163
|
+
obtained = await obtainRequestedSources(request, connections, sourceAcquisition);
|
|
1164
1164
|
observedSources = obtained.observedSources;
|
|
1165
1165
|
if (obtained.unavailable) {
|
|
1166
1166
|
return honestResult(request, observedSources, evidence, 'source_identity_unavailable', execution);
|
|
@@ -1173,17 +1173,20 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1173
1173
|
if (!sandbox)
|
|
1174
1174
|
return honestResult(request, observedSources, evidence, 'files_unreadable', execution);
|
|
1175
1175
|
const reads = await readSurfaceEvidence(request, sandbox);
|
|
1176
|
+
obtained.assertAvailable();
|
|
1176
1177
|
evidence = reads.map((read) => read.evidence);
|
|
1177
1178
|
const retrievalReason = surfaceRetrievalReason(evidence);
|
|
1178
1179
|
if (retrievalReason)
|
|
1179
1180
|
return honestResult(request, observedSources, evidence, retrievalReason, execution);
|
|
1181
|
+
const assessments = await assessCompleteSurface(request, observedSources, reads, callModel, reportDiagnostic, execution);
|
|
1182
|
+
obtained.assertAvailable();
|
|
1180
1183
|
const result = {
|
|
1181
1184
|
investigationId: request.investigationId,
|
|
1182
1185
|
attempt: request.attempt,
|
|
1183
1186
|
resultKey: resultKeyFor(request),
|
|
1184
1187
|
observedSources,
|
|
1185
1188
|
evidence,
|
|
1186
|
-
assessments: ensureAutomaticClaimShapes(request,
|
|
1189
|
+
assessments: ensureAutomaticClaimShapes(request, assessments),
|
|
1187
1190
|
execution: { ...execution },
|
|
1188
1191
|
};
|
|
1189
1192
|
try {
|
|
@@ -1214,6 +1217,7 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1214
1217
|
schema: selectionSchema,
|
|
1215
1218
|
});
|
|
1216
1219
|
selected = filterSelectedPaths(selection, trees);
|
|
1220
|
+
obtained.assertAvailable();
|
|
1217
1221
|
}
|
|
1218
1222
|
catch (error) {
|
|
1219
1223
|
const treeFileCount = trees.reduce((count, tree) => count + tree.paths.length, 0);
|
|
@@ -1223,6 +1227,7 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1223
1227
|
return honestResult(request, observedSources, evidence, 'no_plausible_path');
|
|
1224
1228
|
}
|
|
1225
1229
|
const reads = await readSelectedEvidence(request, selected, obtained.sandboxes);
|
|
1230
|
+
obtained.assertAvailable();
|
|
1226
1231
|
const budgeted = buildBudgetedAssessmentPrompt(request, reads);
|
|
1227
1232
|
evidence = budgeted.reads.map((read) => read.evidence);
|
|
1228
1233
|
if (budgeted.prompt === null) {
|
|
@@ -1257,6 +1262,7 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1257
1262
|
prompt: assessmentPrompt,
|
|
1258
1263
|
schema: assessmentResponseSchema,
|
|
1259
1264
|
});
|
|
1265
|
+
obtained.assertAvailable();
|
|
1260
1266
|
}
|
|
1261
1267
|
catch (error) {
|
|
1262
1268
|
return honestResult(request, observedSources, evidence, reasonForModelFailure(error, 'assessment', assessmentPrompt, budgeted.reads.length, reportDiagnostic));
|
|
@@ -1293,11 +1299,11 @@ export async function runInvestigation(request, connections, callModel, reportDi
|
|
|
1293
1299
|
await obtained?.cleanup();
|
|
1294
1300
|
}
|
|
1295
1301
|
}
|
|
1296
|
-
export async function runAndReportInvestigation(request, connections, callModel, report, reportDiagnostic, execution) {
|
|
1302
|
+
export async function runAndReportInvestigation(request, connections, callModel, report, reportDiagnostic, execution, sourceAcquisition) {
|
|
1297
1303
|
if (request.policyVersion === 'policy-4' && !execution) {
|
|
1298
1304
|
throw new MissingPolicy4ExecutionError();
|
|
1299
1305
|
}
|
|
1300
|
-
const result = await runInvestigation(request, connections, callModel, reportDiagnostic, execution);
|
|
1306
|
+
const result = await runInvestigation(request, connections, callModel, reportDiagnostic, execution, sourceAcquisition);
|
|
1301
1307
|
await report(result);
|
|
1302
1308
|
return result;
|
|
1303
1309
|
}
|
package/dist/managed-repo.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { lstat, realpath, rm } from 'node:fs/promises';
|
|
11
12
|
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
12
13
|
import { parseRemoteUrl } from './local-scan.js';
|
|
13
14
|
export class ManagedRepoError extends Error {
|
|
@@ -84,6 +85,20 @@ export function labelFromCopyPath(repoPath) {
|
|
|
84
85
|
const match = /^github\.com__(.+?)__(.+)$/.exec(basename(repoPath));
|
|
85
86
|
return match ? `${match[1]}/${match[2]}` : basename(repoPath);
|
|
86
87
|
}
|
|
88
|
+
/** Reads a managed repository's durable identity, with the old label/path convention retained
|
|
89
|
+
* only as a compatibility fallback for configs that have not entered the B38 migration yet. */
|
|
90
|
+
export function configuredManagedRepository(connection) {
|
|
91
|
+
if (connection.repository) {
|
|
92
|
+
if (connection.repository.host !== 'github.com' || connection.repository.port !== null) {
|
|
93
|
+
throw new ManagedRepoError(NOT_GITHUB_MESSAGE);
|
|
94
|
+
}
|
|
95
|
+
return parseGitHubRepo(connection.repository.path);
|
|
96
|
+
}
|
|
97
|
+
return parseGitHubRepo(connection.label ?? labelFromCopyPath(connection.repoPath));
|
|
98
|
+
}
|
|
99
|
+
export function usesTemporaryManagedStorage(connection) {
|
|
100
|
+
return connection.managed === true && connection.repositoryStorage === 'temporary';
|
|
101
|
+
}
|
|
87
102
|
const MAX_CAPTURED_STDERR = 16_384;
|
|
88
103
|
/** `stdio: 'inherit'` lets Git (and Git Credential Manager) talk to the person directly; stderr
|
|
89
104
|
* is still teed to the terminal and captured so a failure can be explained in one line. */
|
|
@@ -201,6 +216,10 @@ export function watchManagedCopy(repoPath, label, log, getToken) {
|
|
|
201
216
|
export function forgetManagedCopies() {
|
|
202
217
|
watchedCopies.clear();
|
|
203
218
|
}
|
|
219
|
+
/** Stops background reporting for one copy before its retirement starts. */
|
|
220
|
+
export function forgetManagedCopy(repoPath) {
|
|
221
|
+
watchedCopies.delete(watchKey(repoPath));
|
|
222
|
+
}
|
|
204
223
|
async function tokenFrom(getToken) {
|
|
205
224
|
if (!getToken)
|
|
206
225
|
return null;
|
|
@@ -389,6 +408,119 @@ export async function removeManagedCopy(repoPath, reposDir) {
|
|
|
389
408
|
rmSync(managedStatePath(target), { force: true });
|
|
390
409
|
return true;
|
|
391
410
|
}
|
|
411
|
+
export class ManagedCopyRetirementError extends Error {
|
|
412
|
+
code;
|
|
413
|
+
constructor(code) {
|
|
414
|
+
super(code);
|
|
415
|
+
this.code = code;
|
|
416
|
+
this.name = 'ManagedCopyRetirementError';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function pathMissing(path) {
|
|
420
|
+
try {
|
|
421
|
+
await lstat(path);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (error.code === 'ENOENT')
|
|
426
|
+
return true;
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function hasManagedCopyMarker(repoPath) {
|
|
431
|
+
try {
|
|
432
|
+
const marker = JSON.parse(readFileSync(managedStatePath(repoPath), 'utf8'));
|
|
433
|
+
return marker !== null
|
|
434
|
+
&& typeof marker === 'object'
|
|
435
|
+
&& !Array.isArray(marker)
|
|
436
|
+
&& typeof marker.signInNeeded === 'boolean'
|
|
437
|
+
&& typeof marker.lastUpdatedAt === 'string'
|
|
438
|
+
&& Number.isFinite(Date.parse(marker.lastUpdatedAt));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function normalizedPath(path) {
|
|
445
|
+
const absolute = resolve(path);
|
|
446
|
+
return process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
447
|
+
}
|
|
448
|
+
async function validateManagedCopyTargetPath(repoPath, reposDir, repo) {
|
|
449
|
+
const root = resolve(reposDir);
|
|
450
|
+
const target = resolve(repoPath);
|
|
451
|
+
if (normalizedPath(target) !== normalizedPath(managedCopyPath(root, repo))) {
|
|
452
|
+
throw new ManagedCopyRetirementError('cleanup_path_outside_root');
|
|
453
|
+
}
|
|
454
|
+
if (await pathMissing(root))
|
|
455
|
+
return 'missing';
|
|
456
|
+
const rootStats = await lstat(root);
|
|
457
|
+
if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
|
|
458
|
+
throw new ManagedCopyRetirementError('cleanup_unsafe_link');
|
|
459
|
+
}
|
|
460
|
+
if (await pathMissing(target))
|
|
461
|
+
return 'missing';
|
|
462
|
+
const targetStats = await lstat(target);
|
|
463
|
+
if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
|
|
464
|
+
throw new ManagedCopyRetirementError('cleanup_unsafe_link');
|
|
465
|
+
}
|
|
466
|
+
const [canonicalRoot, canonicalTarget] = await Promise.all([realpath(root), realpath(target)]);
|
|
467
|
+
if (normalizedPath(join(canonicalRoot, basename(target))) !== normalizedPath(canonicalTarget)) {
|
|
468
|
+
throw new ManagedCopyRetirementError('cleanup_path_outside_root');
|
|
469
|
+
}
|
|
470
|
+
return 'present';
|
|
471
|
+
}
|
|
472
|
+
/** Validates the old clone from independent companion records before deletion. The target must be
|
|
473
|
+
* the one deterministic child for this identity, have the state marker written on clone, and
|
|
474
|
+
* still be the top of the expected Git repository. */
|
|
475
|
+
async function validateManagedCopyRetirement(repoPath, reposDir, repo) {
|
|
476
|
+
const target = resolve(repoPath);
|
|
477
|
+
const presence = await validateManagedCopyTargetPath(repoPath, reposDir, repo);
|
|
478
|
+
if (presence === 'missing')
|
|
479
|
+
return presence;
|
|
480
|
+
if (!hasManagedCopyMarker(target)) {
|
|
481
|
+
throw new ManagedCopyRetirementError('cleanup_marker_missing');
|
|
482
|
+
}
|
|
483
|
+
if (!(await isCopyOf(target, repo))) {
|
|
484
|
+
throw new ManagedCopyRetirementError('cleanup_identity_mismatch');
|
|
485
|
+
}
|
|
486
|
+
return 'present';
|
|
487
|
+
}
|
|
488
|
+
/** Retires one validated historical managed clone. `onValidated` durably records cleanup_pending
|
|
489
|
+
* before the first delete. Validation is repeated afterwards to close the write/delete race. */
|
|
490
|
+
export async function retireManagedCopy(options, dependencies = {}) {
|
|
491
|
+
const wait = dependencies.wait
|
|
492
|
+
?? ((delayMs) => new Promise((resolveWait) => setTimeout(resolveWait, delayMs)));
|
|
493
|
+
const retryDelaysMs = dependencies.retryDelaysMs ?? [25, 50, 100, 200];
|
|
494
|
+
const removeTree = dependencies.removeTree ?? ((path) => rm(path, { recursive: true, force: false }));
|
|
495
|
+
const initial = await validateManagedCopyRetirement(options.repoPath, options.reposDir, options.repo);
|
|
496
|
+
if (initial === 'missing') {
|
|
497
|
+
rmSync(managedStatePath(options.repoPath), { force: true });
|
|
498
|
+
return { cleanupAttempts: 0 };
|
|
499
|
+
}
|
|
500
|
+
await options.onValidated();
|
|
501
|
+
await validateManagedCopyRetirement(options.repoPath, options.reposDir, options.repo);
|
|
502
|
+
forgetManagedCopy(options.repoPath);
|
|
503
|
+
await runGit(options.repoPath, ['worktree', 'prune']);
|
|
504
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
505
|
+
try {
|
|
506
|
+
const presence = await validateManagedCopyTargetPath(options.repoPath, options.reposDir, options.repo);
|
|
507
|
+
if (presence === 'present')
|
|
508
|
+
await removeTree(options.repoPath);
|
|
509
|
+
if (!(await pathMissing(options.repoPath)))
|
|
510
|
+
throw new Error('cleanup_delete_failed');
|
|
511
|
+
rmSync(managedStatePath(options.repoPath), { force: true });
|
|
512
|
+
return { cleanupAttempts: attempt };
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
if (error instanceof ManagedCopyRetirementError)
|
|
516
|
+
throw error;
|
|
517
|
+
if (attempt < 5) {
|
|
518
|
+
await wait(retryDelaysMs[Math.min(attempt - 1, retryDelaysMs.length - 1)] ?? 0);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw new ManagedCopyRetirementError('cleanup_delete_failed');
|
|
523
|
+
}
|
|
392
524
|
export function relativeTime(iso, now = new Date()) {
|
|
393
525
|
if (!iso)
|
|
394
526
|
return 'not yet';
|