Reverts c61977c. The live listing needed no codex CLI on the user's
machine — kigi issued the request itself and `client_version` was a
compiled-in string — but the project keeps the catalog in the binary, so
the fetch path, the `OpenaiCodex` listing dialect, its parser and the
pinned client version are gone again, and the codex `User-Agent` returns
to its previous value.
The models.dev provider rename `kimi-for-coding` → `kimi-code-plan-cn`
rode in the same commit and stays: the regenerated snapshot ships under
the new id.
The ChatGPT Codex catalog was compiled in: four models read once from the
Codex CLI cache. The backend moved on — `gpt-6-astra` shipped, and
`gpt-5.3-codex-spark` left — so the picker showed a table that no longer
matched the account.
`GET https://chatgpt.com/backend-api/codex/models?client_version=<v>`
serves that table. Verified against a live ChatGPT account: the OAuth
Bearer alone returns 200, the query is required (400 without it), and the
backend gates the catalog on it — `0.104.0` answers with
`codex-auto-review` alone, `0.150.0` omits `gpt-6-astra`, `0.155.1`
serves the full set. `CODEX_CLIENT_VERSION` pins the current upstream
release (openai/codex rust-v0.155.1) and the codex `User-Agent` now
derives from it, so the two can never drift.
`ListingDialect::OpenaiCodex` + `parse_openai_codex_listing` keep the
`visibility == "list" && supported_in_api` models and carry the wire
context window and per-model reasoning levels; `hardcoded_catalog` and
its short-circuit in the fetch path are gone. The catalog advertises an
`ultra` tier kigi has no level for — `think_efforts_to_options` drops it
with a warning, as it does any unknown token.
The sync record now ends at upstream `a28ee2b` (2026-09-17, ≈v1.0.35).
The items evaluated in this window and left for a later cycle are listed
with the reason each one waits, so the next sync does not triage them
again. `pager.toml` joins that list: its writer still ends in a bare
`fs::rename`.
Two stale lines go: `ultra` left the reasoning-effort vocabulary in
649c5f5, yet the file still described it as a codex-only tier.
Every config.toml writer wrote a tmp beside the path and renamed it over
the path. With config.toml symlinked into a dotfiles repo, the rename
replaced the link with a regular file: the repo copy went stale and the
link was gone.
`config_write_dest` resolves the destination before a writer names its
tmp. For the user's own config path a symlink resolves to its referent,
so the tmp sits on the referent's filesystem and the rename stays atomic.
A dangling link is an error, not a clobber. All eight writers call it:
`save_config`, the four MCP writers, and the three import writers.
Only the user's config path is followed, and only while
`user_kigi_home()` resolves. `kigi mcp add --scope project` and the
Claude project import write `.kigi/config.toml` inside the repo; a
hostile repo could ship that file as a symlink to any file the user
owns. With no home, `kigi_home()` falls back to `./.kigi`, which is that
same project tree, so nothing is followed then. Untrusted paths come
back unchanged and the link itself is replaced, as before. AGENTS.md
records the rule.
Tests (unix; run on Linux under WSL): helper cases including the no-home
case, a project-scope save that spares the link's target, and an
end-to-end save under KIGI_SHARE_DIR. The end-to-end test fails with
following disabled. Windows symlinks were not exercised: the host lacks
the privilege.
Ported from upstream (grok-build config/fs_atomic.rs LeafSymlink::Follow,
1.0.35): the rule, not the 1375-line module.
`try_call_tool` wrapped `call_tool` in a bare `tokio::time::timeout`.
On a timeout or a cancelled turn the future dropped and the server kept
working: no `notifications/cancelled` ever left the client.
`call_tool_cancel_aware` sends the request through
`send_cancellable_request` and holds a guard armed with the request id.
A settled call disarms it. A dropped future (cancelled turn) spawns the
cancel from the guard's drop. A timeout sends the cancel inline, before
returning: the HTTP timeout arm resets the transport next, and dropping
the last service handle killed a spawned cancel before it left.
The inline send waits 500 ms at most. rmcp 2.2.0 awaits its own
timeout-cancel without a bound, and its streamable-HTTP worker awaits
each POST in turn, so against a JSON-mode server the cancel queues
behind the hung call: with rmcp's timeout path the call returned after
3.0 s against a 1 s budget. That server class cannot receive the cancel
until rmcp 3.x; the budget elapses, a warn is logged, and AGENTS.md
records the limitation. stdio and SSE-mode servers receive it.
Tests: dropped call, timed-out call followed by a service drop, settled
call; the HTTP outer-timeout test now asserts the return time.
Ported from upstream (grok-build mcp/servers.rs call_tool_cancel_aware,
1.0.33); upstream leans on rmcp 3.2's timeout path.
Sync record now ends at upstream `3794978` (2026-09-09, ≈v1.0.24). Lists
the two limitations the sync left: the Windows frame writer still prints
through std's stderr lock, and the bwrap builder re-stats /data.
Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
The 0.1.13 sync had to reconstruct the fork point by content
fingerprinting; the sync record (baseline snapshot, last-evaluated
upstream commit, and where upstream hides its per-change lists) now
lives in AGENTS.md so the next sync starts from facts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
All 62 crates inherit `workspace.package.version`, so the single edit
plus the refreshed lockfile carries the release. The `v0.1.10` tag is
what triggers the build; release.yml gates the tag against this value.
Also records the shared-SSRF-policy invariant in AGENTS.md, so a future
port does not reintroduce a second copy. The zero-egress entry is
compressed by the same number of lines to keep the file at 500.
The `agent_swarm` tool works with the mode off; what the mode adds is the
doctrine — decompose finely, give every member a disjoint scope, do not
do the work yourself. `/swarm`, `/swarm on|off`, `/swarm <task>`, gated
on the tool actually being in the toolset.
Two triggers, not upstream's three. Upstream's `tool` trigger exists so
invoking the tool makes the doctrine appear; kigi's tool carries its own
description and works with the mode off, so that trigger would add a
state only the tool can reach. `Manual` persists until switched off,
`Task` expires at turn end — both are real user intents.
Load-bearing decisions, each from a defect found in review:
- Expiry is a DROP guard, not a post-loop call. A user interrupt aborts
the turn future rather than cancelling it, so post-loop code never
runs; three `?` paths skip it too. Either way a `/swarm <task>` mode
would leak into the next, unrelated prompt.
- `enter` is a total no-op while armed, so the `/swarm <task>` shorthand
cannot downgrade a standing `/swarm on` into a per-turn mode that then
disarms itself. This matches upstream; the first cut diverged, and the
test asserting the divergence was inverted with the fix.
- An explicit `/swarm off` retracts UNCONDITIONALLY. The doctrine rides
the conversation and so survives a resume, a fork and a compaction that
an in-memory flag does not; trusting the flag left a session reading
"off" with the instruction still steering and no way to clear it. That
also retires the `reminder_live` field, whose doc comment claimed to
know something the code cannot.
- The mode is deliberately NOT persisted, unlike /goal and /graph: those
strand real work when lost, this is a prompt hint whose recovery is
retyping one command. Recorded in AGENTS.md so the omission reads as a
decision, not a gap.
- The pre-session gate is `subagents_enabled`, not a hardcoded `true`:
the builder strips `agent_swarm` wherever it strips `task`, and
advertising it then offers a menu entry that resolves to literal text.
- The exit reminder is as emphatic as the doctrine it revokes; a single
weak clause is the likelier of the two to be summarised away.
The doctrine rides the existing `push_system_reminder` channel rather
than a second injection path of its own.
Since v0.1.7 — chat-completions dialect correctness (Pi-referenced):
- fix(chat): BYOK/custom endpoints default to Passthrough (vanilla
OpenAI semantics; Kimi body mutations no longer leak to third-party
servers); house/Kimi coding endpoints keep the Kimi dialect via
base-url detection
- fix(chat): dedicated Mistral dialect — exactly-nine-alphanumeric
tool-call id normalizer (deterministic, call/result-symmetric)
- fix(chat): tool-result images relocate to a batched user message
(tool messages are text-only on the OpenAI chat wire)
- docs: AGENTS.md records the dialect-selection contract
Since v0.1.6 — the cross-provider replay audit (Pi transform-messages
policy: every wire builder emits only items valid for its target):
- fix(responses): reasoning items without a native rs_* id dropped (the
GPT/codex 400 Invalid 'input[N].id'); provenance gate drops foreign
reasoning and demotes foreign backend tool calls on model switches
- fix(codex): bare rs_* reasoning references dropped (stateless backend)
- fix(wire): shared ASCII tool-call id sanitizer, symmetric call+result
on both the Messages and Responses legs
- fix(messages): image-source whitelist (raster base64 only, http(s)
urls only) and empty-user-content guard
- fix(conversation): char-boundary-safe code preview (CJK/emoji code
panicked every request build)
- docs: AGENTS.md records the replay policy
Since v0.1.5:
- fix(messages): Claude models no longer 400 with 'Invalid signature in
thinking block' — thinking is replayed only for the active tool loop
and only when genuinely signed (cross-backend/cross-model histories
are stripped)
- fix(codex): ChatGPT Codex no longer 400s with 'System messages are
not allowed' — system prompts ride the instructions field and
encrypted reasoning is requested for stateless replay
- docs: AGENTS.md records both wire contracts
resolve_graph() read only the KIGI_GRAPH env var with default(false)
(plan.md G0 gate), so /graph existed solely on machines whose environment
exported the dev flag — which presented as '/graph is missing on
Windows'. There was never any platform-conditional code: the Mac worked
because the dev env var was set there.
Default is now true (matching /goal's shipped state); KIGI_GRAPH=0
remains the off-switch, and availability still requires the goal harness
(BuiltinGate::Graph). AGENTS.md updated; new test pins fresh-install-on
plus env-zero-off.
Verified: kigi-shell 5262 + kigi-tui 6874 tests green, clippy clean.
Since v0.1.4:
- fix(fs): Windows-safe atomic replace everywhere (util::fs::replace_file)
— model+effort switches now persist on Windows; models cache and session
state writes no longer fail silently under AV/indexer file locks
- fix(tui): a default-model persist failure keeps the live session's model
instead of reverting the pick
- docs: AGENTS.md records the replace_file contract
One authority answers 'which credential may ride this request':
credential_class / manager_for / credential_for / bearer_resolver_for,
keyed by (platform, base_url). SessionCredential is an opaque type with
no production constructor, so a new call site cannot re-introduce the
session-bearer leak. Platform-scoped tests extended across all bearer
channels (session, aux, summary, subagent override).
Verified: cargo check --workspace --all-targets clean; kigi-shell and
kigi-tui suites green (6611+ tests).
29th platform `openai-codex` (uses_oauth, Responses wire). PKCE-localhost login at
auth.openai.com (client app_EMoamEEZ73f0CkXaXp7hrann, redirect localhost:1455/auth/
callback, form token exchange, fresh-random state) reusing the claude-pro-max flow;
OAuthFlow::PkceLocalhost gained a redirect_path and OAuthConfig an authorize_extra
(empty elsewhere, so claude/xai/copilot authorize URLs stay byte-identical).
Codex-specific: the access token is a JWT carrying chatgpt_account_id, which becomes
the `chatgpt-account-id` inference header. It is derived STATELESSLY from whichever
bearer rides each request (so a rotated token needs no persisted field), and BOTH
login and refresh fail fast when the claim is absent — gated on the explicit
OAuthConfig.requires_chatgpt_account_id fact, never inferred from the token-body
encoding (a plain form endpoint is the OAuth norm and must not inherit this).
Inference rides the existing Responses wire at chatgpt.com/backend-api/codex →
/responses, with codex headers (chatgpt-account-id, originator, OpenAI-Beta
responses=experimental, codex UA) gated on SamplerConfig.openai_codex so API-key
`openai` stays byte-identical; store:false was already the global Responses default.
Catalog is HARDCODED (no live endpoint exists for this backend; read from the
official Codex CLI's model cache): gpt-5.6-sol/terra/luna + gpt-5.5, ctx 272000,
each with its real reasoning levels (low..ultra — ReasoningEffort gained Ultra).
Excluded: gpt-5.3-codex-spark (supported_in_api=false), gpt-5.4/-mini and
codex-auto-review (hidden) — they would list but fail at inference. The fetch
short-circuits before any HTTP; Kigi never shells out to the codex CLI or reads
~/.codex.
Security review fixes: redact any `account-id` header from request logs (it was
reaching debug logs), strict 3-segment JWT check (fail closed), refresh no longer
fails open on a missing claim. Inherits leak-safe pooled routing (scope
oauth/openai-codex) — never the Kimi token. Full gate green (234 suites, 0 warnings).
28th platform `github-copilot` (uses_oauth, ChatCompletions wire). Two-stage auth:
RFC-8628 GitHub device flow (client Iv1.b507a08c87ecfe98, scope read:user, errors
in a 200 body) mints the DURABLE github token; a GET api.github.com/copilot_internal/
v2/token exchange re-mints the SHORT-LIVED copilot token. Persisted as key=copilot
token, refresh_token=github token, expires_at=copilot expiry; the "refresh" is a
copilot-token re-mint (not a refresh_token grant), dispatched via
OAuthTokenBody::GithubCopilotExchange in the generic refresher.
VS Code editor-identity headers on /models + /chat/completions, gated on
SamplerConfig.github_copilot / PlatformId::sends_copilot_editor_headers() so every
other ChatCompletions provider stays byte-identical. Live /models filtered
(parse_github_copilot_listing) to the openai-completions-served models: keep iff
model_picker_enabled && policy.state!="disabled" && tool_calls!=false AND not a
claude-4.x/5.x (messages) or gpt-5/oswe/mai- (responses-only) id — those need
per-model wire routing (documented debt), excluded rather than mis-routed.
Inherits the leak-safe pooled routing (scope oauth/github-copilot); its bearer/
refresh/api_key never touch the Kimi token (regression test added). Fail-fast on
an out-of-range copilot expires_at (would otherwise silently 401 mid-session).
Adversarial security review: GO, no CRITICAL/HIGH. Known limitation: Pi's
per-model policy-enablement POST is not ported (documented in AGENTS.md).
27th registry variant, 2nd subscription-OAuth provider. Log in with a Claude
Pro/Max subscription via PKCE authorization-code + S256 (loopback callback on
127.0.0.1:53692, with a manual code-paste fallback), then use it against
api.anthropic.com — reusing the existing Anthropic Messages wire + Anthropic
listing + the multi-provider OAuth foundation (dbce6bf). Sourced from Pi
(earendil-works/pi auth/oauth/anthropic.ts): client 9d1c250a..., authorize
claude.ai/oauth/authorize, token platform.claude.com/v1/oauth/token, scope
'…user:inference user:sessions:claude_code…'.
New machinery (foundation handles token routing — claude-pro-max is a
uses_oauth platform so its bearer/refresh/api_key already route to its own
pooled manager, never Kimi):
- OAuthConfig gains flow{DeviceCode|PkceLocalhost} + token_host + token_body
{Form|JSON}; xai/kimi rows unchanged (DeviceCode/Form).
- auth/oauth_pkce.rs: PKCE S256 wire — loopback listener with STRICT state
validation (CSRF, fail-closed), manual-paste fallback, JSON code→token
exchange + rotating-refresh. Never logs code/verifier/tokens.
- Messages OAuth adaptation gated on SamplerConfig.anthropic_oauth (true only
for a claude-pro-max managed key): Authorization: Bearer + anthropic-beta
oauth + user-agent claude-cli + x-app cli, and the required 'You are Claude
Code' system prefix. API-key anthropic/minimax Messages requests are
BYTE-IDENTICAL (regression-guarded).
- Live /models under the OAuth Bearer + oauth-beta headers (Anthropic listing,
enriched from models.dev anthropic); persistent 401 → 0 models + WARN, NO
hardcoded fallback list (honest failure).
Adversarial review: no blocking findings (secret handling, CSRF/state, the
anthropic_oauth gate, token routing, non-regression all CONFIRMED). Full gate
green. Registry at 27; picker updated. Residual (unverifiable without a real
Claude Pro/Max account): whether GET /v1/models accepts the OAuth bearer, and
the real endpoint's acceptance of the OAuth Messages request.
First subscription-OAuth provider beyond Kimi Code (26th registry variant).
Log in with a Grok/SuperGrok/X subscription via RFC-8628 device-code OAuth
(auth.x.ai), then use it against api.x.ai/v1 — reusing the existing xai wire
(ChatCompletions + OpenAI listing + Passthrough + restrict + models_dev_id
xai). Sourced from Pi (earendil-works/pi auth/oauth/xai.ts): client
b1a00492..., scope 'openid profile email offline_access grok-cli:access
api:access', standard Bearer (no x-xai-token-auth).
Foundation (generalizes Kigi's Kimi-singleton OAuth to per-provider, root
cause, not a patch):
- Registry: OAuthConfig on PlatformSpec (client_id/host/device+token
paths/scope/scope_key); XAI_OAUTH_CONFIG + XAI_GROK_SPEC (uses_oauth, method
id 'xai-grok', an interactive login after kimi-code).
- Generic device-code wire (auth/oauth_device.rs) + GenericDeviceRefresher,
sharing the RFC-8628 core with Kimi; Kimi's bespoke flow is byte-identical
(X-Msh headers, KIMI_CODE_OAUTH_SCOPE, keyring gating unchanged).
- Per-provider AuthManager via a process-global pool (auth/oauth_registry.rs):
build-on-demand with start_proactive_refresh, keyed by scope. The session
resolves the AuthManager for the ACTIVE model's platform for bearer/refresh/
401-recovery/api_key — an oauth-platform model always uses its OWN token,
never the primary.
- Live /models under OAuth; base routes oauth().is_some() -> platform.base_url()
(kimi-code stays on proxy_url).
Security: adversarial review + a systematic token-leak audit found and closed
FIVE channels where the primary Kimi token could reach api.x.ai (bearer
resolver, api_key stamping, aux summary/classifier/image-describe models, and
subagent model-override). Each fix routes through the platform-aware resolver
(the oauth model's pooled token or None, NEVER the primary) and is revert-to-red
verified. No access/refresh token is ever logged.
Registry at 26; picker updated (xai-grok interactive login row); TUI
context-window already auto-updates per model. Full gate green (234 suites,
fmt, clippy -D warnings, deny). GPT/Claude/Grok officially permit third-party
subscription use.
Provider /models listings that return bare ids (OpenAI-style) get context
windows, thinking levels, image support, and display names from models.dev:
kigi-models owns the transform (parse_api_json — ONE field interpretation
for the bundled snapshot AND runtime refreshes), enrich_wire_model fills
gaps with wire values always winning and model availability strictly
wire-truth. Spec rows gained models_dev_id + wire_serves_metadata; all
three current platforms are wire-served, so this pipeline is provably
inert for them (byte-identical catalogs, zero egress, zero ~/.kigi writes
— adversarially verified).
Shell side: enrichment_fetch with a 24h disk cache guarded by binary
version + keep-set + future-stamp sanity (a registry change or downgrade
refetches instead of serving a catalog missing new providers), refresh of
https://models.dev/api.json filtered to registry ids, KIGI_MODELS_DEV_URL
override with case/whitespace-tolerant kill switch, fallback chain
fresh-cache > refresh > stale-cache > bundled (each step logged). The
fast path returns an empty catalog without forcing the bundled parse.
From the review: blast-radius-confined parsing (one drifted provider on
models.dev warn-skips instead of failing the whole refresh), registry-
coverage and field-coverage tests guarding script/parser drift, a path-
injectable core with 8 state-machine tests (one of which caught a guard
patch that had failed to apply), _meta provenance stamp in the snapshot,
and models.dev (MIT) attribution in NOTICE. Snapshot: 29 providers, 1124
models, 246KB, regenerated by scripts/gen_enrichment_snapshot.py (pure
filter, no transform).
Platform API keys now live in auth.json under the platform-id scope (the
per-provider auth.json key contract), resolved env > auth.json > legacy
[platforms.*] config.toml (read-only fallback). The TUI login picker,
paste box, auth-method advertising, and authenticate handler are all
registry-generic: a new PlatformSpec row appears in the login UI and
authenticates with zero UI changes. Spec rows gained vendor/console_host/
login_label display fields (moonshot strings byte-identical, pinned by
tests).
Adversarial review caught that auth.json keys were validated at login but
never stamped onto catalog entries (completions would 401; restart lost
eager auth). Fixed red-green: resolve_model_list/resolve_model_catalog now
take a resolved PlatformApiKeys snapshot consumed by the credential-
stamping layer (auth.json beats stale config.toml, matching the login
validator), with production callers resolving fresh per catalog build.
Also from review: the new auth.json writer takes the manager's cross-
process flock (bounded retry — an unlocked RMW racing a token refresh
could revert a rotated refresh token); the oauth-401 wiremock test is
hermetic (KIGI_SHARE_DIR tempdir; it could read a dev's real auth.json
and hit live moonshot); cli_models resolves real keys; auth.json is read
once per registry sweep; caller-less lock_config_writes deleted; catalog
resolvers tightened to pub(crate); stale config.toml doc comments and the
no-credentials error copy updated.
A restricted optimizer pass now reviews the graph at plan boundaries —
right after initial planning and piggybacked on each replan version
boundary, never mid-execution. An optimizer subagent may emit four ops
over Waiting/Ready nodes only: remove_dep (delete a false dependency,
restoring parallelism — the highest-value edit), reorder (pending
priority for the serial scheduler), merge (fold two tiny nodes; specs
concatenate, deps union, dependents re-point, absorbed self-deps drop),
and split (2-3 focused replacements inheriting the original's deps and
dependents). The optimizer changes graph DATA only; the executor stays
pure deterministic Rust. KIGI_GRAPH_OPTIMIZER=0 disables it entirely.
apply_optimization enforces the contract twice: per-op checks
(pending-only targets, known ids, terminal node untouchable, dead-node
deps rejected as DeadDep, merge/split targets with non-pending
dependents rejected with the true reason instead of tripping the
immutable invariant later), then FINAL invariants — every non-pending
node byte-identical in the result, the gn-final gate rebuilt over all
survivors, node cap, whole-graph acyclicity, and a BIDIRECTIONAL status
re-derivation for pending nodes (adversarial review caught the critical
hole: a merge grafting unsatisfied deps onto a Ready node would
otherwise dispatch it ahead of its new prerequisites, since
recompute_ready is promote-only). Applied passes bump plan_version,
freeze an immutable baseline, and consume a slot of the SHARED replan
cap; an explicit {"ops": []} is a respected free no-op; any failure
degrades to keeping the current plan. Plumbing reuses a new shared
artifact-pass runner (stale-artifact delete, size cap, missing-file
fail-closed) extracted from the replanner.
Tests: remove_dep parallelism restore + loud no-such-dep, immutable and
terminal-node rejections across all four ops, merge/split dependent
rewiring incl. final-gate rebuild and intra-split dep resolution,
result-cycle rejection, dead-dep splits, Ready-demote-on-merge, and
three e2e flows — false-dep removal proven ACTUALLY parallel by the
held-reply fan-out gate, OPTIMIZER=0 spawning zero passes, and the
shared-cap guard. kigi-shell 4959 lib tests green; clippy clean.
Workers, verifiers, and serial node goals can now surface out-of-scope work
as line-anchored 'DISCOVERED: <text>' markers (fence-stripped and
placeholder-filtered — the templates' own examples are fenced so verbatim
echoes never parse; worker summaries embedded in verifier prompts get the
marker neutralized alongside NODE_RESULT/NODE_VERDICT). Discoveries queue on
the orchestration as persisted state and fold into the graph at dispatch
boundaries: a replanner subagent produces a strictly APPEND-ONLY appendix,
validated against the live graph (existing-id deps allowed; edges onto
gn-final rejected — they would cycle the moment the final-gating extension
lands; Blocks deps on Failed/Blocked nodes rejected as DeadDep so the
attempt-2 feedback loop repairs the artifact). Installing an appendix bumps
plan_version, freezes an immutable graph.baseline.v{N}.json next to the
prior versions, extends gn-final's gate (demoting a Ready final back to
Waiting), and recomputes readiness.
DiscoveredFrom edges are audit metadata, never scheduling gates: an origin
is always terminal at replan time, so gating on it is either a no-op or a
permanent wedge — and a failed node's discoveries are still real work.
Replanning is bounded by KIGI_GRAPH_REPLAN_CAP (default 3; 0 disables it
quietly): past the cap, after the final node has achieved, or on replan
failure, discoveries drain to history only — a working graph is never
paused for a failed enhancement pass, and it always converges. The budget
gate now precedes the replan boundary (a budget-dead graph keeps its
discoveries queued for a later --budget top-up instead of spending two
replanner runs first), and both planner runners delete stale artifacts
before spawning so a child that responds without writing can never get a
previous pass's file validated as its own output.
Tests: validate_replan unit coverage (existing-id resolution,
DiscoveredFrom dedup, collisions, dead deps vs dead origins, terminal-node
edges, combined-graph cycles), tracker appendix/regate/audit-edge tests,
and two e2e flows — discovery → replan → appended node runs to Achieved
with both baselines frozen, and cap-0 draining to history while the graph
still converges. kigi-shell 4935 lib tests green; workspace clippy clean.
BudgetLimited is now a resumable state: a budget trip demotes in-flight
nodes to Ready (a resource stop, not a verdict — no forever-Running node is
ever persisted) and '/graph resume --budget <tokens>' re-arms the graph with
fresh headroom (new budget = spent-so-far + extra). The tripped node's
partial burn is charged into tokens_spent_nodes at BOTH cascade sites before
the demotion clears current_node, so the top-up arithmetic never runs on an
under-counted ledger. Any input starting with 'resume' resolves to a resume
(case-insensitive; malformed top-ups surface the usage hint) and setup_graph
refuses to replace any non-Complete graph — a typo can no longer silently
destroy a resumable graph. An explicit --budget on a merely-paused graph is
rejected loudly instead of silently discarded; all trip-time messages now
advertise the top-up.
The pager gains a graph status chip: a new GraphUpdated wire variant
(extensions/notification.rs, old pagers degrade via #[serde(other)]) is
emitted from the single persist_graph_state chokepoint — every transition is
both a checkpoint and a badge tick — with a 'cleared' sentinel on /graph
clear and a one-shot re-emit after session restore (the replayed updates log
otherwise shows the pre-shutdown Active state that from_snapshot just
demoted in memory). TUI side: GraphDisplayState, session-notification arm,
and a goal-idiom chip with node progress, clamped current-node title, and
budget-aware spend. Pre-session command availability now advertises /graph
from the flags (it was fail-closed to the in-session path only, so the
welcome-screen slash menu never showed it).
Coverage: GraphUpdated wire round-trip + minimal-payload + unknown-tag
tests; PTY scenarios graph_slash_presession{,_disabled}.yaml (both run
green against the real pager binary); handle_prompt-level e2e for terminal
slash outcomes (/graph status|resume|pause, /goal refusals while the graph
owns the engine); budget top-up e2e driving a BudgetLimited diamond back to
Complete. Not shimmed: pre-G2 persisted snapshots with budget-Failed nodes
(the KIGI_GRAPH flag has never shipped enabled, so none exist).
kigi-shell 4927 and kigi-tui 6610 lib tests green; workspace clippy clean.
With KIGI_GRAPH_CONCURRENCY > 1 (default 3, clamp [1,8]) and >=2 Ready nodes,
drive_graph — the single dispatch loop shared by setup/advance/resume — runs
parallel batches: each node executes as a bounded worker<->verifier subagent
loop (KIGI_GRAPH_NODE_ROUNDS, default 3; general-purpose children with the
full implementer toolset; worktree isolation on round 1, resume keeps context
and worktree on later rounds; NODE_RESULT/NODE_VERDICT terminal contracts
parsed fail-closed with fence-stripping and line anchoring). Achieved nodes
merge back SEQUENTIALLY via kigi_workspace apply_worktree in Merge mode; a
conflict fails the node, block_dependents fires, and surviving chains keep
going. gn-final always runs serially on the full goal engine. Concurrency=1
is byte-identical to the serial G0 path; non-git projects degrade to serial.
Merge primitive hardened for real use (kigi-workspace): the 3-way apply is
now byte-safe (binary files no longer read as UTF-8 and silently deleted)
and gains the identical-content rule (ours==theirs => already present, not a
conflict) — without it, any dirty file inherited via PreserveWorkingTree
false-conflicted every node merge and multi-wave graphs self-poisoned.
Adversarial review pass (16 confirmed findings, all fixed): in-session
resume demotes orphaned Running nodes instead of wedge-pausing forever after
a mid-batch Esc; cancel re-sweeps subagents AFTER the turn abort so workers
spawned in the cancel window die too; empty child ids are never adopted as
resume targets (no unisolated escape to the shared tree); a successful
isolated round returning no worktree fails the node (soft-fallback can no
longer put N writers in one tree); main-HEAD movement during a batch aborts
merges instead of reverse-applying external commits; failed nodes still
charge the token budget; budget trips terminally fail the in-flight node;
runaway (>600s) rounds are cancelled by spawn id and retried via resume;
worker summaries are marker-sanitized before verifier embedding. Merged
worktrees are removed immediately (storage discipline); failed nodes keep
theirs for postmortem.
Tests: 4922 kigi-shell lib tests green (58 graph-specific), including
fan-out proven by a held-reply gate, real-git batch merge with cleanup
assertions, budget charging across verdicts, cap trimming, resume-after-
cancelled-batch, and backgrounded-round cancel semantics.
A deterministic DAG scheduler layered on the existing goal engine: /graph
<objective> decomposes the objective via a graph-planner subagent, gates the
result through Agentproof-style static validation (cycles, unknown deps,
duplicate slugs, caps), appends a structural final-verification node, then
executes each node as one ordinary goal — planner, worker loop, adversarial
verifier, budget and pause machinery all reused verbatim. The in-turn loop
advances nodes within the same turn (multi-loop closed loop); goal-side
auto-pauses cascade to the graph at a single chokepoint; node goals are armed
with the remaining graph budget so mid-node overruns trip graph-wide.
Gated by KIGI_GRAPH=1 (default off) + the goal harness. State persists to
<session_dir>/graph/state.json with a clear-tombstone; per-version immutable
baselines and per-node artifact archives live under graph/<graph_id>/.
Restore demotes Active->UserPaused and Running->Ready (verifier-gated re-run).
Review hardening (adversarial multi-agent pass, 24 confirmed findings fixed):
the goal-inactive loop break now consults the graph seam (mid-turn
classifier-disabled completions can no longer strand an Active graph), the
pause cascade fires even when no node goal is in flight (cancel during
planning), node bookkeeping precedes the long setup await, /graph clear only
resets the engine it owns, budget trips terminally fail the in-flight node,
and the pause transitions in /goal pause + /graph pause no longer hide inside
debug_assert! (a release-build no-op inherited from upstream).
Tests: 4908 kigi-shell lib tests green, including a serial 4-node closed-loop
e2e, restore/resume, cascade, mutual-exclusion, planning-retry, persistence
round-trip + tombstone, and status rendering.