#6479·gsd-2

Silent skip of unresolvable @-context refs + phases.clear can delete the only copy of pre-drafted phase dirs

Author: TheSeriousJCreated Jul 2, 2026Updated Jul 2, 2026

Filing here as the active successor: we hit this on get-shit-done-cc / get-shit-done 1.42.3, whose bugs URL points at the now-archived gsd-build/get-shit-done. Both failure modes are design-level and likely carry over to wherever the @-context loader and phases.clear handler live in gsd-2 — close if they don't.

Two related failure modes let a milestone close silently corrupt the next milestone's execution context. Both bit us on GSD 1.42.3 in a real project; details and a reference guard implementation below.

1. Unresolvable @-context references are silently skipped

Phase docs (CONTEXT.md, PLAN.md) load supporting artifacts via @<path> context references. When a referenced path does not resolve — typo, or the target file was moved — the reference is skipped silently. The executor then runs with partial context (missing method locks, prior results, data tables) and neither the agent nor the user is told anything is absent.

Real incident: at a milestone close we archived the completed milestone's phase directories from .planning/phases/ to .planning/milestones/v1.1-phases/ (the archive path init.new-milestone itself suggests via phase_archive_path). The next milestone's phase docs had been drafted and ratified before the move; 9 references — including the @-context lines at the top of all three plans — still pointed at the old .planning/phases/... paths. Nothing failed. The breakage was only found by a manual review grep before execution. Had it run, the executor would have proceeded without the method lock and the prior baseline's validation result — the worst kind of failure, because everything looks green.

Ask: fail loudly (or at minimum emit a visible warning) when an @-context reference does not resolve at load time. A useful refinement: allow refs to a phase's own future outputs (paths inside the active phase dir, or an explicit declaration file) to be exempt, since plans legitimately name artifacts they will create.

2. phases.clear can delete the only copy of pre-drafted phase dirs

gsd-sdk query phases.clear --confirm deletes all non-999.* directories under .planning/phases/, with no check on whether they are archived anywhere. The new-milestone workflow invokes it as routine cleanup ("Clear leftover phase directories from the previous milestone").

If the next milestone's phase directories already exist — a common pattern when phases are drafted/ratified ahead of formalizing the milestone — phases.clear sweeps them too, destroying the only copy of unexecuted, possibly-unpushed work. --confirm doesn't help: the operator is confirming "clear the old milestone's leftovers," not "delete the new milestone's plans."

Ask: make phases.clear refuse to delete any phase dir that has no copy under .planning/milestones/*/<dirname> (i.e. deletion would destroy the sole copy), with an explicit override flag. Reference implementation we run as a local patch in phasesClear (sdk/src/query/phase-lifecycle.ts), inserted after the existing --confirm check and before the delete loop:

javascript
const forceUnarchived = Array.isArray(args) && args.includes('--force-unarchived');
// refuse to delete phase dirs that have no archived copy under milestones/
if (dirs.length > 0 && !forceUnarchived) {
  const milestonesDir = join(phasesDir, '..', 'milestones');
  let archiveRoots = [];
  if (existsSync(milestonesDir)) {
    archiveRoots = (await readdir(milestonesDir, { withFileTypes: true }))
      .filter(e => e.isDirectory())
      .map(e => join(milestonesDir, e.name));
  }
  const unarchived = dirs.filter(d => !archiveRoots.some(root => existsSync(join(root, d.name))));
  if (unarchived.length > 0) {
    throw new GSDError(
      `phases clear REFUSED: ${unarchived.length} phase director${unarchived.length === 1 ? 'y has' : 'ies have'} no archived copy under milestones/: ` +
      `${unarchived.map(d => d.name).join(', ')}. These may belong to an unclosed/next milestone — deleting them would destroy the only copy. ` +
      `Archive them first (phases.archive or manual move), or pass --force-unarchived to override.`,
      ErrorClassification.Validation,
    );
  }
}

Behavior verified: unarchived dir → refusal before any deletion; dirs with archived copies → cleared as today; --force-unarchived → current behavior.

The two issues compose: the sanctioned archive move (workflow-suggested) is exactly what turns previously-valid @-refs stale, and the silent skip is what hides it. Fixing either upstream breaks the vector; fixing both closes it properly. Happy to send a PR for either if useful.