* 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>
198 lines
8.7 KiB
JavaScript
198 lines
8.7 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Tests for resolveProfile — computes transitive closure over the requires: graph.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('path');
|
|
|
|
const {
|
|
PROFILES,
|
|
resolveProfile,
|
|
loadSkillsManifest,
|
|
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
|
|
|
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
|
|
|
describe('PROFILES map', () => {
|
|
test('PROFILES is frozen', () => {
|
|
assert.ok(Object.isFrozen(PROFILES));
|
|
});
|
|
|
|
test('PROFILES has core, standard, full keys', () => {
|
|
assert.ok('core' in PROFILES, 'PROFILES.core missing');
|
|
assert.ok('standard' in PROFILES, 'PROFILES.standard missing');
|
|
assert.ok('full' in PROFILES, 'PROFILES.full missing');
|
|
});
|
|
|
|
test('PROFILES.core contains the 6 main-loop skills', () => {
|
|
const core = PROFILES.core;
|
|
assert.ok(Array.isArray(core), 'core should be an array');
|
|
const sorted = [...core].sort();
|
|
assert.deepStrictEqual(sorted, [
|
|
'discuss-phase',
|
|
'execute-phase',
|
|
'help',
|
|
'new-project',
|
|
'plan-phase',
|
|
'update',
|
|
]);
|
|
});
|
|
|
|
test('PROFILES.full is the sentinel "*"', () => {
|
|
assert.strictEqual(PROFILES.full, '*');
|
|
});
|
|
|
|
test('PROFILES.standard contains at least the core skills', () => {
|
|
const core = new Set(PROFILES.core);
|
|
const standard = PROFILES.standard;
|
|
assert.ok(Array.isArray(standard), 'standard should be an array');
|
|
for (const s of core) {
|
|
assert.ok(standard.includes(s), `standard should include core skill: ${s}`);
|
|
}
|
|
});
|
|
|
|
test('PROFILES.standard has at least 10 skills', () => {
|
|
assert.ok(PROFILES.standard.length >= 10, `standard should have >=10 skills, got ${PROFILES.standard.length}`);
|
|
});
|
|
});
|
|
|
|
describe('resolveProfile', () => {
|
|
test('defaults to full when called with no args', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ manifest });
|
|
assert.strictEqual(result.name, 'full');
|
|
assert.strictEqual(result.skills, '*');
|
|
});
|
|
|
|
test('resolves core profile — returns 6+ skills (closure adds phase)', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ modes: ['core'], manifest });
|
|
assert.strictEqual(result.name, 'core');
|
|
assert.ok(result.skills instanceof Set, 'skills should be a Set');
|
|
// core has 6 base; closure adds phase (referenced by discuss/plan/execute/new-project)
|
|
// and config (referenced by discuss-phase, new-project), and more
|
|
assert.ok(result.skills.size >= 6, `core closure should have >=6 skills, got ${result.skills.size}`);
|
|
// All 6 base skills must be present
|
|
for (const s of PROFILES.core) {
|
|
assert.ok(result.skills.has(s), `core closure should include ${s}`);
|
|
}
|
|
// phase must be included via closure (discuss-phase, plan-phase, etc. require it)
|
|
assert.ok(result.skills.has('phase'), 'core closure must include phase (required by discuss/plan/execute-phase)');
|
|
});
|
|
|
|
test('resolves standard profile — returns superset of core', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const coreResult = resolveProfile({ modes: ['core'], manifest });
|
|
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
|
assert.strictEqual(stdResult.name, 'standard');
|
|
assert.ok(stdResult.skills instanceof Set);
|
|
assert.ok(stdResult.skills.size >= coreResult.skills.size, 'standard should have >= skills than core');
|
|
// All core closure skills must be in standard
|
|
for (const s of coreResult.skills) {
|
|
assert.ok(stdResult.skills.has(s), `standard must include core skill: ${s}`);
|
|
}
|
|
});
|
|
|
|
test('resolves full profile — returns sentinel', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ modes: ['full'], manifest });
|
|
assert.strictEqual(result.name, 'full');
|
|
assert.strictEqual(result.skills, '*');
|
|
});
|
|
|
|
test('composable profiles — core,standard union is same as standard', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const stdResult = resolveProfile({ modes: ['standard'], manifest });
|
|
const composed = resolveProfile({ modes: ['core', 'standard'], manifest });
|
|
// name should reflect composed
|
|
assert.ok(composed.name.includes('core') && composed.name.includes('standard'),
|
|
`composed name should include both, got: ${composed.name}`);
|
|
// skills union should equal standard (since core ⊂ standard)
|
|
for (const s of stdResult.skills) {
|
|
assert.ok(composed.skills.has(s), `composed should include standard skill: ${s}`);
|
|
}
|
|
});
|
|
|
|
test('transitive closure: skill that requires phase pulls in phase', () => {
|
|
// Build a minimal manifest: only discuss-phase requiring phase
|
|
const manifest = new Map([
|
|
['discuss-phase', ['phase']],
|
|
['phase', []],
|
|
['help', []],
|
|
]);
|
|
// Profile with only discuss-phase in base
|
|
const miniProfiles = { core: ['discuss-phase', 'help'], full: '*', standard: ['discuss-phase', 'help'] };
|
|
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
|
assert.ok(result.skills.has('phase'), 'phase should be pulled in via closure from discuss-phase');
|
|
assert.ok(result.skills.has('discuss-phase'));
|
|
assert.ok(result.skills.has('help'));
|
|
});
|
|
|
|
test('deep transitive closure works (A→B→C pulls in C)', () => {
|
|
const manifest = new Map([
|
|
['a', ['b']],
|
|
['b', ['c']],
|
|
['c', []],
|
|
]);
|
|
const miniProfiles = { core: ['a'], full: '*', standard: ['a'] };
|
|
const result = resolveProfile({ modes: ['core'], manifest, _profilesOverride: miniProfiles });
|
|
assert.ok(result.skills.has('a'));
|
|
assert.ok(result.skills.has('b'));
|
|
assert.ok(result.skills.has('c'));
|
|
});
|
|
|
|
test('resolveProfile result has agents Set', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ modes: ['core'], manifest });
|
|
assert.ok(result.agents instanceof Set, 'result should have agents Set');
|
|
});
|
|
|
|
test('resolveProfile standard — agents Set is non-empty (plan-phase pulls gsd-planner etc)', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ modes: ['standard'], manifest });
|
|
assert.ok(result.agents instanceof Set, 'agents should be a Set');
|
|
assert.ok(result.agents.size > 0, `standard profile should have >0 agents, got ${result.agents.size}`);
|
|
// plan-phase is in standard and calls gsd-planner, gsd-plan-checker, gsd-phase-researcher
|
|
assert.ok(result.agents.has('gsd-planner'), 'standard should include gsd-planner (called by plan-phase)');
|
|
assert.ok(result.agents.has('gsd-plan-checker'), 'standard should include gsd-plan-checker (called by plan-phase)');
|
|
});
|
|
|
|
test('resolveProfile full — agents is empty Set (full staging uses srcDir directly)', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const result = resolveProfile({ modes: ['full'], manifest });
|
|
assert.strictEqual(result.skills, '*');
|
|
// Full profile: agents Set is empty because stageAgentsForProfile uses srcDir directly
|
|
assert.ok(result.agents instanceof Set, 'agents should still be a Set for full');
|
|
});
|
|
|
|
test('agents are derived from skill body text — synthetic manifest', () => {
|
|
// Build a synthetic manifest where plan-phase calls gsd-planner
|
|
const manifest = new Map([
|
|
['plan-phase', []],
|
|
['phase', []],
|
|
]);
|
|
// Override with calls_agents map
|
|
manifest.get('plan-phase'); // ensure it exists
|
|
// Use the real loadSkillsManifest with REAL dir to verify body parsing works
|
|
const realManifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
const planPhaseAgents = realManifest.get('_calls_agents_plan-phase') ||
|
|
// calls_agents may be stored as a separate key or on the manifest entry itself
|
|
[];
|
|
// This test validates that the real manifest has agent mappings for plan-phase
|
|
// by checking resolveProfile computes agents correctly
|
|
const result = resolveProfile({ modes: ['standard'], manifest: realManifest });
|
|
assert.ok(result.agents.has('gsd-planner'), 'gsd-planner should be derived from plan-phase body');
|
|
});
|
|
|
|
test('agents transitively closed — skill requiring plan-phase also gets its agents', () => {
|
|
const manifest = loadSkillsManifest(REAL_COMMANDS_DIR);
|
|
// quick requires plan-phase (via requires: field or direct) and also calls gsd-planner directly
|
|
// new-project requires plan-phase so inherits its agents
|
|
const result = resolveProfile({ modes: ['standard'], manifest });
|
|
// Since plan-phase is in standard, and plan-phase calls gsd-planner, gsd-planner must be present
|
|
assert.ok(result.agents.has('gsd-planner'));
|
|
});
|
|
});
|