diff --git a/.perry/config.md b/.perry/config.md index b528d5c..4a73dfc 100644 --- a/.perry/config.md +++ b/.perry/config.md @@ -8,6 +8,25 @@ - Code repo path: — - Last updated: 2026-08-16 +## Tracks + +| Track | Mode | Spine | Stages | WIP | SLA | Cycle | Default rung | +|---|---|---|---|---|---|---|---| +| main | project | phase/ | — | — | — | — | V3 | +| intake | queue | standing | new→triaged→in_progress→resolved | 6 | 5d | weekly | V3 | + +`intake` carries the work that ARRIVES — a defect an agent found mid-run, a +sibling a sweep turned up, a review finding. It is not decomposed from a goal, +it shows up, and its useful questions are queue questions: what has been +waiting longest, how deep is the backlog, what keeps recurring. + +`main` carries the work that is DECOMPOSED — the phase, its KRs, the rows that +serve them. + +Declared 2026-08-20 as the experiment in TASK-133. `Default rung` is V3 rather +than queue mode's V2 default: an arriving row here is a code defect, and a +resolution note is not evidence that it is fixed. + ## Why the state root is not `.` Perry's own `design/` directory is the **design lane skill** diff --git a/.perry/events.jsonl b/.perry/events.jsonl index 15010d8..b09e948 100644 --- a/.perry/events.jsonl +++ b/.perry/events.jsonl @@ -512,3 +512,66 @@ {"ts": "2026-08-20T19:50:42", "event": "evidence", "id": "TASK-094", "title": "Delete the header rule and the row splitter for the three stores", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md"} {"ts": "2026-08-20T19:50:53", "event": "next", "id": "TASK-050", "title": "One normalization for a header cell, not two", "track": "main", "actor": "agent", "from": "After TASK-094 lands, rescope this task to header handling still required by adoption", "to": "unblocks on PR #20; re-scope to the adoption reader (parse_board/parse_okr with no store, parse_tracks, read_conformance, parse_phase/parse_decisions) — the fifth hardening round should be a mutation harness, not another regex"} {"ts": "2026-08-20T19:50:53", "event": "next", "id": "TASK-067", "title": "The writer can destroy the table it writes to, and perry-lint cannot see it", "track": "main", "actor": "agent", "from": "After TASK-094 and TASK-095 land, retain only foreign-project adoption coverage and the escaped-pipe behavioural corpus", "to": "unblocks on PR #20 but does not become empty: perry-decide still writes DECISIONS.md, perry-goals still writes OKR.md § Commitments in place, perry-migrate still rewrites a stranger files, and ragged-row is still the only catch"} +{"ts": "2026-08-20T19:57:10", "event": "add", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T19:57:10", "event": "depends", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "depends_on": ["TASK-112"], "from": "", "to": "TASK-112"} +{"ts": "2026-08-20T19:57:28", "event": "add", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T19:57:28", "event": "add", "id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "track": "main", "mode": "project", "priority": "P0", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T19:57:28", "event": "add", "id": "TASK-129", "title": "Agent is five strings that do not join, and role has never once been written", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T19:58:40", "event": "depends", "id": "TASK-129", "title": "Agent is five strings that do not join, and role has never once been written", "track": "main", "actor": "agent", "depends_on": ["TASK-128"], "from": "", "to": "TASK-128"} +{"ts": "2026-08-20T19:58:40", "event": "next", "id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "track": "main", "actor": "agent", "from": "—", "to": "user decision: which lane writes .perry/roles/, or the card stays owner user — then the V5 signature"} +{"ts": "2026-08-20T19:58:40", "event": "next", "id": "TASK-129", "title": "Agent is five strings that do not join, and role has never once been written", "track": "main", "actor": "agent", "from": "—", "to": "blocked on TASK-128; the writer cannot be chosen before the signature names the lane"} +{"ts": "2026-08-20T20:00:06", "event": "evidence", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-127-spec.md"} +{"ts": "2026-08-20T20:00:06", "event": "start", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T20:03:57", "event": "evidence", "id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-128-proposal.md"} +{"ts": "2026-08-20T20:03:57", "event": "status", "id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "track": "main", "actor": "agent", "depends_on": [], "from": "not_started", "to": "review", "reason": ""} +{"ts": "2026-08-20T20:13:22", "event": "done", "id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "track": "main", "owner": "User", "role": "", "actor": "agent", "from": "review", "to": "done", "evidence": "evidence/2026-08/TASK-128-proposal.md", "rung": "V5", "signoff": {"signed_by": "Ran Jiao", "signed_on": "2026-08-20", "items": [{"n": 1, "text": "the edit is one table row in SKILL.md § The hand-off contract; goals and decide are byte-identical across it", "provenance": "Perry verified", "disposition": "checked"}, {"n": 2, "text": "SKILL.md goes 20410 → 20457 bytes against the 20480 cap in tests/test_router_budget.py — 23 bytes left", "provenance": "Perry verified", "disposition": "checked"}, {"n": 3, "text": "schema/state-schema.json line 2102 currently reads OWNER IS user, NOT A LANE for .perry/roles/*.md, and becomes wrong on this commit", "provenance": "Perry verified", "disposition": "checked"}, {"n": 4, "text": "tests/test_ownership.py refuses a lane-owned path the contract does not list; it refused this one", "provenance": "Perry verified", "disposition": "checked"}, {"n": 5, "text": "DESIGN-007 § 5.3 already names .perry/agents.jsonl → .perry/roles/*.md; no path was chosen here", "provenance": "Perry verified", "disposition": "checked"}, {"n": 6, "text": "the paragraph under the table still reads Two changes from the previous contract and gains no mention of this one; there is no room", "provenance": "Perry verified", "disposition": "checked"}, {"n": 7, "text": "DESIGN-007 decision #2 — the store is the definition, the card is rendered output — carries a 2026-08-19 signature and is not reopened here", "provenance": "restated — Perry did not verify this", "disposition": "checked"}, {"n": 8, "text": "a hand edit to a role card will read as drift, the same behaviour BOARD.md has had since ADR-007 decision 2", "provenance": "restated — Perry did not verify this", "disposition": "checked"}], "also_checked": "", "counts": {"checked": 8, "accepted on report": 0, "not looked at": 0}}} +{"ts": "2026-08-20T20:13:31", "event": "next", "id": "TASK-129", "title": "Agent is five strings that do not join, and role has never once been written", "track": "main", "actor": "agent", "from": "blocked on TASK-128; the writer cannot be chosen before the signature names the lane", "to": "unblocked: work owns .perry/agents.jsonl → .perry/roles/ as of the 2026-08-20 signature; needs a spec, then dispatch"} +{"ts": "2026-08-20T20:20:46", "event": "status", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "actor": "agent", "depends_on": [], "from": "in_progress", "to": "review", "reason": ""} +{"ts": "2026-08-20T20:20:46", "event": "evidence", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "actor": "agent", "from": "evidence/2026-08/TASK-127-spec.md", "to": "evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md"} +{"ts": "2026-08-20T20:21:00", "event": "add", "id": "TASK-130", "title": "schema README says three contracts and pins goals at a version that shipped two ago", "track": "main", "mode": "project", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:21:00", "event": "add", "id": "TASK-131", "title": "seventeen emitted contract keys are documented nowhere, and now there is a number for it", "track": "main", "mode": "project", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:21:00", "event": "add", "id": "TASK-132", "title": "the parity check cannot see 23 keys because Perry own state leaves four collections empty", "track": "main", "mode": "project", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:29:08", "event": "add", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:29:08", "event": "start", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T20:29:48", "event": "add", "id": "TASK-134", "title": "probe row for the TASK-133 track experiment", "track": "intake", "mode": "queue", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:30:57", "event": "drop", "id": "TASK-134", "title": "probe row for the TASK-133 track experiment", "track": "intake", "owner": "PMO Agent", "role": "", "stage": "triaged", "actor": "agent", "from": "not_started", "to": "dropped", "reason": "probe row for the TASK-133 experiment; it proved a queue-track row carries a KR edge with no friction, and that is recorded in the evidence rather than left as a live row"} +{"ts": "2026-08-20T20:31:13", "event": "add", "id": "TASK-135", "title": "a track can be declared but no existing row can be moved onto it", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:31:13", "event": "add", "id": "TASK-136", "title": "a queue track SLA is parsed, stored and never measured against anything", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:31:13", "event": "add", "id": "TASK-137", "title": "a new queue row is born in the second stage, not the first", "track": "main", "mode": "project", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:31:48", "event": "evidence", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-133-track-experiment.md"} +{"ts": "2026-08-20T20:31:48", "event": "status", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "actor": "agent", "depends_on": [], "from": "in_progress", "to": "review", "reason": ""} +{"ts": "2026-08-20T20:41:42", "event": "add", "id": "TASK-138", "title": "stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached", "track": "intake", "mode": "queue", "priority": "P0", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T20:41:42", "event": "start", "id": "TASK-138", "title": "stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached", "track": "intake", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T20:47:54", "event": "done", "id": "TASK-138", "title": "stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached", "track": "intake", "owner": "Coding Agent", "role": "", "actor": "agent", "from": "in_progress", "to": "done", "evidence": "evidence/2026-08/TASK-133-track-experiment.md", "rung": "V3"} +{"ts": "2026-08-20T20:51:51", "event": "evidence", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-126-spec.md"} +{"ts": "2026-08-20T20:51:51", "event": "next", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "—", "to": "spec written; dispatch pre-flight REFUSED on the fragment `diagnose` — awaiting per-task clearance"} +{"ts": "2026-08-20T21:04:19", "event": "add", "id": "TASK-139", "title": "a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off", "track": "intake", "mode": "queue", "priority": "P2", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T21:04:20", "event": "depends", "id": "TASK-139", "title": "a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off", "track": "intake", "actor": "agent", "depends_on": ["TASK-102"], "from": "", "to": "TASK-102"} +{"ts": "2026-08-20T21:08:52", "event": "add", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "mode": "project", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T21:09:42", "event": "evidence", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-140-spec.md"} +{"ts": "2026-08-20T21:09:42", "event": "next", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "actor": "agent", "from": "—", "to": "spec written; dispatch pre-flight REFUSED on `design/` — a real hit, the row does write a locked design doc"} +{"ts": "2026-08-20T21:16:55", "event": "next", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "spec written; dispatch pre-flight REFUSED on the fragment `diagnose` — awaiting per-task clearance", "to": "CLEARED by the user 2026-08-20 for the `diagnose` escalation hit; ready to dispatch, spec at evidence/2026-08/TASK-126-spec.md"} +{"ts": "2026-08-20T21:16:55", "event": "next", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "actor": "agent", "from": "spec written; dispatch pre-flight REFUSED on `design/` — a real hit, the row does write a locked design doc", "to": "CLEARED by the user 2026-08-20 for the `design/` escalation hit; ready to dispatch, spec at evidence/2026-08/TASK-140-spec.md"} +{"ts": "2026-08-20T21:17:16", "event": "next", "id": "TASK-100", "title": "tasks.jsonl is in no claims[] entry, so a namespace collision on it cannot be reported", "track": "main", "actor": "agent", "from": "merge PR #14; the shape predicate added to bin/perry-lint is outside the declared scope and was flagged for a reviewer", "to": "PR merged; ready to close once origin is merged down and the suite re-run locally"} +{"ts": "2026-08-20T21:17:16", "event": "next", "id": "TASK-111", "title": "a test reads two files outside the repository, so it is green here and red on CI forever", "track": "main", "actor": "agent", "from": "PR #19 review: the corpus is IN_REPO-only and CI-covered; merge after origin is reconciled", "to": "PR merged; ready to close once origin is merged down and the suite re-run locally"} +{"ts": "2026-08-20T21:17:16", "event": "next", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "actor": "agent", "from": "PR #21: KR-O2.4 measures 17, not 0; the contract count is five, not three", "to": "PR merged; ready to close once origin is merged down and the suite re-run locally"} +{"ts": "2026-08-20T21:17:16", "event": "next", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "actor": "agent", "from": "the RFC decision: does Mode become a preset over (spine, flow, rung), or stay four fixed shapes", "to": "PR merged; ready to close once origin is merged down and the suite re-run locally"} +{"ts": "2026-08-20T21:17:16", "event": "next", "id": "TASK-094", "title": "Delete the header rule and the row splitter for the three stores", "track": "main", "actor": "agent", "from": "scope decision: PR #20 leaves 13 splits / 87 resolutions on four storeless BOARD registers, so verification item 1 (0 call sites) is unmet as written", "to": "PR #20 merged but the row does NOT close on it: verification item 1 asked for 0 call sites and BOARD.md keeps 13 splits / 87 resolutions on four storeless registers — needs a scope decision, not a close"} +{"ts": "2026-08-20T21:21:09", "event": "status", "id": "TASK-037", "title": "perry-goals writer", "track": "main", "actor": "agent", "depends_on": [], "from": "blocked", "to": "not_started", "reason": ""} +{"ts": "2026-08-20T21:21:09", "event": "status", "id": "TASK-045", "title": "Retire the runtime tolerance branches, behind the conformance marker", "track": "main", "actor": "agent", "depends_on": [], "from": "blocked", "to": "not_started", "reason": ""} +{"ts": "2026-08-20T21:21:09", "event": "add", "id": "TASK-141", "title": "a row stays blocked after its blockers close, because the stored status masks the computed one", "track": "intake", "mode": "queue", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T21:24:30", "event": "add", "id": "TASK-142", "title": "triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene", "track": "intake", "mode": "queue", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T21:24:30", "event": "next", "id": "TASK-142", "title": "triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene", "track": "intake", "actor": "agent", "from": "—", "to": "design question answered 2026-08-20: it belongs in conformance, which triage already reads at step 0.5 — not as a new triage feature"} +{"ts": "2026-08-20T23:09:32", "event": "add", "id": "TASK-143", "title": "two PRs each green on their own base merged into a red tree, and nothing checked the pair", "track": "intake", "mode": "queue", "priority": "P1", "actor": "agent", "summary": "", "depends_on": [], "from": null, "to": "not_started"} +{"ts": "2026-08-20T23:13:48", "event": "done", "id": "TASK-100", "title": "tasks.jsonl is in no claims[] entry, so a namespace collision on it cannot be reported", "track": "main", "owner": "Coding Agent", "role": "", "actor": "agent", "from": "review", "to": "done", "evidence": "evidence/2026-08/TASK-100-dispatch-2026-08-20-1730.md", "rung": "V3"} +{"ts": "2026-08-20T23:13:48", "event": "done", "id": "TASK-111", "title": "a test reads two files outside the repository, so it is green here and red on CI forever", "track": "main", "owner": "Coding Agent", "role": "", "actor": "agent", "from": "review", "to": "done", "evidence": "evidence/2026-08/TASK-111-dispatch-2026-08-20-1930.md", "rung": "V3"} +{"ts": "2026-08-20T23:13:48", "event": "done", "id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "track": "main", "owner": "Coding Agent", "role": "", "actor": "agent", "from": "review", "to": "done", "evidence": "evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md", "rung": "V3"} +{"ts": "2026-08-20T23:13:48", "event": "done", "id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "track": "main", "owner": "User + Agent", "role": "", "actor": "agent", "from": "review", "to": "done", "evidence": "evidence/2026-08/TASK-133-track-experiment.md", "rung": "V3"} +{"ts": "2026-08-20T23:17:15", "event": "start", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T23:17:15", "event": "start", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T23:17:15", "event": "evidence", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "evidence/2026-08/TASK-126-spec.md", "to": "evidence/2026-08/TASK-126-dispatch-2026-08-21.md"} +{"ts": "2026-08-20T23:17:16", "event": "evidence", "id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "track": "main", "actor": "agent", "from": "evidence/2026-08/TASK-140-spec.md", "to": "evidence/2026-08/TASK-140-dispatch-2026-08-21.md"} +{"ts": "2026-08-20T23:21:36", "event": "start", "id": "TASK-120", "title": "the linkage edges are read but never folded into KR progress", "track": "main", "actor": "agent", "from": "not_started", "to": "in_progress"} +{"ts": "2026-08-20T23:21:36", "event": "evidence", "id": "TASK-120", "title": "the linkage edges are read but never folded into KR progress", "track": "main", "actor": "agent", "from": "—", "to": "evidence/2026-08/TASK-120-spec.md"} +{"ts": "2026-08-20T23:32:27", "event": "status", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "depends_on": [], "from": "in_progress", "to": "review", "reason": ""} +{"ts": "2026-08-20T23:32:28", "event": "evidence", "id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "track": "main", "actor": "agent", "from": "evidence/2026-08/TASK-126-dispatch-2026-08-21.md", "to": "evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md"} diff --git a/SKILL.md b/SKILL.md index 6e838db..75ccb31 100644 --- a/SKILL.md +++ b/SKILL.md @@ -69,7 +69,7 @@ The table is that sentence applied to a file list. It is a **file-ownership** co | Lane | Only writer of | Proposes, never writes | |---|---|---| | **`goals`** (`goals/`) | `OKR.md` — **including `## Commitments`** — and `phase/-.md` | weekly tasks, handed to `work` | -| **`work`** (`work/`) | `BOARD.md` (incl. `## Intake`, `## Cadence`), `journal/`, `PROJECT_STATE.md`, `evidence/`, `weekly/`, `handoff/` | KR attribution edges, handed to `goals` | +| **`work`** (`work/`) | `BOARD.md` (incl. `## Intake`, `## Cadence`), `journal/`, `PROJECT_STATE.md`, `evidence/`, `weekly/`, `handoff/`, **`.perry/agents.jsonl` → `.perry/roles/`** | KR attribution edges, handed to `goals` | | **`decide`** (`decide/`) | `design/-.md`, **`DECISIONS.md` and `decisions/`** | implementation tasks on lock, handed to `work` | **Two changes from the previous contract** — `DECISIONS.md` + `decisions/` moved from `work` to `decide`, and `OKR.md § Commitments` became explicitly `goals`. **The lane names and the directories now agree**, an edit needing no second signature because the ownership set above is byte-identical across it. Both accounts: `reference/hand-off-contract.md`. diff --git a/bin/perry-codex-preflight b/bin/perry-codex-preflight index 59dad77..d984686 100755 --- a/bin/perry-codex-preflight +++ b/bin/perry-codex-preflight @@ -48,6 +48,21 @@ USAGE esac done +mtime_of() { + # See `bin/perry-dispatch-limit § mtime` (TASK-138). GNU `stat -f` is + # `--file-system` and SUCCEEDS, so the `-f || -c` chain never falls through + # on Linux. Try GNU first (BSD rejects `-c` outright) and accept digits only. + local v + v=$(stat -c %Y "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + v=$(stat -f %m "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + echo 0 +} log() { [ "$QUIET" -eq 1 ] || echo "$@"; } warn() { echo "$@" >&2; } fail() { echo "$@" >&2; exit 1; } @@ -81,7 +96,7 @@ mkdir -p "$CACHE_DIR" NOW=$(date +%s) if [ "$FORCE" -eq 0 ] && [ -f "$SMOKE_FILE" ]; then - LAST=$(stat -f %m "$SMOKE_FILE" 2>/dev/null || stat -c %Y "$SMOKE_FILE" 2>/dev/null || echo 0) + LAST=$(mtime_of "$SMOKE_FILE") ELAPSED=$((NOW - LAST)) if [ "$ELAPSED" -lt "$SMOKE_TTL" ]; then log "🟢 codex smoke test cached ($((ELAPSED / 60))m ago, TTL $((SMOKE_TTL / 60))m)" diff --git a/bin/perry-dispatch-limit b/bin/perry-dispatch-limit index be22a93..b9b4930 100755 --- a/bin/perry-dispatch-limit +++ b/bin/perry-dispatch-limit @@ -57,8 +57,24 @@ Cache: ~/.cache/perry/in-flight/-.json USAGE } +# GNU `stat -f` is `--file-system`: it SUCCEEDS and prints a filesystem block, +# so a `-f %m || -c %Y` chain never reaches its fallback on Linux and hands +# `$((now - last))` four lines of prose. That was green on every mac and red on +# every CI run (TASK-138). Two rules, both load-bearing: try the GNU form +# FIRST — `-c` is rejected outright by BSD stat, so it fails cleanly, which +# `-f` does not — and accept an answer only if it is all digits, because an +# exit code is not evidence that the output means what was asked for. mtime() { - stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0 + local v + v=$(stat -c %Y "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + v=$(stat -f %m "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + echo 0 } release_lock() { diff --git a/bin/perry-update-check b/bin/perry-update-check index d1bb2b3..2f81086 100755 --- a/bin/perry-update-check +++ b/bin/perry-update-check @@ -42,6 +42,21 @@ USAGE esac done +mtime_of() { + # See `bin/perry-dispatch-limit § mtime` (TASK-138). GNU `stat -f` is + # `--file-system` and SUCCEEDS, so the `-f || -c` chain never falls through + # on Linux. Try GNU first (BSD rejects `-c` outright) and accept digits only. + local v + v=$(stat -c %Y "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + v=$(stat -f %m "$1" 2>/dev/null) && case "$v" in + ""|*[!0-9]*) : ;; + *) printf '%s\n' "$v"; return 0 ;; + esac + echo 0 +} log() { [ "$QUIET" -eq 1 ] || echo "$@"; } warn() { echo "$@" >&2; } fail() { warn "$@"; [ "$STRICT" -eq 1 ] && exit 1 || exit 0; } @@ -110,7 +125,7 @@ NOW=$(date +%s) if [ "$FORCE" -eq 0 ] && [ -f "$CHECK_FILE" ]; then # Portable mtime: try macOS stat first, fall back to GNU - LAST=$(stat -f %m "$CHECK_FILE" 2>/dev/null || stat -c %Y "$CHECK_FILE" 2>/dev/null || echo 0) + LAST=$(mtime_of "$CHECK_FILE") ELAPSED=$((NOW - LAST)) if [ "$ELAPSED" -lt "$WEEK_SECS" ]; then # Recently checked; bail without noise diff --git a/bin/perry_store.py b/bin/perry_store.py index a67214a..722df17 100644 --- a/bin/perry_store.py +++ b/bin/perry_store.py @@ -265,8 +265,10 @@ def cell_text(field: str, rec: dict, escape: bool = True) -> str: `escape` is False for a slot that is not inside a markdown table — a `- PMO repo path: …` bullet in `.perry/config.md` carries no cell boundaries, and escaping a pipe there would write a backslash the file - never had. It is the ONE thing that differs between a table cell and a - bullet slot, so it is one flag rather than a second function. + never had. "Is this inside a table?" is the ONE question a table cell and + a bullet slot answer differently, so it is one flag rather than a second + function — and it is the same flag `describe_cell` reads to decide whether + a value may be handed padding it did not come with, for the same reason. """ v = rec.get(field, "") if isinstance(v, list) or field == "depends_on": @@ -314,11 +316,28 @@ def describe_cell(raw: str, field: str, rec: dict, escape: bool = True) -> dict: body = raw.strip() lead = raw[:len(raw) - len(raw.lstrip())] trail = raw[len(raw.rstrip()):] + # **A table cell has boundaries; a bullet slot has neighbours** — so only + # the cell may be given padding it did not come with. `render_line` joins + # cells on `|`, a character that carries no whitespace of its own, so a + # cell whose own padding is missing has to be handed some or `|split|` is + # what gets written. A bullet slot is joined on `""` between literal spans + # that already hold every character around it: the span before the slot in + # `- Repo layout: single` is `'- Repo layout: '`, separator space included. + # A space invented there is written TWICE — two after the colon, and a + # trailing one `git diff --check` reports — on a line the tool's own + # refusal message told the reader to run `render --write` to repair. + # `escape` is the same flag that tells a cell from a slot in `cell_text`, + # for the same reason: a slot is not inside a table. + pad = " " if escape else "" if not body: # A whitespace-only cell is ALL padding, and splitting it into a # leading half and a trailing half counts it twice — `| |` came back - # `| |` on every empty `Depends on` cell of Perry's own board. - c = {"f": field, "lead": " ", "trail": " ", "blank": raw} + # `| |` on every empty `Depends on` cell of Perry's own board. A + # bullet slot has only one side to pad against, so its whitespace is + # kept whole and on the left, where the separator it follows is: an + # empty `- Code repo path: ` gains a value without gaining a second + # space and without losing the one it had. + c = {"f": field, "lead": pad or raw, "trail": pad, "blank": raw} if want: c["disagrees"] = body return c @@ -341,7 +360,7 @@ def describe_cell(raw: str, field: str, rec: dict, escape: bool = True) -> dict: at = body.index(want) return {"f": field, "lead": lead, "trail": trail, "p": body[:at], "s": body[at + len(want):]} - return {"f": field, "lead": lead or " ", "trail": trail or " ", + return {"f": field, "lead": lead or pad, "trail": trail or pad, "disagrees": body} diff --git a/perry/BOARD.md b/perry/BOARD.md index 085fda3..4fcf25d 100644 --- a/perry/BOARD.md +++ b/perry/BOARD.md @@ -12,46 +12,62 @@ > locked the same day. Every row cites its design phase. Perry has no `OKR.md`, so > every row is **declared unlinked** — not guessed into a KR. +## Intake + +| Arrived | Request | Outcome | +|---|---|---| + ## P0 (must finish this period) -| ID | Title | Owner | Status | Next action | Evidence | Verification | Depends on | -|---|---|---|---|---|---|---|---| -| TASK-050 | One normalization for a header cell, not two | Coding Agent | blocked | unblocks on PR #20; re-scope to the adoption reader (parse_board/parse_okr with no store, parse_tracks, read_conformance, parse_phase/parse_decisions) — the fifth hardening round should be a mutation harness, not another regex | — | V4 | TASK-094 | -| TASK-067 | The writer can destroy the table it writes to, and perry-lint cannot see it | Coding Agent | blocked | unblocks on PR #20 but does not become empty: perry-decide still writes DECISIONS.md, perry-goals still writes OKR.md § Commitments in place, perry-migrate still rewrites a stranger files, and ragged-row is still the only catch | evidence/2026-08/TASK-067-finding.md | V4 | TASK-094, TASK-095 | +| ID | Title | Owner | Status | Next action | Evidence | Verification | Depends on | Track | Stage | Arrived | +|---|---|---|---|---|---|---|---|---|---|---| +| TASK-050 | One normalization for a header cell, not two | Coding Agent | blocked | unblocks on PR #20; re-scope to the adoption reader (parse_board/parse_okr with no store, parse_tracks, read_conformance, parse_phase/parse_decisions) — the fifth hardening round should be a mutation harness, not another regex | — | V4 | TASK-094 | main | | | +| TASK-067 | The writer can destroy the table it writes to, and perry-lint cannot see it | Coding Agent | blocked | unblocks on PR #20 but does not become empty: perry-decide still writes DECISIONS.md, perry-goals still writes OKR.md § Commitments in place, perry-migrate still rewrites a stranger files, and ragged-row is still the only catch | evidence/2026-08/TASK-067-finding.md | V4 | TASK-094, TASK-095 | main | | | ## P1 | ID | Title | Owner | Status | Next action | Evidence | Verification | Depends on | Track | Stage | Stage since | Arrived | Parent | Commitment | Role | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | TASK-077 | DESIGN-006 F — a finance-shaped role runs one real task end to end | Coding Agent | not_started | Run the finance-shaped role end to end on a copy of gimegime-pmo, then write the extraction report. | evidence/2026-08/TASK-077-context.md | V5 | TASK-073, TASK-075, TASK-076 | main | | | | | | | -| TASK-094 | Delete the header rule and the row splitter for the three stores | Coding Agent | review | scope decision: PR #20 leaves 13 splits / 87 resolutions on four storeless BOARD registers, so verification item 1 (0 call sites) is unmet as written | evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md | V3 | TASK-090, TASK-092 | main | | | | | | | +| TASK-094 | Delete the header rule and the row splitter for the three stores | Coding Agent | review | PR #20 merged but the row does NOT close on it: verification item 1 asked for 0 call sites and BOARD.md keeps 13 splits / 87 resolutions on four storeless registers — needs a scope decision, not a close | evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md | V3 | TASK-090, TASK-092 | main | | | | | | | | TASK-095 | Remove the parser for the three stores; keep what adoption needs | Coding Agent | not_started | — | — | V4 | TASK-094 | main | | | | | | | | TASK-097 | Migrate the two real projects to the store, at V5 | Coding Agent | not_started | — | — | V5 | TASK-092 | main | | | | | | | | TASK-099 | Sweep bin/, viewer/ and tests/ for document handling that ADR-007 made dead | Coding Agent | not_started | — | — | V4 | TASK-095 | main | | | | | | | | TASK-102 | Evidence becomes a typed relation: {path, kind, round}, not one prose cell | Coding Agent | not_started | — | — | V4 | TASK-090, TASK-092 | main | | | | | | | -| TASK-111 | a test reads two files outside the repository, so it is green here and red on CI forever | Coding Agent | review | PR #19 review: the corpus is IN_REPO-only and CI-covered; merge after origin is reconciled | evidence/2026-08/TASK-111-dispatch-2026-08-20-1930.md | V3 | — | main | | | | | | | | TASK-114 | aiMark reads Perry through the current contracts instead of a pin nine versions old | Coding Agent | in_progress | delegated to an aiMark coding agent; awaiting paste-back | evidence/2026-08/TASK-114-delegation-prompt.md | V4 | — | main | | | | | | | | TASK-119 | the linkage graph is documented as machine-written and no tool writes it | Coding Agent | not_started | — | — | V3 | | main | | | | | | | -| TASK-120 | the linkage edges are read but never folded into KR progress | Coding Agent | not_started | — | — | V3 | | main | | | | | | | +| TASK-120 | the linkage edges are read but never folded into KR progress | Coding Agent | in_progress | dispatched to claude-subagent; worktree pinned to 7c0bb99; state-schema.json scoped out so the gate passes without a release | evidence/2026-08/TASK-120-spec.md | V3 | — | main | | | | | | | | TASK-121 | the sweep that found four more live-state assertions runs once and then is thrown away | Coding Agent | not_started | — | — | V3 | | main | | | | | | | | TASK-122 | the repair path the tools advertise leaves the file needing a whitespace fix | Coding Agent | not_started | — | — | V3 | | main | | | | | | | | TASK-123 | the goals writer takes the file as truth and derives the store, which is the opposite direction from the KR | Coding Agent | not_started | — | — | V4 | | main | | | | | | | +| TASK-126 | closing the dangling-id row requires writing the record that re-dangles it | Coding Agent | review | PR #22 — the suite is fully green; verify the strong anti-vacuity case survives review, then close at V3 | evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md | V3 | TASK-112 | main | | | | | | | +| TASK-129 | Agent is five strings that do not join, and role has never once been written | Coding Agent | not_started | unblocked: work owns .perry/agents.jsonl → .perry/roles/ as of the 2026-08-20 signature; needs a spec, then dispatch | — | V3 | TASK-128 | main | | | | | | | +| TASK-135 | a track can be declared but no existing row can be moved onto it | Coding Agent | not_started | — | — | V3 | | main | | | | | | | +| TASK-136 | a queue track SLA is parsed, stored and never measured against anything | Coding Agent | not_started | — | — | V3 | | main | | | | | | | +| TASK-140 | every mode contract slot is assigned to an axis, and the spine-to-unit map is written down | Coding Agent | in_progress | dispatched to claude-subagent; worktree pinned to 13cfe2f; escalation on `design/` released by the user 2026-08-20 for this row only | evidence/2026-08/TASK-140-dispatch-2026-08-21.md | V3 | — | main | | | | | | | +| TASK-141 | a row stays blocked after its blockers close, because the stored status masks the computed one | Coding Agent | not_started | — | — | V3 | | intake | triaged | | 2026-08-20 | | | | +| TASK-142 | triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene | Coding Agent | not_started | design question answered 2026-08-20: it belongs in conformance, which triage already reads at step 0.5 — not as a new triage feature | — | V3 | — | intake | triaged | | 2026-08-20 | | | | +| TASK-143 | two PRs each green on their own base merged into a red tree, and nothing checked the pair | Coding Agent | not_started | — | — | V3 | | intake | triaged | | 2026-08-20 | | | | ## P2 -| ID | Title | Owner | Status | Next action | Evidence | Verification | Depends on | -|---|---|---|---|---|---|---|---| -| TASK-037 | perry-goals writer | Coding Agent | blocked | After TASK-092 lands, rescope to flag naming and the module-scope handler defect only | — | V4 | TASK-092 | -| TASK-040 | perry-task: Top risks becomes a table with id / opened / cleared | Coding Agent | not_started | Risks still read from a markdown table with empty opened/cleared; make them records in the store. | — | V4 | TASK-089 | -| TASK-045 | Retire the runtime tolerance branches, behind the conformance marker | Coding Agent | blocked | blocked on chain 044 → 047 → 045; switching to the head of it | — | V4 | TASK-044, TASK-047 | -| TASK-066 | Split perry-task by subcommand group | Coding Agent | not_started | Re-size the split after the markdown reader, row renderer and cell escaping are gone. | evidence/2026-08/TASK-066-context.md | V4 | TASK-065, TASK-038 | -| TASK-100 | tasks.jsonl is in no claims[] entry, so a namespace collision on it cannot be reported | Coding Agent | review | merge PR #14; the shape predicate added to bin/perry-lint is outside the declared scope and was flagged for a reviewer | evidence/2026-08/TASK-100-dispatch-2026-08-20-1730.md | V3 | — | -| TASK-112 | the sign-off drafting guard cannot describe itself, so a true statement about it is refused | Coding Agent | not_started | — | — | V3 | | -| TASK-116 | the mention list is write-only, mislabelled, and two of its carve-outs are unpinned | Coding Agent | not_started | — | — | V3 | | -| TASK-117 | two tools disagree about whether the board has drifted when the event log is absent | Coding Agent | not_started | — | — | V3 | | -| TASK-118 | the id minters read three sources and the canonical store is not one of them | Coding Agent | not_started | — | — | V3 | | -| TASK-124 | the conformance corpus reads a project outside the repo and has no committed substitute | Coding Agent | not_started | — | — | V3 | | -| TASK-125 | the Anti-Goals-inside-a-version insert case runs only on the author machine | Coding Agent | not_started | — | — | V3 | | +| ID | Title | Owner | Status | Next action | Evidence | Verification | Depends on | Track | Stage | Arrived | +|---|---|---|---|---|---|---|---|---|---|---| +| TASK-037 | perry-goals writer | Coding Agent | not_started | unblocked: TASK-092 closed 2026-08-20. Re-scope per its own note — flag naming and the module-scope handler defect only; the rest was overtaken by TASK-092 and TASK-123 | — | V4 | TASK-092 | main | | | +| TASK-040 | perry-task: Top risks becomes a table with id / opened / cleared | Coding Agent | not_started | Risks still read from a markdown table with empty opened/cleared; make them records in the store. | — | V4 | TASK-089 | main | | | +| TASK-045 | Retire the runtime tolerance branches, behind the conformance marker | Coding Agent | not_started | unblocked: the whole chain closed — TASK-044 and TASK-047 are both done. The conformance marker enforces on this branch, which is the precondition this row was waiting for | — | V4 | TASK-044, TASK-047 | main | | | +| TASK-066 | Split perry-task by subcommand group | Coding Agent | not_started | Re-size the split after the markdown reader, row renderer and cell escaping are gone. | evidence/2026-08/TASK-066-context.md | V4 | TASK-065, TASK-038 | main | | | +| TASK-112 | the sign-off drafting guard cannot describe itself, so a true statement about it is refused | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-116 | the mention list is write-only, mislabelled, and two of its carve-outs are unpinned | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-117 | two tools disagree about whether the board has drifted when the event log is absent | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-118 | the id minters read three sources and the canonical store is not one of them | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-124 | the conformance corpus reads a project outside the repo and has no committed substitute | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-125 | the Anti-Goals-inside-a-version insert case runs only on the author machine | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-130 | schema README says three contracts and pins goals at a version that shipped two ago | Coding Agent | not_started | — | — | V2 | | main | | | +| TASK-131 | seventeen emitted contract keys are documented nowhere, and now there is a number for it | Coding Agent | not_started | — | — | V2 | | main | | | +| TASK-132 | the parity check cannot see 23 keys because Perry own state leaves four collections empty | Coding Agent | not_started | — | — | V3 | | main | | | +| TASK-137 | a new queue row is born in the second stage, not the first | Coding Agent | not_started | — | — | V2 | | main | | | +| TASK-139 | a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off | Coding Agent | not_started | — | — | V3 | TASK-102 | intake | triaged | 2026-08-20 | ## Cadence (recurring; doesn't consume P0 slots) diff --git a/perry/design/DESIGN-003-work-modes.md b/perry/design/DESIGN-003-work-modes.md index f619000..52dc91e 100644 --- a/perry/design/DESIGN-003-work-modes.md +++ b/perry/design/DESIGN-003-work-modes.md @@ -561,6 +561,18 @@ against the mode table in §5.1), G at V5. - 2026-08-19 — decision 8 superseded by ADR-008 after its own revisit trigger fired in a real OpenCode session. Added the explicit `opencode` host and `opencode-subagent` executor; generic untested host support remains rejected. +- 2026-08-20 — **§ 5.1's `mode` is revisited by `DESIGN-008`**, locked the same + day. Raised by the user: `Mode` answers three questions at once — what the + work is accountable to, how a row advances, and how much verification the + consequence needs — and a project whose shape is not one of the four + diagonals must pick the nearest and mis-handle the rest, which is the defect + § 5.1 rejected *"mode as a property of the whole project"* for, reproduced one + level down. Measured first (`evidence/2026-08/TASK-133-track-experiment.md`): + the coupling is **prose**, not mechanism — a `queue`-track row carried a `kr:` + edge to a `project`-mode KR with no refusal, no warning and no gate anywhere + in the code. DESIGN-008 splits `Mode` into a `Spine` and a `Flow` leg with the + four names surviving as presets; nothing in this document's own semantics + changes, and a project that wrote `Mode: project` is untouched. ## 10. References diff --git a/perry/design/DESIGN-008-track-axes.md b/perry/design/DESIGN-008-track-axes.md new file mode 100644 index 0000000..276e5b1 --- /dev/null +++ b/perry/design/DESIGN-008-track-axes.md @@ -0,0 +1,322 @@ +# DESIGN-008: `Mode` is two axes wearing one name + +> Status: locked +> Date: 2026-08-20 · Locked: 2026-08-20 +> Author: Perry maintainer · Implementation owner: TBD +> Linked OKR: KR-O1.1, KR-O1.2, KR-O1.3 (`perry/OKR.md` v2, Objective 1) +> Supersedes: — · Superseded by: — +> Revisits: `DESIGN-003-work-modes.md` § 5.1 + +## 1. Problem + +DESIGN-003 gave a track a `Mode`, one of four values, and made that value carry +**everything** about the track's shape. The user raised, on 2026-08-20, that +this conflates two different questions: + +> *`project` is how a project's goals are organised and decomposed. `queue` is +> how an individual task is advanced. One project could organise with `project`, +> work its decomposed tasks as a `queue`, and maintain a knowledge base from a +> `pipeline` of inbound documents — all at once.* + +### 1.1 The four modes are four diagonal picks out of a grid + +DESIGN-003 § 5.1's own semantics table is the evidence. Every row of it is an +independent question, and the four modes answer all of them together: + +| Row of § 5.1's table | The question it answers | Axis | +|---|---|---| +| Spine | what the work is accountable to | **spine** | +| Horizon closes when | when the container ends | **spine** | +| Unit that gets an ID | what a row *is* | contested — § 5.2 | +| Item states / stage vocabulary | how a row advances | **flow** | +| Calendar binding vs advisory | whether a date is a commitment | **flow** | +| WIP control | what too much looks like | **flow** | +| Stage clock / Arrival / Dwell | what the clock measures | **flow** | +| Triage asks | derived from spine + flow | derived | +| Default min. rung | how much verification the *consequence* needs | **neither** — see #1 | +| Signature failure | derived | derived | + +Counted across the four mode files, that is **10 / 14 / 12 / 14 = 50 contract +slots**, ~28 distinct. The draft read that as three axes; walking § 4 resolved +it to **two axes, one plain field and four derived slots** — the `Axis` column +above is the observation, and § 5.2 is what it settled into. + +### 1.2 The coupling is prose, not mechanism — measured, not assumed + +This was tested on 2026-08-20 rather than argued (`evidence/2026-08/TASK-133-track-experiment.md`). +A `queue`-mode track was declared on this repository, a row created on it, and +that row attached to `P-O1.3`, a KR of a `project`-mode phase: + +``` +attribution.linked 4 → 5 +attribution.linkage_error '' +perry-lint 0 errors +``` + +Nothing refused it, nothing warned. Searched for the gate and found none: + +- no code in `bin/perry-state` or `bin/perry-task` conditions a KR edge on a + track's mode; +- `perry/phase/002-linkage.md` contains neither `mode` nor `track`; +- **"No objectives cascade" appears exactly twice, both in `modes/queue.md`** — + line 16 (the contract table's `Spine` cell) and line 188. + +**So the combination the user asked for already runs.** What stops it is that +the mode file tells the agent it is not a thing, so triage never asks the KR +question on a queue track and no procedure offers the combination. + +### 1.3 What that costs today + +A project whose shape is not one of the four diagonals must pick the nearest +one and mis-handle the rest — **which is verbatim the defect DESIGN-003 § 5.1 +rejected "mode as a property of the whole project" for.** Tracks fixed that at +the project level and reproduced it one level down. + +Two concrete cases, both live: + +- **This repository.** Goals decompose through `OKR.md` → `phase/002`; the work + that actually arrives (an agent's mid-run finding, a sweep's sibling, a review + result — 8 of the last 15 rows) has an arrival date, a backlog depth and a + recurrence question, and none of that is expressible on a `project` track. +- **`~/proj/TeckWork`.** Ingesting an article into a knowledge node is a + `pipeline`; querying that base for a research report is an `inquiry`. This + case **is already served** by DESIGN-003 — two tracks, one table — and is + recorded here only so § 3 can say so explicitly. + +## 2. Goals + +1. A track can name its spine and its flow independently. +2. **A project that writes only `Mode: project` changes by zero bytes** and + behaves identically. The no-op property `modes/project.md` is built on is + not negotiable. +3. Every one of the ~28 distinct contract slots is assigned to exactly one + axis, in a table, with the ambiguous ones argued rather than assigned + silently. +4. The four mode names survive as **presets**, because they are four observed + clusters and a user should not have to assemble a common shape from parts. +5. No new claimed path, and no new state file. The register already exists. + +## 3. Non-Goals + +- **Not a new mode.** The axes are a factoring of what exists. +- **Not multi-track rows.** A row belongs to one track, as today. +- **Not the per-project case.** DESIGN-003 § 5.1 settled that with `1..N` + tracks and its reasoning stands; § 1.3's `TeckWork` example is served by it + unchanged. This document does not reopen it. +- **Not a config schema rewrite.** `## Tracks` gains optional columns; existing + rows parse unchanged. +- **Not the triage rewrite.** What triage asks *follows* from the axes; the + procedure change is a separate row once the axes are fixed. + +## 4. User Decisions + +ALL rows must be resolved before this doc can move to `Status: locked`. + +| # | Decision | Options | Chosen | Date | +|---|---|---|---|---| +| 1 | How many axes | Two — spine, flow / Three — spine, flow, consequence / **Two plus rung as a plain per-track field** | **Two plus rung as a field** | 2026-08-20 | +| 2 | Which side owns "the unit that gets an ID" | Spine / Flow / **Neither — it is derived from spine** | **Derived from spine** | 2026-08-20 | +| 3 | Whether `Mode` survives | **As a preset name** / As a deprecated alias / Removed, axes only | **Preset name** | 2026-08-20 | +| 4 | What an unspecified leg means | **Inherit from the preset** / Inherit from `project` / Refuse the row | **Inherit from the preset** | 2026-08-20 | +| 5 | Whether a mode file stays one file per mode | **One per mode, annotated** / One per spine + one per flow / A single matrix page | **One per mode, annotated** | 2026-08-20 | +| 6 | Whether existing declarations migrate | **Never — presets are permanent** / On next write / A one-time `perry-config` migration | **Never** | 2026-08-20 | + +Notes on the resolved rows, written after they were walked: + +- **#1 went against this document's own draft, and the reason is a + measurement.** The draft bolded *three axes* on the argument that "how much + verification does this need" is independent of both spine and flow — which is + true. What killed it is that the independence **needs no axis to express it**: + `Default rung` is already a column in the track register, already overridable + per track and per row, and this repository's first real declaration overrode + queue's V2 to V3 on 2026-08-20 without any axis machinery existing. A rung + floor is a scalar, not a dimension with a vocabulary; promoting it would have + meant inventing a set of consequence values (`internal` / `outward-facing` / + …) that nothing today asks for. **Two axes, and `Default rung` stays the + plain field it already is.** + +- **#2 was the one genuinely contested slot, and it resolved to "do not declare + it at all".** The tension was real: "the deliverable, not the task" reads + like a statement about what the work is *for* (spine), but it decides what a + board row **is**, which drives `Stage`, `Arrived` and the WIP denominator + (flow). What settled it is that each spine value implies exactly one unit — + objectives→task, commitments→deliverable, question tree→question — so the + field would be one-to-one with a field that already exists. **A declarable + field that can only ever hold one correct value is a field that can hold a + wrong one**, and the wrong value would only surface when someone mixes two + presets, which is precisely the new case this design creates. + + It also gives the right answer on the motivating case. "Goals decomposed + through `project`, tasks advanced as a `queue`" has an `objectives` spine, so + its unit is a **task** — not a "request", which is what flow-ownership would + have renamed it to. Changing how work is advanced must not change what a row + is. + +- **#3, #4 and #6 are one decision in three places, and Goal 2 forced all + three.** The preset name survives, an unstated leg inherits from the preset, + and nothing migrates. Together they mean a project that wrote `Mode: project` + a year ago is untouched, reads the same, and behaves the same. #6's rejected + options are both rewrites of a line the user hand-wrote, which + `perry/OKR.md § Anti-Goals` forbids: *no automatic rewrite of a project's + existing structure.* + +- **#5 follows #3.** If the preset is the front door, the documentation is + organised by preset. Each of the ~28 slots gains a label naming its axis; no + rule moves file. The cost is accepted and named: reading "what is queue's + flow" means selecting the labelled lines out of one mixed file, and the + router keeps loading exactly one mode file per track. + +## 5. Architecture + +*Written against § 4's six resolved decisions, 2026-08-20. Every claim below +cites the decision it rests on.* + +### 5.1 The declaration + +```markdown +## Tracks + +| Track | Mode | Spine | Flow | Stages | WIP | SLA | Cycle | Default rung | +|--------|----------|------------|-------------|---------|-----|-----|----------|--------------| +| core | project | — | — | — | — | — | — | — | +| ops | project | — | queue | new→… | 6 | 5d | weekly | V3 | +| kb | pipeline | — | — | brief→… | 3 | — | 2026-W34 | V5 | +``` + +**Row 1 is what every project writes today and it parses identically** (goal 2). + +**Row 2 is the motivating case** and shows what decisions #3 and #4 buy: it +declares one leg. `Mode: project` still supplies the spine — objectives and a +phase — and `Flow: queue` overrides only how a row advances. `Spine` is blank +because the preset already answers it; blank means *inherit from the preset*, +never *inherit from `project`* (#4), which is why row 3 can leave both blank and +still be a pipeline in full. + +`Default rung` is **not an axis** (#1). It is the column it already is, and +row 2 overrides it for a reason that belongs to neither axis — an arriving row +on that track is a code defect, and a resolution note is not evidence. + +### 5.2 The slot table — where the work is + +Each of the ~28 distinct slots gets one axis, or is derived, or is a plain +field. The groups are settled; the row-by-row assignment is step 1. + +| Slot | Axis | Note | +|---|---|---| +| Ends when · Horizon · Spine | spine | | +| Item states · Stage vocabulary · Stage clock · Arrival · Dwell · SLA · WIP · Calendar | flow | | +| **Unit that gets an ID** | **derived from spine** (#2) | one-to-one with the spine value; never declared, so it cannot contradict it | +| Commitment link · Question tree · The answer · Sources · Claim → source | spine, provisionally | each names a file or a column that exists because of what the work is accountable to | +| Triage asks · Signature failure | derived | rendered from spine + flow, never declared | +| **Default rung** | **neither — a plain per-track field** (#1) | unchanged from today: a column in the register, overridable per track and per row | + +Two axes, one plain field, and four derived slots. **Completing this table row +by row is the implementation, not a preliminary** — ~28 distinct slots across +four files, and the two hardest were resolved in § 4 rather than left to +whoever writes the code. + +### 5.3 What is not changing + +`BOARD.md` gains no column — `Track`, `Stage`, `Stage since`, `Arrived`, +`Parent`, `Commitment` and `Role` already exist and are already created on +demand. The linkage graph is untouched: § 1.2 measured that it never consulted +the mode. + +### 5.4 Blast radius + +Every surface that reads a track's shape, and what this does to it. Added at +lock pre-flight (`reference/input-quality.md § 3.6`), the same step at which +DESIGN-003 gained its § 5.9. + +| Surface | Reads | Effect | +|---|---|---| +| `bin/perry-state § parse_tracks` | the register's columns | **Changes.** Two optional columns, each defaulting from the preset. A row with neither parses exactly as today | +| `$PERRY_HOME/SKILL.md` step 3b | one mode file per distinct `mode` | **Unchanged** by #5 — still one file per track, still keyed on the preset name | +| `work` triage | the track's mode | **Changes** at step 4: the question is rendered from (spine, flow) instead of from the mode name. A track with no override renders the same question it does today | +| `bin/perry-config` | the register as a store | **Changes** only in that two more columns round-trip. `## Tracks` is already a `track` record kind — measured 2026-08-20, `{'setting': 7, 'track': 2}` | +| `BOARD.md` | `Track`, `Stage`, `Stage since`, `Arrived`, `Parent`, `Commitment`, `Role` | **Unchanged.** Every column already exists and is already created on demand | +| `phase/-linkage.md` | nothing about tracks | **Unchanged.** § 1.2 measured that it never consulted the mode | +| `schema/task-list-contract.md` | `tasks[].track` | **Unchanged.** The track name is what a row carries; its shape is not on the row | +| aiMark and any other consumer | `perry-state --json § project.config.tracks[]` | **Additive.** Two keys appear; none is removed or retyped, so it is a `1.x` change under `tests/test_contract_invariance.py` | +| A project that never declares a register | — | **Nothing.** `modes/project.md`'s no-op property is goal 2 and is what step 5 exercises | + +## 6. Implementation plan + +| # | Step | Depends on | Note | +|---|---|---|---| +| 1 | Complete § 5.2 slot by slot | #1, #2 | The document's real payload | +| 2 | `Spine` / `Flow` columns parsed, defaulted from the preset | 1, #3, #4 | `bin/perry-state § parse_tracks` | +| 3 | Mode files annotated per slot with their axis | 1, #5 | No rule moves; each gains a label | +| 4 | Triage renders its question from (spine, flow) | 2, 3 | Where a mixed track stops being invisible | +| 5 | A mixed track exercised end to end on this repository | 4 | The pass condition, DESIGN-003 § 5.9's sense | + +## 7. Risks & mitigations + +- **The matrix becomes the interface.** If presets erode, every user meets a + 4×4 grid. *Detect:* count declared tracks that override a leg; if most do, + the presets no longer describe real work. *Mitigate:* goal 2 and decision #3 — + the preset name stays the documented way in, and the axes are an override, + not the front door. +- **A combination that cannot work.** `inquiry` spine with `pipeline` flow may + be incoherent. *Detect:* the parser refuses the pair by name, so it surfaces + at declaration rather than as a track that half-works. *Mitigate:* step 1 + must enumerate the illegal pairs; an unenumerated pair is allowed, so the + list being wrong shows up as a bad track rather than as a silent refusal. +- **The unit is derived, so a spine with no obvious unit has none.** #2 removed + the field on the ground that each spine value implies exactly one unit. A + spine added later that does not is then unrepresentable rather than merely + awkward. *Detect:* adding a spine value means adding a row to the spine→unit + map, and an empty cell there is the signal. *Mitigate:* step 1 records that + map explicitly, so a new spine has a row it must fill rather than a + convention to infer. +- **A preset that is half-overridden reads as the preset.** #4 makes a blank + leg inherit, so `Mode: pipeline · Flow: queue` still says "pipeline" at a + glance. *Detect:* the snapshot renders the resolved pair, not the preset + name, wherever a track's shape is reported — so a half-overridden track reads + as one on sight. *Mitigate:* same line; the rendering is the mitigation. +- **This document becomes a second account of work modes.** *Detect:* a rule + stated here that is not also in a `modes/` file or a test. *Mitigate:* it + revisits DESIGN-003 rather than superseding it, and #5 keeps the rules in the + mode files; on lock, DESIGN-003 § 9 gains a `## Changes` entry pointing here. + +## 8. Open questions + +1. **Does `phase/` survive on a mixed track?** DESIGN-003 § 8 already asks this + for non-`project` modes and defers it to a fixture. A spine axis makes the + question sharper — a track with an `objectives` spine has a phase whatever + its flow is — but does not answer it. +2. ~~**Is "consequence" one axis or a property of the row?**~~ **Closed by + decision #1 on 2026-08-20**, not deferred: a floor is a scalar, it is + already an overridable column, and no vocabulary of consequence values is + wanted by anything. It stays the field it is. +3. **What does a track with no flow mean?** `project` mode's flow is "Status + only", which is a real answer. Whether that is a fourth flow value or the + absence of one changes whether `Flow` can be blank. + +## 9. Changes (append-only after lock) + +- 2026-08-20 — created. Raised by the user against DESIGN-003 § 5.1; § 1.2's + measurement run first, as `TASK-133`, so the document opens with what is true + rather than with what is argued. +- 2026-08-20 — § 5.4 blast radius added and every § 7 risk given a detection + signal, at `lock` pre-flight (`reference/input-quality.md` § 3.6, § 3.7) — + the same step at which DESIGN-003 gained its § 5.9. One advisory point was + **overridden rather than fixed**: decision #5 (how `modes/` is organised) is + arguably a choice an agent could have made alone rather than a user-only + one (§ 3.5). It was asked anyway, because #3 had just made the preset the + front door and the documentation shape follows from that — the two answers + had to be given by the same person. +- 2026-08-20 — locked. +- 2026-08-20 — all 6 User Decisions resolved. **#1 was chosen against this + document's own draft**, which had bolded three axes; the argument that beat it + is in § 4's notes and it is a measurement, not a preference. § 5.1's example, + § 5.2's slot table, one § 7 risk and § 8's second open question all moved as a + consequence, and the title's "three axes" is now the position the document + argues *against*. + +## 10. References + +- `perry/design/DESIGN-003-work-modes.md` § 5.1, § 5.2, § 8 — the design this revisits +- `perry/evidence/2026-08/TASK-133-track-experiment.md` — § 1.2's measurement +- `modes/project.md`, `modes/pipeline.md`, `modes/queue.md`, `modes/inquiry.md` +- `perry/OKR.md` v2 § Objective 1 — KR-O1.1, KR-O1.2, KR-O1.3 diff --git a/perry/evidence/2026-08/TASK-120-spec.md b/perry/evidence/2026-08/TASK-120-spec.md new file mode 100644 index 0000000..24c7fed --- /dev/null +++ b/perry/evidence/2026-08/TASK-120-spec.md @@ -0,0 +1,119 @@ +# TASK-120 — the linkage edges are read but never folded into KR progress + +> Source: `perry/phase/002-linkage.md`, `schema/goals-list-contract.md` +> Dispatch mode: auto +> Executor: claude-subagent +> Estimated cycle: medium +> Subjective verification: no +> Touches architecture: `perry-goals/list` is a versioned read contract — any +> field added here is a `1.x`-style addition and must not remove or retype one +> Deployed: no + +## Schema + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Attribution**: unlinked — it serves the ability to score KRs, not one KR + +## The state, measured 2026-08-21 + +**The row's title understates it.** The numbers are not merely absent; they are +**wrong in both directions**, and today's own measurements prove it. + +`perry-state --section linkage` carries `target` and `current` per KR. Both are +hand-written into `phase/-linkage.md`'s YAML frontmatter and **nothing +derives, checks or ages them**: + +| KR | target | current | reads as | actually | +|---|---|---|---|---| +| `P-O1.1` `BOARD.md` rendered from the store | 1.0 | **0.0** | 0% | **met** — TASK-038/088/089/090 all closed | +| `P-O2.2` readers resolving a header cell (baseline 5) | 0.0 | **0.0** | **met** | **not met** — TASK-094 measured 13 row splits and 87 header resolutions still reaching `BOARD.md` | + +The second shape is the systemic one: **six of the eight phase KRs have +`target: 0.0`**, and an unset `current` defaults to `0.0`, so every +*drive-this-to-zero* KR reads as **met on the day it is written**, before any +work starts. + +Meanwhile `perry-goals list --json` (`perry-goals/list/2.0`) returns each KR as +a **bare id string** — `"KR-O1.1"` — carrying neither target nor current. So the +one payload a front-end reads cannot express progress at all, and every OKR +roll-up this project has produced was measured by hand. + +## The trap this row must not fall into + +**Do not compute a completion ratio from the `tasks[]` edges and call it +progress.** `P-O2.1`'s metric is *"0 occurrences of `CLOCK_RE`"*. "TASK-091 is +closed" does not establish that the count is zero — only re-running the count +does. A ratio presented as `current` would be a fabricated measurement, which +`perry/OKR.md § Operating Principles` forbids in its first line: *never compute +a number by reading files and eyeballing it.* + +`goals/SKILL.md` already carries the matching rule: **a KR's `target` / +`current` are numbers or absent**, never a prose target coerced into a number. + +## Deliverable + +The payload stops presenting an author-asserted number as a measured one, and +says when it has gone stale. Concretely, all three: + +1. **Provenance.** Every `current` a payload emits is marked as **asserted by + the author**, with the date it was asserted. A KR whose `current` was never + written is `null` — never `0.0` by default, which is what makes a + drive-to-zero KR read as met before it starts. +2. **Staleness.** A KR whose linked tasks have changed state since `current` was + asserted is reported as stale, naming which tasks moved. This is the edge + finally being read: not to compute the metric, but to know the number can no + longer be trusted. +3. **Reachability.** `perry-goals/list` exposes `target`, `current`, its + provenance and its staleness per KR, so a front-end can render a KR without + parsing `phase/-linkage.md` itself. Additive only. + +**How the three combine is yours to argue.** If you conclude that a separate, +clearly-labelled *linked-task completion* count belongs beside `current` rather +than inside it, make that case in your result — it is a defensible answer, and +the one thing that is not defensible is a single number that hides which of the +two it is. + +## Verification — V3 + +1. **Both of today's wrong readings flip.** With `phase/002-linkage.md` + unchanged, `P-O1.1` no longer reports as 0-of-1 progress with four closed + tasks, and `P-O2.2` no longer reports as met. Assert the reported shape, not + a hand-typed number. +2. **A drive-to-zero KR with no asserted `current` is `null`, not `0.0`** — + proved on a fixture whose KR has `target: 0` and no `current`. Reverting that + default reddens it. +3. **Staleness discriminates.** On a fixture: a KR whose linked tasks have not + moved since the assertion is **not** stale; closing one of its tasks makes it + stale and names that task. Both directions, on the same fixture. +4. **The contract does not break.** `python3 tests/parallel test_contract_invariance` + stays green, and `tests/test_contract_key_parity.py` reports **0 + documented-but-not-emitted** for `perry-goals/list` — every field you add is + documented in `schema/goals-list-contract.md` in the same change. Note the + parity check currently reports 5 emitted-but-undocumented keys on this + contract already; that is TASK-131, not yours — **do not let your number hide + inside it.** Record the before and after counts separately. +5. `python3 tests/parallel -j 4`, `bash tests/run`, `python3 bin/perry-lint`, + `git diff --check`. + +## Files in scope + +- `bin/perry-goals`, `bin/perry-state` (its `linkage` / `attribution` sections) +- `schema/goals-list-contract.md` +- focused tests and their fixtures + +## Out of scope + +- **Writing `phase/-linkage.md`.** No tool writes it today and that is + TASK-119, a separate row. You read it. +- **Changing any KR's asserted `current` in this repository.** `phase/` is the + `goals` lane's file and this row does not edit project state; `git diff -- + perry/` must end empty. +- The five already-undocumented `perry-goals/list` keys (TASK-131). +- Deriving a metric that only re-running a count can establish. +- **The shape declaration in `schema/state-schema.json`.** It sits behind this + project's safety gate as part of the claim surface, and this row is scoped to + need no change there: you are adding fields to a *payload*, not to the + declared shape of a state file. **If you conclude the linkage record's + declared fields genuinely must change, stop and say so in your result** — that + is a per-task release the user gives, not a scope you widen. diff --git a/perry/evidence/2026-08/TASK-122-spec.md b/perry/evidence/2026-08/TASK-122-spec.md new file mode 100644 index 0000000..3548cef --- /dev/null +++ b/perry/evidence/2026-08/TASK-122-spec.md @@ -0,0 +1,93 @@ +# TASK-122 — the repair path the tools advertise leaves the file damaged + +> Source: `bin/perry_store.py § describe_cell`, `bin/perry_md_store.py` +> Dispatch mode: auto +> Executor: claude-subagent +> Estimated cycle: small +> Subjective verification: no +> Touches architecture: no — one padding rule, in the branch that already +> distinguishes a table cell from a bullet slot +> Deployed: no + +## Schema + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Attribution**: unlinked + +## Reproduced 2026-08-21, exactly + +`describe_cell`'s disagreement branch defaults the padding to one space on each +side: + +```python +bin/perry_store.py:344 +return {"f": field, "lead": lead or " ", "trail": trail or " ", + "disagrees": body} +``` + +For a **table cell** that is right: `render_line` joins on `|`, so `| ` + value ++ ` |` needs it. For a **bullet slot** it is wrong, because +`slot_descriptor`'s literal span **already carries the separator's whitespace**: + +``` +input '- Repo layout: single' +literal span '- Repo layout: ' ← the space after the colon is already here +lead / trail ' ' / ' ' ← invented on top of it +rendered '- Repo layout: split ' +``` + +Two spaces after the colon, and a **trailing space `git diff --check` reports**. + +## Why this is not cosmetic + +`bin/perry_md_store.py:972` prints, in its own refusal message: + +> `` ` render --write` to bring the file back in line. `` + +So the tool **instructs the user into the damage.** They follow the advice they +were given and get a file with trailing whitespace their next commit hook or +`git diff --check` complains about — for a repair the tool told them to run. + +## Deliverable + +`render --write` on a file whose bullet value disagrees with the store produces +a line that is byte-correct: the store's value in the slot, and **no whitespace +the input did not have.** The table-cell path keeps its padding — the two are +already distinguished by `escape` in the descriptor, and this is the same seam. + +Whatever shape you choose, the rule must be stated where the padding is decided, +in the voice of the surrounding comments, saying **why a bullet and a cell +differ** — otherwise the next reader re-introduces it. + +## Verification — V3 + +1. **The reproduction above becomes a test and is byte-exact**: + `- Repo layout: single` with a store holding `split` renders + `- Repo layout: split` — one space, no trailing space. +2. **The table cell is unchanged**, proved on the same run: a board row whose + cell disagrees still renders with its `| ` … ` |` padding. **Reverting your + fix must redden case 1 and NOT case 2** — if one change reddens both, the two + paths are not actually separated and you have found something bigger; say so. +3. **The blank-marker path is untouched.** `- Code repo path: —` with an empty + stored value still renders `—` and does not acquire padding. + `tests/test_md_store.py § test_the_declared_blank_marker_survives_the_bullet_path` + must stay green without being edited. +4. **The advertised repair is clean end to end**: on a real disagreeing + `.perry/config.md` copy in a temp directory, `render --write` followed by + `git diff --check` reports nothing. Run it; do not assert it. +5. `python3 tests/parallel -j 4`, `bash tests/run`, `python3 bin/perry-lint`, + `git diff --check`. + +## Files in scope + +- `bin/perry_store.py` +- `bin/perry_md_store.py` only if its refusal message needs to change +- focused tests + +## Out of scope + +- The table-cell padding rule itself. +- `perry/` — no project state changes; `git diff -- perry/` must end empty. +- Anything about *when* a disagreement is reported, as opposed to how the + repaired line is rendered. diff --git a/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md b/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md new file mode 100644 index 0000000..533e7cf --- /dev/null +++ b/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md @@ -0,0 +1,85 @@ +# TASK-126 — result + +> Date: 2026-08-21 · Executor: claude-subagent · PR: https://github.com/ranjiao/Perry/pull/22 +> Branch: `coding/task-126-dangling-self-reference` · Cycle time: ~35 min +> 2 code files: `bin/perry-diagnose` (+109/−…), `tests/test_diagnose.py` (+72/−…) +> **The suite is fully green for the first time: 65 modules · 1933 tests · 0 red.** + +## The fourth mark, and why it is not in `report_lines` + +The first three marks ask whether **this line** is a report. The fourth asks +whether **this id** has one — which cannot be decided from a single file, so it +lives in `split_dangling` as a second pass rather than in `report_lines`. Two +independent conditions, both required: + +- the **document** names one of this checker's findings or tests anywhere in it + (`document_reports_on_a_check`, reusing `names_a_check` over the whole file — + no second vocabulary is introduced); +- the **id** is one the project has already reported on. + +Option 2 from the spec — a distinct fourth *outcome* — was rejected with an +argument: it needs the identical classification work, buys nothing structurally, +and would have forced rewriting an existing assertion. +**Option 1 landed with zero existing assertions changed.** + +## The path exemption was considered and rejected, and the rejection is testable + +Re-run independently by the PMO, not accepted from the agent's report. A temp +project with an `evidence/`-shaped dispatch record that **does** name `LOAD-02` +and a `test_` function, and is genuinely blocked on two ids: + +``` +dangling : ['QQQ-77', 'ZZZ-404'] +dangling_in_reports : ['LOAD-02'] +``` + +**A wholesale `perry/evidence/**` exemption would have printed `dangling: []`.** +The shipped rule contains no path and no English reading. + +The simple direction also holds: a project whose only content is +`Blocked on ZZZ-404 until Friday.` still reports `dangling: ['ZZZ-404']`. + +## On this repository + +``` +before dangling: ['DESIGN-900','REL-00'] dangling_in_reports: ['ZZZ-404'] +after dangling: [] dangling_in_reports: ['DESIGN-900','REL-00','ZZZ-404'] +``` + +Exactly two ids moved between the lists. **Both stay visible; neither was +added to an exemption list** — there is no id list in the change. `git diff -- +perry/` is 0 bytes: no record was edited to make a checker pass, which was the +row's hard bound. + +## Both halves proved load-bearing + +Reverting the mark reddened **four** tests, three of them naming the two halves +separately: + +| test | failure | +|---|---| +| `test_perry_itself_passes_its_own_id_checks` | `['DESIGN-900','REL-00'] != []` | +| `test_perrys_own_repository_reports_the_exemption_it_used` | `'REL-00' unexpectedly found` | +| `test_a_record_narrating_a_check_it_reported_is_not_a_reference` | `['ZZZ-404'] != []` | +| `test_a_record_about_a_check_still_reports_an_id_it_never_reported_on` | `['ZZZ-404','ZZZ-405'] != ['ZZZ-405']` | + +A fifth new test — `test_a_document_that_reports_on_nothing_is_still_a_reference` — +correctly stayed **green** under the revert. It is an anti-vacuity test, and its +staying green is the evidence that the other four are not measuring the same thing. + +Tests 1930 → 1933, none weakened. + +## One note for the PMO, raised by the agent + +`split_dangling` no longer early-exits on the first live mention — it must read +every mention to decide the id half. `perry-explain` caps mentions at 40 per id +and the two predicates share one per-file cache, so the cost is bounded; the +suite ran 155s against a 220s baseline, though that comparison is dominated by +the removed red-test retries rather than by this change. + +## PR hygiene + +PR #22 reports 28 files and +1911/−73. **The code change is 2 files, +162/−19**; +the rest is the unpushed-ancestor sweep — `feat/work-modes` is 14 commits ahead +of origin, so every PR cut from it carries those commits in its diff until the +branch is pushed. Third occurrence. diff --git a/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21.md b/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21.md new file mode 100644 index 0000000..da6eb70 --- /dev/null +++ b/perry/evidence/2026-08/TASK-126-dispatch-2026-08-21.md @@ -0,0 +1,18 @@ +# TASK-126 — dispatch record + +> Date: 2026-08-21 · Executor: claude-subagent · Worktree pinned to `13cfe2f` +> Branch: `coding/task-126-dangling-self-reference` +> Escalation: pre-flight **refused** on the fragment `diagnose` +> (`.perry/hook.md` — *"Writing into a project Perry does not own — … `diagnose` +> execute stage"*). **Released by the user on 2026-08-20, for this row.** + +The release was given knowing the PMO's own reading: the fragment matched the +**filename** `bin/perry-diagnose` rather than the diagnose *execute stage*, and +the spec's `## Out of scope` puts every change under `perry/` outside the row. +Recorded here rather than left in chat, because a release that only exists in a +conversation cannot be audited later. + +Dispatched with the three prohibitions the spec carries: no record may be edited +to make the checker pass (`git diff -- perry/` must end empty), neither id may be +added to an exemption list, and the rule may not be widened until it stops +discriminating. diff --git a/perry/evidence/2026-08/TASK-126-spec.md b/perry/evidence/2026-08/TASK-126-spec.md new file mode 100644 index 0000000..eba2869 --- /dev/null +++ b/perry/evidence/2026-08/TASK-126-spec.md @@ -0,0 +1,112 @@ +# TASK-126 — closing the dangling-id row requires writing the record that re-dangles it + +> Source: `perry/evidence/2026-08/TASK-113-dispatch-2026-08-20-1813.md` +> Dispatch mode: auto +> Executor: claude-subagent +> Estimated cycle: medium +> Subjective verification: no +> Touches architecture: no — one rule inside one checker +> Deployed: no + +## Schema + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Attribution**: unlinked — this serves no phase-002 KR + +## The state, measured 2026-08-20 + +`tests/test_diagnose.py` has two red tests and they are **the last red in the +suite**, on CI and locally: + +``` +test_perry_itself_passes_its_own_id_checks + AssertionError: Lists differ: ['DESIGN-900', 'REL-00'] != [] +test_perrys_own_repository_reports_the_exemption_it_used + AssertionError: 'REL-00' unexpectedly found in ['DESIGN-900', 'REL-00'] +``` + +TASK-113 fixed exactly this and measured `dangling: []` in its own worktree. +It went red again **because of the record written to close it.** Every live +mention of both ids is in one file: + +``` +REL-00 LIVE TASK-113-dispatch-2026-08-20-1813.md:11, 18, 35 + report TASK-113-dispatch…:57, 89 · TASK-113-spec.md:37, 40 + report journal/2026-08/2026-08-20.md:514 (inside `## V5 sign-off`) +DESIGN-900 LIVE TASK-113-dispatch-2026-08-20-1813.md:16 + report TASK-108-dispatch-2026-08-20-1547.md:112 +``` + +Every other mention in the repository **already classifies correctly**. The +three marks in `bin/perry-diagnose § report_lines` work; what they do not cover +is a paragraph describing the check in plain English — *"prose about a check +counted as the thing the check measures"* — with no finding code and no +`test_` name in the same paragraph. + +**So the row cannot be closed without writing a record, and writing the record +reopens it.** That is the defect, not the two ids. + +## Deliverable + +`LOAD-02` can reach zero on a project that has documented its own fix. Either: + +1. the structural marks are extended so that a document whose **subject is the + check** stops counting as a live reference — without its prose being + edited — or +2. the check reports a distinct reason when an id's only live mentions are in + documents about itself, and the two tests assert that reason. + +**Whichever you choose, argue it in the code where the existing three marks are +argued.** That block already explains why the mark is scoped to the paragraph +and not the line; a fourth mark, or a fourth outcome, belongs in the same voice +with the same kind of reason. + +## What you must not do + +- **Do not edit `perry/evidence/2026-08/TASK-113-dispatch-2026-08-20-1813.md`, + or any other record, to make the check pass.** Rewording a true account to + satisfy a checker is the failure this row exists to end, and the next close + would reproduce it. `git diff -- perry/` must be empty at the end, exactly as + it was TASK-113's hard bound. +- **Do not add either id to an exemption list.** TASK-113's record states that + the two ids stay *"visible, not deleted"*. +- **Do not widen the rule until it stops discriminating.** `modes/`, + `reference/` and `README` prose that names an id which resolves to nothing is + still a finding. The guard below is how you show you did not. + +## Verification — V3 + +1. `python3 bin/perry-diagnose --only=user_load --json --root .` reports + `dangling: []` for this repository, with `DESIGN-900` and `REL-00` visible + in whatever list your chosen option puts them in. +2. **Anti-vacuity, both directions**, re-run rather than asserted: + - a temp project whose only content is `Blocked on ZZZ-404 until Friday.` + still reports `dangling: ['ZZZ-404']`; + - a temp project containing a genuinely live reference inside an + `evidence/`-shaped file still reports it. A fix that exempts + `perry/evidence/**` wholesale fails this. +3. **The new mark is proved to be load-bearing**: revert it and watch both + `test_diagnose` tests go red again. +4. The two named tests pass, and `tests/test_diagnose.py` is not weakened — + if you change an assertion, say which and why in your result. +5. `python3 tests/parallel -j 4`, `bash tests/run`, `python3 bin/perry-lint`, + `git diff --check`, and `git diff -- perry/` empty. + +## Baseline + +Measure it in your own worktree. `test_diagnose` should be the **only** red +module; if you see more, say so — three others were fixed today and one of them +(`test_host_support`) was a GNU/BSD `stat` defect that had been red on CI since +long before. + +## Files in scope + +- `bin/perry-diagnose` +- `tests/test_diagnose.py` + +## Out of scope + +- `TASK-112` — the sign-off drafting guard that cannot describe itself. Same + family, different tool, its own row. +- Any change under `perry/`. diff --git a/perry/evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md b/perry/evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md new file mode 100644 index 0000000..67972f3 --- /dev/null +++ b/perry/evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md @@ -0,0 +1,82 @@ +# TASK-127 — dispatch record + +> Date: 2026-08-20 · Executor: claude-subagent · PR: https://github.com/ranjiao/Perry/pull/21 +> Branch: `coding/task-127-contract-key-parity` · Cycle time: ~18 min +> 4 files: `tests/contract_key_parity.py`, `tests/fixtures/contract-key-parity.json`, +> `tests/test_contract_key_parity.py`, `schema/README.md` + +## KR-O2.4 has a number for the first time: **17** + +Five contract files, discovered by glob, not by a list: + +| Contract | documented / emitted | doc'd not emitted | emitted not doc'd | +|---|---|---|---| +| `perry-decide/list/1.0` | 27 / 24 | 0 | 0 | +| `perry-events/list/1.0` | 23 / 23 | 0 | 0 | +| `perry-goals/list/2.0` | 54 / 59 | 0 | **5** | +| `perry-roles/list/1.0` | 4 / 4 | 0 | 0 | +| `perry-task/list/1.11` | 87 / 99 | 0 | **12** | +| **total** | | **0** | **17** | + +The undocumented twelve on the task contract are `semantics` plus its three +children, and the six nested `conformance` entry shapes. The five on goals are +`okr.objectives[].id` and four `phase.objectives*` paths. + +**The KR text said "three contracts". There are five.** Reported, not edited — +that sentence belongs to the `goals` lane. + +## 23 keys cannot be compared, and they are named rather than dropped + +All for one reason: the collection is empty in Perry's own state, so there is no +entry to place an entry-shape table against — the six intake-row fields and +eight ask fields (`intake.rows` and `asks.items` are both `[]`), the six frozen +role-card fields (this repo has no `.perry/roles/`, so `declared: 0`), and +`expired_sunsets[].id/.title/.sunset`. + +They are printed by name with the reason on every run and stored in the +baseline. The agent checked all three existing fixtures and found each poorer +than Perry's own repo, so none helps; the check takes `--root`, so a follow-up +row can point it at a purpose-built project. **It did not widen scope to build +one**, which was the right call. + +## Mutations, with the messages they produced + +1. `startable` removed from all four emit sites → + `documented and not emitted … appeared: ['tasks[].startable']` +2. `task["undeclared_key"]` planted on the live list path → + `emits and does not declare … appeared: ['tasks[].undeclared_key']` +3. Both reverted → totals back to 0 / 17 +4. `schema/roles-list-contract.md` moved out → `contract files discovered: 4` + and `5 != 4`, naming the four survivors +5. A sixth contract added declaring one unemitted key, **with no edit to the + check** → `discovered: 6` and that key reported + +Mutation 2 is worth keeping: the first attempt planted the key on a **dead** +board-era path around line 4502 and produced nothing. `bin/perry-task` carries +two independent `startable` computations and only the store-backed one at ~4724 +is reachable. + +## The check's honest limit, stated by its author + +A markdown key table says nothing about where its keys hang, so each table is +placed against the emitted containers by best coverage, tie-broken on precision, +with a floor. Tables clearing neither floor are reported **unplaced** rather +than guessed at — which is the 23-key set above. An early coverage-only version +placed the eight `asks` fields on `tasks[]` (they share `id`, `blocks`, +`status`, `priority`) and filed four phantom findings. The floor exists because +of that. + +## Baseline + +The agent measured its own: 63 modules · 1867 tests, one module red +(`test_diagnose`, `dangling == ['DESIGN-900','REL-00']` — TASK-126). After: +64 modules · 1880 tests, same one red. `perry-lint` 0 errors, 3 pre-existing +NS-01 warnings. `git diff --check` clean. + +## Two findings handed on + +- `schema/README.md § The three read contracts` repeats the same denominator + error — heading and table both say three — and its `perry-goals` row reads + `perry-goals/list/1.0` when the shipped contract is **2.0**. Untouched by this + row. +- The 17 undocumented keys are now a measured number with no row against them. diff --git a/perry/evidence/2026-08/TASK-127-spec.md b/perry/evidence/2026-08/TASK-127-spec.md new file mode 100644 index 0000000..2f3bdb1 --- /dev/null +++ b/perry/evidence/2026-08/TASK-127-spec.md @@ -0,0 +1,104 @@ +# TASK-127 — the contract docs and the payloads they describe are never diffed against each other + +> Source: `perry/OKR.md § Objective 2` (KR-O2.4) and `schema/README.md` +> Dispatch mode: auto +> Executor: claude-subagent +> Estimated cycle: medium +> Subjective verification: no +> Touches architecture: no — this adds a check; it changes no contract and bumps no version +> Deployed: no + +## Schema + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Attribution**: KR-O2.4 (overall OKR v2, due 2026-09-30) + +## Why this row exists + +KR-O2.4's metric is a count: *contract-payload keys documented but not emitted, +or emitted but not documented*. **Nothing in the repository computes it.** + +`tests/test_contract_invariance.py` is the closest thing and it measures +something else: it captures the payload's SHAPE — every field path and the type +at it — and compares that against a recorded baseline, so a key that is emitted +and undocumented passes cleanly, and a key documented and never emitted is +invisible to it because it was never in the baseline. The other seven modules +that read a contract file (`test_count_fields`, `test_role_on_rows`, +`test_task_writer`, `test_task_summary`, `test_prioritize`, `test_goals_contract`, +`test_goals_writer`) each assert that **one named field** is mentioned in the +prose. Seven spot checks are not a count. + +So the KR currently cannot be scored except by hand, which is the same defect +`perry-goals list` has for KR progress. + +## The denominator is wrong in the KR text + +KR-O2.4 says "across all three contracts". There are **five**: + +| File | Contract | +|---|---| +| `schema/task-list-contract.md` | `perry-task/list/1.11` | +| `schema/goals-list-contract.md` | `perry-goals/list/2.0` | +| `schema/decide-list-contract.md` | `perry-decide/list/1.0` | +| `schema/events-list-contract.md` | `perry-events/list/1.0` | +| `schema/roles-list-contract.md` | `perry-roles/list/1.0` | + +**Do not measure three and report 0.** Measure what is there, report the count +per contract, and hand the KR-text correction back — the `goals` lane owns that +sentence and this row does not edit it. + +## Deliverable + +1. A mechanical two-way diff, per contract file, between the field paths the + **document declares** and the field paths the tool **actually emits**: + - `documented_not_emitted` + - `emitted_not_documented` +2. Discovery is by glob, not by a hand-written list, and the run **names how + many contract files it found**. A contract added later must not be silently + skipped — that is the failure mode this whole row is about. +3. A per-contract baseline that lives in a **file**, not in a memory or a + docstring, so the number can be compared across runs by someone who was not + here. +4. Where the two sides genuinely cannot be compared mechanically — a payload + whose keys are data rather than schema, a documented key behind a flag not + set in a plain run — say so **per contract, by name**, with the reason. + A silently narrowed denominator is worse than a smaller one that is stated. + +## Verification — V3 + +1. **The check discriminates in both directions**, proved by mutation, not by + asserting a test exists: + - remove one documented key from its emit site → red, naming that key; + - add one emitted key the document does not declare → red, naming that key; + - revert both and the count returns to its baseline. +2. The per-contract counts are printed, and the run prints the number of + contract files discovered. Deleting one contract file changes that number. +3. Adding a sixth contract file with one documented, unemitted key is reported + without any edit to the check. +4. `python3 tests/parallel -j 4`, `bash tests/run`, `python3 bin/perry-lint`, + `git diff --check`. + +## Baseline + +The suite's baseline must be measured **in your own worktree**, not taken from +this file. As of this writing `feat/work-modes` is reconciled with the remote +and `test_diagnose` is the one known red — its cause is TASK-126, a check whose +only live references are inside the record describing its own fix, and it is not +yours. Two open PRs (#19, #20) are not merged; cut your worktree from +`feat/work-modes` as it stands and say what your baseline was. + +## Files in scope + +- a new check, plus its baseline file +- the test module that exercises it +- `schema/README.md` only if it must describe where the baseline lives + +## Out of scope + +- **Editing any contract document's content, or bumping any contract version.** + This row measures the gap; closing the gap is whatever rows the measurement + produces. +- Editing KR-O2.4's text. Report the five-vs-three discrepancy; the `goals` + lane makes that edit. +- `tests/test_contract_invariance.py`'s baseline. It measures shape and stays. diff --git a/perry/evidence/2026-08/TASK-128-proposal.md b/perry/evidence/2026-08/TASK-128-proposal.md new file mode 100644 index 0000000..6aa7d0f --- /dev/null +++ b/perry/evidence/2026-08/TASK-128-proposal.md @@ -0,0 +1,62 @@ +# TASK-128 — the exact change awaiting a signature + +> Source: `perry/design/DESIGN-007-the-entity-model.md § 4` decision #2, `§ 5.3`, `§ 6` step 2 +> Dispatch mode: none — this is a human gate, not work +> Deployed: no + +## What was already decided, and by whom + +- **2026-08-19, signed by Ran Jiao** — DESIGN-007 decision #2: *the store is the + definition, the card is rendered output.* Not reopened here. +- **DESIGN-007 § 5.3** already names the paths: `.perry/agents.jsonl` is the + store, `.perry/roles/*.md` is what it renders to. Not chosen here. +- **2026-08-20, chosen by the user** — the writing lane is **`work`**. + +What is left is the one thing that has always required a human: **moving a row +in the ownership table.** + +## The edit, in full + +One line changes in `SKILL.md § The hand-off contract`. The `work` row gains +its fifth and sixth path: + +```diff +-| **`work`** (`work/`) | `BOARD.md` (incl. `## Intake`, `## Cadence`), `journal/`, `PROJECT_STATE.md`, `evidence/`, `weekly/`, `handoff/` | KR attribution edges, handed to `goals` | ++| **`work`** (`work/`) | `BOARD.md` (incl. `## Intake`, `## Cadence`), `journal/`, `PROJECT_STATE.md`, `evidence/`, `weekly/`, `handoff/`, **`.perry/agents.jsonl` → `.perry/roles/`** | KR attribution edges, handed to `goals` | +``` + +Nothing else in that section moves. `goals` and `decide` are untouched. + +## What this costs, stated before the signature rather than after + +1. **`.perry/roles/*.md` becomes drift-reported.** A hand edit to a role card + will read as drift, exactly as it does for `BOARD.md` today. This is the + same trade already accepted for the board on 2026-08-19; it is repeated + here because it applies to a file that has never had it. +2. **`schema/state-schema.json` says the opposite today.** Its note on + `.perry/roles/*.md` reads *"OWNER IS `user`, NOT A LANE"* and gives the + reason. That note becomes wrong on the same commit and must change with it. +3. **`SKILL.md` lands at 20,457 bytes against a 20,480 cap** — 23 bytes of + headroom. The account of this change therefore goes to + `reference/hand-off-contract.md`, which is uncapped and is already where the + section points for both prior accounts. **The next contract change will not + fit and will force a trim of the router first.** +4. **The paragraph below the table still reads "Two changes from the previous + contract".** It describes the 2026-08-16 edit and stays accurate about that + edit; it does not mention this one, because there is no room. Anyone reading + the table alone will see three lanes and no history of the third change. + +## What the signature is attesting + +Not that the code is right — none is written yet. That **this row is the +correct owner**, which is the one thing no test can check: a wrong contract +surfaces later as silent cross-lane writes, never as a lint error. + +`tests/test_ownership.py` refuses a lane-owned path the contract does not list. +It refused this one, correctly, and it will accept it the moment the row lands. + +## After the signature + +TASK-129 unblocks: the Agent store, typed `may_touch[]` / `must_escalate[]`, +the card rendered rather than authored, and `events.actor` carrying an id +instead of the six free-text values it holds today. diff --git a/perry/evidence/2026-08/TASK-133-track-experiment.md b/perry/evidence/2026-08/TASK-133-track-experiment.md new file mode 100644 index 0000000..b144b96 --- /dev/null +++ b/perry/evidence/2026-08/TASK-133-track-experiment.md @@ -0,0 +1,106 @@ +# TASK-133 — the first non-`project` track, and what a mixed spine actually costs + +> Date: 2026-08-20 · Run by: PMO Agent, in the main checkout +> Question it was run to answer: DESIGN-003's `mode` enum bundles *spine* +> (how goals decompose) with *flow* (how an item advances). Is that coupling +> mechanical, or only prose? + +## Answer: it is prose. The mechanism was never coupled. + +The three measurements below are the whole result. + +## 1 · Declaring the track — KR-O1.3 holds, with one condition + +One track added to `.perry/config.md § Tracks`: + +``` +| intake | queue | standing | new→triaged→in_progress→resolved | 6 | 5d | weekly | V3 | +``` + +| | measured | +|---|---| +| files changed by the declaration | **1** (`.perry/config.md`) | +| state files rewritten | **0** — `BOARD.md`, `perry/tasks.jsonl` and `OKR.md` byte-identical by md5 before and after | +| `perry-lint` | 0 errors, 3 pre-existing NS-01 warnings | +| both tracks parsed | yes, with `stage_list`, `wip`, `sla`, `cycle`, `default_rung` all resolved | + +**The condition, and it is the one raised against this KR earlier:** this repo +has **no `.perry/config.jsonl`**. `perry-config verify` says so — +*"no store on disk yet"*. So the config is still a plain document here and the +edit is genuinely one file. The moment someone imports the config store +(TASK-092/097's act), the same declaration becomes a file **and** a store write, +and a hand edit becomes reported drift. **KR-O1.3 is not falsified today; it is +falsified the day the store lands**, and nothing currently warns about that. + +## 2 · The central test — a queue-track row carried a `kr:` edge with zero friction + +A probe row was created on the `intake` track and attached to `P-O1.3`, a KR of +a `project`-mode phase: + +``` +attribution.linked 4 → 5 +attribution.linkage_error '' +TASK-134 in unlinked False +perry-lint 0 errors +``` + +Nothing refused it. Nothing warned. Searched for a gate and found none: + +- no code in `bin/perry-state` or `bin/perry-task` conditions a KR edge on a + track's mode; +- `perry/phase/002-linkage.md` contains neither `mode` nor `track`; +- *"No objectives cascade"* appears twice, both in `modes/queue.md` — at line 16 + (the contract table's `Spine` cell) and line 188 (prose). It is a + documentation statement, not an implemented rule. + +**So "organise goals with `project`, advance tasks with `queue`" already runs.** +What is missing is not the mechanism — it is that the mode file tells the agent +not to, and that triage therefore never asks the KR question on a queue track. + +The probe row was dropped and its edge removed; `linked` is back to 4. + +## 3 · Three defects the experiment surfaced, none of them the one it was testing + +**TASK-135 — a declared track cannot be populated.** `--track` is accepted by +`add` (at creation) and `route` (intake row → task). There is no +`perry-task track `, and `status --track` is refused. So a project that +declares a second track starts it **empty and cannot move any existing work +onto it**. The six rows that genuinely arrived rather than being decomposed — +TASK-124, 125, 126, 130, 131, 132 — belong on `intake` and cannot get there. +This is why `intake` is declared and empty right now, and why KR-O1.1 is not +met by this experiment. + +**TASK-136 — the SLA is decoration.** The track carries `sla: 5d`. `perry-state` +computes `stage_counts` and `wip_breaches` for it and nothing else. The only +consumer of a track SLA anywhere is `lib/__init__.py § classify_due`, which +governs a **Commitments `Due` cell**, not a row clock. `today − Arrived` is +computed nowhere. `modes/queue.md` says a track *without* an SLA "cannot run the +breach step, and triage reports that rather than skipping it" — a track *with* +one cannot run it either, and nothing reports that. + +**TASK-137 — a new queue row is born in the second stage.** `intake` declares +`new→triaged→in_progress→resolved`; the probe row was created with +`stage: triaged`, skipping `new`. + +## 4 · What this says about the design question + +The user's reading is right and cheaper to act on than it looked: + +- **Claim 1 — one folder, several shapes — was already designed.** DESIGN-003 + § 5.1 rejected per-project mode explicitly, calling it "empirically wrong", + and named *this repository* as the counterexample. `1..N` tracks is the + answer; `TeckWork` is two rows in one table. +- **Claim 2 — `mode` is two axes wearing one name — is a new finding.** It is + not in § 8's open questions, and § 5.1's own semantics table is the evidence: + every row of it is an independent axis, and the four modes are four diagonal + picks. `Default rung` is arguably a third axis (consequence), since this very + track overrode queue's V2 to V3 for a reason that has nothing to do with flow. + +**The cheap shape of a fix**: keep `Mode` as a preset **name** that expands to a +`(spine, flow, default_rung)` triple, and let a track override any leg. A +project writing only `Mode: project` changes by zero bytes. What it costs is a +one-time judgement, slot by slot, about which axis each of `modes/*.md`'s ~40 +contract rows belongs to — and some are genuinely ambiguous ("the unit that gets +an ID" reads like spine but drives the board's row shape, which is flow). + +That judgement is an RFC, not a task. This file is its measurement. diff --git a/perry/evidence/2026-08/TASK-140-dispatch-2026-08-21.md b/perry/evidence/2026-08/TASK-140-dispatch-2026-08-21.md new file mode 100644 index 0000000..9f1c3bc --- /dev/null +++ b/perry/evidence/2026-08/TASK-140-dispatch-2026-08-21.md @@ -0,0 +1,17 @@ +# TASK-140 — dispatch record + +> Date: 2026-08-21 · Executor: claude-subagent · Worktree pinned to `13cfe2f` +> Branch: `coding/task-140-mode-slot-axes` +> Escalation: pre-flight **refused** on the fragment `design/` +> (`.perry/hook.md` — *"Destructive filesystem operations — … overwriting a +> project's own `design/`"*). **Released by the user on 2026-08-20, for this +> row only.** + +**This hit was not a false positive and was not presented as one.** The row's +deliverable genuinely rewrites `§ 5.2` of a locked design document. The release +was given on that basis. + +Dispatched as step 1 of `DESIGN-008 § 6`, with steps 2–5 deliberately left +unopened so their acceptance criteria do not all read "per step 1's table". +Decisions #1 (two axes) and #2 (the unit is derived) bind the agent and it was +told to report rather than revisit if it disagrees. diff --git a/perry/evidence/2026-08/TASK-140-spec.md b/perry/evidence/2026-08/TASK-140-spec.md new file mode 100644 index 0000000..b83c421 --- /dev/null +++ b/perry/evidence/2026-08/TASK-140-spec.md @@ -0,0 +1,106 @@ +# TASK-140 — every mode contract slot is assigned to an axis + +> Source: `perry/design/DESIGN-008-track-axes.md` § 5.2, § 6 step 1 +> Dispatch mode: auto +> Executor: claude-subagent +> Estimated cycle: medium +> Subjective verification: no +> Touches architecture: DESIGN-008 § 5.2 — it completes that section; it changes no behaviour +> Deployed: no + +## Schema + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Attribution**: KR-O1.1 / KR-O1.2 / KR-O1.3 (`perry/OKR.md` v2, Objective 1) + +## Read this first + +`perry/design/DESIGN-008-track-axes.md`, locked 2026-08-20, all six user +decisions resolved. **You are step 1 of its § 6, and the document says step 1 +is its real payload rather than a preliminary.** Steps 2–5 are all functions of +your output, so they are not open and you are not writing code that consumes it. + +Two of its decisions bind you and are not yours to revisit: + +- **#1 — two axes.** `spine` and `flow`. `Default rung` is **not** a third + axis; it is the plain per-track column it already is. Do not add a + consequence axis, and do not assign any slot to one. +- **#2 — the unit is derived from the spine, never declared.** Each spine value + implies exactly one unit. That is why the map below is part of this row. + +## Deliverable + +### 1 · The slot table + +Every slot in the four mode contract tables — `modes/project.md`, +`modes/pipeline.md`, `modes/queue.md`, `modes/inquiry.md`, **10 / 14 / 12 / 14 = +50 slots, ~28 distinct** — assigned to exactly one of: + +| Axis | Meaning | +|---|---| +| `spine` | what the work is accountable to | +| `flow` | how one row advances | +| `derived` | rendered from spine + flow; never declared | +| `field` | a plain per-track column, declared but not an axis | + +A slot whose assignment is not obvious gets **one line of reason**. Do not +write a reason for the obvious ones — a table where every row carries prose is +one nobody reads. + +The seed assignments in § 5.2 are a **sketch, not an answer**. Check each one. +If you disagree with one, say so with the argument; the document was written by +someone who had not yet gone slot by slot. + +### 2 · The spine → unit map + +An explicit table: each spine value → the unit that gets an ID. § 1.1 and § 4's +note give three (`objectives`→task, `commitments`→deliverable, question +tree→question) and queue's is the fourth. This exists because decision #2 +deleted the declarable field, so **an unmapped spine value is unrepresentable +rather than merely awkward** (§ 7). + +### 3 · Where it lives + +`DESIGN-008 § 5.2`, replacing the sketch. **`decide` owns `design/`** — this is +the one row where writing a design file is the deliverable, and the design is +locked, so append a `## Changes` entry recording that § 5.2 moved from sketch to +complete. Do not change any other section. + +## Verification — V3 + +1. **Coverage is mechanical, not eyeballed.** A check extracts the slot names + from the four mode files' contract tables and fails on any slot the table + does not mention. Deleting a slot from a mode file reddens it; adding one to + a mode file reddens it. Both proved by doing it, then reverting. +2. **The presets round-trip.** Each of the four mode names expands to a + (spine, flow) pair, and that pair reproduces that mode file's own contract + table **value by value** — not "a pair exists". Where a value cannot be + reproduced, that is a finding about the assignment, not a reason to relax + the check. +3. **The spine → unit map is complete and one-to-one**: every spine value in + the table has exactly one unit, and no unit appears under two spines. A + spine added with no unit reddens. +4. `python3 tests/parallel -j 4`, `bash tests/run`, `python3 bin/perry-lint`, + `git diff --check`. + +## Baseline + +Measure it in your own worktree. `test_diagnose` is expected red for a reason +that is not yours (TASK-126 — a check whose only live references are inside the +record describing its own fix). If anything else is red, say so; your +measurement wins over this line. + +## Files in scope + +- `perry/design/DESIGN-008-track-axes.md` § 5.2 and its `## Changes` +- the coverage check and its test module + +## Out of scope + +- **Any change to `bin/`, `modes/`, or `.perry/config.md`.** Steps 2–4 of § 6 + own those and they are not open. Annotating the mode files is step 3. +- Revisiting decisions #1 or #2. If you believe one is wrong, **say so in your + result and stop at saying so** — a locked decision changes through `revise`, + by a human, not inside the row that implements it. +- Any other section of the design. diff --git a/perry/handoff/2026-08-20.md b/perry/handoff/2026-08-20.md new file mode 100644 index 0000000..db0bb1a --- /dev/null +++ b/perry/handoff/2026-08-20.md @@ -0,0 +1,168 @@ +# Handoff — 2026-08-20 + +> Read this, then `perry/BOARD.md`, then `perry/journal/2026-08/2026-08-20.md`. +> Everything below is from `bin/perry-state --json` unless a line says otherwise. + +## Do these three things first, in this order + +1. **Merge origin down.** Local `feat/work-modes` is **ahead 10, behind 13**. + All four PRs (#18, #19, #20, #21) are merged on the remote and none of that + code is here. This exact divergence cost half of today: three test modules + were red locally for a fix that was already on origin, and an agent measured + its baseline against a tree missing it. + + ``` + git -C /Users/bytedance/proj/Perry merge origin/feat/work-modes + ``` + + It needs the user's clearance (`git pull` family in `.perry/hook.md`), and + last time it was run it stopped before the commit — check `.git/MERGE_HEAD` + and finish with `git commit --no-edit` if it is there. + +2. **Re-run the suite.** Expect **one** red: `test_diagnose`, two tests, which + is TASK-126 and is cleared for dispatch. Anything else is new. + + ``` + python3 tests/parallel -j 4 + ``` + +3. **Close the four review rows** whose PRs are merged and whose evidence is + attached: TASK-100, TASK-111, TASK-127, TASK-133. They were left open on + purpose — the merged code is not in the local tree yet, so nothing here + could verify them. + + **TASK-094 is not one of them.** Its PR merged, but verification item 1 asked + for 0 header-resolution call sites across all three stores and `BOARD.md` + keeps 13 splits / 87 resolutions on four registers that have no store + (`## Cadence`, `## Intake`, `## User Input Queue`, `## Top risks`). Either + its target moves, or it stays open behind DESIGN-007. That is a scope + decision for the user, not a closing note. + +## Cleared and ready to dispatch — two rows, both gated, both released + +The user cleared both escalation refusals on 2026-08-20. Specs are written. + +| Row | Fragment | Spec | +|---|---|---| +| **TASK-126** — the dangling-id row cannot be closed without writing the record that re-dangles it | `diagnose` | `evidence/2026-08/TASK-126-spec.md` | +| **TASK-140** — every mode contract slot is assigned to an axis | `design/` | `evidence/2026-08/TASK-140-spec.md` | + +TASK-126's hit reads as a false positive (the fragment matched the *filename* +`bin/perry-diagnose`, not the diagnose **execute stage**). TASK-140's hit is +**real** — its deliverable genuinely edits a locked design document. Both were +released by name, and the release is recorded on each row's `Next action`. + +**Dispatch rules that still hold:** every automated dispatch goes to +`claude-subagent`, never `codex`. `PERRY_MAX_DISPATCH_SUBAGENT=3`, but the real +bound is CPU — pass `python3 tests/parallel -j 4` in every prompt, and know that +`bash tests/run` ignores `-j` and costs a full 8. Cut every worktree from +`feat/work-modes` **after** step 1, or the PR sweeps unpushed commits into its +diff. + +## What landed today + +**Two signatures.** TASK-038 (DESIGN-005 step 4) and TASK-128 — `work` now owns +`.perry/agents.jsonl` → `.perry/roles/`, which was DESIGN-007 step 2 and had been +blocking steps 3, 4, 12 and 14 plus DESIGN-006 phase F. + +**DESIGN-008, drafted and locked in one session.** `Mode` is two axes wearing +one name. All six decisions resolved; #1 went **against the document's own +draft** (three axes → two plus a plain field). Step 1 is open as TASK-140; +steps 2–5 are deliberately **not** open, because their acceptance criteria would +otherwise all read "per step 1's table", which is not falsifiable. + +**The first non-`project` track**, on this repository: `intake`, mode `queue`. +Measured: **1 file changed, 0 state files rewritten** — `BOARD.md`, +`perry/tasks.jsonl` and `OKR.md` byte-identical by md5. That is KR-O1.3's target +met, **but only while `.perry/config.jsonl` does not exist**; the day the config +store lands, the same declaration becomes a file *and* a store write. + +**A real portability bug.** GNU `stat -f` is `--file-system` — it exits 0 and +prints a filesystem block, so `stat -f %m || stat -c %Y || echo 0` never reached +either fallback on Linux. Three scripts carried it, and `perry-update-check` runs +on **every** Perry invocation, so on Linux the 7-day throttle had been reading a +filesystem block as a timestamp all along. Green on every mac, red on every CI +run. Fixed, with a fake `stat` fixture so it is catchable without a Linux box. + +**Three numbers that did not exist this morning:** + +| | measured | +|---|---| +| KR-O2.4 — contract keys documented ≠ emitted | **17** (not 0), across **5** contracts (the KR text says three), plus **23** keys uncomparable because four of Perry's own collections are empty | +| KR-O1.3 — declaring a track | **1 file, 0 rewrites**, conditional as above | +| The Agent entity | `role` empty on **all 120** records · `owner` empty on **73** · `events.actor` holds **6** distinct free-text values across 505 events | + +## Board + +**38 open** (4 blocked, 5 review, 1 in progress) · **93 closed** — V2 3 · V3 65 · +V4 18 · V5 7 · store drift **0** · 553 events · 0 asks · 0 intake · 0 cadence. +Tracks: `main`/project, `intake`/queue. Designs: **8, all locked**. + +`intake` is declared and **empty**, and that is TASK-135: `--track` is accepted +only by `add` and `route`, so a declared track cannot be populated from work +already on the board. The six rows that genuinely arrived today — TASK-124, 125, +126, 130, 131, 132 — belong on it and cannot get there. + +## Corrected after the handoff was written + +**TASK-037 and TASK-045 were stale-blocked.** The user asked whether +TASK-037's `blocked` was right; it was not, and it was not alone. Both rows had +**every** blocker closed — TASK-037 on TASK-092 (closed the same day) and +TASK-045 on TASK-044 → TASK-047 (both closed). Both are now `not_started`. + +The reason nothing caught it is precise: `bin/perry-task:4728` computes +`startable` as `open and status not in {blocked, review} and not blocked_by` — +so **the stored status is read before `blocked_by` and masks it.** Both rows +reported `blocked_by: []` and `startable: False` at the same time, and no +consumer can see the disagreement because `startable` never contradicts a +stored `blocked`. Opened as **TASK-141**. + +TASK-050 and TASK-067 are still legitimately blocked: `blocked_by` names +TASK-094 (in review) and TASK-095 (not started). + +## The question that came after, and why it changed the answer + +*"Should triage check for tasks stranded by a process bug?"* — yes, and the +investigation found something worse than a missing check. + +- **The home already exists.** `conformance` carries this family already — + `blocked_without_dependency`, `depends_on_unknown`, `dependency_cycles`, + `next_action_cites_closed` — and **triage already reads it at step 0.5** + (`work/reference/subcommands.md:71`). This is a predicate to add, not a + feature. +- **The existing check stops one predicate short.** `blocked_without_dependency` + tests `not t["depends_on"]` — the list being *empty*. Today's rows had a + non-empty list whose every entry had closed. +- **The signal was firing, and it was read as a style nit.** + `next_action_cites_closed` named exactly TASK-037 and TASK-045. Earlier the + same day the PMO saw that check firing and *rewrote the prose to silence it*, + following "write the next step, not the history". It suppressed the symptom. + Those hits were two stranded rows raising their hands. + +Opened as **TASK-142**, with the checks traced to the incidents that motivate +them rather than invented, and one requirement that is not a check at all: +**`next_action_cites_closed` must report what it might mean**, because its +output is currently indistinguishable from a prose-style complaint. + +## Open risks + +- **KR-O3.2 has barely moved.** `gimegime-pmo` 61 → 59 lint errors against a + 2026-10-15 deadline. KR-O3.1 (PolyForge, 11 errors) is due **2026-09-30**. +- **KR-O4.2's write contract does not exist at all**, and KR-O4.3 / O4.4 both + depend on it. Due 2026-10-15. +- **`perry-goals list` returns KR targets with no progress values** (TASK-120), + so every OKR roll-up today was measured by hand. Ask for OKR status and it + gets re-measured from scratch. +- **A recurring defect class, five instances today.** A check reading live + project state as its expected value: TASK-113 fixed three, two more appeared + the moment a track was declared, and `test_diagnose` is the third generation — + *closing the row requires writing the record that reopens it.* TASK-121 exists + to make that sweep repeatable and is still `not_started`. + +## The one thing that would change tomorrow most + +O1 and O4 got **no advancement today** — every one of the 93 closes served O2 +and part of O3. O1 is now unblocked (a track exists, DESIGN-008 is locked, +TASK-140 is cleared), but O4 still has nothing: KR-O4.1 asks for zero lines of +Perry-markdown parsing in aiMark and the whole chain view still parses. +TASK-114 is `in_progress` and covers the **read** half only. diff --git a/perry/journal/2026-08/2026-08-20.md b/perry/journal/2026-08/2026-08-20.md index 2d12ed8..3bb3d09 100644 --- a/perry/journal/2026-08/2026-08-20.md +++ b/perry/journal/2026-08/2026-08-20.md @@ -112,6 +112,69 @@ - [TASK-094] evidence · — → evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md - [TASK-050] next action · unblocks on PR #20; re-scope to the adoption reader (parse_board/parse_okr with no store, parse_tracks, read_conformance, parse_phase/parse_decisions) — the fifth hardening round should be a mutation harness, not another regex - [TASK-067] next action · unblocks on PR #20 but does not become empty: perry-decide still writes DECISIONS.md, perry-goals still writes OKR.md § Commitments in place, perry-migrate still rewrites a stranger files, and ragged-row is still the only catch +- [TASK-126] — → not_started · closing the dangling-id row requires writing the record that re-dangles it · owner: Coding Agent · priority: P1 +- [TASK-126] depends on · — → TASK-112 +- [TASK-127] — → not_started · the contract docs and the payloads they describe are never diffed against each other · owner: Coding Agent · priority: P1 +- [TASK-128] — → not_started · the Agent entity has no writer because the signature that would give it one was never taken · owner: User · priority: P0 +- [TASK-129] — → not_started · Agent is five strings that do not join, and role has never once been written · owner: Coding Agent · priority: P1 +- [TASK-129] depends on · — → TASK-128 +- [TASK-128] next action · user decision: which lane writes .perry/roles/, or the card stays owner user — then the V5 signature +- [TASK-129] next action · blocked on TASK-128; the writer cannot be chosen before the signature names the lane +- [TASK-127] evidence · — → evidence/2026-08/TASK-127-spec.md +- [TASK-127] not_started → in_progress · started +- [TASK-128] evidence · — → evidence/2026-08/TASK-128-proposal.md +- [TASK-128] not_started → review +- [TASK-128] review → done · closed · evidence: `evidence/2026-08/TASK-128-proposal.md` · verification: V5 · signed off: Ran Jiao 2026-08-20 — 8 checked +- [TASK-129] next action · unblocked: work owns .perry/agents.jsonl → .perry/roles/ as of the 2026-08-20 signature; needs a spec, then dispatch +- [TASK-127] in_progress → review +- [TASK-127] evidence · evidence/2026-08/TASK-127-spec.md → evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md +- [TASK-130] — → not_started · schema README says three contracts and pins goals at a version that shipped two ago · owner: Coding Agent · priority: P2 +- [TASK-131] — → not_started · seventeen emitted contract keys are documented nowhere, and now there is a number for it · owner: Coding Agent · priority: P2 +- [TASK-132] — → not_started · the parity check cannot see 23 keys because Perry own state leaves four collections empty · owner: Coding Agent · priority: P2 +- [TASK-133] — → not_started · declare the first non-project track on Perry itself, and measure what a mixed spine costs · owner: User + Agent · priority: P1 +- [TASK-133] not_started → in_progress · started +- [TASK-134] — → not_started · probe row for the TASK-133 track experiment · owner: PMO Agent · priority: P2 +- [TASK-134] not_started → dropped · at stage: triaged · reason: probe row for the TASK-133 experiment; it proved a queue-track row carries a KR edge with no friction, and that is recorded in the evidence rather than left as a live row +- [TASK-135] — → not_started · a track can be declared but no existing row can be moved onto it · owner: Coding Agent · priority: P1 +- [TASK-136] — → not_started · a queue track SLA is parsed, stored and never measured against anything · owner: Coding Agent · priority: P1 +- [TASK-137] — → not_started · a new queue row is born in the second stage, not the first · owner: Coding Agent · priority: P2 +- [TASK-133] evidence · — → evidence/2026-08/TASK-133-track-experiment.md +- [TASK-133] in_progress → review +- [TASK-138] — → not_started · stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached · owner: Coding Agent · priority: P0 +- [TASK-138] not_started → in_progress · started +- [TASK-138] in_progress → done · closed · evidence: `evidence/2026-08/TASK-133-track-experiment.md` · verification: V3 +- [TASK-126] evidence · — → evidence/2026-08/TASK-126-spec.md +- [TASK-126] next action · spec written; dispatch pre-flight REFUSED on the fragment `diagnose` — awaiting per-task clearance +- [TASK-139] — → not_started · a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off · owner: Coding Agent · priority: P2 +- [TASK-139] depends on · — → TASK-102 +- [TASK-140] — → not_started · every mode contract slot is assigned to an axis, and the spine-to-unit map is written down · owner: Coding Agent · priority: P1 +- [TASK-140] evidence · — → evidence/2026-08/TASK-140-spec.md +- [TASK-140] next action · spec written; dispatch pre-flight REFUSED on `design/` — a real hit, the row does write a locked design doc +- [TASK-126] next action · CLEARED by the user 2026-08-20 for the `diagnose` escalation hit; ready to dispatch, spec at evidence/2026-08/TASK-126-spec.md +- [TASK-140] next action · CLEARED by the user 2026-08-20 for the `design/` escalation hit; ready to dispatch, spec at evidence/2026-08/TASK-140-spec.md +- [TASK-100] next action · PR merged; ready to close once origin is merged down and the suite re-run locally +- [TASK-111] next action · PR merged; ready to close once origin is merged down and the suite re-run locally +- [TASK-127] next action · PR merged; ready to close once origin is merged down and the suite re-run locally +- [TASK-133] next action · PR merged; ready to close once origin is merged down and the suite re-run locally +- [TASK-094] next action · PR #20 merged but the row does NOT close on it: verification item 1 asked for 0 call sites and BOARD.md keeps 13 splits / 87 resolutions on four storeless registers — needs a scope decision, not a close +- [TASK-037] blocked → not_started +- [TASK-045] blocked → not_started +- [TASK-141] — → not_started · a row stays blocked after its blockers close, because the stored status masks the computed one · owner: Coding Agent · priority: P1 +- [TASK-142] — → not_started · triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene · owner: Coding Agent · priority: P1 +- [TASK-142] next action · design question answered 2026-08-20: it belongs in conformance, which triage already reads at step 0.5 — not as a new triage feature +- [TASK-143] — → not_started · two PRs each green on their own base merged into a red tree, and nothing checked the pair · owner: Coding Agent · priority: P1 +- [TASK-100] review → done · closed · evidence: `evidence/2026-08/TASK-100-dispatch-2026-08-20-1730.md` · verification: V3 +- [TASK-111] review → done · closed · evidence: `evidence/2026-08/TASK-111-dispatch-2026-08-20-1930.md` · verification: V3 +- [TASK-127] review → done · closed · evidence: `evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md` · verification: V3 +- [TASK-133] review → done · closed · evidence: `evidence/2026-08/TASK-133-track-experiment.md` · verification: V3 +- [TASK-126] not_started → in_progress · started +- [TASK-140] not_started → in_progress · started +- [TASK-126] evidence · evidence/2026-08/TASK-126-spec.md → evidence/2026-08/TASK-126-dispatch-2026-08-21.md +- [TASK-140] evidence · evidence/2026-08/TASK-140-spec.md → evidence/2026-08/TASK-140-dispatch-2026-08-21.md +- [TASK-120] not_started → in_progress · started +- [TASK-120] evidence · — → evidence/2026-08/TASK-120-spec.md +- [TASK-126] in_progress → review +- [TASK-126] evidence · evidence/2026-08/TASK-126-dispatch-2026-08-21.md → evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md ## Notes @@ -498,6 +561,204 @@ - **Out of scope**: — - **KR linkage**: unlinked +### TASK-126 — closing the dangling-id row requires writing the record that re-dangles it + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: LOAD-02 can reach zero on a project that has documented its own fix: the three structural marks (signed record, blockquote, paragraph naming a check by code or test name) are extended so that a dispatch record whose subject IS the check does not count as a live reference, or the check reports a separate reason when its only live mentions are in documents about itself +- **Verification**: measured now: every live mention of REL-00 and DESIGN-900 is in perry/evidence/2026-08/TASK-113-dispatch-2026-08-20-1813.md lines 11/16/18/35, and every other mention in the repository already classifies as a report; after the fix that file stops being a live reference without its prose being edited, and a genuinely live reference in an ordinary evidence file still counts +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-127 — the contract docs and the payloads they describe are never diffed against each other + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: a mechanical two-way diff, per contract file under schema/, between the field paths the document declares and the field paths the tool actually emits: documented-but-not-emitted and emitted-but-not-documented, each reported with a count and a per-contract baseline that lives in a file rather than in a memory. Covers every contract present, not the three the KR text was written against +- **Verification**: removing one documented key from its emit site turns the check red; adding one emitted key the document does not declare turns it red; the per-contract counts are printed; and the run names how many contract files it found, so a contract added later cannot be silently skipped +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-128 — the Agent entity has no writer because the signature that would give it one was never taken + +- **Owner**: User +- **Priority**: P0 +- **Track / mode**: main / project +- **Deliverable**: DESIGN-007 step 2 resolved: either a new signed row in SKILL.md § The hand-off contract naming which lane writes .perry/roles/, or a signed decision that the card stays owner user and the Agent store lives elsewhere. Recorded at the precision of the 2026-08-16 signature — what was checked, not that it was reviewed +- **Verification**: V5 — the signer names the date and what they checked; tests/test_ownership.py accepts the path afterwards rather than refusing it; DESIGN-007 steps 3, 4, 12 and 14 stop citing step 2 as their blocker +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-129 — Agent is five strings that do not join, and role has never once been written + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: DESIGN-007 step 3: Agent becomes a store with an id and typed may_touch[] / must_escalate[]; the card under .perry/roles/ is rendered from it rather than being its definition; the task store role field becomes a foreign key or is deleted; events actor carries the id +- **Verification**: measured 2026-08-20: role is empty on all 120 task records, owner is empty on 73 of 120, and the event actor field holds six distinct free-text values across 505 events (agent 442, codex 28, PMO Agent 26, Coding Agent 4, Review Agent 4, coding agent 1). After: one join key, those six resolve to declared ids, and reverting the change breaks the test +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-130 — schema README says three contracts and pins goals at a version that shipped two ago + +- **Owner**: Coding Agent +- **Priority**: P2 +- **Track / mode**: main / project +- **Deliverable**: schema/README.md § The three read contracts names all five contracts and reads the version out of each contract file rather than restating it, so perry-goals/list/2.0 cannot be listed as 1.0 again +- **Verification**: the section lists five; bumping any contract version in its own file and re-running the check reports the README as stale; TASK-127 parity numbers are unchanged by the edit +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-131 — seventeen emitted contract keys are documented nowhere, and now there is a number for it + +- **Owner**: Coding Agent +- **Priority**: P2 +- **Track / mode**: main / project +- **Deliverable**: the 12 undeclared keys on perry-task/list/1.11 (semantics plus its three children, and the six nested conformance entry shapes) and the 5 on perry-goals/list/2.0 are declared in their contract documents, taking the parity count from 17 to 0 +- **Verification**: the TASK-127 parity check reports 0 emitted_not_documented across all five contracts, and its baseline file records the drop from 17 +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-132 — the parity check cannot see 23 keys because Perry own state leaves four collections empty + +- **Owner**: Coding Agent +- **Priority**: P2 +- **Track / mode**: main / project +- **Deliverable**: a fixture project the parity check can be pointed at with --root that populates intake.rows, asks.items, roles.cards and expired_sunsets, so the 23 unplaced keys are compared rather than reported unplaced +- **Verification**: the check run against the fixture reports 0 unplaced keys where Perry own repo reports 23; removing one documented key from an entry shape reddens it there +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-133 — declare the first non-project track on Perry itself, and measure what a mixed spine costs + +- **Owner**: User + Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: this repository declares one queue track in .perry/config.md § Tracks, at least one row sits on it carrying a kr: edge, and the experiment reports three measured numbers: files changed, state files rewritten, and where the mode-specific triage question diverges from what a project-mode spine needs +- **Verification**: git diff --stat names every file the declaration touched; perry-state --json before and after shows which state files changed; the queue triage step runs and its output is quoted verbatim, including any step it reports it cannot run +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-134 — probe row for the TASK-133 track experiment + +- **Owner**: PMO Agent +- **Priority**: P2 +- **Track / mode**: intake / queue +- **Deliverable**: exists only to test whether a queue-track row can carry a KR edge and appear in queue triage +- **Verification**: deleted at the end of the experiment +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-135 — a track can be declared but no existing row can be moved onto it + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: an existing task row can change track: --track is accepted by a subcommand that operates on a row that already exists, not only by add at creation and route from an intake row. Moving a row onto a queue-mode track stamps Arrived and the first post-intake Stage the same way route does, and moving it off does not silently strand those fields +- **Verification**: measured 2026-08-20: perry-task has no track subcommand and status --track is not accepted, so a project declaring a second track starts it empty and cannot populate it from work already on the board; after the fix, six existing rows move to a declared queue track and the board renders them with Arrived and Stage, and moving a row to a track that is not declared is refused by name +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-136 — a queue track SLA is parsed, stored and never measured against anything + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: the queue breach step exists: today − Arrived is computed per row against its track SLA and surfaced in perry-state --json, so triage can ask what breached rather than being told it cannot +- **Verification**: measured 2026-08-20: the intake track carries sla 5d and perry-state computes only stage_counts and wip_breaches for it; grep finds no consumer of a track SLA outside classify_due, which governs a Commitments Due cell rather than a row clock. After: a row whose Arrived is older than its track SLA appears in the payload, and one inside it does not +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-137 — a new queue row is born in the second stage, not the first + +- **Owner**: Coding Agent +- **Priority**: P2 +- **Track / mode**: main / project +- **Deliverable**: perry-task add --track sets Stage to the first declared stage, or the default is documented as deliberate with the reason +- **Verification**: measured 2026-08-20: intake declares new→triaged→in_progress→resolved and TASK-134 was created with stage triaged, skipping new; after the fix a row created on that track reads new, and a track declaring a different first stage gets that one +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-138 — stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached + +- **Owner**: Coding Agent +- **Priority**: P0 +- **Track / mode**: intake / queue +- **Deliverable**: bin/perry-dispatch-limit mtime() returns an integer on both BSD and GNU stat: it tries the GNU form first, which fails cleanly on BSD, and rejects any output that is not all digits rather than trusting the exit code. A regression fixture with a fake stat on PATH reproduces the GNU behaviour so the defect is catchable on a mac +- **Verification**: with a fake stat that mimics GNU (-f prints a filesystem block and exits 0, -c %Y prints digits), the current code produces the CI error "syntax error in expression" and the fixed code produces a clean count; reverting the fix reddens the new test +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-139 — a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off + +- **Owner**: Coding Agent +- **Priority**: P2 +- **Track / mode**: intake / queue +- **Deliverable**: a task carries its design id in a field the lifecycle does not destroy, and walk_design counts that field; DESIGN-001 stops appearing in pending_handoff without any evidence file being edited to mention it +- **Verification**: measured 2026-08-20: DESIGN-001 is the only design with zero matches in both perry/tasks.jsonl blobs and .perry/events.jsonl, while its six implementation tasks TASK-001..006 are all done at V3 with real evidence, and the 2026-08-16 journal shows every one of them was defined with Next action naming a DESIGN-001 phase. After the fix DESIGN-001 reports handed off, and a design that genuinely has no implementation tasks still reports pending +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-140 — every mode contract slot is assigned to an axis, and the spine-to-unit map is written down + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: main / project +- **Deliverable**: DESIGN-008 § 5.2 completed row by row: each of the ~50 contract slots across modes/project.md, modes/pipeline.md, modes/queue.md and modes/inquiry.md (~28 distinct) is assigned to exactly one of spine / flow / derived / plain-field, with a one-line reason for any that is not obvious; plus an explicit spine-to-unit map, since decision #2 made the unit derived and an unmapped spine value would be unrepresentable rather than merely awkward +- **Verification**: the table accounts for every slot in all four mode files, proved by a check that extracts the slot names from the mode files and fails on one the table does not mention; a slot deleted from a mode file and a slot added to one both redden it; the four presets each expand to a spine and a flow that reproduce that mode file own contract table, checked value by value rather than asserted +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-141 — a row stays blocked after its blockers close, because the stored status masks the computed one + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: intake / queue +- **Deliverable**: a row whose depends_on are all closed stops reporting as blocked without anyone noticing by hand: either the close path clears the status of every dependent it just unblocked, or startable stops letting a stored status mask an empty blocked_by and the payload surfaces the disagreement by name +- **Verification**: measured 2026-08-20: TASK-037 and TASK-045 both read status blocked with blocked_by [] and startable False, because bin/perry-task line 4728 excludes status blocked before it ever reads blocked_by, so the stored value wins over the computed one and nothing can contradict it. TASK-037 had been stale since TASK-092 closed the same day and TASK-045 since TASK-047 did. After the fix a row unblocked by a close is reported at that moment, and a row with a genuinely open blocker still reads blocked +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-142 — triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: intake / queue +- **Deliverable**: the conformance block gains the stranded-row checks that todays incidents would have needed, each traced to the incident that motivated it rather than invented: (a) blocked_by_closed_rows — open, status blocked, depends_on non-empty, every entry terminal, which is one predicate away from the existing blocked_without_dependency; (b) in_progress_with_no_live_run — open, status in_progress, no dispatch slot registered and no event newer than the tracks own staleness threshold; (c) review_idle — open, status review, no event for longer than the threshold. And next_action_cites_closed reports what it might MEAN rather than only the pattern, because its output is currently indistinguishable from a prose-style nit +- **Verification**: measured 2026-08-20: with TASK-037 and TASK-045 restored to blocked, blocked_by_closed_rows names exactly those two and blocked_without_dependency still names none, proving the new predicate is not the old one; a row blocked on a genuinely open dependency (TASK-050 on TASK-094) is NOT named, proving it discriminates; the two agents that starved at the watchdog on 2026-08-20 are reproducible as in_progress_with_no_live_run from a fixture; and reverting each predicate reddens its own case rather than a shared one +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + +### TASK-143 — two PRs each green on their own base merged into a red tree, and nothing checked the pair + +- **Owner**: Coding Agent +- **Priority**: P1 +- **Track / mode**: intake / queue +- **Deliverable**: a merge into the integration branch is checked against the merged result, not only against each PR own base: either CI runs the suite on the merge commit rather than the head, or a pre-merge step re-runs it against the branch tip and reports which pair disagrees +- **Verification**: measured 2026-08-21: PR #14 put the two store files into claims[] at e3f8621 and PR #15 shipped tests asserting the pre-claim world; each was green on its own base and the merged tree had two red tests in test_state_cost that neither PR could have seen. After the fix, reconstructing that pair reproduces the red before the merge lands, and a pair that genuinely does not interact still passes +- **Dependencies**: — +- **Out of scope**: — +- **KR linkage**: unlinked + ## V5 sign-off **TASK-107 — V5 sign-off. Ran Jiao, 2026-08-20.** @@ -584,3 +845,16 @@ report a signal without acting on it. The deciding argument is ADR-004's own shape: migration is the one road an undeclared project has, and a refusal there is the wall with no door this project rejects everywhere else. + +**TASK-128 — V5 sign-off. Ran Jiao, 2026-08-20.** + +**checked** + +- the edit is one table row in SKILL.md § The hand-off contract; goals and decide are byte-identical across it *(Perry verified)* +- SKILL.md goes 20410 → 20457 bytes against the 20480 cap in tests/test_router_budget.py — 23 bytes left *(Perry verified)* +- schema/state-schema.json line 2102 currently reads OWNER IS user, NOT A LANE for .perry/roles/*.md, and becomes wrong on this commit *(Perry verified)* +- tests/test_ownership.py refuses a lane-owned path the contract does not list; it refused this one *(Perry verified)* +- DESIGN-007 § 5.3 already names .perry/agents.jsonl → .perry/roles/*.md; no path was chosen here *(Perry verified)* +- the paragraph under the table still reads Two changes from the previous contract and gains no mention of this one; there is no room *(Perry verified)* +- DESIGN-007 decision #2 — the store is the definition, the card is rendered output — carries a 2026-08-19 signature and is not reopened here *(restated — Perry did not verify this)* +- a hand edit to a role card will read as drift, the same behaviour BOARD.md has had since ADR-007 decision 2 *(restated — Perry did not verify this)* diff --git a/perry/phase/002-linkage.md b/perry/phase/002-linkage.md index 46f3f81..6004397 100644 --- a/perry/phase/002-linkage.md +++ b/perry/phase/002-linkage.md @@ -1,7 +1,7 @@ --- linkage: 1 phase: "002-fields-are-typed" -updated: "2026-08-20T09:57:31Z" +updated: "2026-08-20T20:32:00Z" objectives: - id: O1 title: "The three stores are stores" diff --git a/perry/tasks.jsonl b/perry/tasks.jsonl index 9255a92..71df8d5 100644 --- a/perry/tasks.jsonl +++ b/perry/tasks.jsonl @@ -31,11 +31,9 @@ {"id": "TASK-034", "title": "aimark integration — one call answers both of §1.3's questions", "owner": "", "status": "done", "priority": "", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V5", "evidence": "evidence/2026-08/TASK-034-lifecycle.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": null, "order": null, "summary": ""} {"id": "TASK-035", "title": "perry-decide: writer + missing bootstrap + list contract", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-035-perry-decide.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T00:55:19", "order": null, "summary": ""} {"id": "TASK-036", "title": "perry-goals/list/1.0 read contract", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-036-goals-contract.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T00:55:19", "order": null, "summary": ""} -{"id": "TASK-037", "title": "perry-goals writer", "owner": "Coding Agent", "status": "blocked", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "After TASK-092 lands, rescope to flag naming and the module-scope handler defect only", "depends_on": ["TASK-092"], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-17T00:55:19", "order": 0, "summary": ""} {"id": "TASK-039", "title": "perry-task: User Input Queue — ask/answer subcommands, tool stamps the clock", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-039-user-input-queue.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T12:14:37", "order": null, "summary": ""} {"id": "TASK-041", "title": "perry-task: correct a Next action without a status change", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-041-next-action.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T13:12:01", "order": null, "summary": ""} {"id": "TASK-043", "title": "Conformance marker: a project declares it is Perry-shaped, at version N", "owner": "", "status": "done", "priority": "P0", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-043-close.md", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T15:59:34", "order": null, "summary": ""} -{"id": "TASK-045", "title": "Retire the runtime tolerance branches, behind the conformance marker", "owner": "Coding Agent", "status": "blocked", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "blocked on chain 044 → 047 → 045; switching to the head of it", "depends_on": ["TASK-044", "TASK-047"], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-17T15:59:34", "order": 2, "summary": ""} {"id": "TASK-046", "title": "A queue track must declare an SLA at creation — no default", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "modes/queue.md, bin/perry-lint, schema/state-schema.json, tests/test_work_modes.py", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T17:45:07", "order": null, "summary": ""} {"id": "TASK-048", "title": "perry-diagnose's own output does not validate against Perry's schema", "owner": "", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "bin/perry-lint, schema/state-schema.json, tests/test_diagnose.py", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T17:57:06", "order": null, "summary": ""} {"id": "TASK-049", "title": "Migration's injected header block merges into an adjacent blockquote, and mixes languages", "owner": "", "status": "done", "priority": "P0", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "bin/perry-migrate + tests/test_migrate.py TestAHeaderBlockIsNotAnyQuotedText", "next_action": "", "depends_on": [], "commitment": "", "parent": "", "group": "", "role": "", "created": "2026-08-17T17:59:22", "order": null, "summary": ""} @@ -96,27 +94,47 @@ {"id": "TASK-086", "title": "DESIGN-002 decision 4 says lint warns on a collision; lint does not emit NS-01", "owner": "Coding Agent", "status": "done", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V2", "evidence": "evidence/2026-08/TASK-086-dispatch-2026-08-20-1429.md", "next_action": "user confirms the strict-flag contract change (the namespace warning is no longer promoted to a failure), then merge PR #9; PR #7 is superseded", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-19T10:09:57", "order": null, "summary": ""} {"id": "TASK-038", "title": "tasks: the task store becomes canonical, BOARD.md becomes a projection", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V5", "evidence": "evidence/2026-08/TASK-038-v5-signoff-request.md", "next_action": "V5 sign-off pending; PR #12 lands the selection-based signature and TASK-038 is its first real use", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-17T00:55:19", "order": null, "summary": ""} {"id": "TASK-109", "title": "a V5 sign-off is composed by selection from measured facts, not authored from memory", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V5", "evidence": "evidence/2026-08/TASK-109-dispatch-2026-08-20-1623.md", "next_action": "merge PR #12; the first V5 close after it lands is the real test of the selection prompt", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T15:49:41", "order": null} -{"id": "TASK-112", "title": "the sign-off drafting guard cannot describe itself, so a true statement about it is refused", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T16:52:29", "order": 5} -{"id": "TASK-114", "title": "aiMark reads Perry through the current contracts instead of a pin nine versions old", "summary": "", "owner": "Coding Agent", "status": "in_progress", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-114-delegation-prompt.md", "next_action": "delegated to an aiMark coding agent; awaiting paste-back", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:05:48", "order": 7} +{"id": "TASK-112", "title": "the sign-off drafting guard cannot describe itself, so a true statement about it is refused", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T16:52:29", "order": 4} +{"id": "TASK-114", "title": "aiMark reads Perry through the current contracts instead of a pin nine versions old", "summary": "", "owner": "Coding Agent", "status": "in_progress", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-114-delegation-prompt.md", "next_action": "delegated to an aiMark coding agent; awaiting paste-back", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:05:48", "order": 6} {"id": "TASK-079", "title": "Migration writes a file the user marked read-only, via rename", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-079-dispatch-2026-08-20-1345.md", "next_action": "user verifies: merge PR #6, and decide USER-004 (refuse vs report) which this task deliberately left open", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-18T16:13:35", "order": null, "summary": ""} {"id": "TASK-108", "title": "LOAD-03 counts prose about a decision, so documenting an open question makes the count go up", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-108-dispatch-2026-08-20-1547.md", "next_action": "merge PR #10, then swap the literal GATE_OFF constant in tests/test_diagnose.py for the shared import", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T15:13:45", "order": null} -{"id": "TASK-116", "title": "the mention list is write-only, mislabelled, and two of its carve-outs are unpinned", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:24:46", "order": 6} -{"id": "TASK-117", "title": "two tools disagree about whether the board has drifted when the event log is absent", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:25:40", "order": 7} -{"id": "TASK-118", "title": "the id minters read three sources and the canonical store is not one of them", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:25:40", "order": 8} -{"id": "TASK-100", "title": "tasks.jsonl is in no claims[] entry, so a namespace collision on it cannot be reported", "owner": "Coding Agent", "status": "review", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-100-dispatch-2026-08-20-1730.md", "next_action": "merge PR #14; the shape predicate added to bin/perry-lint is outside the declared scope and was flagged for a reviewer", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-19T11:44:02", "order": 4, "summary": ""} +{"id": "TASK-116", "title": "the mention list is write-only, mislabelled, and two of its carve-outs are unpinned", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:24:46", "order": 5} +{"id": "TASK-117", "title": "two tools disagree about whether the board has drifted when the event log is absent", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:25:40", "order": 6} +{"id": "TASK-118", "title": "the id minters read three sources and the canonical store is not one of them", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:25:40", "order": 7} {"id": "TASK-115", "title": "two guards on the read-only report have a hole beside them, found by mutation not by review", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-115-dispatch-2026-08-20-1751.md", "next_action": "dispatched 17:27 via claude-subagent; worktree pinned to feat/work-modes; awaiting completion", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T17:15:57", "order": null} -{"id": "TASK-119", "title": "the linkage graph is documented as machine-written and no tool writes it", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:58:10", "order": 8} -{"id": "TASK-120", "title": "the linkage edges are read but never folded into KR progress", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:58:10", "order": 9} +{"id": "TASK-119", "title": "the linkage graph is documented as machine-written and no tool writes it", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:58:10", "order": 7} {"id": "TASK-113", "title": "three checks read the project living around them, so ordinary progress turns them red", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-113-dispatch-2026-08-20-1813.md", "next_action": "dispatched 17:09 via claude-subagent; worktree pinned to feat/work-modes; awaiting completion", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:04:40", "order": null} -{"id": "TASK-121", "title": "the sweep that found four more live-state assertions runs once and then is thrown away", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:13:04", "order": 10} +{"id": "TASK-121", "title": "the sweep that found four more live-state assertions runs once and then is thrown away", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:13:04", "order": 9} {"id": "TASK-092", "title": "OKR.md and .perry/config.md become stores with renderers", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-092-dispatch-2026-08-20-1654.md", "next_action": "V4 review re-dispatched after the first reviewer stalled at 600s; PR #16 is merged and nine rows wait on this row closing", "depends_on": ["TASK-090"], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-19T10:27:50", "order": null, "summary": ""} -{"id": "TASK-122", "title": "the repair path the tools advertise leaves the file needing a whitespace fix", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:30:44", "order": 11} -{"id": "TASK-123", "title": "the goals writer takes the file as truth and derives the store, which is the opposite direction from the KR", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:30:45", "order": 12} +{"id": "TASK-122", "title": "the repair path the tools advertise leaves the file needing a whitespace fix", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:30:44", "order": 10} +{"id": "TASK-123", "title": "the goals writer takes the file as truth and derives the store, which is the opposite direction from the KR", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T18:30:45", "order": 11} {"id": "TASK-110", "title": "measure what Perry state costs and propose a retention policy, deleting nothing", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-110-dispatch-2026-08-20-1725.md", "next_action": "the user decides the retention policy; the measurement says the whole proposal recovers 1.4 days of growth", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T16:23:45", "order": null} {"id": "TASK-070", "title": "Perry's own state is 19.5% of the tracked repo and grows unbounded", "owner": "Coding Agent", "status": "dropped", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-070-context.md", "next_action": "decide the retention proposal in TASK-110's evidence; nothing is deleted until then", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-18T00:13:21", "order": null, "summary": ""} -{"id": "TASK-111", "title": "a test reads two files outside the repository, so it is green here and red on CI forever", "summary": "", "owner": "Coding Agent", "status": "review", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-111-dispatch-2026-08-20-1930.md", "next_action": "PR #19 review: the corpus is IN_REPO-only and CI-covered; merge after origin is reconciled", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T16:38:31", "order": 6} -{"id": "TASK-124", "title": "the conformance corpus reads a project outside the repo and has no committed substitute", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T19:23:55", "order": 9} -{"id": "TASK-125", "title": "the Anti-Goals-inside-a-version insert case runs only on the author machine", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T19:23:55", "order": 10} -{"id": "TASK-094", "title": "Delete the header rule and the row splitter for the three stores", "owner": "Coding Agent", "status": "review", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md", "next_action": "scope decision: PR #20 leaves 13 splits / 87 resolutions on four storeless BOARD registers, so verification item 1 (0 call sites) is unmet as written", "depends_on": ["TASK-090", "TASK-092"], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-19T10:27:51", "order": 1, "summary": ""} +{"id": "TASK-124", "title": "the conformance corpus reads a project outside the repo and has no committed substitute", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T19:23:55", "order": 8} +{"id": "TASK-125", "title": "the Anti-Goals-inside-a-version insert case runs only on the author machine", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T19:23:55", "order": 9} {"id": "TASK-050", "title": "One normalization for a header cell, not two", "owner": "Coding Agent", "status": "blocked", "priority": "P0", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "unblocks on PR #20; re-scope to the adoption reader (parse_board/parse_okr with no store, parse_tracks, read_conformance, parse_phase/parse_decisions) — the fifth hardening round should be a mutation harness, not another regex", "depends_on": ["TASK-094"], "commitment": "", "parent": "", "group": "P0 (must finish this period)", "role": "", "created": "2026-08-17T19:24:52", "order": 0, "summary": ""} {"id": "TASK-067", "title": "The writer can destroy the table it writes to, and perry-lint cannot see it", "owner": "Coding Agent", "status": "blocked", "priority": "P0", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "evidence/2026-08/TASK-067-finding.md", "next_action": "unblocks on PR #20 but does not become empty: perry-decide still writes DECISIONS.md, perry-goals still writes OKR.md § Commitments in place, perry-migrate still rewrites a stranger files, and ragged-row is still the only catch", "depends_on": ["TASK-094", "TASK-095"], "commitment": "", "parent": "", "group": "P0 (must finish this period)", "role": "", "created": "2026-08-17T23:34:14", "order": 1, "summary": ""} +{"id": "TASK-128", "title": "the Agent entity has no writer because the signature that would give it one was never taken", "summary": "", "owner": "User", "status": "done", "priority": "P0", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V5", "evidence": "evidence/2026-08/TASK-128-proposal.md", "next_action": "V5 sign-off: a human names the date and what they checked", "depends_on": [], "commitment": "", "parent": "", "group": "P0 (must finish this period)", "role": "", "created": "2026-08-20T19:57:28", "order": null} +{"id": "TASK-129", "title": "Agent is five strings that do not join, and role has never once been written", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "unblocked: work owns .perry/agents.jsonl → .perry/roles/ as of the 2026-08-20 signature; needs a spec, then dispatch", "depends_on": ["TASK-128"], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T19:57:28", "order": 13} +{"id": "TASK-130", "title": "schema README says three contracts and pins goals at a version that shipped two ago", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V2", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T20:21:00", "order": 10} +{"id": "TASK-131", "title": "seventeen emitted contract keys are documented nowhere, and now there is a number for it", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V2", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T20:21:00", "order": 11} +{"id": "TASK-132", "title": "the parity check cannot see 23 keys because Perry own state leaves four collections empty", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T20:21:00", "order": 12} +{"id": "TASK-134", "title": "probe row for the TASK-133 track experiment", "summary": "", "owner": "PMO Agent", "status": "dropped", "priority": "P2", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V2", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T20:29:48", "order": null} +{"id": "TASK-135", "title": "a track can be declared but no existing row can be moved onto it", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T20:31:13", "order": 14} +{"id": "TASK-136", "title": "a queue track SLA is parsed, stored and never measured against anything", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T20:31:13", "order": 15} +{"id": "TASK-137", "title": "a new queue row is born in the second stage, not the first", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V2", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T20:31:13", "order": 13} +{"id": "TASK-138", "title": "stat -f succeeds on GNU with a different meaning, so the mtime fallback is never reached", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P0", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V3", "evidence": "evidence/2026-08/TASK-133-track-experiment.md", "next_action": "fixing in the main checkout; it blocks every PR", "depends_on": [], "commitment": "", "parent": "", "group": "P0 (must finish this period)", "role": "", "created": "2026-08-20T20:41:42", "order": null} +{"id": "TASK-139", "title": "a design back-reference lives in a cell the close path clears, so a finished design reports as never handed off", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": ["TASK-102"], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-20T21:04:19", "order": 14} +{"id": "TASK-094", "title": "Delete the header rule and the row splitter for the three stores", "owner": "Coding Agent", "status": "review", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-094-dispatch-2026-08-20-1958.md", "next_action": "PR #20 merged but the row does NOT close on it: verification item 1 asked for 0 call sites and BOARD.md keeps 13 splits / 87 resolutions on four storeless registers — needs a scope decision, not a close", "depends_on": ["TASK-090", "TASK-092"], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-19T10:27:51", "order": 1, "summary": ""} +{"id": "TASK-037", "title": "perry-goals writer", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "unblocked: TASK-092 closed 2026-08-20. Re-scope per its own note — flag naming and the module-scope handler defect only; the rest was overtaken by TASK-092 and TASK-123", "depends_on": ["TASK-092"], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-17T00:55:19", "order": 0, "summary": ""} +{"id": "TASK-045", "title": "Retire the runtime tolerance branches, behind the conformance marker", "owner": "Coding Agent", "status": "not_started", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V4", "evidence": "—", "next_action": "unblocked: the whole chain closed — TASK-044 and TASK-047 are both done. The conformance marker enforces on this branch, which is the precondition this row was waiting for", "depends_on": ["TASK-044", "TASK-047"], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-17T15:59:34", "order": 2, "summary": ""} +{"id": "TASK-141", "title": "a row stays blocked after its blockers close, because the stored status masks the computed one", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T21:21:09", "order": 17} +{"id": "TASK-142", "title": "triage has no check for a row stranded by a process bug, and the one signal that fired was read as prose hygiene", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V3", "evidence": "—", "next_action": "design question answered 2026-08-20: it belongs in conformance, which triage already reads at step 0.5 — not as a new triage feature", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T21:24:30", "order": 18} +{"id": "TASK-143", "title": "two PRs each green on their own base merged into a red tree, and nothing checked the pair", "summary": "", "owner": "Coding Agent", "status": "not_started", "priority": "P1", "track": "intake", "stage": "triaged", "stage_since": "", "arrived": "2026-08-20", "verification": "V3", "evidence": "—", "next_action": "—", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T23:09:32", "order": 19} +{"id": "TASK-100", "title": "tasks.jsonl is in no claims[] entry, so a namespace collision on it cannot be reported", "owner": "Coding Agent", "status": "done", "priority": "P2", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-100-dispatch-2026-08-20-1730.md", "next_action": "PR merged; ready to close once origin is merged down and the suite re-run locally", "depends_on": [], "commitment": "", "parent": "", "group": "P2", "role": "", "created": "2026-08-19T11:44:02", "order": null, "summary": ""} +{"id": "TASK-111", "title": "a test reads two files outside the repository, so it is green here and red on CI forever", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-111-dispatch-2026-08-20-1930.md", "next_action": "PR merged; ready to close once origin is merged down and the suite re-run locally", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T16:38:31", "order": null} +{"id": "TASK-127", "title": "the contract docs and the payloads they describe are never diffed against each other", "summary": "", "owner": "Coding Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-127-dispatch-2026-08-20-2045.md", "next_action": "PR merged; ready to close once origin is merged down and the suite re-run locally", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T19:57:28", "order": null} +{"id": "TASK-133", "title": "declare the first non-project track on Perry itself, and measure what a mixed spine costs", "summary": "", "owner": "User + Agent", "status": "done", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-133-track-experiment.md", "next_action": "PR merged; ready to close once origin is merged down and the suite re-run locally", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T20:29:08", "order": null} +{"id": "TASK-140", "title": "every mode contract slot is assigned to an axis, and the spine-to-unit map is written down", "summary": "", "owner": "Coding Agent", "status": "in_progress", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-140-dispatch-2026-08-21.md", "next_action": "dispatched to claude-subagent; worktree pinned to 13cfe2f; escalation on `design/` released by the user 2026-08-20 for this row only", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T21:08:52", "order": 16} +{"id": "TASK-120", "title": "the linkage edges are read but never folded into KR progress", "summary": "", "owner": "Coding Agent", "status": "in_progress", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-120-spec.md", "next_action": "dispatched to claude-subagent; worktree pinned to 7c0bb99; state-schema.json scoped out so the gate passes without a release", "depends_on": [], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T17:58:10", "order": 8} +{"id": "TASK-126", "title": "closing the dangling-id row requires writing the record that re-dangles it", "summary": "", "owner": "Coding Agent", "status": "review", "priority": "P1", "track": "main", "stage": "", "stage_since": "", "arrived": "", "verification": "V3", "evidence": "evidence/2026-08/TASK-126-dispatch-2026-08-21-result.md", "next_action": "PR #22 — the suite is fully green; verify the strong anti-vacuity case survives review, then close at V3", "depends_on": ["TASK-112"], "commitment": "", "parent": "", "group": "P1", "role": "", "created": "2026-08-20T19:57:10", "order": 12} diff --git a/reference/hand-off-contract.md b/reference/hand-off-contract.md index fb48a4e..d09ae7f 100644 --- a/reference/hand-off-contract.md +++ b/reference/hand-off-contract.md @@ -45,3 +45,36 @@ This single rule is what keeps the set composable and lets you drop in a fifth lane later (e.g. `research-journal`, `risk-review`) without breakage — a new lane is a directory with a `SKILL.md`, a row in the table above, and an entry in the routing reference. + +## 2026-08-20 — `.perry/agents.jsonl` → `.perry/roles/` moves to `work` + +The third ownership change, and the second to carry its own signature. + +**What moved.** One row: `work`'s "Only writer of" cell gained +`.perry/agents.jsonl` → `.perry/roles/`. `goals` and `decide` are byte-identical +across the edit. + +**Why it needed a signature at all.** `.perry/roles/*.md` was `owner: user` — +deliberately outside every lane's write contract, on the reasoning that a role +card is a declaration the project makes about itself, like `.perry/hook.md`. +DESIGN-007 decision #2 (signed 2026-08-19) made the store the definition and the +card rendered output, which means something renders it, which means a lane +writes it. `tests/test_ownership.py` refused the lane-owned path while this +table did not list it, and it was right to; `schema/state-schema.json`'s own +note said in advance that this was the shape the change would take. + +**Why `work` and not `decide`.** The dispatch pre-flight and `delegate` read a +role card on every run, and both are `work` procedures. `work` already renders a +store to markdown (`perry/tasks.jsonl` → `BOARD.md`), so the pattern is the one +it has. Putting the file behind `decide` would have made a read-hot path depend +on a lane that is not loaded when the read happens. + +**What it costs, recorded because it was known before the signature and not +after.** A hand edit to a role card is now drift, the same behaviour `BOARD.md` +has had since ADR-007 decision 2. `SKILL.md` lands at 20,457 bytes against a +20,480 cap — 23 bytes of headroom, so the next ownership change forces a trim of +the router before it can be written, and the account above is here rather than +there for that reason. The paragraph under the table still reads "Two changes +from the previous contract"; it describes the 2026-08-16 edit accurately and +gains no mention of this one, so the table alone no longer carries its own +history. diff --git a/schema/state-schema.json b/schema/state-schema.json index e65859e..01f9b5f 100644 --- a/schema/state-schema.json +++ b/schema/state-schema.json @@ -2109,11 +2109,11 @@ "id": "role-card", "path": ".perry/roles/*.md", "template": "work/state/role_card_TEMPLATE.md", - "owner": "user", + "owner": "work", "tier": 1, "required": false, "anchor": "project", - "note": "DESIGN-006 section 5.2. A role card is a HIRING CONTRACT the harness instantiates, never a workflow (decision #1). One file per role. A role is warranted only when it has a permission boundary or an acceptance standard distinct from the default - finance and legal that are both 'read files, run nothing, user reviews output' are one role with two knowledge topics, not two roles. OWNER IS `user`, NOT A LANE, and the distinction is load-bearing: `tests/test_ownership.py` refused a lane-owned path that the signed hand-off contract does not list, and it was right to - a new lane-owned file needs a fresh V5 signature, not a quiet entry in the test's exclusion list. A role card is not lane state. It is a declaration the project makes about itself, like `.perry/hook.md`, which is `user` for the same reason. Section 5.2's existence test - a role is warranted only after a real permission or acceptance collision - is a human judgement, and `packs/` ships TEMPLATES rather than written cards. No lane writes here, so the contract's ownership table does not move and needs no second signature.", + "note": "DESIGN-006 section 5.2. A role card is a HIRING CONTRACT the harness instantiates, never a workflow (decision #1). One file per role. A role is warranted only when it has a permission boundary or an acceptance standard distinct from the default - finance and legal that are both 'read files, run nothing, user reviews output' are one role with two knowledge topics, not two roles. OWNER IS `work`, AND THE CARD IS RENDERED OUTPUT, NOT THE DEFINITION - the store `.perry/agents.jsonl` is the truth (DESIGN-007 decision #2 and section 5.3). This owner changed on 2026-08-20 and the change cost exactly what this note used to say it would: `tests/test_ownership.py` refused the lane-owned path while the signed hand-off contract did not list it, and it was right to, so the row moved under a fresh V5 signature on `SKILL.md § The hand-off contract` rather than through a quiet entry in the test's exclusion list. The consequence a reader must know: a HAND EDIT TO A ROLE CARD IS NOW DRIFT, the same behaviour `BOARD.md` has had since ADR-007 decision 2. What did not change: `.perry/hook.md` is still `user`; section 5.2's existence test - a role is warranted only after a real permission or acceptance collision - is still a human judgement, and `packs/` still ships TEMPLATES rather than written cards, so nothing writes a card the project did not ask for.", "header_fields": [ { "name": "Accepted by", diff --git a/tests/test_host_support.py b/tests/test_host_support.py index ed97426..b7922d8 100644 --- a/tests/test_host_support.py +++ b/tests/test_host_support.py @@ -248,6 +248,109 @@ def test_dead_process_lock_is_recovered(self): self.assertEqual(len(self.markers(home)), 1) +class TestMtimeIsPortable(unittest.TestCase): + """`stat -f` means two different things, and only one of them is a format. + + On BSD (`stat -f %m`) it is the format flag. On GNU coreutils `-f` is + `--file-system`: it **succeeds**, exit 0, and prints four lines about the + filesystem. So `stat -f %m || stat -c %Y || echo 0` never reaches either + fallback on Linux, `mtime` returns a paragraph, and `$((now - last))` dies + with *"syntax error in expression"*. + + This was green on every mac and red on every CI run — the same + machine-dependence class as `test_goals_writer`'s corpus. It is reproduced + here with a fake `stat` on `PATH` so the defect is catchable **without a + Linux box**, which is the only reason it stayed alive. + """ + + GNU_STAT = ( + '#!/bin/sh\n' + 'case "$1" in\n' + ' -f) shift; [ "$1" = "%m" ] && shift\n' + ' echo " File: \\"$1\\""\n' + ' echo " ID: 542238b4501be5b9 Namelen: 255 Type: ext2/ext3"\n' + ' echo "Block size: 4096 Fundamental block size: 4096"\n' + ' exit 0 ;;\n' + ' -c) shift; [ "$1" = "%Y" ] && shift\n' + ' for c in /usr/bin/stat /bin/stat; do\n' + ' [ -x "$c" ] && { "$c" -f %m "$1"; exit $?; }\n' + ' done; exit 1 ;;\n' + 'esac\n' + 'exit 1\n' + ) + + def register_under_gnu_stat(self, home: Path): + """Register one slot with a marker already on disk, so `clean_stale` + has something to compute an age for. That arithmetic is the crash.""" + fake = home / "fakebin" + fake.mkdir(parents=True, exist_ok=True) + (fake / "stat").write_text(self.GNU_STAT) + (fake / "stat").chmod(0o755) + flight = home / ".cache/perry/in-flight" + flight.mkdir(parents=True, exist_ok=True) + (flight / "TASK-A-codex.json").write_text("{}") + env = clean_host_env(HOME=str(home)) + env["PATH"] = f"{fake}:{env.get('PATH', '')}" + return subprocess.run( + [str(LIMIT), "register", "TASK-B", "codex"], + capture_output=True, text=True, env=env, cwd=ROOT, + ) + + def test_a_gnu_stat_does_not_break_the_age_arithmetic(self): + with tempfile.TemporaryDirectory() as tmp: + r = self.register_under_gnu_stat(Path(tmp)) + self.assertNotIn("syntax error in expression", r.stderr) + self.assertNotIn("Fundamental block size", r.stderr) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("Slot reserved", r.stdout) + + def test_the_marker_already_present_is_still_counted(self): + """The crash was in `clean_stale`, so a fix that made the error go away + by never reading the directory would pass the test above and be wrong.""" + with tempfile.TemporaryDirectory() as tmp: + r = self.register_under_gnu_stat(Path(tmp)) + self.assertIn("2 / 2 for codex", r.stdout) + + def test_no_script_carries_the_broken_chain(self): + """Three scripts had it, and `perry-update-check` runs on **every** + Perry invocation — so on Linux the throttle was reading a filesystem + block as a timestamp on every single call, silently. A grep guard, + because the other two have no test harness of their own and the defect + is a one-line shape.""" + offenders = [] + for script in sorted((ROOT / "bin").iterdir()): + if not script.is_file() or script.suffix == ".py": + continue + try: + src = script.read_text(encoding="utf-8", errors="replace") + except OSError: + continue + for n, line in enumerate(src.split("\n"), 1): + if line.lstrip().startswith("#"): + continue + if "stat -f" in line and "stat -c" in line and "||" in line: + offenders.append(f"{script.name}:{n}: {line.strip()}") + self.assertEqual( + offenders, [], + "a `stat -f … || stat -c …` chain is unreachable past its first " + "arm on GNU coreutils, where `-f` is `--file-system` and exits 0:\n" + + "\n".join(offenders)) + + def test_the_mac_form_still_works(self): + """The fix tries the GNU form first. BSD `stat` rejects `-c` outright, + which is why that order is safe — but only if it really is rejected.""" + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) + r = self.run_limit(home, "register", "TASK-C", "codex") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("1 / 2 for codex", r.stdout) + + def run_limit(self, home: Path, *args: str, **extra: str): + env = clean_host_env(HOME=str(home), **extra) + return subprocess.run([str(LIMIT), *args], capture_output=True, + text=True, env=env, cwd=ROOT) + + class TestOpenCodeSetup(unittest.TestCase): def run_setup(self, home: Path, cwd: Path, *args: str, path: str | None = None): diff --git a/tests/test_md_store.py b/tests/test_md_store.py index 2cf7bb9..bb2b78f 100644 --- a/tests/test_md_store.py +++ b/tests/test_md_store.py @@ -46,6 +46,7 @@ sys.path.insert(0, str(ROOT / "viewer")) import parsers as P # noqa: E402 import perry_md_store as M # noqa: E402 +import perry_store as S # noqa: E402 from gate import GATE_OFF # noqa: E402 @@ -133,8 +134,24 @@ def test_config_including_its_prose_section(self): # The section V4 step 2 names. It is PROSE: the store must hold no # record for it and the renderer must not touch a byte of it. self.assertIn("## Why the state root is not `.`", path.read_text()) - self.assertEqual(report["kinds"], {"setting": len(records)}) - keys = {r["key"] for r in records} + # Every record is accounted for by kind, and no kind is invented. + # This used to read `{"setting": len(records)}` — true only while this + # repository had declared no tracks, so declaring one reddened it + # (TASK-133). The invariant is that the KINDS PARTITION the records and + # that the prose section contributes none; "which kinds" is a fact + # about the file, so it is derived from the file rather than restated. + self.assertEqual(sum(report["kinds"].values()), len(records)) + self.assertEqual(set(report["kinds"]), + {r["kind"] for r in records}) + has_register = "## Tracks" in path.read_text() + self.assertEqual("track" in report["kinds"], has_register, + "the store holds track records exactly when the file " + "declares a `## Tracks` register") + # Only a `setting` record has a `key`; a `track` record is keyed by + # its track name. The old line iterated every record, which worked + # only while every record was a setting — the same assumption the + # assertion above used to carry, one line further down. + keys = {r["key"] for r in records if r["kind"] == "setting"} for expected in ("document_language", "state_root", "code_repo_path"): self.assertIn(expected, keys) @@ -329,6 +346,110 @@ def test_a_blank_marker_is_replaced_once_the_store_has_a_value(self): self.assertNotIn("- Code repo path: —", rendered) +class TestARepairedLineCarriesNoWhitespaceTheInputDidNotHave( + unittest.TestCase): + """TASK-122 — the repair `bin/perry_md_store.py` advertises, byte for byte. + + The refusal message tells the reader to run `render --write` "to bring the + file back in line". It has to be safe to obey: a repaired bullet came back + with two spaces after the colon and a trailing one, so the advice the tool + gave produced a file the reader's next `git diff --check` complained about. + + Both halves are asserted on the same run, because the value of these cases + is the CONTRAST. A table cell is joined on `|` and must be handed padding + it lost; a bullet slot sits between literal spans that already carry it. + Reverting `describe_cell`'s rule must redden the bullet cases here and + leave `test_a_table_cell_that_lost_its_padding_is_still_given_it_back` + green — one change reddening both would mean the two paths were never + separated at all. + """ + + def test_a_bullet_slot_the_store_disagrees_with_renders_byte_exact(self): + """The reproduction from the spec, unchanged. + + The literal span is `'- Repo layout: '` — the space after the colon is + already in it — so the slot must contribute the value and nothing else. + """ + line = "- Repo layout: single" + start = line.index("single") + rec = {"repo_layout": "split"} + desc, findings = S.slot_descriptor( + line, [(start, len(line), "repo_layout")], rec) + self.assertEqual(S.render_line(desc, rec), "- Repo layout: split") + # Still a disagreement — this is about how the repaired line reads, + # not about whether the drift is reported. + self.assertEqual([f["column"] for f in findings], ["repo_layout"]) + + def test_a_config_setting_slot_ends_without_a_trailing_space(self): + """`scan_config` opens the slot at the colon, so the slot owns the + separator's space and the render must not add a second one at the end. + + Asserted on the real `.perry/config.md`, because that is the file the + refusal message names. + """ + text = (ROOT / ".perry" / "config.md").read_text() + records = M.derive(M.CONFIG, text) + next(r for r in records if r["key"] == "state_root")["value"] = "docs" + rendered, _ = M.render(M.CONFIG, text, records) + line = next(ln for ln in rendered.split("\n") + if ln.startswith("- State root:")) + self.assertEqual(line, "- State root: docs") + self.assertEqual( + [ln for ln in rendered.split("\n") if ln != ln.rstrip()], [], + "render --write introduced trailing whitespace into the file it " + "was advertised as the repair for") + + def test_a_table_cell_that_lost_its_padding_is_still_given_it_back(self): + """The other side of the seam, on the same run. + + `render_line` joins on `|`, which carries no whitespace of its own, so + a cell arriving as `single` has to leave as `| split |`. This case is + what makes the bullet cases above a RULE rather than a blanket ban on + padding. + """ + rec = {"repo_layout": "split"} + cell = S.describe_cell("single", "repo_layout", rec) + self.assertEqual((cell["lead"], cell["trail"]), (" ", " ")) + desc = {"pre": "|", "post": "|", "sep": "|", "escape": True, + "cells": [cell]} + self.assertEqual(S.render_line(desc, rec), "| split |") + + def test_the_advertised_repair_survives_git_diff_check(self): + """V3 item 4, run rather than asserted. + + A real `.perry/config.md` in a real repository, drifted, repaired by + the exact command the refusal message prints, and handed to the exact + check a commit hook would run. + """ + p = Project(self) + self.assertEqual(p.config("write", "--from-file").returncode, 0) + + def git(*args): + return subprocess.run(["git", *args], cwd=str(p.root), + capture_output=True, text=True) + + git("init", "-q") + git("config", "user.email", "t@example.invalid") + git("config", "user.name", "t") + git("add", "-A") + commit = git("commit", "-qm", "baseline") + self.assertEqual(commit.returncode, 0, commit.stderr) + + cfg = p.root / ".perry" / "config.md" + cfg.write_text(cfg.read_text().replace("- State root: perry", + "- State root: elsewhere")) + self.assertEqual(p.config("diff").returncode, 1, + "the planted drift was not reported at all") + self.assertEqual(p.config("render", "--write").returncode, 0) + + check = git("diff", "--check") + self.assertEqual((check.returncode, check.stdout, check.stderr), + (0, "", "")) + # And the repair actually restored the stored value, so the clean + # `--check` is not the cleanliness of a file nothing happened to. + self.assertIn("- State root: perry", cfg.read_text()) + + class Project: """A throwaway project carrying Perry's own two files.""" diff --git a/tests/test_ownership.py b/tests/test_ownership.py index 174c8ad..182bcda 100644 --- a/tests/test_ownership.py +++ b/tests/test_ownership.py @@ -298,6 +298,12 @@ def test_the_contract_table_parses(self): "design/*.md": "design/-.md", "DECISIONS.md": "DECISIONS.md", "decisions/ADR-NNN-.md": "decisions/", + # Moved from `user` to `work` on 2026-08-20 under a fresh V5 + # signature (TASK-128, DESIGN-007 decision #2 and step 2). The + # store `.perry/agents.jsonl` is the definition; the card is + # rendered from it, which is why the card is what the contract + # names. + ".perry/roles/*.md": ".perry/roles/", } # Schema-declared, lane-owned, and NOT named in the signed contract table. diff --git a/tests/test_state_cost.py b/tests/test_state_cost.py index 08d1fba..dfcb877 100644 --- a/tests/test_state_cost.py +++ b/tests/test_state_cost.py @@ -206,12 +206,27 @@ def test_every_claimed_path_is_either_measured_or_declared_empty(self): "table nor the empty list") def test_an_unclaimed_file_under_the_state_root_is_still_reported(self): + """A file no claim covers still has to appear, or the total understates + what the project actually carries. + + **This used to prove the property with `perry/tasks.jsonl`**, which was + unclaimed when this module was written. TASK-100 then put both store + files into `claims[]` (`e3f8621`), so the example became a claimed path + and the assertion failed while the behaviour it names was still + correct. Both PRs were green on their own base and red once merged — + which is why the fixture now writes a file that nothing will ever claim + rather than borrowing one that happened to be unclaimed that week. + """ + self.write("perry/scratch-notes.md", "n" * 700) + self.commit(DAY3) # the tool reads git; an uncommitted file has no history paths = self.payload()["snapshot"]["paths"] unclaimed = [p for p in paths if "unclaimed" in p] - self.assertIn("perry/tasks.jsonl (unclaimed)", unclaimed, - f"the store is 52 KB in the live project and no claim " - f"covers it; dropping it would understate Perry's cost. " - f"got: {sorted(paths)}") + self.assertIn("perry/scratch-notes.md (unclaimed)", unclaimed, + f"a file under the state root that no claim covers was " + f"dropped from the snapshot, which understates the " + f"project's cost. got: {sorted(paths)}") + self.assertEqual(paths["perry/scratch-notes.md (unclaimed)"]["bytes"], + 700, "it is listed but its bytes are not counted") def test_the_claim_list_is_read_from_the_schema_and_not_hardcoded(self): """Anti-vacuity. If the labels were a literal in the tool, a claim @@ -280,7 +295,12 @@ def test_a_rewritten_path_carries_its_superseded_versions(self): Asserted as that number rather than as a ratio: the excess IS the superseded revision, and saying so is what licenses the claim that rotating the file recovers checkout bytes and no repository bytes.""" - row = self.payload()["snapshot"]["paths"][".perry/"] + # Read the file's OWN row. It used to roll up under `.perry/`; TASK-100 + # gave `.perry/events.jsonl` a claim of its own (`e3f8621`), so the + # directory row no longer carries it and `.perry/` now reports only + # `config.md`. The number asserted below is unchanged, because the + # behaviour never was — only which row states it. + row = self.payload()["snapshot"]["paths"][".perry/events.jsonl"] superseded = len('{"ev": 1}\n') * 100 self.assertEqual(row["history"] - row["bytes"], superseded) self.assertGreater(row["history"], row["bytes"]) diff --git a/tests/test_track_attribution.py b/tests/test_track_attribution.py index dd3dc1c..921e7ba 100644 --- a/tests/test_track_attribution.py +++ b/tests/test_track_attribution.py @@ -162,14 +162,37 @@ def test_a_sole_non_project_track_does_not_inherit_repository_evidence(self): self.assertEqual(tracks["ops"]["mode"], "pipeline") -class TestPerrysOwnProjectIsUnmoved(unittest.TestCase): - def test_it_still_reads_one_project_track(self): +class TestAProjectWithNoRegisterIsUnmoved(TrackCase): + """The no-op property `modes/project.md` is built on: a project that never + declares a register behaves exactly as it did before work modes existed. + + **This used to assert it against Perry's own repository**, which held while + Perry had declared nothing and reddened the moment it declared its first + track (TASK-133) — a check reading live project state as its expected + value, the class TASK-113 and TASK-121 are about. The property is about + *absence of a register*, so it is proved on a project that has none. What + is asserted about this repository is only what its own file says. + """ + + def test_a_project_with_no_register_reads_one_implicit_main_track(self): + tracks, w = self.modes("# Config\n\nState root: perry\n") + self.assertFalse(w["register_declared"]) + self.assertEqual([t["track"] for t in w["tracks"]], ["main"]) + self.assertEqual(tracks["main"]["mode"], "project") + + def test_this_repository_reads_back_the_register_its_file_declares(self): + declared = [ + line.split("|")[1].strip() + for line in (ROOT / ".perry" / "config.md").read_text().splitlines() + if line.startswith("|") and "---" not in line + and line.split("|")[1].strip() not in ("", "Track") + ] proc = subprocess.run( [sys.executable, str(TOOL), "--json"], capture_output=True, text=True, cwd=ROOT) w = json.loads(proc.stdout)["work_modes"] - self.assertFalse(w["register_declared"]) - self.assertEqual([t["track"] for t in w["tracks"]], ["main"]) + self.assertEqual(w["register_declared"], bool(declared)) + self.assertEqual([t["track"] for t in w["tracks"]], declared or ["main"]) if __name__ == "__main__":