Skip to content

Commit 0b7dc2b

Browse files
committed
Merge remote-tracking branch 'origin/main' into cl-6797-dead-export-purge-ts-prune-flags-870-candidates-verify
# Conflicts: # src/tui/geometry/index.ts
2 parents 0d0f2a4 + 6744dfc commit 0b7dc2b

42 files changed

Lines changed: 2810 additions & 1262 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,58 @@ by hand.
3232
(Critic on the diff plus at most one extra lens), loading `/review`
3333
for quality rules only.
3434

35+
## [0.3.26] - 2026-09-15
36+
37+
### What's Changed
38+
* Wait briefly for connecting servers before answering an empty tool search by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/996
39+
* Refuse path-trust migration from corrupt stores by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/995
40+
* Abandon a parked approval only on its own timeout by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/997
41+
* fix(provider): keep the Anthropic cache breakpoint off ephemeral turns by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1008
42+
* Do not let OAuth projections overwrite hand-named provider entries by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1007
43+
* Share workspace roots with list_dir and delete_file bounds by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1009
44+
* Route unclaimed popup keys back to the prompt by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1011
45+
* Enforce exec director tool allowlists through tool_search and promote by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1010
46+
* Harden path-escape sanitization for nested and alternate path keys by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1005
47+
* docs(product): remove shipped items from roadmap by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1020
48+
* fix(hardening): small follow-ups from the 0.3.25 review by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1021
49+
* Correct PerfTrace export prerequisites and settings secrets guidance by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1013
50+
* Verify docs/TUI.md against the terminal UI as it behaves by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1016
51+
* Correct MCP trust fingerprint, store path, and auth UX by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1017
52+
* Open transcript URLs on modifier click by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/964
53+
* Correct vendoring doc claims against the vendored tree by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1015
54+
* Restore Emil critique layer from the CMO original at full fidelity by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/999
55+
* feat(tests): make the suite runnable under bun test --parallel by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1023
56+
* Give Muse Spark its own reasoning effort ladder by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/979
57+
* Verify docs/PLUGINS.md against the plugin loader by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1014
58+
* feat(tui): brand the startup transcript with a quiet session header by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1022
59+
* Give Muse Spark tool-discipline rules by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/981
60+
* Only skip the spill URI sandbox for reader tools by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1006
61+
* Re-vendor Interchange trees to main HEAD for the director loader by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1025
62+
* Restore missing Common Patterns in create-issue skill by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/998
63+
* Hold newly promoted tools off the wire until a cache-safe boundary by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1027
64+
* Shard the CI test matrix into four time-balanced slices by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1028
65+
* Resolve transcript link clicks without the removed renderer hit-test by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1033
66+
* Collapse duplicate provider adapters and model catalogs by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1029
67+
* Inline TUI and agent micro-modules and merge the delivery queue trio by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1030
68+
* Rewrite brittle copy-pinned tests as behavior contracts by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1035
69+
* Unify permission verdict path with a mode-invariant catastrophic deny by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1037
70+
* Fold small session, chrome, and compaction helpers into owning modules by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1036
71+
* Correct two stale statements in the vendor patch ledgers by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1039
72+
* Give the two-attempt watchdog stall test an explicit 10s timeout by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1038
73+
* Harden transcript link resolution at markup edges by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1043
74+
* Unify tool, skill, and agent search on one rank-and-cut pipeline by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1045
75+
* Fold Go and Zen model catalogs into one parameterized module by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1053
76+
* Split transcript link helpers into focused modules by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1055
77+
* fix(subagent): queue overlapping steers instead of dropping them by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1063
78+
* Deliver a stashed steer as a follow-up when the run wins the race by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1066
79+
* fix(subagent): reap shell children so interrupt and close settle by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1068
80+
* Fail fast when an approval delivery is never accepted by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1069
81+
* fix(subagent): land steer and shell critique follow-ups by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1070
82+
* fix(tui): keep approved hold across retry and prove shell-guard kill by @TheGreatAxios in https://github.com/corbitsdev/corbits-code/pull/1071
83+
84+
85+
**Full Changelog**: https://github.com/corbitsdev/corbits-code/compare/v0.3.25...v0.3.26
86+
3587
## [0.3.25] - 2026-09-14
3688

3789
### What's Changed

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@corbits/code",
3-
"version": "0.3.25",
3+
"version": "0.3.26",
44
"description": "Single-process coding agent CLI built on the Interchange runtime (Bun + TypeScript)",
55
"private": true,
66
"license": "SEE LICENSE IN LICENSE.md",

‎src/agent/agent-search.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type } from "arktype";
55
import { scrubSecretShapedContent } from "../plugins/tool-result-secret-scrub.js";
66
import {
77
lexicalFields,
8+
rankAndCut,
89
scoreLexical,
910
tokenizeLexical,
1011
} from "./lexical-rank.js";
@@ -23,7 +24,8 @@ export interface AgentIndex {
2324
search(query: string, limit?: number): AgentProfile[];
2425
}
2526

26-
// Lexical ranker over id, description, and role text — same spirit as tool_search.
27+
// Shares the lexical ranker with tool/skill search. The description-substring
28+
// bonus stays here: it is agent-specific, not part of the shared scorer.
2729
export function createAgentIndex(
2830
getProfiles: () => readonly AgentProfile[],
2931
): AgentIndex {
@@ -45,12 +47,11 @@ export function createAgentIndex(
4547
const queryTokens = tokenizeLexical(query);
4648
const profiles = getProfiles();
4749
if (queryTokens.length === 0) return profiles.slice(0, limit);
48-
return profiles
49-
.map((p) => ({ profile: p, score: score(p, queryTokens, rawQuery) }))
50-
.filter((entry) => entry.score > 0)
51-
.sort((a, b) => b.score - a.score)
52-
.slice(0, limit)
53-
.map((entry) => entry.profile);
50+
return rankAndCut(
51+
profiles,
52+
(p) => score(p, queryTokens, rawQuery),
53+
limit,
54+
);
5455
},
5556
};
5657
}

‎src/agent/background-shell-tool.test.ts‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,81 @@ describe("background shell through the agent toolset", () => {
129129
}
130130
});
131131

132+
test("shell_collect with an aborted signal releases as running without killing the child", async () => {
133+
const toolset = await createAgentToolset({
134+
cwd: process.cwd(),
135+
permissionGate: gate(process.cwd()),
136+
});
137+
try {
138+
const started = await toolset.dynamicRunner.run(
139+
{
140+
id: "a-start",
141+
name: "run_shell",
142+
arguments: {
143+
command: "sleep 60",
144+
background: true,
145+
},
146+
},
147+
new AbortController().signal,
148+
);
149+
const { shell_id } = JSON.parse(String(started.content)) as {
150+
shell_id: string;
151+
};
152+
const aborted = new AbortController();
153+
aborted.abort(new Error("interrupted by interrupt_agent"));
154+
const out = await toolset.dynamicRunner.run(
155+
{
156+
id: "a-collect",
157+
name: "shell_collect",
158+
arguments: { shell_id, action: "collect", wait_ms: 60_000 },
159+
},
160+
aborted.signal,
161+
);
162+
expect(JSON.parse(String(out.content))).toMatchObject({
163+
status: "running",
164+
});
165+
// A live-signal collect still sees the child running: the abort above
166+
// released the waiter instead of killing the process. Had the abort
167+
// killed it, this would already report completed.
168+
const stillThere = await toolset.dynamicRunner.run(
169+
{
170+
id: "a-collect2",
171+
name: "shell_collect",
172+
arguments: { shell_id, action: "collect" },
173+
},
174+
new AbortController().signal,
175+
);
176+
expect(JSON.parse(String(stillThere.content))).toMatchObject({
177+
status: "running",
178+
});
179+
// And the child is still killable: cancel settles it as completed.
180+
const cancelled = await toolset.dynamicRunner.run(
181+
{
182+
id: "a-cancel",
183+
name: "shell_collect",
184+
arguments: { shell_id, action: "cancel" },
185+
},
186+
new AbortController().signal,
187+
);
188+
expect(JSON.parse(String(cancelled.content))).toMatchObject({
189+
status: "cancelling",
190+
});
191+
const final = await toolset.dynamicRunner.run(
192+
{
193+
id: "a-collect3",
194+
name: "shell_collect",
195+
arguments: { shell_id, action: "collect", wait_ms: 5_000 },
196+
},
197+
new AbortController().signal,
198+
);
199+
expect(JSON.parse(String(final.content))).toMatchObject({
200+
status: "completed",
201+
});
202+
} finally {
203+
await toolset.dispose();
204+
}
205+
});
206+
132207
test("toolset dispose kills every live background process group", async () => {
133208
const token = `ic_toolset_dispose_${randomUUID()}`;
134209
const toolset = await createAgentToolset({

‎src/agent/background-shell-tool.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ export function createShellCollectTool(
7979
) {
8080
return {
8181
definition: shellCollectDefinition,
82-
handler: async (rawArgs: Record<string, unknown>): Promise<string> => {
82+
handler: async (
83+
rawArgs: Record<string, unknown>,
84+
signal?: AbortSignal,
85+
): Promise<string> => {
8386
const parsed = ShellCollectArgs(rawArgs);
8487
if (parsed instanceof type.errors) {
8588
return "Error: shell_collect requires shell_id (string) and action ('collect' | 'cancel').";
@@ -91,7 +94,11 @@ export function createShellCollectTool(
9194
}
9295
return JSON.stringify({ shell_id, status: "cancelling" });
9396
}
94-
const snapshot = await registry.collect(shell_id, parsed.wait_ms ?? 0);
97+
const snapshot = await registry.collect(
98+
shell_id,
99+
parsed.wait_ms ?? 0,
100+
signal,
101+
);
95102
if (snapshot.state === "running") {
96103
return JSON.stringify({ shell_id, status: "running" });
97104
}

‎src/agent/lexical-rank.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,21 @@ export function scoreLexical(
4040
if (name.includes(rawQuery)) total += 1;
4141
return total;
4242
}
43+
44+
// One shared rank→filter→sort→slice→map cut behind tool_search,
45+
// search_agents, and skill_search. Per-surface scoring (and the agent
46+
// description bonus) stays at each call site; only the cut is shared. The
47+
// sort is score-descending and stable, so tied scores keep catalog order
48+
// identically on every surface.
49+
export function rankAndCut<T>(
50+
items: readonly T[],
51+
scoreItem: (item: T) => number,
52+
limit: number,
53+
): T[] {
54+
return items
55+
.map((item) => ({ item, score: scoreItem(item) }))
56+
.filter((entry) => entry.score > 0)
57+
.sort((a, b) => b.score - a.score)
58+
.slice(0, limit)
59+
.map((entry) => entry.item);
60+
}

‎src/agent/search-scorer-parity.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import type { SkillSummary } from "../extensions/skills.js";
88
import { createToolIndex } from "./tool-search.js";
99

1010
/**
11-
* The tool, skill, and agent search surfaces each carry their own copy of the
12-
* lexical ranker. This fixture drives all three with parallel catalogs so a
13-
* weight change in one copy fails loudly instead of drifting silently.
11+
* The three search surfaces share one lexical ranker and one rank-and-cut
12+
* pipeline; only per-surface scoring bonuses differ. This fixture drives all
13+
* three with parallel catalogs so a weight change in one copy fails loudly
14+
* instead of drifting silently.
1415
*/
1516
const QUERY = "granola";
1617

‎src/agent/skill-search.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type } from "arktype";
66
import type { SkillSummary } from "../extensions/skills.js";
77
import {
88
lexicalFields,
9+
rankAndCut,
910
scoreLexical,
1011
tokenizeLexical,
1112
} from "./lexical-rank.js";
@@ -82,15 +83,11 @@ export function createSkillSearchTool(
8283
if (queryTokens.length === 0) {
8384
return `No skills matched "${query}". Try different keywords describing the capability.`;
8485
}
85-
const matches = catalog
86-
.map((skill) => ({
87-
skill,
88-
score: scoreSkill(skill, queryTokens, rawQuery),
89-
}))
90-
.filter((entry) => entry.score > 0)
91-
.sort((a, b) => b.score - a.score)
92-
.slice(0, DEFAULT_LIMIT)
93-
.map((entry) => entry.skill);
86+
const matches = rankAndCut(
87+
catalog,
88+
(skill) => scoreSkill(skill, queryTokens, rawQuery),
89+
DEFAULT_LIMIT,
90+
);
9491
if (matches.length === 0) {
9592
return `No skills matched "${query}". Try different keywords describing the capability.`;
9693
}

‎src/agent/tool-search.ts‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type } from "arktype";
55

66
import {
77
lexicalFields,
8+
rankAndCut,
89
scoreLexical,
910
tokenizeLexical,
1011
} from "./lexical-rank.js";
@@ -234,9 +235,9 @@ export interface ToolIndex {
234235
search(query: string, limit?: number): string[];
235236
}
236237

237-
// A dependency-free lexical ranker over each tool's name + description. Exact name
238-
// token hits weigh most, then description token hits, then raw-substring matches
239-
// (so "linear" finds mcp__linear__* even though it is not a whole token there).
238+
// Rank registered tools against name + description via the shared lexical
239+
// scorer. Already-advertised tools are always callable so they never rank;
240+
// the allow list (when set) keeps search from promoting outside it.
240241
export function createToolIndex(
241242
getDefs: () => readonly ToolDefinition[],
242243
advertisedNames: readonly string[] = ADVERTISED_TOOL_NAMES,
@@ -260,17 +261,14 @@ export function createToolIndex(
260261
const rawQuery = query.toLowerCase().trim();
261262
const queryTokens = tokenizeLexical(query);
262263
if (queryTokens.length === 0) return [];
263-
return getDefs()
264+
const candidates = getDefs()
264265
.filter((def) => !advertisedNames.includes(def.name))
265-
.filter((def) => allow === undefined || allow.includes(def.name))
266-
.map((def) => ({
267-
name: def.name,
268-
score: score(def, queryTokens, rawQuery),
269-
}))
270-
.filter((entry) => entry.score > 0)
271-
.sort((a, b) => b.score - a.score)
272-
.slice(0, limit)
273-
.map((entry) => entry.name);
266+
.filter((def) => allow === undefined || allow.includes(def.name));
267+
return rankAndCut(
268+
candidates,
269+
(def) => score(def, queryTokens, rawQuery),
270+
limit,
271+
).map((def) => def.name);
274272
},
275273
};
276274
}

‎src/config.test.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,10 @@ import {
4343
} from "../packages/zen/src/index.js";
4444
import {
4545
prefetchGoModels,
46-
resetGoModelDiscoveryForTests,
47-
} from "./provider/opencode-go-models.js";
48-
import {
4946
prefetchZenModels,
47+
resetGoModelDiscoveryForTests,
5048
resetZenModelDiscoveryForTests,
51-
} from "./provider/zen-models.js";
49+
} from "./provider/model-catalogs.js";
5250
import {
5351
generateSessionId,
5452
initSessionDir,

0 commit comments

Comments
 (0)