fix: restore sdk query families in gsd-tools (#353)

* fix: restore sdk query families in gsd-tools

* chore: add changeset

* docs: sync query router inventory
This commit is contained in:
Colin Johnson
2026-05-26 18:56:00 -04:00
committed by GitHub
parent 7a3822fce1
commit 47bba87d90
8 changed files with 612 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 353
---
Restore gsd-tools query handlers for check, task, and agent commands after SDK retirement.

View File

@@ -262,8 +262,10 @@
"cli_modules": [
"active-workstream-store.cjs",
"adr-parser.cjs",
"agent-command-router.cjs",
"artifacts.cjs",
"audit.cjs",
"check-command-router.cjs",
"cjs-command-router-adapter.cjs",
"clusters.cjs",
"code-review-flags.cjs",
@@ -321,6 +323,7 @@
"state-document.cjs",
"state.cjs",
"surface.cjs",
"task-command-router.cjs",
"template.cjs",
"uat.cjs",
"validate-command-router.cjs",

View File

@@ -362,7 +362,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (72 shipped)
## CLI Modules (75 shipped)
Full listing: `get-shit-done/bin/lib/*.cjs`.
@@ -370,8 +370,10 @@ Full listing: `get-shit-done/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` |
| `artifacts.cjs` | Canonical artifact registry — known `.planning/` root file names; used by `gsd-health` W019 lint |
| `audit.cjs` | Audit dispatch, audit open sessions, audit storage helpers |
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |
| `code-review-flags.cjs` | Typed flag parser for `/gsd:code-review`; exports `parseCodeReviewFlags(argv)` (→ `{ fix, all, auto, depth, files }`) and `resolveCodeReviewWorkflow(flags)` (→ `'code-review.md' \| 'code-review-fix.md'`); canonical dispatch seam for `--fix`/`--all`/`--auto` routing |
@@ -429,6 +431,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
| `state-document.cjs` | Pure STATE.md field extraction, replacement, status normalization, and progress calculation transforms |
| `surface.cjs` | Runtime surface module — manages the runtime enable/disable surface state independently of the install-time profile marker (ADR-0011 Phase 2) |
| `task-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools task` |
| `template.cjs` | Template selection and filling with variable substitution |
| `uat.cjs` | UAT file parsing, verification debt tracking, audit-uat support |
| `validate-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools validate` |

View File

@@ -194,6 +194,9 @@ const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
const { routeRoadmapCommand } = require('./lib/roadmap-command-router.cjs');
const { routeAgentCommand } = require('./lib/agent-command-router.cjs');
const { routeCheckCommand } = require('./lib/check-command-router.cjs');
const { routeTaskCommand } = require('./lib/task-command-router.cjs');
const { parseNamedArgs, parseMultiwordArg } = require('./lib/command-arg-projection.cjs');
// ─── Bridge collapsed (Phase 4) ────────────────────────────────────────────────
@@ -362,14 +365,14 @@ async function main() {
// discovery; previously it was a partial subset that didn't include
// phase / roadmap / milestone / progress / etc.
const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>] [--json-errors]\n' +
'Commands: agent-skills, audit-open, audit-uat, check-commit, commit, commit-to-subrepo, ' +
'Commands: agent, agent-skills, audit-open, audit-uat, check, check-commit, commit, commit-to-subrepo, ' +
'config-ensure-section, config-get, config-new-project, config-path, config-set, migrate-config, ' +
'current-timestamp, detect-custom-files, docs-init, extract-messages, find-phase, ' +
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
'learnings, list-todos, milestone, phase, phase-plan-index, phases, profile-questionnaire, ' +
'profile-sample, progress, prompt-budget, requirements, resolve-model, roadmap, scaffold, state, ' +
'template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
'task, template, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
'Global flags:\n' +
' --raw Emit raw output without post-processing\n' +
' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
@@ -509,6 +512,16 @@ function extractField(obj, fieldPath) {
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand, workstreamContext = null) {
switch (command) {
case 'agent': {
routeAgentCommand({ args, raw });
break;
}
case 'check': {
routeCheckCommand({ args, cwd, raw });
break;
}
case 'state': {
routeStateCommand({
state,
@@ -603,6 +616,11 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
break;
}
case 'task': {
routeTaskCommand({ args, cwd, raw });
break;
}
case 'frontmatter': {
// Phase 6 (#3575): dispatch via SDK executeForCjs when available.
// SDK handler: sdk/src/query/frontmatter.ts + frontmatter-mutation.ts.

View File

@@ -0,0 +1,65 @@
'use strict';
const { output, error, ERROR_REASON } = require('./core.cjs');
const QUOTA_SENTINELS = [
'429',
'usage_limit_reached',
'usage limit',
'rate limit',
'rate-limited',
'rate_limit',
'resource_exhausted',
'quota',
'too many requests',
'exceeded your',
];
const CLASSIFY_HANDOFF_SENTINEL = 'classifyhandoffifneeded is not defined';
function parseRetryAfter(body) {
const match = String(body || '').match(/\bretry[-_ ]after[:\s]+(\d+)\b/i);
if (!match) return undefined;
const seconds = Number.parseInt(match[1], 10);
return Number.isFinite(seconds) ? seconds : undefined;
}
function classifyAgentFailure(body) {
const normalized = String(body || '').toLowerCase();
if (normalized.trim() === '') {
return { class: 'unknown-failure' };
}
for (const sentinel of QUOTA_SENTINELS) {
if (normalized.includes(sentinel)) {
const retryAfterSeconds = parseRetryAfter(body);
return retryAfterSeconds === undefined
? { class: 'quota-exceeded', sentinel }
: { class: 'quota-exceeded', sentinel, retryAfterSeconds };
}
}
if (normalized.includes(CLASSIFY_HANDOFF_SENTINEL)) {
return {
class: 'classify-handoff-bug',
sentinel: CLASSIFY_HANDOFF_SENTINEL,
};
}
return { class: 'unknown-failure' };
}
function routeAgentCommand({ args, raw }) {
const subcommand = args[1];
if (subcommand !== 'classify-failure') {
error('Unknown agent subcommand. Available: classify-failure', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
const bodyArgs = args.slice(2).filter((arg) => arg !== '--');
output(classifyAgentFailure(bodyArgs.join(' ')), raw);
}
module.exports = {
classifyAgentFailure,
routeAgentCommand,
};

View File

@@ -0,0 +1,333 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { output, error, ERROR_REASON } = require('./core.cjs');
const { parseDecisions } = require('./decisions.cjs');
function normalizePhrase(text) {
return String(text || '')
.toLowerCase()
.replace(/[^a-z0-9\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
const SOFT_PHRASE_MIN_WORDS = 6;
function softPhrase(text) {
const words = normalizePhrase(text).split(' ').filter(Boolean);
if (words.length < SOFT_PHRASE_MIN_WORDS) return '';
return words.slice(0, SOFT_PHRASE_MIN_WORDS).join(' ');
}
function decisionMentioned(haystack, decision) {
if (!haystack) return false;
if (new RegExp(`\\b${decision.id}\\b`).test(haystack)) return true;
const phrase = softPhrase(decision.text);
return phrase ? normalizePhrase(haystack).includes(phrase) : false;
}
function readIfExists(filePath) {
try {
return fs.readFileSync(filePath, 'utf-8');
} catch {
return '';
}
}
function resolvePath(inputPath, projectDir) {
return path.isAbsolute(inputPath) ? inputPath : path.join(projectDir, inputPath);
}
function readWorkflowConfig(projectDir) {
const configPath = path.join(projectDir, '.planning', 'config.json');
try {
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
return {
...(parsed.workflow || {}),
auto_advance: parsed.workflow?.auto_advance ?? parsed.auto_advance,
_auto_chain_active: parsed.workflow?._auto_chain_active ?? parsed._auto_chain_active,
context_coverage_gate: parsed.workflow?.context_coverage_gate ?? parsed.context_coverage_gate,
};
} catch {
return {};
}
}
function cmdAutoMode(projectDir, raw) {
const workflow = readWorkflowConfig(projectDir);
const autoAdvance = Boolean(workflow.auto_advance ?? false);
const autoChainActive = Boolean(workflow._auto_chain_active ?? false);
let source = 'none';
if (autoChainActive && autoAdvance) source = 'both';
else if (autoChainActive) source = 'auto_chain';
else if (autoAdvance) source = 'auto_advance';
output({
active: autoChainActive || autoAdvance,
source,
auto_chain_active: autoChainActive,
auto_advance: autoAdvance,
}, raw);
}
function gateEnabled(projectDir) {
const value = readWorkflowConfig(projectDir).context_coverage_gate;
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const lower = value.toLowerCase();
if (lower === 'false' || lower === 'true') return lower !== 'false';
}
return true;
}
function loadPlanContents(phaseDir) {
if (!fs.existsSync(phaseDir)) return [];
try {
return fs.readdirSync(phaseDir)
.filter((entry) => /-PLAN\.md$/.test(entry))
.map((entry) => readIfExists(path.join(phaseDir, entry)));
} catch {
return [];
}
}
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
const XML_DECISION_TAGS_RE = /<(?:objective|tasks?|action)(?:\s[^>]*)?>([\s\S]*?)<\/(?:objective|tasks?|action)>/gi;
function stripCommentsAndFences(text) {
return text
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/```[\s\S]*?```/g, ' ')
.replace(/~~~[\s\S]*?~~~/g, ' ');
}
function extractYamlBlock(frontmatter, key) {
const match = frontmatter.match(new RegExp(`^${key}\\s*:(.*)$`, 'm'));
if (!match) return '';
const startIdx = (match.index || 0) + match[0].length;
const rest = frontmatter.slice(startIdx + 1).split(/\r?\n/);
const block = [match[1] || ''];
for (const line of rest) {
if (line === '' || /^\s/.test(line)) block.push(line);
else break;
}
return block.join('\n');
}
function extractXmlTagBodies(text) {
const parts = [];
for (const match of text.matchAll(XML_DECISION_TAGS_RE)) {
if (match[1]) parts.push(match[1]);
}
return parts.join('\n');
}
function extractPlanDesignatedSections(planContent) {
if (!planContent) return '';
const cleaned = stripCommentsAndFences(planContent);
const fmMatch = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
const frontmatter = fmMatch ? fmMatch[1] : '';
const body = fmMatch ? fmMatch[2] : cleaned;
const parts = [];
for (const key of ['must_haves', 'truths', 'objective']) {
const block = extractYamlBlock(frontmatter, key);
if (block) parts.push(block);
}
const bodyParts = [];
let inDesignated = false;
for (const line of body.split(/\r?\n/)) {
const heading = /^#{1,6}\s+/.test(line);
if (heading) {
inDesignated = DESIGNATED_HEADINGS_RE.test(line);
if (inDesignated) bodyParts.push(line);
continue;
}
if (inDesignated) bodyParts.push(line);
}
parts.push(bodyParts.join('\n'));
parts.push(extractXmlTagBodies(cleaned));
return parts.join('\n\n');
}
function buildPlanMessage(uncovered) {
if (uncovered.length === 0) return 'All trackable CONTEXT.md decisions are covered by plans.';
return [
'## Decision Coverage Gap',
'',
`${uncovered.length} CONTEXT.md decision(s) are not covered by any plan:`,
'',
...uncovered.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
'',
'Resolve by citing `D-NN:` in a relevant plan\'s `must_haves`/`truths` (or body),',
'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
].join('\n');
}
function buildVerifyMessage(notHonored) {
if (notHonored.length === 0) return 'All trackable CONTEXT.md decisions are honored by shipped artifacts.';
return [
'### Decision Coverage (warning)',
'',
`${notHonored.length} decision(s) not found in shipped artifacts:`,
'',
...notHonored.map((item) => `- **${item.id}** (${item.category || 'uncategorized'}): ${item.text}`),
'',
'This is a soft warning - verification status is unchanged.',
].join('\n');
}
function loadTrackableDecisions(contextPath) {
return parseDecisions(readIfExists(contextPath)).filter((decision) => decision.trackable);
}
function cmdDecisionCoveragePlan(projectDir, args, raw) {
const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
const contextPath = args[3] ? resolvePath(args[3], projectDir) : '';
if (!gateEnabled(projectDir)) {
output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw);
return;
}
if (!contextPath || !fs.existsSync(contextPath)) {
output({ passed: true, skipped: true, reason: 'CONTEXT.md missing', total: 0, covered: 0, uncovered: [], message: 'No CONTEXT.md - nothing to check.' }, raw);
return;
}
const decisions = loadTrackableDecisions(contextPath);
if (decisions.length === 0) {
output({ passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' }, raw);
return;
}
const sections = loadPlanContents(phaseDir).map(extractPlanDesignatedSections);
const uncovered = [];
let covered = 0;
for (const decision of decisions) {
if (sections.some((section) => decisionMentioned(section, decision))) covered++;
else uncovered.push({ id: decision.id, text: decision.text, category: decision.category });
}
output({
passed: uncovered.length === 0,
skipped: false,
total: decisions.length,
covered,
uncovered,
message: buildPlanMessage(uncovered),
}, raw);
}
function recentCommitMessages(projectDir) {
try {
return execFileSync('git', ['log', '-n', '200', '--pretty=%s%n%b'], {
cwd: projectDir,
encoding: 'utf-8',
maxBuffer: 4 * 1024 * 1024,
});
} catch {
return '';
}
}
function isInsideRoot(candidatePath, rootDir) {
const root = path.resolve(rootDir);
const target = path.resolve(root, candidatePath);
return target === root || target.startsWith(`${root}${path.sep}`);
}
function readModifiedFilesContent(projectDir, summaries) {
const out = [];
let total = 0;
for (const summary of summaries) {
if (!summary) continue;
for (const blockMatch of summary.matchAll(/files_modified:\s*\n((?:[ \t]*-\s+.+\n?)+)/g)) {
const files = [...(blockMatch[1] || '').matchAll(/-\s+(.+)/g)]
.map((match) => match[1].trim().replace(/^["']|["']$/g, ''));
for (const file of files) {
if (total >= 50) break;
if (!file || !isInsideRoot(file, projectDir)) continue;
const raw = readIfExists(resolvePath(file, projectDir));
out.push(raw.length > 256 * 1024 ? raw.slice(0, 256 * 1024) : raw);
total++;
}
if (total >= 50) break;
}
if (total >= 50) break;
}
return out.join('\n\n');
}
function cmdDecisionCoverageVerify(projectDir, args, raw) {
const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
const contextPath = args[3] ? resolvePath(args[3], projectDir) : '';
if (!gateEnabled(projectDir)) {
output({ skipped: true, blocking: false, reason: 'workflow.context_coverage_gate is false', total: 0, honored: 0, not_honored: [], message: 'Decision coverage gate disabled by config.' }, raw);
return;
}
if (!contextPath || !fs.existsSync(contextPath)) {
output({ skipped: true, blocking: false, reason: 'CONTEXT.md missing', total: 0, honored: 0, not_honored: [], message: 'No CONTEXT.md - nothing to check.' }, raw);
return;
}
const decisions = loadTrackableDecisions(contextPath);
if (decisions.length === 0) {
output({ skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' }, raw);
return;
}
const planContents = loadPlanContents(phaseDir);
const summaryParts = fs.existsSync(phaseDir)
? fs.readdirSync(phaseDir).filter((entry) => /-SUMMARY\.md$/.test(entry)).map((entry) => readIfExists(path.join(phaseDir, entry)))
: [];
const haystack = [
planContents.join('\n\n'),
summaryParts.join('\n\n'),
readModifiedFilesContent(projectDir, summaryParts),
recentCommitMessages(projectDir),
].join('\n\n');
const notHonored = [];
let honored = 0;
for (const decision of decisions) {
if (decisionMentioned(haystack, decision)) honored++;
else notHonored.push({ id: decision.id, text: decision.text, category: decision.category });
}
output({
skipped: false,
blocking: false,
total: decisions.length,
honored,
not_honored: notHonored,
message: buildVerifyMessage(notHonored),
}, raw);
}
function routeCheckCommand({ args, cwd, raw }) {
const subcommand = args[1];
if (subcommand === 'auto-mode') {
cmdAutoMode(cwd, raw);
return;
}
if (subcommand === 'decision-coverage-plan') {
cmdDecisionCoveragePlan(cwd, args, raw);
return;
}
if (subcommand === 'decision-coverage-verify') {
cmdDecisionCoverageVerify(cwd, args, raw);
return;
}
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
module.exports = {
routeCheckCommand,
decisionMentioned,
extractPlanDesignatedSections,
};

View File

@@ -0,0 +1,81 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { output, error, ERROR_REASON } = require('./core.cjs');
function isBehaviorAddingTaskContent(content) {
const tddTrue = /\btdd\s*=\s*["']true["']/i.test(content);
const behaviorMatch = content.match(/<behavior>([\s\S]*?)<\/behavior>/i);
const hasBehaviorBlock = Boolean(behaviorMatch && behaviorMatch[1].trim().length > 0);
const filesMatch = content.match(/<files>([\s\S]*?)<\/files>/i);
let hasSourceFiles = false;
if (filesMatch) {
const fileLines = filesMatch[1]
.split(/[\n,]/)
.map((line) => line.trim().replace(/^[-*]\s*/, ''))
.filter(Boolean);
hasSourceFiles = fileLines.some((file) =>
!/\.md$/i.test(file) &&
!/\.json$/i.test(file) &&
!/\.test\.[^.]+$/i.test(file) &&
!/\.spec\.[^.]+$/i.test(file) &&
!/(^|[\\/])tests?[\\/]/i.test(file) &&
!/\.(yml|yaml|toml|ini|cfg|conf|properties)$/i.test(file) &&
!/(^|[\\/])\.env(\..+)?$/i.test(file)
);
}
const isBehaviorAdding = tddTrue && hasBehaviorBlock && hasSourceFiles;
const missing = [];
if (!tddTrue) missing.push('tdd="true" frontmatter absent');
if (!hasBehaviorBlock) missing.push('<behavior> block missing or empty');
if (!hasSourceFiles) missing.push('<files> has no non-test source file');
return {
is_behavior_adding: isBehaviorAdding,
checks: {
tdd_true: tddTrue,
has_behavior_block: hasBehaviorBlock,
has_source_files: hasSourceFiles,
},
reason: isBehaviorAdding ? null : `Not behavior-adding: ${missing.join('; ')}`,
};
}
function routeTaskCommand({ args, cwd, raw }) {
const subcommand = args[1];
if (subcommand !== 'is-behavior-adding') {
error('Unknown task subcommand. Available: is-behavior-adding', ERROR_REASON.SDK_UNKNOWN_COMMAND);
}
let content = null;
if (args[2] === '--task-content') {
content = args[3] || null;
} else if (args[2]) {
const projectRoot = path.resolve(cwd || process.cwd());
const requestedPath = args[2];
const resolvedTaskPath = path.resolve(projectRoot, requestedPath);
const rel = path.relative(projectRoot, resolvedTaskPath);
if (rel === '..' || rel.startsWith(`..${path.sep}`)) {
error(`Task file is outside project scope: ${requestedPath}`, ERROR_REASON.USAGE);
}
if (!fs.existsSync(resolvedTaskPath)) {
error(`Task file not found: ${requestedPath}`, ERROR_REASON.USAGE);
}
content = fs.readFileSync(resolvedTaskPath, 'utf-8');
}
if (!content) {
error('Usage: task.is-behavior-adding <plan-file-path> | --task-content "<xml>"', ERROR_REASON.USAGE);
}
output(isBehaviorAddingTaskContent(content), raw);
}
module.exports = {
isBehaviorAddingTaskContent,
routeTaskCommand,
};

View File

@@ -0,0 +1,101 @@
'use strict';
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
describe('SDK-removal CJS query family dispatch', () => {
let tmpDir;
let phaseDir;
let contextPath;
beforeEach(() => {
tmpDir = createTempProject('gsd-query-families-');
phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
contextPath = path.join(phaseDir, '01-CONTEXT.md');
});
afterEach(() => {
cleanup(tmpDir);
});
test('query agent.classify-failure classifies quota text', () => {
const result = runGsdTools(['query', 'agent.classify-failure', '--', '429 retry-after: 45'], tmpDir);
assert.strictEqual(result.success, true, result.error);
const parsed = JSON.parse(result.output);
assert.equal(parsed.class, 'quota-exceeded');
assert.equal(parsed.sentinel, '429');
assert.equal(parsed.retryAfterSeconds, 45);
});
test('query task.is-behavior-adding supports --pick for workflow gate usage', () => {
const task = [
'<task tdd="true">',
'<behavior>User can save a profile</behavior>',
'<files>',
'- src/profile.js',
'- tests/profile.test.js',
'</files>',
'</task>',
].join('\n');
const result = runGsdTools(['query', 'task.is-behavior-adding', '--task-content', task, '--pick', 'is_behavior_adding'], tmpDir);
assert.strictEqual(result.success, true, result.error);
assert.equal(result.output, 'true');
});
test('query check auto-mode reads workflow flags from config', () => {
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
workflow: {
auto_advance: false,
_auto_chain_active: true,
},
}, null, 2));
const result = runGsdTools(['query', 'check', 'auto-mode', '--pick', 'source'], tmpDir);
assert.strictEqual(result.success, true, result.error);
assert.equal(result.output, 'auto_chain');
});
test('query check.decision-coverage-plan reaches the CJS gate and passes covered decisions', () => {
fs.writeFileSync(contextPath, [
'<decisions>',
'### Product',
'- **D-01:** Keep the runtime command path installed and portable for users.',
'</decisions>',
].join('\n'));
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), [
'---',
'objective: "Implement D-01 in the runtime launcher"',
'must_haves:',
' - "D-01 remains covered"',
'---',
'',
'<tasks>',
'<task><action>Preserve D-01.</action></task>',
'</tasks>',
].join('\n'));
const result = runGsdTools(['query', 'check.decision-coverage-plan', phaseDir, contextPath, '--pick', 'passed'], tmpDir);
assert.strictEqual(result.success, true, result.error);
assert.equal(result.output, 'true');
});
test('query check.decision-coverage-verify returns non-blocking misses', () => {
fs.writeFileSync(contextPath, [
'<decisions>',
'### Product',
'- **D-02:** Keep verification warnings visible when decisions are missing.',
'</decisions>',
].join('\n'));
const result = runGsdTools(['query', 'check.decision-coverage-verify', phaseDir, contextPath], tmpDir);
assert.strictEqual(result.success, true, result.error);
const parsed = JSON.parse(result.output);
assert.equal(parsed.blocking, false);
assert.equal(parsed.not_honored[0].id, 'D-02');
});
});