task-pipeline-skill 1.98.0 → 1.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,262 @@
1
+ ## v1.99.1 (2026-10-21)
2
+
3
+ Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new cases
4
+ are regression tests in `test/audit_regressions/diet_retro_budget.py`.
5
+
6
+ **`retro_budget.py` reads an older retro's floor and migrates it** (B-149, from sshlg-skills
7
+ B-244). That retro writes its standing instructions as a numbered list and its run stamps as
8
+ `- **date: title** ...` narrative items beside a table. `check` printed `standing 0/10 rows`
9
+ for ten instructions, and `migrate` answered `nothing to move` on a 103,737-byte floor with 89
10
+ stamps, so stage 10's byte budget could never pass there.
11
+
12
+ - `check` counts numbered-list standing instructions and list stamps, measures each item
13
+ whole and names it (`standing row #6`, `stamp 2026-09-06 (list)`).
14
+ - `migrate` rotates the oldest stamps past the cap of ten, ordered by their date and never by
15
+ position, verbatim into the quarter's archive (a table row travels under its table's
16
+ header); keeps a long list item's bold rule and its `*(Retire ...)*` trigger and moves the
17
+ rest; keeps a long list stamp's bold title; and, only over budget, moves the Run stamps
18
+ section's loose prune paragraphs. Every move leaves a `sha256:` pointer that `verify` proves.
19
+ - Edge rules from two review rounds: a lead of several bold spans stays whole and a date-only
20
+ lead refuses by name; a same-day tie follows its own block's direction; a paragraph holding
21
+ a heading stays in the floor; the script's own pointer notes never move again, so a floor
22
+ still over budget is left alone by the next run; a table's header leaves only with the rows
23
+ this run rotated; a list marker, a quote, a rule or a `*(` note ends an item; a rule still
24
+ over 600 characters after its story moved is reported, never moved again.
25
+ - On a copy of the sshlg-skills retro: 103,737 bytes to 6,446, every pointer proved, no
26
+ non-blank line lost, the text outside the floor unchanged.
27
+ - Tests: `test/audit_regressions/diet_retro_budget.py` gains seventeen cases. Each failed on the
28
+ 1.99.0 script (it crashed with no archive to read) or on a reviewed intermediate version.
29
+
30
+ ## v1.99.0 (2026-10-21)
31
+
32
+ Guards: 448 → **530**; property checks 15 → **15**. There are 82 new negatives in
33
+ `lessons.py`. Each one plants a revert, and the suite must catch it:
34
+
35
+ - the whole-scan refusal put back;
36
+ - the carry-forward removed;
37
+ - status computed without carried origins;
38
+ - prune on every run;
39
+ - git worktrees no longer discovered;
40
+ - memory ids keyed by retro anchor;
41
+ - one memory id left on two class keys;
42
+ - problems attributed by path prefix;
43
+ - `emit`/`record` ignoring the origin that holds the memory id;
44
+ - memory ids compared together with their `@revision`;
45
+ - a stranger at the same path not recognised;
46
+ - `record` putting one memory id on two class keys;
47
+ - an origin removed on its own;
48
+ - a memory id moved to another project's origin;
49
+ - an origin carried as `checkout holds another project` counted;
50
+ - a project key taken from the checked-out branch;
51
+ - a stranger called on a key alone, though the origin's commit resolves;
52
+ - `record` taking another project's id;
53
+ - `record` writing an id the loader refuses;
54
+ - a title carrying a Unicode line separator;
55
+ - the index split on Unicode line breaks;
56
+ - a rebuild printing absolute paths;
57
+ - `--prune-carried-reason` accepted alone, or taking any string, or offering a reason that
58
+ never occurs;
59
+ - merged duplicate lines dropping an id silently;
60
+ - `add` counting a phantom project after a re-init;
61
+ - a shallow check trusted when it times out;
62
+ - an index line accepted with `mem:x@0` or with an unknown `carried` reason;
63
+ - an origin v1.95.0 wrote kept as a second project, by `rebuild` or by `add`;
64
+ - that re-key made for an origin with provenance, between two candidate repositories, or
65
+ merging two memory ids (in `rebuild` and in `add`);
66
+ - `record` re-attaching an id from another checkout;
67
+ - one memory id left on two live projects, or kept by the later holder;
68
+ - a project key skipping the main worktree's HEAD or `init.defaultBranch`;
69
+ - redaction replacing a relative name, or inside a longer name;
70
+ - a session note, a newer-format notice or a refusal printing an absolute path;
71
+ - a stage id carrying a line break or a path;
72
+ - a same-revision duplicate decided by the order of lines;
73
+ - a re-key taking a commit from a merged history, ignoring the key v1.95.0 would compute, or
74
+ moving a project read this run;
75
+ - `add` leaving one id on two live projects; the id kept by the smaller key instead of the
76
+ earlier holder;
77
+ - a rebuild asking git the same question again;
78
+ - `query`, an `add` refusal or the repository top printing an absolute path;
79
+ - an index set aside for a stage id v1.95.0 accepted, or one normalised past 32 characters;
80
+ - the schema admitting a blank stage id;
81
+ - a partial rebuild or an `add` moving an origin of a project the index already knows;
82
+ - an origin carried as `checkout holds another project` counted as a live holder of its id;
83
+ - a re-key that does not record where the id came from, in `rebuild` or `add`, or loses that
84
+ source on a repeated `add` or a rebuild;
85
+ - `rebuild`, `add` or `record` never giving a re-keyed id back to its source project, or
86
+ giving it back while leaving it on the other project too, or over that project's own id;
87
+ - an index line accepted with an impossible `rekeyedFrom`;
88
+ - an id dropped by the one-project or the one-class rule keeping its `rekeyedFrom`, a re-key
89
+ source given to every project holding that id, a reclaim to a project never read, the main
90
+ worktree's HEAD keying the repository before `init.defaultBranch`, and `save` writing a line
91
+ the loader refuses;
92
+ - `record --replace` keeping the replaced id's `rekeyedFrom`, and an `add` or rebuild re-key
93
+ giving a twin's own id a source it never had.
94
+
95
+ These are the fixes from verification rounds 6 to 18 of release iteration 1.
96
+
97
+ - **A replaced id takes its source with it** (round 18). `record --replace` gave the origin a
98
+ new id but kept the `rekeyedFrom` of the re-keyed id it replaced, so the next read of that
99
+ source project took X's own record, and X's later record of it was refused. An origin given
100
+ another id now drops the source. A re-key whose legacy origin held no id no longer tags the
101
+ twin's own id with the legacy project as its source.
102
+
103
+ - **No write leaves a line the loader refuses; only a read project reclaims** (round 17).
104
+ When the one-project or the one-class rule took a memory id off an origin, `rekeyedFrom`
105
+ stayed behind; the loader refuses a source beside no id, so the next command set the whole
106
+ index aside as `.corrupt-<date>` and every lesson in it left the live index. The source now
107
+ leaves with the id, and it follows the (id, project) it was recorded for: another project
108
+ holding the same id is never given one. `save` reads every line back as the loader does and
109
+ refuses to write one it would refuse, the previous index intact. A re-keyed id goes back
110
+ only to a project actually read (an origin with provenance, not carried), never to a
111
+ v1.95.0 origin that could not be re-keyed in the same run. `init.defaultBranch` now keys
112
+ the repository before the main worktree's HEAD, so a main worktree on an orphan branch does
113
+ not move the key. Known, not fixed here, each with a backlog row: a `git replace --graft`
114
+ moves the key (B-141); a v1.95.0 origin with no twin still counts as a second project
115
+ (B-142); the known-project set is index-wide (B-143); a case-insensitive path read twice
116
+ (B-144).
117
+
118
+ - **Ownership is recoverable, not predicted** (round 16). A re-key can only judge the projects
119
+ it has already seen, so an `add` in X, or a rebuild of X's root alone, could take an id
120
+ from a repository Y that had never been read. A re-key now records its source key on the
121
+ origin (`rekeyedFrom`). The first time Y is read with an origin of that class key, by
122
+ `add` or `rebuild`, or Y records the id, the id moves back to Y, is dropped from X, and a
123
+ `reclaimed:` line names it. Whichever verb runs first, the final owner is the project
124
+ whose key the legacy store used. When Y already holds its own id for that class, nothing
125
+ moves, and that is said. Each lesson plant now requires its own expected failure line, not
126
+ any non-zero exit. Known, not fixed here: the known-project set is index-wide, not per
127
+ class key.
128
+
129
+ - **Ownership no longer depends on verb order** (round 15). The live-project rule now covers
130
+ every project the previous index records with provenance, not only the projects this run
131
+ read: an `add` in a repository, or a rebuild over its root alone, never re-keys another
132
+ known project's origin, and a later full rebuild agrees. That rule subsumes the separate
133
+ "no re-key with provenance" check (an origin with provenance names a known project), so the
134
+ check and its plant (492) are retired; the F1c test stays. Known limit, not fixed here: on a
135
+ case-insensitive file system one checkout reached by two spellings of its path is read
136
+ twice (real paths do not fold case), which can count a phantom second project; no memory
137
+ id ends on two projects.
138
+
139
+ - **The re-key and the one-project rule, closed** (round 14).
140
+ - A provenance-less origin is re-keyed only when, in the checkout that re-derived its anchor,
141
+ its commit descends from the root the new key names (so it resolves there) and v1.95.0
142
+ would have computed its key there. Round 13's separate "commit resolves" plant (490) is
143
+ retired: the roots check subsumes it, so reverting it alone changes nothing. So is its
144
+ "own key re-derived" plant (493): an origin whose own key is re-derived belongs to a project
145
+ read this run, which the next rule already keeps. A monorepo that merged another repository's history keeps
146
+ its hands off that repository's origin and memory id.
147
+ - An origin whose project key any checkout read this run holds is never re-keyed, whatever
148
+ its anchor.
149
+ - `add` keeps one memory id on one live project, against the index as it read it.
150
+ - The M/M2 fixtures put the earlier holder's key above the later one, so "the smaller key"
151
+ fails every run.
152
+ - One `cat-file` per (checkout, commit) in a run.
153
+ - Every verb's messages name files, the index home-relative; `path` alone prints a path.
154
+ - A stage id v1.95.0 accepted that lesson/1 now refuses is normalised on read and said, so no
155
+ line and no memory id goes to the corrupt file. The schema's string stage ids are not blank.
156
+ - The pre-tag admission packet cites the v1.94.0 commit, not the tag object, which a clone
157
+ without tags does not hold.
158
+
159
+ - **Upgrade and ownership in `lessons.py`** (round 13).
160
+ - An index written by v1.95.0 (keyed on the smallest root of HEAD, no provenance) no longer
161
+ turns a repository that merged an unrelated history into two projects. An origin with no
162
+ provenance whose `retro#anchor` this run re-derives under exactly one other key, and whose
163
+ commit resolves in that checkout, is re-keyed: its memory id follows, and `emit` and
164
+ `record` keep using it. `add` does the same for its own checkout. Never with provenance,
165
+ never when its own key is re-derived, never between two candidates, never merging two ids.
166
+ - `record` re-attaches an id held by an origin carried as `checkout holds another project`
167
+ only from a checkout in that origin's provenance (a re-init at the same path).
168
+ - A rebuild never leaves one memory id on two live projects: the earlier holder keeps it,
169
+ and the other origin is named and keeps its place without it.
170
+ - Without `origin/HEAD`, `main` or `master`, the project key is the main worktree's HEAD,
171
+ then `init.defaultBranch`, so a linked worktree on an orphan branch is the same project.
172
+ - Redaction replaces only absolute paths, as whole path components (`--roots .` deleted every
173
+ dot), and stderr (the session's notes, the newer-format notice, a refusal) is redacted
174
+ like stdout (in `rebuild`; every verb since round 14).
175
+ - A stage id given as a string gets the text checks: one line, at most 32 characters, no
176
+ path or credential shape.
177
+ - Two copies of one origin at the same revision keep the lexically smaller id, whatever the
178
+ order of the lines.
179
+
180
+ - **Identity and safety in `lessons.py`** (round 12).
181
+ - The project key is the repository's: the first-parent root of its default branch, once
182
+ per git common dir. A worktree on a branch that merged an unrelated history, or a checkout
183
+ on an orphan `gh-pages`, is still its project.
184
+ - `checkout holds another project` applies only when the origin's own commit does not
185
+ resolve in that checkout. `add` applies the same rule to its checkout's old-key origin, so
186
+ a re-init never counts a phantom project, even before the next rebuild.
187
+ - `record` refuses an id another project's origin holds, naming the project (exit 1), and an
188
+ id the loader would refuse (exit 2).
189
+ - lesson/1 text (a string stage id too, since round 13) refuses every line separator,
190
+ including U+2028, U+2029 and U+0085. The index
191
+ is split on `\n` only, so a value can never make it read as corrupt.
192
+ - Rebuild messages name checkouts and files, never an absolute path.
193
+ - `--prune-carried-reason` needs `--prune-carried` and offers only the reasons a rebuild
194
+ produces; otherwise it is a usage error, exit 2.
195
+ - Merged duplicate lines name the id they drop and keep the higher revision.
196
+ - A shallow check that does not answer is refused: no source, and a problem named.
197
+
198
+ - **`lessons.py rebuild` never removes or moves anything on its own** (round 11, narrowed
199
+ under the loop guard).
200
+ - Every previous origin the run does not re-derive is carried with everything recorded on
201
+ it, its memory id included, and marked `carried: <reason>`.
202
+ - `--prune-carried`, optionally narrowed by `--prune-carried-reason <reason>`, is the only way
203
+ an origin leaves the index.
204
+ - Nothing supersedes and nothing moves: a memory id stays on the origin that holds it, and is
205
+ re-attached only to the same `(project, classKey)`.
206
+ - The reasons are `checkout not read`, `retro file not read`, `no checkout read holds it`
207
+ (the checkout read the file and the entry is gone), `commit does not resolve`,
208
+ `retro cites a commit that does not resolve`, `lesson refused`, `no provenance` and
209
+ `checkout holds another project`. The last one is kept but not counted in `seen`,
210
+ `projects` and `status`; the others count as temporary unavailabilities. So no phantom
211
+ project appears, and an unreadable root never demotes a lesson.
212
+ - A shallow clone is never a source: a problem owned by its checkout, with nothing derived
213
+ under its unstable key. `add`, `emit-observatory` and `record` refuse there, exit 1, and say
214
+ to deepen it.
215
+ - Known limit: a project whose root commit changes (re-init, unshallow) starts new store
216
+ records. The operator re-attaches by `record`-ing the old id on the new origin of the same
217
+ class key.
218
+ - `record` over a different id the origin already holds needs `--replace`. It also refuses an
219
+ id another class key holds and a revision older than the one held.
220
+ - Each origin records its provenance, `checkouts`, the real paths it was derived from. It
221
+ decides only the carried reason; it stays in the local index, is never emitted, and `show`
222
+ omits it. A path is not an identity.
223
+ - Git worktrees of every checkout found are discovered, including those under
224
+ `.worktrees/` and `.claude/worktrees/`. Problems carry a structural owner and are printed by
225
+ name, never by absolute path or prefix match.
226
+ - Retro files are stamped when read and again under the lock. A file that changed in
227
+ between, such as an `add` landing mid-run, leaves its checkout not read for that run.
228
+ - Memory ids follow `(project, classKey)`, compare on the part before `@`, start at revision
229
+ 1, and keep the highest revision everywhere. One id is never on two class keys, and the run
230
+ refuses to write if an id would vanish other than by `--prune-carried`.
231
+ - Index lines are re-derived from their origins on every read: `seen`, `projects`, `status`
232
+ and the rest are never trusted from disk, and duplicate origins and duplicate lines merge.
233
+ A line with wrong types, an origin lesson that fails lesson/1, a `carried` outside the
234
+ enum, or `mem:…@0` is a corrupt line. It is salvaged around and never causes a traceback.
235
+ A memory id that only a bad line held is named in the output.
236
+ - Exit 1 means nothing could be read: every root was missing or unreadable. A readable empty
237
+ root, or one holding only a shallow clone, exits 0.
238
+
239
+ - **`lessons.py rebuild` never loses what it cannot re-derive.** Round 6's narrowed refusal
240
+ dropped an origin and its memory id while a project's root was unreadable. It also still
241
+ refused the routine case: an indexed lesson whose commit was pruned after a squash merge.
242
+ Rebuild now derives every lesson it can read. Every previous origin whose source it could
243
+ not re-derive is carried forward with everything recorded on it, marked `carried`, and
244
+ named. That covers a root not read or outside the scan, a commit that no longer resolves,
245
+ and a refused entry. Carried origins count toward `seen`, `projects` and `status`. They are
246
+ re-derived and un-marked when the source reads again, and are removed only by the new
247
+ `--prune-carried`. Rebuild exits 0 whenever anything was read, and exits 1, touching
248
+ nothing, only when nothing could be read. The loss guard and `--force` are gone.
249
+ `templates/lesson.schema.json` declares the `carried` field.
250
+ - The merge notice no longer says "read both" when the family index was corrupt and set aside.
251
+ - Docs: `continuity.md` names `graph.py next` exit `6` beside `4`, pinned by
252
+ `continuity_doc_test.py`, and `waves.md` lists `6` for `next --wave N`. The `graph.py`
253
+ docstring says exit `3` counts parked nodes as finished. In `stages.md` and `README.md`,
254
+ `checks` mode puts the release run on a watcher. The release-authority rule has one home,
255
+ `deploy-targets.md`, and `pipeline.schema.json` points to it.
256
+ - Lessons of this run, now doctrine: `iteration.md` checks for parallel work on the same
257
+ nodes before each cycle, across every tool's branch prefixes. `build.md` adds a
258
+ command-hygiene block that every background dispatch carries.
259
+
1
260
  ## v1.98.0 (2026-10-20)
2
261
 
3
262
  Guards: 448 → **448**; property checks 15 → **15**.
package/README.md CHANGED
@@ -201,7 +201,7 @@ and the inline fallback satisfy the same [provider contract](plugins/task-pipeli
201
201
  | 3 + 4 · every spec and plan | the self-review reads its rules back — is every named check real, does anything contradict a locked decision or a rejected option, and what does this cost now versus at design time. Answers land as a committed `## Self-review` of computed numbers |
202
202
  | 6–10 + any check you write | [`gates.md`](plugins/task-pipeline/skills/task-pipeline/references/gates.md) — the three axes, the promotion ladder, gate anatomy, the probe recipe, ratchet floors |
203
203
  | any agent-time enforcement | [`hooks.md`](plugins/task-pipeline/skills/task-pipeline/references/hooks.md) — the `PreToolUse` contract, the fail-open hazard, the Claude-Code-only limit |
204
- | 7 · the tag | [`deploy-targets.md`](plugins/task-pipeline/skills/task-pipeline/references/deploy-targets.md) → *When the release waits for an approval* — `pipeline.json` → `release.approval`: `checks` (tag, watch the run selected by the tag, report) or `reviewers` (the direct link to the run that is `waiting`) |
204
+ | 7 · the tag | [`deploy-targets.md`](plugins/task-pipeline/skills/task-pipeline/references/deploy-targets.md) → *When the release waits for an approval* — `pipeline.json` → `release.approval`: `checks` (push the tag, put the run selected by the tag on a watcher, take independent work until it reports) or `reviewers` (the direct link to the run that is `waiting`) |
205
205
  | between stages | [`continuity.md`](plugins/task-pipeline/skills/task-pipeline/references/continuity.md) Part 3 — workflow memory through Observatory, a planned handoff (`stage_checkpoint.py handoff` / `accept`), and `hooks/memory-nudge.sh`, which reminds once when a gate returned with no memory line after it — unless the run recorded memory unavailable with `--for-run` |
206
206
 
207
207
  **Ported, not depended on.** Stage 0 is adapted from
package/SKILL-CARD.md CHANGED
@@ -12,7 +12,7 @@ harmless.
12
12
  |---|---|
13
13
  | **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
14
14
  | **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
15
- | **Version** | 1.98.0 |
15
+ | **Version** | 1.99.1 |
16
16
  | **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
17
17
  | **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. The engine is WebKit (Safari) first; `chrome-devtools` is Chrome-only, a stated exception. Every stage's doctrine ships in-repo; user-facing stage 3 requires a valid scenario artifact; super-ux is preferred, with equivalent-provider and inline-fallback paths under the provider contract |
18
18
  | **Evaluation status** | Suite authored, 5 categories. One recorded run, **self-observed by the author**; **zero blind runs on zero of three models** — the split, and the numbers, live in [`evals/RESULTS.md`](evals/RESULTS.md) and are computed by `evals/run.py` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-pipeline-skill",
3
- "version": "1.98.0",
3
+ "version": "1.99.1",
4
4
  "description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "task-pipeline": "bin/task-pipeline.js"
@@ -3,7 +3,7 @@
3
3
  "name": "task-pipeline",
4
4
  "displayName": "Task Pipeline",
5
5
  "description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/judgment/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a proportional short path for small changes, a loop guard whose ceilings stop and record a blocker, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs); a third, `waves`, takes the whole board and closes it in waves that each finish in one pass. Retro insights can publish upstream as issues, opt-in and redacted.",
6
- "version": "1.98.0",
6
+ "version": "1.99.1",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -236,7 +236,7 @@
236
236
  "minLength": 1,
237
237
  "pattern": "\\S",
238
238
  "not": {"pattern": "[\\r\\n]"},
239
- "description": "Optional single-line source locator (for example docs/DECISIONS.md:42) for the specific standing authorization covering the tag and its publication. The locator is not authorization: read the source and verify its authority, currentness and scope at stage 7. Absent: inspect existing recorded authority before asking; do not infer a grant from checks approval, loop mode or this field's mere presence (references/deploy-targets.md)."
239
+ "description": "Optional single-line source locator (for example docs/DECISIONS.md:42) for the standing authorization covering the tag and its publication. A locator, not a grant: how it is read and verified lives in references/deploy-targets.md, When the release waits for an approval."
240
240
  },
241
241
  "approval": {
242
242
  "enum": [
@@ -233,6 +233,21 @@ Record the implementer's agent identity: fix rounds 1–3 resume it.
233
233
  > commit range, a one-line test summary, and your concerns. Ask before starting
234
234
  > if anything in the brief is ambiguous — questions are cheaper than rework.
235
235
 
236
+ **Command hygiene** — put this in every background dispatch, whether it goes to an
237
+ implementer, a reviewer or a verifier:
238
+
239
+ > Avoid commands that the harness holds for permission. Do not write `cd X;` compounds: use
240
+ > absolute paths or `git -C`. Do not write `rm -rf $VAR`: use a literal path or `"${VAR:?}"`.
241
+ > Do not edit repository files with inline heredocs: use the editor tool or a script file.
242
+ > If a command has not returned in about 5 minutes and no process is running, rephrase it.
243
+
244
+ *A lesson of release iteration 1 (2026-10-13): background agents stopped silently. Each was
245
+ waiting on a command the harness held for a permission no one could see. The triggers were a
246
+ `cd X; python3 - <<EOF` compound and `rm -rf $HOME` with HOME overridden. Source: the
247
+ iteration's run record, which is private, and
248
+ [task-pipeline#125](https://github.com/ssheleg/task-pipeline/pull/125), which carries the
249
+ rule.*
250
+
236
251
  ### 4.1a Decisions settled inside a task — and who may write them down
237
252
 
238
253
  The report above already asks for *"decisions you made"*, and stage 5 settles more
@@ -108,9 +108,12 @@ questions are marked `needs-operator` and made answerable without context
108
108
  ([`prioritisation.md`](prioritisation.md) → *5. Where the human goes*); external waits
109
109
  get watchers, and project-owned questions go to the owning Project under
110
110
  [`iteration.md`](iteration.md)'s board rule. The run continues on every node it does
111
- not block. When the whole frontier is blocked — `graph.py next` exits `4` — classify the
112
- recorded reasons: external waits go on watchers, human decisions go into the question
113
- batch. Exit 4 does not prove a human gate or completion. A watcher keeps the recorded
111
+ not block. When the whole frontier is blocked, `graph.py next` says which kind of block it
112
+ is. Exit `6` means every remaining dependency leaf is a recorded watched wait: keep those
113
+ watchers armed and take independent work ([`work-graph.md`](work-graph.md) → *Watched
114
+ external waits*). Exit `4` means a mixed, unclassified, running or parked barrier remains:
115
+ classify the recorded reasons, put external waits on watchers and human decisions into the
116
+ question batch ([`waves.md`](waves.md)). Neither code proves a human gate or completion. A watcher keeps the recorded
114
117
  next action alive while no independent node is runnable. **When the board is empty, say so once**, and do not invent work to fill it.
115
118
 
116
119
  **Stop is not idle.** A finish line reached closes that iteration — stages 9–10, the
@@ -75,6 +75,22 @@ act. **Step 11 runs stage 8's verification trio** on what was released. Steps 12
75
75
  with stage 10. No gate in [`stages.md`](stages.md) is skipped. The cycle changes when each
76
76
  stage runs, and over what.
77
77
 
78
+ **Before each cycle, check for parallel work on the same nodes.** In every target repository,
79
+ list the open PRs and the recently merged ones by branch prefix. Include other CLIs' prefixes,
80
+ such as `codex/`, `fix/` and `release/`, and do not stop at your own tool's live sessions:
81
+
82
+ ```bash
83
+ gh pr list -R <owner>/<repo> --state open --json number,headRefName,title
84
+ gh pr list -R <owner>/<repo> --state merged -L 20 --json number,headRefName,mergedAt
85
+ ```
86
+
87
+ Each node has one owner. When work has to be split, record the split on the PR you would
88
+ otherwise collide with. *A lesson of release iteration 1 (2026-10-13): fix PRs from other
89
+ tools' branches landed on nodes a fix cycle was rebuilding, and a later cycle was briefed on
90
+ a PR that had already merged. Source: the iteration's run record, which is private, and
91
+ [task-pipeline#125](https://github.com/ssheleg/task-pipeline/pull/125), which carries the
92
+ rule.*
93
+
78
94
  ## Module dossiers
79
95
 
80
96
  **One dossier per cluster.** The inventory is grouped by the part of the system each item
@@ -259,13 +275,152 @@ private to its project and never leaves it.
259
275
  **Layer 1 — every install, no service.** The project retro and [`learned.md`](learned.md),
260
276
  as before, plus a **local index** of every project's lessons on this machine:
261
277
  `scripts/lessons.py`, one JSON line per class, a pure function of the retros
262
- (`lessons.py rebuild --roots <dir>` re-derives it). Rebuild replaces the projection for
263
- its explicitly selected roots and depth: choose the whole intended scope. A readable empty
264
- scope can produce an empty index; a missing or unreadable root, discovery error, malformed
265
- artifact configuration, missing explicitly configured artifact directory, dangling source
266
- link, nonregular retro input, or invalid/unresolved lesson refuses before changing the index
267
- or its migration state. An absent default artifact directory in a new repository remains
268
- a valid empty source. Repair the named source and retry; unknown is never measured zero.
278
+ (`lessons.py rebuild --roots <dir>` re-derives it). **A rebuild never removes or moves
279
+ anything on its own.** It derives every lesson from the retros it can read under the roots and
280
+ depth given, and from every git worktree of each checkout it finds. Worktrees under
281
+ `.worktrees/` or `.claude/worktrees/` are included, even though the walk skips
282
+ dot-directories.
283
+
284
+ An origin is one project's copy of a lesson, `(project, retro)`. It records its
285
+ **provenance**: the checkouts it was derived from, by real path. That record stays in the
286
+ local index; it is never emitted, and `show` omits it. Provenance decides only the reason an
287
+ origin is carried, never a removal.
288
+
289
+ **Every previous origin the run does not re-derive is carried.** Nothing is removed and nothing
290
+ is moved: no origin is dropped because a checkout read its retro and no longer found the entry,
291
+ and no memory id moves to another project's origin. **`rebuild --prune-carried` is the only way
292
+ an origin leaves the index**. `--prune-carried-reason <reason>` narrows it to one reason. It
293
+ needs `--prune-carried`, and it offers only the reasons listed below; anything else is a usage
294
+ error, exit 2. A
295
+ carried origin keeps everything recorded on it, including its memory id, and is marked
296
+ `carried: <reason>`, named in the summary. It is re-derived and un-marked as soon as a checkout
297
+ holding it reads again. One thing is re-keyed rather than carried, on positive evidence only:
298
+ an origin written before provenance existed (v1.95.0 keyed a project on the smallest root
299
+ commit of HEAD) whose `retro#anchor`, under the same class key, this run re-derives under
300
+ exactly one other key, and in the checkout that re-derived it: its own commit resolves there,
301
+ v1.95.0 would have computed exactly its key there, and the commit descends from the root the new
302
+ key names (its own history, not a history the repository merged). It is the same repository, so
303
+ it takes the new key and its memory id follows; `add` does the same for its own checkout. It is
304
+ never re-keyed when it has provenance, when its project key is held by any checkout read this
305
+ run or by any origin of the previous index that has provenance (so the verb and the roots
306
+ given never decide who owns an id), when two keys could claim it, or when the copy under the new key holds a different memory
307
+ id (then it stays carried, and that is named). A monorepo that merged another repository's
308
+ history therefore never takes that repository's origin.
309
+
310
+ A re-key can only judge the projects already seen, so ownership is made recoverable: the
311
+ re-keyed origin records its source key as `rekeyedFrom`. The first time that project is read
312
+ with an origin of the same class key (by `add` or `rebuild`), or records the id itself, it
313
+ takes the id back; the other origin keeps its place without it, and a `reclaimed:` line says
314
+ so. Read means an origin with provenance that is not carried: a v1.95.0 origin under that key
315
+ that could not be re-keyed is not a read, so the run that re-keys never hands the id back to
316
+ it. Whichever verb runs first, the final owner is the project whose key the legacy store used,
317
+ once that project is read. If that project already holds its own id for the class, nothing
318
+ moves, and that is named. An origin that loses its id to the one-project or the one-class rule
319
+ loses `rekeyedFrom` with it, so does an origin `record --replace` gives another id, and a
320
+ re-key records a source only for an id it moved from the legacy origin; and no verb writes an index line the loader would refuse: `save`
321
+ refuses instead, the previous index intact. The reasons are:
322
+
323
+ - `checkout not read`: the checkout was not discovered, or a problem is owned by it. That
324
+ problem may be an unreadable retro, a refused `git worktree list`, a shallow clone, a broken
325
+ configuration, or a retro that changed while the rebuild ran (an `add` landing mid-run).
326
+ - `retro file not read`: the checkout read completely, but that file was not read, for
327
+ example because it lies outside the artifact root or was moved.
328
+ - `no checkout read holds it`: the checkout read the file, holds the same project, and the
329
+ entry is no longer there.
330
+ - `commit does not resolve`, `retro cites a commit that does not resolve` or
331
+ `lesson refused`: the entry is there but could not be derived. A dangling commit is routine
332
+ after a squash merge and a deleted branch.
333
+ - `no provenance`: the origin was written before provenance existed, and no re-derived origin
334
+ proves it is the same repository's.
335
+ - `checkout holds another project`: every checkout in its provenance now holds a different
336
+ project key, and the origin's own commit does not resolve there. A path is not an identity.
337
+ The same repository under another key still holds the commit, so it is not a stranger;
338
+ `add` applies the same rule to its own checkout's old-key origin.
339
+
340
+ The project key is the repository's own: the first-parent root of its default branch
341
+ (`origin/HEAD`, then `main`, then `master`, then the branch `init.defaultBranch` names).
342
+ Without any of them it is the main worktree's HEAD, read in the common directory, so every
343
+ worktree gets one answer; then this checkout's HEAD. It is computed once per git common directory. A worktree on a branch that merged an unrelated history, or a
344
+ checkout sitting on an orphan `gh-pages` branch, is still its project.
345
+
346
+ **Counting.** `seen`, `projects` and `status` count re-derived origins and every carried origin
347
+ except `checkout holds another project`. Those are kept, with their memory ids, but not counted.
348
+ So a temporarily unreadable root never demotes a lesson two projects hold, and a stranger at a
349
+ path, or one checkout under a new root commit, never makes a phantom second project. Derived
350
+ fields are recomputed from the origins whenever the index is read; they are never trusted from
351
+ disk.
352
+
353
+ **Shallow clones.** A shallow checkout is never a source. It is a problem owned by that
354
+ checkout, nothing is derived from it under its unstable key, and its origins are carried as
355
+ `checkout not read`. A shallow check that does not answer is refused the same way, and since
356
+ it taught nothing, it does not count as something read. `add`, `emit-observatory` and
357
+ `record` refuse in a shallow clone, exit 1 and write nothing, and say to deepen it with
358
+ `git fetch --unshallow`.
359
+
360
+ **Known limit.** A project whose root commit changes, through a re-init or an unshallow, starts
361
+ new store records: its old origins are carried as `checkout holds another project` with their
362
+ memory ids, and the new origins start without one. The operator re-attaches by running
363
+ `record` with the old id on the new origin, from the same checkout. That is allowed, because
364
+ the new origin is of the same class key and the origin holding the id is carried as `checkout
365
+ holds another project` with this checkout in its provenance. From any other checkout it is
366
+ refused.
367
+
368
+ Another checkout of the same project, even one reading the same retro path, decides nothing
369
+ for an origin it never held. Problems carry their owner, a checkout or none, and are never
370
+ matched by path prefix.
371
+
372
+ **Memory ids.** A memory id belongs to `(project, classKey)`, the store's own identity, and
373
+ never to a retro anchor:
374
+
375
+ - A rotated or retitled entry keeps its id on the re-derived origin. The old anchor is carried.
376
+ - A class key changed inside one entry does not inherit another key's id.
377
+ - One id is one `(project, classKey)`. `record` refuses each of the following, exits 1 and
378
+ writes nothing:
379
+ - an id that another class key holds;
380
+ - an id that another project's origin holds (the refusal names that project), unless that
381
+ origin is carried as `checkout holds another project` and this checkout is in its
382
+ provenance (the re-attach above);
383
+ - a revision older than the one held;
384
+ - without `--replace`, a different id over the one the origin already holds.
385
+
386
+ It also refuses an id the index cannot hold, such as `mem:`, a quote, a backslash or a
387
+ space: that exits 2.
388
+ - One id is one project's store record. Two projects may hold it only while one of them is
389
+ carried as `checkout holds another project`. When a rebuild would leave it on two live
390
+ projects (that checkout came back), the id stays on the earlier holder: the project the
391
+ previous index carried as `checkout holds another project` with it, else the smaller project
392
+ key. The other origin keeps its place without the id, and the run names it. `add` applies the
393
+ same rule against the index as it read it, so an add from an old clone never leaves the id on
394
+ both projects either.
395
+ - Merged duplicate lines name every id they drop. Of two copies of one origin the higher
396
+ revision wins; on equal revisions, the lexically smaller id.
397
+ - Ids compare on the part before `@`, revisions start at 1, and the highest revision wins.
398
+
399
+ `emit-observatory` and `record` both use the origin of the project that holds the id. The run
400
+ refuses to write if a memory id would vanish other than through `--prune-carried`.
401
+
402
+ Unreadable sources are named under `not read`, by name rather than by absolute path. Every
403
+ root, checkout, repository top, file given and directory is shown by its name — the index files
404
+ home-relative, such as `~/.sshlg-skills/lessons/index.jsonl` — in every verb's messages, on stdout
405
+ and stderr alike (the session's notes and a refusal too). `path` alone prints the path: that is
406
+ its answer. A git question about one commit in one checkout is asked once per run. Only absolute paths are replaced, and only as whole
407
+ path components, so a root given as `.` changes no other text. The index is split on `\n` only, and
408
+ lesson/1 text refuses every line separator, including U+2028, U+2029 and U+0085. So no value
409
+ can make a good index read as corrupt. The run
410
+ exits 0 whenever anything was read: a retro, a checkout discovered in full, or a shallow
411
+ checkout found. A readable root that holds no checkout also exits 0: the index is written,
412
+ and every origin is carried. The run exits 1 only when nothing could be read because every
413
+ root was missing or unreadable; then no index, lock directory or migration is touched. A held
414
+ lock or an unreadable index is its own refusal. On the default path the one-time merge of a
415
+ standalone index runs first, as for every verb.
416
+
417
+ So does setting a corrupt index aside:
418
+ - The file is kept whole as `.corrupt-<date>`.
419
+ - Every line that still parses is salvaged into the live index.
420
+ - Two lines for one class key are merged.
421
+ - A memory id that only a bad line held is named in the output.
422
+
423
+ Unknown is never measured zero, and never measured gone.
269
424
  Two projects holding one class key make
270
425
  one lesson seen twice, status `candidate`. The index path is a parameter — `--index`, then
271
426
  `$TASK_PIPELINE_LESSONS`, then `~/.sshlg-skills/lessons/index.jsonl` **when
@@ -290,7 +445,8 @@ rule 2 (no host identifiers) for exactly two fields, and nothing else:
290
445
  repository's SHA names nothing to someone without the clone; **a public repository's SHA
291
446
  is searchable** and leads to the repository, so it stays in this field and never in text;
292
447
  - **`origin.project`, the pseudonymous project key** — `p-` and 12 hex digits of the
293
- repository's root commit — stable across clones and never the repository's name, because
448
+ repository's root commit (the first-parent root of its default branch, so every worktree
449
+ and branch of one repository shares it) — stable across clones and never the repository's name, because
294
450
  lessons are read into *other* projects' briefs and a repository name can be a customer's
295
451
  or a product's. The shared store receives the same key, never a `project:<slug>` id.
296
452
 
@@ -298,7 +454,10 @@ Rules 1, 3, 4 and 5 still bind every text field, on the estate as off it: no pat
298
454
  no code or value, no person's, company's, customer's or product's name, and a title that
299
455
  states the class rather than the incident. Never, in either layer, a transcript or a URL.
300
456
  `lessons.py` refuses what a pattern can see (paths, URLs, addresses, credential shapes, long
301
- hex or base64 runs) and any field lesson/1 does not name; a name it cannot see stays the
457
+ hex or base64 runs, line breaks) in every text field and in a stage id given as a string (at
458
+ most 32 characters, never blank), and any field lesson/1 does not name. A stage id an older
459
+ index holds that fails those checks is normalised when the index is read (one line, cut to 32
460
+ characters, or `stage`), and said, so the line and its memory id are never set aside; a name it cannot see stays the
302
461
  writer's duty. Off the estate (`retro.publish`) the relaxation ends: the whole origin is
303
462
  stripped.
304
463
 
@@ -205,6 +205,10 @@ narrative are how a capped list grows anyway.
205
205
  `Because`) **verbatim** into the quarter's archive, each block carrying the sha256 of
206
206
  its text, and leaves a pointer; `retro_budget.py verify` proves every pointer. An
207
207
  instruction still over 600 characters after its story moved is rewritten by hand.
208
+ An older retro whose standing instructions are a numbered list and whose stamps are
209
+ `- **date: title**` items is read the same way: the item keeps its bold rule and its
210
+ `*(Retire ...)*` trigger, a stamp keeps its title, stamps past the cap rotate out by date,
211
+ and, over budget, the Run stamps section's loose prune paragraphs move.
208
212
  - **A prune that retires nothing writes its count into the stamp row**, as
209
213
  `prune: 0 retired`, and no paragraph. Repeated "nothing to prune" notes were a
210
214
  measured share of the floor.
@@ -476,7 +480,8 @@ private to this project.
476
480
  **A lesson is not a publication, but the redaction list below binds it too — with one
477
481
  relaxation.** On this machine or estate (the local index, a shared store), rule 2 is relaxed
478
482
  for exactly two fields: the evidence commit and the pseudonymous project key (`p-` and 12 hex
479
- of the root commit, the same key in the store, never the repository's name or a
483
+ of the root commit — the first-parent root of the default branch — the same key in the store,
484
+ never the repository's name or a
480
485
  `project:<slug>` id). A private repository's SHA names nothing; a public repository's SHA is
481
486
  searchable, so it stays in its field and never in text. Rules 1, 3, 4 and 5 still bind every
482
487
  text field: no path at all, no code or value, no person's, company's, customer's or
@@ -315,14 +315,11 @@ never that the work was skipped quietly.
315
315
  close before it ships. **The
316
316
  carry-over count is printed beside this verdict.** Deploy is outward → explicit
317
317
  operator go, or a specific standing authorization naming that act, cited with its source
318
- and scope. Authorization to release does not silently authorize a separate deploy. If
319
- `release.authorized_by` is present, read that source and verify its authority,
320
- currentness and scope for the tag and publication; the locator is not authorization.
321
- If absent, inspect existing recorded authority before asking for a missing grant.
322
- **Before the tag, read
323
- the release's approval mode** (`pipeline.json` → `release.approval`; then
324
- [`deploy-targets.md`](deploy-targets.md) → *When the release waits for an approval*):
325
- `checks` — push the tag, watch the run selected by the tag, report; `reviewers` — the
318
+ and scope. How a release's authority is established, `release.authorized_by` included,
319
+ has one home: [`deploy-targets.md`](deploy-targets.md) → *When the release waits for an
320
+ approval*. **Before the tag, read the release's approval mode** (`pipeline.json` →
321
+ `release.approval`; the same section): `checks` — push the tag, put the run selected by
322
+ the tag on a watcher and take independent work until it reports; `reviewers` — the
326
323
  direct link to the run that is actually `waiting`, never a request without it. Respect deploy-from-main rules if the project mandates them. **Before
327
324
  tagging, the CI verdict for what was just pushed is READ, not assumed**
328
325
  ([`conventions.md`](conventions.md) → *The CI verdict*) — a tag on a commit whose
@@ -263,7 +263,8 @@ The work graph ([`work-graph.md`](work-graph.md)) carries `wave` on a node, an i
263
263
  - `graph.py validate` refuses an edge from a later wave's node into an earlier wave's
264
264
  node: an earlier wave that waits on a later one cannot close in its own pass.
265
265
  - `graph.py next --wave N` prints wave N's frontier only. Exit `3` says the wave is done,
266
- `4` that nothing in it is runnable, `1` that no node is in wave N.
266
+ `4` that nothing in it is runnable behind a mixed or unclassified barrier, `6` that only
267
+ recorded watched waits hold it, `1` that no node is in wave N.
267
268
  - `graph.py wave N-007 --to 3 --why "<reason>"` moves a node and writes the revision.
268
269
  It refuses a move that would break the order above.
269
270