Commit Graph

2 Commits

Author SHA1 Message Date
Cristian Uibar
c5657fcbfd fix(surface): default missing optional fields in readSurface, normalize writeSurface input (#3666)
* surface: default missing optional fields in readSurface, normalize writeSurface input

readSurface used to reject any .gsd-surface.json missing one of its four fields
and return null with no diagnostic, so the active surface silently degraded to
the 'full' profile. Optional array fields (disabledClusters, explicitAdds,
explicitRemoves) now default to [] when missing or wrong-typed; hard failures
(malformed JSON, non-object root, missing/non-string baseProfile) still return
null but emit a console.warn naming the file + reason.

writeSurface now normalizes its input to the full SurfaceState shape and
throws on missing baseProfile, so partial writes can no longer land on disk
and trip readSurface later. Tests extended to cover the new lenient and
warn-on-hard-fail behavior plus the writer guard.

Fixes #3662

* surface: reject whitespace-only baseProfile + migrate new tests to helpers.cjs

Applies CodeRabbit findings on PR #3666:

1. readSurface and writeSurface now reject baseProfile values that are blank
   after trim() (e.g. "   "), not only the empty string. Whitespace-only
   strings would split-by-comma to [''] downstream and silently produce an
   unresolvable profile mode. Both guards updated symmetrically; warn/error
   messages reworded to "missing, non-string, or blank".

2. tests/surface-state.test.cjs now uses createTempDir + cleanup from
   tests/helpers.cjs instead of local mkdtempSync + fs.rmSync, aligning with
   the repo coding guideline for root-level tests. The local tmpDir() helper
   delegates to createTempDir for backward-compat with the existing test
   bodies. Per-test cleanup calls swapped to cleanup(dir).

3. Added regression tests:
   - readSurface rejects whitespace-only baseProfile and warns
   - writeSurface rejects whitespace-only baseProfile, non-string baseProfile,
     and null surfaceState

Refs #3662

* surface: warn on unknown baseProfile mode names in read and write

Applies a Codex review finding on PR #3666:

readSurface and writeSurface used to accept any non-blank string as
baseProfile. A typo like {"baseProfile":"standrad"} would pass validation,
then resolveProfile() in install-profiles.cjs would silently fall back to
'full' with no diagnostic — the same silent-degradation symptom that #3662
was filed to fix, just through a different code path.

Both functions now split baseProfile by comma, validate each mode against
the registered PROFILES set ('core', 'standard', 'full'), and emit a single
[gsd] console.warn line that names the unknown modes and lists the valid
ones. The state is still parsed/written — resolveProfile() decides the
actual resolution fallback. Composed profiles where some modes are valid
and some are not warn only about the unknown subset.

Side note: the pre-existing 'round-trips composed base profile' test used
'core,audit' as a stand-in composed string. 'audit' is not a registered
profile (the three known profiles are 'core', 'standard', 'full'), so the
test was relying on the old lack of validation. Switched to 'core,standard'
to preserve the round-trip intent without producing diagnostic noise.

Refs #3662

* changeset: include blank/typo baseProfile in documented read failure cases

CodeRabbit minor finding on PR #3666 — the changeset wording only mentioned
"missing/non-string baseProfile" but the implementation also rejects blank
(including whitespace-only) baseProfile values, and warns on typo'd /
unknown profile mode names. Updated to match actual behavior.

* changeset: pr field should be PR number, not issue number

Codex review finding on PR #3666. The changeset's `pr: 3662` was the linked
issue (#3662), but the convention across other .changeset/*.md files is that
`pr:` carries the PR number. Verified by spot-checking other changesets
(2937 → pr: 3515, 3298 → pr: 3306, 3541 → pr: 3547 — all PR numbers).
Updated to `pr: 3666`. The body text still references the issue.

* surface: reject comma-only baseProfile + warn on wrong-typed optional fields

Three Gemini review findings on PR #3666 — all spirit-of-#3662 edge cases:

1. Comma-only baseProfile bypasses validation. readSurface used to accept
   baseProfile: ", ," because trim() returned "," (non-empty). Downstream
   resolveProfile() would split-and-filter to [] and silently fall back to
   'full'. Added effectiveProfileModes() helper that splits, trims, filters
   empty — both readSurface and writeSurface now reject when the result is
   empty. Same silent-degradation symptom as the original bug.

2. Wrong-typed optional fields silently coerced. readSurface used to coerce
   {disabledClusters: 42} to {disabledClusters: []} with no diagnostic.
   Now warns via mistypedOptionalFields() before normalizeSurfaceState()
   does the coercion, in both reader and writer.

3. Missing test coverage for the EACCES branch in readSurface. Added a
   chmod-000 unreadable-file test, skipped on Windows and root accounts
   (mode bits are ignored on those platforms).

48/48 surface tests pass. Final codex pass returned LGTM on the prior
state; these three additions strengthen the same lenient/loud contract.

Refs #3662
2026-05-18 00:14:07 -04:00
Tom Boucher
d0f916728b feat(skill-surface): install-time profiles + runtime /gsd:surface (#3408) (#3456)
* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references

Mechanical migration from docs/research/data/2026-05-12-skill-audit.json.
Every skill whose body references another GSD skill now declares those
dependencies in `requires:` YAML frontmatter (flow-style array).

Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`,
which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled
by the core loop but was never in the allowlist. The profile closure model
(ADR-0010 Phase 1) resolves this automatically.

Closes part of #3408.

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

* feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO

Implements the Skill Surface Budget Module core (ADR-0010, Phase 1):

- PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*')
- loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md
  into a Map<stem, string[]> without external YAML dep
- resolveProfile({modes, manifest}): computes transitive closure over the
  requires: graph; composable (modes=['core','audit'] unions closures)
- stageSkillsForProfile / stageAgentsForProfile: filesystem staging with
  same exit-cleanup machinery as the legacy stageSkillsForMode
- readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip
- Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode,
  shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass

The phase latent bug is now resolved by closure: discuss-phase, plan-phase,
and execute-phase all require phase, so any profile including any of them
automatically includes phase via transitive closure.

Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green).
Back-compat anchor: 80/80 passing.

Closes part of #3408.

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

* feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries

Two lint checks (scripts/lint-skill-deps.cjs):
  a) Frontmatter-body consistency: skill body references must appear in requires:
  b) Profile closure: every requires: dep of any profile skill must be in closure

Running the lint revealed 19 body references missed by the audit JSON (the
audit used static analysis; some bodies have conditional references). Fixed:
  complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone
  fast: +quick
  health: +thread
  map-codebase: +new-project, plan-phase
  new-milestone, new-project, review, ultraplan-phase: +plan-phase
  ship: +verify-work
  sketch, spike: +new-project
  verify-work: +execute-phase
  workstreams: +new-milestone, resume-work

Wired into package.json as lint:skill-deps and added to pretest.
8 fixture-based tests: all green.

Closes part of #3408.

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

* feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js

- Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable).
  Mutually exclusive with --minimal / --core-only (aliases for --profile=core).
  Default (no flag): full.
- Import readActiveProfile / writeActiveProfile from install-profiles.cjs.
- After writeManifest: persist active profile to .gsd-profile marker.
- gsd update path: if no --profile flag given, read existing .gsd-profile
  marker so non-full profiles are not silently re-expanded to full (ADR-0010).
- Update --help block to document --profile= with per-tier token costs.

New test: install-minimal-backcompat.test.cjs (6 tests):
  - PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract)
  - --minimal writes .gsd-profile marker "core"
  - --profile=core, --profile=standard write correct markers
  - default install writes marker "full"

Closes part of #3408.

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

* chore(changeset): add feat-3408-skill-profiles changelog fragment

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

* feat(install-profiles): derive agents from skill body refs and wire into resolveProfile

Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce
a non-empty agents Set instead of always returning empty. resolveProfile() scans
each skill body for gsd-* agent name references (via new parseCallsAgents()),
stores them in _calls_agents_<stem> manifest entries, and unions them across the
resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents
— it now gets real data so tiered profiles install the correct subset of agents
instead of zero.

Closes #3408 (partial — Deviation 1 only)

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

* feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile

Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now
actually honored when re-running without explicit flags (e.g. gsd update). The
dead-end logging block is replaced by resolveEffectiveProfile(), which picks the
marker profile over 'full' when no explicit --profile= flag was given. The resolved
profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills)
so updates install only the previously-chosen skill subset.

--minimal retains its back-compat behavior (strict 6-skill allowlist, no closure)
while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers
that need to reconcile disagreeing markers across runtimes (smallest skill set wins).

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

* feat(surface): add CLUSTERS data + state IO module

Add clusters.cjs with 10 named skill groups covering all 66 skills
(verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/
writeSurface atomic IO, resolveSurface, applySurface, and listSurface.
Tests: 17 passing (11 state IO + 6 cluster integrity).

Closes #3408

* docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment)

Records the install-time profile staging decision (Phase 1, landed) and the
runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an
amendment. Updates the ADR README index.

Closes #3408

* docs(install-profiles): update module docblock for Phase 2 and ADR-0011

Corrects the ADR reference from 0010 to 0011, documents the three-profile
model and back-compat aliases, adds resolveEffectiveProfile precedence rule,
and notes the companion surface.cjs Phase 2 engine.

* docs(context): add Skill Surface Budget Module canonical entry

Adds the Domain terms entry for the Skill Surface Budget Module covering
both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2
(runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json),
per ADR-0011 Consequences requirement.

* feat(surface): add resolveSurface and applySurface engine + tests

Tests cover: profile → surface equivalence, cluster disable/enable,
explicitAdds transitive closure, applySurface file sync (add missing,
remove superseded, preserve non-gsd files), listSurface token cost.
16 new tests passing.

* docs(readme): document --profile= flag and /gsd:surface command

Brief user-facing mention of install profiles (core/standard/full) and the
/gsd:surface slash command in the Commands table. Points to ADR-0011 for details.

* feat(surface): add /gsd:surface slash command runbook

New skill: gsd:surface — runtime profile/cluster toggle without reinstall.
Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset.
Persists state to .gsd-surface.json (independent of .gsd-profile).
Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations.

* feat(surface): add changeset fragment for /gsd:surface runtime toggle

* feat(surface): add surface skill stem to utility cluster

surface.md is a new skill; add it to the utility cluster so the
surface-clusters.test.cjs coverage invariant stays satisfied.

* docs(adr): fix ADR references to 0011 and record Phase 2 as shipped

ADR-0010 number was already claimed by the file-operation-engine ADR; this
ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR
references in clusters.cjs, surface.cjs, install-profiles.cjs, and the
Phase 2 changeset to ADR-0011. Update the ADR Status section to record
Phase 2 artifacts as shipped on this branch rather than "in progress".

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

* docs(research): port skill-surface-budget memo and audit data

ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and
docs/research/data/2026-05-12-skill-audit.json, which only existed in the
research worktree. Port both onto this branch so the ADR's References
section resolves and reviewers can read the cluster taxonomy (§3.2),
dependency topology (§3.1), and option grading (§4) that justify Phase 1
and Phase 2 decisions.

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

* fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md

- surface.md: convert allowed-tools from inline YAML array to block style
  (was parsed as a single tool name "[Read, Write, Bash]" by test harness)
- docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs;
  add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67
- docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs,
  and /gsd-surface (filename-based command key)
- docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands
- get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section

Fixes registration failures introduced by Phase 2 of #3408.

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

* fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens

Two PR regressions introduced earlier on this branch:

1. surface.cjs JSDoc comments contained the canonical paths
   (~/.claude/commands/gsd, ~/.claude/agents) as example values, which the
   cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents
   |hooks)) flagged as install-time path leaks. Reworded the docblocks to
   describe runtime-resolved paths without literal ~/.claude tokens.

2. The ported research memo proposed hypothetical Option C dispatchers
   using slash syntax (/gsd:milestone, /gsd:research). The
   docs-parity-live-registry test enforces that every slash-command token
   in docs/ resolves to a real command. Rewrote the Option C sketch
   without the slash prefix and added a clarifying note that the
   dispatchers are illustrative, not shipped.

Targeted tests now pass: tests/cline-install.test.cjs and
tests/docs-parity-live-registry.test.cjs both green.

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

* test: remove raw output/source grep in lint tests

* fix: close coderabbit profile and requires issues

* test: align surface token-cost assertion wording

* fix(install): align core profile alias and defer profile marker write

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 12:45:16 -04:00