fix(#4040): route partially-created .planning to initialization recovery (#4283)

* test(#4040): add failing-first regression tests for partial-init routing

Red: init.progress/init.resume/init.new-project payloads carry no
partial-init discriminator, and progress.md/resume-project.md/
new-project.md route an interrupted bootstrap (.planning/PROJECT.md +
config.json only) to Route F / STATE reconstruction / a hard error.

* fix(#4040): route partially-created .planning to initialization recovery

A bootstrap interrupted after .planning/PROJECT.md (but before
REQUIREMENTS.md/ROADMAP.md/STATE.md) was mis-routed three ways:
progress.md read it as between-milestones (Route F) or 'no planning
structure', resume-project.md offered STATE.md reconstruction, and
new-project.md errored 'already initialized' — a routing loop with no
recovery exit.

Add a shared buildInitCompletenessFields discriminator
(planning_exists / requirements_exists / milestones_exists /
init_incomplete) to the init.progress, init.resume and init.new-project
payloads, and branch on init_incomplete in progress.md, resume-project.md
and new-project.md BEFORE the legacy branches. MILESTONES.md presence
excludes the archival between-milestones state, so Route F and the
STATE-reconstruction path keep working.

Emitted-Drift-Ack-Growth: progress.md — deliberate #4040 growth: new init_incomplete recovery branch (routing text + guard on the no-planning and Route F branches) added ahead of the legacy init_context routes.
Emitted-Drift-Ack-Growth: resume-project.md — deliberate #4040 growth: new init_incomplete branch routing an interrupted bootstrap to initialization recovery before the STATE.md-reconstruction branch.
Emitted-Drift-Ack-Growth: new-project.md — deliberate #4040 growth: project_exists gate split on init_incomplete so a partial bootstrap resumes initialization instead of erroring.

* chore(#4040): add changeset fragment

* chore(#4040): backfill PR number in changeset fragment

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-04 16:03:42 -04:00
committed by GitHub
parent b1b7cabfb5
commit 580059251a
7 changed files with 274 additions and 7 deletions

View File

@@ -1299,6 +1299,36 @@ function cmdInitPlanPhase(
output(withProjectRoot(cwd, result), raw);
}
// #4040: shared partial-init discriminator for the init.progress / init.resume /
// init.new-project payloads. A bootstrap interrupted before the core quartet
// (PROJECT.md / REQUIREMENTS.md / ROADMAP.md / STATE.md) all landed is a
// DISTINCT routing state from "new project" and from "between milestones";
// pre-#4040 the payloads could not express it, so progress.md mis-routed it to
// Route F and resume-project.md offered STATE.md reconstruction.
//
// Negative-space guard: `milestone.complete` archives ROADMAP.md (and
// REQUIREMENTS.md) but always leaves MILESTONES.md behind — so MILESTONES.md
// present proves missing core files are archival (between-milestones), never an
// unfinished bootstrap. REQUIREMENTS.md is written by new-project BEFORE
// ROADMAP/STATE, so its absence also proves init never finished.
function buildInitCompletenessFields(cwd: string): Record<string, boolean> {
const dir = planningDir(cwd);
const planningExists = fs.existsSync(dir);
const requirementsExists = fs.existsSync(path.join(dir, 'REQUIREMENTS.md'));
const milestonesExists = fs.existsSync(path.join(dir, 'MILESTONES.md'));
const coreComplete =
fs.existsSync(path.join(dir, 'PROJECT.md')) &&
requirementsExists &&
fs.existsSync(path.join(dir, 'ROADMAP.md')) &&
fs.existsSync(path.join(dir, 'STATE.md'));
return {
planning_exists: planningExists,
requirements_exists: requirementsExists,
milestones_exists: milestonesExists,
init_incomplete: planningExists && !coreComplete && !milestonesExists,
};
}
function cmdInitNewProject(cwd: string, raw: boolean, options: Record<string, unknown> = {}): void {
const config = loadConfig(cwd);
@@ -1325,6 +1355,12 @@ function cmdInitNewProject(cwd: string, raw: boolean, options: Record<string, un
commit_docs: config.commit_docs,
// #4040: partial-init discriminator (see buildInitCompletenessFields).
// Spread BEFORE this literal's own planning_exists so the existing
// root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
// one key stay byte-identical for existing consumers.
...buildInitCompletenessFields(cwd),
project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))),
has_codebase_map: hasCodebaseMap,
planning_exists: pathExistsInternal(cwd, '.planning'),
@@ -1626,6 +1662,12 @@ function cmdInitResume(cwd: string, raw: boolean): void {
if (agentIdRaw !== null) interruptedAgentId = agentIdRaw.trim();
const result: Record<string, unknown> = {
// #4040: partial-init discriminator (see buildInitCompletenessFields).
// Spread FIRST so this literal's own root-scoped planning_exists
// (planningRoot) keeps its existing semantics; init_incomplete itself
// keys off the artifact dir (planningDir) where the core docs live.
...buildInitCompletenessFields(cwd),
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))),
@@ -3329,6 +3371,10 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))),
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
// #4040: partial-init discriminator (see buildInitCompletenessFields) —
// also adds planning_exists / requirements_exists / milestones_exists so
// progress.md's init_context routing never has to fall back to Glob.
...buildInitCompletenessFields(cwd),
// #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
state_path: toPosixPath(path.join(planningDir(cwd), 'STATE.md')),
roadmap_path: toPosixPath(path.join(planningDir(cwd), 'ROADMAP.md')),