Commit Graph

6 Commits

Author SHA1 Message Date
0xdhx
c516c39b33 fix(#3203): stop npm-global installs validating bundled agents against themselves (#3229)
* fix(#3203): stop npm-global installs validating bundled agents against themselves

getAgentsDir's claude branch derived the agents directory from __dirname,
which is correct for repo runs and runtime-config-dir installs (where
<root>/../agents IS the user's agents dir) but on an npm-global install
resolves to the package's own bundled agents/, so checkAgentsInstalled
validated the package against itself and agents_installed could never be
false. The new-project and new-milestone halt/warn gates were silently
dead for npm-global users.

Keep the install-relative path for the shapes where it is correct, but
when it lies inside a node_modules tree — the provably self-validating
case — resolve getGlobalConfigDir('claude')/agents like every other
runtime, honouring CLAUDE_CONFIG_DIR. GSD_AGENTS_DIR stays priority 1.
Repair the doc comment that asserted the __dirname form was correct for
both install shapes.

Regression test mirrors the published npm-global layout (package under
node_modules with a complete bundled agents/) and pins the resolved
directory plus the issue's negative control (one agent missing from the
config dir → agents_installed:false). Verified red against pre-fix code,
green post-fix; the repo-layout W010 health test stays green.

* chore(#3203): set changeset fragment pr to 3229

* docs(#3203): describe the node_modules guard as lexical, in CONTEXT.md and at the call site

The Agent Install Check Module glossary entry asserted that Claude resolves the
agents directory `__dirname`-relative unconditionally. That is the premise this
PR falsified: on an npm-global install the install-relative path resolves to the
package's own bundled `agents/`, so the check validated the package against
itself and `agents_installed` could never be false.

The inline doc comment above `getAgentsDir` was repaired with the fix; this
external predicate was left behind and has been false since. CONTRIBUTING.md's
`Fixed`-fragment docs exemption names this case explicitly — "Edit the docs
anyway if a fix corrects something the docs got wrong."

Both surfaces now describe the guard as what it is: an exact, case-sensitive
path-segment test that TARGETS those layouts rather than detecting them, so
neither claims more certainty than the predicate has. The call-site comment
carried the same conflation the glossary did. A path merely carrying a directory
of that name resolves the same way — the edge already disclosed on this PR — and
a non-empty GSD_AGENTS_DIR overrides it.

Comment-only in `src/`; no behaviour change.

`CONFIG.LOCATION.SEAM.two-families` needs no change: `GSD_AGENTS_DIR ->
getAgentsDir priority 1` is still accurate.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-08-12 20:45:00 -04:00
Tom Boucher
7a7bf19fc1 enhance(#2872): record scope and runtime in the install manifest (#3323)
* enhance(#2872): record scope and runtime in the install manifest

gsd-file-manifest.json gains manifestVersion, runtime and scope, and a new
read-only Installed Surface Resolver Module reads both install scopes for a
runtime in one call -- the first code path in the repo that does.

Phase 3 of epic #2866 (ADR-2866). Blocks Phase 4 (#2873), which resolves
#2218: the resolver's shadowedBy field is that defect expressed as a value
for the first time. It ships computed-and-unread here.

Installed-ness is decided by manifest PRESENCE, never by the new fields, so
a manifest written by an older GSD stays fully functional and no user needs
to reinstall. Recorded runtime/scope are corroboration: a disagreement with
the probed config dir is reported as declaredScopeMatchesProbe: false, never
silently corrected.

readInstallManifest is widened additively -- version/timestamp/mode/files
keep their exact names, types and meanings for all four existing callers.
manifestVersion is a new field rather than a reinterpretation of version,
which holds the package version and is read by the golden-parity fixtures.

Stems are derived from the installed manifest's own file keys, the inverse
of Phase 2's filename composition, guarded by a fast-check round-trip
property plus a kebab-case charset check so a crafted manifest key cannot
put a traversal segment, control character or ANSI escape into a trigger
that Phase 4 renders back to the user.

Also fixes two defects found while working:
- bin/install.js hardcoded manifestVersion: 2 while the reader owned
  MANIFEST_SCHEMA_VERSION = 2. Now single-sourced, with a parity test.
- docs/installer-migrations.md documented an install-state schema of five
  snake_case fields that have never been written; InstallState has only ever
  been { schemaVersion, appliedMigrations }. Corrected with a dated note.

Verification runs on the remote runner.

* fix(#2872): fold review findings from three independent engines

Standards axis:
- convert the manifest-schema suite from a hybrid setup(t) closure to
  beforeEach/afterEach (CONTRIBUTING.md:319-354 Pattern 1). The hybrid was
  neither approved pattern and a new test forgetting the call got no warning.
- SCOPE_ORDER was declared twice with no parity test -- this repo's recorded
  generative-fix-divergence class. Give the ordering one owner: install-scope
  exports it frozen, the layout module and the resolver both import it, and a
  test locks it against scopeRank so the constant and the ranks cannot drift.
- drop the defaultReadManifest passthrough (Middle Man).

Spec axis:
- add the VOLATILE_FILES exclusion test and source comment the acceptance
  table promised and did not deliver. gsd-file-manifest.json stays excluded:
  the new fields are deterministic, but timestamp -- the original reason --
  is unchanged.

Security axis:
- bound the reported manifest runtime at 64 chars, matching the
  truncatePostureValue convention already used in this subsystem. It reached
  declaredRuntime unbounded while the adjacent stems were gated by SAFE_STEM;
  an inconsistent posture on the same attacker-influenceable document. The
  charset stays ungated on purpose -- declaredRuntimeMatchesProbe needs to see
  the real value -- so Phase 4 must sanitize before rendering, recorded in the
  design's Known limits.

Both new parity tests were verified to FAIL when the two sides are made to
disagree, then pass again on revert. Verification runs on the remote runner.

* chore(#2872): backfill changeset pr number to 3323

* fix(#2872): give git fixture construction its own timeout class

PR #3323's full test (windows-latest, 22, shard 2/3) failed with

  gitOrThrow: 'git init' failed -- outcome=timed_out exitCode=null
  gitOrThrow: 'git commit --allow-empty' failed -- outcome=timed_out

from drift-detection.test.cjs's beforeEach, a file this branch never touched.
Every other lane passed the same commit, including windows-latest node 24 on
all three shards, and next is green.

Root cause is a bound sized for the wrong class. DEFAULT_GIT_TIMEOUT_MS is
15000 and its own comment scopes it to plumbing READS -- rev-parse, branch,
log -- against an existing repo. createFixture uses it for six sequential
repo-CONSTRUCTION spawns: init, three config writes, add -A, commit. init and
commit each write dozens of files, and on Windows every spawn is
Defender-scanned. Sibling tests in the failing block took 15.6-22.0s against
a 15000ms bound.

This repo already diagnosed this exact shape once: timeouts.cjs's
HOOK_FANOUT_TIMEOUT_MS records PR #3285 failing in the SAME job with the SAME
outcome=timed_out exitCode=null signature at the SAME bound while every other
lane passed, and concludes 'a bound sized for the wrong class, not a slow
machine'. It was fixed by splitting out a heavier class-norm at 60000. Same
remedy here: GIT_FIXTURE_TIMEOUT_MS = 60000, 4x the bound that failed and half
INSTALL_TIMEOUT_MS.

DEFAULT_GIT_TIMEOUT_MS deliberately stays at 15000 -- a blanket raise would
stop a genuinely hung plumbing read from surfacing fast.

Verified the value reaches the spawn rather than being an ignored option:
spawnSync was monkeypatched before requiring the fixture module, and all six
git construction calls were captured carrying timeout: 60000.

This branch's two new test files shift shard composition, which is how a
pre-existing fragility landed in the heaviest shard on the slowest lane.
Fixed here rather than deferred, per the no-defer rule.

Verification runs on the remote runner.

---------

Co-authored-by: sim <sim@local>
2026-08-10 15:50:55 -04:00
Tom Boucher
4a1ed2531f enhance(#3242): validate codex .toml model posture, not just presence (#3290)
* test(#3242): failing-first suite for the codex posture health-check

Specifies ADR-2313 D6 before the implementation exists, so the tests
bind to the contract rather than to whatever the code happens to do.

RED is established by construction, not by a remote run:
checkCodexModelPosture and POSTURE_REASON are absent from the compiled
lib today, so every row fails on the missing export. A remote checkpoint
here would prove only that the function is missing, which is already
known — so the run is deliberately deferred to the combined green
checkpoint rather than spent proving a tautology.

That makes the NEGATIVE PROOFS the rows that carry real signal. Every
positive row passes even for a naive implementation that greps
/model\s*=/ over the whole file. Six rows fail it: light-tier
service_tier/model_verbosity decoupling (#774), hand-added keys, a
commented pin, the model_verbosity key-prefix collision, the runtime
no-op ordering, and the headline case — a literal `model = "sonnet"`
inside the developer_instructions ''' block, which the emitter fills
with agent prompts that discuss models constantly.

Row 14's fixture was verified to discriminate before being written: a
whole-file scan matches it and a header-slice scan does not. Without
that check the test would pass trivially and prove nothing, which is the
vacuous-test failure this epic has already hit repeatedly.

Adversarial TOML fixtures are hand-authored against the real Codex shape
rather than generated by generateCodexAgentToml, per #2371 — a fixture
from the writer can only confirm what the writer already believed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(#3242): validate codex .toml model posture, not just presence

Implements ADR-2313 D6. checkCodexModelPosture is a new sibling export,
not a branch inside checkAgentsInstalled — that function carries 33
upstream dependents, cyclomatic 25, and sits in two traced process
flows, so it is deliberately left untouched.

It imports isAnthropicFlavoredModel from model-catalog, a genuine leaf.
That is what Phase 1's constant move bought: agent-install-check is
documented as pure read/verify and imports only leaves, so reaching the
rule through model-resolver would have dragged config-loader into it.

Reads liberally, judges strictly, and never guesses. Tolerates comments,
key order, whitespace, CRLF, and a BOM; anchors on full key names so
model_verbosity does not satisfy a `model` probe; treats extra
hand-added keys as none of its business, since the check is a predicate
on the two fields the posture owns rather than a whitelist over the
document. An unreadable file becomes a named violation and the loop
keeps going.

The scan covers only the header slice — the lines before the
developer_instructions ''' marker. The emitter writes agent prompts into
that block and GSD's prompts discuss models constantly, so a whole-file
scan reports violations for prose. This is the highest-risk defect in
the phase and the reason its fixture was verified to discriminate before
being written.

The non-codex short-circuit runs before any filesystem call, so a stray
.toml under another runtime is never inspected.

Wired through cmdValidateAgents as an additive codex_posture key, so a
violating install is visible from a command a user actually runs rather
than only from a library nothing calls.

Also fixes a test defect found while implementing: .gitattributes forces
`* text=auto eol=lf` repo-wide, so the committed CRLF fixture was
normalized to LF in the index — `git ls-files --eol` reported `i/lf
w/crlf`, the working copy being stale pre-normalization bytes. The CRLF
row was asserting against a file that could not survive a fresh clone.
CRLF is now derived at runtime, which puts it under the test's control
rather than git's, instead of adding a .gitattributes exception that
fights a deliberate repo-wide policy and that anyone could re-normalize.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#3242): document the codex posture check where a user will look

Three quadrants, filed by where the reader actually arrives.

How-to (recover-and-troubleshoot.md, under Install and update problems)
is titled by the SYMPTOM — "If Codex agents fail to spawn with a 400
about an unsupported model" — and opens with the verbatim error string.
Someone hitting this does not know the words "posture" or "ADR-2313";
they have a 400 in their terminal and will search for that.

Reference (COMMANDS.md) had no `validate agents` entry at all, though
sibling gsd-tools subcommands are documented. Adding user-visible output
to an undocumented command and then linking to it from the new how-to
would have left a dangling reference. The entry carries the
violation-reason table, since the frozen POSTURE_REASON enum is the
machine-readable contract a reader needs rather than the prose.

Both surfaces state that presence and posture are separate verdicts — a
missing agent lands in `missing`, never as a posture violation. That is
a deliberate design decision and would otherwise be invisible to someone
watching one command emit both.

Explanation stays in ADR-2313, which already covers D6 and the
liberal-parse/strict-judge boundary. Pointing at it beats duplicating it
into COMMANDS.md and creating two copies to drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3242): close two false negatives in the posture scan

Both found by an isolated reviewer and reproduced before fixing. Both
made the check report clean when it was not — the worst direction for
this function, since the how-to tells users an empty violations list
means the install is posture-clean.

A quoted TOML key was never matched. `"model" = "sonnet"` is legal TOML,
but the key pattern required a bare identifier, so the pin was silently
invisible. Bare, "double" and 'single' quoted forms now normalize to the
same key name.

The block marker was found by unanchored whole-content search and used
to truncate the header. A `description` value merely containing the
literal text `developer_instructions = '''` truncated the scan before a
real pin, and a user who hand-reordered `model` to sit after the block —
still legal TOML — was never scanned at all.

Fixed by changing the strategy rather than the regex: find the block's
range, anchored at line start, and scan every line OUTSIDE it. That
covers both failures and is strictly more correct than truncation, while
still never reading prompt prose. An unterminated block excludes the
rest of the file, which fails toward a false positive — the safe
direction, since misreading prose as a pin wastes a user's time while
the alternative hides a real one.

Also corrects two overclaims of mine. The how-to named "v1.11", a
version that does not exist — package.json is 1.10.0 and unreleased — so
it now describes the boundary by behavior and links the ADR. And the
test matrix asserted that a naive whole-file scan "fails exactly rows
12,13,14,15,16,25"; the reviewer computed that rows 12, 13, 15 and 16
produce the correct result against that baseline too. They guard real
but *different* mistakes, and the matrix now says which one each catches
instead of attributing them all to the header-slice defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3242): skip symlinked agent files instead of following them

Security review finding. The scan listed entries with readdirSync and
read them with readFileSync, which follows symlinks — so a symlink in
the agents directory pointing anywhere would have its contents read, and
any line matching the model pattern echoed into cmdValidateAgents'
output through the `value` field. A read-and-echo primitive on an
arbitrary path.

It needs write access to the agents directory, so it crosses no new
trust boundary today. Fixed anyway, for two reasons.

This repo already does it correctly next door: cmdEffortSync filters
with lstatSync().isFile() and the comment "Skip symlinks — only write
regular files to avoid clobbering symlink targets." Being inconsistent
with a sibling in the same subsystem IS the defect.

And Phase 3 (#3243) extends that same cmdEffortSync to WRITE these
files. Establishing symlink-following as the house pattern for Codex
.toml handling here would hand Phase 3 a worse starting point while it
writes rather than reads.

Skipped silently rather than reported, matching the sibling: a symlinked
agent file is a structural install choice, which checkAgentsInstalled
owns, not a model-content posture defect. An lstat that itself throws
excludes the file rather than crashing the scan.

That does narrow the guarantee slightly, so the how-to now says an empty
list means every REGULAR .toml is clean, and tells anyone symlinking
their configs to check the targets by hand. Claiming a clean bill of
health over files the check declined to open would be the same kind of
false confidence the two false negatives above produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3242): backfill changeset pr number (#3290)

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:50:49 -04:00
Daniel Einspanjer
f0ff23635e fix(#2602): discover project-local Codex agents (#2623)
* fix(#2602): discover project-local Codex agents

- Select an existing local Codex agents directory before global fallback
- Prove init reports the canonical local installation through compiled CJS

* test(#2602): lock Codex agent precedence

- Cover override, local authority, global fallback, and runtime compatibility
- Exercise installed state through the compiled resolver

* fix(#2602): resolve local Codex agent skills

- Pass the canonical project root to the non-Claude persona fallback
- Cover nested-Codex fallback and Claude compatibility through the CLI

* test(#2602): cover local Codex validation status

- Assert emitted validate and health commands use the project-local install
- Preserve empty local-directory authority beside complete global agents

* fix(#2602): align validation with local Codex discovery

- Pass the resolved runtime and project root to health W010
- Resolve the validate-agents runtime before checking installation status

* test(#2602): cover local Codex docs status

- Assert docs-init reports an authoritative empty local install as unhealthy

* fix(#2602): align docs with local Codex discovery

- Pass the resolved runtime and canonical project root to the shared agent checker

* fix(#2602): honor agent-skills runtime override

- Resolve agent-skills fallback runtime through the canonical project resolver
- Cover conflicting config and GSD_RUNTIME values through the emitted CLI

* fix(#2602): ignore non-directory local agents paths

- Treat only a local Codex agents directory as authoritative
- Cover regular-file fallback through the emitted install checker

* chore(#2602): add changelog fragment

- record the user-visible local Codex agent discovery fix for PR #2623

* fix(#2602): align local agent discovery with runtime policy

- Resolve Codex's local config directory through the canonical runtime policy
- Use test-managed cleanup for local-agent discovery coverage

* fix(#2602): discover local agents across runtimes

- Prefer manifest-backed project-local installs for non-Claude runtimes
- Respect runtime-specific local install roots and preserve global fallback behavior
- Cover native, partial, cross-runtime, and project-root local discovery

* fix(#2602): preserve agent discovery fallback

- Fall back globally when local-install probes fail
- Document and test symlink rejection
- Align the changeset with repository format

* fix(#2602): reuse local directory policy

- Resolve runtimes without local config through the canonical sentinel
- Document the manifest gate and refresh the context index

---------

Co-authored-by: Daniel E. <daniel.e@teachingstrategies.com>
Co-authored-by: Rezolv <dave@sienkowski.com>
2026-07-31 21:20:46 -04:00
Tom Boucher
54420bae9e refactor(#1277): T1 — decouple agent-install-check + git-base-branch from the core spine (#1280)
First leaf-migration tranche of epic #1267 (after T0 #1268). Migrate the
via-core callers of the two leaves T0 created to import from the leaf
modules directly, and stop core re-exporting their symbols:
- checkAgentsInstalled: docs.cts, verify.cts, init.cts -> agent-install-check.cjs
- gitWorktreeInfoInternal: init.cts -> git-base-branch.cjs
- getAgentsDir had no external via-core caller (internal to the leaf)

core no longer re-exports getAgentsDir / checkAgentsInstalled /
gitWorktreeInfoInternal; the now-unused agent-install-check + git-base-branch
requires are dropped from core; the shim-identity assertions for these are
deleted (behaviour tests retained). Convergence lint stays green (0 new).
No behaviour change.

Closes #1277

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:46:41 -04:00
Tom Boucher
48d9cec6fe refactor(#1268): re-home core re-export-spine squatters + migration-convergence lint (#1272)
Re-home the 6 implementation functions squatting in the core.cjs re-export
spine (ADR-857) into the modules whose interface they belong to, with core
re-exporting them BY REFERENCE so all 32 callers + the shim-identity tests
keep resolving unchanged:
- worktree-safety: resolveWorktreeRoot, pruneOrphanedWorktrees
- git-base-branch (broadened to the Git Query Module): gitWorktreeInfoInternal
- agent-install-check (new leaf): getAgentsDir, checkAgentsInstalled
- delete the _resetRuntimeWarningCacheForTests wrapper; consumers use a
  shared resetRuntimeWarningCaches() helper in tests/helpers.cjs

Add scripts/lint-core-spine-imports.cjs (migration-convergence lint with a
30-importer allowlist, wired into lint:ci) so the staged spine retirement
provably converges: CI fails on any new ./core import. Register the new
generated agent-install-check.cjs in eslint-ignore + .gitignore +
INVENTORY-MANIFEST.json.

No behaviour change. First tranche (T0) of epic #1267.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:08:58 -04:00