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:
5
.changeset/graceful-mice-wave.md
Normal file
5
.changeset/graceful-mice-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 353
|
||||
---
|
||||
Restore gsd-tools query handlers for check, task, and agent commands after SDK retirement.
|
||||
@@ -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",
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
65
get-shit-done/bin/lib/agent-command-router.cjs
Normal file
65
get-shit-done/bin/lib/agent-command-router.cjs
Normal 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,
|
||||
};
|
||||
333
get-shit-done/bin/lib/check-command-router.cjs
Normal file
333
get-shit-done/bin/lib/check-command-router.cjs
Normal 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,
|
||||
};
|
||||
81
get-shit-done/bin/lib/task-command-router.cjs
Normal file
81
get-shit-done/bin/lib/task-command-router.cjs
Normal 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,
|
||||
};
|
||||
101
tests/sdk-removal-query-family-dispatch.test.cjs
Normal file
101
tests/sdk-removal-query-family-dispatch.test.cjs
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user