Merge branch 'next' into fix/2009-load-failed-capability-injects-blocking-

This commit is contained in:
Tom Boucher
2026-07-07 23:12:09 -04:00
committed by GitHub
64 changed files with 3060 additions and 365 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2065
---
**Phases that integrate an external API/SDK/service can no longer seal without a decided coverage matrix** — a new `api-coverage` gate on the `ai-integration` capability blocks `/gsd:verify-work` until the phase produces a `COVERAGE.md` enumerating the API's full capability surface, with every non-integrated capability an explicit, reasoned opt-out. Full coverage is the default; the matrix is the subtraction record, so "we integrated the API" can no longer silently mean "we integrated whatever the first use case exercised." Toggleable via `workflow.api_coverage_gate` (on by default). (#1562)

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2076
---
**`gsd-tools effort sync` no longer crashes in an installed runtime.** In any global install (e.g. `~/.claude/gsd-core/`), `effort sync` threw `Cannot find module '../../../bin/install.js'` — the command reached into the package-root `bin/install.js` for its install-time effort resolvers, but the installer only copies the `gsd-core/` subtree into a runtime home, so that file is never present there. As a result, `effort` config changes (`routing_tier_defaults` / `agent_overrides`) silently never reached installed agents without a full reinstall. The two resolvers (`readGsdEffectiveEffortConfig` + `resolveInstallTimeEffort`, with their helpers) are now extracted into a shipped `gsd-core/bin/lib/install-effort-resolver.cjs` that both `effort sync` and the installer import — a single source of truth that is always present in the installed tree. (#2076)

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2074
---
**`model_overrides` and per-phase-type models now actually apply to the assumptions-analyzer, code-reviewer, and code-fixer agents on Claude Code.** Previously `model_overrides["gsd-code-reviewer"]` / `["gsd-assumptions-analyzer"]` / `["gsd-code-fixer"]` (and `models.verification` / `models.discuss` / `models.execution`) were accepted and resolved but silently dropped — the workflows spawned these agents with no model, so they inherited the session model and the configured routing never took effect (no warning). Every spawn now threads its resolved model: `discuss-phase-assumptions`, `code-review`, and `code-review-fix` (both the re-review and the two fixer spawns) resolve it inline, and `quick`'s review step uses the code-reviewer's own resolved model instead of the executor's. The stale "`discuss` — reserved, no subagent" model-profile docs are corrected to list `gsd-assumptions-analyzer`, and the `verification` row now includes `gsd-code-reviewer`. (#2074)

1
.gitignore vendored
View File

@@ -70,6 +70,7 @@ build/
/gsd-core/bin/lib/host-integration.cjs
/gsd-core/bin/lib/host-integration-sdk.cjs
/gsd-core/bin/lib/handshake-serialized.cjs
/gsd-core/bin/lib/install-effort-resolver.cjs
/gsd-core/bin/lib/install-engine.cjs
/gsd-core/bin/lib/embedding-adapter.cjs
/gsd-core/bin/lib/adapter-declarative.cjs

View File

@@ -273,60 +273,20 @@ const {
} = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
const {
resolveTierEntry: gsdResolveTierEntry,
EFFORT_SET: GSD_EFFORT_SET,
} = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
// #443 — model-catalog and config-defaults.manifest.json exports needed only
// by effort-resolution code paths (resolveInstallTimeEffort /
// generateCodexAgentToml / Claude .md effort injection). Loaded lazily the
// first time they are needed so that requiring install.js in test contexts that
// never trigger an install does NOT produce module-load-time side effects (the
// manifest read + hard throw) that could alter subprocess exit codes or stderr.
let _gsdEffortCatalogCache = null;
function _getGsdEffortCatalog() {
if (_gsdEffortCatalogCache) return _gsdEffortCatalogCache;
const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
const manifestPath = path.join(
__dirname,
'..',
'gsd-core',
'bin',
'shared',
'config-defaults.manifest.json'
);
let manifestData;
try {
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
} catch (_err) {
// Fail loudly — a missing manifest is a broken install, not a soft degradation.
throw new Error(
`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${_err.message}`
);
}
const tierDefaults =
(manifestData.effort &&
manifestData.effort.routing_tier_defaults &&
typeof manifestData.effort.routing_tier_defaults === 'object' &&
!Array.isArray(manifestData.effort.routing_tier_defaults))
? manifestData.effort.routing_tier_defaults
: { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid
const effortDefault =
(manifestData.effort && typeof manifestData.effort.default === 'string')
? manifestData.effort.default
: 'high'; // guard: unreachable if manifest is valid
_gsdEffortCatalogCache = {
AGENT_DEFAULT_TIERS,
renderEffortForRuntime,
EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
EFFORT_MANIFEST_DEFAULT: effortDefault,
};
return _gsdEffortCatalogCache;
}
// #2071 — install-time effort resolution (readGsdEffectiveEffortConfig /
// resolveInstallTimeEffort, plus their _getGsdEffortCatalog + _readGsdConfigFile
// helpers) was extracted into the shipped gsd-core/bin/lib/install-effort-resolver.cjs
// so `gsd-tools effort sync` can require it from the installed runtime instead of this
// package-root bin/install.js, which the installer never copies (#2071 crash). The
// installer imports it back here — single source of truth for both surfaces.
const {
readGsdEffectiveEffortConfig,
resolveInstallTimeEffort,
_getGsdEffortCatalog,
_readGsdConfigFile,
} = require(path.join(_gsdLibDir, 'install-effort-resolver.cjs'));
const {
MINIMAL_SKILL_ALLOWLIST,
@@ -1071,126 +1031,6 @@ function readGsdEffectiveModelOverrides(targetDir = null) {
return { ...(global || {}), ...(projectOverrides || {}) };
}
/**
* #443 — Read the merged `effort` config block for install-time effort resolution.
*
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
* `effort` object instead of the model-profile fields.
*
* Returns the merged `effort` object or null when neither source defines one.
* The caller can pass this to resolveInstallTimeEffort() which is pure and
* requires no filesystem access beyond what this helper already performs.
*
* @param {string|null} targetDir Runtime install root (walks up to find .planning/).
* @returns {object|null}
*/
function readGsdEffectiveEffortConfig(targetDir = null) {
const homeDefaults = _readGsdConfigFile(
path.join(os.homedir(), '.gsd', 'defaults.json'),
'~/.gsd/defaults.json'
);
let projectConfig = null;
if (targetDir) {
let probeDir = path.resolve(targetDir);
for (let depth = 0; depth < 8; depth += 1) {
const candidate = path.join(probeDir, '.planning', 'config.json');
if (fs.existsSync(candidate)) {
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
break;
}
const parent = path.dirname(probeDir);
if (parent === probeDir) break;
probeDir = parent;
}
}
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
? homeDefaults.effort
: null;
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
? projectConfig.effort
: null;
if (!homeEffort && !projectEffort) return null;
// Per-project wins on conflict within each sub-field. Merge field-by-field so
// a project config that only sets agent_overrides still inherits global
// routing_tier_defaults and default.
return {
...(homeEffort || {}),
...(projectEffort || {}),
// Deep-merge agent_overrides (project wins per-key)
agent_overrides: {
...((homeEffort && homeEffort.agent_overrides) || {}),
...((projectEffort && projectEffort.agent_overrides) || {}),
},
};
}
/**
* #443 — Resolve install-time effort for a given agent, using the same
* precedence chain as resolveEffortInternal() in core.cjs, but operating
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
*
* Precedence (mirrors resolveEffortInternal):
* 1. effortCfg.agent_overrides[agentName]
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
* — OR manifest tier defaults when effortCfg is null
* 3. effortCfg.default
* 4. 'high' (hardcoded fallback)
*
* @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig().
* @param {string} agentName e.g. 'gsd-planner'
* @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal)
*/
function resolveInstallTimeEffort(effortCfg, agentName) {
// Validates each candidate against the canonical EFFORT_SET (sourced once
// from core.cjs) before accepting it, mirroring resolveEffortInternal exactly.
// Invalid values fall through to the next precedence layer; final fallback 'high'.
// Step 1: agent_overrides
if (effortCfg) {
const ao = effortCfg.agent_overrides;
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
const v = ao[agentName];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
}
}
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
const agentTier = AGENT_DEFAULT_TIERS[agentName];
if (agentTier) {
if (effortCfg && effortCfg.routing_tier_defaults &&
typeof effortCfg.routing_tier_defaults === 'object' &&
!Array.isArray(effortCfg.routing_tier_defaults)) {
const v = effortCfg.routing_tier_defaults[agentTier];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
} else if (!effortCfg) {
// No effort config — use manifest tier defaults
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
}
// effortCfg exists but has no routing_tier_defaults — fall through
}
// Step 3: effort.default
if (effortCfg) {
const d = effortCfg.default;
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
}
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
// If even the manifest default is invalid, fall back to 'high'.
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
return EFFORT_MANIFEST_DEFAULT;
}
return 'high';
}
/**
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
* file in a newline-agnostic way (LF and CRLF source files are both handled).
@@ -1296,29 +1136,6 @@ const READONLY_AGENT_DISALLOWED_TOOLS = {
'gsd-ui-auditor': 'Edit, MultiEdit',
};
/**
* #2517 — Read a single GSD config file (defaults.json or per-project
* config.json) into a plain object, returning null on missing/empty files
* and warning to stderr on JSON parse failures so silent corruption can't
* mask broken configs (review finding #5).
*/
function _readGsdConfigFile(absPath, label) {
if (!fs.existsSync(absPath)) return null;
let raw;
try {
raw = fs.readFileSync(absPath, 'utf-8');
} catch (err) {
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
return null;
}
try {
return JSON.parse(raw);
} catch (err) {
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
return null;
}
}
/**
* #2517 — Build a runtime-aware tier resolver for the install path.
*

View File

@@ -30,6 +30,11 @@
"type": "boolean",
"default": true,
"description": "Prompt for an AI-SPEC design contract before planning phases that involve AI systems."
},
"workflow.api_coverage_gate": {
"type": "boolean",
"default": true,
"description": "Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre the planner is prompted to enumerate the API surface into COVERAGE.md; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out. Independent of ai_integration_phase (applies to any external-API integration, not only AI)."
}
},
"steps": [
@@ -48,6 +53,32 @@
"onError": "skip"
}
],
"contributions": [],
"gates": []
"contributions": [
{
"point": "plan:pre",
"into": "planner",
"fragment": {
"path": "fragments/api-coverage-plan-pre.md"
},
"produces": [
"COVERAGE.md"
],
"consumes": [
"CONTEXT.md"
],
"when": "workflow.api_coverage_gate",
"onError": "skip"
}
],
"gates": [
{
"point": "verify:pre",
"check": {
"query": "api-coverage.verify-pre"
},
"when": "workflow.api_coverage_gate",
"blocking": true,
"onError": "halt"
}
]
}

View File

@@ -0,0 +1,105 @@
# API Coverage Decision Checkpoint
> Full API Coverage by Default — Opt Out, Never Opt In. Fires when a phase
> integrates an external API / SDK / service. Most non-API phases will not fire
> it — that is the point.
## Why this exists
"We integrated the API" too often silently means "we integrated whatever the
first use case exercised." Every un-built capability is then an invisible hole,
discovered later by a user who reasonably expected it to work. The phase sealed
green because its tasks completed; nobody decided the gaps were acceptable,
because nobody enumerated them. This checkpoint makes the surface **visible and
decided** before the phase can seal.
## Detect whether this phase integrates an external API
The detector is a deterministic scan over the phase scope. It strips fenced
code blocks first, so a trigger term inside a code snippet does not fire. It
returns a typed result: `{ detected, signals[], terms }`. Run it on the phase
scope (the concatenation of this phase's ROADMAP section + the PLAN body):
```bash
SCOPE="$(cat "${PHASE_DIR}"/*-PLAN.md 2>/dev/null) $(gsd_run query roadmap.get-phase "${PHASE}" 2>/dev/null || true)"
API_COVERAGE_JSON=$(printf '%s' "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs --json 2>/dev/null || echo '{"detected":false,"signals":[]}')
```
Read `API_COVERAGE_JSON.detected`. Act on it only — do **not** pattern-match the
prose yourself.
**If `detected` is `false`:** this phase does not integrate an external API. Skip
the checkpoint entirely and continue planning. Do not raise it with the user.
**If `detected` is `true`:** an external-API integration is in scope. You MUST
produce a **coverage matrix** before the plan is finalized.
## Produce the coverage matrix
Enumerate the external API's full **capability surface** — the verb/endpoint/method
list (e.g. for a music service: `search`, `play`, `pause`, `skip`, `set_volume`,
`get_playlist`, `create_playlist`, `add_to_playlist`, …). For each capability
record a decision, starting from **full coverage** as the default:
| capability | decision | reason |
|---|---|---|
| `<capability-id>` | `INTEGRATE` \| `OPT-OUT` | `<one-line reason if OPT-OUT>` |
Rules:
- **`INTEGRATE` is the default.** Every capability starts as INTEGRATE; the
matrix is the *subtraction record*.
- **Every `OPT-OUT` MUST carry a one-line reason** (`not needed`, `not needed
yet`, `explicitly out of scope`, …). An opt-out without a reason is an
un-decided hole — the exact failure mode this gate exists to close.
- **A second integration against the same need** (e.g. a second platform for the
same capability) starts from the **same full-coverage baseline** as the first.
Do not carry over the first integration's opt-outs silently — re-decide each
capability for the new surface, so a first-class/fallback asymmetry cannot
accumulate.
Write the matrix to `${PHASE_DIR}/COVERAGE.md` (canonical markdown-table form):
```markdown
# API Coverage — <service>
> Full coverage by default. Opt-outs are explicit, reasoned decisions.
| capability | decision | reason |
|---|---|---|
| search | INTEGRATE | |
| playlists | INTEGRATE | |
| skip | OPT-OUT | not needed yet — tracked for follow-up phase |
```
A fenced ` ```coverage ` JSON block is also accepted for machine-generated
matrices; the markdown table is preferred (human-editable, diff-friendly).
## The seal-time gate
This checkpoint is enforced. At `verify:pre` the `api-coverage.verify-pre` gate
runs `check api-coverage.verify-pre <phase-dir>`:
- If `COVERAGE.md` exists, it is validated — every row needs a valid decision and
every `OPT-OUT` a reason. A malformed/partial matrix **blocks the seal**.
- If `COVERAGE.md` is absent, the detector runs again over the phase scope. If a
strong external-API-integration signal is found, the seal is **blocked** until a
matrix is produced. If no signal is found, the phase is treated as a non-API
phase and the seal proceeds.
So: an API-integrating phase cannot seal without a decided matrix. Produce it at
plan time; do not leave it for seal time.
## Tuning the vocabulary (optional)
The trigger vocabulary is a curated, additive-only set in
`gsd-core/bin/lib/api-coverage.cjs` (`DEFAULT_API_COVERAGE_TERMS`). To widen it
for a project, override at the call site:
```bash
printf '%s' "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs --json \
--verbs integrate,wrap,connect,embed --nouns api,sdk,rest,grpc,webhook,plugin
```
The whole checkpoint is toggleable via `workflow.api_coverage_gate` in
`.planning/config.json`.

View File

@@ -325,6 +325,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `workflow.cross_ai_timeout` | number | `300` | Timeout in seconds for cross-AI execution commands. Prevents runaway external processes. Added in v1.36 |
| `workflow.test_gate_timeout` | number | `600` | Wall-clock timeout (seconds) for a verification test gate; a watch-mode runner (vitest/jest) that never exits is aborted after this budget instead of hanging the orchestrator (#1857) |
| `workflow.ai_integration_phase` | boolean | `true` | Enable the `/gsd-ai-integration-phase` command. When `false`, the command exits with a configuration gate message |
| `workflow.api_coverage_gate` | boolean | `true` | Require an explicit API-coverage decision before a phase that integrates an external API/SDK/service can seal. At `plan:pre` the planner is prompted to produce a `COVERAGE.md` matrix (full coverage by default, every opt-out reasoned); at `verify:pre` a blocking gate fails the seal unless the matrix is complete. Independent of `ai_integration_phase` (#1562) |
| `workflow.auto_prune_state` | boolean | `false` | When `true`, automatically prune stale entries from STATE.md at phase boundaries instead of prompting |
| `workflow.pattern_mapper` | boolean | `true` | Run the `gsd-pattern-mapper` agent between research and planning to map new files to existing codebase analogs |
| `workflow.subagent_timeout` | number | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes) |
@@ -1114,10 +1115,10 @@ for the change to take effect. See issue #2256.
| Phase type | Agents |
|---|---|
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `discuss` | (reserved — no subagent today) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `completion` | (reserved — no subagent today) |
`discuss` and `completion` are accepted by the schema for forward compatibility; setting them today is a no-op until a subagent maps to them.

View File

@@ -2751,10 +2751,10 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
| Slot | Agents assigned |
|------|-----------------|
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `discuss` | (reserved for future subagent) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `completion` | (reserved for future subagent) |
**Accepted values:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"`

View File

@@ -207,6 +207,7 @@
"agent-skills-bootstrap.md",
"ai-evals.md",
"ai-frameworks.md",
"api-coverage.md",
"artifact-types.md",
"autonomous-smart-discuss.md",
"checkpoints.md",
@@ -292,6 +293,7 @@
"adr-parser.cjs",
"agent-command-router.cjs",
"agent-install-check.cjs",
"api-coverage.cjs",
"artifacts.cjs",
"assumption-delta.cjs",
"audit-command-router.cjs",
@@ -354,6 +356,7 @@
"host-integration.cjs",
"init-command-router.cjs",
"init.cjs",
"install-effort-resolver.cjs",
"install-engine.cjs",
"install-profiles.cjs",
"installer-migration-authoring.cjs",

View File

@@ -331,6 +331,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
| `autonomous-smart-discuss.md` | Smart-discuss logic for autonomous mode. |
| `ios-scaffold.md` | iOS application scaffolding patterns. |
| `ai-evals.md` | AI evaluation design reference for `/gsd-ai-integration-phase`. |
| `api-coverage.md` | API-coverage gate reference (full-coverage-by-default) for the `ai-integration` capability's `verify:pre` blocking gate (#1562) — matrix format, trigger, tuning, detector CLI. |
| `ai-frameworks.md` | AI framework decision-matrix reference for `gsd-framework-selector`. |
| `executor-examples.md` | Worked examples for the gsd-executor agent. |
| `doc-conflict-engine.md` | Shared conflict-detection contract for ingest/import workflows. |
@@ -397,6 +398,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `active-workstream-store.cjs` | Workstream source precedence and selection (CLI `--ws` > `GSD_WORKSTREAM` env > stored pointer); name validation and environment propagation |
| `adr-parser.cjs` | ADR decision parser for plan-phase ingest express path; normalizes section synonyms, parses status/decision/scope fences, and enforces status rejection gates |
| `agent-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools agent` |
| `api-coverage.cjs` | API-coverage detector + matrix validator (#1562) — pure `detectApiIntegration` (compound verb+noun signal + `<Service> API/SDK` surface; strips fenced code) and `validateCoverageMatrix`/`parseCoverageMatrix`/`renderCoverageMatrix` for the COVERAGE.md artifact; STDIN CLI (`echo "$SCOPE" \| node .../api-coverage.cjs [--json]`, exit 0=detected/1=none/2=error); consumed by the `ai-integration` capability's `plan:pre` contribution and blocking `verify:pre` gate (`check api-coverage.verify-pre`) |
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
| `audit-command-router.cjs` | ADR-959 capability command router for `gsd-tools audit-uat` and `gsd-tools audit-open` — extracted from hardcoded cases in `gsd-tools.cjs`; dispatches to `uat.cjs:cmdAuditUat` and `audit.cjs:{auditOpenArtifacts,formatAuditReport}`; phase 4d-impl-3 |
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
@@ -449,6 +451,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` |
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |
| `init.cjs` | Compound context loading for each workflow type |
| `install-effort-resolver.cjs` | Install-time effort resolution — `readGsdEffectiveEffortConfig` (merges `~/.gsd/defaults.json` + project `.planning/config.json`) + `resolveInstallTimeEffort`, extracted from `bin/install.js` (#2071) so `gsd-tools effort sync` can require it from the shipped runtime instead of the never-copied package-root installer; install.js imports them back (single source) |
| `install-engine.cjs` | Runtime-artifact install engine — `installRuntimeArtifacts`/`uninstallRuntimeArtifacts`/`installOpencodeFamilySkills` + their helpers, extracted from `bin/install.js` (ADR-1239 Phase B, #1679); install.js imports them back and injects `getCommitAttribution` |
| `install-profiles.cjs` | Install profile allowlist + skill staging for `--minimal` install (#2762); single source of truth for which `gsd-*` skills/agents land in runtime config dirs |
| `installer-migration-authoring.cjs` | Installer migration authoring guardrails for record metadata, explicit scopes, ownership evidence, and runtime contract citations |

View File

@@ -90,8 +90,9 @@ Phase types and their agents:
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `discuss`, `completion` | Reserved — no subagent today; accepted by schema for forward compatibility |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `discuss` | `gsd-assumptions-analyzer` |
| `completion` | Reserved — no subagent today; accepted by schema for forward compatibility |
The `models` block accepts tier aliases only (`opus`, `sonnet`, `haiku`, `inherit`). For a fully-qualified model ID, use `model_overrides` per agent instead.

View File

@@ -2676,10 +2676,10 @@ capture_thought({
| スロット | 割り当てられたエージェント |
|---------|----------------------|
| `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` |
| `discuss` | (将来のサブエージェント用に予約) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` |
| `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` |
| `completion` | (将来のサブエージェント用に予約) |
**受け入れられる値:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"`

View File

@@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # または --opencode、--kilo
| `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` |
| `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` |
| `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` |
| `discuss`、`completion` | 予約済み — 現在はサブエージェントなし。スキーマの前方互換性のために受け入れられます |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` |
| `discuss` | `gsd-assumptions-analyzer` |
| `completion` | 予約済み — 現在はサブエージェントなし。スキーマの前方互換性のために受け入れられます |
`models` ブロックはティアエイリアス(`opus`、`sonnet`、`haiku`、`inherit`)のみを受け入れます。特定のエージェントに完全修飾のモデル ID を指定するには `model_overrides` を使用してください。

View File

@@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # 또는 --opencode, --kilo 등
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `discuss`, `completion` | 예약됨 — 현재 서브에이전트 없음; 향후 호환성을 위해 스키마에서 허용 |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `discuss` | `gsd-assumptions-analyzer` |
| `completion` | 예약됨 — 현재 서브에이전트 없음; 향후 호환성을 위해 스키마에서 허용 |
`models` 블록은 티어 별칭만 허용합니다(`opus`, `sonnet`, `haiku`, `inherit`). 완전히 정규화된 모델 ID는 에이전트별 `model_overrides`를 사용하세요.

View File

@@ -836,10 +836,10 @@ para que a alteração entre em vigor. Consulte a issue #2256.
| Tipo de fase | Agentes |
|---|---|
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `discuss` | (reservado — sem subagente atualmente) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `completion` | (reservado — sem subagente atualmente) |
`discuss` e `completion` são aceitos pelo esquema para compatibilidade futura; defini-los hoje é um no-op até que um subagente seja mapeado para eles.

View File

@@ -88,8 +88,9 @@ Tipos de fase e seus agentes:
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `discuss`, `completion` | Reservado — nenhum subagente hoje; aceito pelo esquema para compatibilidade futura |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `discuss` | `gsd-assumptions-analyzer` |
| `completion` | Reservado — nenhum subagente hoje; aceito pelo esquema para compatibilidade futura |
O bloco `models` aceita apenas aliases de nível (`opus`, `sonnet`, `haiku`, `inherit`). Para um ID de modelo totalmente qualificado, use `model_overrides` por agente.

View File

@@ -52,7 +52,7 @@ points.
| id | role | tier | engines.gsd | extension points | hook kinds | source |
|---|---|---|---|---|---|---|
| `ai-integration` | feature | full | `>=1.6.0` | `plan:pre` | step | first-party |
| `ai-integration` | feature | full | `>=1.6.0` | `plan:pre`, `verify:pre` | step, contribution, gate | first-party |
| `assumption-delta` | feature | full | `>=1.6.0` | `plan:pre` | contribution | first-party |
| `audit` | feature | full | `>=1.6.0` | — | — | first-party |
| `claude-orchestration` | feature | full | `>=1.7.0` | `plan:post`, `execute:wave:post` | contribution | first-party |

View File

@@ -805,10 +805,10 @@ gsd-tools query config-set features.thinking_partner false
| 阶段类型 | Agents |
|---|---|
| `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` |
| `discuss` | (保留——当前无 subagent) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` |
| `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` |
| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` |
| `completion` | (保留——当前无 subagent) |
`discuss` 和 `completion` 被 schema 接受以保持前向兼容性;今天设置它们是无操作,直到某个 subagent 映射到它们为止。

View File

@@ -2692,10 +2692,10 @@ capture_thought({
| 槽位 | 分配的智能体 |
|------|-----------------|
| `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` |
| `discuss` | (为未来子智能体保留) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` |
| `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` |
| `completion` | (为未来子智能体保留) |
**接受的值:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"`

View File

@@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # or --opencode, --kilo, etc.
| `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` |
| `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` |
| `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` |
| `discuss`、`completion` | 保留——目前无子代理;已被模式接受以备向后兼容 |
| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` |
| `discuss` | `gsd-assumptions-analyzer` |
| `completion` | 保留——目前无子代理;已被模式接受以备向后兼容 |
`models` 块仅接受层级别名(`opus`、`sonnet`、`haiku`、`inherit`)。如需使用完全限定的模型 ID,请改用按代理设置的 `model_overrides`。

View File

@@ -62,6 +62,7 @@ export default tseslint.config(
'gsd-core/bin/lib/host-integration.cjs',
'gsd-core/bin/lib/handshake-serialized.cjs',
'gsd-core/bin/lib/host-integration-sdk.cjs',
'gsd-core/bin/lib/install-effort-resolver.cjs',
'gsd-core/bin/lib/install-engine.cjs',
'gsd-core/bin/lib/capability-loader.cjs',
'gsd-core/bin/lib/capability-source.cjs',
@@ -80,6 +81,7 @@ export default tseslint.config(
'gsd-core/bin/lib/prohibition-enforcement.cjs',
'gsd-core/bin/lib/code-review-flags.cjs',
'gsd-core/bin/lib/context-utilization.cjs',
'gsd-core/bin/lib/api-coverage.cjs',
'gsd-core/bin/lib/artifacts.cjs',
'gsd-core/bin/lib/assumption-delta.cjs',
'gsd-core/bin/lib/state-transition.cjs',

View File

@@ -0,0 +1,466 @@
"use strict";
/**
* API-Coverage detector + matrix validator (#1562).
*
* The enforcement half of "Full API Coverage by Default — Opt Out, Never Opt In."
* When a phase integrates an external API/service/SDK, the planner must produce a
* coverage matrix (COVERAGE.md) enumerating the API's capability surface; every
* non-integrated capability is an explicit, reasoned opt-out. The seal-time gate
* (capabilities/ai-integration, verify:pre) consumes this module to (a) detect
* whether a phase integrates an external API and (b) validate the produced matrix.
*
* Design notes (rubber-duck'd):
* - DETERMINISTIC + TYPED IR. Both the "does this phase integrate an external
* API?" decision and the "is this matrix complete?" decision are pure
* functions returning typed IR, not LLM judgments — so the low-false-positive
* guarantee (acceptance criterion #4) and the completeness guarantee
* (acceptance #2) are testable. Mirrors assumption-delta.cts (#1561).
* - COMPOUND SIGNAL for low false positives. A bare word like "api" appears in
* countless non-integration phases ("the public API of UserController"). The
* detector requires an INTEGRATION VERB co-occurring with an EXTERNAL-API
* NOUN (or an explicit "<Service> API/SDK" phrase). Single weak tokens do not
* fire. This is the issue's "low false-positive trigger" made mechanical.
* - FENCED CODE BLOCKS ARE STRIPPED first (markdown-sectionizer seam) so a
* trigger term inside a code snippet does not fire.
* - THE DETECTOR IS A FALLBACK. The primary path is the plan:pre contribution
* prompting COVERAGE.md creation. The detector runs only when COVERAGE.md is
* ABSENT, to catch the "nobody decided" case (acceptance #1). Its precision
* therefore matters but is not the only line of defense.
* - MATRIX FORMAT. The matrix is a markdown table (human-editable, diff-friendly)
* with a header row `| capability | decision | reason |` and one row per
* capability. decision ∈ {INTEGRATE, OPT-OUT}. An OPT-OUT row MUST carry a
* non-empty reason. A fenced ```coverage JSON block is also accepted for
* machine-generated matrices. This dual shape is bijective (parse/render
* round-trip) and covered by a fast-check property test.
* - ADDITIVE-ONLY VOCABULARY (Hyrum's Law). Once shipped, the verb/noun sets
* are depended-upon interfaces; they only grow. Tunable via the `terms`
* parameter so teams can widen them without forking.
*
* Public API:
* detectApiIntegration(text, terms?) -> { detected, signals, terms }
* parseCoverageMatrix(text) -> { rows, errors, format }
* validateCoverageMatrix(text) -> { valid, errors, counts }
* renderCoverageMatrix(rows) -> string
* DEFAULT_API_COVERAGE_TERMS
*
* CLI:
* echo "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs [--json]
* exit 0 = integration detected, 1 = none, 2 = startup error
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_API_COVERAGE_TERMS = void 0;
exports.detectApiIntegration = detectApiIntegration;
exports.parseCoverageMatrix = parseCoverageMatrix;
exports.validateCoverageMatrix = validateCoverageMatrix;
exports.renderCoverageMatrix = renderCoverageMatrix;
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
/**
* Curated default trigger vocabulary. ADDITIVE-ONLY (Hyrum's Law). Tunable via
* the `terms` parameter.
*
* VERBS are deliberately conservative: common verbs like "add", "use", "call",
* "implement" are EXCLUDED because they appear in nearly every phase and would
* make the gate fire on prose that has nothing to do with an external API. The
* verbs kept all connote BRINGING IN an external surface.
*
* NOUNS name an external-API surface. Bare "client" is excluded — too ambiguous
* (client-side UI vs API client). "service" alone is excluded (internal
* services); a phase integrating an external service virtually always pairs it
* with "API"/"SDK"/"REST"/etc., which the compound verb+noun rule captures.
*/
exports.DEFAULT_API_COVERAGE_TERMS = {
verbs: [
'integrate',
'integrates',
'integrating',
'integration',
'wrap',
'wraps',
'wrapping',
'connect',
'connects',
'connecting',
'consume',
'consumes',
'consuming',
'wire',
'wires',
'wiring',
'onboard',
'onboarding',
'adopt',
'adopts',
'adopting',
],
nouns: [
'api',
'apis',
'sdk',
'sdks',
'rest',
'graphql',
'grpc',
'endpoint',
'endpoints',
'oauth',
'oauth2',
'webhook',
'webhooks',
'mcp',
],
};
/** Hardening caps for the tunable vocabulary (hostile `--terms` defense). */
const MAX_TERMS_PER_KIND = 200;
const MAX_TERM_LEN = 32;
/**
* Field-length caps for matrix cell values. Cell content flows from a
* semi-trusted COVERAGE.md into the gate `message` that the orchestrator LLM
* reads, so it is bounded to keep the prompt-injection surface small and to
* document the format contract (short, single-line prose — not paragraphs).
*/
const CAPABILITY_MAX_LEN = 80;
const REASON_MAX_LEN = 200;
function normalizeTerms(list) {
if (!Array.isArray(list))
return [];
const seen = new Set();
const out = [];
for (const raw of list) {
if (typeof raw !== 'string')
continue;
const t = raw.trim().toLowerCase().slice(0, MAX_TERM_LEN);
if (!t || !/[a-z0-9]/.test(t))
continue;
if (seen.has(t))
continue;
seen.add(t);
out.push(t);
if (out.length >= MAX_TERMS_PER_KIND)
break;
}
return out;
}
function resolveTerms(terms) {
const merge = (key) => {
const t = terms && terms[key];
return Array.isArray(t) ? normalizeTerms(t) : [...exports.DEFAULT_API_COVERAGE_TERMS[key]];
};
return { verbs: merge('verbs'), nouns: merge('nouns') };
}
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function makeSnippet(line, anchor) {
const cleaned = line.replace(/\s+/g, ' ').trim();
if (cleaned.length <= 120)
return cleaned;
const idx = cleaned.toLowerCase().indexOf(anchor);
if (idx < 0)
return cleaned.slice(0, 120);
const start = Math.max(0, idx - 50);
const end = Math.min(cleaned.length, idx + anchor.length + 50);
const prefix = start > 0 ? '…' : '';
const suffix = end < cleaned.length ? '…' : '';
return `${prefix}${cleaned.slice(start, end)}${suffix}`;
}
/** `<Service> API` / `<Service> SDK` — a capitalized proper noun immediately
* followed by API/SDK. Strong signal on its own (no verb required).
*
* STOPWORDS guard against the false positive where an ordinary capitalized
* sentence starter ("The API …", "An SDK …", "Our REST …") matches the
* `[A-Z]\w+ API` shape. Those are common English, not a service name, so they
* are rejected before counting as a surface signal (acceptance #4 — low false
* positives). */
const SERVICE_SURFACE_API_RE = /\b([A-Z][A-Za-z0-9_-]{1,})\s+(API|SDK|REST|GraphQL)\b/;
const SERVICE_STOPWORDS = new Set([
'the', 'an', 'a', 'our', 'this', 'these', 'that', 'those', 'new', 'add',
'use', 'your', 'my', 'no', 'some', 'any', 'all', 'each', 'every', 'both',
'if', 'when', 'while', 'with', 'via', 'using', 'into', 'its', 'their',
'we', 'you', 'they', 'it',
]);
/**
* Detect whether phase-scope prose describes integrating an external API/SDK.
*
* Fires when EITHER:
* (a) a compound verb+noun signal co-occurs on the same line, OR
* (b) an explicit `<Service> API|SDK|REST|GraphQL` surface appears.
*
* Non-string inputs degrade to `{ detected: false }` without throwing.
*/
function detectApiIntegration(text, terms) {
const effective = resolveTerms(terms);
if (typeof text !== 'string') {
return { detected: false, signals: [], terms: effective };
}
const stripped = (0, markdown_sectionizer_cjs_1.stripFencedCode)(text.replace(/\r\n/g, '\n')).text;
if (stripped.trim().length === 0) {
return { detected: false, signals: [], terms: effective };
}
const signals = [];
const seen = new Set();
const lines = stripped.split('\n');
// (a) compound verb+noun on the same line.
if (effective.verbs.length > 0 && effective.nouns.length > 0) {
const verbRe = new RegExp('(^|[^a-zA-Z0-9])(' + effective.verbs.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)', 'gi');
const nounRe = new RegExp('(^|[^a-zA-Z0-9])(' + effective.nouns.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)', 'gi');
for (const line of lines) {
verbRe.lastIndex = 0;
nounRe.lastIndex = 0;
const vMatch = verbRe.exec(line);
if (!vMatch)
continue;
const nMatch = nounRe.exec(line);
if (!nMatch)
continue;
const verb = (vMatch[2] || '').toLowerCase();
const noun = (nMatch[2] || '').toLowerCase();
const key = `${verb}+${noun}`;
if (seen.has(key))
continue;
seen.add(key);
signals.push({ verb, noun, snippet: makeSnippet(line, noun) });
}
}
// (b) explicit <Service> API|SDK|REST|GraphQL surface.
for (const line of lines) {
SERVICE_SURFACE_API_RE.lastIndex = 0;
const m = SERVICE_SURFACE_API_RE.exec(line);
if (!m)
continue;
// Reject ordinary capitalized sentence starters ("The API …", "Our REST …").
if (SERVICE_STOPWORDS.has((m[1] || '').toLowerCase()))
continue;
const noun = (m[2] || '').toLowerCase();
const key = `surface+${noun}`;
if (seen.has(key))
continue;
seen.add(key);
signals.push({ verb: '(surface)', noun, snippet: makeSnippet(line, m[1]) });
}
return { detected: signals.length > 0, signals, terms: effective };
}
const VALID_DECISIONS = new Set(['INTEGRATE', 'OPT-OUT']);
/**
* Parse a coverage matrix from COVERAGE.md. Accepts two bijective formats:
*
* 1. Markdown table (canonical, human-editable):
* | capability | decision | reason |
* |---|---|---|
* | search | INTEGRATE | |
* | playlists | OPT-OUT | not needed yet |
*
* 2. Fenced ```coverage JSON block (machine-generated):
* ```coverage
* [ {"capability":"search","decision":"INTEGRATE","reason":""}, ... ]
* ```
*
* Rows are trimmed; decisions upper-cased; missing reason → "". Returns
* `{ rows: [], errors: [], format: 'none' }` for empty/non-matrix input.
*/
function parseCoverageMatrix(text) {
const out = { rows: [], errors: [], format: 'none' };
if (typeof text !== 'string')
return out;
const src = text.replace(/\r\n/g, '\n');
// (1) fenced ```coverage JSON block takes precedence if present.
// Case-insensitive info string (```coverage and ```Coverage are both legal CommonMark).
// allow-adhoc-markdown: extracting a NAMED ```coverage fence (extraction of one tagged block), not stripping all fences — stripFencedCode/extractTaggedBlocks do not cover named-fence extraction.
const fenceMatch = src.match(/```coverage\s*\n([\s\S]*?)\n```/i);
if (fenceMatch && fenceMatch[1]) {
out.format = 'json';
let parsed;
try {
parsed = JSON.parse(fenceMatch[1]);
}
catch {
out.errors.push('fenced ```coverage block is not valid JSON');
return out;
}
if (!Array.isArray(parsed)) {
out.errors.push('fenced ```coverage block must be a JSON array');
return out;
}
for (let i = 0; i < parsed.length; i++) {
const row = rowFromJson(parsed[i]);
if ('error' in row) {
out.errors.push(`row[${i}]: ${row.error}`);
continue;
}
out.rows.push(row);
}
return out;
}
// (2) markdown table — collect table rows whose decision column parses.
const lines = src.split('\n');
let sawHeader = false;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('|'))
continue;
const cells = trimmed.slice(1, trimmed.endsWith('|') ? -1 : trimmed.length).split('|');
if (cells.length < 2)
continue;
const cleaned = cells.map((c) => c.trim());
// skip separator rows (|---|---|); require ≥3 dashes so a literal "-" cell
// is not mistaken for a separator.
if (cleaned.every((c) => /^:?-{3,}:?$/.test(c)))
continue;
const decisionCell = (cleaned[1] || '').toUpperCase();
// header detection
if (!sawHeader && cleaned[0].toLowerCase() === 'capability') {
sawHeader = true;
out.format = 'table';
continue;
}
if (!VALID_DECISIONS.has(decisionCell)) {
// A row that otherwise looks like data (≥3 cells, non-empty capability)
// but carries a malformed decision is a real error, not a row to skip
// silently — otherwise a single typo'd row collapses the matrix to
// "empty" and the user sees a confusing message.
if (cleaned.length >= 3 && cleaned[0]) {
out.errors.push(`row: decision "${decisionCell}" not in {INTEGRATE, OPT-OUT}`);
}
continue;
}
if (out.format === 'none')
out.format = 'table';
// A coverage row has exactly 3 cells. Extra cells mean an unescaped pipe in
// a value silently corrupted the row — surface it rather than parse garbage.
if (cleaned.length > 3) {
out.errors.push(`row: ${cleaned.length} columns (expected 3 — unescaped pipe in a cell?)`);
}
out.rows.push({
capability: cleaned[0] || '',
decision: decisionCell,
reason: (cleaned[2] ?? '').trim(),
});
}
return out;
}
function rowFromJson(v) {
if (!v || typeof v !== 'object' || Array.isArray(v))
return { error: 'not an object' };
const o = v;
const capability = typeof o['capability'] === 'string' ? o['capability'].trim() : '';
if (!capability)
return { error: 'missing/empty "capability"' };
const dRaw = typeof o['decision'] === 'string' ? o['decision'].trim().toUpperCase() : '';
if (!VALID_DECISIONS.has(dRaw)) {
return { error: `decision "${dRaw}" not in {INTEGRATE, OPT-OUT}` };
}
const reason = typeof o['reason'] === 'string' ? o['reason'].trim() : '';
return { capability, decision: dRaw, reason };
}
/**
* Validate a parsed matrix. A matrix is valid when:
* - it is non-empty (acceptance #1: "enumerating the API surface"),
* - every capability name is non-empty,
* - every decision is INTEGRATE or OPT-OUT (enforced by parser, re-checked
* here for defense-in-depth),
* - every OPT-OUT row carries a non-empty reason (acceptance #2).
*
* Un-enumerated remainder is not representable in the format — the gate blocks
* when an integration is detected and NO matrix exists. This validator catches
* a malformed/partial matrix that does exist.
*/
function validateCoverageMatrix(text) {
const parsed = parseCoverageMatrix(text);
const errors = [...parsed.errors];
const rows = parsed.rows;
if (rows.length === 0) {
if (errors.length === 0)
errors.push('matrix is empty — no capabilities enumerated');
return { valid: false, errors, counts: { surface: 0, integrate: 0, optout: 0 } };
}
const seen = new Set();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (!row.capability) {
errors.push(`row[${i}]: empty capability name`);
}
else {
// Format contract + prompt-injection bound: cell values must be short,
// single-line, pipe-free prose (the matrix is a markdown table whose
// content flows into the gate message). Pipes/newlines would corrupt the
// table and let a COVERAGE.md inject unbounded text into the seal message.
if (/[|\n\r]/.test(row.capability)) {
errors.push(`row[${i}]: capability contains a pipe or newline (unsupported in a table cell)`);
}
if (row.capability.length > CAPABILITY_MAX_LEN) {
errors.push(`row[${i}]: capability exceeds ${CAPABILITY_MAX_LEN} chars`);
}
}
if (row.reason && /[|\n\r]/.test(row.reason)) {
errors.push(`row[${i}]: reason contains a pipe or newline (unsupported in a table cell)`);
}
if (row.reason.length > REASON_MAX_LEN) {
errors.push(`row[${i}]: reason exceeds ${REASON_MAX_LEN} chars`);
}
const key = row.capability.toLowerCase();
if (key && seen.has(key))
errors.push(`row[${i}]: duplicate capability`);
if (key)
seen.add(key);
if (!VALID_DECISIONS.has(row.decision)) {
errors.push(`row[${i}]: decision not in {INTEGRATE, OPT-OUT}`);
}
if (row.decision === 'OPT-OUT' && !row.reason) {
errors.push(`row[${i}]: OPT-OUT missing reason`);
}
}
const counts = {
surface: rows.length,
integrate: rows.filter((r) => r.decision === 'INTEGRATE').length,
optout: rows.filter((r) => r.decision === 'OPT-OUT').length,
};
return { valid: errors.length === 0, errors, counts };
}
/** Render rows back to the canonical markdown-table format (bijective with parse). */
function renderCoverageMatrix(rows) {
const body = rows
.map((r) => `| ${r.capability} | ${r.decision} | ${r.reason} |`)
.join('\n');
return `| capability | decision | reason |\n|---|---|---|\n${body}`;
}
// ── CLI entry point ──────────────────────────────────────────────────────────
// Reads phase-scope text from STDIN (not argv) to avoid OS ARG_MAX limits.
// Invoked by workflow bash as: echo "$SCOPE" | node .../api-coverage.cjs [--json]
// Exit 0 = integration detected, 1 = none, 2 = startup error. Mirrors
// assumption-delta.cjs / ui-safety-gate.cjs.
if (require.main === module) {
const argv = process.argv.slice(2);
const wantJson = argv.includes('--json');
let termsOverride;
const verbsIdx = argv.indexOf('--verbs');
const verbsVal = verbsIdx !== -1 ? argv[verbsIdx + 1] : undefined;
const nounsIdx = argv.indexOf('--nouns');
const nounsVal = nounsIdx !== -1 ? argv[nounsIdx + 1] : undefined;
// A non-empty, non-flag value is an override. An EMPTY value ("") restores
// the curated defaults (does NOT silently zero the vocabulary).
const verbsOverride = typeof verbsVal === 'string' && verbsVal.length > 0 && !verbsVal.startsWith('-');
const nounsOverride = typeof nounsVal === 'string' && nounsVal.length > 0 && !nounsVal.startsWith('-');
if (verbsOverride || nounsOverride) {
termsOverride = {};
if (verbsOverride) {
termsOverride.verbs = verbsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
}
if (nounsOverride) {
termsOverride.nouns = nounsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
}
}
const chunks = [];
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => chunks.push(chunk));
process.stdin.on('end', () => {
const input = chunks.join('');
const result = detectApiIntegration(input, termsOverride);
if (wantJson) {
process.stdout.write(JSON.stringify(result) + '\n');
}
process.exit(result.detected ? 0 : 1);
});
process.stdin.on('error', (err) => {
process.stderr.write(`ERROR: api-coverage.cjs stdin read failed: ${err.message}\n`);
process.exit(2);
});
}

View File

@@ -39,6 +39,11 @@ const capabilities = {
"type": "boolean",
"default": true,
"description": "Prompt for an AI-SPEC design contract before planning phases that involve AI systems."
},
"workflow.api_coverage_gate": {
"type": "boolean",
"default": true,
"description": "Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre the planner is prompted to enumerate the API surface into COVERAGE.md; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out. Independent of ai_integration_phase (applies to any external-API integration, not only AI)."
}
},
"steps": [
@@ -57,8 +62,35 @@ const capabilities = {
"onError": "skip"
}
],
"contributions": [],
"gates": []
"contributions": [
{
"point": "plan:pre",
"into": "planner",
"fragment": {
"path": "fragments/api-coverage-plan-pre.md",
"inline": "# API Coverage Decision Checkpoint\n\n> Full API Coverage by Default — Opt Out, Never Opt In. Fires when a phase\n> integrates an external API / SDK / service. Most non-API phases will not fire\n> it — that is the point.\n\n## Why this exists\n\n\"We integrated the API\" too often silently means \"we integrated whatever the\nfirst use case exercised.\" Every un-built capability is then an invisible hole,\ndiscovered later by a user who reasonably expected it to work. The phase sealed\ngreen because its tasks completed; nobody decided the gaps were acceptable,\nbecause nobody enumerated them. This checkpoint makes the surface **visible and\ndecided** before the phase can seal.\n\n## Detect whether this phase integrates an external API\n\nThe detector is a deterministic scan over the phase scope. It strips fenced\ncode blocks first, so a trigger term inside a code snippet does not fire. It\nreturns a typed result: `{ detected, signals[], terms }`. Run it on the phase\nscope (the concatenation of this phase's ROADMAP section + the PLAN body):\n\n```bash\nSCOPE=\"$(cat \"${PHASE_DIR}\"/*-PLAN.md 2>/dev/null) $(gsd_run query roadmap.get-phase \"${PHASE}\" 2>/dev/null || true)\"\nAPI_COVERAGE_JSON=$(printf '%s' \"$SCOPE\" | node gsd-core/bin/lib/api-coverage.cjs --json 2>/dev/null || echo '{\"detected\":false,\"signals\":[]}')\n```\n\nRead `API_COVERAGE_JSON.detected`. Act on it only — do **not** pattern-match the\nprose yourself.\n\n**If `detected` is `false`:** this phase does not integrate an external API. Skip\nthe checkpoint entirely and continue planning. Do not raise it with the user.\n\n**If `detected` is `true`:** an external-API integration is in scope. You MUST\nproduce a **coverage matrix** before the plan is finalized.\n\n## Produce the coverage matrix\n\nEnumerate the external API's full **capability surface** — the verb/endpoint/method\nlist (e.g. for a music service: `search`, `play`, `pause`, `skip`, `set_volume`,\n`get_playlist`, `create_playlist`, `add_to_playlist`, …). For each capability\nrecord a decision, starting from **full coverage** as the default:\n\n| capability | decision | reason |\n|---|---|---|\n| `<capability-id>` | `INTEGRATE` \\| `OPT-OUT` | `<one-line reason if OPT-OUT>` |\n\nRules:\n\n- **`INTEGRATE` is the default.** Every capability starts as INTEGRATE; the\n matrix is the *subtraction record*.\n- **Every `OPT-OUT` MUST carry a one-line reason** (`not needed`, `not needed\n yet`, `explicitly out of scope`, …). An opt-out without a reason is an\n un-decided hole — the exact failure mode this gate exists to close.\n- **A second integration against the same need** (e.g. a second platform for the\n same capability) starts from the **same full-coverage baseline** as the first.\n Do not carry over the first integration's opt-outs silently — re-decide each\n capability for the new surface, so a first-class/fallback asymmetry cannot\n accumulate.\n\nWrite the matrix to `${PHASE_DIR}/COVERAGE.md` (canonical markdown-table form):\n\n```markdown\n# API Coverage — <service>\n\n> Full coverage by default. Opt-outs are explicit, reasoned decisions.\n\n| capability | decision | reason |\n|---|---|---|\n| search | INTEGRATE | |\n| playlists | INTEGRATE | |\n| skip | OPT-OUT | not needed yet — tracked for follow-up phase |\n```\n\nA fenced ` ```coverage ` JSON block is also accepted for machine-generated\nmatrices; the markdown table is preferred (human-editable, diff-friendly).\n\n## The seal-time gate\n\nThis checkpoint is enforced. At `verify:pre` the `api-coverage.verify-pre` gate\nruns `check api-coverage.verify-pre <phase-dir>`:\n\n- If `COVERAGE.md` exists, it is validated — every row needs a valid decision and\n every `OPT-OUT` a reason. A malformed/partial matrix **blocks the seal**.\n- If `COVERAGE.md` is absent, the detector runs again over the phase scope. If a\n strong external-API-integration signal is found, the seal is **blocked** until a\n matrix is produced. If no signal is found, the phase is treated as a non-API\n phase and the seal proceeds.\n\nSo: an API-integrating phase cannot seal without a decided matrix. Produce it at\nplan time; do not leave it for seal time.\n\n## Tuning the vocabulary (optional)\n\nThe trigger vocabulary is a curated, additive-only set in\n`gsd-core/bin/lib/api-coverage.cjs` (`DEFAULT_API_COVERAGE_TERMS`). To widen it\nfor a project, override at the call site:\n\n```bash\nprintf '%s' \"$SCOPE\" | node gsd-core/bin/lib/api-coverage.cjs --json \\\n --verbs integrate,wrap,connect,embed --nouns api,sdk,rest,grpc,webhook,plugin\n```\n\nThe whole checkpoint is toggleable via `workflow.api_coverage_gate` in\n`.planning/config.json`.\n"
},
"produces": [
"COVERAGE.md"
],
"consumes": [
"CONTEXT.md"
],
"when": "workflow.api_coverage_gate",
"onError": "skip"
}
],
"gates": [
{
"point": "verify:pre",
"check": {
"query": "api-coverage.verify-pre"
},
"when": "workflow.api_coverage_gate",
"blocking": true,
"onError": "halt"
}
]
},
"antigravity": {
"id": "antigravity",
@@ -2832,6 +2864,23 @@ const byLoopPoint = {
}
],
"contributions": [
{
"capId": "ai-integration",
"point": "plan:pre",
"into": "planner",
"fragment": {
"path": "fragments/api-coverage-plan-pre.md",
"inline": "# API Coverage Decision Checkpoint\n\n> Full API Coverage by Default — Opt Out, Never Opt In. Fires when a phase\n> integrates an external API / SDK / service. Most non-API phases will not fire\n> it — that is the point.\n\n## Why this exists\n\n\"We integrated the API\" too often silently means \"we integrated whatever the\nfirst use case exercised.\" Every un-built capability is then an invisible hole,\ndiscovered later by a user who reasonably expected it to work. The phase sealed\ngreen because its tasks completed; nobody decided the gaps were acceptable,\nbecause nobody enumerated them. This checkpoint makes the surface **visible and\ndecided** before the phase can seal.\n\n## Detect whether this phase integrates an external API\n\nThe detector is a deterministic scan over the phase scope. It strips fenced\ncode blocks first, so a trigger term inside a code snippet does not fire. It\nreturns a typed result: `{ detected, signals[], terms }`. Run it on the phase\nscope (the concatenation of this phase's ROADMAP section + the PLAN body):\n\n```bash\nSCOPE=\"$(cat \"${PHASE_DIR}\"/*-PLAN.md 2>/dev/null) $(gsd_run query roadmap.get-phase \"${PHASE}\" 2>/dev/null || true)\"\nAPI_COVERAGE_JSON=$(printf '%s' \"$SCOPE\" | node gsd-core/bin/lib/api-coverage.cjs --json 2>/dev/null || echo '{\"detected\":false,\"signals\":[]}')\n```\n\nRead `API_COVERAGE_JSON.detected`. Act on it only — do **not** pattern-match the\nprose yourself.\n\n**If `detected` is `false`:** this phase does not integrate an external API. Skip\nthe checkpoint entirely and continue planning. Do not raise it with the user.\n\n**If `detected` is `true`:** an external-API integration is in scope. You MUST\nproduce a **coverage matrix** before the plan is finalized.\n\n## Produce the coverage matrix\n\nEnumerate the external API's full **capability surface** — the verb/endpoint/method\nlist (e.g. for a music service: `search`, `play`, `pause`, `skip`, `set_volume`,\n`get_playlist`, `create_playlist`, `add_to_playlist`, …). For each capability\nrecord a decision, starting from **full coverage** as the default:\n\n| capability | decision | reason |\n|---|---|---|\n| `<capability-id>` | `INTEGRATE` \\| `OPT-OUT` | `<one-line reason if OPT-OUT>` |\n\nRules:\n\n- **`INTEGRATE` is the default.** Every capability starts as INTEGRATE; the\n matrix is the *subtraction record*.\n- **Every `OPT-OUT` MUST carry a one-line reason** (`not needed`, `not needed\n yet`, `explicitly out of scope`, …). An opt-out without a reason is an\n un-decided hole — the exact failure mode this gate exists to close.\n- **A second integration against the same need** (e.g. a second platform for the\n same capability) starts from the **same full-coverage baseline** as the first.\n Do not carry over the first integration's opt-outs silently — re-decide each\n capability for the new surface, so a first-class/fallback asymmetry cannot\n accumulate.\n\nWrite the matrix to `${PHASE_DIR}/COVERAGE.md` (canonical markdown-table form):\n\n```markdown\n# API Coverage — <service>\n\n> Full coverage by default. Opt-outs are explicit, reasoned decisions.\n\n| capability | decision | reason |\n|---|---|---|\n| search | INTEGRATE | |\n| playlists | INTEGRATE | |\n| skip | OPT-OUT | not needed yet — tracked for follow-up phase |\n```\n\nA fenced ` ```coverage ` JSON block is also accepted for machine-generated\nmatrices; the markdown table is preferred (human-editable, diff-friendly).\n\n## The seal-time gate\n\nThis checkpoint is enforced. At `verify:pre` the `api-coverage.verify-pre` gate\nruns `check api-coverage.verify-pre <phase-dir>`:\n\n- If `COVERAGE.md` exists, it is validated — every row needs a valid decision and\n every `OPT-OUT` a reason. A malformed/partial matrix **blocks the seal**.\n- If `COVERAGE.md` is absent, the detector runs again over the phase scope. If a\n strong external-API-integration signal is found, the seal is **blocked** until a\n matrix is produced. If no signal is found, the phase is treated as a non-API\n phase and the seal proceeds.\n\nSo: an API-integrating phase cannot seal without a decided matrix. Produce it at\nplan time; do not leave it for seal time.\n\n## Tuning the vocabulary (optional)\n\nThe trigger vocabulary is a curated, additive-only set in\n`gsd-core/bin/lib/api-coverage.cjs` (`DEFAULT_API_COVERAGE_TERMS`). To widen it\nfor a project, override at the call site:\n\n```bash\nprintf '%s' \"$SCOPE\" | node gsd-core/bin/lib/api-coverage.cjs --json \\\n --verbs integrate,wrap,connect,embed --nouns api,sdk,rest,grpc,webhook,plugin\n```\n\nThe whole checkpoint is toggleable via `workflow.api_coverage_gate` in\n`.planning/config.json`.\n"
},
"produces": [
"COVERAGE.md"
],
"consumes": [
"CONTEXT.md"
],
"when": "workflow.api_coverage_gate",
"onError": "skip"
},
{
"capId": "assumption-delta",
"point": "plan:pre",
@@ -3101,7 +3150,18 @@ const byLoopPoint = {
"verify:pre": {
"steps": [],
"contributions": [],
"gates": []
"gates": [
{
"capId": "ai-integration",
"point": "verify:pre",
"check": {
"query": "api-coverage.verify-pre"
},
"when": "workflow.api_coverage_gate",
"blocking": true,
"onError": "halt"
}
]
},
"verify:post": {
"steps": [
@@ -3211,6 +3271,7 @@ const byLoopPoint = {
const configKeys = {
"workflow.ai_integration_phase": "ai-integration",
"workflow.api_coverage_gate": "ai-integration",
"workflow.assumption_delta": "assumption-delta",
"claude_orchestration.enabled": "claude-orchestration",
"claude_orchestration.execution_backend": "claude-orchestration",
@@ -3260,6 +3321,12 @@ const configSchema = {
"default": true,
"description": "Prompt for an AI-SPEC design contract before planning phases that involve AI systems."
},
"workflow.api_coverage_gate": {
"owner": "ai-integration",
"type": "boolean",
"default": true,
"description": "Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre the planner is prompted to enumerate the API surface into COVERAGE.md; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out. Independent of ai_integration_phase (applies to any external-API integration, not only AI)."
},
"workflow.assumption_delta": {
"owner": "assumption-delta",
"type": "boolean",

View File

@@ -0,0 +1,104 @@
# API Coverage Gate (Full Coverage by Default — Opt Out, Never Opt In)
> Reference for the `api-coverage` gate on the `ai-integration` capability (#1562).
> Config key: `workflow.api_coverage_gate` (default `true`). Gate point: `verify:pre`.
## The problem this closes
"We integrated the API" too often silently means "we integrated whatever the
first use case exercised." Every un-built capability is then an invisible hole,
discovered later by a user who reasonably expected it to work. The phase sealed
green because its tasks completed — nobody *decided* the gaps were acceptable,
because nobody *enumerated* them.
This gate makes the API surface **visible and decided** before the phase can
seal. Full coverage is the default starting position; the coverage matrix is the
*subtraction record*. Every gap is an explicit, reasoned opt-out rather than a
surprise.
## When it fires
The gate runs at `verify:pre` (before `/gsd:verify-work` begins UAT). A phase is
treated as an external-API integration when **either**:
1. a `COVERAGE.md` matrix is present in the phase directory (the planner produced
one at `plan:pre`), **or**
2. the phase scope shows a strong external-API-integration signal (an integration
verb co-occurring with an external-API noun, or an explicit `<Service>
API|SDK|REST|GraphQL` surface) and no matrix yet exists.
Non-API phases (refactors, bug fixes, internal-only work, features that merely
*mention* an existing internal API) do **not** fire the gate — the trigger
requires a compound signal, so a bare word like "api" in "the public API of
UserController" is intentionally ignored.
## The two touch points
1. **Plan time (`plan:pre`).** A contribution to the planner prompts it to run
the deterministic detector over the phase scope and, when an integration is
detected, produce `COVERAGE.md`. See
`capabilities/ai-integration/fragments/api-coverage-plan-pre.md`.
2. **Seal time (`verify:pre`).** The blocking `api-coverage.verify-pre` gate
runs `check api-coverage.verify-pre <phase-dir>` and blocks unless a valid
matrix exists (or no integration is detected).
## The coverage matrix format
Canonical form — a markdown table (human-editable, diff-friendly):
```markdown
# API Coverage — <service>
> Full coverage by default. Opt-outs are explicit, reasoned decisions.
| capability | decision | reason |
|---|---|---|
| search | INTEGRATE | |
| playlists | INTEGRATE | |
| skip | OPT-OUT | not needed yet — tracked for follow-up phase |
```
- **`INTEGRATE` is the default.** Every capability starts as INTEGRATE.
- **Every `OPT-OUT` MUST carry a one-line reason** (`not needed`, `not needed
yet`, `explicitly out of scope`, …). An opt-out without a reason is an
un-decided hole — the exact failure mode this gate exists to close.
- A fenced ` ```coverage ` JSON block (`[{"capability":…,"decision":…,"reason":…}]`)
is also accepted for machine-generated matrices.
Rules enforced at seal time: the matrix must be non-empty; every capability name
must be non-empty and unique; every decision must be `INTEGRATE` or `OPT-OUT`;
every `OPT-OUT` must have a reason. Violations block the seal with a precise
error.
## A second integration against the same need
A second platform for an existing capability (e.g. adding YouTube alongside
Spotify for media playback) starts from the **same full-coverage baseline** as
the first. Do not carry over the first integration's opt-outs silently —
re-decide each capability for the new surface, so a first-class/fallback
asymmetry cannot accumulate into a later user-facing bug.
## The matrix persists
`COVERAGE.md` is a phase artifact. A future phase that extends the same
integration starts from the recorded surface and decisions rather than from
zero — the matrix is the durable subtraction record.
## Tuning
- **Disable entirely:** set `workflow.api_coverage_gate: false` in
`.planning/config.json` (the gate unregisters from `verify:pre`).
- **Widen the trigger vocabulary:** the detector accepts `--verbs` / `--nouns`
overrides (see `capabilities/ai-integration/fragments/api-coverage-plan-pre.md`).
The default vocabulary is additive-only.
## Detector CLI
```bash
echo "$PHASE_SCOPE" | node gsd-core/bin/lib/api-coverage.cjs --json
# exit 0 = integration detected, 1 = none, 2 = startup error
```
The detector is a pure function (`detectApiIntegration` → `{ detected, signals,
terms }`) shared by the plan-time prompt and the seal-time gate, so the
low-false-positive guarantee is testable rather than a judgment call.

View File

@@ -45,10 +45,10 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
| Phase type | Agents |
|---|---|
| `planning` | gsd-planner, gsd-roadmapper, gsd-pattern-mapper |
| `discuss` | (reserved — no subagent today) |
| `discuss` | `gsd-assumptions-analyzer` |
| `research` | gsd-phase-researcher, gsd-project-researcher, gsd-research-synthesizer, gsd-codebase-mapper, gsd-ui-researcher |
| `execution` | gsd-executor, gsd-debugger, gsd-doc-writer |
| `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier |
| `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier, gsd-code-reviewer |
| `completion` | (reserved — no subagent today) |
### Resolution precedence (highest to lowest)

View File

@@ -256,6 +256,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
| `workflow.node_repair` | boolean | `true` | `true`, `false` | Attempt automatic repair of failed plan nodes |
| `workflow.node_repair_budget` | number | `2` | Any positive integer | Max repair retries per failed node |
| `workflow.ai_integration_phase` | boolean | `true` | `true`, `false` | Run /gsd:ai-integration-phase before planning AI system phases |
| `workflow.api_coverage_gate` | boolean | `true` | `true`, `false` | Require an explicit API-coverage decision (full-by-default, opt-out-not-opt-in) before a phase that integrates an external API/SDK/service can seal. At plan:pre prompts a COVERAGE.md matrix; at verify:pre a blocking gate fails the seal unless the matrix exists with every non-integrated capability an explicit, reasoned opt-out (#1562) |
| `workflow.ui_phase` | boolean | `true` | `true`, `false` | Generate UI-SPEC.md for frontend phases |
| `workflow.ui_safety_gate` | boolean | `true` | `true`, `false` | Require safety gate approval for UI changes |
| `workflow.text_mode` | boolean | `false` | `true`, `false` | Use plain-text numbered lists instead of AskUserQuestion menus |

View File

@@ -23,6 +23,10 @@ INIT=$(gsd_run query init.phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_FIXER=$(gsd_run query agent-skills gsd-code-fixer)
AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer)
# #2072: resolve the routed models so model_overrides / models.<phaseType> are honored
# (gsd-code-reviewer → "verification", gsd-code-fixer → "execution"); thread them below.
REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw)
FIXER_MODEL=$(gsd_run query resolve-model gsd-code-fixer --raw)
```
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
@@ -194,7 +198,7 @@ echo "Fix scope: ${FIX_SCOPE}"
Use Agent() to spawn agent:
```text
Agent(subagent_type="gsd-code-fixer", prompt="
Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="
<files_to_read>
${REVIEW_PATH}
</files_to_read>
@@ -277,7 +281,7 @@ if [ "$AUTO_MODE" = "true" ]; then
# Spawn gsd-code-reviewer agent to re-review (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
# (This overwrites REVIEW_PATH with latest review state)
Agent(subagent_type="gsd-code-reviewer", prompt="
Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="
<config>
depth: ${REVIEW_DEPTH}
phase_dir: ${PHASE_DIR}
@@ -311,7 +315,7 @@ ${AGENT_SKILLS_REVIEWER}")
# Still has issues — spawn fixer again (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)
echo "Issues remain. Applying fixes for iteration ${ITERATION}..."
Agent(subagent_type="gsd-code-fixer", prompt="
Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt="
<files_to_read>
${REVIEW_PATH}
</files_to_read>

View File

@@ -22,6 +22,9 @@ PHASE_ARG="${1}"
INIT=$(gsd_run query init.phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer)
# #2072: resolve the routed model so model_overrides / models.verification are honored
# (the resolver maps gsd-code-reviewer → phaseType "verification"); thread it below.
REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw)
```
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
@@ -482,7 +485,7 @@ Spawn the gsd-code-reviewer agent:
Print: `◆ Spawning code reviewer... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
```
Agent(subagent_type="gsd-code-reviewer", prompt="
Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt="
<files_to_read>
${FILES_TO_READ}
</files_to_read>

View File

@@ -68,6 +68,9 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars
INIT=$(gsd_run query init.phase-op "${PHASE}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_ANALYZER=$(gsd_run query agent-skills gsd-assumptions-analyzer)
# #2072: resolve the routed model so model_overrides / models.discuss are honored
# (the resolver maps gsd-assumptions-analyzer → phaseType "discuss"); thread it below.
ANALYZER_MODEL=$(gsd_run query resolve-model gsd-assumptions-analyzer --raw)
```
Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`,
@@ -255,7 +258,7 @@ If no USER-PROFILE.md: calibration_tier = "standard"
**Spawn Explore subagent** (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)**:**
```
Agent(subagent_type="gsd-assumptions-analyzer", prompt="""
Agent(subagent_type="gsd-assumptions-analyzer", model="{ANALYZER_MODEL}", prompt="""
Analyze the codebase for Phase {PHASE}: {phase_name}.
Phase goal: {roadmap_description}

View File

@@ -134,7 +134,7 @@ AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker)
AGENT_SKILLS_VERIFIER=$(gsd_run query agent-skills gsd-verifier)
```
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `reviewer_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
```bash
USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true")
@@ -868,7 +868,7 @@ Agent(
Output: ${QUICK_DIR}/${quick_id}-REVIEW.md
Depth: quick",
subagent_type="gsd-code-reviewer",
model="{executor_model}"
model="{reviewer_model}"
)
```

View File

@@ -58,6 +58,44 @@ MVP_MODE=$(gsd_run query phase.mvp-mode "${phase_number}" ${GSD_WS} --pick activ
```
</step>
<step name="verify_pre_hooks">
**Verify:pre gate dispatch.** Before verification begins, dispatch every active
gate hook registered at the `verify:pre` loop extension point. Each gate is
data-driven — resolved from the capability registry, not hardcoded here.
```bash
VERIFY_PRE_HOOKS_JSON=$(gsd_run loop render-hooks verify:pre --raw)
PHASE_DIR=$(printf '%s' "$INIT" | jq -r '.phase_dir // empty')
```
Resolve active gate hooks from `VERIFY_PRE_HOOKS_JSON` where `kind == "gate"`.
For each active gate hook, run its declared check (a `check.query` gate runs
`gsd_run check ${hook.check.query} "${PHASE_DIR}" --raw`; a `predicate` gate
runs `gsd_run check predicate --predicate '<hook.check.predicate as JSON>' --phase-dir "${PHASE_DIR}" --raw`):
```bash
GATE_RESULT=$(gsd_run check "${hook_check_query}" "${PHASE_DIR}" --raw)
GATE_BLOCK=$(printf '%s' "$GATE_RESULT" | jq -r '.block // false' 2>/dev/null || echo "false")
```
**Two-step gate contract (same as execute:wave:post / execute:post):**
- **Step 1 — command failure:** if the `gsd_run check ...` invocation itself
fails (non-zero exit, no JSON), route by the gate's `onError`. An `onError:
halt` gate HALTs; an `onError: skip` gate logs a warning and continues.
- **Step 2 — block evaluation:** parse `GATE_RESULT.block`. For a **blocking
gate** (`hook.blocking == true`) with `block == true`: HALT — do not begin UAT,
present the gate's `message`, and tell the user what artifact resolves it. For
a **non-blocking gate** with a non-empty `message`: print
`⚠ {hook.capId} advisory: {GATE_RESULT.message}` and continue. For any gate
with `block == false`: continue silently.
Example — the `ai-integration` capability's `api-coverage.verify-pre` gate
(when `workflow.api_coverage_gate` is on) blocks here if the phase integrates an
external API without a decided COVERAGE.md matrix. Present its `message` and
point the user at producing COVERAGE.md before re-running verification.
</step>
<step name="check_active_session">
**First: Check for active UAT sessions**

514
src/api-coverage.cts Normal file
View File

@@ -0,0 +1,514 @@
/**
* API-Coverage detector + matrix validator (#1562).
*
* The enforcement half of "Full API Coverage by Default — Opt Out, Never Opt In."
* When a phase integrates an external API/service/SDK, the planner must produce a
* coverage matrix (COVERAGE.md) enumerating the API's capability surface; every
* non-integrated capability is an explicit, reasoned opt-out. The seal-time gate
* (capabilities/ai-integration, verify:pre) consumes this module to (a) detect
* whether a phase integrates an external API and (b) validate the produced matrix.
*
* Design notes (rubber-duck'd):
* - DETERMINISTIC + TYPED IR. Both the "does this phase integrate an external
* API?" decision and the "is this matrix complete?" decision are pure
* functions returning typed IR, not LLM judgments — so the low-false-positive
* guarantee (acceptance criterion #4) and the completeness guarantee
* (acceptance #2) are testable. Mirrors assumption-delta.cts (#1561).
* - COMPOUND SIGNAL for low false positives. A bare word like "api" appears in
* countless non-integration phases ("the public API of UserController"). The
* detector requires an INTEGRATION VERB co-occurring with an EXTERNAL-API
* NOUN (or an explicit "<Service> API/SDK" phrase). Single weak tokens do not
* fire. This is the issue's "low false-positive trigger" made mechanical.
* - FENCED CODE BLOCKS ARE STRIPPED first (markdown-sectionizer seam) so a
* trigger term inside a code snippet does not fire.
* - THE DETECTOR IS A FALLBACK. The primary path is the plan:pre contribution
* prompting COVERAGE.md creation. The detector runs only when COVERAGE.md is
* ABSENT, to catch the "nobody decided" case (acceptance #1). Its precision
* therefore matters but is not the only line of defense.
* - MATRIX FORMAT. The matrix is a markdown table (human-editable, diff-friendly)
* with a header row `| capability | decision | reason |` and one row per
* capability. decision ∈ {INTEGRATE, OPT-OUT}. An OPT-OUT row MUST carry a
* non-empty reason. A fenced ```coverage JSON block is also accepted for
* machine-generated matrices. This dual shape is bijective (parse/render
* round-trip) and covered by a fast-check property test.
* - ADDITIVE-ONLY VOCABULARY (Hyrum's Law). Once shipped, the verb/noun sets
* are depended-upon interfaces; they only grow. Tunable via the `terms`
* parameter so teams can widen them without forking.
*
* Public API:
* detectApiIntegration(text, terms?) -> { detected, signals, terms }
* parseCoverageMatrix(text) -> { rows, errors, format }
* validateCoverageMatrix(text) -> { valid, errors, counts }
* renderCoverageMatrix(rows) -> string
* DEFAULT_API_COVERAGE_TERMS
*
* CLI:
* echo "$SCOPE" | node gsd-core/bin/lib/api-coverage.cjs [--json]
* exit 0 = integration detected, 1 = none, 2 = startup error
*/
import { stripFencedCode } from './markdown-sectionizer.cjs';
// ─── Integration-signal vocabulary ────────────────────────────────────────────
export interface ApiCoverageTermSet {
verbs: string[];
nouns: string[];
}
export interface ApiCoverageSignal {
verb: string;
noun: string;
snippet: string;
}
export interface ApiCoverageDetectionResult {
detected: boolean;
signals: ApiCoverageSignal[];
terms: ApiCoverageTermSet;
}
/**
* Curated default trigger vocabulary. ADDITIVE-ONLY (Hyrum's Law). Tunable via
* the `terms` parameter.
*
* VERBS are deliberately conservative: common verbs like "add", "use", "call",
* "implement" are EXCLUDED because they appear in nearly every phase and would
* make the gate fire on prose that has nothing to do with an external API. The
* verbs kept all connote BRINGING IN an external surface.
*
* NOUNS name an external-API surface. Bare "client" is excluded — too ambiguous
* (client-side UI vs API client). "service" alone is excluded (internal
* services); a phase integrating an external service virtually always pairs it
* with "API"/"SDK"/"REST"/etc., which the compound verb+noun rule captures.
*/
export const DEFAULT_API_COVERAGE_TERMS: Readonly<ApiCoverageTermSet> = {
verbs: [
'integrate',
'integrates',
'integrating',
'integration',
'wrap',
'wraps',
'wrapping',
'connect',
'connects',
'connecting',
'consume',
'consumes',
'consuming',
'wire',
'wires',
'wiring',
'onboard',
'onboarding',
'adopt',
'adopts',
'adopting',
],
nouns: [
'api',
'apis',
'sdk',
'sdks',
'rest',
'graphql',
'grpc',
'endpoint',
'endpoints',
'oauth',
'oauth2',
'webhook',
'webhooks',
'mcp',
],
};
/** Hardening caps for the tunable vocabulary (hostile `--terms` defense). */
const MAX_TERMS_PER_KIND = 200;
const MAX_TERM_LEN = 32;
/**
* Field-length caps for matrix cell values. Cell content flows from a
* semi-trusted COVERAGE.md into the gate `message` that the orchestrator LLM
* reads, so it is bounded to keep the prompt-injection surface small and to
* document the format contract (short, single-line prose — not paragraphs).
*/
const CAPABILITY_MAX_LEN = 80;
const REASON_MAX_LEN = 200;
function normalizeTerms(list: unknown): string[] {
if (!Array.isArray(list)) return [];
const seen = new Set<string>();
const out: string[] = [];
for (const raw of list) {
if (typeof raw !== 'string') continue;
const t = raw.trim().toLowerCase().slice(0, MAX_TERM_LEN);
if (!t || !/[a-z0-9]/.test(t)) continue;
if (seen.has(t)) continue;
seen.add(t);
out.push(t);
if (out.length >= MAX_TERMS_PER_KIND) break;
}
return out;
}
function resolveTerms(terms?: Partial<ApiCoverageTermSet>): ApiCoverageTermSet {
const merge = (key: 'verbs' | 'nouns'): string[] => {
const t = terms && terms[key];
return Array.isArray(t) ? normalizeTerms(t) : [...DEFAULT_API_COVERAGE_TERMS[key]];
};
return { verbs: merge('verbs'), nouns: merge('nouns') };
}
function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function makeSnippet(line: string, anchor: string): string {
const cleaned = line.replace(/\s+/g, ' ').trim();
if (cleaned.length <= 120) return cleaned;
const idx = cleaned.toLowerCase().indexOf(anchor);
if (idx < 0) return cleaned.slice(0, 120);
const start = Math.max(0, idx - 50);
const end = Math.min(cleaned.length, idx + anchor.length + 50);
const prefix = start > 0 ? '…' : '';
const suffix = end < cleaned.length ? '…' : '';
return `${prefix}${cleaned.slice(start, end)}${suffix}`;
}
/** `<Service> API` / `<Service> SDK` — a capitalized proper noun immediately
* followed by API/SDK. Strong signal on its own (no verb required).
*
* STOPWORDS guard against the false positive where an ordinary capitalized
* sentence starter ("The API …", "An SDK …", "Our REST …") matches the
* `[A-Z]\w+ API` shape. Those are common English, not a service name, so they
* are rejected before counting as a surface signal (acceptance #4 — low false
* positives). */
const SERVICE_SURFACE_API_RE = /\b([A-Z][A-Za-z0-9_-]{1,})\s+(API|SDK|REST|GraphQL)\b/;
const SERVICE_STOPWORDS = new Set([
'the', 'an', 'a', 'our', 'this', 'these', 'that', 'those', 'new', 'add',
'use', 'your', 'my', 'no', 'some', 'any', 'all', 'each', 'every', 'both',
'if', 'when', 'while', 'with', 'via', 'using', 'into', 'its', 'their',
'we', 'you', 'they', 'it',
]);
/**
* Detect whether phase-scope prose describes integrating an external API/SDK.
*
* Fires when EITHER:
* (a) a compound verb+noun signal co-occurs on the same line, OR
* (b) an explicit `<Service> API|SDK|REST|GraphQL` surface appears.
*
* Non-string inputs degrade to `{ detected: false }` without throwing.
*/
export function detectApiIntegration(
text: unknown,
terms?: Partial<ApiCoverageTermSet>,
): ApiCoverageDetectionResult {
const effective = resolveTerms(terms);
if (typeof text !== 'string') {
return { detected: false, signals: [], terms: effective };
}
const stripped = stripFencedCode(text.replace(/\r\n/g, '\n')).text;
if (stripped.trim().length === 0) {
return { detected: false, signals: [], terms: effective };
}
const signals: ApiCoverageSignal[] = [];
const seen = new Set<string>();
const lines = stripped.split('\n');
// (a) compound verb+noun on the same line.
if (effective.verbs.length > 0 && effective.nouns.length > 0) {
const verbRe = new RegExp(
'(^|[^a-zA-Z0-9])(' + effective.verbs.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)',
'gi',
);
const nounRe = new RegExp(
'(^|[^a-zA-Z0-9])(' + effective.nouns.map(escapeRegex).join('|') + ')([^a-zA-Z0-9]|$)',
'gi',
);
for (const line of lines) {
verbRe.lastIndex = 0;
nounRe.lastIndex = 0;
const vMatch = verbRe.exec(line);
if (!vMatch) continue;
const nMatch = nounRe.exec(line);
if (!nMatch) continue;
const verb = (vMatch[2] || '').toLowerCase();
const noun = (nMatch[2] || '').toLowerCase();
const key = `${verb}+${noun}`;
if (seen.has(key)) continue;
seen.add(key);
signals.push({ verb, noun, snippet: makeSnippet(line, noun) });
}
}
// (b) explicit <Service> API|SDK|REST|GraphQL surface.
for (const line of lines) {
SERVICE_SURFACE_API_RE.lastIndex = 0;
const m = SERVICE_SURFACE_API_RE.exec(line);
if (!m) continue;
// Reject ordinary capitalized sentence starters ("The API …", "Our REST …").
if (SERVICE_STOPWORDS.has((m[1] || '').toLowerCase())) continue;
const noun = (m[2] || '').toLowerCase();
const key = `surface+${noun}`;
if (seen.has(key)) continue;
seen.add(key);
signals.push({ verb: '(surface)', noun, snippet: makeSnippet(line, m[1]) });
}
return { detected: signals.length > 0, signals, terms: effective };
}
// ─── Coverage matrix parse / validate / render ────────────────────────────────
export type CoverageDecision = 'INTEGRATE' | 'OPT-OUT';
export interface CoverageRow {
capability: string;
decision: CoverageDecision;
reason: string;
}
export interface CoverageParseResult {
rows: CoverageRow[];
errors: string[];
format: 'table' | 'json' | 'none';
}
export interface CoverageValidationResult {
valid: boolean;
errors: string[];
counts: { surface: number; integrate: number; optout: number };
}
const VALID_DECISIONS = new Set<CoverageDecision>(['INTEGRATE', 'OPT-OUT']);
/**
* Parse a coverage matrix from COVERAGE.md. Accepts two bijective formats:
*
* 1. Markdown table (canonical, human-editable):
* | capability | decision | reason |
* |---|---|---|
* | search | INTEGRATE | |
* | playlists | OPT-OUT | not needed yet |
*
* 2. Fenced ```coverage JSON block (machine-generated):
* ```coverage
* [ {"capability":"search","decision":"INTEGRATE","reason":""}, ... ]
* ```
*
* Rows are trimmed; decisions upper-cased; missing reason → "". Returns
* `{ rows: [], errors: [], format: 'none' }` for empty/non-matrix input.
*/
export function parseCoverageMatrix(text: unknown): CoverageParseResult {
const out: CoverageParseResult = { rows: [], errors: [], format: 'none' };
if (typeof text !== 'string') return out;
const src = text.replace(/\r\n/g, '\n');
// (1) fenced ```coverage JSON block takes precedence if present.
// Case-insensitive info string (```coverage and ```Coverage are both legal CommonMark).
// allow-adhoc-markdown: extracting a NAMED ```coverage fence (extraction of one tagged block), not stripping all fences — stripFencedCode/extractTaggedBlocks do not cover named-fence extraction.
const fenceMatch = src.match(/```coverage\s*\n([\s\S]*?)\n```/i);
if (fenceMatch && fenceMatch[1]) {
out.format = 'json';
let parsed: unknown;
try {
parsed = JSON.parse(fenceMatch[1]);
} catch {
out.errors.push('fenced ```coverage block is not valid JSON');
return out;
}
if (!Array.isArray(parsed)) {
out.errors.push('fenced ```coverage block must be a JSON array');
return out;
}
for (let i = 0; i < parsed.length; i++) {
const row = rowFromJson(parsed[i]);
if ('error' in row) {
out.errors.push(`row[${i}]: ${row.error}`);
continue;
}
out.rows.push(row);
}
return out;
}
// (2) markdown table — collect table rows whose decision column parses.
const lines = src.split('\n');
let sawHeader = false;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('|')) continue;
const cells = trimmed.slice(1, trimmed.endsWith('|') ? -1 : trimmed.length).split('|');
if (cells.length < 2) continue;
const cleaned = cells.map((c) => c.trim());
// skip separator rows (|---|---|); require ≥3 dashes so a literal "-" cell
// is not mistaken for a separator.
if (cleaned.every((c) => /^:?-{3,}:?$/.test(c))) continue;
const decisionCell = (cleaned[1] || '').toUpperCase();
// header detection
if (!sawHeader && cleaned[0].toLowerCase() === 'capability') {
sawHeader = true;
out.format = 'table';
continue;
}
if (!VALID_DECISIONS.has(decisionCell as CoverageDecision)) {
// A row that otherwise looks like data (≥3 cells, non-empty capability)
// but carries a malformed decision is a real error, not a row to skip
// silently — otherwise a single typo'd row collapses the matrix to
// "empty" and the user sees a confusing message.
if (cleaned.length >= 3 && cleaned[0]) {
out.errors.push(`row: decision "${decisionCell}" not in {INTEGRATE, OPT-OUT}`);
}
continue;
}
if (out.format === 'none') out.format = 'table';
// A coverage row has exactly 3 cells. Extra cells mean an unescaped pipe in
// a value silently corrupted the row — surface it rather than parse garbage.
if (cleaned.length > 3) {
out.errors.push(`row: ${cleaned.length} columns (expected 3 — unescaped pipe in a cell?)`);
}
out.rows.push({
capability: cleaned[0] || '',
decision: decisionCell as CoverageDecision,
reason: (cleaned[2] ?? '').trim(),
});
}
return out;
}
function rowFromJson(v: unknown): CoverageRow | { error: string } {
if (!v || typeof v !== 'object' || Array.isArray(v)) return { error: 'not an object' };
const o = v as Record<string, unknown>;
const capability = typeof o['capability'] === 'string' ? o['capability'].trim() : '';
if (!capability) return { error: 'missing/empty "capability"' };
const dRaw = typeof o['decision'] === 'string' ? o['decision'].trim().toUpperCase() : '';
if (!VALID_DECISIONS.has(dRaw as CoverageDecision)) {
return { error: `decision "${dRaw}" not in {INTEGRATE, OPT-OUT}` };
}
const reason = typeof o['reason'] === 'string' ? o['reason'].trim() : '';
return { capability, decision: dRaw as CoverageDecision, reason };
}
/**
* Validate a parsed matrix. A matrix is valid when:
* - it is non-empty (acceptance #1: "enumerating the API surface"),
* - every capability name is non-empty,
* - every decision is INTEGRATE or OPT-OUT (enforced by parser, re-checked
* here for defense-in-depth),
* - every OPT-OUT row carries a non-empty reason (acceptance #2).
*
* Un-enumerated remainder is not representable in the format — the gate blocks
* when an integration is detected and NO matrix exists. This validator catches
* a malformed/partial matrix that does exist.
*/
export function validateCoverageMatrix(text: unknown): CoverageValidationResult {
const parsed = parseCoverageMatrix(text);
const errors = [...parsed.errors];
const rows = parsed.rows;
if (rows.length === 0) {
if (errors.length === 0) errors.push('matrix is empty — no capabilities enumerated');
return { valid: false, errors, counts: { surface: 0, integrate: 0, optout: 0 } };
}
const seen = new Set<string>();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (!row.capability) {
errors.push(`row[${i}]: empty capability name`);
} else {
// Format contract + prompt-injection bound: cell values must be short,
// single-line, pipe-free prose (the matrix is a markdown table whose
// content flows into the gate message). Pipes/newlines would corrupt the
// table and let a COVERAGE.md inject unbounded text into the seal message.
if (/[|\n\r]/.test(row.capability)) {
errors.push(`row[${i}]: capability contains a pipe or newline (unsupported in a table cell)`);
}
if (row.capability.length > CAPABILITY_MAX_LEN) {
errors.push(`row[${i}]: capability exceeds ${CAPABILITY_MAX_LEN} chars`);
}
}
if (row.reason && /[|\n\r]/.test(row.reason)) {
errors.push(`row[${i}]: reason contains a pipe or newline (unsupported in a table cell)`);
}
if (row.reason.length > REASON_MAX_LEN) {
errors.push(`row[${i}]: reason exceeds ${REASON_MAX_LEN} chars`);
}
const key = row.capability.toLowerCase();
if (key && seen.has(key)) errors.push(`row[${i}]: duplicate capability`);
if (key) seen.add(key);
if (!VALID_DECISIONS.has(row.decision)) {
errors.push(`row[${i}]: decision not in {INTEGRATE, OPT-OUT}`);
}
if (row.decision === 'OPT-OUT' && !row.reason) {
errors.push(`row[${i}]: OPT-OUT missing reason`);
}
}
const counts = {
surface: rows.length,
integrate: rows.filter((r) => r.decision === 'INTEGRATE').length,
optout: rows.filter((r) => r.decision === 'OPT-OUT').length,
};
return { valid: errors.length === 0, errors, counts };
}
/** Render rows back to the canonical markdown-table format (bijective with parse). */
export function renderCoverageMatrix(rows: readonly CoverageRow[]): string {
const body = rows
.map((r) => `| ${r.capability} | ${r.decision} | ${r.reason} |`)
.join('\n');
return `| capability | decision | reason |\n|---|---|---|\n${body}`;
}
// ── CLI entry point ──────────────────────────────────────────────────────────
// Reads phase-scope text from STDIN (not argv) to avoid OS ARG_MAX limits.
// Invoked by workflow bash as: echo "$SCOPE" | node .../api-coverage.cjs [--json]
// Exit 0 = integration detected, 1 = none, 2 = startup error. Mirrors
// assumption-delta.cjs / ui-safety-gate.cjs.
if (require.main === module) {
const argv = process.argv.slice(2);
const wantJson = argv.includes('--json');
let termsOverride: Partial<ApiCoverageTermSet> | undefined;
const verbsIdx = argv.indexOf('--verbs');
const verbsVal = verbsIdx !== -1 ? argv[verbsIdx + 1] : undefined;
const nounsIdx = argv.indexOf('--nouns');
const nounsVal = nounsIdx !== -1 ? argv[nounsIdx + 1] : undefined;
// A non-empty, non-flag value is an override. An EMPTY value ("") restores
// the curated defaults (does NOT silently zero the vocabulary).
const verbsOverride = typeof verbsVal === 'string' && verbsVal.length > 0 && !verbsVal.startsWith('-');
const nounsOverride = typeof nounsVal === 'string' && nounsVal.length > 0 && !nounsVal.startsWith('-');
if (verbsOverride || nounsOverride) {
termsOverride = {};
if (verbsOverride) {
termsOverride.verbs = verbsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
}
if (nounsOverride) {
termsOverride.nouns = nounsVal.split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
}
}
const chunks: string[] = [];
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk: string) => chunks.push(chunk));
process.stdin.on('end', () => {
const input = chunks.join('');
const result = detectApiIntegration(input, termsOverride);
if (wantJson) {
process.stdout.write(JSON.stringify(result) + '\n');
}
process.exit(result.detected ? 0 : 1);
});
process.stdin.on('error', (err: Error) => {
process.stderr.write(`ERROR: api-coverage.cjs stdin read failed: ${err.message}\n`);
process.exit(2);
});
}

View File

@@ -35,6 +35,9 @@ import { routeProhibitionEnforcement } from './prohibition-enforcement.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import gatePredicateEval = require('./gate-predicate-evaluator.cjs');
const { evaluatePredicate } = gatePredicateEval;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import apiCoverageMod = require('./api-coverage.cjs');
const { detectApiIntegration, validateCoverageMatrix } = apiCoverageMod;
import { execTool } from './shell-command-projection.cjs';
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -986,6 +989,279 @@ function cmdCheckPredicate(projectDir: string, args: string[], raw: boolean): vo
output(result, raw, undefined);
}
// ─── api-coverage-verify-pre ──────────────────────────────────────────────────
/**
* api-coverage.verify-pre: BLOCKING seal-time gate for the ai-integration
* capability (#1562). Enforces "Full API Coverage by Default — Opt Out, Never
* Opt In." A phase that integrates an external API/SDK/service may not seal
* until a COVERAGE.md matrix enumerates the surface and every non-integrated
* capability is an explicit, reasoned opt-out.
*
* Contract (two touch points composed into one check):
* 1. If COVERAGE.md exists in the phase dir → validate it (acceptance #2).
* Block on any validation error (empty matrix, OPT-OUT without reason,
* duplicate/empty capability).
* 2. If COVERAGE.md is absent → run detectApiIntegration over the phase scope
* (PLAN.md body, then ROADMAP phase section as fallback). If a strong
* external-API-integration signal is detected → BLOCK ("integration
* detected without coverage matrix"). If no signal → PASS (treat as a
* non-API phase; acceptance #4 — low false positives).
*
* The detector is the FALLBACK for the "nobody decided / forgot the matrix"
* case; the primary path is the plan:pre contribution prompting COVERAGE.md.
*
* Args: check api-coverage.verify-pre <phase-dir>
* Emits the uniform gate contract: { block, passed, message, ...details }.
*/
function cmdApiCoverageVerifyPre(projectDir: string, args: string[], raw: boolean): void {
const phaseArg = typeof args[2] === 'string' ? args[2] : '';
if (!phaseArg) {
error(
'api-coverage.verify-pre requires a phase argument: check api-coverage.verify-pre <phase-dir-or-token>',
ERROR_REASON.SDK_MISSING_ARG,
);
return;
}
const pDir = planningDir(projectDir);
const phasesRoot = path.join(pDir, 'phases');
// SECURITY (path traversal): the phase argument is taken ONLY as a phase
// token — its basename — and resolved by findPhaseInternal strictly under
// .planning/phases/ (or a milestone archive). The raw arg is never used as a
// path, so `..`, absolute paths, and arbitrary directories cannot reach a
// file read. Mirrors cmdVerifySchemaDrift's token-match approach.
let token = phaseArg.replace(/\\/g, '/').split('/').filter(Boolean).pop() || '';
// A token like ".." or "." carries no phase identity → unresolvable.
if (token === '.' || token === '..') token = '';
// Not a GSD project (no phases tree at all) → fail-open: nothing to gate.
if (!fs.existsSync(phasesRoot)) {
output(
{
block: false,
passed: true,
coverage_present: false,
detected: false,
message: 'api-coverage: no .planning/phases directory; gate skipped (not a GSD project layout)',
},
raw,
undefined,
);
return;
}
// Resolve the phase dir under the contained phases root.
let resolvedDir: string | null = null;
let phaseNumber = '';
if (token) {
const found = findPhaseInternal(projectDir, token);
if (found && found.directory) {
resolvedDir = found.directory;
phaseNumber = found.phase_number || '';
}
}
if (!resolvedDir) {
// The phases tree EXISTS but THIS phase could not be resolved. For a
// BLOCKING gate, fail-closed: a missing phase dir must not silently bypass
// the coverage requirement. (Distinguished from "no .planning at all"
// above, which is a genuine non-GSD-project → pass.)
output(
{
block: true,
passed: false,
coverage_present: false,
detected: false,
phase_lookup_failed: true,
message:
`api-coverage: could not resolve phase "${phaseArg}" under .planning/phases/. ` +
'Resolve the phase directory (or produce COVERAGE.md) before sealing.',
},
raw,
undefined,
);
return;
}
// Defense-in-depth: the resolved dir must be inside the phases root (or a
// milestone archive under .planning/milestones).
const milestonesRoot = path.join(pDir, 'milestones');
if (!isInsideRoot(resolvedDir, phasesRoot) && !isInsideRoot(resolvedDir, milestonesRoot)) {
output(
{
block: true,
passed: false,
coverage_present: false,
detected: false,
message: 'api-coverage: resolved phase dir escapes .planning/ — refusing to evaluate',
},
raw,
undefined,
);
return;
}
// (1) locate COVERAGE.md — prefer the exact name, then a single *-COVERAGE.md.
let coverageFile = '';
let suffixed: string[] = [];
try {
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
const exact = files.find((f) => /^COVERAGE\.md$/i.test(f));
if (exact) {
coverageFile = exact;
} else {
suffixed = files.filter((f) => /-COVERAGE\.md$/i.test(f)).sort();
if (suffixed.length === 1) coverageFile = suffixed[0];
}
} catch {
// readdir failure → treat as no matrix readable; fall through to detection.
}
if (coverageFile) {
let matrixText: string;
try {
matrixText = fs.readFileSync(path.join(resolvedDir, coverageFile), 'utf8');
} catch {
// COVERAGE.md exists but is unreadable (EACCES/EIO/encoding). Fail-closed
// with a useful message rather than a raw throw.
output(
{
block: true,
passed: false,
coverage_present: true,
message: `api-coverage: COVERAGE.md exists but is unreadable — fix file permissions/encoding before sealing`,
},
raw,
undefined,
);
return;
}
const v = validateCoverageMatrix(matrixText);
if (v.valid) {
output(
{
block: false,
passed: true,
coverage_present: true,
matrix: coverageFile,
counts: v.counts,
message: `api-coverage: matrix present (${v.counts.surface} capabilities, ${v.counts.optout} opt-out)`,
},
raw,
undefined,
);
return;
}
// Fixed-template message (no raw cell content echoed into the LLM-facing
// message). The structured `errors` array is safe (row-indexed, no cell
// values) and travels as data for tooling that wants detail.
output(
{
block: true,
passed: false,
coverage_present: true,
matrix: coverageFile,
error_count: v.errors.length,
errors: v.errors,
message: `api-coverage: COVERAGE.md has ${v.errors.length} problem(s) — fix the matrix (every capability INTEGRATE or OPT-OUT with a reason) before sealing`,
},
raw,
undefined,
);
return;
}
if (suffixed.length > 1) {
output(
{
block: true,
passed: false,
coverage_present: false,
message: `api-coverage: multiple *-COVERAGE.md files found (${suffixed.length}) — consolidate into one COVERAGE.md before sealing`,
},
raw,
undefined,
);
return;
}
// (2) no matrix — detect whether this phase integrates an external API.
const scopeText = readPhaseScope(projectDir, resolvedDir, phaseNumber);
const detection = detectApiIntegration(scopeText);
if (detection.detected) {
// Surface only verb/noun (typed, bounded) — NOT raw prose snippets — so the
// gate output cannot relay injected PLAN.md instructions to the orchestrator.
const signals = detection.signals.map((s) => ({ verb: s.verb, noun: s.noun }));
output(
{
block: true,
passed: false,
coverage_present: false,
detected: true,
signals,
message:
'api-coverage: external-API integration detected without a coverage matrix. ' +
'Produce COVERAGE.md enumerating the API surface (every capability INTEGRATE or ' +
'OPT-OUT with a reason) before sealing. Full coverage is the default.',
},
raw,
undefined,
);
return;
}
output(
{
block: false,
passed: true,
coverage_present: false,
detected: false,
message: 'api-coverage: no external-API integration detected; coverage matrix not required',
},
raw,
undefined,
);
}
/**
* Read the phase-scope text used for API-integration detection. Uses the
* resolved plan files (PLAN.md bodies — the planner's own words about what the
* phase does) and, as a fallback, ONLY THIS PHASE'S ROADMAP section (not the
* whole roadmap, which would cross-contaminate sibling phases). Strips nothing
* here — detectApiIntegration strips fenced code itself.
*/
function readPhaseScope(projectDir: string, phaseDir: string, phaseNumber: string): string {
const chunks: string[] = [];
try {
const entries = fs.readdirSync(phaseDir, { withFileTypes: true });
const plans = entries
.filter((e) => e.isFile() && /-PLAN\.md$/i.test(e.name))
.map((e) => e.name)
.sort();
for (const p of plans) {
chunks.push(fs.readFileSync(path.join(phaseDir, p), 'utf8'));
}
} catch {
// ignore — fall through to roadmap
}
if (chunks.join('').trim().length > 0) return chunks.join('\n\n');
// Fallback: ONLY this phase's ROADMAP section (not the whole file, which
// would pollute detection with sibling-phase prose). Best-effort; absence or
// an unresolvable section is non-fatal (detector returns not-detected).
if (phaseNumber) {
try {
const section = getRoadmapPhaseWithFallback(projectDir, phaseNumber);
if (section) return section;
} catch {
// ignore
}
}
return '';
}
function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
// Normalize dots to hyphens in the subcommand so both forms are accepted.
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
@@ -1015,6 +1291,12 @@ function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
cmdGapAnalysisPlanPost(cwd, args, raw);
return;
}
if (subcommand === 'api-coverage-verify-pre') {
// ai-integration capability blocking gate at verify:pre (#1562). Dot-to-
// hyphen normalization means query "api-coverage.verify-pre" routes here.
cmdApiCoverageVerifyPre(cwd, args, raw);
return;
}
if (subcommand === 'tdd-review-checkpoint') {
cmdTddReviewCheckpoint(cwd, args, raw);
return;
@@ -1055,7 +1337,7 @@ function routeCheckCommand({ args, cwd, raw }: RouteCheckCommandOptions): void {
routeProhibitionEnforcement(args, raw);
return;
}
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
error('Unknown check subcommand. Available: api-coverage-verify-pre, auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, predicate, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
export = {

View File

@@ -564,9 +564,11 @@ function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; con
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { getGlobalConfigDir } = require('./runtime-homes.cjs') as { getGlobalConfigDir(runtime: string, explicitDir?: string | null): string };
// Use install-time resolvers: they merge ~/.gsd/defaults.json with project config,
// matching the exact logic used when agents were originally installed.
// matching the exact logic used when agents were originally installed. #2071: these
// live in the shipped sibling install-effort-resolver.cjs (extracted from the
// package-root bin/install.js, which the installer never copies into a runtime home).
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('../../../bin/install.js') as {
const { readGsdEffectiveEffortConfig, resolveInstallTimeEffort } = require('./install-effort-resolver.cjs') as {
readGsdEffectiveEffortConfig(cwd: string): Record<string, unknown>;
resolveInstallTimeEffort(cfg: Record<string, unknown>, agentName: string): string;
};

View File

@@ -108,6 +108,7 @@ const CONFIG_DEFAULTS = {
verifier: _getNestedConfigDefault('workflow', 'verifier'),
nyquist_validation: _getNestedConfigDefault('workflow', 'nyquist_validation'),
ai_integration_phase: _getNestedConfigDefault('workflow', 'ai_integration_phase'),
api_coverage_gate: _getNestedConfigDefault('workflow', 'api_coverage_gate'),
parallelization: _getConfigDefault('parallelization'),
brave_search: _getConfigDefault('brave_search'),
firecrawl: _getConfigDefault('firecrawl'),

View File

@@ -246,6 +246,7 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
ui_phase: true,
ui_safety_gate: true,
ai_integration_phase: true,
api_coverage_gate: true,
human_verify_mode: 'end-of-phase',
context_guard_mode: 'warn',
text_mode: false,

View File

@@ -758,6 +758,9 @@ function cmdInitQuick(cwd: string, description: string | undefined, raw: boolean
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
// #2072: the quick review step spawns gsd-code-reviewer; resolve its own model
// so model_overrides / models.verification apply (was reusing executor_model).
reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'),
commit_docs: config.commit_docs,
branch_name: quickBranchName,

View File

@@ -0,0 +1,242 @@
/**
* install-effort-resolver — install-time effort resolution (#443), extracted from
* the package-root `bin/install.js` (#2071).
*
* `gsd-tools effort sync` (src/commands.cts `cmdEffortSync`) must mirror what the
* installer writes — home `~/.gsd/defaults.json` merged with the project's
* `.planning/config.json` — which the runtime resolver (`resolveEffortInternal`
* via `loadConfig`) does NOT do. It previously reached those two functions via
* `require('../../../bin/install.js')`, but the installer never copies the
* package-root `bin/install.js` into a runtime home, so `effort sync` crashed with
* MODULE_NOT_FOUND in every installed runtime (#2071). Moving the logic here — a
* `src/*.cts` module compiled into the shipped `gsd-core/bin/lib/` tree — lets both
* the installer AND `effort sync` require it from a location that is always present,
* keeping a single source of truth (no duplication / drift).
*
* Pure with respect to config: `readGsdEffectiveEffortConfig` performs the config
* reads; `resolveInstallTimeEffort` is pure given a pre-merged effort object.
*/
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
import modelResolver = require('./model-resolver.cjs');
const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver as { EFFORT_SET: Set<string> };
interface EffortConfig {
agent_overrides?: Record<string, unknown>;
routing_tier_defaults?: Record<string, unknown>;
default?: unknown;
[k: string]: unknown;
}
/**
* #2517 — Read a single GSD config file (defaults.json or per-project
* config.json) into a plain object, returning null on missing/empty files
* and warning to stderr on JSON parse failures so silent corruption can't
* mask broken configs (review finding #5).
*/
function _readGsdConfigFile(absPath: string, label: string): Record<string, unknown> | null {
if (!fs.existsSync(absPath)) return null;
let raw: string;
try {
raw = fs.readFileSync(absPath, 'utf-8');
} catch (err) {
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${(err as Error).message}\n`);
return null;
}
try {
return JSON.parse(raw) as Record<string, unknown>;
} catch (err) {
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${(err as Error).message}\n`);
return null;
}
}
interface EffortCatalog {
AGENT_DEFAULT_TIERS: Record<string, string>;
renderEffortForRuntime: (runtime: string, effort: string) => { value: string };
EFFORT_MANIFEST_TIER_DEFAULTS: Record<string, string>;
EFFORT_MANIFEST_DEFAULT: string;
}
// #443 — model-catalog and config-defaults.manifest.json exports needed only
// by effort-resolution code paths (resolveInstallTimeEffort /
// generateCodexAgentToml / Claude .md effort injection). Loaded lazily the
// first time they are needed so that requiring this module in test contexts that
// never trigger an install does NOT produce module-load-time side effects (the
// manifest read + hard throw) that could alter subprocess exit codes or stderr.
let _gsdEffortCatalogCache: EffortCatalog | null = null;
function _getGsdEffortCatalog(): EffortCatalog {
if (_gsdEffortCatalogCache) return _gsdEffortCatalogCache;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require('./model-catalog.cjs') as {
AGENT_DEFAULT_TIERS: Record<string, string>;
renderEffortForRuntime: (runtime: string, effort: string) => { value: string };
};
// This module lives in gsd-core/bin/lib/, so the shared manifest is one level
// up in gsd-core/bin/shared/ (bin/lib → bin → shared). (In bin/install.js this
// path was `.., gsd-core, bin, shared` relative to the package-root bin/.)
const manifestPath = path.join(__dirname, '..', 'shared', 'config-defaults.manifest.json');
let manifestData: { effort?: { routing_tier_defaults?: Record<string, string>; default?: string } };
try {
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as {
effort?: { routing_tier_defaults?: Record<string, string>; default?: string };
};
} catch (_err) {
// Fail loudly — a missing manifest is a broken install, not a soft degradation.
throw new Error(
`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${(_err as Error).message}`,
);
}
const tierDefaults =
(manifestData.effort &&
manifestData.effort.routing_tier_defaults &&
typeof manifestData.effort.routing_tier_defaults === 'object' &&
!Array.isArray(manifestData.effort.routing_tier_defaults))
? manifestData.effort.routing_tier_defaults
: { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid
const effortDefault =
(manifestData.effort && typeof manifestData.effort.default === 'string')
? manifestData.effort.default
: 'high'; // guard: unreachable if manifest is valid
_gsdEffortCatalogCache = {
AGENT_DEFAULT_TIERS,
renderEffortForRuntime,
EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
EFFORT_MANIFEST_DEFAULT: effortDefault,
};
return _gsdEffortCatalogCache;
}
/**
* #443 — Read the merged `effort` config block for install-time effort resolution.
*
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
* `effort` object instead of the model-profile fields.
*
* Returns the merged `effort` object or null when neither source defines one.
* The caller can pass this to resolveInstallTimeEffort() which is pure and
* requires no filesystem access beyond what this helper already performs.
*
* @param targetDir Runtime install root (walks up to find .planning/).
*/
function readGsdEffectiveEffortConfig(targetDir: string | null = null): EffortConfig | null {
const homeDefaults = _readGsdConfigFile(
path.join(os.homedir(), '.gsd', 'defaults.json'),
'~/.gsd/defaults.json',
);
let projectConfig: Record<string, unknown> | null = null;
if (targetDir) {
let probeDir = path.resolve(targetDir);
for (let depth = 0; depth < 8; depth += 1) {
const candidate = path.join(probeDir, '.planning', 'config.json');
if (fs.existsSync(candidate)) {
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
break;
}
const parent = path.dirname(probeDir);
if (parent === probeDir) break;
probeDir = parent;
}
}
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
? (homeDefaults.effort as EffortConfig)
: null;
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
? (projectConfig.effort as EffortConfig)
: null;
if (!homeEffort && !projectEffort) return null;
// Per-project wins on conflict within each sub-field. Merge field-by-field so
// a project config that only sets agent_overrides still inherits global
// routing_tier_defaults and default.
return {
...(homeEffort || {}),
...(projectEffort || {}),
// Deep-merge agent_overrides (project wins per-key)
agent_overrides: {
...((homeEffort && homeEffort.agent_overrides) || {}),
...((projectEffort && projectEffort.agent_overrides) || {}),
},
};
}
/**
* #443 — Resolve install-time effort for a given agent, using the same
* precedence chain as resolveEffortInternal() in core.cjs, but operating
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
*
* Precedence (mirrors resolveEffortInternal):
* 1. effortCfg.agent_overrides[agentName]
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
* — OR manifest tier defaults when effortCfg is null
* 3. effortCfg.default
* 4. 'high' (hardcoded fallback)
*
* @param effortCfg Result of readGsdEffectiveEffortConfig().
* @param agentName e.g. 'gsd-planner'
* @returns Universal effort string (low/medium/high/xhigh/max/minimal)
*/
function resolveInstallTimeEffort(effortCfg: EffortConfig | null, agentName: string): string {
// Validates each candidate against the canonical EFFORT_SET (sourced once
// from model-resolver.cjs) before accepting it, mirroring resolveEffortInternal
// exactly. Invalid values fall through to the next precedence layer; final
// fallback 'high'.
// Step 1: agent_overrides
if (effortCfg) {
const ao = effortCfg.agent_overrides;
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
const v = ao[agentName];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
}
}
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
const agentTier = AGENT_DEFAULT_TIERS[agentName];
if (agentTier) {
if (effortCfg && effortCfg.routing_tier_defaults &&
typeof effortCfg.routing_tier_defaults === 'object' &&
!Array.isArray(effortCfg.routing_tier_defaults)) {
const v = effortCfg.routing_tier_defaults[agentTier];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
} else if (!effortCfg) {
// No effort config — use manifest tier defaults
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
}
// effortCfg exists but has no routing_tier_defaults — fall through
}
// Step 3: effort.default
if (effortCfg) {
const d = effortCfg.default;
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
}
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
// If even the manifest default is invalid, fall back to 'high'.
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
return EFFORT_MANIFEST_DEFAULT;
}
return 'high';
}
export = {
readGsdEffectiveEffortConfig,
resolveInstallTimeEffort,
_getGsdEffortCatalog,
_readGsdConfigFile,
};

View File

@@ -0,0 +1,273 @@
'use strict';
/**
* E2E capability-wiring tests for the API-coverage gate (#1562).
*
* Drives the real CLI subprocess (`loop render-hooks verify:pre` and
* `check api-coverage.verify-pre`) against temp projects to prove:
* - the gate is data-driven (activates/deactivates by config) — acceptance #5
* - the seal contract (block / pass) — acceptance #1, #2, #4
* - the matrix persists on disk and is read at seal time — acceptance #6
*
* CONTENT/E2E only: every test drives a real CLI subprocess. No readFileSync
* source-grep. Genuine assertions: each case asserts the SPECIFIC differing
* value (block true/false, capId presence), not a count.
*/
const { describe, test, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const TOOLS_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
const TEST_ENV_BASE = {
GSD_SESSION_KEY: '',
CODEX_THREAD_ID: '',
CLAUDE_SESSION_ID: '',
CLAUDE_CODE_SSE_PORT: '',
OPENCODE_SESSION_ID: '',
GEMINI_SESSION_ID: '',
CURSOR_SESSION_ID: '',
WINDSURF_SESSION_ID: '',
TERM_SESSION: '',
WT_SESSION: '',
TMUX_PANE: '',
ZELLIJ_SESSION_NAME: '',
TTY: '',
SSH_TTY: '',
};
function runTools(args, cwd) {
const argv = Array.isArray(args)
? args
: (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [])
.map((t) => t.replace(/"([^"]*)"/g, '$1').replace(/'([^']*)'/g, '$1'));
try {
const stdout = execFileSync(process.execPath, [TOOLS_PATH, ...argv], {
cwd,
encoding: 'utf-8',
env: { ...process.env, ...TEST_ENV_BASE },
timeout: 60000,
});
return { success: true, output: stdout.trim(), exitCode: 0, error: '' };
} catch (err) {
return {
success: false,
output: err.stdout?.toString().trim() || '',
error: err.stderr?.toString().trim() || err.message,
exitCode: err.status ?? 1,
};
}
}
function makeProject(workflow) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-apicov-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ workflow }),
'utf8'
);
return tmpDir;
}
function makePhaseDir(projectDir, phaseSlug) {
const dir = path.join(projectDir, '.planning', 'phases', phaseSlug);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
function writePlan(phaseDir, planFile, body) {
fs.writeFileSync(path.join(phaseDir, planFile), body, 'utf8');
}
function writeCoverage(phaseDir, body) {
fs.writeFileSync(path.join(phaseDir, 'COVERAGE.md'), body, 'utf8');
}
function verifyPreHooks(cwd) {
const result = runTools('loop render-hooks verify:pre --raw', cwd);
assert.ok(result.success, `render-hooks verify:pre should succeed. stderr: ${result.error}`);
const envelope = JSON.parse(result.output);
assert.strictEqual(envelope.point, 'verify:pre', 'point field must be verify:pre');
assert.ok(Array.isArray(envelope.activeHooks), 'activeHooks must be an array');
return envelope;
}
function findCap(envelope, capId) {
return envelope.activeHooks.find((h) => h.capId === capId) || null;
}
function runGate(cwd, phaseDir) {
return runTools(['check', 'api-coverage.verify-pre', phaseDir, '--raw'], cwd);
}
// ─── Capability wiring: data-driven activation (acceptance #5) ───────────────
describe('api-coverage verify:pre gate — capability wiring (#1562 acceptance #5)', () => {
let tmpDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
test('gate is ACTIVE when workflow.api_coverage_gate is true', () => {
tmpDir = makeProject({ api_coverage_gate: true });
const env = verifyPreHooks(tmpDir);
const hook = findCap(env, 'ai-integration');
assert.ok(hook, 'ai-integration gate must register at verify:pre when enabled');
assert.strictEqual(hook.kind, 'gate');
assert.strictEqual(hook.blocking, true);
assert.strictEqual(hook.check.query, 'api-coverage.verify-pre');
});
test('gate is ABSENT when workflow.api_coverage_gate is false', () => {
tmpDir = makeProject({ api_coverage_gate: false });
const env = verifyPreHooks(tmpDir);
assert.strictEqual(findCap(env, 'ai-integration'), null, 'gate must not register when disabled');
});
test('gate is ACTIVE by default when the key is absent (opt-out, not opt-in)', () => {
tmpDir = makeProject({});
const env = verifyPreHooks(tmpDir);
assert.ok(findCap(env, 'ai-integration'), 'gate must default ON (full-coverage-by-default)');
});
});
// ─── Seal contract: block / pass (acceptance #1, #2, #4, #6) ──────────────────
describe('api-coverage.verify-pre — seal contract (#1562 acceptance #1,#2,#4,#6)', () => {
let tmpDir;
let phaseDir;
afterEach(() => { if (tmpDir) { cleanup(tmpDir); tmpDir = null; } });
function fresh() {
tmpDir = makeProject({ api_coverage_gate: true });
phaseDir = makePhaseDir(tmpDir, '01-pay');
return phaseDir;
}
test('#1 API phase without a matrix → BLOCKS the seal', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nIntegrate the Stripe API for payment processing.');
const r = runGate(tmpDir, phaseDir);
assert.ok(r.success, `gate should succeed (JSON). stderr: ${r.error}`);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, true, 'must block when API integration has no matrix');
assert.strictEqual(j.detected, true);
assert.strictEqual(j.coverage_present, false);
});
test('#4 non-API phase without a matrix → does NOT block', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nRefactor the auth helper to use bcrypt.');
const r = runGate(tmpDir, phaseDir);
assert.ok(r.success, `gate should succeed. stderr: ${r.error}`);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, false, 'must not block a non-API phase');
assert.strictEqual(j.detected, false);
});
test('#1/#6 API phase WITH a valid matrix → passes (matrix persists on disk)', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nIntegrate the Stripe API for payment processing.');
writeCoverage(
phaseDir,
'| capability | decision | reason |\n|---|---|---|\n' +
'| charge | INTEGRATE | |\n| refund | OPT-OUT | not needed yet |\n'
);
const r = runGate(tmpDir, phaseDir);
assert.ok(r.success, `gate should succeed. stderr: ${r.error}`);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, false);
assert.strictEqual(j.coverage_present, true);
assert.strictEqual(j.counts.surface, 2);
assert.strictEqual(j.counts.optout, 1);
});
test('#2 OPT-OUT without a reason → BLOCKS (un-decided hole)', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nIntegrate the Stripe API.');
writeCoverage(
phaseDir,
'| capability | decision | reason |\n|---|---|---|\n| refund | OPT-OUT | |\n'
);
const r = runGate(tmpDir, phaseDir);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, true, 'opt-out without reason must block');
assert.ok(j.errors.some((e) => /missing reason/i.test(e)));
});
test('#2 empty matrix → BLOCKS (surface must be enumerated)', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nIntegrate the Stripe API.');
writeCoverage(phaseDir, '| capability | decision | reason |\n|---|---|---|\n');
const r = runGate(tmpDir, phaseDir);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, true);
assert.ok(j.errors.some((e) => /empty/i.test(e)));
});
test('#3 a second platform with full-coverage baseline is accepted (no asymmetry)', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nAdd a YouTube SDK as a second media platform.');
// Full-coverage baseline for the second platform: every capability decided.
writeCoverage(
phaseDir,
'| capability | decision | reason |\n|---|---|---|\n' +
'| search | INTEGRATE | |\n| playlists | INTEGRATE | |\n| skip | INTEGRATE | |\n'
);
const r = runGate(tmpDir, phaseDir);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, false, 'a fully-decided second platform seals clean');
assert.strictEqual(j.counts.surface, 3);
});
test('JSON-fenced matrix is accepted (machine-generated form)', () => {
fresh();
writePlan(phaseDir, '01-PLAN.md', '# Plan\nIntegrate the Stripe API.');
writeCoverage(
phaseDir,
'```coverage\n[{"capability":"charge","decision":"INTEGRATE","reason":""}]\n```\n'
);
const r = runGate(tmpDir, phaseDir);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, false);
assert.strictEqual(j.counts.surface, 1);
});
// ── Security (#1562 security review S1/S2): the phase arg is taken only as a
// token resolved under .planning/phases/. Traversal / unresolvable args must
// NOT read files outside the phase dir, and — since the phases tree exists —
// must fail CLOSED (a blocking gate must not silently bypass on a bad arg).
test('path-traversal arg is contained and fails CLOSED (phases tree exists)', () => {
fresh(); // creates .planning/phases/01-pay
const r = runTools(['check', 'api-coverage.verify-pre', '../../etc', '--raw'], tmpDir);
assert.ok(r.success, `gate should succeed (JSON). stderr: ${r.error}`);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, true, 'unresolvable phase under an existing phases tree must block');
assert.strictEqual(j.phase_lookup_failed, true);
});
test('no .planning/phases at all → fail-open (genuine non-GSD project)', () => {
// A project with .planning/config.json but no phases directory.
const noPhases = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-apicov-nophase-'));
try {
fs.mkdirSync(path.join(noPhases, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(noPhases, '.planning', 'config.json'),
JSON.stringify({ workflow: { api_coverage_gate: true } }),
'utf8'
);
const r = runTools(['check', 'api-coverage.verify-pre', '01-pay', '--raw'], noPhases);
assert.ok(r.success);
const j = JSON.parse(r.output);
assert.strictEqual(j.block, false, 'no phases tree → pass (not a GSD project)');
} finally {
cleanup(noPhases);
}
});
});

410
tests/api-coverage.test.cjs Normal file
View File

@@ -0,0 +1,410 @@
/**
* Tests for the API-coverage detector + matrix validator (#1562).
*
* Two pure functions under test, both returning typed IR (asserted, never
* text-matched on prose):
* - detectApiIntegration(text) -> { detected, signals, terms }
* - validateCoverageMatrix(text) -> { valid, errors, counts }
*
* Acceptance-criterion mapping:
* #2 (opt-out needs reason; un-enumerated blocks) → validateCoverageMatrix suite
* #4 (non-API phases unaffected, low false-positive) → false-positive suite
* Matrix parse/render bijectivity → fast-check property
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const fc = require('fast-check');
const MODULE_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'api-coverage.cjs');
describe('detectApiIntegration — pure detector (#1562)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(
`Could not require ${MODULE_PATH}. Run "npm run build:lib" first. Underlying: ${err.message}`
);
}
const { detectApiIntegration, DEFAULT_API_COVERAGE_TERMS } = mod;
test('result shape — always carries detected, signals[], terms', () => {
const r = detectApiIntegration('refactor the login function');
assert.strictEqual(r.detected, false);
assert(Array.isArray(r.signals));
assert.strictEqual(r.signals.length, 0);
assert.ok(r.terms && Array.isArray(r.terms.verbs));
assert.ok(Array.isArray(r.terms.nouns));
});
test('non-string input degrades to {detected:false} without throwing', () => {
assert.strictEqual(detectApiIntegration(undefined).detected, false);
assert.strictEqual(detectApiIntegration(null).detected, false);
assert.strictEqual(detectApiIntegration(42).detected, false);
assert.strictEqual(detectApiIntegration({}).detected, false);
});
test('empty / whitespace-only input does not fire', () => {
assert.strictEqual(detectApiIntegration('').detected, false);
assert.strictEqual(detectApiIntegration(' \n\t ').detected, false);
});
test('terms echo is the effective set actually used', () => {
const r = detectApiIntegration('nothing relevant here');
assert.deepStrictEqual(r.terms.verbs, [...DEFAULT_API_COVERAGE_TERMS.verbs]);
assert.deepStrictEqual(r.terms.nouns, [...DEFAULT_API_COVERAGE_TERMS.nouns]);
});
test('terms override — explicit verbs+nouns replace defaults', () => {
const r = detectApiIntegration('xyzzy the frobninator', { verbs: ['xyzzy'], nouns: ['frobninator'] });
assert.deepStrictEqual(r.terms.verbs, ['xyzzy']);
assert.deepStrictEqual(r.terms.nouns, ['frobninator']);
assert.strictEqual(r.detected, true);
});
// ── Positive: compound verb+noun (acceptance #1 trigger) ─────────────────
for (const scope of [
'Integrate the Stripe API for payment processing',
'Wrap the GitHub GraphQL API for issue triage',
'Connect to the SendGrid REST endpoint for transactional email',
'Consume the billing service over gRPC',
'Wire up the Slack webhook for deploy notifications',
'Onboard the Twilio SDK for SMS',
'integrate oauth for login',
]) {
test(`POSITIVE fires on: "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true, `expected detection for: ${scope}`);
assert.ok(r.signals.length > 0);
assert.ok(r.signals.every((s) => s.snippet.length > 0));
});
}
// ── Positive: explicit <Service> API|SDK surface (no verb needed) ────────
for (const scope of ['Add a Spotify API client', 'Ship the Notion SDK helper']) {
test(`POSITIVE (surface) fires on: "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, true);
assert.ok(r.signals.some((s) => s.verb === '(surface)'));
});
}
// ── Negative: false-positive guards (acceptance #4 — the crux) ───────────
// Each of these is a phase that is NOT an external-API integration. The
// detector must stay silent. The "public API of UserController" case is the
// canonical FP trap: "api" is present but there is no integration verb.
for (const [label, scope] of [
['internal API mention', 'The public API of the UserController should accept pagination params'],
['refactor', 'Refactor the authentication module to use bcrypt'],
['feature toggle', 'Add a dark mode toggle to the settings page'],
['bug fix', 'Fix the off-by-one error in the pagination helper'],
['docs', 'Update the README to document the config options'],
['internal client code', 'Add a client-side helper to debounce input'],
['bare noun no verb', 'We expose a REST-ish JSON shape already'],
['bare verb no noun', 'We will integrate the new design system tokens'],
]) {
test(`NEGATIVE does not fire (${label}): "${scope}"`, () => {
const r = detectApiIntegration(scope);
assert.strictEqual(r.detected, false, `unexpected detection for [${label}]: ${scope}`);
});
}
test('fenced code blocks are stripped — trigger inside a code fence does not fire', () => {
const scope = [
'Refactor the helpers.',
'',
'```bash',
'# integrate the Stripe API (example command in docs)',
'```',
'',
'No integration in this phase.',
].join('\n');
assert.strictEqual(detectApiIntegration(scope).detected, false);
});
// ── H1 fix (#1562 code review): capitalized sentence starters must not fire
// the <Service> API/SDK surface rule. These are common English, not services.
for (const [label, scope] of [
['The API', 'The API documentation needs updating.'],
['An SDK', 'An SDK is already present in the repo.'],
['Our REST', 'Our REST endpoints return JSON.'],
['This GraphQL', 'This GraphQL schema is internal.'],
['New API', 'New API surface was added by the refactor.'],
]) {
test(`NEGATIVE (stopword) does not fire (${label}): "${scope}"`, () => {
assert.strictEqual(detectApiIntegration(scope).detected, false);
});
}
test('compound signal fires when verb and noun are on the same line only', () => {
const sameLine = 'Phase A integrates things.\nLater we mention an api.';
const splitLine = 'Phase A integrates things.\nLater we mention an api here too.';
assert.strictEqual(detectApiIntegration(sameLine).detected, false);
assert.strictEqual(detectApiIntegration(splitLine).detected, false);
assert.strictEqual(detectApiIntegration('integrates the api').detected, true);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Matrix parse / validate / render
// ──────────────────────────────────────────────────────────────────────────────
describe('coverage matrix — parse / validate (#1562 acceptance #2)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib". Underlying: ${err.message}`);
}
const { parseCoverageMatrix, validateCoverageMatrix, renderCoverageMatrix } = mod;
test('parse markdown table — header + 2 rows', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| playlists | OPT-OUT | not needed yet |',
].join('\n');
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'table');
assert.strictEqual(p.rows.length, 2);
assert.strictEqual(p.rows[0].capability, 'search');
assert.strictEqual(p.rows[0].decision, 'INTEGRATE');
assert.strictEqual(p.rows[1].decision, 'OPT-OUT');
assert.strictEqual(p.rows[1].reason, 'not needed yet');
assert.strictEqual(p.errors.length, 0);
});
test('parse fenced ```coverage JSON block', () => {
const md = [
'Some prose.',
'',
'```coverage',
'[{"capability":"search","decision":"INTEGRATE","reason":""},',
' {"capability":"skip","decision":"OPT-OUT","reason":"out of scope"}]',
'```',
].join('\n');
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.strictEqual(p.rows.length, 2);
assert.strictEqual(p.errors.length, 0);
});
test('parse empty / non-matrix input → format none, no rows, no errors', () => {
const p = parseCoverageMatrix('# Notes\n\nNothing here.');
assert.strictEqual(p.format, 'none');
assert.strictEqual(p.rows.length, 0);
assert.strictEqual(p.errors.length, 0);
});
test('parse non-string → empty result, no throw', () => {
const p = parseCoverageMatrix(undefined);
assert.strictEqual(p.rows.length, 0);
});
test('parse rejects malformed fenced JSON with an error', () => {
const md = '```coverage\n{not json}\n```';
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.ok(p.errors.length > 0);
assert.strictEqual(p.rows.length, 0);
});
// ── validate: boundaries 0 / 1 / 2 rows (limit-1, limit, limit+1) ────────
test('validate — empty matrix is invalid (acceptance #1: surface must be enumerated)', () => {
const v = validateCoverageMatrix('| capability | decision | reason |\n|---|---|---|');
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /empty/i.test(e)));
assert.strictEqual(v.counts.surface, 0);
});
test('validate — single INTEGRATE row is valid (limit)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| search | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 1);
assert.strictEqual(v.counts.integrate, 1);
assert.strictEqual(v.counts.optout, 0);
});
test('validate — two rows valid (limit+1)', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| playlists | OPT-OUT | not needed |',
].join('\n');
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 2);
assert.strictEqual(v.counts.optout, 1);
});
// ── acceptance #2: every OPT-OUT must carry a reason ─────────────────────
test('validate — OPT-OUT without reason is INVALID (acceptance #2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| skip | OPT-OUT | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /missing reason/i.test(e)));
});
test('validate — OPT-OUT with one-char reason is valid (boundary)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| skip | OPT-OUT | x |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, true);
});
test('validate — duplicate capability is invalid (matrix is a set of decisions)', () => {
const md = [
'| capability | decision | reason |',
'|---|---|---|',
'| search | INTEGRATE | |',
'| Search | OPT-OUT | dup |',
].join('\n');
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /duplicate/i.test(e)));
});
test('validate — empty capability name is invalid', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /empty capability/i.test(e)));
});
// ── review-fix coverage: parser robustness (#1562 code review M1/M2/L1/L2) ──
test('validate — invalid decision cell in a table row is an error, not silently dropped (M1)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| x | INTEGRAT | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /not in \{INTEGRATE, OPT-OUT\}/i.test(e)));
});
test('validate — a pipe in a cell adds extra columns → invalid (M2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| x | OPT-OUT | a|b |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /columns|pipe/i.test(e)));
});
test('validate — a pipe in a JSON-fence reason → invalid (M2)', () => {
const md = '```coverage\n[{"capability":"x","decision":"OPT-OUT","reason":"a|b"}]\n```';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
});
test('validate — capability over the length cap → invalid (S3 bound)', () => {
const longCap = 'x'.repeat(81);
const md = `| capability | decision | reason |\n|---|---|---|\n| ${longCap} | INTEGRATE | |`;
const v = validateCoverageMatrix(md);
assert.strictEqual(v.valid, false);
assert.ok(v.errors.some((e) => /exceeds/i.test(e)));
});
test('parse — case-insensitive ```Coverage fence is accepted (L1)', () => {
const md = '```Coverage\n[{"capability":"a","decision":"INTEGRATE","reason":""}]\n```';
const p = parseCoverageMatrix(md);
assert.strictEqual(p.format, 'json');
assert.strictEqual(p.rows.length, 1);
});
test('parse — a single-dash cell is not mistaken for a separator (L2)', () => {
const md = '| capability | decision | reason |\n|---|---|---|\n| - | INTEGRATE | |';
const v = validateCoverageMatrix(md);
assert.strictEqual(v.counts.surface, 1);
assert.ok(v.valid, 'a capability named "-" is legal');
});
// ── render round-trip (manual) ───────────────────────────────────────────
test('render → parse round-trips a valid matrix', () => {
const rows = [
{ capability: 'search', decision: 'INTEGRATE', reason: '' },
{ capability: 'playlists', decision: 'OPT-OUT', reason: 'not needed yet' },
];
const rendered = renderCoverageMatrix(rows);
const v = validateCoverageMatrix(rendered);
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, 2);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// Property test: parse/render bijectivity (RULESET.TESTS property-based)
// ──────────────────────────────────────────────────────────────────────────────
describe('coverage matrix — parse/render bijection (fast-check)', () => {
let mod;
try {
mod = require(MODULE_PATH);
} catch (err) {
throw new Error(`Could not require ${MODULE_PATH}. Run "npm run build:lib". Underlying: ${err.message}`);
}
const { renderCoverageMatrix, validateCoverageMatrix } = mod;
test('any valid row set renders and re-validates to the same counts', () => {
const capabilityGen = fc.stringMatching(/^[a-z][a-z0-9-]{0,14}$/);
const rowGen = fc.record({
capability: capabilityGen,
decision: fc.constantFrom('INTEGRATE', 'OPT-OUT'),
// Reasons are short prose (e.g. "not needed yet"). The matrix is a
// markdown table, so cell text is format-safe: no pipes / newlines.
reason: fc.stringMatching(/^[a-z0-9 ,.\-!?]{0,20}$/),
});
// OPT-OUT rows must carry a non-empty reason for the round-trip to validate.
const validRowGen = rowGen.map((r) =>
r.decision === 'OPT-OUT' && r.reason.trim() === ''
? { ...r, reason: 'because' }
: { ...r, reason: r.reason.trim() }
);
const matrixGen = fc.uniqueArray(validRowGen, {
minLength: 1,
maxLength: 8,
selector: (r) => r.capability.toLowerCase(),
});
fc.assert(
fc.property(matrixGen, (rows) => {
const rendered = renderCoverageMatrix(rows);
const v = validateCoverageMatrix(rendered);
// Injected reason guarantees validity; any invalid result is a parser bug.
assert.strictEqual(v.valid, true);
assert.strictEqual(v.counts.surface, rows.length);
return true;
}),
{ numRuns: 100 }
);
});
});
// ──────────────────────────────────────────────────────────────────────────────
// CLI entry point (STDIN → exit codes mirror grep, like assumption-delta)
// ──────────────────────────────────────────────────────────────────────────────
describe('api-coverage CLI — STDIN + exit codes', () => {
const CLI = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'api-coverage.cjs');
function runCli(stdin) {
const r = spawnSync(process.execPath, [CLI, '--json'], {
input: stdin,
encoding: 'utf-8',
timeout: 15000,
});
return { exitCode: r.status, stdout: r.stdout, stderr: r.stderr };
}
test('exit 0 + JSON IR when integration detected', () => {
const r = runCli('Integrate the Stripe API for payments');
assert.strictEqual(r.exitCode, 0);
const body = JSON.parse(r.stdout);
assert.strictEqual(body.detected, true);
assert.ok(Array.isArray(body.signals));
});
test('exit 1 when no integration', () => {
const r = runCli('Refactor the login helper');
assert.strictEqual(r.exitCode, 1);
});
});

View File

@@ -81,6 +81,7 @@ describe('config-field-docs', () => {
verifier: 'workflow.verifier',
nyquist_validation: 'workflow.nyquist_validation',
ai_integration_phase: 'workflow.ai_integration_phase',
api_coverage_gate: 'workflow.api_coverage_gate',
text_mode: 'workflow.text_mode',
subagent_timeout: 'workflow.subagent_timeout',
branching_strategy: 'git.branching_strategy',

View File

@@ -0,0 +1,72 @@
'use strict';
/**
* #2071 — `gsd-tools effort sync` crashed in an INSTALLED runtime because
* commands.cjs did `require('../../../bin/install.js')`, but the installer only
* copies the `gsd-core/` subtree into a runtime home — the package-root
* `bin/install.js` is never present there, so the require threw MODULE_NOT_FOUND.
*
* This does a real minimal install into a temp home (the same helper the
* golden-parity suite uses) and runs the exact repro from the issue against the
* installed shim: `node <configDir>/gsd-core/bin/gsd-tools.cjs effort sync`. Pre-fix
* this throws `Cannot find module '../../../bin/install.js'`; post-fix the
* install-time resolvers live in the shipped sibling
* `gsd-core/bin/lib/install-effort-resolver.cjs` and the require resolves.
*
* `--config-dir <temp>` keeps it hermetic (targets the temp install, never the
* developer's real ~/.claude); effort sync defaults to dry-run so nothing is written.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
describe('#2071: effort sync runs in an installed runtime (no package-root bin/install.js)', () => {
test('effort sync does not crash reaching for the un-shipped bin/install.js', () => {
if (process.platform === 'win32') return; // install layout is POSIX-path-shaped
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
try {
// Installed layout invariant: the package-root installer is never copied in.
assert.ok(!fs.existsSync(path.join(root, 'bin', 'install.js')), 'installed home must not contain bin/install.js');
assert.ok(!fs.existsSync(path.join(configDir, 'bin', 'install.js')), 'no bin/install.js beside gsd-core');
// A project effort config gives the sync something to resolve.
fs.mkdirSync(path.join(root, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(root, '.planning', 'config.json'),
JSON.stringify({ effort: { default: 'high' } }),
);
const gsdTools = path.join(configDir, 'gsd-core', 'bin', 'gsd-tools.cjs');
let combined = '';
try {
combined = execFileSync(
process.execPath,
[gsdTools, 'effort', 'sync', '--config-dir', configDir],
{ cwd: root, encoding: 'utf-8', env: { ...process.env, HOME: root } },
);
} catch (e) {
combined = `${e.stdout || ''}${e.stderr || ''}${e.message || ''}`;
}
assert.doesNotMatch(
combined,
/Cannot find module[^\n]*install\.js|'\.\.\/\.\.\/\.\.\/bin\/install\.js'/,
`effort sync must not reach for the un-shipped bin/install.js:\n${combined}`,
);
assert.doesNotMatch(
combined,
/MODULE_NOT_FOUND/,
`effort sync must not crash on module resolution in an installed runtime:\n${combined}`,
);
} finally {
cleanup(root);
}
});
});

View File

@@ -52,6 +52,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "51b5d0ba5b1e98d9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "425dd69c629230e7",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "e176817364a7cbf4",
"gsd-core/references/autonomous-smart-discuss.md": "efd80aca449032ad",
"gsd-core/references/checkpoints.md": "2de680837faa9752",
@@ -93,7 +94,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "de81380316d8a37f",
"gsd-core/references/model-profiles.md": "d6ed560db6357ad2",
"gsd-core/references/model-profiles.md": "6568ca29b6ee00d8",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8",
@@ -108,7 +109,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "b0e55e9d585f90c1",
"gsd-core/references/planning-config.md": "2e2f418328e52f6a",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -206,13 +207,13 @@
"gsd-core/workflows/autonomous.md": "603ce2019835f00e",
"gsd-core/workflows/check-todos.md": "5a62092df800ad7c",
"gsd-core/workflows/cleanup.md": "12c8fd85d3010fe6",
"gsd-core/workflows/code-review-fix.md": "5738cb929c995008",
"gsd-core/workflows/code-review.md": "d454365f1704d0bc",
"gsd-core/workflows/code-review-fix.md": "60640e633b0a124b",
"gsd-core/workflows/code-review.md": "5c40505c01871153",
"gsd-core/workflows/complete-milestone.md": "aaf272074acec69d",
"gsd-core/workflows/debug.md": "68f1ddc74886ebe5",
"gsd-core/workflows/diagnose-issues.md": "c8c41993c277363c",
"gsd-core/workflows/discovery-phase.md": "3de990caffdde4f8",
"gsd-core/workflows/discuss-phase-assumptions.md": "42210a0cbe1bac70",
"gsd-core/workflows/discuss-phase-assumptions.md": "8581fd77def7ce84",
"gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383",
"gsd-core/workflows/discuss-phase.md": "c0a977154470b7f5",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "ab0c68941386998b",
@@ -278,7 +279,7 @@
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
"gsd-core/workflows/progress.md": "79a11ce798082054",
"gsd-core/workflows/quick.md": "35582887917ef938",
"gsd-core/workflows/quick.md": "7108075f69e88e7c",
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
"gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798",
@@ -308,7 +309,7 @@
"gsd-core/workflows/update.md": "2c58df5e21c41c31",
"gsd-core/workflows/validate-phase.md": "6c0ab739d15709fa",
"gsd-core/workflows/verify-phase.md": "0eefbb6bb1b0bed6",
"gsd-core/workflows/verify-work.md": "be699ed7920f61b0",
"gsd-core/workflows/verify-work.md": "59276d94999ee022",
"hooks/gsd-check-update-worker.js": "fa301e6366270d5f",
"hooks/gsd-check-update.js": "4617a98bf529e4c3",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "8484ad9799b7f680",
"gsd-core/references/model-profiles.md": "e067ad3df6770db1",
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "eb168188abd00101",
"gsd-core/references/planning-config.md": "ac409835e8260a3e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
"gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6",
"gsd-core/workflows/code-review.md": "ad450c6ef8459dad",
"gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075",
"gsd-core/workflows/code-review.md": "334c90c401f291f8",
"gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2",
"gsd-core/workflows/debug.md": "849d7e5b9c12dcae",
"gsd-core/workflows/diagnose-issues.md": "6cc3900891dfb927",
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
"gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60",
"gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88",
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
"gsd-core/workflows/discuss-phase.md": "5c5e8d8c2c9d95aa",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "14263db831230142",
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
"gsd-core/workflows/quick.md": "5790ceb09aa685be",
"gsd-core/workflows/quick.md": "069fe37d083a94a3",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "fd160e13f8b7e83c",
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
"gsd-core/workflows/verify-work.md": "34e980a6950cd83c",
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
"hooks/gsd-check-update-worker.js": "cc1ef5f840f9dfc9",
"hooks/gsd-check-update.js": "7b3a7983d5f1f5d3",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",

View File

@@ -51,6 +51,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
@@ -92,7 +93,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "90e2c9bc577278fd",
"gsd-core/references/model-profiles.md": "c249163663bbea53",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -107,7 +108,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "99c46b7d318adf1a",
"gsd-core/references/planning-config.md": "b025429fc72f9285",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -205,13 +206,13 @@
"gsd-core/workflows/autonomous.md": "722397c04272dfaa",
"gsd-core/workflows/check-todos.md": "6c2a43d1d3e86589",
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
"gsd-core/workflows/code-review-fix.md": "adb62c695b39ef61",
"gsd-core/workflows/code-review.md": "6c40b95e799907d0",
"gsd-core/workflows/code-review-fix.md": "722416b71b31c5fc",
"gsd-core/workflows/code-review.md": "506412604f767adc",
"gsd-core/workflows/complete-milestone.md": "dcf1182398efb1ca",
"gsd-core/workflows/debug.md": "a9397fd35cca2240",
"gsd-core/workflows/diagnose-issues.md": "75ffc381ac3059ff",
"gsd-core/workflows/discovery-phase.md": "6161c60d752d0058",
"gsd-core/workflows/discuss-phase-assumptions.md": "46876f83547db40a",
"gsd-core/workflows/discuss-phase-assumptions.md": "a8cd1db094fefd35",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "df56c8cd170b2150",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -277,7 +278,7 @@
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
"gsd-core/workflows/progress.md": "bd1ecf9207331bda",
"gsd-core/workflows/quick.md": "da83b1a15b7f1bf7",
"gsd-core/workflows/quick.md": "3363bdfefd403686",
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
"gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed",
@@ -307,7 +308,7 @@
"gsd-core/workflows/update.md": "f9e7d8a760d0d3c8",
"gsd-core/workflows/validate-phase.md": "2ac231dc541441c2",
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
"gsd-core/workflows/verify-work.md": "efe57bdbbb3af03f",
"gsd-core/workflows/verify-work.md": "c8ffee621e7319de",
"hooks/gsd-check-update-worker.js": "a530efdb5fdc0da3",
"hooks/gsd-check-update.js": "25cde66a12d6b886",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",

View File

@@ -55,6 +55,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "59f782556e4e611f",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "9a7ba3a17ece1698",
@@ -96,7 +97,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "eb144b83e31196b8",
"gsd-core/references/model-profiles.md": "1794ad3d9854129e",
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -111,7 +112,7 @@
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "a0dc2dddb953b22c",
"gsd-core/references/planning-config.md": "5a40b2934db6a321",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -209,13 +210,13 @@
"gsd-core/workflows/autonomous.md": "cc5217b1b2238a4c",
"gsd-core/workflows/check-todos.md": "32fdf33f5dc8bdde",
"gsd-core/workflows/cleanup.md": "b0ebfc48792b407b",
"gsd-core/workflows/code-review-fix.md": "b946fe7bcb303852",
"gsd-core/workflows/code-review.md": "0b24efcfa71a2ed2",
"gsd-core/workflows/code-review-fix.md": "e4549af672e74e6f",
"gsd-core/workflows/code-review.md": "a65e3e869508f89e",
"gsd-core/workflows/complete-milestone.md": "c0808127038a8f86",
"gsd-core/workflows/debug.md": "f42e8e3cbc298694",
"gsd-core/workflows/diagnose-issues.md": "e616d0d730328d68",
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
"gsd-core/workflows/discuss-phase-assumptions.md": "89987fd183e0d552",
"gsd-core/workflows/discuss-phase-assumptions.md": "9efcb2ef6a9245b3",
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
"gsd-core/workflows/discuss-phase.md": "a290d5ee19607bb7",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "250de9aae90daebc",
@@ -281,7 +282,7 @@
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
"gsd-core/workflows/progress.md": "9f326d63afb4b76b",
"gsd-core/workflows/quick.md": "68a9dfcd53f0859f",
"gsd-core/workflows/quick.md": "2afc046e94daad0a",
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
"gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90",
@@ -311,7 +312,7 @@
"gsd-core/workflows/update.md": "165beec33490bd28",
"gsd-core/workflows/validate-phase.md": "5b4ae14c87859bd2",
"gsd-core/workflows/verify-phase.md": "a4f918c92c927268",
"gsd-core/workflows/verify-work.md": "6fa216623778c541",
"gsd-core/workflows/verify-work.md": "3fb282f2bce34a79",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "8484ad9799b7f680",
"gsd-core/references/model-profiles.md": "e067ad3df6770db1",
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "eb168188abd00101",
"gsd-core/references/planning-config.md": "ac409835e8260a3e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
"gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6",
"gsd-core/workflows/code-review.md": "ad450c6ef8459dad",
"gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075",
"gsd-core/workflows/code-review.md": "334c90c401f291f8",
"gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2",
"gsd-core/workflows/debug.md": "849d7e5b9c12dcae",
"gsd-core/workflows/diagnose-issues.md": "77d98ac07c4a26ff",
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
"gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60",
"gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88",
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
"gsd-core/workflows/discuss-phase.md": "02a5381c9a2173be",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
"gsd-core/workflows/quick.md": "9033dbe58443af36",
"gsd-core/workflows/quick.md": "cc530299ba461539",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "5ff1f77222977648",
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
"gsd-core/workflows/verify-work.md": "34e980a6950cd83c",
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
"hooks/gsd-check-update-worker.js": "bdc9324a2f080ddd",
"hooks/gsd-check-update.js": "b7669f605631e506",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",

View File

@@ -87,6 +87,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "51b5d0ba5b1e98d9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "425dd69c629230e7",
"gsd-core/references/api-coverage.md": "524382216a8e713f",
"gsd-core/references/artifact-types.md": "3218cafb0c92dc32",
"gsd-core/references/autonomous-smart-discuss.md": "4156025334411073",
"gsd-core/references/checkpoints.md": "9feb961f644afa96",
@@ -128,7 +129,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "86e81f17c2f23fff",
"gsd-core/references/model-profiles.md": "bd90862f68007f2e",
"gsd-core/references/mvp-concepts.md": "23201c8118fb074a",
"gsd-core/references/phase-argument-parsing.md": "531176f66da49c98",
"gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8",
@@ -143,7 +144,7 @@
"gsd-core/references/planner-reviews.md": "7889bfa28e82156b",
"gsd-core/references/planner-revision.md": "2ebf1a714d1ec4bf",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "40d4abb1ed0f3723",
"gsd-core/references/planning-config.md": "b01856d7a63e73a2",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -241,13 +242,13 @@
"gsd-core/workflows/autonomous.md": "92f08626d4aea668",
"gsd-core/workflows/check-todos.md": "b2b103e8638e760a",
"gsd-core/workflows/cleanup.md": "5d48d64664222a3e",
"gsd-core/workflows/code-review-fix.md": "3ff5f7eac2269ca3",
"gsd-core/workflows/code-review.md": "82858ab037b9176e",
"gsd-core/workflows/code-review-fix.md": "ae7f9c6b39a23c12",
"gsd-core/workflows/code-review.md": "eadada9e0a89adf2",
"gsd-core/workflows/complete-milestone.md": "017df7443bd08da5",
"gsd-core/workflows/debug.md": "cc7b2d2fd4307a78",
"gsd-core/workflows/diagnose-issues.md": "e38bb21d06dff077",
"gsd-core/workflows/discovery-phase.md": "71a4b78ff876a854",
"gsd-core/workflows/discuss-phase-assumptions.md": "f23abfe76623ed63",
"gsd-core/workflows/discuss-phase-assumptions.md": "0d936ec25299917c",
"gsd-core/workflows/discuss-phase-power.md": "3f8b83dc6be2e9d5",
"gsd-core/workflows/discuss-phase.md": "7c9df6656b81fda9",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -313,7 +314,7 @@
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
"gsd-core/workflows/progress.md": "7bedc431bb55edb8",
"gsd-core/workflows/quick.md": "3e7686705da2af19",
"gsd-core/workflows/quick.md": "62b9e138dc70b479",
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
"gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62",
@@ -343,7 +344,7 @@
"gsd-core/workflows/update.md": "5c35c0ec0f462ea6",
"gsd-core/workflows/validate-phase.md": "020201a41049679f",
"gsd-core/workflows/verify-phase.md": "2e12c3cb97a9122a",
"gsd-core/workflows/verify-work.md": "69e27f6f419d0bba",
"gsd-core/workflows/verify-work.md": "5b348e73fc0d0829",
"hooks/gsd-check-update.js": "ef48957eb6ac6a10",
"hooks/gsd-context-monitor.js": "76fecaaa2babd6c1",
"scripts/changeset/README.md": "86ff89331dfd94b2",

View File

@@ -53,6 +53,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "51b5d0ba5b1e98d9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "425dd69c629230e7",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "f992de8b2b1a4420",
"gsd-core/references/autonomous-smart-discuss.md": "efd80aca449032ad",
"gsd-core/references/checkpoints.md": "c70b323dcb1583d5",
@@ -94,7 +95,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "1d392e37a746742a",
"gsd-core/references/model-profiles.md": "d6ed560db6357ad2",
"gsd-core/references/model-profiles.md": "6568ca29b6ee00d8",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8",
@@ -109,7 +110,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "fd81dd276828eab4",
"gsd-core/references/planning-config.md": "309a566a0e13e43e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -207,13 +208,13 @@
"gsd-core/workflows/autonomous.md": "dd972ddde9663295",
"gsd-core/workflows/check-todos.md": "be9b50b5f28d7504",
"gsd-core/workflows/cleanup.md": "8233b05ebf011bec",
"gsd-core/workflows/code-review-fix.md": "ab2b467b41522c10",
"gsd-core/workflows/code-review.md": "1bbbe61f45fccf4c",
"gsd-core/workflows/code-review-fix.md": "fda53892ae4b17fc",
"gsd-core/workflows/code-review.md": "f1c045ec4d33abc8",
"gsd-core/workflows/complete-milestone.md": "470cf39261400ee2",
"gsd-core/workflows/debug.md": "36cb536be452d79e",
"gsd-core/workflows/diagnose-issues.md": "42acbe2a43fc886e",
"gsd-core/workflows/discovery-phase.md": "8e99da61fb2b7074",
"gsd-core/workflows/discuss-phase-assumptions.md": "ab2d26b194805ac1",
"gsd-core/workflows/discuss-phase-assumptions.md": "ab0c432b84038681",
"gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383",
"gsd-core/workflows/discuss-phase.md": "31a79ea11c1cdd5f",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "654cdaf1138d5e27",
@@ -279,7 +280,7 @@
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
"gsd-core/workflows/progress.md": "6b9a84a43dc55af5",
"gsd-core/workflows/quick.md": "7ddfe17f048541ec",
"gsd-core/workflows/quick.md": "eb68ea8748546331",
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
"gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754",
@@ -309,7 +310,7 @@
"gsd-core/workflows/update.md": "f444a7cfcd246cfb",
"gsd-core/workflows/validate-phase.md": "2f705775a4b76d42",
"gsd-core/workflows/verify-phase.md": "5c72780e34214e27",
"gsd-core/workflows/verify-work.md": "58e9b1b16f773b53",
"gsd-core/workflows/verify-work.md": "c966971a3cbd1d71",
"hooks/gsd-session.json": "0a462834f2a28fee",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "3d62d178004db5cc",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "273b371c5751f35a",
"gsd-core/references/checkpoints.md": "3001eeccb319781b",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "f4c013e700c52b08",
"gsd-core/references/model-profiles.md": "5452b2e19e19f77e",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "8af05de88771e9f4",
"gsd-core/references/planning-config.md": "a3f1b4aca291db59",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "4b20eda9a0b587ce",
"gsd-core/workflows/check-todos.md": "1a67337d1630848f",
"gsd-core/workflows/cleanup.md": "db47963f103c2748",
"gsd-core/workflows/code-review-fix.md": "49e7f024c551b3a5",
"gsd-core/workflows/code-review.md": "6a9fd2996a6b600e",
"gsd-core/workflows/code-review-fix.md": "c57af378033b1b58",
"gsd-core/workflows/code-review.md": "32c37bcbec8b8698",
"gsd-core/workflows/complete-milestone.md": "1400a4856f592f3e",
"gsd-core/workflows/debug.md": "a73d8018986131ba",
"gsd-core/workflows/diagnose-issues.md": "cd582747131726e3",
"gsd-core/workflows/discovery-phase.md": "7dcf150998559c11",
"gsd-core/workflows/discuss-phase-assumptions.md": "373788c6b7eab272",
"gsd-core/workflows/discuss-phase-assumptions.md": "c25c6a6c633d71b6",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "14688ff19ce2184c",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "30612a2de153cb5b",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
"gsd-core/workflows/progress.md": "65aabee5e8a6dd82",
"gsd-core/workflows/quick.md": "762256cf6d177c06",
"gsd-core/workflows/quick.md": "32a71f62041c8510",
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
"gsd-core/workflows/remove-workspace.md": "433affcd1a200826",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "23e294ba707c3580",
"gsd-core/workflows/validate-phase.md": "2df0c6e298a5f249",
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
"gsd-core/workflows/verify-work.md": "145596b2542c457a",
"gsd-core/workflows/verify-work.md": "e7e7e900c4874490",
"hooks/gsd-cursor-post-tool.js": "019d503aee8b4a3f",
"hooks/gsd-cursor-session-start.js": "c6e04ed597ea7020",
"scripts/changeset/README.md": "86ff89331dfd94b2",

View File

@@ -52,6 +52,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "892f086e846cd8e4",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "db8a7425ed808e24",
@@ -93,7 +94,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "8472d453a325cc1b",
"gsd-core/references/model-profiles.md": "6012c3b53473f04f",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -108,7 +109,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "1358a14bded944f5",
"gsd-core/references/planning-config.md": "831d626c214d92e4",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -206,13 +207,13 @@
"gsd-core/workflows/autonomous.md": "dd572ca89862e5ec",
"gsd-core/workflows/check-todos.md": "ea9a303c48a5d752",
"gsd-core/workflows/cleanup.md": "6c488059fc152a49",
"gsd-core/workflows/code-review-fix.md": "04b143a963067236",
"gsd-core/workflows/code-review.md": "ff28724fc216972c",
"gsd-core/workflows/code-review-fix.md": "e829d3baf9901b54",
"gsd-core/workflows/code-review.md": "50a05ab8957bd05f",
"gsd-core/workflows/complete-milestone.md": "f1866541148dc291",
"gsd-core/workflows/debug.md": "15cf6999e85dcc8c",
"gsd-core/workflows/diagnose-issues.md": "16f2d2a85335641f",
"gsd-core/workflows/discovery-phase.md": "6161c60d752d0058",
"gsd-core/workflows/discuss-phase-assumptions.md": "db60ec68b59a40eb",
"gsd-core/workflows/discuss-phase-assumptions.md": "3a1e215890d2b3f4",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "0aa052ae4ee70bf6",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "536e2fa842f1bc95",
@@ -278,7 +279,7 @@
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
"gsd-core/workflows/progress.md": "f18db000584d9cb1",
"gsd-core/workflows/quick.md": "5044ce4e7512e174",
"gsd-core/workflows/quick.md": "e7a395b9e7786b56",
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
"gsd-core/workflows/remove-workspace.md": "8facde381657dd71",
@@ -308,7 +309,7 @@
"gsd-core/workflows/update.md": "7499bb4cb2a3ce6f",
"gsd-core/workflows/validate-phase.md": "75e8971d3981d06b",
"gsd-core/workflows/verify-phase.md": "5c8d1305b47fbef4",
"gsd-core/workflows/verify-work.md": "e56e07475d5eb51e",
"gsd-core/workflows/verify-work.md": "7a9c9541d2d73fdc",
"hooks/gsd-check-update-worker.js": "7989cc2bedd1138d",
"hooks/gsd-check-update.js": "25f5ad726f76fc11",
"hooks/gsd-config-reload.js": "880b696458e85e9b",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "51b5d0ba5b1e98d9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "425dd69c629230e7",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "3986d58011bf9006",
"gsd-core/references/checkpoints.md": "9feb961f644afa96",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "d6ed560db6357ad2",
"gsd-core/references/model-profiles.md": "6568ca29b6ee00d8",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "aad463ddda23dbaf",
"gsd-core/references/planning-config.md": "37ab69107a2f7dc6",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "73f429f7472c9949",
"gsd-core/workflows/check-todos.md": "ed4b4eb12be222d7",
"gsd-core/workflows/cleanup.md": "c5f1bf186395d67d",
"gsd-core/workflows/code-review-fix.md": "adb62c695b39ef61",
"gsd-core/workflows/code-review.md": "6c40b95e799907d0",
"gsd-core/workflows/code-review-fix.md": "722416b71b31c5fc",
"gsd-core/workflows/code-review.md": "506412604f767adc",
"gsd-core/workflows/complete-milestone.md": "59753bf44d4300da",
"gsd-core/workflows/debug.md": "a72d830ac3df74aa",
"gsd-core/workflows/diagnose-issues.md": "210b5b313e8a559a",
"gsd-core/workflows/discovery-phase.md": "ca7b2be46e59e862",
"gsd-core/workflows/discuss-phase-assumptions.md": "36f7edcb666745f2",
"gsd-core/workflows/discuss-phase-assumptions.md": "3ef1df313e715387",
"gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383",
"gsd-core/workflows/discuss-phase.md": "bd4b26ba168bb51f",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "f8ae26ba4e07672b",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
"gsd-core/workflows/progress.md": "9381c59676ccb937",
"gsd-core/workflows/quick.md": "fd7461e5a92fe450",
"gsd-core/workflows/quick.md": "e71c92cf0463a461",
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
"gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "07dc2fba78ad1865",
"gsd-core/workflows/validate-phase.md": "557e3251e3b9349a",
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
"gsd-core/workflows/verify-work.md": "b68ac37f6301a3b5",
"gsd-core/workflows/verify-work.md": "9fc717845828c6e3",
"kilo.json": "13151e97ff23c1aa",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",

View File

@@ -88,6 +88,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
@@ -129,7 +130,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "8484ad9799b7f680",
"gsd-core/references/model-profiles.md": "e067ad3df6770db1",
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -144,7 +145,7 @@
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "eb168188abd00101",
"gsd-core/references/planning-config.md": "ac409835e8260a3e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -242,13 +243,13 @@
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
"gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6",
"gsd-core/workflows/code-review.md": "ad450c6ef8459dad",
"gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075",
"gsd-core/workflows/code-review.md": "334c90c401f291f8",
"gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2",
"gsd-core/workflows/debug.md": "849d7e5b9c12dcae",
"gsd-core/workflows/diagnose-issues.md": "67c058fc7ae6026b",
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
"gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60",
"gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88",
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
"gsd-core/workflows/discuss-phase.md": "0c0465ba668adb33",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -314,7 +315,7 @@
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
"gsd-core/workflows/quick.md": "e265e01cfe117215",
"gsd-core/workflows/quick.md": "09d88dd1c0b3e03a",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
@@ -344,7 +345,7 @@
"gsd-core/workflows/update.md": "6718e0632bba26ca",
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
"gsd-core/workflows/verify-work.md": "34e980a6950cd83c",
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "51b5d0ba5b1e98d9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "425dd69c629230e7",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "218c55caf8aff6df",
"gsd-core/references/autonomous-smart-discuss.md": "3986d58011bf9006",
"gsd-core/references/checkpoints.md": "9feb961f644afa96",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "858c16730df68ac2",
"gsd-core/references/model-profiles.md": "d6ed560db6357ad2",
"gsd-core/references/model-profiles.md": "6568ca29b6ee00d8",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "aad463ddda23dbaf",
"gsd-core/references/planning-config.md": "37ab69107a2f7dc6",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "6cb0c014f5f56965",
"gsd-core/workflows/check-todos.md": "6526b64ee88c7d2e",
"gsd-core/workflows/cleanup.md": "94d771d26f62dc86",
"gsd-core/workflows/code-review-fix.md": "5d97b960da4fef73",
"gsd-core/workflows/code-review.md": "aec8ef4de1829607",
"gsd-core/workflows/code-review-fix.md": "adb9388bb157610c",
"gsd-core/workflows/code-review.md": "ae6bcbd1575aeec4",
"gsd-core/workflows/complete-milestone.md": "614299b2c08e66c3",
"gsd-core/workflows/debug.md": "9d4a8afc8d36be93",
"gsd-core/workflows/diagnose-issues.md": "2971c699d52f1b85",
"gsd-core/workflows/discovery-phase.md": "724408336596c50c",
"gsd-core/workflows/discuss-phase-assumptions.md": "310b08dc31710cbc",
"gsd-core/workflows/discuss-phase-assumptions.md": "92e43cd12200c610",
"gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383",
"gsd-core/workflows/discuss-phase.md": "3a62a8d4c374c69f",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "83ca3ea4fac785af",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
"gsd-core/workflows/progress.md": "8fc3404087f50b95",
"gsd-core/workflows/quick.md": "10cc8ddd4bc3f0ee",
"gsd-core/workflows/quick.md": "9bdccb7c6a5530c9",
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
"gsd-core/workflows/remove-workspace.md": "446847e71aa52504",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "71b6cd852f38b4bc",
"gsd-core/workflows/validate-phase.md": "abcdbc1b56780565",
"gsd-core/workflows/verify-phase.md": "126be1d026900102",
"gsd-core/workflows/verify-work.md": "f0d205568abfaf74",
"gsd-core/workflows/verify-work.md": "ae07b3fa8b6f864e",
"hooks/gsd-check-update-worker.js": "385fb7c67810baf6",
"hooks/gsd-check-update.js": "4549451414ffa7d7",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",

View File

@@ -52,6 +52,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "be09755fed7ad856",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "046171320f816346",
@@ -93,7 +94,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "cc1efc2942164744",
"gsd-core/references/model-profiles.md": "0b7e06ed2e4abac8",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -108,7 +109,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "0037314fe38ca55c",
"gsd-core/references/planning-config.md": "4c6de9b6d66aca73",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -206,13 +207,13 @@
"gsd-core/workflows/autonomous.md": "3014ff90115f0daf",
"gsd-core/workflows/check-todos.md": "ec8c22920f6b2df1",
"gsd-core/workflows/cleanup.md": "6a18b165752e3087",
"gsd-core/workflows/code-review-fix.md": "e14afce87cf0d420",
"gsd-core/workflows/code-review.md": "d6d385b6dadae5a0",
"gsd-core/workflows/code-review-fix.md": "f3725ae9d685bed2",
"gsd-core/workflows/code-review.md": "29125604bed2c467",
"gsd-core/workflows/complete-milestone.md": "40085d32b15805c8",
"gsd-core/workflows/debug.md": "c23d067580b09f63",
"gsd-core/workflows/diagnose-issues.md": "652ae26975f82242",
"gsd-core/workflows/discovery-phase.md": "6161c60d752d0058",
"gsd-core/workflows/discuss-phase-assumptions.md": "3865afc23e9cd46a",
"gsd-core/workflows/discuss-phase-assumptions.md": "18712f78bb960ec8",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "8644b72386a51241",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "3102430dfad9c012",
@@ -278,7 +279,7 @@
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
"gsd-core/workflows/progress.md": "3b1b2142a74af85c",
"gsd-core/workflows/quick.md": "0a9a7dacc73f8e53",
"gsd-core/workflows/quick.md": "bb9a4a145a7edc36",
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
"gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e",
@@ -308,7 +309,7 @@
"gsd-core/workflows/update.md": "b34cb866152d4de3",
"gsd-core/workflows/validate-phase.md": "e989cbaa4228564c",
"gsd-core/workflows/verify-phase.md": "0dc52b9e629a5f5a",
"gsd-core/workflows/verify-work.md": "b5afb65fdf311301",
"gsd-core/workflows/verify-work.md": "3355ddc14f052fff",
"hooks/gsd-check-update-worker.js": "4bb354044e0dff91",
"hooks/gsd-check-update.js": "d2065cb3e725a42a",
"hooks/gsd-config-reload.js": "4f52b8a0120bb1b8",

View File

@@ -52,6 +52,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "18d1ff7c7fa0bab1",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "4b8c645ffa695067",
@@ -93,7 +94,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "41b0af6f5f77af81",
"gsd-core/references/model-profiles.md": "b4527b0f255d193f",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -108,7 +109,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "e47cdb2337e10dac",
"gsd-core/references/planning-config.md": "a686c1363ae626c5",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -206,13 +207,13 @@
"gsd-core/workflows/autonomous.md": "c482645c5d1ead46",
"gsd-core/workflows/check-todos.md": "0dda8236355e8c9c",
"gsd-core/workflows/cleanup.md": "82f65f5214ecd748",
"gsd-core/workflows/code-review-fix.md": "959a65773b325ed2",
"gsd-core/workflows/code-review.md": "b31bbd7dbe5ed303",
"gsd-core/workflows/code-review-fix.md": "f2761f7f8c4a5674",
"gsd-core/workflows/code-review.md": "47663a2922756c5e",
"gsd-core/workflows/complete-milestone.md": "6e918b72bd885426",
"gsd-core/workflows/debug.md": "197d3642a6704de5",
"gsd-core/workflows/diagnose-issues.md": "9274b11a3db98c65",
"gsd-core/workflows/discovery-phase.md": "b32b6197b66c9a13",
"gsd-core/workflows/discuss-phase-assumptions.md": "3d05b08eae75c7bd",
"gsd-core/workflows/discuss-phase-assumptions.md": "e376b1cf29379df4",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "4509657816c5566a",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "c9cd7db62e76ebcb",
@@ -278,7 +279,7 @@
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
"gsd-core/workflows/progress.md": "94768f835b0b8908",
"gsd-core/workflows/quick.md": "9bbf907e39688638",
"gsd-core/workflows/quick.md": "a568015cccb53615",
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
"gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663",
@@ -308,7 +309,7 @@
"gsd-core/workflows/update.md": "1f935251fca1f276",
"gsd-core/workflows/validate-phase.md": "1c0ebe56d96a14d1",
"gsd-core/workflows/verify-phase.md": "a151ed36eb51813b",
"gsd-core/workflows/verify-work.md": "d3a0970205acc6a5",
"gsd-core/workflows/verify-work.md": "dde2a42a56c27c4e",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",

View File

@@ -52,6 +52,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "849977da771f2b06",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "8bd01fd75a2ba70e",
"gsd-core/references/autonomous-smart-discuss.md": "273b371c5751f35a",
"gsd-core/references/checkpoints.md": "808e4fcaa2fda15c",
@@ -93,7 +94,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "b3ac5dc094bce1dc",
"gsd-core/references/model-profiles.md": "09aa53e3f1764a41",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -108,7 +109,7 @@
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "aafbfc62a3bb84b1",
"gsd-core/references/planning-config.md": "1fc70920778d7c8e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -206,13 +207,13 @@
"gsd-core/workflows/autonomous.md": "fe7bb7c978f305a2",
"gsd-core/workflows/check-todos.md": "afc840fceb07bf99",
"gsd-core/workflows/cleanup.md": "93c14107979a4428",
"gsd-core/workflows/code-review-fix.md": "9b2de93a6087774d",
"gsd-core/workflows/code-review.md": "2f0ebfb7a2e33764",
"gsd-core/workflows/code-review-fix.md": "b99b1f20bb27c291",
"gsd-core/workflows/code-review.md": "faa87faf07ae765a",
"gsd-core/workflows/complete-milestone.md": "f463bf4e86ac26f6",
"gsd-core/workflows/debug.md": "52243eb936a43150",
"gsd-core/workflows/diagnose-issues.md": "447072aa72385271",
"gsd-core/workflows/discovery-phase.md": "7dcf150998559c11",
"gsd-core/workflows/discuss-phase-assumptions.md": "f57000d5c5fb178a",
"gsd-core/workflows/discuss-phase-assumptions.md": "b091b3d3e580dd29",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "ce4e6abce9cb2d4e",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "cb49f485c4a1f09e",
@@ -278,7 +279,7 @@
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
"gsd-core/workflows/progress.md": "18813a345bd2343a",
"gsd-core/workflows/quick.md": "3acd391cc0c3813a",
"gsd-core/workflows/quick.md": "b7813e1810c683e1",
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
"gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a",
@@ -308,7 +309,7 @@
"gsd-core/workflows/update.md": "79aaf4b8f1f83045",
"gsd-core/workflows/validate-phase.md": "2db47bf5547d7b9d",
"gsd-core/workflows/verify-phase.md": "f961cdb3ef03ff05",
"gsd-core/workflows/verify-work.md": "5ad63a5edfb6acac",
"gsd-core/workflows/verify-work.md": "cce8ae44b30957f1",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",

View File

@@ -123,6 +123,7 @@
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
@@ -164,7 +165,7 @@
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
"gsd-core/references/model-profiles.md": "8484ad9799b7f680",
"gsd-core/references/model-profiles.md": "e067ad3df6770db1",
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
@@ -179,7 +180,7 @@
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "eb168188abd00101",
"gsd-core/references/planning-config.md": "ac409835e8260a3e",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
@@ -277,13 +278,13 @@
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
"gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6",
"gsd-core/workflows/code-review.md": "ad450c6ef8459dad",
"gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075",
"gsd-core/workflows/code-review.md": "334c90c401f291f8",
"gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2",
"gsd-core/workflows/debug.md": "849d7e5b9c12dcae",
"gsd-core/workflows/diagnose-issues.md": "aa8d787db8f3c46c",
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
"gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60",
"gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88",
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
"gsd-core/workflows/discuss-phase.md": "d0a67b6f77b4d339",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
@@ -349,7 +350,7 @@
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
"gsd-core/workflows/quick.md": "5747c2d8a37affbb",
"gsd-core/workflows/quick.md": "12917fdc9a624a0c",
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
@@ -379,7 +380,7 @@
"gsd-core/workflows/update.md": "dc580dee13f881a6",
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
"gsd-core/workflows/verify-work.md": "34e980a6950cd83c",
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",

View File

@@ -5,7 +5,10 @@
* Hook points tested: discuss:pre, discuss:post, execute:pre, execute:wave:pre,
* verify:pre, ship:post
*
* All 6 points have zero hooks in the real registry by design.
* 5 of these points have zero hooks in the real registry by design.
* verify:pre graduated out of the empty set in #1562 (it now carries the
* ai-integration `api-coverage.verify-pre` blocking gate); SECTION 5 pins the
* new contract + retains synthetic extension-point-mechanics coverage.
* Tests pin: exact envelope shape, placeholder string contract (Hyrum's Law),
* resolver-filter mechanics (schema default / config-override / capabilityStatesById),
* CLI contract (missing-arg, invalid-point), and Postel-leniency (malformed config).
@@ -481,8 +484,13 @@ describe('execute:wave:pre — real registry empty-resolution + synthetic mechan
// ─────────────────────────────────────────────────────────────────────────────
// SECTION 5: verify:pre
// ─────────────────────────────────────────────────────────────────────────────
// NOTE: verify:pre was an empty extension point until #1562 added the
// ai-integration `api-coverage.verify-pre` blocking gate (default-on, opt-out
// via workflow.api_coverage_gate=false). The real-registry assertions below pin
// the NEW contract; the synthetic BVA tests retain extension-point-mechanics
// coverage. Empty-point coverage for the other 5 points is unaffected.
describe('verify:pre — real registry empty-resolution + synthetic extension-point readiness', () => {
describe('verify:pre — real registry carries the api-coverage gate (#1562); synthetic mechanics', () => {
let tmpEmptyProjectDir;
let tmpProjectDirAllOn;
before(() => {
@@ -494,29 +502,47 @@ describe('verify:pre — real registry empty-resolution + synthetic extension-po
cleanup(tmpProjectDirAllOn);
});
it('[empty-resolution] verify:pre with real registry and no config yields empty activeHooks and exact placeholder (Gall\'s Law)', () => {
it('[default-on] verify:pre with real registry and no config yields the api-coverage blocking gate (full-coverage-by-default)', () => {
const resolved = resolveLoopHooks({ point: 'verify:pre', registry: realRegistry, config: {} });
assert.strictEqual(resolved.activeHooks.length, 0);
assert.strictEqual(renderLoopHooks(resolved), '_No active hooks at verify:pre._');
const gate = resolved.activeHooks.find((h) => h.capId === 'ai-integration' && h.kind === 'gate');
assert.ok(gate, 'ai-integration api-coverage gate must register at verify:pre by default');
assert.strictEqual(gate.blocking, true);
assert.strictEqual(gate.check.query, 'api-coverage.verify-pre');
});
it('[happy] verify:pre E2E subprocess returns well-formed 3-key JSON envelope with empty activeHooks (Hyrum\'s Law contract pin)', () => {
it('[happy] verify:pre E2E subprocess returns a well-formed envelope carrying the gate (Hyrum\'s Law contract pin)', () => {
const result = spawnGsd(['loop', 'render-hooks', 'verify:pre', '--cwd', tmpEmptyProjectDir, '--raw'], tmpEmptyProjectDir);
assert.strictEqual(result.status, 0, `expected exit 0. stderr: ${result.stderr}`);
const envelope = JSON.parse(result.stdout.trim());
assert.strictEqual(envelope.point, 'verify:pre');
assert.deepEqual(envelope.activeHooks, []);
assert.strictEqual(envelope.rendered, '_No active hooks at verify:pre._');
assert.ok(Array.isArray(envelope.activeHooks));
assert.ok(envelope.activeHooks.some((h) => h.capId === 'ai-integration' && h.kind === 'gate'));
assert.deepEqual(Object.keys(envelope).sort(), ['activeHooks', 'point', 'rendered']);
});
it('[negative] verify:pre with all capability config keys set to true still yields empty activeHooks — no leakage from other points', () => {
it('[opt-out] verify:pre with workflow.api_coverage_gate=false yields NO ai-integration gate — config opt-out empties the point', () => {
const resolved = resolveLoopHooks({
point: 'verify:pre',
registry: realRegistry,
config: { workflow: { api_coverage_gate: false } },
});
assert.strictEqual(
resolved.activeHooks.find((h) => h.capId === 'ai-integration'),
undefined,
'opting out api_coverage_gate must remove the gate from verify:pre'
);
});
it('[negative] verify:pre with unrelated capability keys on does not bleed non-verify:pre hooks into the point', () => {
const resolved = resolveLoopHooks({
point: 'verify:pre',
registry: realRegistry,
config: { workflow: { ui_phase: true, ui_review: true, ui_safety_gate: true } },
});
assert.strictEqual(resolved.activeHooks.length, 0, 'UI and other capabilities must not bleed through to verify:pre');
// The only verify:pre hook is the api-coverage gate; UI/other hooks must not bleed in.
for (const h of resolved.activeHooks) {
assert.notStrictEqual(h.capId, 'ui', 'UI capability must not bleed through to verify:pre');
}
});
it('[bva] Synthetic step at verify:pre with configSchema default=true fires correctly — extension point readiness', () => {
@@ -547,7 +573,10 @@ describe('verify:pre — real registry empty-resolution + synthetic extension-po
const result = spawnGsd(['loop', 'render-hooks', 'verify:pre', '--cwd', malformedDir, '--raw'], malformedDir);
assert.strictEqual(result.status, 0, `must not crash on malformed config. stderr: ${result.stderr}`);
const envelope = JSON.parse(result.stdout.trim());
assert.deepEqual(envelope.activeHooks, []);
// Malformed config degrades to defaults (gate default-on); the contract
// here is leniency (exit 0, well-formed envelope), not emptiness.
assert.strictEqual(envelope.point, 'verify:pre');
assert.ok(Array.isArray(envelope.activeHooks));
} finally {
cleanup(malformedDir);
}
@@ -720,13 +749,12 @@ describe('CLI contract — missing/invalid point argument (shared across all 6 e
// SECTION 8: Parametric empty-point sweep across all 6 points (E2E regression guard)
// ─────────────────────────────────────────────────────────────────────────────
describe('Parametric E2E sweep — 5 empty points return correct envelope shape via real registry (ship:post excluded — mempalace registers 1 step there)', () => {
describe('Parametric E2E sweep — 4 empty points return correct envelope shape via real registry (ship:post excluded — mempalace registers 1 step there; verify:pre excluded since #1562 added the api-coverage gate)', () => {
const EMPTY_POINTS = [
'discuss:pre',
'discuss:post',
'execute:pre',
'execute:wave:pre',
'verify:pre',
];
for (const point of EMPTY_POINTS) {

View File

@@ -120,6 +120,36 @@ describe('resolveModelInternal', () => {
assert.ok(typeof model === 'string' && model.length > 0);
});
// #2072 acceptance: these two catalog agents' config MUST resolve — the bug was
// that the workflows never threaded the resolved value, not that the resolver
// ignored it. These assert the value the (now-threaded) spawns receive.
test('#2072: model_overrides applies to gsd-code-reviewer', () => {
writeConfig(tmpDir, { model_overrides: { 'gsd-code-reviewer': 'my-custom-model' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-reviewer'), 'my-custom-model');
});
test('#2072: model_overrides applies to gsd-assumptions-analyzer', () => {
writeConfig(tmpDir, { model_overrides: { 'gsd-assumptions-analyzer': 'my-custom-model' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-assumptions-analyzer'), 'my-custom-model');
});
test('#2072: models.verification tier applies to gsd-code-reviewer', () => {
writeConfig(tmpDir, { models: { verification: 'opus' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-reviewer'), 'opus');
});
test('#2072: models.discuss tier applies to gsd-assumptions-analyzer', () => {
writeConfig(tmpDir, { models: { discuss: 'opus' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-assumptions-analyzer'), 'opus');
});
test('#2072: model_overrides + models.execution apply to gsd-code-fixer', () => {
writeConfig(tmpDir, { model_overrides: { 'gsd-code-fixer': 'my-custom-model' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-fixer'), 'my-custom-model');
writeConfig(tmpDir, { models: { execution: 'opus' } });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-fixer'), 'opus');
});
test('runtime non-claude + model_profile_overrides for runtime tier', () => {
writeConfig(tmpDir, {
runtime: 'codex',

View File

@@ -0,0 +1,150 @@
// allow-test-rule: source-text-is-the-product #2072
// Workflow .md files ARE the deployed orchestration contract the runtime executes;
// asserting that a spawn threads a resolved model= is asserting the deployed contract.
'use strict';
/**
* #2072 — model_overrides / models.<phaseType> were silently inert for
* gsd-assumptions-analyzer and gsd-code-reviewer on Claude: the resolver honored
* them, but the workflows spawned the agents with NO model= param, so the resolved
* value never reached the Agent tool and the agents inherited the session model.
*
* Fix contract: every spawn of these two catalog agents must thread a resolved
* model=, and the workflow must obtain that model (inline `resolve-model` for the
* single-agent workflows, or the `reviewer_model` field of the init.quick bundle).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const WF = path.join(__dirname, '..', 'gsd-core', 'workflows');
const read = (rel) => fs.readFileSync(path.join(WF, rel), 'utf-8');
// Every .md under gsd-core/workflows (incl. nested steps/ and modes/).
function allWorkflowMd() {
const out = [];
(function walk(dir, rel) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory()) walk(path.join(dir, e.name), path.join(rel, e.name));
else if (e.name.endsWith('.md')) out.push(path.join(rel, e.name));
}
})(WF, '');
return out;
}
// Return the full text of every `Agent( … )` call in `content`, tracking string
// state (triple- and single-double-quoted) and paren depth so a prompt body's own
// parens/quotes don't end the call early. Order-independent: a call's params are
// captured whether subagent_type= appears before or after the prompt.
function agentCalls(content) {
const calls = [];
const re = /Agent\(/g;
let m;
while ((m = re.exec(content)) !== null) {
let i = m.index + m[0].length;
let depth = 1;
let tq = false; // inside """ … """
let sq = false; // inside " … "
while (i < content.length && depth > 0) {
if (tq) {
if (content.startsWith('"""', i)) { tq = false; i += 3; continue; }
i++; continue;
}
if (sq) {
if (content[i] === '\\') { i += 2; continue; }
if (content[i] === '"') { sq = false; }
i++; continue;
}
if (content.startsWith('"""', i)) { tq = true; i += 3; continue; }
if (content[i] === '"') { sq = true; i++; continue; }
if (content[i] === '(') { depth++; }
else if (content[i] === ')') { depth--; }
i++;
}
calls.push(content.slice(m.index, i));
re.lastIndex = i; // don't re-scan inside this call
}
return calls;
}
describe('#2072: routed-agent spawns thread the resolved model', () => {
test('discuss-phase-assumptions.md resolves + threads gsd-assumptions-analyzer model', () => {
const c = read('discuss-phase-assumptions.md');
assert.match(c, /resolve-model gsd-assumptions-analyzer/, 'must resolve the routed model');
assert.match(
c,
/subagent_type="gsd-assumptions-analyzer",\s*model="\{ANALYZER_MODEL\}"/,
'spawn must thread the resolved model=',
);
});
test('code-review.md resolves + threads gsd-code-reviewer model', () => {
const c = read('code-review.md');
assert.match(c, /resolve-model gsd-code-reviewer/);
assert.match(c, /subagent_type="gsd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/);
});
test('code-review-fix.md re-review resolves + threads gsd-code-reviewer model', () => {
const c = read('code-review-fix.md');
assert.match(c, /resolve-model gsd-code-reviewer/);
assert.match(c, /subagent_type="gsd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/);
});
test('quick.md review step threads gsd-code-reviewer own model (not executor_model)', () => {
const c = read('quick.md');
// reviewer_model comes from the init.quick bundle; the spawn must use it.
assert.match(c, /subagent_type="gsd-code-reviewer",\s*\n\s*model="\{reviewer_model\}"/);
assert.doesNotMatch(
c,
/subagent_type="gsd-code-reviewer",\s*\n\s*model="\{executor_model\}"/,
'reviewer must not reuse the executor model (own model_overrides would be ignored)',
);
});
test('code-review-fix.md threads gsd-code-fixer model at both fixer spawns', () => {
const c = read('code-review-fix.md');
assert.match(c, /resolve-model gsd-code-fixer/, 'must resolve the fixer model');
const fixerSpawns = c.match(/subagent_type="gsd-code-fixer", model="\{FIXER_MODEL\}"/g) || [];
assert.strictEqual(fixerSpawns.length, 2, 'both gsd-code-fixer spawns must thread FIXER_MODEL');
});
// Parity guard: EVERY spawn of the fixed routed agents across ALL workflows must
// carry a model= in its Agent(...) call, so a new silently-inert spawn cannot
// regress. Uses a quote/paren-aware scan of the WHOLE Agent(...) call, so it holds
// for single-line and multi-line calls, multiple spawns per file, and either param
// ordering (subagent_type= before OR after the prompt body).
test('no spawn of the fixed routed agents is missing a model= (parity guard)', () => {
const ROUTED = /subagent_type="(gsd-code-reviewer|gsd-assumptions-analyzer|gsd-code-fixer)"/;
const offenders = [];
for (const rel of allWorkflowMd()) {
for (const call of agentCalls(read(rel))) {
const routed = call.match(ROUTED);
if (routed && !/\bmodel\s*=/.test(call)) {
offenders.push(`${rel}: ${routed[1]} spawn missing model=`);
}
}
}
assert.deepEqual(offenders, [], `routed-agent spawn(s) missing model=:\n${offenders.join('\n')}`);
});
// The scanner itself must catch a prompt-first, un-threaded spawn (the exact blind
// spot a naive "params before prompt=" heuristic misses) — otherwise the guard
// above could pass vacuously.
test('parity guard detects a prompt-first spawn that omits model=', () => {
const synthetic = [
'Agent(',
' prompt="""do the thing (with parens) and a " quote""",',
' subagent_type="gsd-code-reviewer"',
')',
].join('\n');
const [call] = agentCalls(synthetic);
assert.ok(/subagent_type="gsd-code-reviewer"/.test(call), 'scanner must capture the prompt-first subagent_type');
assert.ok(!/\bmodel\s*=/.test(call), 'and correctly see that model= is absent');
// A well-formed prompt-first spawn WITH model= must be accepted.
const ok = agentCalls(synthetic.replace(')', ' model="{reviewer_model}"\n)'))[0];
assert.ok(/\bmodel\s*=/.test(ok));
});
});

View File

@@ -246,6 +246,7 @@ describe('plan:pre all-off — empty resolution', () => {
tmpDir = makeProject({
workflow: {
ai_integration_phase: false,
api_coverage_gate: false,
tdd_mode: false,
security_enforcement: false,
ui_phase: false,

View File

@@ -11,13 +11,13 @@
"autonomous.md": 42474,
"check-todos.md": 9475,
"cleanup.md": 9941,
"code-review-fix.md": 23934,
"code-review.md": 31646,
"code-review-fix.md": 24320,
"code-review.md": 31916,
"complete-milestone.md": 31071,
"debug.md": 13549,
"diagnose-issues.md": 12864,
"discovery-phase.md": 8651,
"discuss-phase-assumptions.md": 27028,
"discuss-phase-assumptions.md": 27302,
"discuss-phase-power.md": 11273,
"discuss-phase.md": 31986,
"do.md": 10353,
@@ -59,7 +59,7 @@
"pr-branch.md": 15963,
"profile-user.md": 21246,
"progress.md": 30599,
"quick.md": 50452,
"quick.md": 50470,
"reapply-patches.md": 20312,
"remove-phase.md": 8513,
"remove-workspace.md": 7551,
@@ -89,5 +89,5 @@
"update.md": 20914,
"validate-phase.md": 10789,
"verify-phase.md": 40923,
"verify-work.md": 38267
"verify-work.md": 40247
}