@gajae-code/coding-agent 0.12.12 → 0.12.14
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/CHANGELOG.md +32 -0
- package/dist/types/cli/marketplace-hint.d.ts +28 -0
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/config/file-lock-gc.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/models-config-schema.d.ts +99 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +24 -1
- package/dist/types/sdk/bus/html-format.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +15 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/tools/index.d.ts +7 -0
- package/package.json +8 -8
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/marketplace-hint.ts +39 -0
- package/src/cli/plugin-cli.ts +10 -0
- package/src/cli.ts +23 -0
- package/src/config/file-lock-gc.ts +47 -26
- package/src/config/model-profiles.ts +18 -18
- package/src/config/model-registry.ts +17 -5
- package/src/config/models-config-schema.ts +9 -4
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
- package/src/gjc-runtime/deep-interview-state.ts +27 -3
- package/src/gjc-runtime/gc-render.ts +8 -1
- package/src/gjc-runtime/gc-runtime.ts +15 -1
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/main.ts +11 -3
- package/src/modes/acp/acp-agent.ts +123 -7
- package/src/modes/components/model-selector.ts +52 -5
- package/src/modes/print-mode.ts +10 -0
- package/src/prompts/system/project-prompt.md +19 -0
- package/src/sdk/bus/html-format.ts +19 -3
- package/src/sdk/bus/index.ts +21 -12
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -2
- package/src/sdk/bus/telegram-daemon.ts +50 -16
- package/src/sdk/bus/telegram-reference.ts +72 -9
- package/src/sdk/session.ts +5 -1
- package/src/session/agent-session.ts +0 -4
- package/src/session/internal/managed-session-storage.ts +24 -2
- package/src/session/session-manager.ts +77 -22
- package/src/setup/provider-presets.json +3 -3
- package/src/thinking-metadata.ts +2 -1
- package/src/tools/ask.ts +10 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/todo-write.ts +4 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-failure hints for marketplace-shaped plugin names.
|
|
3
|
+
*
|
|
4
|
+
* A bare spec (no `@marketplace`) is always classified as npm, so a user who
|
|
5
|
+
* copies a plugin name out of `plugin discover` gets an npm resolution failure
|
|
6
|
+
* with no hint that the same name is offered by a registered marketplace.
|
|
7
|
+
*
|
|
8
|
+
* Kept dependency-free — the marketplace barrel loads native addons — so this
|
|
9
|
+
* takes the narrow catalog surface it actually reads instead of the manager.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The catalog lookups this module needs; `MarketplaceManager` satisfies it. */
|
|
13
|
+
export interface MarketplaceCatalogLookup {
|
|
14
|
+
listMarketplaces(): Promise<ReadonlyArray<{ name: string }>>;
|
|
15
|
+
getPluginInfo(name: string, marketplace: string): Promise<unknown | null>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A bare name carries no `@scope`, version specifier, or path separator, so it
|
|
20
|
+
* is the only install spec shape that is safe to echo back in an error.
|
|
21
|
+
*/
|
|
22
|
+
export function isBareInstallName(spec: string): boolean {
|
|
23
|
+
return spec.length > 0 && !spec.includes("@") && !spec.includes("/") && !spec.includes("\\");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Names of registered marketplaces whose catalog offers a plugin under exactly
|
|
28
|
+
* `name`. Only marketplace names are returned: they are user-chosen registry
|
|
29
|
+
* labels, never the plugin source, so they carry no credentials or home paths.
|
|
30
|
+
*/
|
|
31
|
+
export async function findMarketplacesOffering(catalogs: MarketplaceCatalogLookup, name: string): Promise<string[]> {
|
|
32
|
+
const marketplaces = await catalogs.listMarketplaces();
|
|
33
|
+
const offering: string[] = [];
|
|
34
|
+
for (const marketplace of marketplaces) {
|
|
35
|
+
const info = await catalogs.getPluginInfo(name, marketplace.name).catch(() => null);
|
|
36
|
+
if (info) offering.push(marketplace.name);
|
|
37
|
+
}
|
|
38
|
+
return offering;
|
|
39
|
+
}
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -155,6 +155,7 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
import { classifyInstallTarget } from "./classify-install-target";
|
|
158
|
+
import { findMarketplacesOffering, isBareInstallName } from "./marketplace-hint";
|
|
158
159
|
|
|
159
160
|
export { classifyInstallTarget } from "./classify-install-target";
|
|
160
161
|
|
|
@@ -609,6 +610,15 @@ async function handleInstall(
|
|
|
609
610
|
// The spec can carry credentials, a query string, or an absolute home
|
|
610
611
|
// path, so report the failure without echoing it or the raw cause.
|
|
611
612
|
console.error(chalk.red(`${theme.status.error} Failed to install plugin (${describeInstallFailure(err)})`));
|
|
613
|
+
// A bare name (no `@scope`, no version, no path separator) is safe to
|
|
614
|
+
// echo, and it is exactly the shape a user copies out of
|
|
615
|
+
// `plugin discover`. Point at the qualified spec that would work.
|
|
616
|
+
if (isBareInstallName(spec)) {
|
|
617
|
+
const offering = await findMarketplacesOffering(mktMgr, spec).catch(() => []);
|
|
618
|
+
for (const marketplace of offering) {
|
|
619
|
+
console.error(chalk.dim(` Try: ${APP_NAME} plugin install ${spec}@${marketplace}`));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
612
622
|
process.exit(1);
|
|
613
623
|
}
|
|
614
624
|
}
|
package/src/cli.ts
CHANGED
|
@@ -389,11 +389,34 @@ function routeLegacyRootArgv(argv: readonly string[]): string[] | undefined {
|
|
|
389
389
|
return ["team", size, ...remaining];
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Map the common mistaken `models` subcommand spelling to non-agent listing.
|
|
394
|
+
*
|
|
395
|
+
* Agents frequently run `gjc models` from the bash tool expecting a catalog.
|
|
396
|
+
* Without this route, `models` was a positional launch prompt and nested agents
|
|
397
|
+
* re-invoked `gjc models`, spawning an unbounded process chain (#3857).
|
|
398
|
+
* Always rewrite to `launch --list-models` so the invocation exits after a
|
|
399
|
+
* bounded listing and never starts an interactive agent session.
|
|
400
|
+
*/
|
|
401
|
+
export function routeModelsAlias(argv: readonly string[]): string[] | undefined {
|
|
402
|
+
if (argv[0] !== "models") return undefined;
|
|
403
|
+
const rest = argv.slice(1);
|
|
404
|
+
if (rest.length === 0) return ["launch", "--list-models"];
|
|
405
|
+
// Pure search tokens become a single fuzzy pattern (matches --list-models).
|
|
406
|
+
if (rest.every(token => !token.startsWith("-") && !token.startsWith("@"))) {
|
|
407
|
+
return ["launch", "--list-models", rest.join(" ")];
|
|
408
|
+
}
|
|
409
|
+
// Mixed flags still go through list-models first so "models" is never a prompt.
|
|
410
|
+
return ["launch", "--list-models", ...rest];
|
|
411
|
+
}
|
|
412
|
+
|
|
392
413
|
/** Apply the same default-launch routing used by runCli after root fast paths. */
|
|
393
414
|
export function routeRootArgv(argv: readonly string[]): string[] {
|
|
394
415
|
const normalizedArgv = normalizeResumeAlias(argv);
|
|
395
416
|
const legacyArgv = routeLegacyRootArgv(normalizedArgv);
|
|
396
417
|
if (legacyArgv) return legacyArgv;
|
|
418
|
+
const modelsArgv = routeModelsAlias(normalizedArgv);
|
|
419
|
+
if (modelsArgv) return modelsArgv;
|
|
397
420
|
const first = normalizedArgv[0];
|
|
398
421
|
return first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help"
|
|
399
422
|
? normalizedArgv
|
|
@@ -13,13 +13,15 @@ import type {
|
|
|
13
13
|
GcPruneOutcome,
|
|
14
14
|
GcRecord,
|
|
15
15
|
GcStoreAdapter,
|
|
16
|
+
GcWarning,
|
|
16
17
|
} from "../gjc-runtime/gc-runtime";
|
|
17
18
|
import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
|
|
18
19
|
import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
|
|
19
20
|
import { readFileLockInfoForGc, removeFileLockDirForGc } from "./file-lock";
|
|
20
21
|
|
|
21
22
|
const MAX_WALK_DEPTH = 6;
|
|
22
|
-
|
|
23
|
+
/** Default per-root walk budget. Truncation is a warning, not a hard error. */
|
|
24
|
+
export const FILE_LOCK_GC_MAX_WALK_ENTRIES = 20_000;
|
|
23
25
|
|
|
24
26
|
// High-cardinality, lock-free subtrees we never descend into. `.lock` dirs are
|
|
25
27
|
// created next to config files, never inside these.
|
|
@@ -30,6 +32,11 @@ interface WalkState {
|
|
|
30
32
|
truncated: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface FileLocksGcCollectOptions {
|
|
36
|
+
/** Override per-root entry budget (tests). Defaults to {@link FILE_LOCK_GC_MAX_WALK_ENTRIES}. */
|
|
37
|
+
maxWalkEntries?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
// Global, env-aware GJC lock roots. Per the approved scope this covers the
|
|
34
41
|
// user config root, the agent dir (honors GJC_CODING_AGENT_DIR), and the
|
|
35
42
|
// configured receipt-spool dir — NOT the invocation cwd's project `.gjc`.
|
|
@@ -102,8 +109,9 @@ async function walkForLockDirs(
|
|
|
102
109
|
state: WalkState,
|
|
103
110
|
lockDirs: Set<string>,
|
|
104
111
|
errors: GcError[],
|
|
112
|
+
maxWalkEntries: number,
|
|
105
113
|
): Promise<void> {
|
|
106
|
-
if (state.entries >=
|
|
114
|
+
if (state.entries >= maxWalkEntries) {
|
|
107
115
|
state.truncated = true;
|
|
108
116
|
return;
|
|
109
117
|
}
|
|
@@ -137,45 +145,58 @@ async function walkForLockDirs(
|
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
for (const entry of entries) {
|
|
140
|
-
if (state.entries >=
|
|
148
|
+
if (state.entries >= maxWalkEntries) {
|
|
141
149
|
state.truncated = true;
|
|
142
150
|
return;
|
|
143
151
|
}
|
|
144
152
|
if (PRUNED_DIR_NAMES.has(entry)) continue;
|
|
145
|
-
await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors);
|
|
153
|
+
await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors, maxWalkEntries);
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Discover + classify file-lock records. Each known lock root gets its own walk
|
|
159
|
+
* budget so truncating one root never skips the others. Caps surface as
|
|
160
|
+
* warnings (partial results), not hard discovery errors.
|
|
161
|
+
*/
|
|
162
|
+
export async function collectFileLocksForGc(
|
|
163
|
+
ctx: GcContext,
|
|
164
|
+
options: FileLocksGcCollectOptions = {},
|
|
165
|
+
): Promise<GcCollectResult> {
|
|
166
|
+
const maxWalkEntries = options.maxWalkEntries ?? FILE_LOCK_GC_MAX_WALK_ENTRIES;
|
|
167
|
+
const records: GcRecord[] = [];
|
|
168
|
+
const errors: GcError[] = [];
|
|
169
|
+
const warnings: GcWarning[] = [];
|
|
170
|
+
const lockDirs = new Set<string>();
|
|
171
|
+
|
|
172
|
+
for (const root of knownFileLockRoots(ctx)) {
|
|
173
|
+
// Fresh budget per root so a huge agent dir cannot starve config/spool.
|
|
155
174
|
const state: WalkState = { entries: 0, truncated: false };
|
|
156
|
-
|
|
157
|
-
for (const root of knownFileLockRoots(ctx)) {
|
|
158
|
-
await walkForLockDirs(root, 0, state, lockDirs, errors);
|
|
159
|
-
if (state.truncated) break;
|
|
160
|
-
}
|
|
161
|
-
|
|
175
|
+
await walkForLockDirs(root, 0, state, lockDirs, errors, maxWalkEntries);
|
|
162
176
|
if (state.truncated) {
|
|
163
|
-
|
|
177
|
+
warnings.push({
|
|
164
178
|
store: "file_locks",
|
|
165
|
-
scope:
|
|
166
|
-
message: `file lock discovery capped at ${
|
|
179
|
+
scope: root,
|
|
180
|
+
message: `file lock discovery capped at ${maxWalkEntries} entries for root ${root} (scanned ${state.entries})`,
|
|
167
181
|
});
|
|
168
182
|
}
|
|
183
|
+
}
|
|
169
184
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
185
|
+
for (const lockDir of lockDirs) {
|
|
186
|
+
try {
|
|
187
|
+
records.push(await collectLockRecord(lockDir, ctx));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
errors.push({ store: "file_locks", scope: lockDir, message: errorMessage(error) });
|
|
176
190
|
}
|
|
191
|
+
}
|
|
177
192
|
|
|
178
|
-
|
|
193
|
+
return { records, errors, warnings };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
197
|
+
store: "file_locks",
|
|
198
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
199
|
+
return collectFileLocksForGc(ctx);
|
|
179
200
|
},
|
|
180
201
|
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
181
202
|
const lockDir = record.path ?? record.id;
|
|
@@ -264,25 +264,25 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
264
264
|
architect: "cursor/composer-2.5-fast",
|
|
265
265
|
}),
|
|
266
266
|
profile("minimax-eco", ["minimax-code"], {
|
|
267
|
-
default: "minimax-code/
|
|
268
|
-
executor: "minimax-code/
|
|
269
|
-
planner: "minimax-code/
|
|
270
|
-
critic: "minimax-code/
|
|
271
|
-
architect: "minimax-code/
|
|
267
|
+
default: "minimax-code/MiniMax-M3:low",
|
|
268
|
+
executor: "minimax-code/MiniMax-M3:minimal",
|
|
269
|
+
planner: "minimax-code/MiniMax-M3:low",
|
|
270
|
+
critic: "minimax-code/MiniMax-M3:medium",
|
|
271
|
+
architect: "minimax-code/MiniMax-M3:high",
|
|
272
272
|
}),
|
|
273
273
|
profile("minimax-medium", ["minimax-code"], {
|
|
274
|
-
default: "minimax-code/
|
|
275
|
-
executor: "minimax-code/
|
|
276
|
-
planner: "minimax-code/
|
|
277
|
-
critic: "minimax-code/
|
|
278
|
-
architect: "minimax-code/
|
|
274
|
+
default: "minimax-code/MiniMax-M3:medium",
|
|
275
|
+
executor: "minimax-code/MiniMax-M3:low",
|
|
276
|
+
planner: "minimax-code/MiniMax-M3:medium",
|
|
277
|
+
critic: "minimax-code/MiniMax-M3:high",
|
|
278
|
+
architect: "minimax-code/MiniMax-M3:xhigh",
|
|
279
279
|
}),
|
|
280
280
|
profile("minimax-pro", ["minimax-code"], {
|
|
281
|
-
default: "minimax-code/
|
|
282
|
-
executor: "minimax-code/
|
|
283
|
-
planner: "minimax-code/
|
|
284
|
-
critic: "minimax-code/
|
|
285
|
-
architect: "minimax-code/
|
|
281
|
+
default: "minimax-code/MiniMax-M3:xhigh",
|
|
282
|
+
executor: "minimax-code/MiniMax-M3:medium",
|
|
283
|
+
planner: "minimax-code/MiniMax-M3:high",
|
|
284
|
+
critic: "minimax-code/MiniMax-M3:xhigh",
|
|
285
|
+
architect: "minimax-code/MiniMax-M3:xhigh",
|
|
286
286
|
}),
|
|
287
287
|
profile("alibaba-token-plan-balanced", ["alibaba-token-plan"], {
|
|
288
288
|
default: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
@@ -306,11 +306,11 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
306
306
|
critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
307
307
|
}),
|
|
308
308
|
profile("alibaba-token-plan-qwen-deepseek", ["alibaba-token-plan"], {
|
|
309
|
-
default: "alibaba-token-plan/
|
|
309
|
+
default: "alibaba-token-plan/qwen3.8-max:high",
|
|
310
310
|
executor: "alibaba-token-plan/deepseek-v4-flash-0731:high",
|
|
311
311
|
planner: "alibaba-token-plan/deepseek-v4-flash-0731:max",
|
|
312
|
-
architect: "alibaba-token-plan/
|
|
313
|
-
critic: "alibaba-token-plan/
|
|
312
|
+
architect: "alibaba-token-plan/qwen3.8-max:xhigh",
|
|
313
|
+
critic: "alibaba-token-plan/qwen3.8-max:xhigh",
|
|
314
314
|
}),
|
|
315
315
|
profile("alibaba-token-plan-glm-deepseek", ["alibaba-token-plan"], {
|
|
316
316
|
default: "alibaba-token-plan/glm-5.2:high",
|
|
@@ -926,12 +926,24 @@ const customReferenceMap = buildCustomReferenceMap();
|
|
|
926
926
|
|
|
927
927
|
function getCustomReferenceCandidateIds(modelId: string): string[] {
|
|
928
928
|
const candidates = new Set<string>();
|
|
929
|
-
const
|
|
930
|
-
const
|
|
929
|
+
const trimmedId = modelId.trim();
|
|
930
|
+
const minimaxM = /^minimax-m(\d+(?:\.\d+)*)$/i.exec(trimmedId);
|
|
931
|
+
const queue = minimaxM ? [`MiniMax-M${minimaxM[1]}`, trimmedId] : [trimmedId];
|
|
931
932
|
if (minimaxM) {
|
|
932
|
-
// MiniMax catalogs
|
|
933
|
-
//
|
|
934
|
-
// canonical display casing
|
|
933
|
+
// First-class MiniMax catalogs expose canonical `MiniMax-M*` ids only,
|
|
934
|
+
// but custom providers may still use lowercase wire ids. Normalize to
|
|
935
|
+
// the canonical display casing so metadata inheritance keeps working.
|
|
936
|
+
}
|
|
937
|
+
// Namespaced wire IDs (e.g. `cline-pass/deepseek-v4-flash`) keep the full id for
|
|
938
|
+
// the API request, but should still try the leaf segment against bundled
|
|
939
|
+
// references so capability metadata is not silently replaced by 128K/16K defaults.
|
|
940
|
+
// Only an exact leaf match in the reference map inherits; unknown leaves stay defaulted.
|
|
941
|
+
const slashIndex = trimmedId.lastIndexOf("/");
|
|
942
|
+
if (slashIndex >= 0 && slashIndex < trimmedId.length - 1) {
|
|
943
|
+
const leafId = trimmedId.slice(slashIndex + 1).trim();
|
|
944
|
+
if (leafId && leafId !== trimmedId) {
|
|
945
|
+
queue.push(leafId);
|
|
946
|
+
}
|
|
935
947
|
}
|
|
936
948
|
for (let index = 0; index < queue.length; index += 1) {
|
|
937
949
|
const candidate = queue[index]?.trim();
|
|
@@ -20,7 +20,7 @@ const ReasoningEffortMapSchema = z.object({
|
|
|
20
20
|
max: z.string().optional(),
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const ModelCompatSchema = z.object({
|
|
24
24
|
supportsStore: z.boolean().optional(),
|
|
25
25
|
supportsDeveloperRole: z.boolean().optional(),
|
|
26
26
|
sendSessionHeaders: z.boolean().optional(),
|
|
@@ -48,8 +48,13 @@ export const OpenAICompatSchema = z.object({
|
|
|
48
48
|
extraBody: z.record(z.string(), z.unknown()).optional(),
|
|
49
49
|
supportsStrictMode: z.boolean().optional(),
|
|
50
50
|
toolStrictMode: z.enum(["all_strict", "none"]).optional(),
|
|
51
|
+
supportsLongCacheRetention: z.boolean().optional(),
|
|
52
|
+
promptCacheMode: z.enum(["none", "explicit", "automatic"]).optional(),
|
|
51
53
|
});
|
|
52
54
|
|
|
55
|
+
// Backward-compatible export for callers that imported the original schema name.
|
|
56
|
+
export const OpenAICompatSchema = ModelCompatSchema;
|
|
57
|
+
|
|
53
58
|
export const GJC_MODEL_EFFORT_IDS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
54
59
|
export const GJC_MODEL_ASSIGNMENT_TARGET_IDS = ["default", "executor", "architect", "planner", "critic"] as const;
|
|
55
60
|
export const EffortSchema = z.enum(GJC_MODEL_EFFORT_IDS);
|
|
@@ -146,7 +151,7 @@ const ModelDefinitionSchema = z
|
|
|
146
151
|
contextWindow: z.number().optional(),
|
|
147
152
|
maxTokens: z.number().optional(),
|
|
148
153
|
headers: z.record(z.string(), z.string()).optional(),
|
|
149
|
-
compat:
|
|
154
|
+
compat: ModelCompatSchema.optional(),
|
|
150
155
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
151
156
|
wireModelId: z.string().min(1).optional(),
|
|
152
157
|
requestTransform: RequestTransformSchema.optional(),
|
|
@@ -173,7 +178,7 @@ export const ModelOverrideSchema = z
|
|
|
173
178
|
contextWindow: z.number().optional(),
|
|
174
179
|
maxTokens: z.number().optional(),
|
|
175
180
|
headers: z.record(z.string(), z.string()).optional(),
|
|
176
|
-
compat:
|
|
181
|
+
compat: ModelCompatSchema.optional(),
|
|
177
182
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
178
183
|
wireModelId: z.string().min(1).optional(),
|
|
179
184
|
requestTransform: RequestTransformSchema.optional(),
|
|
@@ -221,7 +226,7 @@ const ProviderConfigSchema = z
|
|
|
221
226
|
])
|
|
222
227
|
.optional(),
|
|
223
228
|
headers: z.record(z.string(), z.string()).optional(),
|
|
224
|
-
compat:
|
|
229
|
+
compat: ModelCompatSchema.optional(),
|
|
225
230
|
webSearch: z.enum(["on", "off", "auto"]).optional(),
|
|
226
231
|
authHeader: z.boolean().optional(),
|
|
227
232
|
auth: ProviderAuthSchema.optional(),
|
|
@@ -122,7 +122,7 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
|
|
|
122
122
|
- Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
|
|
123
123
|
- Include `threshold_source` in the first `gjc deep-interview write` payload and preserve it on later state updates; do not edit `.gjc/_session-{sessionid}/state` files directly unless an explicit force override is active.
|
|
124
124
|
- Include both threshold and source in the final spec metadata.
|
|
125
|
-
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless `
|
|
125
|
+
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless the appended `User:` request makes another user/session language obvious or the user explicitly requests another language. Do not add language-specific special cases.
|
|
126
126
|
|
|
127
127
|
## Phase 0.5: Suitability Gate
|
|
128
128
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
assertDeepInterviewInputWithinLimit,
|
|
12
12
|
assertDeepInterviewIntentManifest,
|
|
13
13
|
assertDeepInterviewStructuredResponseWithinLimit,
|
|
14
|
+
canonicalizeDeepInterviewText,
|
|
14
15
|
createDeepInterviewIntentManifest,
|
|
15
16
|
type DeepInterviewEstablishedFact,
|
|
16
17
|
type DeepInterviewIntentItem,
|
|
@@ -106,16 +107,19 @@ export function buildAnswerShell(
|
|
|
106
107
|
input: DeepInterviewAnswerInput,
|
|
107
108
|
now: string = new Date().toISOString(),
|
|
108
109
|
): DeepInterviewRoundRecord {
|
|
110
|
+
const questionText = canonicalizeDeepInterviewText(input.questionText);
|
|
111
|
+
const selectedOptions = input.selectedOptions?.map(canonicalizeDeepInterviewText);
|
|
112
|
+
const customInput = input.customInput === undefined ? undefined : canonicalizeDeepInterviewText(input.customInput);
|
|
109
113
|
return {
|
|
110
114
|
round_key: deriveRoundKey(input.interviewId, input),
|
|
111
115
|
round_id: input.round_id,
|
|
112
116
|
round: input.round,
|
|
113
117
|
question_id: input.questionId,
|
|
114
|
-
question_text:
|
|
115
|
-
question_hash: questionHash(
|
|
116
|
-
answer_hash: answerHash(
|
|
117
|
-
selected_options:
|
|
118
|
-
custom_input:
|
|
118
|
+
question_text: questionText,
|
|
119
|
+
question_hash: questionHash(questionText),
|
|
120
|
+
answer_hash: answerHash(selectedOptions, customInput),
|
|
121
|
+
selected_options: selectedOptions,
|
|
122
|
+
custom_input: customInput,
|
|
119
123
|
component: input.component,
|
|
120
124
|
dimension: input.dimension,
|
|
121
125
|
ambiguity_at_ask: input.ambiguity,
|
|
@@ -97,13 +97,34 @@ export function hashContent(value: string): string {
|
|
|
97
97
|
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Canonicalize interview prose to NFC.
|
|
102
|
+
*
|
|
103
|
+
* Hangul, unlike most Latin prose, routinely reaches the interview in both
|
|
104
|
+
* composed and decomposed form (macOS-sourced pastes and some IME/clipboard
|
|
105
|
+
* paths emit NFD), and the two forms are the same text for the user while
|
|
106
|
+
* being different JavaScript strings. Canonicalizing at the identity and
|
|
107
|
+
* length boundaries keeps one round per answer and keeps the character caps
|
|
108
|
+
* script-fair: decomposed Hangul otherwise costs two to three code points per
|
|
109
|
+
* syllable against the same budget.
|
|
110
|
+
*/
|
|
111
|
+
export function canonicalizeDeepInterviewText(value: string): string {
|
|
112
|
+
return value.normalize("NFC");
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
export function questionHash(questionText: string): string {
|
|
101
|
-
return hashContent(questionText);
|
|
116
|
+
return hashContent(canonicalizeDeepInterviewText(questionText));
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
export function answerHash(selectedOptions: string[] | undefined, customInput: string | undefined): string {
|
|
105
120
|
return createHash("sha256")
|
|
106
|
-
.update(
|
|
121
|
+
.update(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
selected: (selectedOptions ?? []).map(canonicalizeDeepInterviewText),
|
|
124
|
+
custom:
|
|
125
|
+
customInput === undefined || customInput === null ? null : canonicalizeDeepInterviewText(customInput),
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
107
128
|
.digest("hex");
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -471,10 +492,13 @@ export function assertDeepInterviewStructuredResponseWithinLimit(value: unknown)
|
|
|
471
492
|
/**
|
|
472
493
|
* Assert a free-text input is within its size cap. Never inspects content for shell
|
|
473
494
|
* metacharacters — free-text fields accept prose verbatim; this only bounds length.
|
|
495
|
+
* The cap is measured on the NFC form so decomposed Hangul is charged the same
|
|
496
|
+
* budget as the identical composed text.
|
|
474
497
|
*/
|
|
475
498
|
export function assertDeepInterviewInputWithinLimit(value: string, max: number, fieldName = "input"): void {
|
|
476
499
|
if (typeof value !== "string") throw new Error(`${fieldName} must be a string`);
|
|
477
|
-
if (deepInterviewCharacterCount(value) > max)
|
|
500
|
+
if (deepInterviewCharacterCount(canonicalizeDeepInterviewText(value)) > max)
|
|
501
|
+
throw new Error(`${fieldName} exceeds max length ${max}`);
|
|
478
502
|
}
|
|
479
503
|
|
|
480
504
|
/** Validate user-supplied deep-interview prose before an envelope is persisted. */
|
|
@@ -72,6 +72,12 @@ export function buildGcReportText(report: GcReport): string {
|
|
|
72
72
|
lines.push("");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
if (report.warnings.length > 0) {
|
|
76
|
+
lines.push(`Warnings (${report.warnings.length})`);
|
|
77
|
+
for (const warning of report.warnings) lines.push(` [${warning.store}/${warning.scope}] ${warning.message}`);
|
|
78
|
+
lines.push("");
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
if (report.errors.length > 0) {
|
|
76
82
|
lines.push(`Errors (${report.errors.length})`);
|
|
77
83
|
for (const err of report.errors) lines.push(` [${err.store}/${err.scope}] ${err.message}`);
|
|
@@ -82,7 +88,8 @@ export function buildGcReportText(report: GcReport): string {
|
|
|
82
88
|
lines.push(
|
|
83
89
|
`Summary: discovered=${c.discovered} stale=${c.stale} alive=${c.alive} eperm=${c.eperm} unknown=${c.unknown} ` +
|
|
84
90
|
`terminal_lifecycle=${c.terminal_lifecycle} unclassified=${c.unclassified} ` +
|
|
85
|
-
`${report.dry_run ? `would_remove=${c.would_remove}` : `removed=${c.removed} failed=${c.failed}`}
|
|
91
|
+
`${report.dry_run ? `would_remove=${c.would_remove}` : `removed=${c.removed} failed=${c.failed}`} ` +
|
|
92
|
+
`errors=${c.errors} warnings=${report.warnings.length}`,
|
|
86
93
|
);
|
|
87
94
|
lines.push("");
|
|
88
95
|
return `${lines.join("\n")}`;
|
|
@@ -78,9 +78,18 @@ export interface GcError {
|
|
|
78
78
|
message: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Non-fatal discovery partials (e.g. traversal caps). Does not affect exit code. */
|
|
82
|
+
export interface GcWarning {
|
|
83
|
+
store: GcStore;
|
|
84
|
+
scope: string;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
export interface GcCollectResult {
|
|
82
89
|
records: GcRecord[];
|
|
83
90
|
errors: GcError[];
|
|
91
|
+
/** Optional partial-result notices; omitted by adapters that have none. */
|
|
92
|
+
warnings?: GcWarning[];
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
export interface GcPruneOutcome {
|
|
@@ -140,6 +149,8 @@ export interface GcReport {
|
|
|
140
149
|
stores: Record<GcStore, GcRecord[]>;
|
|
141
150
|
counts: GcCounts;
|
|
142
151
|
errors: GcError[];
|
|
152
|
+
/** Partial-result notices that do not fail the run (e.g. walk caps). */
|
|
153
|
+
warnings: GcWarning[];
|
|
143
154
|
session_index?: GcSessionIndexHealth;
|
|
144
155
|
}
|
|
145
156
|
|
|
@@ -298,12 +309,14 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
298
309
|
export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext, prune: boolean): Promise<GcReport> {
|
|
299
310
|
const stores = emptyStores();
|
|
300
311
|
const errors: GcError[] = [];
|
|
312
|
+
const warnings: GcWarning[] = [];
|
|
301
313
|
|
|
302
314
|
for (const adapter of adapters) {
|
|
303
315
|
try {
|
|
304
316
|
const result = await adapter.collect(ctx);
|
|
305
317
|
stores[adapter.store].push(...result.records);
|
|
306
318
|
errors.push(...result.errors);
|
|
319
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
307
320
|
} catch (error) {
|
|
308
321
|
errors.push({
|
|
309
322
|
store: adapter.store,
|
|
@@ -350,7 +363,7 @@ export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext
|
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
|
|
353
|
-
return { dry_run: !prune, stores, counts: computeCounts(stores, errors), errors };
|
|
366
|
+
return { dry_run: !prune, stores, counts: computeCounts(stores, errors), errors, warnings };
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
/**
|
|
@@ -358,6 +371,7 @@ export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext
|
|
|
358
371
|
* - usage/parse error => 2
|
|
359
372
|
* - hard discovery errors => 1 (both modes)
|
|
360
373
|
* - prune mode with a failed intended removal => 1
|
|
374
|
+
* - warnings alone never fail the run
|
|
361
375
|
* - otherwise => 0
|
|
362
376
|
*/
|
|
363
377
|
export function computeExitCode(report: GcReport): number {
|