Skip to content

Commit 4780cf9

Browse files
committed
Merge origin/main
2 parents 1c6ccfc + 722dee5 commit 4780cf9

175 files changed

Lines changed: 22237 additions & 2549 deletions

File tree

Some content is hidden

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

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ It authenticates over HTTPS via `gh`'s credential helper and rewrites the SSH re
6767

6868
## Building on Interchange
6969

70-
Interchange is the standard library for this repo, consumed as published `@intx/*` npm packages pinned at 0.2.2 (`@intx/inference` resolves to the vendored copy in `vendor/intx-inference` upstream 0.2.2 plus the audited patch set on CL-4352). We never modify or push to the upstream interchange repository. Before writing any new infrastructure — plugins, middleware, utilities, state management, logging, authz, inference, tools — check these packages.
70+
Interchange is the standard library for this repo, consumed as published `@intx/*` npm packages pinned at 0.2.2, except `@intx/inference`, `@intx/types`, and `@intx/storage-isogit`, which resolve to vendored source under `vendor/intx-*` at upstream head (coupled by the reactor's approval-suspend primitive; `@intx/inference` also carries a local patch set). See `docs/VENDORING.md` for what's vendored, from which upstream commit, and the re-sync procedure. We never modify or push to the upstream interchange repository. Before writing any new infrastructure — plugins, middleware, utilities, state management, logging, authz, inference, tools — check these packages.
7171

7272
| Package | Covers |
7373
|---|---|

bun.lock

Lines changed: 30 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ In TUI chat mode there is no completion gate — the session stays open across t
106106

107107
Two directors, selected by role:
108108

109-
- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, multi-turn chat semantics, and an optional **goal governor** (session-scoped auto-continue until every acceptance criterion is done). It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Auto mode is toggled by CLI flags (`--auto` / `--no-auto`); there is currently no in-session key to toggle it (default on; constrained envelope — workspace writes and unconstrained shell auto-allow; installs, recursive rm, worktree changes, sensitive-path and opaque-wrapper shell still ask; shell file-mutation denied). It is not a separate edit/plan mode.
109+
- **ChatDirector** (interactive, `src/agent/director.ts`) — Extends `DefaultDirector` with task list tracking, workflow nudges, LSP auto-activation, multi-turn chat semantics, and an optional **goal governor** (session-scoped auto-continue until every acceptance criterion is done). It never terminates the session: operator declines are surfaced as replies and the reactor stays alive for the next message. Auto mode is toggled by CLI flags (`--auto` / `--no-auto`); there is currently no in-session key to toggle it (default on; constrained envelope — workspace writes and unconstrained shell auto-allow; installs, recursive rm, force/uncontained worktree changes, sensitive-path and opaque-wrapper shell still ask; contained non-force `git worktree add`/`remove`/`prune` and `list` auto-allow; shell file-mutation denied). It is not a separate edit/plan mode.
110110
- **SubAgentDirector** (delegated work, `src/subagent/index.ts`) — Drives a dispatched worker until a turn arrives with no tool calls, then replies with the final assistant text and ends the run. A tool-less completion with **zero tool calls in the entire run** is returned as a **never-acted** salvage report (not a successful implement); explore/read-only workers that used tools then replied with findings remain normal completes. Hard stops also fire after 2 consecutive identical tool-call fingerprints (**no-progress**), on progressive re-read pressure (**thrash** — the same path re-read past a limit amid enough tool volume, tracked by `src/subagent/thrash.ts`), or after the leaf turn budget (**turn-budget**, default 30, overridable via `task(maxTurns)`, agent profile `maxTurns`, or `settings.subagentMaxTurns`, capped at 100), each returning a structured salvage report (reason, partial findings, blockers) so a thrashing child cannot burn tokens indefinitely. A fourth hard stop, **repetition**, is detected outside the director entirely: `runSubAgent`'s stream sink watches the streamed text of the in-flight cycle for degenerate token loops (`src/subagent/repetition.ts`) — whitespace-collapsed raw text, a smallest-period KMP check over the probe tail, default window >= 16 chars repeated >= 8 times, evaluated every 256 streamed chars — and on a hit aborts the run controller mid-cycle, returning a `repetition` salvage report that leads with the looped window and warns the parent against re-dispatching the identical brief. Because directors only see completed turns, this is the only stop that can catch a loop inside a single turn that never finishes. A one-shot **report-forced** signal fires a few turns before the cap while the leaf is still tooling — it is not a stop: the director injects a wrap-up nudge and lets the leaf finish on its own, so turn-budget stays reachable for a leaf still making progress. Operator/parent cancel after any progress likewise returns a **cancelled** salvage report (partial findings + tool activity) instead of a bare cancel string; cancel before progress still surfaces as cancelled-by-operator. Optional `task(tier=)` (`fast` | `standard` | `clever`) overrides profile inference, profile tier, and the parent provider for that spawn only, and fails closed when the tier is unconfigured. The parent `task` tool keeps a session-scoped brief-dispatch ledger (`src/subagent/brief-dispatch.ts`): fingerprints cover prompt + agent + intent + success_criteria + do_not (not maxTurns/description/tier). After thrash / no-progress / repetition / never-acted salvage, an identical re-dispatch is hard-blocked for the rest of the parent chat; change at least one fingerprint field to force a re-run. Turn-budget salvage still invites a higher maxTurns for a few same-brief retries without a successful complete, then flips the parent hint to stop and change approach (soft — further identical dispatches are still admitted). A successful complete resets the same-brief retry budget.
111111

112112

@@ -274,7 +274,7 @@ tool call
274274

275275
- **classify** — Read-only tools (`read_file`, `search_files`, `grep`, `list_dir`) are tier `allow`; everything else is tier `ask`. Builds approval requests: shell yields one request for the full command the model asked to run (security still splits under the gate); file tools keyed on the target path; other tools keyed on tool name.
276276
- **command** — Splits chained commands for security classification and derives command-shape approval scopes. Multi-segment chains only offer an exact-command persist pattern (a prefix like `npm *` must not cover `npm i && rm -rf /` later).
277-
- **auto-shell-policy** — Constrains `run_shell` even when auto mode would otherwise rubber-stamp it. Before matching, `expandShellSubjects` peels `bash`/`sh`/`zsh -c`, `xargs` utility tails, and transparent prefixes (`env`, `nice`, `timeout`, …) so rules see the real payload; an unparseable wrapper (variable expansion or command substitution) sets an opaque flag that forces `ask`. Effects: `deny` blocks outright (file mutations through ad-hoc tooling — output redirection, `tee`, `sed -i`/`perl -i`, interpreter inline programs or heredocs — which must instead go through `write_file`/`edit_file`); `ask` declines to auto-allow and falls through to the operator prompt (recursive `rm`, dependency installs and remote runners: npm/yarn/pnpm/bun, pip, cargo, go, brew, npx/bunx, …, git worktree add/remove/prune, shell that references a sensitive path such as `.env` or a private key, and opaque wrappers). Deny beats ask when multiple subjects match. Quoted spans are stripped before pattern matching so a quoted `>` or install word in an argument is not flagged, and program names are matched only in command position. Adding a table category is a one-line rule append in `AUTO_SHELL_RULES`.
277+
- **auto-shell-policy** — Constrains `run_shell` even when auto mode would otherwise rubber-stamp it. Before matching, `expandShellSubjects` peels `bash`/`sh`/`zsh -c`, `xargs` utility tails, and transparent prefixes (`env`, `nice`, `timeout`, …) so rules see the real payload; an unparseable wrapper (variable expansion or command substitution) sets an opaque flag that forces `ask`. Effects: `deny` blocks outright (file mutations through ad-hoc tooling — output redirection, `tee`, `sed -i`/`perl -i`, interpreter inline programs or heredocs — which must instead go through `write_file`/`edit_file`); `ask` declines to auto-allow and falls through to the operator prompt (recursive `rm`, dependency installs and remote runners: npm/yarn/pnpm/bun, pip, cargo, go, brew, npx/bunx, …, force or uncontained `git worktree` ops, shell that references a sensitive path such as `.env` or a private key, and opaque wrappers). Contained non-force `git worktree add`/`remove`/`prune` and read-only `list` auto-allow (sibling destinations like `../corbits-dispatch-wts/…` included; absolute outside, `~`, globs, and credential basenames still ask). Deny beats ask when multiple subjects match. Quoted spans are stripped before pattern matching so a quoted `>` or install word in an argument is not flagged, and program names are matched only in command position. Adding a table category is a one-line rule append in `AUTO_SHELL_RULES`.
278278
- **gate** — Evaluates a call: `skipPermissions` allows everything; `allow`-tier passes; for `ask`-tier, checks persisted approvals, otherwise requests operator approval. Shell security classifies each chain segment (`||` / `&&` / `|` / `;` / newlines), but the operator is prompted once for the full command block — any unapproved segment fails the whole block, and execution always runs the unsplit original. Safe pipeline tails and pure shell no-ops (`true` / `false` / `:` and bare control-flow keywords stranded by chain-splitting) skip without a prompt. In a non-interactive run an unresolved `ask` becomes a denial. In auto mode: non-shell built-ins in `AUTO_ALLOWED_TOOLS` (writes/edits/deletes, `manage_tasks`, `task`, …) auto-allow when not path-restricted; for `run_shell` the gate consults the auto-shell policy — a `deny` rule fails the call, an `ask` rule skips the auto-allow shortcut and proceeds to the normal approval flow, and anything unmatched is auto-allowed. Paths outside the workspace and writes under the session state root (`~/.corbits/projects/...` and legacy `.agent-state`) still ask. Mutating MCP and unknown built-ins are not blanket-allowed. Newly granted scopes are appended in memory and persisted.
279279

280280
- **matcher** — Approval pattern matching via `@intx/authz` `matchPattern` (`*` wildcards). Exact-command grants store a backslash before each metacharacter; those patterns match by equality after unescape (the package has no escape syntax).

docs/TUI.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,58 @@ goal/task/agents strips, then progress, then the prompt itself shrinks one
3636
row at a time down to its 3-row base — never the transcript
3737
(`COLLAPSE_ORDER` in `zones.ts`).
3838

39+
Horizontally, every surface sits inside one shared gutter
40+
(`resolveSideMargin`, `src/tui-opentui/geometry/margins.ts`) so the shell reads
41+
as a single column of content rather than stacked panes. The gutter is one
42+
column per side at every width that can afford it, and zero below
43+
`MARGIN_MIN_COLUMNS` (40), where every column belongs to content. There is no
44+
middle tier: one column is already enough to keep content off the frame edge,
45+
which is the gutter's entire job, and anything wider only read as excess air on
46+
a wide pane. The gutter costs no rows.
47+
3948
The prompt box's border carries the metadata that would otherwise cost a
4049
titlebar row: the model label sits right-aligned in the top rule; the brand
4150
lockup sits at the left of the bottom rule with the working directory and git
4251
branch at its right (`AppShell.promptTopRule` / `promptBottomRule`,
4352
`src/tui-opentui/shell.ts`). Both rules cost zero transcript rows because they
4453
ride the prompt box's own border.
4554

55+
While a turn is live the lockup slot swaps the wordmark for the phase word —
56+
`thinking`, `streaming 12 tok`, the running tool's name — led by a single
57+
density cell (`rampPulse`, `src/tui-opentui/ramp.ts`). The cell, not the word,
58+
is what says whether the session is healthy, and it carries four states:
59+
60+
| State | Cell | Reads as |
61+
|---|---|---|
62+
| `working` | cycles `░ ▒ ▓ █` on `RAMP_CYCLE_MS` | moving |
63+
| `done` | static `` | finished |
64+
| `blocked` | static `` | waiting on the operator |
65+
| `stalled` | `!` blinking against ``, then a static `!` | a problem |
66+
67+
Every state is separated by glyph and motion before colour, so all four survive
68+
a monochrome terminal and are readable without stopping to read the word. A
69+
static `working` word was the original failure: a live run and a hung one
70+
printed identically, so the only way to tell them apart was to wait.
71+
72+
`blocked` and `stalled` share the orange deliberately — both name a turn
73+
waiting on something outside itself — and are told apart by motion: `blocked`
74+
holds perfectly still, which is the signal that the session is waiting on *you*.
75+
76+
The stall phase is driven by the watchdog's own silence clock
77+
(`stallLevel`, `src/tui-opentui/stall-watchdog.ts`), so the indicator and the
78+
abort can never disagree about which runs are stuck. It arms at
79+
`STALL_NOTICE_MS` and keeps reading as stalled straight through the abort
80+
threshold. Its blink is a bounded burst (`STALL_BLINK_BURST_MS`) that settles
81+
to a static `!`: an alarm that strobes for the whole stall window becomes
82+
wallpaper, and settling also lets the render loop drop back to the slow
83+
cadence. The burst is measured from the moment silence crossed the notice
84+
threshold, so a resumed session with already-stale activity shows the settled
85+
glyph immediately rather than alarming about silence the operator missed, and
86+
a stall that breaks and re-arms bursts again.
87+
88+
An idle session animates nothing at all: the monitor tick stops entirely
89+
rather than repainting an unchanging frame.
90+
4691
Color is a small, deliberate palette, not decoration
4792
(`src/tui-opentui/theme.ts`). Dimmed text is a dimmed cream, never a neutral
4893
gray, so every emphasis level keeps the same warm hue. Orange

0 commit comments

Comments
 (0)