Back to Get Shit Done

Query handler conventions (`sdk/src/query/`)

sdk/src/query/QUERY-HANDLERS.md

1.40.039.5 KB
Original Source

Query handler conventions (sdk/src/query/)

This document records contracts for the typed query layer consumed by gsd-sdk query and programmatic createRegistry() callers.

Registry coverage vs gsd-tools.cjs

  • In scope: Native handlers are registered in createRegistry() (index.ts) so SDK output can match get-shit-done/bin/gsd-tools.cjs JSON (see sdk/src/golden/).
  • Explicitly not registered (product decision): **graphify**, **from-gsd2** / **gsd2-import** — remain CLI-only.
  • CLI name differences (same behavior, different dispatch string):
    • CJS **summary-extract** → SDK **summary.extract** / **summary extract** / **history-digest** (see index.ts).
    • CJS top-level **scaffold <type> ...** → SDK **phase.scaffold** / **phase scaffold** with the scaffold type as the first argument (no separate scaffold alias on the registry).

Manifest-backed family ownership

These families are sourced from command-manifest.*.ts files and expanded into generated alias artifacts (command-aliases.generated.ts + CJS mirror):

  • state.*command-manifest.state.ts
  • verify.*command-manifest.verify.ts
  • init.*command-manifest.init.ts
  • phase.*command-manifest.phase.ts
  • phases.*command-manifest.phases.ts
  • validate.*command-manifest.validate.ts
  • roadmap.*command-manifest.roadmap.ts

CJS routing seams mirror these families with thin adapters (state/verify/init/phase/phases/validate/roadmap-command-router.cjs) so gsd-tools.cjs stays orchestration-only.

gsd-sdk query routing

  1. normalizeQueryCommand() (normalize-query-command.ts) — maps the first argv tokens to the same command + subcommand patterns as gsd-tools runCommand() where needed (e.g. state jsonstate.json, init execute-phase 9init.execute-phase with args ['9'], scaffold …phase.scaffold). Re-exported from @gsd-build/sdk and createRegistry’s module (sdk/src/query/index.ts) so programmatic callers can mirror CLI tokenization without importing a deep path.
  2. resolveQueryArgv() (registry.ts) — longest-prefix match on the normalized argv: tries joined keys a.b.c then a b c for each prefix length, longest first. Example: state update status X → handler state.update with args [status, X].
  3. Dotted single token: one token like init.new-project matches the registry; if the first pass finds no handler, a single dotted token is split and matching runs again.
  4. CJS fallback (CLI): if nothing matches a registered handler and GSD_QUERY_FALLBACK is not off/never/false/0, the CLI shells out to gsd-tools.cjs with argv derived from the normalized tokens (dotted commands are split into CJS-style segments). stderr receives a short bridge warning. Set GSD_QUERY_FALLBACK=off for strict mode (parity tests). CLI-only commands such as graphify rely on this path until native handlers exist.
  5. Output: JSON written to stdout for successful handler results.

Registered: phase.add-batch / phase add-batch — batch append (see phaseAddBatch in phase-lifecycle.ts).

Error handling

  • Validation and programmer errors: Handlers throw GSDError with an ErrorClassification (e.g. missing required args, invalid phase). The CLI maps these to exit codes via exitCodeFor().
  • Expected domain failures: Handlers return { data: { error: string, ... } } for cases that are not exceptional in normal use (file not found, intel disabled, todo missing, etc.). Callers must check data.error when present.
  • Do not mix both styles for the same failure mode in new code: prefer throw for "caller must fix input"; prefer **data.error** for "operation could not complete in this project state."

Mutation commands and events

  • QUERY_MUTATION_COMMANDS in index.ts lists every command name (including space-delimited aliases) that performs durable writes. It drives optional GSDEventStream wrapping so mutations emit structured events.
  • Init composition handlers (init.*) are not included: they return JSON for workflows; agents perform filesystem work.
  • **state.validate** is read-only — not listed in QUERY_MUTATION_COMMANDS.
  • **skill-manifest**: writes to disk only when invoked with **--write**. It is not in QUERY_MUTATION_COMMANDS, so conditional writes do not emit mutation events today. If event consumers need skill-manifest writes, add a follow-up that either registers a dedicated command name for the write path or documents the exception.

Intel: intel.update

  • **intel.update** / **intel update** matches CJS intel.cjs intelUpdate JSON (not an in-process graph refresh): when intel is enabled it returns { action: 'spawn_agent', message: '...' }; when disabled, { disabled: true, message: '...' }. The gsd-intel-updater agent performs the actual refresh after spawn. Golden tests use full toEqual vs gsd-tools.cjs on this repo’s intel config.

Session correlation (sessionId)

  • createRegistry(eventStream, sessionId) threads the optional sessionId string into mutation-related events emitted via eventStream. GSDTools accepts sessionId in its constructor and forwards it to createRegistry; GSD accepts sessionId in GSDOptions and passes it through createTools(). When omitted, sessionId is empty.

Lockfiles (state-mutation.ts)

  • STATE.md (and ROADMAP) locks use a sibling .lock file with the holder's PID. Stale locks are cleared when the PID no longer exists (process.kill(pid, 0) fails) or when the lock file is older than the existing time-based threshold.
  • searchJsonEntries in intel.ts caps recursion depth (MAX_JSON_SEARCH_DEPTH) to avoid stack overflow on pathological nested JSON.

Phase / plan listing (SDK-only)

No gsd-tools.cjs mirror — agents use these instead of shell ls/find/grep:

  • **phase.list-plans** <phase> [**--with-schema** <yamlKey>] — PLAN files in the phase dir; optional filter when a frontmatter key is present (phase-list-queries.ts).
  • **phase.list-artifacts** <phase> **--type** context|summary|verification|research — matching *-CONTEXT.md, *-SUMMARY.md, etc.
  • **plan.task-structure** <path-to-PLAN.md> — wave, depends_on, task/checkpoint counts via parsePlan().
  • **requirements.extract-from-plans** <phase> — deduped requirements: frontmatter across plans.

State extensions (Phase 3)

Handlers for **state.signal-waiting**, **state.signal-resume**, **state.validate**, **state.sync** (supports --verify dry-run), and **state.prune** live in state-mutation.ts, with dotted and state … space aliases in index.ts.

state.add-roadmap-evolution (bug #2662) — appends one entry to the ### Roadmap Evolution subsection under ## Accumulated Context in STATE.md, creating the subsection if missing. argv: --phase, --action (inserted|removed|moved|edited|added), optional --note, --after (for inserted), and --urgent flag. Returns { added: true, entry } or { added: false, reason: 'duplicate', entry }. Throws GSDError(Validation) when --phase / --action are missing or action is not in the allowed set. Canonical replacement for raw Edit/Write on STATE.md in insert-phase.md / add-phase.md workflows — required when projects ship a protect-files.sh PreToolUse hook that blocks direct STATE.md writes.

state.json vs state.load (different CJS commands):

  • state.json / state json — port of cmdStateJson (state.ts stateJson): rebuilt STATE.md frontmatter JSON. Read-only golden: read-only-parity.integration.test.ts compares to CJS state json with last_updated stripped.
  • state.load / state load — port of cmdStateLoad (state-project-load.ts stateProjectLoad): { config, state_raw, state_exists, roadmap_exists, config_exists }; config comes from get-shit-done/bin/lib/core.cjs loadConfig (resolved via the same candidate paths as a normal GSD install). Read-only golden: full toEqual vs state load. If core.cjs cannot be resolved, dispatch throws GSDError (document for minimal @gsd-build/sdk-only installs).

stateExtractField in helpers.ts uses horizontal whitespace only after Field: so YAML keys such as lowercase progress: in frontmatter are not mistaken for the body Progress: line (see get-shit-done/bin/lib/state.cjs — same rule).

Golden parity: coverage and exceptions

Subprocess reference: captureGsdToolsOutput() / captureGsdToolsStdout()get-shit-done/bin/gsd-tools.cjs (sdk/src/golden/capture.ts). Plain-text commands (e.g. config-path) use stdout string comparison in read-only-parity.integration.test.ts.

Authoritative accounting (every canonical handler): sdk/src/golden/golden-policy.ts merges golden-integration-covered.ts (canonicals hit by golden.integration.test.ts) with read-only-golden-rows.ts / special cases (verify.commits, config-path) into GOLDEN_PARITY_INTEGRATION_COVERED, and builds GOLDEN_PARITY_EXCEPTIONS for the rest. getCanonicalRegistryCommands() (registry-canonical-commands.ts) lists one dispatch string per unique handler; each canonical must be either covered or receive a built-in exception string (mutations → shared rationale; read-only without a subprocess row → per-command note). sdk/src/golden/golden-policy.test.ts calls verifyGoldenPolicyComplete() so the policy cannot drift silently.

Integration test files:

FileRole
sdk/src/golden/golden.integration.test.tsPrimary golden suite: subset/shape/full parity as documented in the tables below.
sdk/src/golden/read-only-parity.integration.test.tsRead-only handlers with full toEqual on sdkResult.data vs CJS JSON; rows listed in read-only-golden-rows.ts. Also config-path / verify.commits, dedicated blocks for state.json (strip last_updated) and state.load (full cmdStateLoad parity).

This section summarizes how each covered command is compared so readers do not have to infer rules from assertions alone.

Golden registry coverage matrix (human summary)

  • Covered by subprocess golden — canonical names appear in GOLDEN_PARITY_INTEGRATION_COVERED; see the tables below and the two integration files for assertion style (mostly full toEqual; remaining subset cases: frontmatter.get, find-phase).
  • Not in covered set — either listed in QUERY_MUTATION_COMMANDS (durable writes; handler tests in sdk/src/query/*.test.ts and mutation-focused tests) or a read-only handler whose full CJS JSON match is deferred (see auto-generated exception text in golden-policy.ts).

Full JSON equality (toEqual on result data)

These tests expect sdkResult.data to match the parsed CJS stdout JSON (possibly after shared normalization helpers):

SDK dispatch (representative)Notes
generate-slugIncludes fixture + multi-word cases.
config-getSample: top-level key model_profile.
config-setTemp .planning/ tree; reset between CJS capture and SDK dispatch; toEqual on { updated, key, value, previousValue? }.
state.validateFull object parity.
state.syncWith --verify (dry-run); full object parity.
detect-custom-filesTemp --config-dir fixture; full object parity.
roadmap.analyze / progressFull object parity (progress uses progress json CJS path).
frontmatter.validatePlan schema fixture under .planning/phases/11-state-mutations/.
verify.plan-structure / validate.consistency / verify.phase-completenessFull object parity on representative repo paths.
init.execute-phase / init.plan-phase / init.resume / init.verify-workFull toEqual vs CJS.
init.quickFull parity after stripping quick_id, timestamp, branch_name, task_dir (init-golden-normalize.ts).
intel.updateFull toEqual vs CJS for this project (disabled vs spawn-hint payload per intel.cjs).

From read-only-parity.integration.test.ts (full toEqual on this repo):

SDK dispatch (canonical)Notes
resolve-modelArgs e.g. gsd-planner.
phase-plan-indexPhase number arg.
roadmap.get-phasePhase number arg.
list.todosNo args.
phase.next-decimalPhase number arg.
phases.listNo args.
verify.summaryPlan path.
verify.path-existsPath under repo.
verify.artifactsPlan path.
verify.commitsTwo git SHAs (HEAD~1 / HEAD or fallback).
websearchLimited query (may hit network — test uses small limit).
workstream.get / workstream.list / workstream.statusDefault workstream where applicable (status uses full CJS shape when the workstream dir exists).
learnings.listNo args.
intel.statusNo args.
intel.diff / intel.validate / intel.queryWhen intel is disabled, disabled payload matches CJS (including message text).
init.list-workspacesNo args.
agent-skillsNo agent type → JSON "" (same as CJS).
scan-sessions--json; SDK scanSessions output matches CJS project array (profile-scan-sessions.ts).
summary.extractFixture sdk/src/golden/fixtures/summary-extract-sample.md; uses extractFrontmatterLeading (first --- block) for parity with frontmatter.cjs.
history.digestNo args; aggregate over .planning/phases + archived milestone phase dirs (commands.cjs cmdHistoryDigest).
audit-uatNo args; full JSON parity with uat.cjs cmdAuditUat (results, summary with by_category / by_phase).
skill-manifestNo args; full manifest parity with init.cjs buildSkillManifest / cmdSkillManifest. Handler uses extractFrontmatterLeading (first --- block) like CJS frontmatter.cjs extractFrontmatter — not TS extractFrontmatter (last block), so skills with multiple --- sections match CJS.
validate.agentsNo args; agents_dir matches core.cjs getAgentsDir (GSD_AGENTS_DIR or sdk/dist/query/../../../agents in this monorepo — same absolute path as CLI). MODEL_PROFILES / expected list stays aligned with get-shit-done/bin/lib/model-profiles.cjs.
state.getDedicated tests: no args → full { content } vs state get; one field (milestone) → { milestone: "…" } vs state get milestone (frontmatter line match).
state.jsonstate json vs SDK; last_updated stripped before toEqual (volatile).
state.loadstate load vs SDK; full cmdStateLoad object graph (config, state_raw, existence flags).
uat.render-checkpointFixture sdk/src/golden/fixtures/uat-render-checkpoint-sample.md; full JSON parity with uat.cjs cmdRenderCheckpoint (file_path, test_number, test_name, checkpoint — same box + buildCheckpoint text as CJS; sanitizeForDisplay on name/expected).
config-pathPlain stdout path vs { path } — compared with path.normalize in tests.

Normalized or field-omitted comparison

SDK / testRule
audit-openaudit-open --json: **scanned_at** stripped before toEqual (volatile ISO time). sanitizeForDisplay in audit-open.ts matches security.cjs (CRLF body lines can leave \r in items.todos[].summary, matching CLI).
extract.messages / extract-messagesFixture sdk/src/golden/fixtures/extract-messages-sessions/ passed as --path (sessions root). **output_file** stripped before toEqual (temp path under os.tmpdir()); then the two JSONL files are compared byte-for-byte. Parity with profile-pipeline.cjs cmdExtractMessages (streamExtractMessages, isGenuineUserMessage, batch limit 300).
docs-initexisting_docs sorted by path before compare; **agents_installed** and **missing_agents** omitted (subprocess vs in-process path resolution for ~/.claude/...).

Structural, subset, or shape-only parity

Assertions deliberately compare only selected fields (not full toEqual):

SDK dispatch (representative)What is compared
frontmatter.getScalar fields phase, plan, type; same top-level key set as CJS.
find-phasefound, directory, phase_number, phase_name, plans (SDK payload is a subset of CJS — extra CJS fields ignored).

template.select is not in golden.integration.test.ts: CJS template select <plan-path> scores PLAN content for summary templates; SDK template.select <phase> uses phase-directory heuristics — different algorithms. Covered in sdk/src/query/template.test.ts.

Time- and environment-dependent

CommandRule
current-timestamp**full**: same shape and valid ISO strings; not the same instant. **date**: same calendar day when the test does not cross midnight. **filename**: full toEqual (back-to-back capture vs SDK).

Conditional writes (not in QUERY_MUTATION_COMMANDS)

CommandRule
skill-manifestDisk writes only with **--write**; registry does not emit mutation events for this command (see Mutation commands and events above).

Registered but not in the golden suite

Handlers in createRegistry() that are not covered by golden.integration.test.ts are not automatically “non-parity” — they simply have no automated cross-check against CJS yet. Add golden tests when tightening coverage; until then, treat absence here as a test gap, not a behavior guarantee.


Decision routing (SDK-only)

These handlers implement .planning/research/decision-routing-audit.mdno gsd-tools.cjs mirror yet (orchestration JSON only). Invoke via gsd-sdk query / registry.dispatch() after normalizeQueryCommand() where argv uses check … / detect … / route … prefixes.

Tier 1

DispatchPurpose
check.config-gates / check config-gates [workflow]Single JSON blob of merged workflow.* (+ context_window) for batch config gates.
check.phase-ready / check phase-ready <phase>Phase directory stats, dependencies_met, next_step (discuss / plan / execute / verify / complete).
route.next-action / route next-actionSuggested next slash command from next.md-style rules (/gsd-discuss-phase, /gsd-execute-phase, /gsd-resume-work, gates, etc.).

Tier 2

DispatchPurpose
check.auto-mode / check auto-modeactive (OR of workflow.auto_advance and workflow._auto_chain_active), source (none / auto_advance / auto_chain / both), plus the two booleans. Replaces paired config-get calls in checkpoint and auto-advance steps. Use --pick active or --pick auto_chain_active when a workflow only needs one field.
detect.phase-type / detect phase-type <phase>Structured UI/schema/API/infra detection for a phase. Returns has_frontend, frontend_indicators, has_schema, schema_orm, schema_files, has_api, has_infra, push_command (null, reserved). Replaces fragile grep-based UI detection in autonomous.md, plan-phase.md, etc. (audit §3.6).
check.completion / check completion <phase|milestone> <id>Phase or milestone completion rollup. Phase mode: plans_total, plans_with_summaries, missing_summaries, verification_status, uat_status, debt (uat_gaps, verification_failures, human_needed), complete. Milestone mode: phase_count, phases_complete, phases_incomplete, complete. Replaces PLAN/SUMMARY counting in transition.md, complete-milestone.md (audit §3.7).

Tier 3

DispatchPurpose
check.gates / check gates <workflow> [--phase <N>]Safety gate consolidation. Checks .continue-here.md presence (blocker), STATE.md error/failed status (blocker), and VERIFICATION.md FAIL rows (warning). Returns passed, blockers, warnings. Replaces per-workflow gate logic in next.md, execute-phase.md, discuss-phase.md (audit §3.2). SDK-only — no CJS mirror.
check.verification-status / check verification-status <phase>VERIFICATION.md parser. Returns status (pass/fail/partial/missing), score (e.g. "3/4"), gaps, human_items, deferred. Handles prefixed filenames and missing files. Replaces VERIFICATION.md grep/parse in execute-phase.md, autonomous.md, progress.md (audit §3.8). SDK-only — no CJS mirror.
check.ship-ready / check ship-ready <phase>Ship preflight: clean_tree, on_feature_branch, current_branch, base_branch, remote_configured, gh_available, gh_authenticated (always false — advisory, no network call), verification_passed, blockers, ready. Replaces ship.md preflight checks (audit §3.9). SDK-only — no CJS mirror.

Stability: Shapes are versioned with the audit doc; add integration tests when workflows adopt these queries. Re-run after file writes that change .planning/ (stale read caveat in audit §6). All Tier 1–3 handlers are implemented and unit-tested.


CJS command surface vs SDK registry

Authoritative CJS entry points: runCommand switch (command) in get-shit-done/bin/gsd-tools.cjs. SDK entry points: createRegistry() in sdk/src/query/index.ts.

Naming aliases (registered, different string):

  • CJS **summary-extract** → SDK **summary.extract**, **summary extract**, **history-digest** (history digest helpers).
  • CJS top-level **scaffold <type> …** → SDK **phase.scaffold** / **phase scaffold** (type + options in args).

CLI-only (no SDK registry handler; intentional unless requirements change):

CJS surfaceJustification
**graphify**Depends on Graphify CLI / Python stack; not ported to the typed query layer.
**from-gsd2**Legacy GSD2 → GSD migration (gsd2-import.cjs); CLI-only helper.

SDK-only (registered dispatch without an equivalent gsd-tools top-level subcommand):

SDK dispatchNotes
**phases.archive** / **phases archive**CJS phases supports only **list** and **clear**; archive behavior is available via SDK (and workflows), not as gsd-tools phases archive.

Matrix: top-level gsd-tools command → SDK

Disposition: Registered = handled in createRegistry() under the listed SDK name(s); CLI-only = no registry handler; Alias = same behavior, different primary dispatch string.

CJS command (first argv)SDK dispatch name(s)DispositionNotes
state (subcommands)state.load, state.json, state.get, state.update, state.patch, …RegisteredDotted and state … space aliases in index.ts.
resolve-modelresolve-modelRegistered
find-phasefind-phaseRegisteredGolden: subset parity (see above).
commit, check-commit, commit-to-subrepocommit, check-commit, commit-to-subrepoRegistered
verify-summaryverify-summary, verify.summary, verify summaryRegistered
templatetemplate.fill, template.select, …Registered
frontmatterfrontmatter.get, frontmatter.set, …Registered
verifyverify.plan-structure, verify.phase-completeness, …Registered
generate-sluggenerate-slugRegistered
current-timestampcurrent-timestampRegisteredGolden: time semantics (see above).
list-todoslist-todos, list.todosRegistered
verify-path-existsverify-path-exists, verify.path-exists, …Registered
config-ensure-section, config-set, config-set-model-profile, config-get, config-new-project, config-pathsame kebab-case namesRegistered
agent-skillsagent-skillsRegistered
skill-manifestskill-manifest, skill manifestRegisteredWrites only with --write.
history-digesthistory-digest, history.digest, …AliasSame as **summary.extract** family for digest-style output.
phasesphases.list, phases.clear, phases.archive, …Registered (+ SDK-only)CJS: **list**, **clear** only; **archive** is SDK-only (see above).
roadmaproadmap.analyze, roadmap.get-phase, roadmap.update-plan-progress, …Registered
requirementsrequirements.mark-complete, …Registered
phasephase.add, phase.add-batch, phase.insert, …Registered
milestonemilestone.complete, …Registered
validatevalidate.consistency, validate.health, validate.agents, …Registered
progressprogress, progress.json, progress.bar, …Registered
audit-uataudit-uatRegistered
audit-openaudit-open, audit openRegistered
uatuat.render-checkpoint, …Registered
statsstats, stats.json, …Registered
todotodo.complete, todo.match-phase, …Registered
scaffoldphase.scaffold, phase scaffoldAliasTop-level **scaffold** in CJS; no separate scaffold registry key.
initinit.execute-phase, init.new-project, …RegisteredDotted and init … space aliases.
phase-plan-indexphase-plan-indexRegistered
state-snapshotstate-snapshotRegistered
summary-extractsummary.extract, summary extract, history-digest, …Alias
websearchwebsearchRegistered
scan-sessionsscan-sessionsRegistered
extract-messagesextract-messages, extract.messagesRegisteredGolden: output_file strip + JSONL bytes (see Normalized table).
profile-sample, profile-questionnaire, write-profile, generate-dev-preferences, generate-claude-profile, generate-claude-mdsame kebab-case namesRegistered
workstreamworkstream.get, workstream.list, …Registered
intelintel.status, intel.diff, intel.update, …Registered**intel.update**: JSON parity with CJS spawn hint / disabled payload (see Intel: intel.update).
graphifyCLI-onlySee CLI-only table.
docs-initdocs-initRegisteredGolden: normalized compare (see above).
learningslearnings.list, learnings.query, …Registered
detect-custom-filesdetect-custom-filesRegisteredRequires --config-dir.
from-gsd2CLI-onlySee CLI-only table.

Other registered areas

  • **detect-custom-files**: requires --config-dir <path>; scans installer manifest vs GSD-managed dirs (detect-custom-files.ts).
  • **docs-init**: docs-update workflow payload (docs-init.ts), aligned with docs.cjs. Golden tests omit **agents_installed** / **missing_agents** when comparing SDK vs CLI because the subprocess may resolve ~/.claude/... differently than in-process checks.