product-console-companion 0.7.0 → 0.7.2
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/dist/api-key-caller.js
CHANGED
|
@@ -16,6 +16,7 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { buildLocalDesignContext } from './design-context.js';
|
|
17
17
|
import { buildLocalRepoContext, listRepoFiles } from './local-scan.js';
|
|
18
18
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
19
|
+
import { replyText, streamWithFallback } from './text-model.js';
|
|
19
20
|
const MAX_CANDIDATE_FILES = 8;
|
|
20
21
|
/** A skip threshold, not a truncation — a file over this is left OUT of what's read entirely
|
|
21
22
|
* (with a note in the log), rather than read and cut off partway through. The whole point of
|
|
@@ -25,7 +26,7 @@ const MAX_FILE_BYTES = 512 * 1024;
|
|
|
25
26
|
/** Structured model-only call used by the investigation reader. The reader, not this call and
|
|
26
27
|
* not the model, owns every repository read and evidence id. */
|
|
27
28
|
export async function runStructuredModelCall(client, request) {
|
|
28
|
-
const stream = client
|
|
29
|
+
const stream = streamWithFallback(client, {
|
|
29
30
|
model: request.model,
|
|
30
31
|
max_tokens: request.maxTokens,
|
|
31
32
|
thinking: { type: 'adaptive' },
|
|
@@ -46,14 +47,12 @@ export async function runStructuredModelCall(client, request) {
|
|
|
46
47
|
}
|
|
47
48
|
throw error;
|
|
48
49
|
}
|
|
49
|
-
if (message.stop_reason === 'refusal')
|
|
50
|
-
throw new Error('The model declined the structured investigation call.');
|
|
51
50
|
if (message.stop_reason === 'max_tokens')
|
|
52
51
|
throw new Error('budget_exhausted: the structured investigation call reached its output-token limit.');
|
|
53
|
-
const text = message
|
|
54
|
-
if (!text
|
|
52
|
+
const text = replyText(message, 'The model declined the structured investigation call.');
|
|
53
|
+
if (!text)
|
|
55
54
|
throw new Error('The structured investigation call returned no text.');
|
|
56
|
-
return JSON.parse(text
|
|
55
|
+
return JSON.parse(text);
|
|
57
56
|
}
|
|
58
57
|
export const fileSelectSchema = {
|
|
59
58
|
type: 'object',
|
|
@@ -19,6 +19,7 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
19
19
|
import { assertSingleLineArguments } from './cli-arguments.js';
|
|
20
20
|
import { gitErrorMessage } from './sandbox.js';
|
|
21
21
|
import { appendDiffToVolatile, prepareShipmentAnalysis } from './shipment.js';
|
|
22
|
+
import { ModelRefusalError } from './text-model.js';
|
|
22
23
|
/** Independent of --max-budget-usd — a dollar cap doesn't prevent a network-stall hang. No
|
|
23
24
|
* automatic retry on top of this (see cli.ts): auto-retry would risk double-spending the
|
|
24
25
|
* customer's own subscription/API quota. */
|
|
@@ -85,6 +86,7 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
85
86
|
assertSingleLineArguments('claude', args);
|
|
86
87
|
const filesRead = new Set();
|
|
87
88
|
let resultEnvelope = null;
|
|
89
|
+
let lastStopReason = null;
|
|
88
90
|
let stderrTail = '';
|
|
89
91
|
let timedOut = false;
|
|
90
92
|
let stdinError = null;
|
|
@@ -152,6 +154,9 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
152
154
|
resultEnvelope = event;
|
|
153
155
|
}
|
|
154
156
|
else if (eventType === 'assistant') {
|
|
157
|
+
const stopReason = event.message?.stop_reason;
|
|
158
|
+
if (typeof stopReason === 'string')
|
|
159
|
+
lastStopReason = stopReason;
|
|
155
160
|
const blocks = event.message?.content ?? [];
|
|
156
161
|
for (const b of blocks) {
|
|
157
162
|
if (b.type === 'tool_use' && b.name === 'Read' && b.input?.file_path) {
|
|
@@ -211,6 +216,11 @@ async function runViaCli(spec, maxBudgetUsd, repoPath, connectionless = false) {
|
|
|
211
216
|
const message = envelope.errors?.length ? envelope.errors.join('; ') : envelope.result;
|
|
212
217
|
throw new Error(message || 'Unknown claude CLI error');
|
|
213
218
|
}
|
|
219
|
+
// A refused turn can still end in a non-error result envelope; its result is empty or partial
|
|
220
|
+
// text, so it must fail here rather than be returned (or JSON-parsed) as an answer.
|
|
221
|
+
if (envelope.stop_reason === 'refusal' || lastStopReason === 'refusal') {
|
|
222
|
+
throw new ModelRefusalError('The model declined this request.');
|
|
223
|
+
}
|
|
214
224
|
if (capturedStdinError) {
|
|
215
225
|
throw promptWriteFailure(capturedStdinError);
|
|
216
226
|
}
|
package/dist/http-client.js
CHANGED
package/dist/import-resolver.js
CHANGED
|
@@ -14,7 +14,7 @@ const JS_TO_TS = {
|
|
|
14
14
|
'.mjs': ['.mts'],
|
|
15
15
|
'.cjs': ['.cts'],
|
|
16
16
|
};
|
|
17
|
-
const
|
|
17
|
+
const BLOCKING_V1 = new Set([
|
|
18
18
|
'unresolved_internal',
|
|
19
19
|
'unresolved_dynamic',
|
|
20
20
|
'declaration_only',
|
|
@@ -28,6 +28,16 @@ const BLOCKING = new Set([
|
|
|
28
28
|
'limit_files',
|
|
29
29
|
'limit_bytes',
|
|
30
30
|
]);
|
|
31
|
+
/** Under @2, `limit_bytes` is a deliberate trim (nearest files kept), not a block. */
|
|
32
|
+
const BLOCKING_V2 = new Set([...BLOCKING_V1].filter((code) => code !== 'limit_bytes'));
|
|
33
|
+
/** Every mapper version this companion can produce; advertised as capabilities. */
|
|
34
|
+
export const SUPPORTED_MAPPER_VERSIONS = [
|
|
35
|
+
'auto-surface-mapper@1',
|
|
36
|
+
'auto-surface-mapper@2',
|
|
37
|
+
];
|
|
38
|
+
export function isSupportedMapperVersion(value) {
|
|
39
|
+
return SUPPORTED_MAPPER_VERSIONS.includes(value);
|
|
40
|
+
}
|
|
31
41
|
export class MappingFailure extends Error {
|
|
32
42
|
kind;
|
|
33
43
|
code;
|
|
@@ -501,6 +511,8 @@ function seedRoots(seeds, workspace) {
|
|
|
501
511
|
}
|
|
502
512
|
export async function expandImports(input) {
|
|
503
513
|
const settings = input.settings ?? DEFAULT_MAPPER_SETTINGS;
|
|
514
|
+
const mapperVersion = input.mapperVersion ?? 'auto-surface-mapper@1';
|
|
515
|
+
const blocking = mapperVersion === 'auto-surface-mapper@2' ? BLOCKING_V2 : BLOCKING_V1;
|
|
504
516
|
const workspace = await discoverWorkspace(input.sandboxPath, input.tree);
|
|
505
517
|
const diagnostics = [...workspace.diagnostics];
|
|
506
518
|
const externals = [];
|
|
@@ -606,11 +618,11 @@ export async function expandImports(input) {
|
|
|
606
618
|
const uniqueDiagnostics = [...new Map(diagnostics.map((entry) => [`${entry.code}\0${entry.path}\0${entry.detail}`, entry])).values()].sort(compareDiagnostic);
|
|
607
619
|
const uniqueExternals = [...new Map(externals.map((entry) => [`${entry.from}\0${entry.specifier}`, entry])).values()].sort((a, b) => a.from.localeCompare(b.from) || a.specifier.localeCompare(b.specifier));
|
|
608
620
|
return {
|
|
609
|
-
mapper: { version:
|
|
621
|
+
mapper: { version: mapperVersion, settings },
|
|
610
622
|
resolutionInputs: workspace.resolutionInputs,
|
|
611
623
|
files,
|
|
612
624
|
externals: uniqueExternals,
|
|
613
|
-
status: uniqueDiagnostics.some((diagnostic) =>
|
|
625
|
+
status: uniqueDiagnostics.some((diagnostic) => blocking.has(diagnostic.code)) ? 'incomplete' : 'complete',
|
|
614
626
|
diagnostics: uniqueDiagnostics,
|
|
615
627
|
};
|
|
616
628
|
}
|
|
@@ -15,7 +15,7 @@ export const MAX_EVIDENCE_FILE_BYTES = 512 * 1024;
|
|
|
15
15
|
* ceiling leaves at least 3x headroom while bounding serialized prompt overhead.
|
|
16
16
|
*/
|
|
17
17
|
export const MAX_ASSESSMENT_PROMPT_CHARACTERS = 384 * 1024;
|
|
18
|
-
export const INVESTIGATION_MODEL = 'claude-sonnet-5';
|
|
18
|
+
export const INVESTIGATION_MODEL = 'claude-sonnet-5-5';
|
|
19
19
|
const execFileAsync = promisify(execFile);
|
|
20
20
|
export class MissingPolicy4ExecutionError extends Error {
|
|
21
21
|
name = 'MissingPolicy4ExecutionError';
|
package/dist/surface-mapper.js
CHANGED
|
@@ -3,11 +3,11 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { statSync } from 'node:fs';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { hasLinkedPathComponent } from './investigation-reader.js';
|
|
6
|
-
import { DEFAULT_MAPPER_SETTINGS, enumerateTrackedFiles, expandImports, MappingFailure, trackedNamedPathStatus, } from './import-resolver.js';
|
|
6
|
+
import { DEFAULT_MAPPER_SETTINGS, enumerateTrackedFiles, expandImports, isSupportedMapperVersion, MappingFailure, trackedNamedPathStatus, } from './import-resolver.js';
|
|
7
7
|
import { repositoryMissingReason, spawnFailureCode } from './local-scan.js';
|
|
8
8
|
import { createSandbox } from './sandbox.js';
|
|
9
9
|
export const SURFACE_SEED_PROMPT_VERSION = 'map_module_seeds@1';
|
|
10
|
-
export const SURFACE_SEED_MODEL = 'claude-sonnet-5';
|
|
10
|
+
export const SURFACE_SEED_MODEL = 'claude-sonnet-5-5';
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
const MODEL_ROLES = [
|
|
13
13
|
'implementation',
|
|
@@ -191,6 +191,9 @@ function seedPrompt(claim, trackedPaths) {
|
|
|
191
191
|
}
|
|
192
192
|
export async function mapSurfaceSnapshot(input) {
|
|
193
193
|
const { claim, sandboxPath, tree } = input;
|
|
194
|
+
if (!isSupportedMapperVersion(claim.mapperVersion)) {
|
|
195
|
+
throw new MappingFailure('stable', 'mapper_version_unsupported', `Unsupported mapper version ${String(claim.mapperVersion)}`);
|
|
196
|
+
}
|
|
194
197
|
const diagnostics = [];
|
|
195
198
|
const namedPaths = [];
|
|
196
199
|
const namedSeeds = [];
|
|
@@ -275,6 +278,7 @@ export async function mapSurfaceSnapshot(input) {
|
|
|
275
278
|
tree,
|
|
276
279
|
seeds,
|
|
277
280
|
settings: claim.mapperSettings,
|
|
281
|
+
mapperVersion: claim.mapperVersion,
|
|
278
282
|
});
|
|
279
283
|
const expansion = diagnostics.some((diagnostic) => diagnostic.code === 'named_path_unverifiable')
|
|
280
284
|
? { ...expanded, status: 'incomplete' }
|
package/dist/text-model.js
CHANGED
|
@@ -36,6 +36,34 @@ const PURPOSE_ERRORS = {
|
|
|
36
36
|
empty: 'No result returned.',
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
+
/** Server-side refusal fallback: on a policy decline the API re-runs the request on Anthropic's
|
|
40
|
+
* recommended substitute inside the same call. The scalar "default" form needs exactly this header
|
|
41
|
+
* (the array form is gated by -2026-06-01; pairing either header with the other form is a 400). */
|
|
42
|
+
const FALLBACK_BETA = 'server-side-fallback-2026-07-01';
|
|
43
|
+
/** SDK 0.111 types `fallbacks` as the array form only; the API also takes the scalar "default". */
|
|
44
|
+
const DEFAULT_FALLBACKS = 'default';
|
|
45
|
+
/** Every API-key call goes through here, so each one carries the refusal fallback. */
|
|
46
|
+
export function streamWithFallback(client, params) {
|
|
47
|
+
return client.beta.messages.stream({ ...params, betas: [FALLBACK_BETA], fallbacks: DEFAULT_FALLBACKS });
|
|
48
|
+
}
|
|
49
|
+
/** stop_reason 'refusal' — after the fallback, the whole chain declined. Thrown before content is
|
|
50
|
+
* read, so a partial or empty reply is never parsed as a result. */
|
|
51
|
+
export class ModelRefusalError extends Error {
|
|
52
|
+
category;
|
|
53
|
+
constructor(message, category = null) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'ModelRefusalError';
|
|
56
|
+
this.category = category;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The reply text once a refusal is ruled out. A mid-output fallback continues the declined
|
|
60
|
+
* model's partial text in a later block, so every text block is joined; null when there is none. */
|
|
61
|
+
export function replyText(message, refusal) {
|
|
62
|
+
if (message.stop_reason === 'refusal')
|
|
63
|
+
throw new ModelRefusalError(refusal, message.stop_details?.category ?? null);
|
|
64
|
+
const text = message.content.map((block) => (block.type === 'text' ? block.text : '')).join('');
|
|
65
|
+
return text || null;
|
|
66
|
+
}
|
|
39
67
|
function usageRecord(value) {
|
|
40
68
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
41
69
|
return null;
|
|
@@ -68,7 +96,7 @@ export function anthropicTextModel(client) {
|
|
|
68
96
|
const content = request.purpose.endsWith(':file_select') && blocks.length === 1 && blocks[0].type === 'text'
|
|
69
97
|
? blocks[0].text
|
|
70
98
|
: blocks;
|
|
71
|
-
const stream = client
|
|
99
|
+
const stream = streamWithFallback(client, {
|
|
72
100
|
model: request.model,
|
|
73
101
|
max_tokens: request.maxTokens,
|
|
74
102
|
thinking: { type: 'adaptive' },
|
|
@@ -83,11 +111,11 @@ export function anthropicTextModel(client) {
|
|
|
83
111
|
refusal: `The model declined the ${request.purpose} call.`,
|
|
84
112
|
empty: `The ${request.purpose} call returned no text.`,
|
|
85
113
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const text = message.
|
|
89
|
-
if (!text
|
|
114
|
+
// A purpose with no refusal wording of its own still fails as a refusal, under its empty-reply
|
|
115
|
+
// wording — never by reading whatever partial text the declined attempt left behind.
|
|
116
|
+
const text = replyText(message, errors.refusal ?? errors.empty);
|
|
117
|
+
if (!text)
|
|
90
118
|
throw new Error(errors.empty);
|
|
91
|
-
return { text
|
|
119
|
+
return { text, usage: usageRecord(message.usage) };
|
|
92
120
|
};
|
|
93
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-console-companion",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Local-first BYOS companion for the Fynch Product Console — reads code and runs AI calls from your own machine instead of the hosted server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|