Skip to content

TASK-135 — an existing row can change track - #29

Open
ranjiao wants to merge 9 commits into
feat/work-modesfrom
coding/task-135-move-row-to-track
Open

TASK-135 — an existing row can change track#29
ranjiao wants to merge 9 commits into
feat/work-modesfrom
coding/task-135-move-row-to-track

Conversation

@ranjiao

@ranjiao ranjiao commented Aug 20, 2026

Copy link
Copy Markdown
Owner

The gap

--track was accepted by exactly two commands: add, at creation, and route, which turns an intake row into a task. There was no perry-task track <id>, and status --track is refused.

So a project that declared a second track started it empty and could not move any existing work onto it. This repository declared intake (mode queue) on 2026-08-20 and it is still empty, while six rows that genuinely arrived rather than being decomposed belong on it and could not get there. A track with no rows is not a mode running on a live track — which is why the declaration did not meet KR-O1.1.

Source: perry/evidence/2026-08/TASK-133-track-experiment.md, spec perry/evidence/2026-08/TASK-135-spec.md.

What ships

perry-task track <ID> --track <name> [--stage S] [--arrived YYYY-MM-DD] [--reason "…"]

Onto a queue track it stamps what route stampsArrived, and the track's first post-intake Stage via the same entry_stage that add and route call. today − Arrived is the number every SLA check measures, so a second entrance that omitted it would make Arrived mean one thing per entrance and silently exempt every moved row from the only clock governing it.

An existing Arrived is carried, never restamped. A queue → queue move that reset the clock to today would erase an in-flight breach — the same exemption, arriving through the other door. --arrived overrides.

Off a track that reads neither, Stage / Stage since / Arrived are cleared, and what they held is recorded in the journal line and the event (stage_from, arrived_from). This is deliverable 2's decision, and it is drop's precedent applied one field down. Arrived is not provenance, it is a queue's clock, and one left on a row no queue governs is a live-looking number the next SLA reader picks up. It is already load-bearing in the negative direction and measured: cmd_route had to stop writing Arrived onto pipeline rows precisely because a non-empty arrived hides the row from rows_with_no_computable_age — the finding that says "this row has no clock at all". Keeping it would suppress the one report that could notice the move. The journal is append-only, so the cell goes and the fact does not.

An undeclared track is refused by name, in check_role's shape:

perry-task: refused — track 'ops' is not declared in `.perry/config.md § Tracks`.
Declared: main, intake, press. Add a row to that table naming its mode, or name
one of those. Nothing was written

It used to print a Python list repr and stop, which names the set and not the way forward. Nothing is created — a tool that invented the track would turn a typo into a second track every count then reports as real.

The two seams that would have half-landed the write

  • commit's changed map carries ("track", "stage", "stage_since", "arrived"). Listing only track would land the new track in the store beside yesterday's clock; the board is rendered from the store, so the move would half-revert on the next render. Asserted against perry-tasks render --write.
  • widen_row_section asks the widening question for both section shapes. Board.find returns P0/P1/P2 or a project's own heading, and the two widenings live on different methods — cmd_stage widens only in the first case, so on the board shape --group exists for a stamp is dropped silently at exit 0. A track move writes four such cells.

Verification — the spec's six items

  1. Six rows move. TestSixRowsMove — six existing rows reach a declared queue track, the board renders Arrived and Stage, and perry-state --json reports stage_counts: {"triaged": 6} where it reported {} before.
  2. The refusal is by name. TestTheRefusalIsByName — names the track, lists the declared ones, exit 1 with no traceback, creates no track and writes no event.
  3. Both directions of the field question. TestBothDirectionsOfTheFieldQuestion — onto a queue track, then off it, asserting the store record both ways, plus the row that already had an Arrived before the move.
  4. An event per move. TestAnEventPerMove — one track event per move, field: "track", reaching both timeline[] and perry-task events.
  5. No drift. TestTheProjectionAgreesAfterAMoveperry-lint --json reports no store-drift finding after a move, and after a move that clears the clock, which is the direction a renderer silently skips.
  6. python3 tests/parallel -j 4, bash tests/run, python3 bin/perry-lint, git diff --check — all below.

Tests

baseline (6893da6) after
python3 tests/parallel -j 4 65 modules · 1930 tests · 1 red 66 modules · 1960 tests · 1 red

The one red is test_diagnose with user_load.dangling non-empty — TASK-126, fixed on unmerged PR #22, inherited by every worktree cut from this branch. Identical before and after; nothing else is red. bash tests/run agrees. perry-lint: 0 errors, 3 pre-existing NS-01 warnings, 0 rows drifted. git diff --check clean.

Out of scope, and honoured

git diff -- perry/ is empty. The six live rows are not moved — this ships the tool; using it on this project's board is a separate act the user takes.

Note on the file count

feat/work-modes is ahead of origin, so this PR carries those commits until the branch is pushed. This row's own code diff is 6 files, +713 / −18: bin/perry-task, tests/test_track_move.py (new, 30 tests), tests/test_prioritize.py, schema/task-list-contract.md, schema/events-list-contract.md, work/reference/subcommands.md.

The contract-doc edits were not optional: EVENT_FIELD gains a key, and tests/test_prioritize.py binds that map's size to a number written in the contract's prose. That guard hardcoded of the fourteen in its own regex, so the day a fifteenth task event landed the doc could be corrected and the test would stop matching altogether and pass by finding nothing — it now reads both numbers out of the prose and checks the total against TASK_EVENTS.

🤖 Generated with Claude Code

ranjiao and others added 9 commits August 20, 2026 23:38
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TASK-122 corrected the PMO: its prompt said test_diagnose's red had been
fixed on a sibling branch. It has not — PR #22 is unmerged, so every
worktree cut from feat/work-modes inherits it. The agent measured its own
baseline, found the red, and reported rather than absorbing it.

And the list is now three: ZZZ-404 came from the TASK-126 spec and result
records, both written by the PMO. Writing about the self-reference defect
produced another instance of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TASK-141 found two live copies of the startable rule and fixed both. That
corrects TASK-127's record, which called the board-era path dead —
_cmd_list_from_board is called at bin/perry-task:1525.

Its own REL-00 diagnosis was wrong and is corrected in its record: the id
is a literal the PMO wrote into a V5 sign-off, not a truncated match of
REL-002. Verified both ways against the harvester — a file containing
only REL-002 harvests ['REL-002'], and one containing REL-00 harvests
['REL-00'].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--track` was accepted by exactly two commands: `add`, at creation, and
`route`, which turns an intake row into a task. So a project that declared
a second track started it empty and could not move any existing work onto
it — measured on this repository, which declared `intake` (mode `queue`)
and left six rows that genuinely arrived stranded on `main`. A track with
no rows is not a mode running on a live track.

`perry-task track <ID> --track <name>` is the third entrance, and it
stamps what `route` stamps. Onto a queue track it sets `Arrived` and the
track's first post-intake `Stage` through the same `entry_stage` the other
two entrances call, because `today − Arrived` is the number every SLA
check measures and a second entrance that omitted it would make `Arrived`
mean one thing per door. An existing `Arrived` is CARRIED rather than
restamped: a queue → queue move that reset the clock would erase an
in-flight breach, which is the same exemption arriving the other way.

Off a track whose destination reads neither, `Stage` / `Stage since` /
`Arrived` are CLEARED and what they held is written into the journal line
and the event. `Arrived` is not provenance, it is a queue's clock, and one
left on a row no queue governs is a live-looking number the next SLA
reader picks up — `cmd_route` already had to stop writing it onto
non-queue rows because a non-empty `arrived` hides the row from
`rows_with_no_computable_age`. Dropping the cell is not dropping the fact:
that is `drop`'s precedent, one field down.

A track no row of `.perry/config.md § Tracks` declares is refused by name
with the declared ones listed, in `check_role`'s shape — `track_of` used
to print a Python list repr and stop, which names the set and not the way
forward. Nothing is created.

- `commit`'s `changed` map carries all four cells with the track, so the
  store cannot keep yesterday's clock beside the new track and the board,
  which is rendered FROM the store, cannot half-revert on re-render.
- `widen_row_section` asks the widening question for both section shapes.
  `cmd_stage` widens only under `## P0`/`P1`/`P2`, so on the board shape
  `--group` exists for a stamp is dropped silently at exit 0.
- `tests/test_track_move.py`: 30 tests, one class per V3 item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant