Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/dev-backlog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ everything else yourself.

Goal: know where execution stands and what comes next.
Rail: `status.sh` / `next.sh` (`--json`; `--track <slug>` 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
Expand Down
77 changes: 77 additions & 0 deletions tests/dev-backlog/sprint-state.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading