2f4f7538e9be7b8dc99fbb3aff986eaea7a997e6
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c61dd49d95 |
enhance(#2255): blocking catastrophic-shrink guard for curated .planning/ writes (#2301)
* feat(#2255): blocking catastrophic-shrink guard for .planning writes Adds hooks/gsd-write-guard.js, a PreToolUse hook that hard-blocks (decision: 'block', exit 2) a whole-file Write collapsing a curated .planning/ artifact (ROADMAP.md, .planning/milestones/*-ROADMAP.md, STATE.md) below 40% of its on-disk line count. Files under 40 lines are exempt; GSD_ALLOW_PLANNING_SHRINK=1 (named in the block message) bypasses for legitimate milestone resets. Fix 3 of #973 — the only defense independent of per-agent tool config. Registered on the Claude plugin surface (hooks.json), settings-json runtimes (runtime-hooks-surface.cts, self-contained pattern), Kimi spec, and the OpenCode/Kilo plugin buses. Golden install fixtures and INVENTORY regenerated; regression tests negative-controlled (16/16 RED with the hook absent, 16/16 GREEN with it present). * chore(#2255): backfill changeset pr number to 2301 * enhance(#2255): address review — fail-closed reads, typed block output, registration, property test Review fixes for trek-e's CHANGES_REQUESTED on PR #2301: - Blocker 2: register gsd-write-guard.js in BUNDLED_GSD_HOOK_FILES (no-shipping-drift test). - Blocker 3: update the always-on hook enumerations in ADR-766 and CONTEXT.md from six to seven. - Major 4: fail CLOSED on non-ENOENT read errors — only a missing file (new-file Write) passes; EACCES/EISDIR/ELOOP/etc now block, with a typed readError field and the override still honored. Tested, with a negative control against the pre-fix hook. - Major 5: fast-check property test for the SHRINK_RATIO/FLOOR_LINES budget contract (blocked ⟺ newLines < oldLines*SHRINK_RATIO above the floor; sub-floor always exempt), boundary examples pinned. - Major 6: block output now carries typed oldLines/newLines/ overrideEnvVar fields; tests assert on those instead of regexing the free-form reason string. - Minor: CURATED_PATTERNS are case-insensitive (case-insensitive-FS bypass on macOS/Windows); limit+1 boundary tests added for both the floor and the ratio. * enhance(#2255): engage the write guard on Kimi's native payload shape The guard shipped with Claude-vocabulary checks (tool_name 'Write', tool_input.file_path), which #2304 showed leaves a guard dormant on Kimi: the [[hooks]] matcher is registered pre-translated but kimi-cli forwards its native payload verbatim — tool_name 'WriteFile' (bare or module-qualified) and tool_input.path per its tool schemas (src/kimi_cli/tools/file/write.py). The guard matched, saw an unknown name, and exited 0. Apply the same per-guard normalization PR #2326 gives the three sibling guards (name + field mapping, inlined — hook scripts stage as standalone files), and write the block reason to stderr as well as stdout JSON: Kimi feeds stderr, not stdout, back to the model on exit 2, so a stdout-only reason blocks without telling the model why or naming the documented override. Regression tests pipe Kimi-shaped payloads (engage, qualified-name, stderr-reason) plus exemption pins (StrReplaceFile stays out of scope by design; non-curated paths pass) — verified red against the pre-fix guard, green after. * enhance(#2255): rebase onto next; regenerate golden-parity fixtures * enhance(#2255): wire the escape hatch into complete-milestone's reorganize step Review Blocker 1: the guard hard-blocked /gsd:complete-milestone's ROADMAP reorganize — the tree's only legitimate milestone reset and the exact caller GSD_ALLOW_PLANNING_SHRINK was built for. The reorganize step now performs the rewrite through a shell write with the hatch set on the command (a hook inherits the runtime env, so a bare Write cannot carry a per-step override), and a binding test derives the env var name from the guard's typed output and asserts (a) the workflow step sets it and (b) the guard passes the identical catastrophic payload under it — so the next complete-milestone.md edit cannot silently re-break the wiring. * enhance(#2255): drop dead Edit-class mapping from normalizeKimiPayload Review Major 1: StrReplaceFile -> 'Edit' and the old_string/new_string reconstruction were unreachable-by-effect — the guard exits 0 for any tool_name !== 'Write', so nothing ever read the fields they set, leaving guaranteed-surviving mutants against the Stryker bar. The map now carries only WriteFile -> 'Write'; the StrReplaceFile exemption test message states the fall-through it actually exercises. * enhance(#2255): review minors — American spellings; writeSync before exit(2) Minor 1: normalised/normalise -> American house style. Minor 2: the two block paths wrote stdout+stderr via async pipe writes then exit(2) — async-on-Windows, unflushed at exit; fs.writeSync(1/2, ...) makes the block payload durable. * enhance(#2255): assert stderr equals the typed reason, not raw prose Minor 3: the last raw-text match in the suite pinned override-name prose on stderr. The contract is "stderr carries the reason Kimi feeds back" — now asserted as stderr non-empty and byte-equal to the parsed stdout.reason. * enhance(#2255): bind the write-guard's Kimi normalization into the parity test Review Major 2: the guard's normalizeKimiPayload is a 4th inlined copy with nothing binding it. This extends PR #2326's kimi-guard-normalization-parity test (same path and helpers, authored as a superset so either merge order resolves cleanly): sibling byte-parity is existence-gated zero-or-all — trivially green until #2326 lands, full-strength after — and the write-guard copy is bound semantically (map is the value-inverse of convertKimiToolName; the Kimi name for Write must map, or the guard is dormant on Kimi; the path -> file_path half must be present). Byte-parity is deliberately not asserted for this copy: it legitimately omits the Edit-class mapping (Major 1 — dead code in a Write-only guard). * enhance(#2255): refresh golden-parity fixtures for revised guard + workflow * chore(#2255): regenerate golden fixtures after rebase onto next The committed fixture hashes were generated against a tree predating next's latest 11 commits, which independently modified the same install-parity surface. Rebased onto next and regenerated with `npm run gen:golden`. Verified: against upstream/next the regenerated fixtures differ by exactly this PR's own entries -- hooks/gsd-write-guard.js (new), hooks/managed-hooks-registry.cjs, plugins/gsd-core.js, and gsd-core/workflows/complete-milestone.md. No unrelated drift. * fix(#2255): regenerate workflow size baseline for complete-milestone `complete-milestone.md` grew 31071 -> 32061 (+990) when the round-2 review fix bound GSD_ALLOW_PLANNING_SHRINK=1 into the reorganize step, but tests/workflow-size-baseline.json was never regenerated. The per-file workflow baseline test (issue #1074) failed on ubuntu-latest/22 and both macOS shard 1/3 jobs. The growth is justified: it is the escape-hatch binding requested in review round 2 (the guard must not hard-block the tree's only legitimate milestone reset), not incidental bloat. Regenerated via `npm run size:baseline`; the diff is exactly the one entry. * chore(#2255): regenerate golden fixtures and size baseline after rebase onto next * enhance(#2255): bind the shrink escape hatch mechanically — single-use sentinel the guard consumes Round-5 M1: the per-step `GSD_ALLOW_PLANNING_SHRINK=1 tee` prefix was inert (no PreToolUse hook exists on Bash in this family; the write succeeded by dodging the guard, not by the override firing) and the protection was prose. The hatch is now a transport code consults: complete-milestone's reorganize step arms `.planning/.gsd-allow-shrink` with the target's path, keeps the Write tool as the sanctioned path, and the guard — at the block point only — verifies the sentinel is fresh (15 min) and names the pending target, then CONSUMES it and allows that one write. Path-bound + single-use + freshness keep it from becoming a standing unlock. The env var remains as the interactive transport, where it can actually reach the hook. Regression tests written first (negative control: 3 failed pre-fix): the armed-sentinel Write passes and consumes; stale does not exempt; a token for a different file neither exempts nor is consumed; the binding test now takes the sentinel name from the guard's typed output (overrideSentinel), asserts the step arms it, and asserts the step no longer routes the rewrite around Write via a shell pipe. Also in this commit, same file: - m2: block emission is exception-safe — emitBlock() wraps both writeSync sites in their own try/catch that still exits 2, so an EPIPE can no longer convert fail-closed into the outer catch's fail-open. - Header discloses the two reviewed design limits (cumulative sequential shrink; lexical match vs symlinked paths) per round-5 scoping. * docs(#2255): document the sentinel transport across guard surfaces; changeset ends with the (#2255) parenthetical (m4) USER-GUIDE bullet, INVENTORY row (en + ja/ko/pt/zh), the runtime-hooks-surface registration comment, and the changeset now describe both hatches — the single-use sentinel for workflow steps and the env var for interactive use — instead of implying a per-step env can reach a hook. The changeset's trailing `Resolves #2255.` prose becomes the `(#2255)` parenthetical the repo's fragments use (round-5 m4). * chore(#2255): regenerate derived families on the rebased tree (full sweep) Full generator sweep after rebasing onto next @ the body-parser-patched lockfile: build, gen-inventory-manifest, gen:golden, size:baseline. Every regen delta verified to be either a PR-owned entry (gsd-write-guard.js, complete-milestone.md, INVENTORY/USER-GUIDE) or exact convergence to next's committed value for entries our arbitrary-side conflict resolution had left stale (all 18 runtime fixtures checked mechanically). * test(#2255): use helpers.cleanup for sentinel teardown, not raw fs.rmSync The repo's local/no-raw-rmsync-in-tests rule exists for the Windows-EBUSY retry budget; the sentinel disarm now rides it like every other teardown. * chore(#2255): regenerate derived families after rebase onto next Full sweep on the rebased tree (build -> gen-inventory-manifest -> gen:golden -> size:baseline). Every delta is either a PR-owned entry (hooks/gsd-write-guard.js, its registration surfaces hooks/managed-hooks-registry.cjs and the two plugin buses, gsd-core/workflows/complete-milestone.md) or exact convergence to next's committed value across all 18 runtime fixtures. * chore(#2255): regenerate derived families after rebase onto next @ |
||
|
|
ddcf459c81 |
fix(#2697): skip the context-monitor spawn in-process when context_warnings disabled (#2824)
* test(#2697): failing-first regression for context-monitor spawn not hoisted behind toggle * fix(#2697): skip the context-monitor spawn in-process when context_warnings disabled * test(#2697): patch spawnSync before require (plugin destructures at load time) * test(#2697): establish session before asserting on context-monitor spawn (review) * docs(changeset): #2697 context-monitor spawn skipped when context_warnings disabled * docs(changeset): backfill #2697 PR number to 2824 |
||
|
|
50efae13ce |
fix(#2305): stage the shared guard hooks Kilo's native plugin spawns (#2327)
* fix(#2305): stage shared guard hooks for Kilo — drop skipSharedHooksInstall Kilo's capability descriptor declared BOTH hostBehaviors.nativePlugin (a plugin that spawns the shared PreToolUse guard scripts as subprocesses) AND hostBehaviors.skipSharedHooksInstall:true, which suppresses staging of hooks/*.js into the Kilo config dir. The plugin's runHook treats an absent hook script as a silent allow, so every guard it spawned (gsd-prompt-guard, gsd-read-guard, gsd-worktree-path-guard) no-opped on every Kilo install. OpenCode uses the byte-identical plugin with hook staging on and is unaffected — it is the reference shape. The skip flag predates Kilo's plugin surface: it dates to #1821 (hooks were dead weight for a runtime with no hook consumer), and #2093 added the hooks-dependent nativePlugin without revisiting it. - capabilities/kilo/capability.json: remove skipSharedHooksInstall (regenerated gsd-core/bin/lib/capability-registry.cjs accordingly) - bin/install.js: correct the stale #1821 comments claiming Kilo has no plugin surface - tests/kilo-upgrades.test.cjs: install-fixture tests (global + local) asserting the guard scripts land where the plugin's walk-up resolves them; an end-to-end test driving a disallowed out-of-worktree write through the REAL installed Kilo tree and asserting the guard rejects it; a cross-runtime descriptor invariant (nativePlugin and skipSharedHooksInstall:true must never coexist) - tests/kilo-imperative-reference.test.cjs: flip the pinned assertion - golden fixtures regenerated (kilo now stages the 24 hook files, same set as OpenCode) Fixes #2305 * fix(#2305): warn loudly when a guard hook script is missing (runHook) runHook's absent-file branch returned a silent exit-0 allow — the mechanism that let #2305 ship undetected: with the hooks bundle never staged on Kilo, every PreToolUse guard the plugin spawned resolved to "file not found → allow" with zero signal anywhere. Keep the adapter's design contract (a missing hook must never break the tool call — pinned by the existing adapter test) but make the absence loud: console.error once per hook file, naming the unresolved path and the remediation. Applied identically to .kilo/ and .opencode/ plugin copies (byte-parity guard). Golden parity fixtures regenerated (the installed plugin file's hash changed). Fixes #2305 * chore(#2305): add changeset fragment * test(#2305): include gsd-workflow-guard.js in the staged-guards regression list The native plugin spawns four guards on write-like tool calls — the regression test's PLUGIN_GUARD_HOOKS list covered three. Staging itself was already asserted via the golden fixtures (the full bundle), but the named per-guard assertion should cover every guard the plugin actually dispatches. Surfaced by cross-AI review of PR #2327. * test(#2305): update the #1821 tests that encoded Kilo's false no-plugin premise The #1821 hook-copy test asserted Kilo must receive no staged hooks — the exact behavior this PR reverses (and the cause of all 8 CI failures). Kilo moves from the ZCode "no dead hooks" loop to the OpenCode group, with positive assertions on the new contract: the three guard hooks the plugin spawns, hooks/lib/git-cmd.js, and plugins/gsd-core.js all staged. The integration runtime contract flips kilo packageJson to true (the CommonJS marker ships with the bundle), and the pi contract comment no longer cites Kilo as a no-plugin runtime. * chore(#2305): scope the queued #1821 changeset fragment to ZCode only The fragment still claimed the installer skips hooks for Kilo — rendering both it and this PR's fragment into the same release would ship two contradictory statements about Kilo's install behavior. It now claims ZCode only and notes that #2327 reverses the Kilo half. * chore(#2305): rename changeset fragment to the generator naming convention 2305-kilo-stage-guard-hooks.md -> loud-guard-hooks.md, matching the <adjective>-<noun>-<noun> shape npm run changeset generates (review nit). --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
396f44bd0b |
feat(architecture): [EoS/opencode] Migrate OpenCode onto the Embeddable Orchestration System (ADR-1239, #2087)
Route OpenCode (and its Kilo sibling) through the public Host-Integration Interface and land two Context7-verified capability upgrades. Byte-identical install output for all 16 runtimes (golden parity asserted). Through the interface (AC2): - OpenCode/Kilo's bespoke commands+skills+plugin install (the inline `else if (isOpencode || isKilo)` block) moves into the engine (installOpencodeFamilyCommands/Artifacts in src/install-engine.cts), dispatched by installRuntimeArtifacts when the descriptor declares hostBehaviors.combinedFamilyInstall. opencode/kilo now flow CLI -> _runtimeAdapter -> installRuntimeArtifacts like the skills runtimes. _isSkillsRuntime no longer excludes them; the bespoke block + dead copyFlattenedCommands are removed. - Every hardcoded `runtime === 'opencode'`/`isOpencode` branch is folded into descriptor-driven runtime.hostBehaviors. ZERO `runtime === 'opencode'`/`'kilo'` string-equality remain in bin/install.js / install-engine.cts / runtime-artifact-conversion.cts. Upgrades (AC4): - Background dispatch: OpenCode shipped experimental background subagents in v1.15 and made them default-on in v1.17 -> dispatch.background/backgroundDispatch flip to true; shouldFlattenDispatch(opencode) now returns false (behavioral change; type: Changed). - Expanded event surface: the OpenCode plugin subscribes permission.asked/replied + session.error. Tests: opencode-imperative-reference (adapter/profile, shouldFlattenDispatch pin, fail-closed negotiate, hostBehaviors, AC2 source-guard) + extended plugin surface test. Docs: capability matrix v1.15/v1.17 citations. Changeset (Changed). gitignore .memdb//.memtrace/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
325e9fad4b |
feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c (#1930)
* feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c - Plugin (.opencode/plugins/gsd-core.js): handle session.idle (↔ Claude Stop lifecycle point; no-op sentinel — state already persisted to .planning/). Completes the compaction/idle pair (#1914 shipped compaction). - Declare hookEvents: 'opencode-subset' in the OpenCode descriptor — the reserved dialect now has a real consumer (no longer zero-consumer). - host-integration.cts: add HOOK_EVENT_SURFACES + hookEventSurfaceFor() — the pure consumer that resolves a dialect to its host-fireable event surface. opencode-subset = session/tool/file subset with NO workflow-phase events (engine owns phase sequencing; ADR-1239 §OpenCode binding). - Tests: hookEventSurfaceFor unit tests (claude/gemini/opencode-subset/null); plugin session.idle no-throw; compaction breadcrumb; opencode-subset surface parity vs the plugin's handlers (Claude parity). * fix(#1682): don't declare hookEvents on opencode (hooksSurface:none invariant) The repo invariant couples runtime.hookEvents to the managed settings.json hook surface: hooksSurface:'none' runtimes (opencode — plugin owns hooks) must NOT declare hookEvents. Declaring 'opencode-subset' there violated 3 capability- registry invariants + 2 install-plan golden masters + opencode golden parity. The opencode-subset dialect is still IMPLEMENTED — just not via the legacy descriptor field: hookEventSurfaceFor() (host-integration.cts) is its consumer, and the OpenCode plugin consumes the subset events at runtime (session.idle added here; compaction shipped in #1914). Declaring it on the descriptor would require weakening the hooksSurface:none ⇔ no-hookEvents invariant (flagged for decision). * fix(#1682): refresh opencode golden parity for plugins/gsd-core.js (session.idle) * docs(changeset): OpenCode session.idle + opencode-subset dialect (#1682) * docs(changeset): backfill PR #1930 |
||
|
|
ff6b5cb024 |
feat(#1914): OpenCode native plugin integration (Option 1 file-copy) (#1923)
* feat(#1914): OpenCode native plugin integration (Option 1 file-copy) Ship a native OpenCode plugin (.opencode/plugins/gsd-core.js) plus the installer step that delivers it, so GSD's lifecycle hooks run on OpenCode. OpenCode declares hooksSurface:'none', so GSD's hook scripts already ship to <configDir>/hooks/ but nothing invokes them; the plugin bridges OpenCode's event bus onto those scripts as subprocesses (prompt/read/worktree/workflow guards, injection scanner, context monitor). Distribution is Option 1 (file copy) per the #1914 triage decision: no scripts.build rename, no prepare/prepack removal. package.json gains main + .opencode in files[] for discovery. Corrected against OpenCode's docs + loader source (not the reference branch): - Auto-discovery globs {plugin,plugins}/*.{ts,js} — .cjs is never matched, so the installed adapter must be .js (config dir carries {"type":"commonjs"}). - No opencode.json plugin-array patch — that array is npm-only; local files are auto-discovered. - REPO_ROOT is resolved by walking up to the dir holding hooks/ + gsd-core/, correct for package tree, global install, and local install. - Config-hook registration is gated (IS_PACKAGE_TREE) so it never double-registers commands/agents/skills already delivered by native copy. Also fixes an incidental .gitignore drift: 8 ADR-1239 .cts-generated .cjs artifacts were untracked-and-not-ignored (leak risk) — now ignored. Tests: tests/opencode-plugin-adapter.test.cjs (14, pure helpers + real subprocess bridge against stub hooks); golden-install-parity regenerated. Green on Mac + Linux (gsd-test): 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1914): harden OpenCode plugin export shape + advisory accumulation (adversarial findings) Address Codex adversarial-review findings: - HIGH: export `{ id, server }` could trip OpenCode's loader (`for (entry of Object.values(mod)) getServerPlugin(entry)` throws on a non-extractable value). Make `id` NON-ENUMERABLE and assign module.exports from a variable (not a literal) so no string `id` is ever iterated — verified loader-safe under real import(pathToFileURL) (default + module.exports alias, both objects with .server; no bare id string). - MEDIUM: sequential advisory hooks clobbered output.metadata._gsdAdvisory; now accumulate into an array. - LOW: resolveRepoRoot fallback returned ".." while the comment said "../.." — aligned to "../.." (package-tree depth). Tests: added a faithful loader-loop emulation (raw CJS + ESM namespace views), advisory-accumulation, and a real bin/install.js copy→manifest→uninstall integration test. golden regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1914): add changeset fragment for OpenCode plugin integration (PR #1923) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1914): Windows — assert rewritten path with string include, not path-regex The Read content-rewrite test built a RegExp from `path.join(root,'gsd-core')`. On Windows the backslashes in the path are interpreted as regex escapes, so the assertion never matched and `test (windows-latest, 24)` failed — even though the adapter rewrote the path correctly. Replace the RegExp with a separator-agnostic `String.includes` check (the repo's no-path-literal-in-assert concern). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |