fix(#2415): close_phase_todos stages the pending/ deletion alongside completed/ (#2447)

* fix(#2415): close_phase_todos stages the pending/ deletion alongside completed/

Bug: the workflow step moved resolved todos from .planning/todos/pending/
to .planning/todos/completed/ with a plain 'mv', then committed listing
ONLY the destination directory in --files:

    mv "$TODO_FILE" "$COMPLETED_DIR/"
    gsd_run query commit '...' --files .planning/todos/completed/ .planning/STATE.md

Git's index still tracked the moved file at its old pending/<name>.md path.
The commit therefore only staged the new completed/<name>.md copy — the
deletion at pending/ was never staged, never committed, and lingered as an
unstaged deletion in git status indefinitely until some later broad
'git add -A' caught it. The phase genuinely closed the todo, but the
working tree was never clean.

Fix: add .planning/todos/pending/ to the --files list. 'git add' of that
directory (since git 2.0) stages deletions of tracked files in the pathspec,
so the moved-away file is staged as a deletion atomically with the new
completed/ copy in the same commit.

Chose plain mv + two-dir --files over 'git mv' because git mv FAILS on:
  - untracked todos (new todo file not yet committed)
  - non-git .planning dirs (worktree safety / pre-init projects)
Plain mv has neither failure mode.

Regression tests in tests/close-phase-todos-stage-deletion.test.cjs
(source-text-is-the-product: workflow .md text IS what the runtime loads)
cover:
  - the commit --files list includes BOTH completed/ AND pending/
  - the move uses plain 'mv' (not 'git mv') so untracked + non-git cases work

* chore(#2415): trim commit subject to keep execute-phase.md under byte ceiling

The fix added '.planning/todos/pending/' (~26 bytes) to the commit --files
list. To stay under the ADR-857 Phase 6 pre-phase-6 byte ceiling margin
(93400 bytes, hard ceiling 93600), shortened the commit subject from
'auto-close N todo(s) resolved by this phase' to 'close N resolved todo(s)'.
Net change vs origin/next: +6 bytes (93384 → 93390), well under the margin.

Also regenerates the golden-install-parity fixtures (execute-phase.md
content-hash update across all runtimes).

* chore(#2415): bump execute-phase.md workflow-size baseline (93384 → 93390)

The +pending/ fix added 6 net bytes (93384 → 93390), still well under the
ADR-857 Phase 6 pre-phase-6 byte ceiling margin (93400).

* chore(changeset): backfill pr:2447 in .changeset/sturdy-wasps-run.md

* fix(#2415): add issue ref to allow-test-rule annotation (ADR-456)

CI lint-allow-test-rule-refs failed on the prior commit — ADR-456 requires
'// allow-test-rule: <category> see #NNNN' so every exemption is traceable
to an issue. Added 'see #2415' to the source-text-is-the-product annotation.
This commit is contained in:
Tom Boucher
2026-07-20 08:22:23 -04:00
committed by GitHub
parent 6c00cdad07
commit eb45fc0e8c
22 changed files with 82 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2447
---
**`close_phase_todos` no longer leaves moved todos as phantom unstaged deletions in `git status`** — the workflow step moved resolved todos from `.planning/todos/pending/` to `.planning/todos/completed/` with a plain `mv`, then committed by listing only the destination directory in `--files`. Git's index still tracked the moved file at its old `pending/` path, so the deletion was never staged and the moved-away file lingered as an unstaged deletion in `git status` until some later broad `git add -A` happened to catch it. The step's commit `--files` list now includes BOTH directories so `git add .planning/todos/pending/` stages the deletion atomically with the new `completed/` copy in the same commit. (#2415)

View File

@@ -1541,7 +1541,7 @@ for TODO_FILE in "$PENDING_DIR"/*.md; do
done
if [ ${#CLOSED[@]} -gt 0 ]; then
gsd_run query commit "docs(phase-${PHASE_NUMBER}): auto-close ${#CLOSED[@]} todo(s) resolved by this phase" --files .planning/todos/completed/ .planning/STATE.md|| true
gsd_run query commit "docs(phase-${PHASE_NUMBER}): close ${#CLOSED[@]} resolved todo(s)" --files .planning/todos/completed/ .planning/todos/pending/ .planning/STATE.md|| true
echo "◆ Closed ${#CLOSED[@]} todo(s) resolved by Phase ${PHASE_NUMBER}:"
for f in "${CLOSED[@]}"; do echo " ✓ $f"; done
fi

View File

@@ -0,0 +1,57 @@
// allow-test-rule: source-text-is-the-product see #2415
// Workflow .md files — their text IS what the runtime loads. Testing text content
// tests the deployed contract. Per CONTRIBUTING.md exception matrix.
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const EXECUTE_PHASE = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-phase.md');
describe('#2415: close_phase_todos must stage the pending/ deletion alongside completed/', () => {
test('the close_phase_todos commit --files list includes .planning/todos/pending/', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf8');
// Isolate the close_phase_todos step body so we don't match unrelated --files lists
// elsewhere in the workflow (other steps commit different paths for different reasons).
const stepStart = content.indexOf('<step name="close_phase_todos">');
assert.ok(stepStart > -1, 'close_phase_todos step must exist in execute-phase.md');
const stepEnd = content.indexOf('</step>', stepStart);
assert.ok(stepEnd > stepStart, 'close_phase_todos step must be properly closed');
const stepBody = content.slice(stepStart, stepEnd);
// The commit must include BOTH the destination (completed/) AND the source (pending/)
// — git add of pending/ stages the deletion of each moved file. Without pending/ in
// the list, only the new completed/ copy gets committed and the moved-away file
// persists as an unstaged deletion in git status until some later broad git add -A
// happens to catch it (#2415).
const gsdRunCommit = /gsd_run\s+query\s+commit\b[^\n]*--files\s+([^\n]+)/;
const match = stepBody.match(gsdRunCommit);
assert.ok(match, `close_phase_todos step must contain a gsd_run query commit ... --files invocation. Step body:\n${stepBody}`);
const filesList = match[1];
assert.match(filesList, /\.planning\/todos\/completed/, 'commit --files must include .planning/todos/completed/ (destination of the move)');
assert.match(filesList, /\.planning\/todos\/pending/, 'commit --files must include .planning/todos/pending/ so the moved-away file is staged as a deletion (#2415)');
assert.match(filesList, /\.planning\/STATE\.md/, 'commit --files must still include .planning/STATE.md (the step also updates state)');
});
test('close_phase_todos uses plain mv (not git mv) so untracked todos and non-git .planning dirs still work', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf8');
const stepStart = content.indexOf('<step name="close_phase_todos">');
const stepEnd = content.indexOf('</step>', stepStart);
const stepBody = content.slice(stepStart, stepEnd);
// Strip bash comments so a doc comment mentioning "git mv" (rationale) doesn't
// trip the assertion. We care about the actual command, not the prose.
const withoutComments = stepBody.replace(/^\s*#.*$/gm, '');
// git mv would stage the rename atomically, but it FAILS on untracked todos and on
// non-git .planning dirs. Plain mv + the two-dir --files list (verified above) is
// more robust and what the fix uses. Pin the choice so a future contributor doesn't
// switch to git mv without revisiting the failure modes.
assert.match(withoutComments, /\bmv\s+"\$TODO_FILE"\s+"\$COMPLETED_DIR\/"/, 'close_phase_todos must use plain shell mv to move the file');
assert.doesNotMatch(withoutComments, /\bgit\s+mv\b/, 'close_phase_todos must NOT use git mv as the actual move command — it fails on untracked todos and on non-git .planning dirs');
});
});

View File

@@ -243,7 +243,7 @@
"gsd-core/workflows/docs-update.md": "8e986e26d0e6e1a0",
"gsd-core/workflows/edit-phase.md": "fc932e82ba1f585a",
"gsd-core/workflows/eval-review.md": "eb4040eaa5b8497f",
"gsd-core/workflows/execute-phase.md": "21c3aa8c771fcdc7",
"gsd-core/workflows/execute-phase.md": "2e9ca8842df9e543",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "55d0706e80a2554a",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "13aa54f8279960ae",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "09d5f9829424c68b",
"gsd-core/workflows/execute-phase.md": "982d35b8ed16619d",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -313,7 +313,7 @@
"gsd-core/workflows/docs-update.md": "cd753783ab95da00",
"gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e",
"gsd-core/workflows/eval-review.md": "086a1f2b3c11462c",
"gsd-core/workflows/execute-phase.md": "0086536412e218d3",
"gsd-core/workflows/execute-phase.md": "66e4f92099ba3a7a",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "6d38bfd540030da4",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "a7f9b9a45303382f",

View File

@@ -242,7 +242,7 @@
"gsd-core/workflows/docs-update.md": "63082608d3ae92be",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "f59e8329dae1e528",
"gsd-core/workflows/execute-phase.md": "1c67162c7a87cdbc",
"gsd-core/workflows/execute-phase.md": "7a7f63e7839f530d",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -246,7 +246,7 @@
"gsd-core/workflows/docs-update.md": "39f288623a8f6f32",
"gsd-core/workflows/edit-phase.md": "9c9fadc047c61d74",
"gsd-core/workflows/eval-review.md": "3e1d7829ed2ed494",
"gsd-core/workflows/execute-phase.md": "6984700bf0a94ce7",
"gsd-core/workflows/execute-phase.md": "5aa6d3220bb3c755",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "67ebc93f51968cb6",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "35612890c1173577",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "02bc29efd6441a58",
"gsd-core/workflows/execute-phase.md": "77629722f47ec6c7",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -349,7 +349,7 @@
"gsd-core/workflows/docs-update.md": "e255317df939e302",
"gsd-core/workflows/edit-phase.md": "e592a4d85ce5380f",
"gsd-core/workflows/eval-review.md": "63d0d0670b54c244",
"gsd-core/workflows/execute-phase.md": "d7a6237dc2097828",
"gsd-core/workflows/execute-phase.md": "4771222b9f9609d0",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "f4cacd27d37bac65",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -244,7 +244,7 @@
"gsd-core/workflows/docs-update.md": "9292cfa3c52c434e",
"gsd-core/workflows/edit-phase.md": "8667c28b22b1599f",
"gsd-core/workflows/eval-review.md": "81c8e72ba3862856",
"gsd-core/workflows/execute-phase.md": "2ab0e3d8db52f54b",
"gsd-core/workflows/execute-phase.md": "9ecbabf9c4e62e80",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "63b712920f21a40f",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "25bebed74e645109",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "e86d7d7e2e3dac6d",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "a86279dd98dd5c03",
"gsd-core/workflows/execute-phase.md": "203a3e4afa6c0ba4",
"gsd-core/workflows/execute-phase.md": "0b9c81c3085926e6",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -243,7 +243,7 @@
"gsd-core/workflows/docs-update.md": "ba4cf926fc463cbe",
"gsd-core/workflows/edit-phase.md": "7f27003f20e88fb8",
"gsd-core/workflows/eval-review.md": "f510e5762212dc6f",
"gsd-core/workflows/execute-phase.md": "3ab3465c0071f873",
"gsd-core/workflows/execute-phase.md": "df3020f022405a84",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "14a27cc0828f59d3",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "26ee34c543926402",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "362123fdf99e9980",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "79afaaf19fd527cc",
"gsd-core/workflows/edit-phase.md": "8323bfe10faa0c0a",
"gsd-core/workflows/eval-review.md": "926eda8bbee28b23",
"gsd-core/workflows/execute-phase.md": "37b8f39d1ef48997",
"gsd-core/workflows/execute-phase.md": "ae91076f29981cc2",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "facb0e816d87a0c7",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -307,7 +307,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "79094f7c09b76e5b",
"gsd-core/workflows/execute-phase.md": "ae2c1d3f9518bee5",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "850366c2ef8fb780",
"gsd-core/workflows/edit-phase.md": "1876c855fb0a0a39",
"gsd-core/workflows/eval-review.md": "5394694d29ad7543",
"gsd-core/workflows/execute-phase.md": "a0de8c44d3234203",
"gsd-core/workflows/execute-phase.md": "83e7df675b781f8f",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "1804215577f1ad35",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "c4cba01539f0368a",

View File

@@ -210,7 +210,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "a0becf39cc735598",
"gsd-core/workflows/execute-phase.md": "60d61e7d2332e889",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -243,7 +243,7 @@
"gsd-core/workflows/docs-update.md": "45d2f0d173c84e07",
"gsd-core/workflows/edit-phase.md": "0fb5e0123cfc6f36",
"gsd-core/workflows/eval-review.md": "6dee8a1e40ececd4",
"gsd-core/workflows/execute-phase.md": "a771448bed59407a",
"gsd-core/workflows/execute-phase.md": "c06b2630c14ebac3",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "36af8d91e4ae8b9c",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "0b04cc2dcab61107",

View File

@@ -243,7 +243,7 @@
"gsd-core/workflows/docs-update.md": "f13571f08e083356",
"gsd-core/workflows/edit-phase.md": "7facd0faa33c8cad",
"gsd-core/workflows/eval-review.md": "37d545d4f0db4927",
"gsd-core/workflows/execute-phase.md": "364c2617ed78c71d",
"gsd-core/workflows/execute-phase.md": "4a8b484097cae892",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "c985a30317a1aa6b",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7d30384e9bf82664",

View File

@@ -243,7 +243,7 @@
"gsd-core/workflows/docs-update.md": "70f73cc8c27e0ca0",
"gsd-core/workflows/edit-phase.md": "c0ae7d0063f3e789",
"gsd-core/workflows/eval-review.md": "b28be79ef29f16fd",
"gsd-core/workflows/execute-phase.md": "8e349e33d7fa4b41",
"gsd-core/workflows/execute-phase.md": "3989e6302807b826",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "47ae5482f8e64100",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "fb4497767cdf73a3",

View File

@@ -314,7 +314,7 @@
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
"gsd-core/workflows/execute-phase.md": "945cab2f2b2d6fc7",
"gsd-core/workflows/execute-phase.md": "e0f1d13f102364a8",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "7b2d9a4a4013fd6a",

View File

@@ -24,7 +24,7 @@
"docs-update.md": 55706,
"edit-phase.md": 12927,
"eval-review.md": 9967,
"execute-phase.md": 93384,
"execute-phase.md": 93390,
"execute-plan.md": 34262,
"explore.md": 10541,
"extract-learnings.md": 12893,