* 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:
46
src/init.cts
46
src/init.cts
@@ -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')),
|
||||
|
||||
Reference in New Issue
Block a user