fix(#2767): pass paths via --files to gsd-sdk query commit + lint guard (#2781)

* fix(#2767): pass paths via --files to gsd-sdk query commit + lint guard

Workflows, agents, commands, and references passed file paths positionally
to `gsd-sdk query commit`, which silently appended them to the commit
subject and triggered the `.planning/` wholesale-stage fallback in
sdk/src/query/commit.ts:136. Regression of #733/#798.

Inserted `--files` before the path list at every site (81 invocations
across 50 files). Added tests/bug-2767-gsd-sdk-commit-files-flag.test.cjs
as a permanent lint that scans every shipped .md file and asserts each
`gsd-sdk query commit[-to-subrepo]` invocation either uses `--files` or
carries no path arguments.

Closes #2767

* test(#2767): replace source-grep with behavioral SDK test

The original test walked every shipped .md file and regex-tokenized
`gsd-sdk query commit` invocations to assert `--files` was present.
CONTRIBUTING.md prohibits this source-grep pattern.

Rewrite as behavioral SDK tests against `sdk/dist/cli.js` over a real
tmp git project (createTempGitProject helper). Cover both the
well-formed (`--files <paths>`) form — clean subject, exactly-staged
files, .planning/ left untouched — and the buggy positional form,
asserting the documented misbehavior (paths leak into subject + the
`.planning/` wholesale-stage fallback at commit.ts:136). Also asserts
`commit-to-subrepo` rejects when `--files` is omitted (commit.ts:258).

The doc-lint is retained as a supplementary defense-in-depth guard
since agent-prompt markdown invocations cannot be exercised end-to-end
— but it is no longer the primary contract.

* docs(#2767): correct contradictory --files guidance in zh-CN/en docs + fix test docstring
This commit is contained in:
Tom Boucher
2026-04-27 12:31:43 -04:00
committed by GitHub
parent 3ac3a2ae70
commit 54e6da3126
51 changed files with 385 additions and 82 deletions

View File

@@ -345,6 +345,7 @@ Use `gsd-sdk query commit` with conventional format (message first, then every s
```bash
gsd-sdk query commit \
"fix({padded_phase}): {finding_id} {short_description}" \
--files \
{all_modified_files}
```
@@ -354,7 +355,7 @@ Examples:
**Multiple files:** List ALL modified files after the message (space-separated):
```bash
gsd-sdk query commit "fix(02): CR-01 ..." \
gsd-sdk query commit "fix(02): CR-01 ..." --files \
src/api/auth.ts src/types/user.ts tests/auth.test.ts
```

View File

@@ -1168,7 +1168,7 @@ Root cause: {root_cause}"
Then commit planning docs via CLI (respects `commit_docs` config automatically):
```bash
gsd-sdk query commit "docs: resolve debug {slug}" .planning/debug/resolved/{slug}.md
gsd-sdk query commit "docs: resolve debug {slug}" --files .planning/debug/resolved/{slug}.md
```
**Append to knowledge base:**
@@ -1199,7 +1199,7 @@ Then append the entry:
Commit the knowledge base update alongside the resolved session:
```bash
gsd-sdk query commit "docs: update debug knowledge base with {slug}" .planning/debug/knowledge-base.md
gsd-sdk query commit "docs: update debug knowledge base with {slug}" --files .planning/debug/knowledge-base.md
```
Report completion and offer next steps.

View File

@@ -563,7 +563,7 @@ gsd-sdk query state.add-blocker "Blocker description"
<final_commit>
```bash
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" \
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" --files \
.planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md .planning/REQUIREMENTS.md
```

View File

@@ -755,7 +755,7 @@ Write to: `$PHASE_DIR/$PADDED_PHASE-RESEARCH.md`
## Step 7: Commit Research (optional)
```bash
gsd-sdk query commit "docs($PHASE): research phase domain" "$PHASE_DIR/$PADDED_PHASE-RESEARCH.md"
gsd-sdk query commit "docs($PHASE): research phase domain" --files "$PHASE_DIR/$PADDED_PHASE-RESEARCH.md"
```
## Step 8: Return Structured Result

View File

@@ -1136,7 +1136,7 @@ Plans:
<step name="git_commit">
```bash
gsd-sdk query commit "docs($PHASE): create phase plan" \
gsd-sdk query commit "docs($PHASE): create phase plan" --files \
.planning/phases/$PHASE-*/$PHASE-*-PLAN.md .planning/ROADMAP.md
```
</step>

View File

@@ -139,7 +139,7 @@ Write to `.planning/research/SUMMARY.md`
The 4 parallel researcher agents write files but do NOT commit. You commit everything together.
```bash
gsd-sdk query commit "docs: complete project research" .planning/research/
gsd-sdk query commit "docs: complete project research" --files .planning/research/
```
## Step 8: Return Summary

View File

@@ -292,7 +292,7 @@ Fill all sections. Write to `$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md`.
## Step 6: Commit (optional)
```bash
gsd-sdk query commit "docs($PHASE): UI design contract" "$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md"
gsd-sdk query commit "docs($PHASE): UI design contract" --files "$PHASE_DIR/$PADDED_PHASE-UI-SPEC.md"
```
## Step 7: Return Structured Result

View File

@@ -54,7 +54,7 @@ the normal phase sequence and accumulate context over time.
5. **Commit:**
```bash
gsd-sdk query commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" .planning/ROADMAP.md ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
gsd-sdk query commit "docs: add backlog item ${NEXT} — ${ARGUMENTS}" --files .planning/ROADMAP.md ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
```
6. **Report:**

View File

@@ -47,7 +47,7 @@ milestone sequence or remove stale entries.
6. **Commit changes:**
```bash
gsd-sdk query commit "docs: review backlog — promoted N, removed M" .planning/ROADMAP.md
gsd-sdk query commit "docs: review backlog — promoted N, removed M" --files .planning/ROADMAP.md
```
7. **Report summary:**

View File

@@ -83,7 +83,7 @@ When SUBCMD=close and SLUG is set (already sanitized):
3. Commit:
```bash
gsd-sdk query commit "docs: resolve thread — {SLUG}" ".planning/threads/{SLUG}.md"
gsd-sdk query commit "docs: resolve thread — {SLUG}" --files ".planning/threads/{SLUG}.md"
```
4. Print:
@@ -191,7 +191,7 @@ updated: {today ISO date}
5. Commit:
```bash
gsd-sdk query commit "docs: create thread — ${ARGUMENTS}" ".planning/threads/${SLUG}.md"
gsd-sdk query commit "docs: create thread — ${ARGUMENTS}" --files ".planning/threads/${SLUG}.md"
```
6. Report:

View File

@@ -51,7 +51,7 @@ Phases:
提交内容:
```bash
gsd-sdk query commit "docs: initialize [project-name] ([N] phases)" .planning/
gsd-sdk query commit "docs: initialize [project-name] ([N] phases)" --files .planning/
```
</format>
@@ -129,7 +129,7 @@ SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md
提交内容:
```bash
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
```
**注意:** 代码文件不包含 - 已按任务提交。
@@ -149,7 +149,7 @@ Current: [task name]
提交内容:
```bash
gsd-sdk query commit "wip: [phase-name] paused at task [X]/[Y]" .planning/
gsd-sdk query commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
```
</format>

View File

@@ -4,12 +4,12 @@
## 通过 CLI 提交
先传提交说明,再传文件路径(位置参数)。`commit` 不要使用 `--files`(该标志仅用于 `commit-to-subrepo`)。
先传提交说明,然后用 `--files` 显式传入文件路径。`commit` 与 `commit-to-subrepo` 都应使用 `--files` 来声明要提交的路径。
对 `.planning/` 文件始终使用此方式 —— 它会自动处理 `commit_docs` 与 gitignore 检查:
```bash
gsd-sdk query commit "docs({scope}): {description}" .planning/STATE.md .planning/ROADMAP.md
gsd-sdk query commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
```
如果 `commit_docs` 为 `false` 或 `.planning/` 被 gitignore,CLI 会返回 `skipped`(带原因)。无需手动条件检查。
@@ -19,7 +19,7 @@ gsd-sdk query commit "docs({scope}): {description}" .planning/STATE.md .planning
将 `.planning/` 文件变更合并到上次提交:
```bash
gsd-sdk query commit "" .planning/codebase/*.md --amend
gsd-sdk query commit "" --files .planning/codebase/*.md --amend
```
## 提交消息模式

View File

@@ -40,7 +40,7 @@
```bash
# 提交时自动检查 commit_docs + gitignore:
gsd-sdk query commit "docs: update state" .planning/STATE.md
gsd-sdk query commit "docs: update state" --files .planning/STATE.md
# 通过 state load 加载配置(返回 JSON):
INIT=$(gsd-sdk query state.load)
@@ -58,7 +58,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
**通过 CLI 提交(自动处理检查):**
```bash
gsd-sdk query commit "docs: update state" .planning/STATE.md
gsd-sdk query commit "docs: update state" --files .planning/STATE.md
```
CLI 在内部检查 `commit_docs` 配置和 gitignore 状态 —— 无需手动条件判断。

View File

@@ -266,7 +266,7 @@ Write the file.
**Commit:**
```bash
gsd-sdk query commit "docs(${PADDED_PHASE}): smart discuss context" "${phase_dir}/${padded_phase}-CONTEXT.md"
gsd-sdk query commit "docs(${PADDED_PHASE}): smart discuss context" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
```
Display confirmation:

View File

@@ -51,7 +51,7 @@ Phases:
What to commit:
```bash
gsd-sdk query commit "docs: initialize [project-name] ([N] phases)" .planning/
gsd-sdk query commit "docs: initialize [project-name] ([N] phases)" --files .planning/
```
</format>
@@ -133,7 +133,7 @@ SUMMARY: .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md
What to commit:
```bash
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-PLAN.md .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md
```
**Note:** Code files NOT included - already committed per-task.
@@ -153,7 +153,7 @@ Current: [task name]
What to commit:
```bash
gsd-sdk query commit "wip: [phase-name] paused at task [X]/[Y]" .planning/
gsd-sdk query commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/
```
</format>

View File

@@ -4,12 +4,12 @@ Commit planning artifacts via `gsd-sdk query commit`, which checks `commit_docs`
## Commit via CLI
Pass the message first, then file paths (positional). Do not use `--files` for `commit` (that flag is only for `commit-to-subrepo`).
Pass the message first, then file paths via `--files`. Both `commit` and `commit-to-subrepo` use `--files` to declare the paths to commit.
Always use this for `.planning/` files — it handles `commit_docs` and gitignore checks automatically:
```bash
gsd-sdk query commit "docs({scope}): {description}" .planning/STATE.md .planning/ROADMAP.md
gsd-sdk query commit "docs({scope}): {description}" --files .planning/STATE.md .planning/ROADMAP.md
```
The CLI will return `skipped` (with reason) if `commit_docs` is `false` or `.planning/` is gitignored. No manual conditional checks needed.
@@ -19,7 +19,7 @@ The CLI will return `skipped` (with reason) if `commit_docs` is `false` or `.pla
To fold `.planning/` file changes into the previous commit:
```bash
gsd-sdk query commit "" .planning/codebase/*.md --amend
gsd-sdk query commit "" --files .planning/codebase/*.md --amend
```
## Commit Message Patterns

View File

@@ -55,7 +55,7 @@ Group by plan, dimension, severity.
### Step 6: Commit
```bash
gsd-sdk query commit "fix($PHASE): revise plans based on checker feedback" .planning/phases/$PHASE-*/$PHASE-*-PLAN.md
gsd-sdk query commit "fix($PHASE): revise plans based on checker feedback" --files .planning/phases/$PHASE-*/$PHASE-*-PLAN.md
```
### Step 7: Return Revision Summary

View File

@@ -58,7 +58,7 @@ Configuration options for `.planning/` directory behavior.
```bash
# Commit with automatic commit_docs + gitignore checks:
gsd-sdk query commit "docs: update state" .planning/STATE.md
gsd-sdk query commit "docs: update state" --files .planning/STATE.md
# Load config via state load (returns JSON):
INIT=$(gsd-sdk query state.load)
@@ -76,7 +76,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
**Commit via CLI (handles checks automatically):**
```bash
gsd-sdk query commit "docs: update state" .planning/STATE.md
gsd-sdk query commit "docs: update state" --files .planning/STATE.md
```
The CLI checks `commit_docs` config and gitignore status internally — no manual conditionals needed.

View File

@@ -121,7 +121,7 @@ If `.planning/STATE.md` exists:
Commit the todo and any updated state:
```bash
gsd-sdk query commit "docs: capture todo - [title]" .planning/todos/pending/[filename] .planning/STATE.md
gsd-sdk query commit "docs: capture todo - [title]" --files .planning/todos/pending/[filename] .planning/STATE.md
```
Tool respects `commit_docs` config and gitignore automatically.

View File

@@ -249,7 +249,7 @@ None — discuss phase skipped.
Commit the minimal context:
```bash
gsd-sdk query commit "docs(${PADDED_PHASE}): auto-generated context (discuss skipped)" "${phase_dir}/${padded_phase}-CONTEXT.md"
gsd-sdk query commit "docs(${PADDED_PHASE}): auto-generated context (discuss skipped)" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
```
Proceed to 3b.

View File

@@ -157,7 +157,7 @@ If todo was moved to done/, commit the change:
```bash
git rm --cached .planning/todos/pending/[filename] 2>/dev/null || true
gsd-sdk query commit "docs: start work on todo - [title]" .planning/todos/completed/[filename] .planning/STATE.md
gsd-sdk query commit "docs: start work on todo - [title]" --files .planning/todos/completed/[filename] .planning/STATE.md
```
Tool respects `commit_docs` config and gitignore automatically.

View File

@@ -124,7 +124,7 @@ Repeat for all milestones in the cleanup set.
Commit the changes:
```bash
gsd-sdk query commit "chore: archive phase directories from completed milestones" .planning/milestones/ .planning/phases/
gsd-sdk query commit "chore: archive phase directories from completed milestones" --files .planning/milestones/ .planning/phases/
```
</step>

View File

@@ -494,7 +494,7 @@ Append the extracted Backlog content verbatim to the end of the newly written RO
**Safety commit — commit archive files BEFORE deleting any originals:**
```bash
gsd-sdk query commit "chore: archive v[X.Y] milestone files" .planning/milestones/v[X.Y]-ROADMAP.md .planning/milestones/v[X.Y]-REQUIREMENTS.md .planning/milestones/v[X.Y]-MILESTONE-AUDIT.md .planning/MILESTONES.md .planning/PROJECT.md .planning/STATE.md .planning/ROADMAP.md
gsd-sdk query commit "chore: archive v[X.Y] milestone files" --files .planning/milestones/v[X.Y]-ROADMAP.md .planning/milestones/v[X.Y]-REQUIREMENTS.md .planning/milestones/v[X.Y]-MILESTONE-AUDIT.md .planning/MILESTONES.md .planning/PROJECT.md .planning/STATE.md .planning/ROADMAP.md
```
This creates a durable checkpoint in git history. If anything fails after this point, the working tree can be reconstructed from git.
@@ -563,7 +563,7 @@ If the "## Cross-Milestone Trends" section exists, update the tables with new da
**Commit:**
```bash
gsd-sdk query commit "docs: update retrospective for v${VERSION}" .planning/RETROSPECTIVE.md
gsd-sdk query commit "docs: update retrospective for v${VERSION}" --files .planning/RETROSPECTIVE.md
```
</step>

View File

@@ -179,7 +179,7 @@ Update status in frontmatter to "diagnosed".
Commit the updated UAT.md:
```bash
gsd-sdk query commit "docs({phase_num}): add root causes from diagnosis" ".planning/phases/XX-name/{phase_num}-UAT.md"
gsd-sdk query commit "docs({phase_num}): add root causes from diagnosis" --files ".planning/phases/XX-name/{phase_num}-UAT.md"
```
</step>

View File

@@ -552,7 +552,7 @@ Write file.
Commit phase context and discussion log:
```bash
gsd-sdk query commit "docs(${padded_phase}): capture phase context (assumptions mode)" "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
gsd-sdk query commit "docs(${padded_phase}): capture phase context (assumptions mode)" --files "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
```
Confirm: "Committed: docs(${padded_phase}): capture phase context (assumptions mode)"
@@ -570,7 +570,7 @@ gsd-sdk query state.record-session \
Commit STATE.md:
```bash
gsd-sdk query commit "docs(state): record phase ${PHASE} context session" .planning/STATE.md
gsd-sdk query commit "docs(state): record phase ${PHASE} context session" --files .planning/STATE.md
```
</step>

View File

@@ -445,7 +445,7 @@ rm -f "${phase_dir}/${padded_phase}-DISCUSS-CHECKPOINT.json"
Commit phase context and discussion log:
```bash
gsd-sdk query commit "docs(${padded_phase}): capture phase context" "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
gsd-sdk query commit "docs(${padded_phase}): capture phase context" --files "${phase_dir}/${padded_phase}-CONTEXT.md" "${phase_dir}/${padded_phase}-DISCUSSION-LOG.md"
```
Confirm: "Committed: docs(${padded_phase}): capture phase context"
@@ -459,7 +459,7 @@ gsd-sdk query state.record-session \
--stopped-at "Phase ${PHASE} context gathered" \
--resume-file "${phase_dir}/${padded_phase}-CONTEXT.md"
gsd-sdk query commit "docs(state): record phase ${PHASE} context session" .planning/STATE.md
gsd-sdk query commit "docs(state): record phase ${PHASE} context session" --files .planning/STATE.md
```
</step>

View File

@@ -812,7 +812,7 @@ increases monotonically across waves. `{status}` is `complete` (success),
# Only commit tracking files if they actually changed
if ! git diff --quiet .planning/ROADMAP.md .planning/STATE.md 2>/dev/null; then
gsd-sdk query commit "docs(phase-${PHASE_NUMBER}): update tracking after wave ${N}" .planning/ROADMAP.md .planning/STATE.md
gsd-sdk query commit "docs(phase-${PHASE_NUMBER}): update tracking after wave ${N}" --files .planning/ROADMAP.md .planning/STATE.md
fi
elif [ "${TEST_EXIT}" -eq 124 ]; then
echo "⚠ Skipping tracking update — test suite timed out. Plans remain in-progress. Run tests manually to confirm."
@@ -1162,7 +1162,7 @@ mv .planning/debug/{slug}.md .planning/debug/resolved/
**6. Commit updated artifacts:**
```bash
gsd-sdk query commit "docs(phase-${PARENT_PHASE}): resolve UAT gaps and debug sessions after ${PHASE_NUMBER} gap closure" .planning/phases/*${PARENT_PHASE}*/*-UAT.md .planning/debug/resolved/*.md
gsd-sdk query commit "docs(phase-${PARENT_PHASE}): resolve UAT gaps and debug sessions after ${PHASE_NUMBER} gap closure" --files .planning/phases/*${PARENT_PHASE}*/*-UAT.md .planning/debug/resolved/*.md
```
</step>
@@ -1405,7 +1405,7 @@ blocked: 0
Commit the file:
```bash
gsd-sdk query commit "test({phase_num}): persist human verification items as UAT" "{phase_dir}/{phase_num}-HUMAN-UAT.md"
gsd-sdk query commit "test({phase_num}): persist human verification items as UAT" --files "{phase_dir}/{phase_num}-HUMAN-UAT.md"
```
**Step B: Present to user:**
@@ -1477,7 +1477,7 @@ These items are tracked and will appear in `/gsd-progress` and `/gsd-audit-uat`.
```
```bash
gsd-sdk query commit "docs(phase-{X}): complete phase execution" .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md
gsd-sdk query commit "docs(phase-{X}): complete phase execution" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md {phase_dir}/*-VERIFICATION.md
```
</step>
@@ -1527,7 +1527,7 @@ for TODO_FILE in "$PENDING_DIR"/*.md; do
done
if [ ${#CLOSED[@]} -gt 0 ]; then
gsd-sdk query commit "docs(phase-${PHASE_NUMBER}): auto-close ${#CLOSED[@]} todo(s) resolved by this phase" .planning/todos/completed/ .planning/STATE.md || true
gsd-sdk query commit "docs(phase-${PHASE_NUMBER}): auto-close ${#CLOSED[@]} todo(s) resolved by this phase" --files .planning/todos/completed/ .planning/STATE.md|| true
echo "◆ Closed ${#CLOSED[@]} todo(s) resolved by Phase ${PHASE_NUMBER}:"
for f in "${CLOSED[@]}"; do echo " ✓ $f"; done
fi
@@ -1552,7 +1552,7 @@ PROJECT.md falls behind silently over multiple phases.
5. Commit the change:
```bash
gsd-sdk query commit "docs(phase-{X}): evolve PROJECT.md after phase completion" .planning/PROJECT.md
gsd-sdk query commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md
```
**Skip this step if** `.planning/PROJECT.md` does not exist.

View File

@@ -440,9 +440,9 @@ IS_WORKTREE=$([ -f .git ] && echo "true" || echo "false")
# In parallel mode: exclude STATE.md and ROADMAP.md (orchestrator commits these)
if [ "$IS_WORKTREE" = "true" ]; then
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/REQUIREMENTS.md
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/REQUIREMENTS.md
else
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md .planning/REQUIREMENTS.md
gsd-sdk query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .planning/phases/XX-name/{phase}-{plan}-SUMMARY.md .planning/STATE.md .planning/ROADMAP.md .planning/REQUIREMENTS.md
fi
```
</step>
@@ -458,7 +458,7 @@ git diff --name-only ${FIRST_TASK}^..HEAD 2>/dev/null || true
Update only structural changes: new src/ dir → STRUCTURE.md | deps → STACK.md | file pattern → CONVENTIONS.md | API client → INTEGRATIONS.md | config → STACK.md | renamed → update paths. Skip code-only/bugfix/content changes.
```bash
gsd-sdk query commit "" .planning/codebase/*.md --amend
gsd-sdk query commit "" --files .planning/codebase/*.md --amend
```
</step>

View File

@@ -115,7 +115,7 @@ For each selected output, write the file:
Commit if `commit_docs` is enabled:
```bash
gsd-sdk query commit "docs: capture exploration — {topic_slug}" {file_list}
gsd-sdk query commit "docs: capture exploration — {topic_slug}" --files {file_list}
```
## Step 6: Close

View File

@@ -220,7 +220,7 @@ Update `.planning/STATE.md` if appropriate (e.g., increment total plan count).
Commit the imported plan and updated files:
```bash
gsd-sdk query commit "docs({phase}): import plan from {basename FILEPATH}" .planning/phases/{phase}/{plan}-PLAN.md .planning/ROADMAP.md
gsd-sdk query commit "docs({phase}): import plan from {basename FILEPATH}" --files .planning/phases/{phase}/{plan}-PLAN.md .planning/ROADMAP.md
```
Display completion:

View File

@@ -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)" \
gsd-sdk query commit "docs: ingest {N} docs from {SCAN_PATH} (#2387)" --files \
.planning/PROJECT.md \
.planning/REQUIREMENTS.md \
.planning/ROADMAP.md \

View File

@@ -378,7 +378,7 @@ Continue to commit_codebase_map.
Commit the codebase map:
```bash
gsd-sdk query commit "docs: map existing codebase" .planning/codebase/*.md
gsd-sdk query commit "docs: map existing codebase" --files .planning/codebase/*.md
```
Continue to offer_next.

View File

@@ -189,7 +189,7 @@ mkdir -p .planning/reports
Write the summary, then commit:
```bash
gsd-sdk query commit "docs(v${VERSION}): generate milestone summary for onboarding" \
gsd-sdk query commit "docs(v${VERSION}): generate milestone summary for onboarding" --files \
".planning/reports/MILESTONE_SUMMARY-v${VERSION}.md"
```

View File

@@ -212,7 +212,7 @@ gsd-sdk query phases.clear --confirm
```
```bash
gsd-sdk query commit "docs: start milestone v[X.Y] [Name]" .planning/PROJECT.md .planning/STATE.md
gsd-sdk query commit "docs: start milestone v[X.Y] [Name]" --files .planning/PROJECT.md .planning/STATE.md
```
## 7. Load Context and Resolve Models
@@ -444,7 +444,7 @@ If "adjust": Return to scoping.
**Commit requirements:**
```bash
gsd-sdk query commit "docs: define milestone v[X.Y] requirements" .planning/REQUIREMENTS.md
gsd-sdk query commit "docs: define milestone v[X.Y] requirements" --files .planning/REQUIREMENTS.md
```
## 10. Create Roadmap
@@ -530,7 +530,7 @@ Success criteria:
**Commit roadmap** (after approval):
```bash
gsd-sdk query commit "docs: create milestone v[X.Y] roadmap ([N] phases)" .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
gsd-sdk query commit "docs: create milestone v[X.Y] roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md
```
## 10.5. Link Pending Todos to Roadmap Phases
@@ -573,7 +573,7 @@ files: [existing]
**If any todos were linked:**
```bash
gsd-sdk query commit "docs: tag [count] pending todos with resolves_phase after milestone v[X.Y] roadmap" .planning/todos/pending/*.md
gsd-sdk query commit "docs: tag [count] pending todos with resolves_phase after milestone v[X.Y] roadmap" --files .planning/todos/pending/*.md
```
Print a summary:

View File

@@ -236,7 +236,7 @@ gsd-sdk query config-new-project '{"mode":"yolo","granularity":"[selected]","par
```bash
mkdir -p .planning
gsd-sdk query commit "chore: add project config" .planning/config.json
gsd-sdk query commit "chore: add project config" --files .planning/config.json
```
**Persist auto-advance chain flag to config (survives context compaction):**
@@ -447,7 +447,7 @@ Do not compress. Capture everything gathered.
```bash
mkdir -p .planning
gsd-sdk query commit "docs: initialize project" .planning/PROJECT.md
gsd-sdk query commit "docs: initialize project" --files .planning/PROJECT.md
```
## 5. Workflow Preferences
@@ -666,7 +666,7 @@ gsd-sdk query config-new-project '{"mode":"[yolo|interactive]","granularity":"[s
**Commit config.json:**
```bash
gsd-sdk query commit "chore: add project config" .planning/config.json
gsd-sdk query commit "chore: add project config" --files .planning/config.json
```
## 5.1. Sub-Repo Detection
@@ -1114,7 +1114,7 @@ If "adjust": Return to scoping.
**Commit requirements:**
```bash
gsd-sdk query commit "docs: define v1 requirements" .planning/REQUIREMENTS.md
gsd-sdk query commit "docs: define v1 requirements" --files .planning/REQUIREMENTS.md
```
## 8. Create Roadmap
@@ -1266,7 +1266,7 @@ This ensures new projects get the default GSD workflow-enforcement guidance and
**Commit roadmap (after approval or auto mode):**
```bash
gsd-sdk query commit "docs: create roadmap ([N] phases)" .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md "$INSTRUCTION_FILE"
gsd-sdk query commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md "$INSTRUCTION_FILE"
```
## 9. Done

View File

@@ -207,7 +207,7 @@ timestamp=$(gsd-sdk query current-timestamp full --raw)
<step name="commit">
```bash
gsd-sdk query commit "wip: [context-name] paused at [X]/[Y]" [handoff-path] .planning/HANDOFF.json
gsd-sdk query commit "wip: [context-name] paused at [X]/[Y]" --files [handoff-path] .planning/HANDOFF.json
```
</step>

View File

@@ -146,7 +146,7 @@ mkdir -p ".planning/phases/{NN}-{name}"
## 9. Commit Roadmap and Requirements Update
```bash
gsd-sdk query commit "docs(roadmap): add gap closure phases {N}-{M}" .planning/ROADMAP.md .planning/REQUIREMENTS.md
gsd-sdk query commit "docs(roadmap): add gap closure phases {N}-{M}" --files .planning/ROADMAP.md .planning/REQUIREMENTS.md
```
## 10. Offer Next Steps

View File

@@ -201,7 +201,7 @@ Use full relative paths. Group by topic area.]
5. Commit:
```bash
gsd-sdk query commit "docs(${padded_phase}): generate context from PRD" "${phase_dir}/${padded_phase}-CONTEXT.md"
gsd-sdk query commit "docs(${padded_phase}): generate context from PRD" --files "${phase_dir}/${padded_phase}-CONTEXT.md"
```
6. Set `context_content` to the generated CONTEXT.md content and continue to step 5 (Handle Research).
@@ -925,7 +925,7 @@ For each plan entry extracted from `PLAN-OUTLINE.md`:
5. **Commit per-plan:**
```bash
gsd-sdk query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" "${PHASE_DIR}/${plan_id}-PLAN.md"
gsd-sdk query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" --files "${PHASE_DIR}/${plan_id}-PLAN.md"
```
After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
@@ -1263,7 +1263,7 @@ After the script returns, check that the bounced file still has valid YAML front
6. **Commit surviving bounced plans:** If at least one plan survived both the frontmatter validation and the checker re-run, commit the changes:
```bash
gsd-sdk query commit "refactor(${padded_phase}): bounce plans through external refinement" "${PHASE_DIR}/*-PLAN.md"
gsd-sdk query commit "refactor(${padded_phase}): bounce plans through external refinement" --files "${PHASE_DIR}/*-PLAN.md"
```
Display summary:

View File

@@ -143,7 +143,7 @@ Related code and decisions found in the current codebase:
<step name="commit_seed">
```bash
gsd-sdk query commit "docs: plant seed — {$IDEA}" .planning/seeds/SEED-{PADDED}-{slug}.md
gsd-sdk query commit "docs: plant seed — {$IDEA}" --files .planning/seeds/SEED-{PADDED}-{slug}.md
```
</step>

View File

@@ -970,7 +970,7 @@ if [ "$COMMIT_DOCS" = "false" ]; then
else
git add ${file_list} 2>/dev/null
fi
gsd-sdk query commit "docs(quick-${quick_id}): ${DESCRIPTION}" ${file_list}
gsd-sdk query commit "docs(quick-${quick_id}): ${DESCRIPTION}" --files ${file_list}
```
Get final commit hash:

View File

@@ -103,7 +103,7 @@ Extract from result: `removed`, `directory_deleted`, `renamed_directories`, `ren
Stage and commit the removal:
```bash
gsd-sdk query commit "chore: remove phase {target} ({original-phase-name})" .planning/
gsd-sdk query commit "chore: remove phase {target} ({original-phase-name})" --files .planning/
```
The commit message preserves the historical record of what was removed.

View File

@@ -410,7 +410,7 @@ plans_reviewed: [{list of PLAN.md files}]
Commit:
```bash
gsd-sdk query commit "docs: cross-AI review for phase {N}" {phase_dir}/{padded_phase}-REVIEWS.md
gsd-sdk query commit "docs: cross-AI review for phase {N}" --files {phase_dir}/{padded_phase}-REVIEWS.md
```
</step>

View File

@@ -257,7 +257,7 @@ gsd-sdk query state.update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_
If `commit_docs` is true:
```bash
gsd-sdk query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" .planning/STATE.md
gsd-sdk query commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md
```
</step>

View File

@@ -232,7 +232,7 @@ If this routing line already exists (append mode), leave it as-is.
Commit all artifacts (if `COMMIT_DOCS` is true):
```bash
gsd-sdk query commit "docs(sketch-wrap-up): package [N] sketch findings into project skill" .planning/sketches/WRAP-UP-SUMMARY.md
gsd-sdk query commit "docs(sketch-wrap-up): package [N] sketch findings into project skill" --files .planning/sketches/WRAP-UP-SUMMARY.md
```
</step>

View File

@@ -296,7 +296,7 @@ Iterate until satisfied.
**h.** Commit (if `COMMIT_DOCS` is true):
```bash
gsd-sdk query commit "docs(sketch-NNN): [winning direction] — [key visual insight]" .planning/sketches/NNN-descriptive-name/ .planning/sketches/MANIFEST.md
gsd-sdk query commit "docs(sketch-NNN): [winning direction] — [key visual insight]" --files .planning/sketches/NNN-descriptive-name/ .planning/sketches/MANIFEST.md
```
**i.** Report:

View File

@@ -246,7 +246,7 @@ Patterns and stack choices established across spike sessions. New spikes follow
Commit all artifacts (if `COMMIT_DOCS` is true):
```bash
gsd-sdk query commit "docs(spike-wrap-up): package [N] spike findings into project skill" .planning/spikes/WRAP-UP-SUMMARY.md .planning/spikes/CONVENTIONS.md
gsd-sdk query commit "docs(spike-wrap-up): package [N] spike findings into project skill" --files .planning/spikes/WRAP-UP-SUMMARY.md .planning/spikes/CONVENTIONS.md
```
</step>

View File

@@ -334,7 +334,7 @@ tags: [tag1, tag2]
**i.** Commit (if `COMMIT_DOCS` is true):
```bash
gsd-sdk query commit "docs(spike-NNN): [VERDICT] — [key finding]" .planning/spikes/NNN-descriptive-name/ .planning/spikes/MANIFEST.md
gsd-sdk query commit "docs(spike-NNN): [VERDICT] — [key finding]" --files .planning/spikes/NNN-descriptive-name/ .planning/spikes/MANIFEST.md
```
**j.** Report:
@@ -388,7 +388,7 @@ Only include patterns that repeated across 2+ spikes or were explicitly chosen b
Commit (if `COMMIT_DOCS` is true):
```bash
gsd-sdk query commit "docs(spikes): update conventions" .planning/spikes/CONVENTIONS.md
gsd-sdk query commit "docs(spikes): update conventions" --files .planning/spikes/CONVENTIONS.md
```
</step>

View File

@@ -298,7 +298,7 @@ Dimensions: 6/6 passed
## 11. Commit (if configured)
```bash
gsd-sdk query commit "docs(${padded_phase}): UI design contract" "${PHASE_DIR}/${PADDED_PHASE}-UI-SPEC.md"
gsd-sdk query commit "docs(${padded_phase}): UI design contract" --files "${PHASE_DIR}/${PADDED_PHASE}-UI-SPEC.md"
```
## 12. Update State

View File

@@ -176,7 +176,7 @@ tools is detected at runtime.
## 5. Commit (if configured)
```bash
gsd-sdk query commit "docs(${padded_phase}): UI audit review" "${PHASE_DIR}/${PADDED_PHASE}-UI-REVIEW.md"
gsd-sdk query commit "docs(${padded_phase}): UI audit review" --files "${PHASE_DIR}/${PADDED_PHASE}-UI-REVIEW.md"
```
</process>

View File

@@ -391,7 +391,7 @@ Clear Current Test section:
Commit the UAT file:
```bash
gsd-sdk query commit "test({phase_num}): complete UAT - {passed} passed, {issues} issues" ".planning/phases/XX-name/{phase_num}-UAT.md"
gsd-sdk query commit "test({phase_num}): complete UAT - {passed} passed, {issues} issues" --files ".planning/phases/XX-name/{phase_num}-UAT.md"
```
Present summary:

View File

@@ -0,0 +1,302 @@
/**
* Bug #2767: Workflows pass paths positionally to `gsd-sdk query commit`.
*
* Runtime behavior under the buggy form (paths positional, no `--files`):
* 1. positional path tokens are joined into the commit subject (commit.ts:110); and
* 2. `filePaths` is empty, so the handler falls back to staging `.planning/`
* wholesale (commit.ts:136), silently swapping the user's intent.
*
* Under the well-formed form (`--files <path...>`):
* - subject is the message arg only;
* - exactly the listed files are staged;
* - `commit-to-subrepo` rejects when `--files` is absent (commit.ts:258).
*
* Note: the supplementary doc-lint's `isWellFormed` accepts any invocation that
* has no positional path args after the message — i.e. message-only commits pass
* regardless of any trailing comment. There is no required marker (`# message-only`
* or otherwise); the absence of positional path tokens is the sole signal.
*
* Primary test: invoke the actual `gsd-sdk query commit[-to-subrepo]` binary
* against a real tmp git project and assert the runtime behavior. Supplementary
* test: a doc-lint that scans every shipped .md file to catch regressions of
* the 50-file invocation cleanup landed in this PR. The behavioral tests are
* the contract; the lint is a defense-in-depth guard.
*/
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 { execFileSync } = require('node:child_process');
const { createTempGitProject, cleanup } = require('./helpers.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const SDK_CLI = path.join(REPO_ROOT, 'sdk', 'dist', 'cli.js');
/**
* Run a git command with hardcoded argv (no shell). Returns trimmed stdout.
*/
function git(projectDir, args) {
return execFileSync('git', args, { cwd: projectDir, encoding: 'utf-8' }).trim();
}
/**
* Invoke `gsd-sdk query <subcommand> <...args>` against a project dir.
* Returns { exitCode, stdout, stderr, json } where json is the parsed handler
* payload (the SDK prints a single JSON object to stdout for query handlers).
*/
function runSdkQuery(subcommand, args, projectDir) {
const argv = ['query', subcommand, ...args, '--project-dir', projectDir];
let stdout = '';
let stderr = '';
let exitCode = 0;
try {
stdout = execFileSync(process.execPath, [SDK_CLI, ...argv], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, GSD_SESSION_KEY: '' },
});
} catch (err) {
exitCode = err.status ?? 1;
stdout = err.stdout?.toString() ?? '';
stderr = err.stderr?.toString() ?? '';
}
// Extract the trailing JSON object — the CLI prints status lines before it.
let json = null;
const lastBrace = stdout.lastIndexOf('{');
if (lastBrace >= 0) {
try { json = JSON.parse(stdout.slice(lastBrace).trim()); } catch { /* leave null */ }
if (!json) {
try { json = JSON.parse(stdout.trim()); } catch { /* leave null */ }
}
}
return { exitCode, stdout, stderr, json };
}
function gitSubject(projectDir) {
return git(projectDir, ['log', '-1', '--pretty=%s']);
}
function gitFilesAt(projectDir) {
return git(projectDir, ['show', '--pretty=', '--name-only', 'HEAD'])
.split('\n').filter(Boolean).sort();
}
// ─── Behavioral SDK tests ────────────────────────────────────────────────────
describe('bug #2767 (behavioral): gsd-sdk query commit --files', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject('gsd-2767-');
fs.writeFileSync(path.join(tmpDir, 'foo.md'), 'foo body\n');
fs.writeFileSync(path.join(tmpDir, 'bar.md'), 'bar body\n');
// .planning/ change that MUST NOT leak into the commit when --files is used.
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'state\n');
});
afterEach(() => {
cleanup(tmpDir);
});
test('well-formed: --files <paths> stages exactly those files with clean subject', () => {
const message = 'test(#2767): well-formed commit';
const result = runSdkQuery('commit', [message, '--files', 'foo.md', 'bar.md'], tmpDir);
assert.equal(result.exitCode, 0, `cli failed: ${result.stderr}`);
assert.ok(result.json, `expected JSON body in stdout, got:\n${result.stdout}`);
assert.equal(result.json.committed, true, `commit failed: ${JSON.stringify(result.json)}`);
assert.equal(result.json.message, message, 'subject must be message-only — no path leakage');
assert.deepEqual(result.json.files.slice().sort(), ['bar.md', 'foo.md']);
// Cross-check via git itself.
assert.equal(gitSubject(tmpDir), message);
assert.deepEqual(gitFilesAt(tmpDir), ['bar.md', 'foo.md']);
// The .planning/STATE.md change must remain unstaged/unrelated.
const stillDirty = git(tmpDir, ['status', '--porcelain']);
assert.ok(stillDirty.includes('.planning/STATE.md'),
`.planning/STATE.md should remain unstaged, got status:\n${stillDirty}`);
});
test('buggy form (positional, no --files): paths leak into subject AND .planning/ fallback fires', () => {
// Documents the misbehavior #2767 prevents at every workflow call site.
// Any future change that makes the buggy form silently "do the right thing"
// trips this test and must justify the change.
const message = 'test(#2767): positional buggy';
const result = runSdkQuery('commit', [message, 'foo.md', 'bar.md'], tmpDir);
assert.equal(result.exitCode, 0);
assert.ok(result.json, `expected JSON body, got:\n${result.stdout}`);
assert.equal(result.json.committed, true);
// Subject got polluted with the path tokens.
assert.equal(result.json.message, `${message} foo.md bar.md`,
'paths leak into the commit subject under the buggy form (commit.ts:110)');
assert.equal(gitSubject(tmpDir), `${message} foo.md bar.md`);
// Fallback staged .planning/STATE.md, NOT foo.md/bar.md.
assert.deepEqual(gitFilesAt(tmpDir), ['.planning/STATE.md'],
'positional form triggers the .planning/ wholesale fallback (commit.ts:136)');
const dirty = git(tmpDir, ['status', '--porcelain']);
assert.ok(dirty.includes('foo.md') && dirty.includes('bar.md'),
`foo.md/bar.md should remain unstaged under the buggy form, got:\n${dirty}`);
});
test('positional form with no .planning/ change: returns "nothing staged"', () => {
// Reset the .planning/STATE.md change so the fallback has nothing to stage.
fs.rmSync(path.join(tmpDir, '.planning', 'STATE.md'), { force: true });
const result = runSdkQuery('commit', ['msg', 'foo.md'], tmpDir);
assert.equal(result.exitCode, 0);
assert.ok(result.json, `expected JSON body, got:\n${result.stdout}`);
assert.equal(result.json.committed, false);
assert.equal(result.json.reason, 'nothing staged',
'positional form ignores foo.md and finds nothing under .planning/ to fall back to');
});
});
describe('bug #2767 (behavioral): commit-to-subrepo requires --files', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject('gsd-2767-sub-');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ sub_repos: ['vendor/lib'] }, null, 2),
);
fs.mkdirSync(path.join(tmpDir, 'vendor', 'lib'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'vendor', 'lib', 'README.md'), 'sub\n');
});
afterEach(() => {
cleanup(tmpDir);
});
test('rejects with explicit error when --files is omitted', () => {
const result = runSdkQuery('commit-to-subrepo', ['only message, no files'], tmpDir);
assert.equal(result.exitCode, 0);
assert.ok(result.json, `expected JSON body, got:\n${result.stdout}`);
assert.equal(result.json.committed, false);
assert.equal(result.json.reason, '--files required for commit-to-subrepo',
'commit-to-subrepo enforces --files at runtime (commit.ts:258)');
});
});
// ─── Supplementary doc-lint (defense-in-depth) ───────────────────────────────
//
// The behavioral tests above prove the SDK semantics. This lint scans every
// shipped .md invocation to catch regressions in the 50-file workflow cleanup
// landed by this PR — without it, a future contributor adding a new workflow
// could reintroduce the positional form silently. allow-test-rule: doc-text
// invocations cannot be exercised end-to-end (they are agent-prompt strings
// rendered into chat, not invoked by gsd-tools.cjs), so a textual guard is
// the only available enforcement layer.
const SCAN_DIRS = ['agents', 'commands', 'get-shit-done', 'docs', 'scripts'];
const KNOWN_FLAGS = new Set(['--force', '--amend', '--no-verify', '--files']);
function walk(dir, out = []) {
if (!fs.existsSync(dir)) return out;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
walk(full, out);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
out.push(full);
}
}
return out;
}
function extractInvocations(filePath) {
const text = fs.readFileSync(filePath, 'utf-8');
const lines = text.split(/\r?\n/);
const invocations = [];
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/(gsd-sdk\s+query\s+commit(?:-to-subrepo)?\b.*)$/);
if (!m) continue;
const tail = m[1].match(/^gsd-sdk\s+query\s+commit(?:-to-subrepo)?(.?)/);
if (tail && tail[1] === '`') continue;
let cmd = m[1];
let j = i;
while (cmd.endsWith('\\') && j + 1 < lines.length) {
cmd = cmd.slice(0, -1).trimEnd() + ' ' + lines[j + 1].trim();
j++;
}
invocations.push({ line: i + 1, cmd, file: filePath });
}
return invocations;
}
function stripTail(cmd) {
let inSingle = false, inDouble = false;
for (let i = 0; i < cmd.length; i++) {
const c = cmd[i];
if (c === "'" && !inDouble) inSingle = !inSingle;
else if (c === '"' && !inSingle) inDouble = !inDouble;
else if (!inSingle && !inDouble) {
if (c === '#' && (i === 0 || /\s/.test(cmd[i - 1]))) return cmd.slice(0, i);
if (c === '|' || c === ';' || c === '>' || c === '<') return cmd.slice(0, i);
if (c === '&' && cmd[i + 1] === '&') return cmd.slice(0, i);
}
}
return cmd;
}
function tokenize(cmd) {
const out = [];
let cur = '', inSingle = false, inDouble = false;
for (let i = 0; i < cmd.length; i++) {
const c = cmd[i];
if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; continue; }
if (c === '"' && !inSingle) { inDouble = !inDouble; cur += c; continue; }
if (/\s/.test(c) && !inSingle && !inDouble) {
if (cur) { out.push(cur); cur = ''; }
continue;
}
cur += c;
}
if (cur) out.push(cur);
return out;
}
function isWellFormed(cmd) {
const truncated = stripTail(cmd);
const parts = tokenize(truncated);
if (parts.length < 3) return true;
const args = parts.slice(3);
if (args.length === 0) return true;
if (args.includes('--files')) return true;
let sawMessage = false;
for (const tok of args) {
if (KNOWN_FLAGS.has(tok)) continue;
if (!sawMessage) { sawMessage = true; continue; }
return false;
}
return true;
}
describe('bug #2767 (supplementary doc-lint): no positional invocations in shipped .md', () => {
const allFiles = SCAN_DIRS.flatMap(d => walk(path.join(REPO_ROOT, d)));
const allInvocations = allFiles.flatMap(extractInvocations);
test('lint scanned at least one invocation (sanity)', () => {
assert.ok(allInvocations.length > 0, 'lint scanned 0 invocations — globs are wrong');
});
test('every gsd-sdk query commit invocation either uses --files or has no path args', () => {
const broken = allInvocations.filter(({ cmd }) => !isWellFormed(cmd));
if (broken.length > 0) {
const detail = broken.map(({ file, line, cmd }) => {
const rel = path.relative(REPO_ROOT, file);
return ` ${rel}:${line}\n ${cmd}`;
}).join('\n');
assert.fail(
`${broken.length} \`gsd-sdk query commit\` invocation(s) pass paths positionally.\n` +
`The behavioral test above proves what goes wrong at runtime; this lint catches\n` +
`regressions in shipped workflow markdown.\n${detail}`
);
}
});
});