* 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:
5
.changeset/happy-ravens-zip.md
Normal file
5
.changeset/happy-ravens-zip.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4283
|
||||
---
|
||||
**Partial `.planning` directories now route to initialization recovery** — a bootstrap interrupted after `.planning/PROJECT.md` no longer mis-routes `/gsd:progress` to between-milestones or "no project", nor `resume` to STATE.md reconstruction; both now resume `/gsd:new-project` until the missing REQUIREMENTS.md/ROADMAP.md/STATE.md exist. (#4040)
|
||||
@@ -37,7 +37,7 @@ AGENT_SKILLS_SYNTHESIZER=$(gsd_run query agent-skills gsd-research-synthesizer)
|
||||
AGENT_SKILLS_ROADMAPPER=$(gsd_run query agent-skills gsd-roadmapper)
|
||||
```
|
||||
|
||||
Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `git_worktree_root`, `in_nested_subdir`, `project_path`, `agents_installed`, `missing_agents`, `agent_runtime`, `agents_dir`, `required_agents`, `required_agents_installed`, `missing_required_agents`, `agent_skill_payloads_available`, `agent_skill_payload_agents`, `requirements_path`, `roadmap_path`, `config_path`, `research_dir`, `response_language`.
|
||||
Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `git_worktree_root`, `in_nested_subdir`, `project_path`, `agents_installed`, `missing_agents`, `agent_runtime`, `agents_dir`, `required_agents`, `required_agents_installed`, `missing_required_agents`, `agent_skill_payloads_available`, `agent_skill_payload_agents`, `requirements_exists`, `init_incomplete`, `requirements_path`, `roadmap_path`, `config_path`, `research_dir`, `response_language`.
|
||||
|
||||
**If `response_language` is set:** All user-facing questions, prompts, and explanations in this workflow MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated.
|
||||
|
||||
@@ -92,7 +92,9 @@ INSTRUCTION_FILE=$(gsd_run query project-instruction-file --runtime "$RUNTIME")
|
||||
|
||||
All subsequent references to the project instruction file use `$INSTRUCTION_FILE`.
|
||||
|
||||
**If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`.
|
||||
**If `project_exists` is true and `init_incomplete` is true (#4040 — interrupted bootstrap):** Resume initialization instead of erroring. `.planning/` exists but initialization stopped before all core artifacts landed. Keep the existing `PROJECT.md` and any already-created artifacts (`REQUIREMENTS.md` if present, `config.json`); skip the steps that would recreate them and continue the flow from the first missing artifact in init order — `REQUIREMENTS.md` → `ROADMAP.md` + `STATE.md` — until all exist. Do not error and do not bounce the user back to `/gsd:progress` (that routing loop is the #4040 bug).
|
||||
|
||||
**If `project_exists` is true and `init_incomplete` is false:** Error — project already initialized. Use `/gsd:progress`.
|
||||
|
||||
**Git init (#3491 — never nest `.git` inside an existing worktree):**
|
||||
|
||||
|
||||
@@ -18,13 +18,33 @@ INIT=$(gsd_run query init.progress $FORENSIC_PARAM)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Extract from init JSON: `project_exists`, `roadmap_exists`, `state_exists`, `phases`, `current_phase`, `next_phase`, `milestone_version`, `completed_count`, `phase_count`, `paused_at`, `state_path`, `roadmap_path`, `project_path`, `config_path`, `phase_mvp_mode`.
|
||||
Extract from init JSON: `project_exists`, `roadmap_exists`, `state_exists`, `requirements_exists`, `planning_exists`, `milestones_exists`, `init_incomplete`, `phases`, `current_phase`, `next_phase`, `milestone_version`, `completed_count`, `phase_count`, `paused_at`, `state_path`, `roadmap_path`, `project_path`, `config_path`, `phase_mvp_mode`.
|
||||
|
||||
```bash
|
||||
DISCUSS_MODE=$(gsd_run query config-get workflow.discuss_mode --raw 2>/dev/null || echo "discuss")
|
||||
```
|
||||
|
||||
If `project_exists` is false (no `.planning/` directory):
|
||||
**If `init_incomplete` is true (#4040 — interrupted bootstrap):**
|
||||
|
||||
`.planning/` exists but the core initialization artifacts are missing (one or more of `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` were never created — a bootstrap that stopped partway, e.g. after PROJECT.md). This is NOT a new project, NOT a missing STATE.md, and NOT a between-milestones state — do not fall through to any of those routes. Route to initialization recovery:
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
## ⚠ Initialization Incomplete
|
||||
|
||||
A partial `.planning/` was found: project initialization started but stopped before creating all core artifacts (missing: REQUIREMENTS.md, ROADMAP.md, STATE.md — whichever `requirements_exists` / `roadmap_exists` / `state_exists` report as false).
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:new-project` — resumes initialization from the first missing artifact; existing PROJECT.md (and any already-created artifacts) are kept, not regenerated.
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
Exit. (The payload's `init_incomplete` is computed with the between-milestones archive case excluded — `MILESTONES.md` present means missing ROADMAP/REQUIREMENTS is archival, and that state still routes to Route F below.)
|
||||
|
||||
If `init_incomplete` is false and `planning_exists` is false and `project_exists` is false (no `.planning/` directory at all):
|
||||
|
||||
```
|
||||
No planning structure found.
|
||||
@@ -36,7 +56,7 @@ Exit.
|
||||
|
||||
If missing STATE.md: suggest `/gsd:new-project`.
|
||||
|
||||
**If ROADMAP.md missing but PROJECT.md exists:**
|
||||
**If ROADMAP.md missing but PROJECT.md exists (and `init_incomplete` is false):**
|
||||
|
||||
This means a milestone was completed and archived. Go to **Route F** (between milestones).
|
||||
|
||||
|
||||
@@ -25,10 +25,12 @@ INIT=$(gsd_run query init.resume)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Parse JSON for: `state_exists`, `roadmap_exists`, `project_exists`, `planning_exists`, `has_interrupted_agent`, `interrupted_agent_id`, `commit_docs`.
|
||||
Parse JSON for: `state_exists`, `roadmap_exists`, `project_exists`, `planning_exists`, `requirements_exists`, `init_incomplete`, `has_interrupted_agent`, `interrupted_agent_id`, `commit_docs`.
|
||||
|
||||
**If `init_incomplete` is true (#4040 — interrupted bootstrap):** `.planning/` exists but initialization never finished — one or more of `REQUIREMENTS.md`, `ROADMAP.md`, `STATE.md` were never created. This is NOT a STATE.md-reconstruction case (there is no project history to reconstruct from). Route to initialization recovery: resume `/gsd:new-project`, which continues from the first missing artifact and keeps the existing PROJECT.md and any already-created artifacts. Do not proceed to load_state.
|
||||
|
||||
**If `state_exists` is true:** Proceed to load_state
|
||||
**If `state_exists` is false but `roadmap_exists` or `project_exists` is true:** Offer to reconstruct STATE.md
|
||||
**If `state_exists` is false but `roadmap_exists` or `project_exists` is true (and `init_incomplete` is false):** Offer to reconstruct STATE.md
|
||||
**If `planning_exists` is false:** This is a new project - route to /gsd:new-project
|
||||
</step>
|
||||
|
||||
|
||||
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')),
|
||||
|
||||
@@ -5155,3 +5155,100 @@ describe('init — GSD_PROJECT scoping (#3964)', () => {
|
||||
assert.equal(out['codebase_dir_exists'], true, 'unscoped probe of the root codebase dir');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// #4040: partial-init routing signal (interrupted bootstrap detection)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('#4040 partial-init completeness fields', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.realpathSync(createFixture());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('#4040 init.progress: interrupted bootstrap (PROJECT.md only) is flagged init_incomplete', () => {
|
||||
// Issue repro: bootstrap died after PROJECT.md + config.json, before
|
||||
// REQUIREMENTS.md / ROADMAP.md / STATE.md.
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\nTest\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), '{}\n');
|
||||
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.planning_exists, true);
|
||||
assert.strictEqual(output.project_exists, true);
|
||||
assert.strictEqual(output.requirements_exists, false);
|
||||
assert.strictEqual(output.roadmap_exists, false);
|
||||
assert.strictEqual(output.state_exists, false);
|
||||
assert.strictEqual(output.milestones_exists, false);
|
||||
assert.strictEqual(output.init_incomplete, true,
|
||||
'interrupted bootstrap must be distinguishable from new project / between milestones');
|
||||
});
|
||||
|
||||
test('#4040 init.progress: complete project is not init_incomplete', () => {
|
||||
writePlanningDocs(tmpDir); // STATE.md + ROADMAP.md + REQUIREMENTS.md
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\nTest\n');
|
||||
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.requirements_exists, true);
|
||||
assert.strictEqual(output.init_incomplete, false);
|
||||
});
|
||||
|
||||
test('#4040 init.progress: between-milestones archive state is not init_incomplete', () => {
|
||||
// milestone.complete archives ROADMAP (and REQUIREMENTS) but leaves
|
||||
// MILESTONES.md + STATE.md — Route F territory, NOT a partial bootstrap.
|
||||
writePlanningDocs(tmpDir, { roadmap: false, requirements: false });
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\nTest\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), '# Milestones\n\n## v1.0\n');
|
||||
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.milestones_exists, true);
|
||||
assert.strictEqual(output.init_incomplete, false,
|
||||
'an archived milestone (MILESTONES.md present) must keep the between-milestones route');
|
||||
});
|
||||
|
||||
test('#4040 init.progress: empty .planning (config only) is init_incomplete, not "no planning"', () => {
|
||||
// Bootstrap that died before even PROJECT.md: .planning/ exists, so the
|
||||
// workflow must not claim "no planning structure found".
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), '{}\n');
|
||||
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.planning_exists, true);
|
||||
assert.strictEqual(output.project_exists, false);
|
||||
assert.strictEqual(output.init_incomplete, true);
|
||||
});
|
||||
|
||||
test('#4040 init.resume: interrupted bootstrap flagged init_incomplete', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\nTest\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), '{}\n');
|
||||
|
||||
const result = runGsdTools('init resume', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.requirements_exists, false);
|
||||
assert.strictEqual(output.init_incomplete, true,
|
||||
'resume must route to initialization recovery, not STATE.md reconstruction');
|
||||
});
|
||||
|
||||
test('#4040 init.new-project: interrupted bootstrap flagged init_incomplete', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\nTest\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), '{}\n');
|
||||
|
||||
const result = runGsdTools('init new-project', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.init_incomplete, true,
|
||||
'new-project gate must resume a partial bootstrap instead of erroring');
|
||||
});
|
||||
});
|
||||
|
||||
95
tests/partial-init-routing.test.cjs
Normal file
95
tests/partial-init-routing.test.cjs
Normal file
@@ -0,0 +1,95 @@
|
||||
// allow-test-rule: source-text-is-the-product (see #4040)
|
||||
// Workflow .md files — their text IS what the runtime loads. Testing text
|
||||
// content tests the deployed contract. Per CONTRIBUTING.md exception matrix.
|
||||
|
||||
/**
|
||||
* GSD Tools Tests - #4040 partial-init routing contract
|
||||
*
|
||||
* Validates that the progress / resume-project / new-project workflows route
|
||||
* an interrupted bootstrap (`init_incomplete` from the init.progress /
|
||||
* init.resume payload) to initialization RECOVERY, and that this branch runs
|
||||
* BEFORE the branches that mis-classified the state pre-fix:
|
||||
* - progress.md Route F ("between milestones": PROJECT.md present,
|
||||
* ROADMAP.md missing)
|
||||
* - progress.md "no planning structure found" (project_exists=false while
|
||||
* .planning/ exists)
|
||||
* - resume-project.md "reconstruct STATE.md"
|
||||
* - new-project.md "project already initialized" error
|
||||
*
|
||||
* Closes: #4040
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('#4040 partial-init routing', () => {
|
||||
const workflowsDir = path.join(__dirname, '..', 'gsd-core', 'workflows');
|
||||
const progressMd = () => fs.readFileSync(path.join(workflowsDir, 'progress.md'), 'utf8');
|
||||
const resumeMd = () => fs.readFileSync(path.join(workflowsDir, 'resume-project.md'), 'utf8');
|
||||
const newProjectMd = () => fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8');
|
||||
|
||||
test('progress.md routes init_incomplete to initialization recovery before Route F', () => {
|
||||
const content = progressMd();
|
||||
const recoveryIdx = content.indexOf('init_incomplete');
|
||||
assert.ok(recoveryIdx > -1, 'progress.md must branch on init_incomplete');
|
||||
const routeFIdx = content.indexOf('Route F: Between milestones');
|
||||
assert.ok(routeFIdx > -1, 'Route F definition must exist');
|
||||
assert.ok(
|
||||
recoveryIdx < routeFIdx,
|
||||
'the init_incomplete recovery branch must appear before the Route F (between milestones) definition'
|
||||
);
|
||||
const recoverySlice = content.slice(recoveryIdx, recoveryIdx + 1200);
|
||||
assert.ok(
|
||||
/REQUIREMENTS\.md/.test(recoverySlice) && /ROADMAP\.md/.test(recoverySlice) && /STATE\.md/.test(recoverySlice),
|
||||
'the recovery branch must name the missing REQUIREMENTS.md / ROADMAP.md / STATE.md artifacts'
|
||||
);
|
||||
assert.ok(
|
||||
recoverySlice.includes('new-project'),
|
||||
'the recovery branch must route to /gsd:new-project to resume initialization'
|
||||
);
|
||||
});
|
||||
|
||||
test('progress.md init_context no-planning branch is gated on planning_exists', () => {
|
||||
const content = progressMd();
|
||||
const noPlanningIdx = content.indexOf('No planning structure found');
|
||||
assert.ok(noPlanningIdx > -1, 'no-planning branch must exist');
|
||||
const branchSlice = content.slice(Math.max(0, noPlanningIdx - 600), noPlanningIdx);
|
||||
assert.ok(
|
||||
branchSlice.includes('planning_exists'),
|
||||
'the no-planning branch must check planning_exists so an existing-but-partial .planning/ is not reported as absent'
|
||||
);
|
||||
});
|
||||
|
||||
test('resume-project.md routes init_incomplete to initialization recovery before STATE reconstruction', () => {
|
||||
const content = resumeMd();
|
||||
const recoveryIdx = content.indexOf('init_incomplete');
|
||||
assert.ok(recoveryIdx > -1, 'resume-project.md must branch on init_incomplete');
|
||||
const reconstructIdx = content.indexOf('Offer to reconstruct STATE.md');
|
||||
assert.ok(reconstructIdx > -1, 'STATE reconstruction branch must exist');
|
||||
assert.ok(
|
||||
recoveryIdx < reconstructIdx,
|
||||
'the init_incomplete recovery branch must appear before the reconstruct-STATE.md branch'
|
||||
);
|
||||
const parseIdx = content.indexOf('Parse JSON for');
|
||||
const parseSlice = content.slice(parseIdx, parseIdx + 400);
|
||||
assert.ok(
|
||||
parseSlice.includes('init_incomplete'),
|
||||
'the resume parse list must include init_incomplete (payload is the source of truth)'
|
||||
);
|
||||
});
|
||||
|
||||
test('new-project.md resumes initialization when init_incomplete instead of erroring', () => {
|
||||
const content = newProjectMd();
|
||||
const gateIdx = content.indexOf('project_already');
|
||||
const gateText = 'If `project_exists` is true';
|
||||
const gate = gateIdx > -1 ? gateIdx : content.indexOf(gateText);
|
||||
assert.ok(gate > -1, 'the project_exists gate must exist');
|
||||
const gateSlice = content.slice(gate, gate + 1500);
|
||||
assert.ok(
|
||||
gateSlice.includes('init_incomplete'),
|
||||
'the project_exists gate must discriminate on init_incomplete (resume a partial bootstrap instead of erroring)'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user