@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/README.md
CHANGED
|
@@ -175,8 +175,8 @@ your Claude subscription, not a separate API key.
|
|
|
175
175
|
## No code on your computer?
|
|
176
176
|
|
|
177
177
|
If you can open the repo on GitHub but don't keep the code on this computer, the companion can
|
|
178
|
-
|
|
179
|
-
`--backend cli`, Claude Code. Then run one command from any folder:
|
|
178
|
+
access it temporarily when work needs it. You need Git installed (https://git-scm.com/downloads)
|
|
179
|
+
and, for `--backend cli`, Claude Code. Then run one command from any folder:
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
182
|
truing connect \
|
|
@@ -190,24 +190,26 @@ truing connect \
|
|
|
190
190
|
`--branch <name>` to follow a branch other than the repo's main one.
|
|
191
191
|
|
|
192
192
|
- **GitHub access.** If the repo was connected with **Connect GitHub** in the Console, the
|
|
193
|
-
companion
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
- **If GitHub stops
|
|
200
|
-
needed". Fix it with:
|
|
193
|
+
companion asks for a fresh access pass for each operation and never saves it on your computer.
|
|
194
|
+
The connection is validated before it is saved.
|
|
195
|
+
- **No permanent code copy.** When the Console enables temporary repository access, each task gets
|
|
196
|
+
an isolated copy that is removed when the task ends. `status` shows the repository identity,
|
|
197
|
+
not a folder. A Console that has not enabled the rollout continues using the earlier managed-copy
|
|
198
|
+
behavior until it is upgraded.
|
|
199
|
+
- **If GitHub access stops working**, reconnect GitHub in the Console, then check it with:
|
|
201
200
|
|
|
202
201
|
```bash
|
|
203
202
|
truing signin
|
|
204
203
|
```
|
|
205
204
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
- **Disconnecting
|
|
210
|
-
|
|
205
|
+
For a temporary connection this performs a bounded access check and stores no source. Add
|
|
206
|
+
`--repo owner/name` to check just one repo. Legacy managed copies retain their existing sign-in
|
|
207
|
+
flow while the rollout is off.
|
|
208
|
+
- **Disconnecting removes the pairing.** Temporary connections have no code folder to remove.
|
|
209
|
+
When the rollout is enabled, an older companion-managed copy is removed after its repository
|
|
210
|
+
identity and ownership records are verified. Developer folders, foreign folders, linked paths,
|
|
211
|
+
and copies still referenced by a legacy pairing are never removed. Locked copies stay marked
|
|
212
|
+
`cleanup_pending` and are retried when the companion restarts.
|
|
211
213
|
|
|
212
214
|
## ChatGPT subscription through Codex CLI
|
|
213
215
|
|
|
@@ -215,7 +217,7 @@ truing connect \
|
|
|
215
217
|
the companion's own Codex home. This is separate from your normal Codex login:
|
|
216
218
|
|
|
217
219
|
```powershell
|
|
218
|
-
New-Item -ItemType Directory -Force "$HOME\.product-console-companion\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\.product-console-companion\codex-home"; codex login; Remove-Item Env:CODEX_HOME
|
|
220
|
+
New-Item -ItemType Directory -Force "$HOME\.product-console-companion\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\.product-console-companion\codex-home"; codex.cmd login; Remove-Item Env:CODEX_HOME
|
|
219
221
|
```
|
|
220
222
|
|
|
221
223
|
```bash
|
|
@@ -321,13 +323,10 @@ The package's `publishConfig` makes this a public scoped-package publish. Then,
|
|
|
321
323
|
GitHub Actions** for this repo (`fynchconsulting/product-console`) and the exact workflow filename
|
|
322
324
|
`publish-companion.yml`, with no environment. No npm token is generated, pasted, or stored.
|
|
323
325
|
|
|
324
|
-
After that, releases
|
|
325
|
-
`package.json`
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
or trigger the workflow manually (Actions tab, or `gh workflow run publish-companion.yml`). Either
|
|
332
|
-
way it publishes whatever's currently in `package.json` at that point — there's no separate
|
|
333
|
-
"which version" input to get wrong.
|
|
326
|
+
After that, releases are automatic (TRUING.md §9): any PR that changes `companion/` bumps the
|
|
327
|
+
version in `companion/package.json` (`npm version patch --no-git-tag-version` in `companion/`, or
|
|
328
|
+
`minor`), and CI fails the PR if it doesn't. Merging it to main runs
|
|
329
|
+
`.github/workflows/publish-companion.yml`, which publishes that version to npm and tags the commit
|
|
330
|
+
`companion-v<version>`. If the version is already on npm the publish is skipped. The workflow can
|
|
331
|
+
also be run by hand (Actions tab, or `gh workflow run publish-companion.yml`) to recover from a
|
|
332
|
+
failed run; it publishes whatever is in `package.json` at that point.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const REJECTED_ANTHROPIC_KEYWORDS = new Set([
|
|
2
|
+
'minimum',
|
|
3
|
+
'maximum',
|
|
4
|
+
'exclusiveMinimum',
|
|
5
|
+
'exclusiveMaximum',
|
|
6
|
+
'multipleOf',
|
|
7
|
+
'minLength',
|
|
8
|
+
'maxLength',
|
|
9
|
+
'minItems',
|
|
10
|
+
'maxItems',
|
|
11
|
+
'pattern',
|
|
12
|
+
'format',
|
|
13
|
+
]);
|
|
14
|
+
const SCHEMA_MAP_KEYWORDS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']);
|
|
15
|
+
function converted(value, preserveKeys = false) {
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map((item) => converted(item));
|
|
18
|
+
if (value === null || typeof value !== 'object')
|
|
19
|
+
return value;
|
|
20
|
+
const convertedNode = Object.fromEntries(Object.entries(value)
|
|
21
|
+
.filter(([key]) => preserveKeys || !REJECTED_ANTHROPIC_KEYWORDS.has(key))
|
|
22
|
+
.map(([key, child]) => [key, converted(child, SCHEMA_MAP_KEYWORDS.has(key))]));
|
|
23
|
+
const type = value.type;
|
|
24
|
+
const enumValues = value.enum;
|
|
25
|
+
if (Array.isArray(type) && type.length === 2 && Array.isArray(enumValues) && enumValues.includes(null)) {
|
|
26
|
+
const nonNullType = type.find((item) => item !== 'null');
|
|
27
|
+
if (typeof nonNullType === 'string' && type.includes('null')) {
|
|
28
|
+
const annotations = Object.fromEntries(Object.entries(convertedNode)
|
|
29
|
+
.filter(([key]) => key !== 'type' && key !== 'enum'));
|
|
30
|
+
const nonNullEnum = enumValues.filter((item) => item !== null).map((item) => converted(item));
|
|
31
|
+
return nonNullEnum.length === 0
|
|
32
|
+
? { ...annotations, type: 'null' }
|
|
33
|
+
: { ...annotations, anyOf: [{ type: nonNullType, enum: nonNullEnum }, { type: 'null' }] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return convertedNode;
|
|
37
|
+
}
|
|
38
|
+
/** Keep this provider-boundary copy in sync with server/anthropic-output-schema.ts. */
|
|
39
|
+
export function toAnthropicOutputSchema(schema) {
|
|
40
|
+
return converted(schema);
|
|
41
|
+
}
|
package/dist/api-key-caller.js
CHANGED
|
@@ -11,18 +11,25 @@
|
|
|
11
11
|
* two in sync by hand if either changes.
|
|
12
12
|
*/
|
|
13
13
|
import Anthropic from '@anthropic-ai/sdk';
|
|
14
|
+
import { toAnthropicOutputSchema } from './anthropic-output-schema.js';
|
|
14
15
|
import { readFile } from 'node:fs/promises';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
17
|
import { buildLocalDesignContext } from './design-context.js';
|
|
17
18
|
import { buildLocalRepoContext, listRepoFiles } from './local-scan.js';
|
|
18
19
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
19
|
-
import { replyText, streamWithFallback } from './text-model.js';
|
|
20
|
+
import { anthropicTextModel, replyText, streamWithFallback, } from './text-model.js';
|
|
20
21
|
const MAX_CANDIDATE_FILES = 8;
|
|
21
22
|
/** A skip threshold, not a truncation — a file over this is left OUT of what's read entirely
|
|
22
23
|
* (with a note in the log), rather than read and cut off partway through. The whole point of
|
|
23
24
|
* this backend is that what IS read is never truncated; this only guards against an
|
|
24
25
|
* accidentally-selected generated/binary/data file blowing up the prompt. */
|
|
25
26
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
27
|
+
/** Dedicated whole-read entry point; the optional safe-mode contract cannot be omitted here. */
|
|
28
|
+
export async function runWholeReadModelCall(client, request) {
|
|
29
|
+
if (!request.wholeRead)
|
|
30
|
+
throw new TypeError('whole_read_context_required');
|
|
31
|
+
return anthropicTextModel(client)(request);
|
|
32
|
+
}
|
|
26
33
|
/** Structured model-only call used by the investigation reader. The reader, not this call and
|
|
27
34
|
* not the model, owns every repository read and evidence id. */
|
|
28
35
|
export async function runStructuredModelCall(client, request) {
|
|
@@ -32,7 +39,7 @@ export async function runStructuredModelCall(client, request) {
|
|
|
32
39
|
thinking: { type: 'adaptive' },
|
|
33
40
|
output_config: {
|
|
34
41
|
effort: request.effort,
|
|
35
|
-
format: { type: 'json_schema', schema: request.schema },
|
|
42
|
+
format: { type: 'json_schema', schema: toAnthropicOutputSchema(request.schema) },
|
|
36
43
|
},
|
|
37
44
|
system: request.system,
|
|
38
45
|
messages: [{ role: 'user', content: request.prompt }],
|
|
@@ -20,7 +20,8 @@ import { assertSingleLineArguments } from './cli-arguments.js';
|
|
|
20
20
|
import { spawnClaude } from './claude-binary.js';
|
|
21
21
|
import { gitErrorMessage } from './sandbox.js';
|
|
22
22
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
23
|
-
import { ModelRefusalError } from './text-model.js';
|
|
23
|
+
import { ModelRefusalError, WholeReadTransportError, } from './text-model.js';
|
|
24
|
+
import { isUsageLimitError, parseUsageLimitReset } from './usage-limit.js';
|
|
24
25
|
/** Independent of --max-budget-usd — a dollar cap doesn't prevent a network-stall hang. No
|
|
25
26
|
* automatic retry on top of this (see cli.ts): auto-retry would risk double-spending the
|
|
26
27
|
* customer's own subscription/API quota. */
|
|
@@ -69,6 +70,51 @@ function promptWriteFailure(error) {
|
|
|
69
70
|
wrapped.syscall = error.syscall;
|
|
70
71
|
return wrapped;
|
|
71
72
|
}
|
|
73
|
+
function numericUsage(value) {
|
|
74
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
75
|
+
return null;
|
|
76
|
+
const result = {};
|
|
77
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
78
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate))
|
|
79
|
+
result[key] = candidate;
|
|
80
|
+
}
|
|
81
|
+
return Object.keys(result).length ? result : null;
|
|
82
|
+
}
|
|
83
|
+
function claudeMetadata(envelope, fallbackStopReason) {
|
|
84
|
+
const reportedModels = envelope.modelUsage ? Object.keys(envelope.modelUsage) : [];
|
|
85
|
+
const model = typeof envelope.model === 'string'
|
|
86
|
+
? envelope.model
|
|
87
|
+
: reportedModels.length === 1 ? reportedModels[0] : null;
|
|
88
|
+
const usage = numericUsage(envelope.usage)
|
|
89
|
+
?? (model && envelope.modelUsage ? numericUsage(envelope.modelUsage[model]) : null);
|
|
90
|
+
return { usage, metadata: {
|
|
91
|
+
finishReason: envelope.stop_reason ?? fallbackStopReason,
|
|
92
|
+
model,
|
|
93
|
+
usageAvailable: usage !== null,
|
|
94
|
+
} };
|
|
95
|
+
}
|
|
96
|
+
function safeClaudeFailure(envelope) {
|
|
97
|
+
const raw = `${envelope.subtype ?? ''} ${envelope.terminal_reason ?? ''} ${envelope.errors?.join(' ') ?? ''} ${envelope.result}`;
|
|
98
|
+
if (isUsageLimitError(raw) || envelope.terminal_reason === 'budget_exhausted'
|
|
99
|
+
|| envelope.subtype === 'error_max_budget_usd') {
|
|
100
|
+
const reset = parseUsageLimitReset(raw);
|
|
101
|
+
return new WholeReadTransportError('usage_limit', 'claude_cli', reset?.toISOString());
|
|
102
|
+
}
|
|
103
|
+
if (/billing|credit|payment required/i.test(raw)) {
|
|
104
|
+
return new WholeReadTransportError('billing_exhausted', 'claude_cli');
|
|
105
|
+
}
|
|
106
|
+
if (/rate_limit_error|http 429|too many requests|overloaded/i.test(raw)) {
|
|
107
|
+
return new WholeReadTransportError('provider_rate_limit', 'claude_cli');
|
|
108
|
+
}
|
|
109
|
+
return new WholeReadTransportError('capability_failure', 'claude_cli');
|
|
110
|
+
}
|
|
111
|
+
async function terminateAndWait(child) {
|
|
112
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
113
|
+
return;
|
|
114
|
+
const closed = new Promise((resolve) => child.once('close', () => resolve()));
|
|
115
|
+
child.kill();
|
|
116
|
+
await closed;
|
|
117
|
+
}
|
|
72
118
|
/** The model sometimes emits an absolute path matching the sandbox root, sometimes a
|
|
73
119
|
* POSIX-flavoured path as if --add-dir were mounted at "/" — normalize both down to a
|
|
74
120
|
* repo-relative path, matching api-key-caller.ts's own filesRead convention. */
|
|
@@ -81,7 +127,7 @@ function normalizeToRepoRelative(filePath, root) {
|
|
|
81
127
|
* sandbox as `repoPath`; connection-less calls grant no tools and need no sandbox.
|
|
82
128
|
* Returns whatever structured_output the model produced plus every file it actually read via
|
|
83
129
|
* the Read tool. Every exported per-kind function below is a thin wrapper over this. */
|
|
84
|
-
async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
130
|
+
async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false, wholeRead) {
|
|
85
131
|
if (spec.stableDocumentBase64) {
|
|
86
132
|
throw new Error("This task includes a PDF PRD, which the cli backend can't read yet. Pair with " +
|
|
87
133
|
'--backend api-key instead, or re-upload the PRD as .md/.txt/.docx.');
|
|
@@ -89,7 +135,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
89
135
|
const sandboxPath = connectionless ? tmpdir() : repoPath;
|
|
90
136
|
let systemPromptDir = null;
|
|
91
137
|
try {
|
|
92
|
-
systemPromptDir = await mkdtemp(join(tmpdir(), 'product-console-claude-system-'));
|
|
138
|
+
systemPromptDir = await mkdtemp(join(wholeRead?.attemptDirectory ?? tmpdir(), 'product-console-claude-system-'));
|
|
93
139
|
const systemPromptPath = join(systemPromptDir, 'prompt.txt');
|
|
94
140
|
await writeFile(systemPromptPath, spec.system, { encoding: 'utf8', mode: 0o600 });
|
|
95
141
|
const emptyMcpConfigPath = await getEmptyMcpConfigPath();
|
|
@@ -140,7 +186,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
140
186
|
// default here, so both are always real streams in practice; the ! just satisfies the type.
|
|
141
187
|
const stdoutDecoder = new StringDecoder('utf8');
|
|
142
188
|
const recordStdout = (text) => {
|
|
143
|
-
if (!text)
|
|
189
|
+
if (!text || wholeRead)
|
|
144
190
|
return;
|
|
145
191
|
if (stdoutFirst.length < STDOUT_SAMPLE_CHARS) {
|
|
146
192
|
stdoutFirst += text.slice(0, STDOUT_SAMPLE_CHARS - stdoutFirst.length);
|
|
@@ -157,7 +203,8 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
157
203
|
child.kill();
|
|
158
204
|
});
|
|
159
205
|
child.stderr.on('data', (chunk) => {
|
|
160
|
-
|
|
206
|
+
if (!wholeRead)
|
|
207
|
+
stderrTail = (stderrTail + chunk.toString()).slice(-2000);
|
|
161
208
|
});
|
|
162
209
|
const rl = createInterface({ input: child.stdout });
|
|
163
210
|
const timer = setTimeout(() => {
|
|
@@ -172,7 +219,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
172
219
|
}
|
|
173
220
|
catch {
|
|
174
221
|
unparseableLines++;
|
|
175
|
-
if (firstUnparseableLineLength === null) {
|
|
222
|
+
if (!wholeRead && firstUnparseableLineLength === null) {
|
|
176
223
|
firstUnparseableLineLength = line.length;
|
|
177
224
|
firstUnparseableLineFirst = line.slice(0, UNPARSEABLE_LINE_SAMPLE_CHARS);
|
|
178
225
|
firstUnparseableLineLast = line.slice(-UNPARSEABLE_LINE_SAMPLE_CHARS);
|
|
@@ -204,6 +251,12 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
204
251
|
child.on('error', reject); // e.g. ENOENT — claude isn't installed / not on PATH
|
|
205
252
|
child.on('close', (code) => resolvePromise(code ?? 1));
|
|
206
253
|
});
|
|
254
|
+
const unregisterReader = wholeRead?.registerReader({ cancel: () => terminateAndWait(child) });
|
|
255
|
+
const abort = () => { void terminateAndWait(child); };
|
|
256
|
+
if (wholeRead?.signal.aborted)
|
|
257
|
+
abort();
|
|
258
|
+
else
|
|
259
|
+
wholeRead?.signal.addEventListener('abort', abort, { once: true });
|
|
207
260
|
try {
|
|
208
261
|
if (spec.stableText)
|
|
209
262
|
child.stdin.write(spec.stableText + '\n\n');
|
|
@@ -214,13 +267,24 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
214
267
|
stdinError ??= error;
|
|
215
268
|
child.kill();
|
|
216
269
|
}
|
|
217
|
-
|
|
218
|
-
|
|
270
|
+
let exitCode;
|
|
271
|
+
try {
|
|
272
|
+
exitCode = await exitPromise;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
wholeRead?.signal.removeEventListener('abort', abort);
|
|
277
|
+
unregisterReader?.();
|
|
278
|
+
}
|
|
279
|
+
if (wholeRead?.signal.aborted)
|
|
280
|
+
throw new WholeReadTransportError('cancelled', 'claude_cli');
|
|
219
281
|
if (!resultEnvelope && timedOut) {
|
|
220
282
|
throw new Error(`claude CLI took longer than ${WALL_CLOCK_TIMEOUT_MS / 60_000} minutes and was killed.`);
|
|
221
283
|
}
|
|
222
284
|
const capturedStdinError = stdinError;
|
|
223
285
|
if (!resultEnvelope) {
|
|
286
|
+
if (wholeRead)
|
|
287
|
+
throw new WholeReadTransportError('capability_failure', 'claude_cli');
|
|
224
288
|
if (capturedStdinError) {
|
|
225
289
|
throw promptWriteFailure(capturedStdinError);
|
|
226
290
|
}
|
|
@@ -244,6 +308,25 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
244
308
|
`types: ${types}; raw stdout: ${stdoutSample}${unparseableSample}`);
|
|
245
309
|
}
|
|
246
310
|
const envelope = resultEnvelope;
|
|
311
|
+
if (wholeRead) {
|
|
312
|
+
if (envelope.is_error || envelope.terminal_reason === 'budget_exhausted'
|
|
313
|
+
|| envelope.subtype === 'error_max_budget_usd')
|
|
314
|
+
throw safeClaudeFailure(envelope);
|
|
315
|
+
const metadata = claudeMetadata(envelope, lastStopReason);
|
|
316
|
+
if (metadata.metadata.finishReason === 'max_tokens') {
|
|
317
|
+
throw new WholeReadTransportError('output_truncated', 'claude_cli');
|
|
318
|
+
}
|
|
319
|
+
if (metadata.metadata.finishReason === 'refusal') {
|
|
320
|
+
throw new WholeReadTransportError('model_declined', 'claude_cli');
|
|
321
|
+
}
|
|
322
|
+
if (capturedStdinError)
|
|
323
|
+
throw new WholeReadTransportError('capability_failure', 'claude_cli');
|
|
324
|
+
const text = envelope.structured_output === undefined
|
|
325
|
+
? envelope.result
|
|
326
|
+
: JSON.stringify(envelope.structured_output);
|
|
327
|
+
return { result: envelope.structured_output ?? envelope.result, filesRead: [],
|
|
328
|
+
reply: { text, usage: metadata.usage, metadata: metadata.metadata } };
|
|
329
|
+
}
|
|
247
330
|
if (envelope.terminal_reason === 'budget_exhausted' || envelope.subtype === 'error_max_budget_usd') {
|
|
248
331
|
throw new Error(`Exceeded the $${maxBudgetUsd.toFixed(2)} budget for this task (spent $${envelope.total_cost_usd.toFixed(4)} before stopping).`);
|
|
249
332
|
}
|
|
@@ -271,6 +354,22 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
271
354
|
await rm(systemPromptDir, { recursive: true, force: true });
|
|
272
355
|
}
|
|
273
356
|
}
|
|
357
|
+
/** Whole-read model-only entry point: no tools, no raw child diagnostics, attempt-owned files. */
|
|
358
|
+
export async function runWholeReadModelCallViaCli(request) {
|
|
359
|
+
if (!request.wholeRead)
|
|
360
|
+
throw new TypeError('whole_read_context_required');
|
|
361
|
+
const outcome = await runViaCli({
|
|
362
|
+
kind: 'whole-read',
|
|
363
|
+
model: request.model,
|
|
364
|
+
effort: request.effort,
|
|
365
|
+
maxTokens: request.maxTokens,
|
|
366
|
+
system: request.system,
|
|
367
|
+
stableText: request.stableText,
|
|
368
|
+
volatileText: request.text,
|
|
369
|
+
schema: request.schema,
|
|
370
|
+
}, INVESTIGATION_CALL_BUDGET_USD, request.wholeRead.attemptDirectory, true, request.wholeRead);
|
|
371
|
+
return outcome.reply;
|
|
372
|
+
}
|
|
274
373
|
/** The CLI is only a structured-output model transport for investigations. It gets no tools and
|
|
275
374
|
* no repository directory; all evidence reads happen in investigation-reader.ts first. */
|
|
276
375
|
export async function runStructuredModelCallViaCli(request) {
|