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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ and excessive permissions fail it, and every other finding is shown. |
| [**roadmap**](docs/commands/roadmap.md) | `devops roadmap intake [OPTIONS]` | Turn each open issue with no priority label into a roadmap item, or file a new candidate: intake sets the labels, adds a reason comment and names a possible duplicate for a person to close. It writes nothing without --confirm. Details: docs/roadmap/intake.md. |
| | `devops roadmap refine [OPTIONS]` | Refine roadmap items to Ready: investigate open questions with tools that read the code and the documentation, then write the design, tasks and acceptance criteria to GitHub. Without --confirm, refine prints its plan only. Details: docs/roadmap/refine.md. |
| | `devops roadmap alternatives [OPTIONS]` | Check open items for an open, maintained tool or library that already covers their need. A check posts one comment and a body block, and a person decides what the tool means for the item. Details: docs/roadmap/alternatives.md. |
| | `devops roadmap retitle [OPTIONS]` | Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. Without --confirm, it prints the new titles and writes nothing. |
| | `devops roadmap retitle [OPTIONS]` | Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. An item that the job leaves gets the mark retitle.skipped, and the job does not select it again until its title, body or plan pull request changes. Without --confirm, it prints the new titles and writes nothing. |
| | `devops roadmap stage [OPTIONS]` | Make Ready items Staged: for each item, push a branch with its OpenSpec change in one commit and open a draft plan pull request. The Service marks the draft ready when its checks pass. Details: docs/roadmap/stage.md. |
| | `devops roadmap run [OPTIONS]` | Run the roadmap jobs that are due, in order: metrics, then intake, refine and the alternatives check, and record each one's last success. Intake is due on an issue reopened, and decides at most 5 candidates a run and keeps a record of those it left beside the schedule; refine is due every 31 minutes, on a first run, and after an intake that placed a critical fix or a P0 item; retitle is due every 61 minutes and on a first run; the alternatives check is due every 719 minutes and on a first run, and checks at most roadmap.alternatives.first_pass_limit items while an item has no check, and roadmap.alternatives.limit items after. Without --confirm, or with --dry-run, prints the due list and runs nothing. |
| | `devops roadmap implement [OPTIONS]` | Implement a Staged or Planned item in bounded steps: with --group, run one task group in a checkout; without it, run the job, which takes items, runs their steps, and marks the pull request ready. Details: docs/roadmap/implement.md. |
Expand Down
2 changes: 1 addition & 1 deletion docs/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6721,7 +6721,7 @@ devops roadmap alternatives [OPTIONS]

### `devops roadmap retitle`

**Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. Without --confirm, it prints the new titles and writes nothing.**
**Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. An item that the job leaves gets the mark retitle.skipped, and the job does not select it again until its title, body or plan pull request changes. Without --confirm, it prints the new titles and writes nothing.**

```bash
devops roadmap retitle [OPTIONS]
Expand Down
1 change: 1 addition & 0 deletions docs/ROADMAP_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ These are all the keys of the job record, by the job that writes them:
| Refine | `refine.tries`, `refine.failed_check`, `refine.failure` | The count of rounds whose job failed on the same body, the names of the failed checks, and the last error. `refine.next_step` is then `retry`, and `failed` at 3 tries (`CONST_ROADMAP_REFINE_MAX_TRIES`). Only a decision marked `(a person decides)` makes refine wait on a person |
| Refine | `refine.comments_seen`, `refine.commented_at`, `refine.wait_refs` | What the last read of the comments of a waiting item gave: the count of comments of the issue listing, the time of refine's last comment, and the issues and paths that the wait text names. A round reads the comments again only when the count moves |
| Alternatives | `alternatives.checked_at`, `alternatives.body_hash`, `alternatives.verdict`, `alternatives.tool`, `alternatives.failed_at`, `alternatives.failed_error` | When the check ran, the text it read, its verdict and the tool it found; when the last check failed and the class of its error, until a check is written |
| Retitle | `retitle.skipped` | A hash of the title, the body and the `stage.pr` mark that the retitle job read when it left an Item with a long title (a person's edit, no plan title, or no usable reply of the model). The job writes it once, and does not select the Item again while the hash holds. A change to the title, the body or `stage.pr` selects the Item again; a comment does not |
| Stage | `stage.branch`, `stage.pr`, `stage.sha` | The staged branch, its draft pull request and its last commit |
| Stage | `stage.groups`, `stage.needs_split`, `stage.file_cap` | The task groups with their size, the group over the budget, and the cap of that measure |
| Stage | `stage.ready` | The plan pull request that the Service has seen leave the draft state |
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ devops roadmap alternatives [OPTIONS]

## `devops roadmap retitle`

**Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. Without --confirm, it prints the new titles and writes nothing.**
**Shorten the title summary of open items that is over 60 characters. The job keeps the type and the scope. A Planned item takes the summary of its plan pull request title and needs no model call; another item gets one model call. The job leaves a title that a person changed after the last job comment. An item that the job leaves gets the mark retitle.skipped, and the job does not select it again until its title, body or plan pull request changes. Without --confirm, it prints the new titles and writes nothing.**

```bash
devops roadmap retitle [OPTIONS]
Expand Down
1 change: 1 addition & 0 deletions docs/roadmap/intake.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ devops gh issues close <number> --reason not-planned
- A Planned item takes the summary of the title of its plan pull request (`plan #<issue> <summary>`), which its job record names. The job makes no model call for it. When that title gives no valid summary, the job leaves the item and comments the reason once.
- Another item gets one request to the `triage` model, and one more with the faults of the first reply. A reply that fails twice leaves the item.
- It leaves a title that someone changed after the last comment of a job. It reads the title changes on the timeline of the item.
- When it leaves an item, it writes the mark `retitle.skipped` once. The value is a hash of the title, the body and the `stage.pr` mark. The selection leaves the item out while the hash holds, so a later round reads nothing of it and makes no model request for it. A change to the title, the body or `stage.pr` selects it again. A comment does not.
- Without `--confirm` it prints the new titles and writes nothing. With `--confirm` it writes each title and one comment with the marker `<!-- devops roadmap retitle -->`. `--dry-run` makes no request.
- The Service runs the job as the row `retitle` of the model lane. The row is due on a first run and every 61 minutes. It runs after `refine` and before `stage`, so the stage job reads the new titles in the same round. Each run takes at most 25 items and writes as `--confirm` does. The row needs no clone.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-07
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Design

## Decisions

- **Hash the whole body of the item, as `Item.body` holds it.**: The issue says "the body". `Item.body` has the record block taken out, so the write of the mark does not change the hash. A rewrite of the refine section or of the alternatives block returns the item to the selection once. That costs one decision and keeps one rule that a reader can state.
- **Record a model failure as a skip.**: The model gave no title that passes the form in two tries. A third try on the same text costs 2 model calls and gives the same result in most cases. The item returns when the title, the body or `stage.pr` changes.
- **Record the mark for a person's edit too.**: The issue asks for it. The decision reads the comments and the title changes, and neither changes while the title and body stay the same.
- **Write the mark in `apply_retitle`, once for each `TitleSkip`, with `store.set_marks`.**: `apply_retitle` is the one write point of the job, and `plan_retitle` writes nothing. A `ModelGatewayUnreachableError` raises in the plan, before the apply step, so a gateway fault records no mark and the item stays in the selection.
- **Make `long_title_items` do the check.**: The selection is where the item holds a place of the limit. A check inside `_decide` would still count the item against the limit.
- **Use `StageRecordKey.PR` for the plan input.**: The plan pull request is the only source of a title for a Planned item, so its number is the one mark that the decision reads.
- **Add the key family before the failure-class issues of #3027.**: Those issues extend the job record, and this change gives them the union that they extend.

## Rejected alternatives

- **Hash `text_of_a_person(item.body)`.**: That text leaves out the refine section and the alternatives block, so a round of those jobs would not return the item. The model reads the first characters of the whole body (`_request`), so a change in a section can change the reply. The issue names "the body".
- **Keep the skips in memory for the run.**: The job runs every 61 minutes as a new process, so a memory set is empty at the next round. The mark must outlive the run.
- **A timestamp or a retry count in the mark.**: The issue asks for a hash of the inputs. A timestamp returns the item with no change of its inputs and costs the reads again.
- **Skip the mark for a plan pull request with no usable title.**: The reason changes only when the plan pull request or its title changes. The item already carries a comment for it. A mark ends the repeated reads.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Proposal

## Why

`devops roadmap retitle` records no mark for an item that it leaves (#4038). `long_title_items` selects every open item with a long title summary, the lowest number first, and `plan_retitle` takes the first 25. An item that the job cannot shorten is taken again in every round, which runs every 61 minutes. Each round reads the comments and the title changes of the item, and for an item that is not Planned it makes up to 2 model calls. These items hold the places of items with higher numbers. #3630 found 34 Ready items that the title rule left out, so this row blocks the stage job.

The job leaves an item for one of three reasons: a person changed the title, the plan pull request has no usable title, or the model gave no summary that passes the title form in two tries.

## What Changes

- Add the record key family `RetitleRecordKey` with the key `retitle.skipped` to `src/devops_cli/roadmap/stores/store.py`. Add it to the `JobRecord` union, to the parsed families `_JOB_RECORD_KEYS` and to `__all__`.
- Add `skip_hash(item)` to `src/devops_cli/roadmap/retitle.py`. It is the SHA-256 hash (`hash_text` of `body_hash.py`) of the title, the body and the `stage.pr` mark of the item.
- Make `long_title_items` leave out an item whose `retitle.skipped` equals `skip_hash(item)`. The item returns to the selection when its title, its body or its plan pull request changes.
- Make `apply_retitle` write the mark once for each `TitleSkip`, with one `set_marks` call. This covers all three reasons, a person's edit included. A model call that cannot reach the gateway raises before the apply step, so it records no mark.
- Add the write of the mark to `retitle_requests` in `src/devops_cli/roadmap/request_plan.py`, with its condition text in `src/devops_cli/lang/`, so that the dry run lists it.
- Add the key family to the table of record keys in `docs/ROADMAP_WORKFLOW.md`.
- The mark is a hash only. It has no link to the failure class table.
- The change alters the output of `devops roadmap retitle --dry-run` and the selection of the job, which a consumer observes, so it carries a spec delta (`roadmap-retitle`) and `skip_specs` is false.
- The change fits one pull request.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `roadmap-retitle`: Skip mark, Skipped item returns when its inputs change, Cost of a skipped item

## Impact

- Code: `src/devops_cli/roadmap/stores/store.py`, `src/devops_cli/roadmap/retitle.py`, `src/devops_cli/roadmap/request_plan.py`, `src/devops_cli/lang/en/messages.py`
- Documents: `docs/ROADMAP_WORKFLOW.md`
- Tests: `tests/roadmap/test_roadmap_retitle_skip.py` (new)

## Feasibility

- Read at origin/main, in the worktree: `retitle.py` has `long_title_items`, which takes every open item with a long summary, and `plan_retitle`, which takes the first `DEFAULT_ROADMAP_RETITLE_LIMIT` (25) of them. `_decide` reads `comments_on` and `last_renamed_at` for each of them, and `_model_title` makes up to `_ATTEMPTS` (2) model calls. A `TitleSkip` is only reported, and commented when `comment=True`. Nothing records it, so the premise of the issue holds.
- `stores/store.py` lists `ItemField | RefineRecordKey | AlternativesRecordKey | StageRecordKey | ImplementRecordKey` in `JobRecord`, and the same families in `_JOB_RECORD_KEYS`. No `RetitleRecordKey` exists.
- `RoadmapStore.set_marks(item, marks)` writes marks in one call and changes no field. `open_items()` returns each item with its `job_record` and its `body` with the record block taken out, so the check needs no extra read and the write of the mark does not change the body.
- `body_hash.hash_text` gives the SHA-256 hash, so no new library is needed.
- No merged or open pull request and no newer issue or ADR supersedes the item: #3535, #3630 and #3811 added the row and the schedule only, and no open pull request touches `retitle.py`.
- Check of the new tests against the real code: `uv run devops ci test tests/roadmap/test_roadmap_retitle_skip.py` fails on the unchanged selection (15 of 17 tests fail on the call; the 2 that pass are controls that hold with no mark). `uv run devops ci fail-to-pass --base origin/main` is the proof for the pull request.
- Risks settled in `design.md`: the hash covers the whole body, a model failure is recorded as a skip, and the key family lands before the failure-class issues of #3027.

## Existing Tool Considered

The record marks of the item job record and `hash_text` of `body_hash.py` cover the need: the job record is the open, in-repository store for per-item state, and refine, alternatives and stage use it the same way. No library is needed. The change builds one small function (`skip_hash`) and one key family. Cost: about 35k tokens and 2 h to build, about 2k tokens per quarter to keep, and about 8k tokens and 0.5 h to fix. The change deletes no internal code.

## Dependencies

None.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## ADDED Requirements

### Requirement: Skip mark
When the repair pass leaves an item with a long title, it SHALL write the mark `retitle.skipped` in the job record of the item, once, with one write. The value SHALL be a hash of the title, the body and the `stage.pr` mark that the decision read. This covers an item whose title a person changed, a Planned item without a usable plan title, and an item for which the model gave no summary that passes `title_problems` in two tries. The pass SHALL write no mark for an item that it retitles.

#### Scenario: The model gives no usable summary
- **WHEN** both replies of the model fail `title_problems` and the pass runs with `--confirm`
- **THEN** the job record of the item holds `retitle.skipped`, and the pass wrote that mark once

#### Scenario: A person changed the title
- **WHEN** a person renamed an item after the last job comment and the pass runs with `--confirm`
- **THEN** the job record of the item holds `retitle.skipped`

#### Scenario: The title is changed
- **WHEN** the pass retitles an item
- **THEN** the pass writes no `retitle.skipped` for it

### Requirement: A skipped item is not selected again
The selection of the repair pass SHALL leave out an open item whose `retitle.skipped` equals the hash of its current title, body and `stage.pr` mark. It SHALL select the item again when its title, its body or its `stage.pr` mark changes. A comment on the item SHALL NOT select it again. A skipped item SHALL NOT hold a place of `--limit`.

#### Scenario: Next round after a skip
- **WHEN** the pass skipped an item and runs again with no change to the item
- **THEN** the selection does not hold the item, and a later item with a long title takes the place of the limit

#### Scenario: The item changes
- **WHEN** the title, the body or the `stage.pr` mark of a skipped item changes
- **THEN** the selection holds the item again

### Requirement: A skipped item costs one write once
A skipped item SHALL cost one write in the round that skips it. In a later round with no change to the item, it SHALL cost no read of its comments, no read of its title changes, no read of a pull request title and no model request.

#### Scenario: Two rounds
- **WHEN** the pass runs twice with `--confirm` on one item that it cannot shorten
- **THEN** the first round makes one write of the mark and the second round makes no write, no read of the item and no model request

### Requirement: Dry run lists the write of the mark
The dry run of the repair pass SHALL list the write of the mark `retitle.skipped` with the condition under which it runs.

#### Scenario: Preview of the writes
- **WHEN** the repair pass runs with `--dry-run`
- **THEN** the planned writes hold one write of the body of an item with a condition for a skipped item
Loading
Loading