fix(#2717): write CommonJS marker for cursor/windsurf/codex staged .js hooks (#2846)

* test(#2717): CommonJS marker for cursor/windsurf/codex staged .js hooks

Cursor/windsurf (skipSharedHooksInstall) and codex (!isCodex gate) stage .js
hook scripts via dedicated paths that bypass installSharedHooksBundle — the
only writer of the {"type":"commonjs"} marker. Under a config root declaring
{"type":"module"}, Node loaded those scripts as ESM and every require()
failed with 'require is not defined', silently disabling the runtime's hooks.

Adds regression tests (RED first, fix lands next commit):
- parametrized cursor/windsurf/codex install asserts hooks/package.json exists
  with exactly GSD's marker content;
- end-to-end: a cursor require()-using hook loads under a planted ESM-typed
  config root without the require-is-not-defined error;
- the ensureCommonJsMarker / removeCommonJsMarkerIfGsdOwned contract: GSD
  markers are removed on uninstall, user-authored package.json is never touched.

* fix(#2717): write CommonJS marker for cursor/windsurf/codex staged .js hooks

The {"type":"commonjs"} marker lived only inside installSharedHooksBundle,
which cursor/windsurf (skipSharedHooksInstall) and codex (!isCodex gate) never
reach. Their .js hooks are staged by dedicated paths, so under a config root
declaring {"type":"module"} Node loaded them as ESM and every require()
failed with 'require is not defined', silently disabling those runtimes' hooks.

Decouple the marker write into a shared helper so any code path that stages
.js hooks can ensure it lands in the SAME directory as the scripts:

- src/runtime-hooks-surface.cts: add ensureCommonJsMarker(dir) +
  removeCommonJsMarkerIfGsdOwned(dir) (byte-identical content to
  installSharedHooksBundle's marker; preserves a user-authored package.json on
  both write and uninstall). Call ensureCommonJsMarker(hooksDir) from
  writeCursorHooksJson + writeWindsurfHooksJson; call
  removeCommonJsMarkerIfGsdOwned on their matching remove paths. Export both.
- bin/install.js: call hooksSurface.ensureCommonJsMarker after the codex hook
  copy; call hooksSurface.removeCommonJsMarkerIfGsdOwned in the generic
  hooks-removal loop (safe no-op where no marker exists).

No change to which runtimes receive the shared bundle, the !isCodex gate,
skipSharedHooksInstall, or kimi/kimi-code/cline/copilot/trae/zcode (all
unchanged — audit in the diagnosis). RED @ dbb7d2bb (6 failures: 3 missing
markers + the ESM require error + missing helpers); GREEN pending.

* docs(#2717): changeset fragment (pr:0, backfilled post-PR)

* chore(#2717): regen codex/cursor/windsurf install-tree fixtures + attribution ack

The fix adds hooks/package.json to those three runtimes' install trees (the
new CommonJS marker), so the golden install-tree fixtures gain one path each
(regenerated via npm run gen:install-tree). emitted-attribution (ADR-2719)
flags the 3 emitted hooks/package.json paths under the hooks-built rule;
acknowledge them. Also drops 5 spent ack entries left by now-merged PRs
(#2694 code-review.md/code-review-fix.md, #2695 worker/registry, #2794
review.md) — they are stale on this branch (base already carries them).

* fix(#2717): codex ESM-root behavioral test + hooks-built provenance for package.json

Two review-driven follow-ups on the #2717 fix:
- Adversarial review noted the ESM-root behavioral test covered only cursor;
  refactor it into a helper and add a codex case (the !isCodex-gated path most
  likely to regress, whose marker write lives in bin/install.js). gsd-check-update.js
  require()s at module load, so it surfaces the ESM failure immediately.
- emitted-provenance flagged hooks/package.json as 'attributed source does not
  exist' — the marker is code-derived (a fixed literal emitted by
  ensureCommonJsMarker at install time), not built from a tracked source. Route
  the hooks-built rule's sources/transforms for package.json to the surface
  source file, mirroring the existing .cmd-shim sub-family.

* chore(#2717): drop now-redundant hooks/package.json attribution ack

The hooks-built provenance routing (prior commit) now self-attributes the
emitted hooks/package.json to src/runtime-hooks-surface.cts, which IS in this
diff — so the attribution is self-explaining and the emitted-drift-ack entry
became stale. Delete the (now-empty) ack file per ADR-2719's empty-file rule.

* docs(changeset): backfill #2717 PR number to 2846
This commit is contained in:
Tom Boucher
2026-07-29 22:18:48 -04:00
committed by GitHub
parent b12d4df03b
commit 4f6935e29b
9 changed files with 289 additions and 22 deletions

View File

@@ -347,6 +347,16 @@ const PROVENANCE_RULES = [
// bytes; its CONTENT never does — see the `sources` comment below for why
// that rules out attributing to `hooks/<name>.js`.
pattern: /^(?!gsd-session\.json$).+$/,
// `hooks/package.json` (the CommonJS marker) is ALSO code-derived, not built
// from a tracked hooks/package.json source: its bytes are a fixed literal
// emitted at install time by ensureCommonJsMarker (HOOKS_WINDOWS_SHIM_SRC,
// a.k.a. src/runtime-hooks-surface.cts) for cursor/windsurf, and by the codex
// copy block (bin/install.js) which calls that same exported helper (#2717).
// So — like the `.cmd` shim below — it routes `sources`/`transforms` to that
// source file rather than to a nonexistent `hooks/package.json`. The codex
// emission path is covered transitively: it requires + calls the helper whose
// content literal defines the marker bytes.
//
// `.cmd` shim bytes are code-derived (a literal template + the install-time
// interpreter/path tokens in HOOKS_WINDOWS_SHIM_SRC) — the wrapped `.js`
// file's NAME flows in (as a hardcoded literal filename inside that same
@@ -356,8 +366,8 @@ const PROVENANCE_RULES = [
// used elsewhere in this table (copilot-hook-registration, cline-rules-
// code-derived, hermes-category-description). The redundancy between
// `sources` and `transforms` here is harmless — the mis-attribution was not.
sources: (m) => [m[0].endsWith('.cmd') ? HOOKS_WINDOWS_SHIM_SRC : `hooks/${m[0]}`],
transforms: (m) => (m[0].endsWith('.cmd') ? [HOOKS_WINDOWS_SHIM_SRC] : []),
sources: (m) => [m[0].endsWith('.cmd') || m[0] === 'package.json' ? HOOKS_WINDOWS_SHIM_SRC : `hooks/${m[0]}`],
transforms: (m) => (m[0].endsWith('.cmd') || m[0] === 'package.json' ? [HOOKS_WINDOWS_SHIM_SRC] : []),
},
{
id: 'copilot-hook-registration',