Skip to content

chore: merge release v1.17.0 to main - #5429

Merged
trek-e merged 70 commits into
mainfrom
release/1.17.0
Oct 14, 2026
Merged

trek-e merged 70 commits into
mainfrom
release/1.17.0

Conversation

@trek-e

@trek-e trek-e commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Merge release branch back to main after v1.17.0 stable release.

github-actions Bot and others added 30 commits October 5, 2026 02:13
chore: sync next package version to 1.16.0
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ent-planning-references

docs(#5237): record persistent planning references as out-of-scope
* fix(#4839): keep update hook advisory on launch failure

* chore: link changeset to PR 5159

* test: cover update hook success path (#4839)

---------

Co-authored-by: agammann <agammann@users.noreply.github.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
)

* fix(#5084): scan config text in the anchor's separator projection

configuredEntrypointsIn anchored its match on a posix-normalized configDir
but only undid JSON's backslash doubling in the scanned text, so a
registration spelled with Windows-native backslashes never matched and
the dangling-hook check skipped it silently. The text now goes through
the same posixNormalize projection as the anchor, after the collapse, so
a UNC home keeps the anchor's //server form.

Test L uses a Windows-shaped configDir literal, so it fails on every host;
I/J/K only see the bug on Windows, where this suite does not run in CI.

* fix(#5084): preserve literal UNC registrations alongside escaped paths

* test(#5084): add fast-check property for configured entrypoint spellings

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
…ME (#5093)

* fix(#5083): check npm cache isolation against the injected sandbox HOME

Test 2 of the bug-131 suite asserted the npm cache dir does not start with
os.homedir(). On Windows os.tmpdir() is %LOCALAPPDATA%\Temp, inside the
user profile, so a correctly sandboxed cache failed that check.

The child now reports the sandbox HOME its helpers.cjs injected, and
isIsolatedNpmCache() asserts isolation directly: the cache sits inside
that sandbox, and the sandbox neither is nor contains the caller's HOME.
Test 5 runs the check over Windows-shaped paths through path.win32, so it
holds on the Linux CI hosts that run this suite. Test 3 is unchanged.

* test(#5083): prove cache isolation boundaries and nested-home npm behavior

* test(#5083): isolate caller-home case equivalence in the negative fixture

* test(#5083): give each isolation test its own heading and normalize matrix row style

Test 5 (real runNpm subprocess with tmpdir inside HOME) and Test 6
(isIsolatedNpmCache path matrix) each get one heading; the duplicated
comment sentences are gone. The 13 JSON-style matrix rows now use the
file's single-quoted JS literals; case values are unchanged.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
…mutation check (#4935)

* feat(#4866): add the StateWriteIntent verifying executor and bounded-mutation check

ADR-4629 migration step (2), epic #4629 child C2. Adds applyStateWriteIntent,
which applies a declared StateWriteIntent through the owned seam
(syncAndPreserveStateMd) and fails loud instead of reporting success:

- §8.2: every required assertion is verified against the re-read file; a
  missed or deleted target is a loud failure, never a success payload.
- §8.3: the write's delta must stay inside its declared scope. Narrow scope
  bounds frontmatter and body; broad scope admits the whole frontmatter and
  keeps the body bounded; rebuild intents are unbounded by contract.

reconcileReportedFields' private valueOf/changed closures are lifted to
readStateTarget/stateTargetChanged so the verifier reuses the reporter's
rule (ADR-4629 Decision 4), and the reporter learns whole-section targets.
StateFieldAssertion gains optional target ('field' | 'section') and value.

No behavior change: no production caller constructs a StateWriteIntent yet
(C3+ migrates callers), so every existing state verb is byte-identical.

Claude-Session: https://claude.ai/code/session_01HUs8yE76QPR5ye8cFEwT1o

* chore(#4866): add the changeset fragment for the C2 verifying executor

Type Changed, docs-exempt (internal capability, no caller migrated, same
posture as C1 #4676). pr: set to the number this PR receives; corrected in a
follow-up commit if it differs.

Claude-Session: https://claude.ai/code/session_01HUs8yE76QPR5ye8cFEwT1o

* feat(#4866): bound declared() to schema leaves and add §8.3 property tests

Addresses the #4935 review:
- declared() now covers a key named exactly, or a schema-declared leaf
  (declaredLeavesOf) of a declared parent. A top-level key that merely
  contains a dot is no longer covered by its prefix. Documented in the
  verifyStateWriteIntent docstring and pinned by an example test.
- Five fast-check properties for the §8.3 body diff/replay contract
  (clean verify, stray section, same-section sibling, repeated label,
  CRLF on either side). Inputs are built by an independent line setter,
  not the seam writer the verifier replays with.
- Changeset type Changed -> Added.

* fix(#4866): reject an unknown requirement or scope when constructing a StateWriteIntent

ADR-4629 §8.2 judges only 'required' assertions, so a mistyped requirement
('Required', 'mandatory') was silently skipped and a required assertion was
never verified (fail-open). createStateWriteIntent now rejects any requirement
other than 'required' / 'best-effort' (STATE_WRITE_INTENT_REQUIREMENT_INVALID)
and any scope other than 'narrow' / 'broad' (STATE_WRITE_INTENT_SCOPE_INVALID),
the same construction-time posture as STATE_WRITE_INTENT_TARGET_INVALID.

* fix(#4866): a declared section covers its first occurrence only, not a repeated heading

verifyStateWriteIntent cleared every changed body unit whose heading lineage
contained a declared section name, so with two sections of the same name,
declaring one put a stray edit to the other (and to its children) in scope.
The field path already resolves a first-match owner unit (stateFieldOwnerUnit);
the section path now does the same: stateSectionScopeUnits resolves the
declared name to its first occurrence, the one readStateTarget reads, plus the
units nested under it by heading level. The now-unused lineage field is
removed.

* docs(#4866): scope the refused-before-write claim; test the reread_mismatch arm

applyStateWriteIntent's doc comment said a violation is refused before the
write, but its reread_mismatch arm is found after the write and returns
written: true with no restore. The comment and the written field's doc now
state that this is the one post-write failure, that the file is not restored
there, and why outOfScope / missedRequired are empty on it. Adds the arm's
first test: a forced re-read mismatch returns ok=false, written=true, an empty
updated[], and the verified content stays on disk.

* chore(#4866): changeset type Changed with a bold-lead body, matching C1 (#4676)

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
* fix(#5054): retire the unscoped Bash(npx gsd-core *) allow rule

The Claude installer pre-authorized `npx gsd-core *`, but the project
ships as `@opengsd/gsd-core` and the unscoped npm name belongs to a
different owner. The rule let Claude Code run whatever is published
under that name without asking. GSD itself never runs the bare form.

Move the rule from GSD_CLAUDE_ALLOW_PERMISSIONS to
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS, so the existing #2278 retirement
filter removes it on the next install or upgrade (byte-equal only) and
uninstall keeps cleaning it up. GEMINI.md now gives the scoped
`npx @opengsd/gsd-core@latest --antigravity --global` command, and the
USER-GUIDE permission list drops the rule and explains the retirement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(#5054): cover the local upgrade path and tighten the doc claim

Add a --claude --local upgrade row: a pre-#5054 settings.local.json
carrying the unscoped rule loses it and keeps the user's own rule. It
fails on the base installer. The guards also catch `--yes`, and the
USER-GUIDE note now says which entries are kept instead of promising
that every other permission is untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(#5054): add changeset

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(#5054): note the npx rule retirement in the installer CONTEXT entry

GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS now also retires the unscoped
`Bash(npx gsd-core *)` rule; the Installer Module entry said it covered
only the #2278 Write->Edit retirement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
…(Phase 0) (#5341)

Records the architecture decisions for epic #5331: OpenCode 2.x as a
version-gated target of the existing `opencode` runtime, version detection
via `opencode --version`, a dual-export plugin, version-aware tool-name and
CLI-flag mapping, migration record 011, complete uninstall, and a sunset of
OpenCode 1.x that keeps the shared V1 path for Kilo.

Every claim cites official OpenCode/Kilo documentation, a reproduction run
(E1-E11) or a repository path:line; eight questions that only a real V2 run
with a provider can answer are recorded as undecided. Docs-only: no src/,
bin/, tests/ or gsd-core/ change. ADR index regenerated.


Claude-Session: https://claude.ai/code/session_01JRt8GZ7KFqqg9x4XfgAByi

Co-authored-by: Claude <noreply@anthropic.com>
…d 2.x-safe spawn args

Phase 1 of epic #5331 (ADR-5331 D2, D5, D8).

- src/opencode-version.cts: bounded argv-only 'opencode --version' probe,
  2.x-only config signals, GSD_OPENCODE_GENERATION override, memoized
  detection, and spawn-time adapters (no --dir / --variant on 2.x).
- gsd-tools: worktree dispatch binds the cwd through the process on 2.x;
  the reviewer lane carries effort as a #variant model suffix on 2.x.
- Plugin: dual export (server for 1.x, setup for 2.x) reusing the existing
  guards; shell/patch tool ids mapped; Kilo mirror stays 1.x via markers.
- Plugin hooks now spawn a real JS runtime instead of process.execPath, which
  is the OpenCode binary on both generations (guards were silently skipped).
- Installer prints the detected OpenCode generation.
- Tests: unit, plugin adapter, and an opt-in real-binary smoke.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRt8GZ7KFqqg9x4XfgAByi
…d changeset

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRt8GZ7KFqqg9x4XfgAByi
… conformance tier

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRt8GZ7KFqqg9x4XfgAByi
…ents, 2.x reviewer plan

- Record the OpenCode generation in gsd-install-state.json; update-context reports it.
- 2.x plugin: compaction breadcrumb, shell env, config reload derived from edit tools.
- 2.x reviewer plan: drop the stdin '-' marker (sent to the model as text) and run --standalone.
- Document $PWD binding for worktree dispatch, descriptor reconciliation, and ADR-5331 amendment (E12-E17).
- Smoke tests now cover the real reviewer lane and cwd binding; 1.18.29 verified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRt8GZ7KFqqg9x4XfgAByi
… config edits and complete uninstall

Phase 2 of epic #5331 (ADR-5331 D4, D5, D6, D8).

- Config: comment-preserving JSONC editing for every installer write, migration and uninstall of
  opencode/kilo config (#5334); the GSD MCP entry registers --no-prompts so OpenCode stops listing each GSD
  command twice; migration 011 converges existing entries in both shapes.
- Agents: a tools allowlist / disallowedTools become a permission block denying the omitted tools (valid on
  1.x and 2.x); effort sync handles the 2.x model#variant form.
- Installer: reads the OpenCode data-dir schema and warns when 1.x is installed over a directory 2.x created.
- Uninstall: removes every staged hooks/lib helper (derived from what install copies), the install state and
  journal records, the GSD mcp entry in both shapes (#5333), an empty settings.json, empty staging
  directories, and the ~/.gsd defaults the install set (#5335); round-trip test on 1.x and 2.x.
- MCP server: --no-prompts withholds the prompt catalog.
- Docs, ADR-5331 amendment (E18-E25), tests, and a lint rule fix so node:sqlite counts as a builtin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
lint-docs-guard-registration reads a docs/ path in the comment as a docs read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…atusline, 1.x deprecation

- Run the feature verification matrix on real OpenCode 2.0.27 and 1.18.29
  (tests/opencode-matrix.smoke.test.cjs, 51 rows) and record it in
  docs/reference/opencode-v2-verification-matrix.md.
- Fix what the matrix found: a 2.x run emits no session.created so the
  SessionStart scripts never ran; hook advisories never reached the model on
  either generation (now appended to the tool result).
- Wire the opt-in community hooks (commit validation, phase boundary, session
  state) and graphify auto-update into the OpenCode plugin, outside the Kilo
  mirror.
- Add the OpenCode 2.x terminal-client statusline plugin
  (hostBehaviors.nativeCliPlugin).
- Print the OpenCode 1.x deprecation notice (bug fixes only until 2026-12-31).
- Descriptor: dispatch.nested false, maxDepth 1.
- ADR-5331 Phase 3 amendment (E26-E34), ADR-1239 dated note, upgrade how-to.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude and others added 23 commits October 13, 2026 20:37
… the next phase's disk state

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…kills/

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ry slug (#5363)

* test(#4854): init verbs emit the ROADMAP display name, not the directory slug

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#4854): init verbs emit the ROADMAP display name, not the directory slug

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(#4854): changeset fragment

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
…5364)

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ts, and the full window when auto-compact is off (#5252)

* fix(#4844): skip the auto-compact buffer when autoCompactEnabled is false

The context meter always subtracted the ~16.5% auto-compact buffer
(or the CLAUDE_CODE_AUTO_COMPACT_WINDOW share) from `remaining` and showed
used% against the "usable" range. With `"autoCompactEnabled": false` in
settings.json that buffer is ordinary context: raw 40% read as 48%, raw
83.5% pinned the bar at 100%, and the last sixth of the window was invisible.

isAutoCompactDisabled(dir) reads, fail-soft and first-definitive-wins,
DISABLE_AUTOCOMPACT / CLAUDE_CODE_DISABLE_AUTO_COMPACT, then
<project>/.claude/settings.local.json, settings.json, then
(CLAUDE_CONFIG_DIR || ~/.claude)/settings.local.json, settings.json for the
boolean `autoCompactEnabled`; when disabled the buffer is 0 and the bar
shows round(100 - remaining). The bridge file's used_pct was already raw.

Tests: autoCompactEnabled:false / env / precedence cases; the #2219 and
developer's real settings.json cannot steer the expected percentages.

* chore(#4844): backfill the changeset fragment with PR #4959

* fix(#4844): read the env vars and the payload field Claude Code actually has

Review found both names in this PR were invented, which meant the gate could
never fire for a real user — #4844's symptom reached another way.

- The env vars are `DISABLE_AUTO_COMPACT` and `DISABLE_COMPACT` (the latter
  switches off manual /compact as well). `DISABLE_AUTOCOMPACT` and
  `CLAUDE_CODE_DISABLE_AUTO_COMPACT` do not exist: 0 occurrences in the
  shipped 2.1.282 binary, absent from code.claude.com/docs/en/env-vars.
  Truthy is `1` / `true` / `yes` / `on`, trimmed and lowercased — Claude
  Code's own accepted set, which this PR's `/^(1|true|yes)$/i` was missing
  `on` from. Precedence is unchanged and matches the host's own resolution:
  either env var wins, and only then does `autoCompactEnabled` decide.
- `context_window.total_tokens` is not a statusline payload field; the
  documented name is `context_window.context_window_size`
  (code.claude.com/docs/en/statusline). The meter read the absent field, so
  `totalCtx` always took its 1M fallback in a real session — and the
  fixtures supplied the invented name, so the dynamic-buffer assertions
  passed on a code path that could never run.

Tests: every fixture now builds its `context_window` through
`contextWindow()`, which rejects any field the host does not send, with a
positive control proving the guard fires. The gate's unit test pins the env
names and the truthy set against the host's own list, asserts the full
truthy/falsy matrix for both names, and asserts the invented name is NOT
honoured.

Both constants are exported so the assertions name one source.

* fix(#4844): single JSDoc opener; keep the fixture comment off the docs-guard detector

Review nit: two consecutive `/**` lines before the constants block — an
editing leftover, now one.

CI: lint-docs-guard-registration flagged the test file because its detector
reads any `docs/` sitting between two backticks anywhere in the file as a
template literal, and the fixture helper's comment cited the statusline
reference URL between two inline-code spans. Reworded rather than exempted:
the file does not read a repo docs/ path, so an exemption marker would
record a fact that is not true.

* docs(#4844): name the env vars Claude Code reads in the changeset and REQ-HOOK-07

The code and PR body were corrected in a02af98, but the release note and the
feature docs still told users to export DISABLE_AUTOCOMPACT /
CLAUDE_CODE_DISABLE_AUTO_COMPACT, neither of which Claude Code reads. Now
DISABLE_AUTO_COMPACT / DISABLE_COMPACT; FEATURES.md regenerated.

* test(#4844): drop the literal docs/ from the fixture guard's comment

After rebasing onto next, the comment's own docs/ sat between two backticks
again and lint-docs-guard-registration flagged the file.

* test(#4844): name the source of the DISABLE_AUTO_COMPACT truthy set; fix the perf-317 fixture's context_window field

* fix(#4985): put the context meter's 100% at the auto-compact threshold

The meter treated the model window (or the env-var window) as 100%, and
never read the `autoCompactWindow` setting `/autocompact` saves. With
`/autocompact 650k` on a 1M model it read 74% at the moment Claude Code
compacted, and the context monitor, gating on the bridge's model-window
`remaining_percentage`, could not warn first.

- resolveAutoCompactThreshold(): the window from
  CLAUDE_CODE_AUTO_COMPACT_WINDOW, then settings `autoCompactWindow`, then
  the model window; capped at the model window; minus 20,000 + 13,000;
  lowered by CLAUDE_AUTOCOMPACT_PCT_OVERRIDE. Null when
  isAutoCompactDisabled() (#4844) says compaction is off.
- contextMeter(): used tokens from current_usage (the /context count)
  against that threshold, or the model window when compaction is off.
  Falls back to Claude Code's own percentage when the window size is
  absent.
- The settings-file walk isAutoCompactDisabled() did inline is now
  readClaudeSetting(), shared by both lookups; project settings come from
  workspace.project_dir.
- Bridge: remaining_percentage / used_pct on the meter's scale, plus
  used_tokens / threshold_tokens. The monitor's 35% / 25% are unchanged and
  now measure runway to compaction; its message quotes the token counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsvroGuHEwg1cUd6n13fHK

* test(#4985): move perf-317's #2451 bridge assertions to the threshold scale

#4959 switched this file's runStatuslineHook fixture from the invented
context_window.total_tokens to context_window_size, so its #2451 bridge
tests now reach the #4985 meter instead of the unscaled fallback, and
they asserted the old model-window scale (used_pct 65 for remaining 35).

Under the bridge-scale choice on #4985 (option a), used_pct and
remaining_percentage count down to the auto-compact threshold (650,000 of
967,000 → 67%), and the /context agreement #2451 protected moves to the
bridge's used_tokens, which must equal what /context reports as used.
Same rewrite as the #2451 test in gsd-statusline.test.cjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* fix(#4985): read the per-model autoCompactWindow that /autocompact saves

Claude Code 2.1.292 saves /autocompact's window per model, under
modelSettings.<canonical model name>.autoCompactWindow, not as the
top-level autoCompactWindow older versions wrote. Reading only the
top-level key left the meter on the model window for anyone who ran
/autocompact on a current Claude Code — the #4985 headline case.

The window is now merged the way the binary's lFt()/bv() merge it:
settings files lowest precedence first, each file's per-model entries
layered over the lower files', a file's top-level autoCompactWindow
replacing the default and dropping the lower files' per-model entries,
and the session model's own entry winning over the default. The model
key is the payload's model.id, canonicalized to match its dated, [1m],
Bedrock and Vertex spellings. "auto" (Claude Code's tuned per-model
window, not visible to a statusline) falls back to the model window, as
does an invalid value or an unmatched model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* chore(#4844): credit #4959 in its changeset; drop "unchanged when enabled"

This PR carries #4959's commits and also changes the enabled case
(#4985), so the fragment's closing "Unchanged when auto-compact is
enabled" no longer holds. It now credits the contribution instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* fix(#4985): keep the settings read lazy, the model key exact, and the tests isolated

Pre-filing review findings on the per-model window:

- readClaudeSetting stops at the first settings file that answers again;
  the shared reader had made isAutoCompactDisabled parse all four files.
- canonicalModelKey strips a Bedrock -v<n> revision only after a version
  or date segment, so anthropic.claude-v1 and claude-v2 no longer share
  the key "claude".
- Tests pin that an invalid top-level autoCompactWindow merges (keeps the
  lower files' per-model entries) while a top-level "auto" resets, as the
  binary's lFt() does; both mutants are caught.
- The perf-317 #2451 helper pins CLAUDE_CONFIG_DIR to a scratch dir and
  clears the compaction env vars (the statusline helpers clear the pct
  override too), so a developer's own settings cannot move the threshold.
- docs/context-monitor.md and the #4985 changeset name the
  CLAUDE_AUTOCOMPACT_PCT_OVERRIDE lowering; the #4844 changeset states the
  disabled-case meter as the share of the model window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* docs(#4985): say when the bridge's used_tokens is estimated

Without current_usage (before the first response) the statusline
recovers the used tokens from Claude Code's percentage, so the count is
an estimate rather than the one /context shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* test(#4844, #4985): no try/finally in test bodies

CONTRIBUTING.md bans try/finally inside test bodies. The
isAutoCompactDisabled precedence test now registers its cleanup with
t.after(), and the perf-317 #2451 helper cleans its scratch config dir
after the try/catch instead of in a finally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* chore(#4985): set changeset fragment pr to 5252

* chore(#4844): set carried #4959 changeset pr to 5252

The #4844 fragment was authored on #4959, which this PR supersedes.
It now carries this PR's number; the credit to @tanji-dg is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APo9rB1VNczs8nVkQ9G28K

* fix(#4985): read a settings file only when it is a regular file of at most 1 MiB

The statusline reads the project's .claude/settings*.json on every
render, and that directory is repository content. A FIFO there blocked
the hook until it was killed, and a huge file was read whole each turn.

readSettingsFile now opens the file non-blocking, checks it through its
own descriptor, and treats anything that is not a regular file of at
most 1 MiB as absent. A symlink is still followed: a settings file
linked in from a dotfiles repo is ordinary, and lstat would have
dropped it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* test(#4985): pin each window, pct and threshold bound at limit-1, limit and limit+1

The autoCompactWindow setting (top-level and per model) and the
CLAUDE_CODE_AUTO_COMPACT_WINDOW env are now tested at 99,999 / 100,000 /
100,001 and 999,999 / 1,000,000 / 1,000,001, on a 2M model window so an
accepted 1M window reads differently from a rejected one. The pct
override is tested at -1 / 0 / 1 / 100 / 101, and threshold > 0 at a pct
that floors to 0 or 1 and at model windows of 32,999 / 33,000 / 33,001.

At pct = 100 the override is always above the threshold it is min'd
with, so its upper edge is not observable; the test says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* fix(#4985): make canonicalModelKey a fixpoint, so a key is its own key

One pass of the rules left some ids half-canonical: `x[1m][1m]` keyed
as `x[1m]`, a date after a Bedrock revision left the revision, and a
space before `[1m]` survived the trim. Keying the result again changed
it, so a settings entry and a model id could each be canonical and
still not match.

The rules now repeat until nothing changes. Each one only removes
characters, so the loop ends. A fast-check property asserts
canonicalModelKey(canonicalModelKey(s)) === canonicalModelKey(s) over
arbitrary strings and strings built from the pieces the rules strip;
the single-pass version fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* test(#4985): fast-check properties for the window parser, threshold, meter and settings merge

TESTING-STANDARDS.md asks parsing and budget/limit logic for a
fast-check property. Eight new properties (seed and run count from
tests/helpers/fast-check-setup.cjs):

- parseAutoCompactWindowEnv never throws and returns null or an integer
  in 100k-1M; reads back any such integer, plain or digit-grouped; is
  monotone in a positive value, clamped into 100k-1M.
- canonicalModelKey keys dated, [1m], Bedrock (with and without region
  and date) and Vertex spellings, in either case, as the model.
- resolveAutoCompactThreshold is a positive integer at most
  min(window, model window) - 33,000, or none; never falls as the window
  or the model window grows; the pct override only ever lowers it.
- contextMeter: used + remaining = 100, both integers in 0-100, measured
  against the threshold or the model window, monotone in usage.
- The settings merge, over four generated settings files: the window is
  one a file set or the model window, and the highest file decides when
  it speaks - its own model entry under any spelling, else its top-level
  value, which drops the lower files' model entries.

Each was checked against a mutant of the rule it covers: single-pass
key, no [1m] strip, Bedrock :0 kept, a 19k reserve, a pct override that
raises, an unclamped meter, top-level keeping lower model entries,
reversed file order, a non-canonical spelling overriding the canonical
one, and an env window not raised to 100k. Each mutant fails.

parseAutoCompactWindowEnv and isAutoCompactWindowValue are exported for
the properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* docs(#4985): reflow the context-monitor paragraph on the compaction point

One line ran past the paragraph's wrap width and left a ragged
one-word line after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* fix(#4985): read no more of a settings file than its size check saw

fce0e58 checked the size through the open descriptor and then read it
with readFileSync(fd), which reads to EOF. A file that was 2 bytes at the
check and grew past 1 MiB before the read was parsed whole, so the cap
held only for a file nobody was writing.

The read now stops one byte past the checked size, and a file whose
length differs from that size is treated as absent - grown or shrunk.
The test mocks fstatSync to report a size the file does not have; the
readFileSync(fd) version fails its growth case, and a version that
accepts a short read fails its shrink case.

Found by the cross-AI review of this round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* fix(#4985): bound canonicalModelKey's work on a hostile settings key

383215b made the key a fixpoint by re-running every rule until nothing
changed, stripping one [1m] per pass. That is quadratic: a 524 KB project
.claude/settings.json whose model key ends in 131,072 [1m] took 16.9 s
to resolve, and the statusline hook was killed before writing its bridge.

A key longer than 256 characters is now no model's and keys as '' -
no model id is near that length - and a run of [1m] suffixes is stripped
in one pass. Lowercasing moves before the loop, because it can lengthen
a string (U+0130 lowercases to two characters); inside the loop every
rule only removes characters, so the loop still ends.

The test renders that file through the hook within its normal timeout;
the pre-change key function stalls it. A second test pins the 255 / 256
/ 257-character boundary.

Found by the cross-AI review of this round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

* test(#4985): check the settings merge against a reference model, and the meter's percentage

Two properties from cc125e5 passed on wrong code:

- The settings-merge property checked only the highest file, then that
  the window was one some file set, and it decided which spelling
  belonged to which model with canonicalModelKey itself. A merge that
  dropped the lower files' entries, and a key function that filed a
  dated Opus id under Sonnet, both passed. It now compares the window
  with a reference model of the documented merge, written out in the
  test with its own spelling-to-model table and its own window check.
- The meter property checked used + remaining = 100 and the limit, not
  the percentage: used = 0 passed. It now requires used to be within 0.5
  of usedTokens / limit x 100, and 100 at or past the limit.

Each of the four wrong versions now fails.

Found by the cross-AI review of this round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVFQpCdfnwgWSkxUDF8tcF

---------

Co-authored-by: Hideki Tanji <75657150+tanji-dg@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Bumps the npm_and_yarn group with 1 update in the / directory: [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk).


Updates `@modelcontextprotocol/sdk` from 1.29.0 to 1.32.1
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](modelcontextprotocol/typescript-sdk@v1.29.0...1.32.1)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.32.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsd-bot <gsd-bot@users.noreply.github.com>
…orm (#5372)

* test(#4859): runtime-generated guidance names the registered command form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(#4859): runtime-generated guidance names the registered command form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(#4859): changeset fragment

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(#4859): existing tests expect the registered command form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(#4859): the smart-entry command-resolution test accepts the registered /gsd- form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(#4859): gate-cutover goldens name the registered command form

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Brings in every fix landed on next since the branch was cut, including
the proxy-addr and @modelcontextprotocol/sdk advisory bumps.
@github-actions

Copy link
Copy Markdown
Contributor

PR title needs the issue-ref convention

chore: merge release v1.17.0 to main

PR title must follow type(#<issue>): summary. The type must come first (no leading tags like [security]) and the scope must carry the linked issue ref so the release changelog links to it. Examples: fix(#1542): roadmap rollback, feat(#39)!: drop legacy flag, enhance(#1549): add PR-title validator.

How to fix: click "Edit" next to the PR title above and retitle it
as type(#<issue>): summary. No need to recreate the PR — this check
re-runs when you edit the title.

Why this is enforced

The release changelog is built from PR titles. A leading tag mis-files
the entry into the wrong section, and a scope without (#<issue>) leaves
the changelog line with no link back to the issue. See issue #1549.

@github-actions

Copy link
Copy Markdown
Contributor

Please use the PR template format

This PR is allowed to stay open because the author is a contributor or higher, but the body does not follow one of the required PR templates.

Please update the PR description to use the fix, enhancement, or feature template format from CONTRIBUTING.md so the automated documentation modules can do their job.

Detected problem: PR body does not match the fix, enhancement, or feature template.

@trek-e
trek-e merged commit 87bc15c into main Oct 14, 2026
7 checks passed
@trek-e
trek-e deleted the release/1.17.0 branch October 14, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants