From 2e9a50edccfd36e42f08d2de2e340c60ded8876f Mon Sep 17 00:00:00 2001 From: SJ Lee Date: Tue, 22 Sep 2026 14:24:38 +0000 Subject: [PATCH] fix(orient): recover Progress and in-flight pointers for scenarios 1 and 8 (Closes #490) --- skills/dev-backlog/SKILL.md | 2 +- tests/dev-backlog/sprint-state.test.js | 77 ++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/skills/dev-backlog/SKILL.md b/skills/dev-backlog/SKILL.md index 9126411..a91013d 100644 --- a/skills/dev-backlog/SKILL.md +++ b/skills/dev-backlog/SKILL.md @@ -93,7 +93,7 @@ everything else yourself. Goal: know where execution stands and what comes next. Rail: `status.sh` / `next.sh` (`--json`; `--track ` for one track of a portfolio) for `latest_progress`, `next_batch`, and `in_flight`. `_context.md` and the active sprint file are the readable picture. -Done when you can name the next live Issue and, when a sprint exists, its latest Progress (or its absence), current state, and next actionable `[ ]` batch (`next_batch`; `[ ]` selection is not `[~]` work and is not dependency authorization). +Done when, before acting, you report all of: the next live Issue; the latest recorded Progress entry or its absence (`latest_progress` empty); the rail's `next_batch` (the first `[ ]` item's batch, or every `[ ]` item when the Plan has no batch headings — never an earlier `[~]`-only batch); and every in-flight PR/branch pointer or `unmoored` (`in_flight`). Naming `next_batch` is selection only: `[ ]` is not `[~]` work, and selection is not dependency authorization. Recovery: name every in-flight PR/branch pointer (or `unmoored`) from the JSON rail and any ownership recorded in the active sprint (unknown if absent); when the declared authority is unavailable, treat local orientation as last-recorded/provisional (the next live Issue stays unknown) and stop before dispatch, Plan mutation, execution, or AC/lifecycle claims. ### Create diff --git a/tests/dev-backlog/sprint-state.test.js b/tests/dev-backlog/sprint-state.test.js index 0972493..467438f 100644 --- a/tests/dev-backlog/sprint-state.test.js +++ b/tests/dev-backlog/sprint-state.test.js @@ -417,6 +417,83 @@ started: 2026-06-30 }); }); +describe("Orient recovery rail (scenarios 1 and 8, #490)", () => { + it("selects the first [ ] batch as next_batch, distinct from [~] work, and reports latest Progress or its absence", () => { + const state = parseSprintContent({ + sprintPath: ".dev-backlog/sprints/scenario-1.md", + content: `--- +status: active +started: 2026-07-01 +--- + +## Plan +### Batch 1 — Done +- [x] #1 Landed + +### Batch 2 — In flight +- [~] #2 Running → PR #20 (reviewing) + +### Batch 3 — Next +- [ ] #3 Selected only +- [ ] #4 Also selected + +## Progress +- 2026-07-02: #2 dispatched → PR #20 +`, + today: new Date("2026-07-03T00:00:00Z"), + }); + + // next_batch is the first [ ] batch, never the earlier [~]-only batch. + assert.equal(state.next_batch.heading, "### Batch 3 — Next"); + assert.deepEqual(state.next_batch.items.map((item) => item.ref), ["#3", "#4"]); + assert.deepEqual(state.next_batch.items.map((item) => item.state), ["todo", "todo"]); + // [ ] selection is distinct from [~] work. + assert.deepEqual(state.in_flight.map((item) => item.ref), ["#2"]); + assert.equal(state.in_flight[0].state, "in_flight"); + assert.deepEqual(state.latest_progress.map((entry) => entry.line), [ + "- 2026-07-02: #2 dispatched → PR #20", + ]); + + // Absence is explicit: an empty latest_progress, not a missing field. + const noProgress = parseSprintContent({ + sprintPath: ".dev-backlog/sprints/scenario-1-empty.md", + content: "---\nstatus: active\n---\n\n## Plan\n- [ ] #5 Todo\n\n## Progress\n", + today: new Date("2026-07-03T00:00:00Z"), + }); + assert.deepEqual(noProgress.latest_progress, []); + assert.equal(noProgress.next_batch.heading, null); + assert.deepEqual(noProgress.next_batch.items.map((item) => item.ref), ["#5"]); + }); + + it("recovers every in-flight PR/branch pointer, or unmoored, from the JSON rail", () => { + const state = parseSprintContent({ + sprintPath: ".dev-backlog/sprints/scenario-8.md", + content: `--- +status: active +started: 2026-07-01 +--- + +## Plan +- [~] #1 With a PR → PR #11 (open) +- [~] #2 With a branch [branch:feat/two] +- [~] #3 With no pointer + +## Progress +`, + today: new Date("2026-07-03T00:00:00Z"), + }); + + assert.deepEqual( + state.in_flight.map((item) => [item.ref, item.pr, item.branch, item.unmoored]), + [ + ["#1", { number: 11, state: "open" }, null, false], + ["#2", null, "feat/two", false], + ["#3", null, null, true], + ] + ); + }); +}); + const BATCHED_SPRINT = `--- milestone: Text Sprint status: active