From eddb2a205be7fc2120b194a0bd6b852cb5dc7e58 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 28 Apr 2026 20:22:40 -0400 Subject: [PATCH] fix(#2801): add ingest-docs handler to gsd-tools init dispatch (#2820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2801): add ingest-docs handler to gsd-tools init dispatch The `/gsd-ingest-docs` workflow was broken because `workflows/ingest-docs.md` called `gsd-sdk query init.ingest-docs` but the installed binary is `gsd-tools`, and `gsd-tools init` had no `ingest-docs` case in its dispatch switch. - Added `cmdInitIngestDocs` function to `init.cjs` and exported it; returns `project_exists`, `planning_exists`, `has_git`, `project_path`, `commit_docs` - Added `case 'ingest-docs'` to the `init` switch in `gsd-tools.cjs` - Updated `workflows/ingest-docs.md` to call `gsd-tools init ingest-docs` (line 55) and `gsd-tools commit` (line 292) instead of `gsd-sdk query ...` - Regression test: `tests/bug-2801-ingest-docs-handler.test.cjs` Closes #2801 Co-Authored-By: Claude Sonnet 4.6 * fix(#2801): address CodeRabbit — commit_docs assertion, broader gsd-sdk detection, bash fence --------- Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/bin/gsd-tools.cjs | 5 +- get-shit-done/bin/lib/init.cjs | 20 +++ get-shit-done/workflows/ingest-docs.md | 4 +- tests/bug-2801-ingest-docs-handler.test.cjs | 140 ++++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 tests/bug-2801-ingest-docs-handler.test.cjs diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index c93bcea19..eaa518259 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -884,6 +884,9 @@ async function runCommand(command, args, cwd, raw, defaultValue) { case 'quick': init.cmdInitQuick(cwd, args.slice(2).join(' '), raw); break; + case 'ingest-docs': + init.cmdInitIngestDocs(cwd, raw); + break; case 'resume': init.cmdInitResume(cwd, raw); break; @@ -918,7 +921,7 @@ async function runCommand(command, args, cwd, raw, defaultValue) { init.cmdInitRemoveWorkspace(cwd, args[2], raw); break; default: - error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, manager, new-workspace, list-workspaces, remove-workspace`); + error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, ingest-docs, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, manager, new-workspace, list-workspaces, remove-workspace`); } break; } diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 36dec4201..74ae4ec51 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -562,6 +562,25 @@ function cmdInitQuick(cwd, description, raw) { output(withProjectRoot(cwd, result), raw); } +/** + * Init handler for ingest-docs workflow (#2801). + * + * Returns the minimal set of fields that ingest-docs.md needs to detect + * whether a project/planning dir exists and choose new vs merge mode. + * Mirrors the initIngestDocs SDK handler in sdk/src/query/init.ts. + */ +function cmdInitIngestDocs(cwd, raw) { + const config = loadConfig(cwd); + const result = { + project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'), + planning_exists: fs.existsSync(planningRoot(cwd)), + has_git: fs.existsSync(path.join(cwd, '.git')), + project_path: '.planning/PROJECT.md', + commit_docs: config.commit_docs, + }; + output(withProjectRoot(cwd, result), raw); +} + function cmdInitResume(cwd, raw) { const config = loadConfig(cwd); @@ -1928,6 +1947,7 @@ module.exports = { cmdInitNewProject, cmdInitNewMilestone, cmdInitQuick, + cmdInitIngestDocs, cmdInitResume, cmdInitVerifyWork, cmdInitPhaseOp, diff --git a/get-shit-done/workflows/ingest-docs.md b/get-shit-done/workflows/ingest-docs.md index 2273ba4ef..b9b6a0d07 100644 --- a/get-shit-done/workflows/ingest-docs.md +++ b/get-shit-done/workflows/ingest-docs.md @@ -52,7 +52,7 @@ If `PATH_NOT_FOUND` or `MANIFEST_NOT_FOUND`: display error and exit. Run the init query: ```bash -INIT=$(gsd-sdk query init.ingest-docs) +INIT=$(gsd-tools init ingest-docs) ``` Parse `project_exists`, `planning_exists`, `has_git`, `project_path` from INIT. @@ -289,7 +289,7 @@ Preview the merge diff to the user and gate via approve-revise-abort before writ Commit the ingest results: ```bash -gsd-sdk query commit "docs: ingest {N} docs from {SCAN_PATH} (#2387)" --files \ +gsd-tools commit "docs: ingest {N} docs from {SCAN_PATH} (#2387)" --files \ .planning/PROJECT.md \ .planning/REQUIREMENTS.md \ .planning/ROADMAP.md \ diff --git a/tests/bug-2801-ingest-docs-handler.test.cjs b/tests/bug-2801-ingest-docs-handler.test.cjs new file mode 100644 index 000000000..977c2f265 --- /dev/null +++ b/tests/bug-2801-ingest-docs-handler.test.cjs @@ -0,0 +1,140 @@ +/** + * Regression test for bug #2801 + * + * `/gsd-ingest-docs` was broken because: + * 1. `workflows/ingest-docs.md` called `gsd-sdk query init.ingest-docs` but the + * installed binary is `gsd-tools` (not `gsd-sdk`). + * 2. `gsd-tools init` had no `ingest-docs` case in its dispatch switch. + * + * The fix: + * - Added `case 'ingest-docs'` to the `init` switch in `gsd-tools.cjs`. + * - Exported `cmdInitIngestDocs` from `init.cjs`. + * - Updated `workflows/ingest-docs.md` to call `gsd-tools init ingest-docs`. + * + * This test prevents regression of the dispatch omission. + */ + +'use strict'; + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const childProc = require('node:child_process'); +const { createTempProject, cleanup, TOOLS_PATH } = require('./helpers.cjs'); + +const REPO_ROOT = path.join(__dirname, '..'); +const WORKFLOW_FILE = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'ingest-docs.md'); + +function spawnGsdTools(args, projectDir) { + let stdout = ''; + let exitCode = 0; + try { + stdout = childProc.execFileSync( + process.execPath, + [TOOLS_PATH, ...args, '--cwd', projectDir], + { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, GSD_SESSION_KEY: '' }, + } + ); + } catch (err) { + exitCode = err.status ?? 1; + stdout = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? ''); + } + return { exitCode, stdout }; +} + +describe('bug-2801: gsd-tools init ingest-docs handler exists', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject('gsd-test-2801-'); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('init ingest-docs exits 0 (not "Unknown init workflow")', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0, `expected exit 0, got: ${stdout}`); + }); + + test('init ingest-docs returns JSON with project_exists field', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0); + let json; + try { json = JSON.parse(stdout.trim()); } catch { assert.fail(`non-JSON output: ${stdout}`); } + assert.ok(Object.prototype.hasOwnProperty.call(json, 'project_exists'), 'project_exists present'); + }); + + test('init ingest-docs returns JSON with planning_exists field', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0); + const json = JSON.parse(stdout.trim()); + assert.ok(Object.prototype.hasOwnProperty.call(json, 'planning_exists'), 'planning_exists present'); + }); + + test('init ingest-docs returns JSON with has_git field', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0); + const json = JSON.parse(stdout.trim()); + assert.ok(Object.prototype.hasOwnProperty.call(json, 'has_git'), 'has_git present'); + }); + + test('init ingest-docs returns JSON with project_path field', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0); + const json = JSON.parse(stdout.trim()); + assert.ok(Object.prototype.hasOwnProperty.call(json, 'project_path'), 'project_path present'); + assert.ok(Object.prototype.hasOwnProperty.call(json, 'commit_docs'), 'commit_docs present'); + }); + + test('planning_exists is true when .planning/ directory exists', () => { + const { exitCode, stdout } = spawnGsdTools(['init', 'ingest-docs', '--raw'], tmpDir); + assert.strictEqual(exitCode, 0); + const json = JSON.parse(stdout.trim()); + assert.strictEqual(json.planning_exists, true, 'planning_exists should be true (.planning/ created by createTempProject)'); + }); +}); + +describe('bug-2801: ingest-docs.md workflow calls gsd-tools not gsd-sdk', () => { + test('no bash code block in ingest-docs.md calls gsd-sdk', () => { + const content = fs.readFileSync(WORKFLOW_FILE, 'utf-8'); + // Extract bash fenced code blocks structurally. + const bashBlocks = []; + const codeBlockRe = /```bash\n([\s\S]*?)```/g; + let m; + while ((m = codeBlockRe.exec(content)) !== null) { + bashBlocks.push(m[1]); + } + assert.ok(bashBlocks.length > 0, 'expected bash code blocks in workflow'); + + // Check every line in every bash block — not just lines that start with the token, + // since gsd-sdk can appear in subshell expansions like $(gsd-sdk query ...). + const sdkCalls = bashBlocks + .join('\n') + .split('\n') + .filter((line) => /\bgsd-sdk\b/.test(line)); + + assert.deepStrictEqual( + sdkCalls, + [], + `workflow bash blocks still reference gsd-sdk (should use gsd-tools): ${sdkCalls.join(', ')}` + ); + }); + + test('ingest-docs.md init step uses gsd-tools init ingest-docs', () => { + const content = fs.readFileSync(WORKFLOW_FILE, 'utf-8'); + const lines = content.split('\n'); + const initLine = lines.find(l => /gsd-tools\s+init\s+ingest-docs/.test(l)); + assert.ok(initLine, 'workflow must contain "gsd-tools init ingest-docs"'); + }); + + test('cmdInitIngestDocs is exported from init.cjs', () => { + const init = require(path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'init.cjs')); + assert.strictEqual(typeof init.cmdInitIngestDocs, 'function', 'cmdInitIngestDocs must be exported'); + }); +});