task-pipeline-skill 1.60.1 → 1.62.0

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,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.62.0 — a board row that says work exists names where it lives
4
+
5
+ **`open` claims nothing exists. `parked` claims something does — and now has to say
6
+ where.** B-58: a row read as ready-to-merge for two days while its artifacts had already
7
+ been deleted. It said *"built and parked … held at `scratchpad/b29-exposure/`"*, a
8
+ session-scoped temp directory, and when a run finally went to land it nothing on disk, in
9
+ git history, in a stash or in a dangling object held any of it. **A board cannot tell a
10
+ claim about a filesystem from a claim about a repository**, and that row was the first
11
+ kind while reading like the second.
12
+
13
+ The status cell of a parked row now carries a branch or a commit, and two rules gate it:
14
+ an open row may not home its work in a per-session directory, and a `parked` status
15
+ without a ref is refused.
16
+
17
+ **A prose detector was written first and thrown away.** Matching *"parked"*, *"is built"*,
18
+ *"ready to merge"* in the description cell fired on **three rows out of 187 and every one
19
+ was false** — two closed rows narrating this incident and the row that asked for the rule.
20
+ A check whose every current hit is wrong teaches evasion; what ships reads the status
21
+ cell, which is never prose, and both rules measured **zero** across 191 rows including the
22
+ templates.
23
+
24
+ The first draft of the guard read `cells[-2]`, which is the status in an eight-column
25
+ board and the *Home* column in the ten-column template — so the parked rule examined the
26
+ wrong cell and reported nothing, fifty lines below the comment explaining why positional
27
+ reads fail on this corpus. It is position-free now, and the plant that was silently
28
+ passing is the one that proved it.
29
+
30
+ Guards: 347 → **349**. Property checks: 9 → 9.
31
+
32
+ ## v1.61.0 — the command that was built, parked, and lost
33
+
34
+ **The exposure command lands, two days after it was declared built.** B-43 recorded
35
+ `templates/exposure.sh` and its fixtures as finished and parked in a session scratchpad,
36
+ blocked only on a concurrent release. The scratchpad was cleaned; nothing on disk, in git
37
+ history, in a stash or in a dangling object held any of it. **The row described work that
38
+ no longer existed**, and nothing would have said so until someone went to merge it.
39
+
40
+ So it is rebuilt, and this time the validator asserts it: a seeded shell script must
41
+ exist, must carry a shebang, and must still be named by the doctrine that tells a project
42
+ to copy it. All three watched failing.
43
+
44
+ `exposure.sh` turns [`verification.md`](plugins/task-pipeline/skills/task-pipeline/references/verification.md)
45
+ into the line and the check-list `references/exposure.md` specifies, so a host project
46
+ gets the number without an agent in the room. **It exits 0 whatever the number is** — a
47
+ threshold here would be a target on `never`, and the cheapest way to satisfy such a
48
+ target is a date nobody earned.
49
+
50
+ Three of its fourteen fixtures caught real defects in the first draft, all of them lying
51
+ in the reassuring direction:
52
+
53
+ - `$(grep -c "" f || echo 0)` prints **two** zeroes when nothing matches — grep prints its
54
+ own 0 and exits 1, so the fallback runs as well. The variable became `"0\n0"`, every
55
+ numeric test after it died with *integer expression expected*, and the script fell over
56
+ precisely in the case that means "everything is confirmed".
57
+ - BSD `sort` exits with *Illegal byte sequence* on the non-ASCII `What` column under a
58
+ UTF-8 locale. The error went to stderr, the check-list came out **empty**, and the
59
+ count above it still said 126 unverified. A list that silently empties is worse than no
60
+ list.
61
+ - Byte-wise `substr` cut a Cyrillic letter in half. Truncation is by whole words now, which
62
+ cannot land inside a character.
63
+
64
+ Guards: 344 → **347**. Property checks: 9 → 9. Three new plants: the seeded script
65
+ disappearing, the doctrine that stops naming it, and a percentage reaching the exposure
66
+ line — the last because the doctrine says *no percentage, ever*, and a later hand adding
67
+ `(N%)` would be adding it to look helpful.
68
+
3
69
  ## v1.60.1 — the gate can see an invariant it breaks elsewhere
4
70
 
5
71
  **This gate can now see an invariant it breaks one repository away.** The family umbrella
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.60.1 |
15
+ | **Version** | 1.62.0 |
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. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
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.60.1",
3
+ "version": "1.62.0",
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"
@@ -2,7 +2,7 @@
2
2
  "name": "task-pipeline",
3
3
  "displayName": "Task Pipeline",
4
4
  "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/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 loop guard whose review ceiling measures rather than stops, 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). Retro insights can publish upstream as issues, opt-in and redacted.",
5
- "version": "1.60.1",
5
+ "version": "1.62.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -82,7 +82,8 @@ Usually the map and the gate; often the register already exists in some shape.
82
82
  ```bash
83
83
  cp <skill>/templates/docmap.md docs/DOCMAP.md # only if absent
84
84
  cp <skill>/templates/docgate.sh scripts/check-docs.sh # only if absent
85
- chmod +x scripts/check-docs.sh
85
+ cp <skill>/templates/exposure.sh scripts/exposure.sh # only if absent
86
+ chmod +x scripts/check-docs.sh scripts/exposure.sh
86
87
  ```
87
88
 
88
89
  **Seeding never overwrites.** An existing brief, register or map is the project's
@@ -18,6 +18,7 @@ exactly one seam, and that seam already existed as a dangling pointer.
18
18
 
19
19
  - The seam the ledger left open
20
20
  - Seeded or picked up
21
+ - A row that says work exists names where it lives
21
22
  - Priority is computed, not assigned
22
23
  - What a loop iteration does with it
23
24
  - Rationalizations
@@ -60,6 +61,42 @@ work on, and only one of them can be appended to.
60
61
  at the top of the run, not from what the last run's report said — the previous run is
61
62
  precisely what invalidated that number ([`learned.md`](learned.md) rule 16).
62
63
 
64
+ ## A row that says work exists names where it lives
65
+
66
+ **`open` claims nothing exists yet. `parked` claims something does — and then has to say
67
+ where.** The status cell of a parked row carries a branch or a commit:
68
+
69
+ ```
70
+ | B-043 | The exposure command … | 2026-08-14 run | 1 | 0 | 1 | 1.0 | parked — feat/exposure |
71
+ | B-044 | The migration script … | 2026-08-15 run | 2 | 0 | 2 | 1.5 | parked — a1b2c3d |
72
+ ```
73
+
74
+ The rule exists because a row read as ready-to-merge for two days while its artifacts had
75
+ already been deleted. It said *"built and parked … held at `scratchpad/b29-exposure/`"* —
76
+ a session-scoped temp directory. When a run finally went to land it, nothing on disk, in
77
+ git history, in a stash or in a dangling object held any of it. **A board cannot tell a
78
+ claim about a filesystem from a claim about a repository**, and that row was the first
79
+ kind while reading like the second.
80
+
81
+ So:
82
+
83
+ - **A scratchpad, `/tmp`, or any per-session directory is not a home.** Work that lives
84
+ only there is not parked, it is unwritten — say `open` and describe what is wanted, not
85
+ what supposedly exists.
86
+ - **Uncommitted work in a working tree is not parked either.** Another agent's checkout,
87
+ a stash, an unpushed branch on one machine: none of them survives the question *"can
88
+ somebody else pick this up?"*, which is the only question the board is answering.
89
+ - **The ref is checkable and that is the point.** `git rev-parse` either resolves it or it
90
+ does not, so a row that has quietly expired can be found before somebody plans around
91
+ it.
92
+
93
+ **A prose detector was tried first and discarded.** Matching *"parked"*, *"is built"*,
94
+ *"ready to merge"* in the description cell fired on **three rows out of 187 and all three
95
+ were false** — two closed rows narrating this very incident and the row that asked for
96
+ this rule. A check whose every current hit is wrong teaches evasion, and this repository
97
+ throws that shape away rather than tuning it. What is gated is the **status cell**, which
98
+ is never prose.
99
+
63
100
  ## Priority is computed, not assigned
64
101
 
65
102
  ```
@@ -15,6 +15,7 @@ list `/task-pipeline checkup` hands an operator.
15
15
  - The components, each named
16
16
  - The check-list, and how it is ordered
17
17
  - `/task-pipeline checkup`
18
+ - The seeded script
18
19
  - What the loop does with it
19
20
  - Rationalizations
20
21
 
@@ -89,6 +90,29 @@ ledgers' unresolved count, and the code graph's staleness where one exists.
89
90
  names the checkup and its date — so a row a machine created is distinguishable from one a
90
91
  run surfaced. It prints what it would add first. Never silently.
91
92
 
93
+ ## The seeded script
94
+
95
+ [`../templates/exposure.sh`](../templates/exposure.sh) computes the line and the
96
+ check-list from the two files above, so a project gets the number without an agent in the
97
+ room. Seed it the way the gate is seeded:
98
+
99
+ ```bash
100
+ cp <skill>/templates/exposure.sh scripts/exposure.sh # only if absent
101
+ chmod +x scripts/exposure.sh
102
+ ```
103
+
104
+ **It exits 0 whatever the number is, and that is load-bearing.** A threshold here would
105
+ be a target on `never`, and this file has already said that column may never have one —
106
+ the moment *"unverified must be under ten"* exists, the cheapest way to satisfy it is a
107
+ date nobody earned. Exit 1 means the ledger is present and unreadable, which is a
108
+ different fact and deserves a different code.
109
+
110
+ Two of its behaviours exist because the alternative lies in the reassuring direction. A
111
+ project with no ledger prints `dormant:` rather than a clean zero, and a check-list that
112
+ cannot be built is a failure rather than an empty section under a non-zero count — the
113
+ first draft's `sort` died on a non-ASCII `What` column, printed its error to stderr, and
114
+ left a confident number above nothing at all.
115
+
92
116
  ## What the loop does with it
93
117
 
94
118
  [`continuity.md`](continuity.md) has always required each iteration to re-measure the
@@ -20,6 +20,7 @@ from `super-ux`.
20
20
  | `decisions.md` | `docs/DECISIONS.md` — the decision register | 0 seeds it, the Doc Loop appends |
21
21
  | `open-questions.md` | `docs/OPEN_QUESTIONS.md` | 0 seeds it, the Doc Loop resolves rows |
22
22
  | `docgate.sh` | `scripts/check-docs.sh` | 0 seeds it · 9 runs it · 10 proves it |
23
+ | `exposure.sh` | `scripts/exposure.sh` | 0 reads it · 10 recomputes it · `checkup` prints it |
23
24
  | `hygiene.sh` | `scripts/check-hygiene.sh` | 0 seeds it · **5 runs it after every task** · 6 and 9 run it · 10 proves it |
24
25
  | `stage-coverage.sh` | `scripts/stage-coverage.sh` | 0 seeds it · **10 runs it before the coverage table** — every stage the flow declares must carry a verdict, or the flow stops declaring one it merges |
25
26
  | `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
@@ -14,6 +14,7 @@
14
14
  | B-001 | `export.ts` has no size guard on the write path | `2026-07-28-export` / 5 Review | S | 2 | 1 | 12 | **2** | open | — |
15
15
  | B-002 | XLSX export path | `2026-07-28-export` / 5 Dev | M | 1 | 2 | 12 | 2 | open | LIN-483 |
16
16
  | B-003 | REQ-007 bulk export | `2026-07-28-export` / 2 Brainstorm | L | 1 | 1 | 12 | 1 | dropped | operator 2026-07-28 |
17
+ | B-004 | Retry/backoff on the upload path | `2026-07-28-export` / 5 Dev | M | 2 | 2 | 4 | **4** | parked — feat/upload-retry | — |
17
18
 
18
19
  ## Columns
19
20
 
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # exposure.sh — how much unverified work has piled up in <project>, and what to look at.
3
+ #
4
+ # Seeded by task-pipeline (references/exposure.md). IT IS YOURS NOW.
5
+ #
6
+ # SCOPE: reads the verification ledger and the git tag list. It does NOT know whether a
7
+ # confirmation was any good, whether a `pass` in the Auto column was watched failing,
8
+ # or anything about code. Read this header before quoting a number from here.
9
+ #
10
+ # IT IS A MEASUREMENT, NOT A GATE, AND THAT IS LOAD-BEARING. It exits 0 whatever the
11
+ # number is. A threshold here would be a target on `never`, and the ledger's own
12
+ # doctrine says that column may never have one — the moment "unverified must be under
13
+ # ten" exists, the cheapest way to satisfy it is to write a date nobody earned.
14
+ # Exit 1 is reserved for "the ledger is here and I could not read it".
15
+ #
16
+ # NO PERCENTAGE, EVER. The request that produced this asked for "the probability of an
17
+ # error". That is not computable from these inputs, and a number wearing a
18
+ # measurement's clothes is the failure this whole pipeline exists to remove. What
19
+ # prints is a vector with its components named, and a guard below refuses a `%`.
20
+ #
21
+ # PORTABLE to macOS bash 3.2: no grep -P, no readarray, no mapfile, no date -d.
22
+ #
23
+ # DORMANT, NOT SILENT: a project with no ledger yet prints why and exits 0. Dormant is
24
+ # visible so it is not forgotten, and green so a freshly seeded project is not red.
25
+
26
+ set -u
27
+
28
+ DOCS_DIR=${DOCS_DIR:-docs}
29
+ # The artifact root is RESOLVED, not assumed — renamed `superpowers` → `evidence` on
30
+ # 2026-08-13, and every gate that hardcoded the old name went dormant in migrated
31
+ # projects, which reads exactly like having nothing to check.
32
+ if [ -z "${EVIDENCE_DIR:-}" ]; then
33
+ if [ -d "$DOCS_DIR/evidence" ]; then EVIDENCE_DIR="$DOCS_DIR/evidence"
34
+ elif [ -d "$DOCS_DIR/superpowers" ]; then EVIDENCE_DIR="$DOCS_DIR/superpowers"
35
+ else EVIDENCE_DIR="$DOCS_DIR/evidence"
36
+ fi
37
+ fi
38
+ LEDGER=${LEDGER:-$EVIDENCE_DIR/verification.md}
39
+ BOARD=${BOARD:-$EVIDENCE_DIR/backlog.md}
40
+ LIST_MAX=${LIST_MAX:-8}
41
+
42
+ if [ ! -f "$LEDGER" ]; then
43
+ echo "dormant: exposure — no $LEDGER yet, so nothing has been shipped-and-unconfirmed"
44
+ exit 0
45
+ fi
46
+
47
+ TMP=$(mktemp -d 2>/dev/null || mktemp -d -t exposure)
48
+ trap 'rm -rf "$TMP"' EXIT
49
+
50
+ # ---------- the ledger's rows ----------
51
+ # A row is a table line whose first cell is a REQ id. The header and the |---| separator
52
+ # are excluded by that shape rather than by counting lines, because a ledger grows
53
+ # sections and a line offset stops being true on the first one added.
54
+ awk -F'|' '
55
+ NF >= 7 && $2 ~ /^[[:space:]]*[A-Za-z][A-Za-z0-9-]*-?[0-9]*[[:space:]]*$/ &&
56
+ $2 !~ /^[[:space:]]*REQ[[:space:]]*$/ { print }
57
+ ' "$LEDGER" > "$TMP/rows" 2>/dev/null
58
+
59
+ # `$(grep -c … || echo 0)` prints TWO zeroes when there are no matches: grep prints its
60
+ # 0 and exits 1, so the fallback runs too and the variable becomes "0\n0" — which then
61
+ # fails `[ "$ROWS" -eq 0 ]` with *integer expression expected* and takes the rest of the
62
+ # script with it. The fallback belongs OUTSIDE the substitution.
63
+ ROWS=$(grep -c '' "$TMP/rows" 2>/dev/null) || ROWS=0
64
+ if [ "$ROWS" -eq 0 ]; then
65
+ echo "dormant: exposure — $LEDGER has no REQ rows yet"
66
+ exit 0
67
+ fi
68
+
69
+ # Column order is the template's: REQ | What | Run | Shipped in | Auto | Human | Note.
70
+ # With awk -F'|' on a leading-pipe line those are $2..$8.
71
+
72
+ awk -F'|' '{ h=$7; gsub(/^[ \t]+|[ \t]+$/, "", h); if (h == "never") print }' \
73
+ "$TMP/rows" > "$TMP/unverified"
74
+ UNVERIFIED=$(grep -c '' "$TMP/unverified" 2>/dev/null) || UNVERIFIED=0
75
+
76
+ # ---------- since: a date, or the literal `never checked` ----------
77
+ # ZERO WOULD BE A LIE IN THE DANGEROUS DIRECTION. "0 days" reads as *checked today*,
78
+ # which is the exact opposite of "nobody has ever looked", so the two cases print
79
+ # different words rather than different numbers.
80
+ awk -F'|' '{ h=$7; gsub(/^[ \t]+|[ \t]+$/, "", h);
81
+ if (h ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) print h }' \
82
+ "$TMP/rows" | sort > "$TMP/dates"
83
+ NEWEST=$(tail -1 "$TMP/dates" 2>/dev/null)
84
+
85
+ if [ -z "$NEWEST" ]; then
86
+ SINCE="never checked"
87
+ else
88
+ # Portable day arithmetic: BSD date and GNU date disagree on every flag that matters,
89
+ # so ask python, which both platforms have and which will not silently return today.
90
+ DAYS=$(python3 - "$NEWEST" <<'PY' 2>/dev/null
91
+ import datetime, sys
92
+ try:
93
+ d = datetime.date.fromisoformat(sys.argv[1])
94
+ except ValueError:
95
+ sys.exit(1)
96
+ print((datetime.date.today() - d).days)
97
+ PY
98
+ )
99
+ if [ -z "${DAYS:-}" ]; then
100
+ echo "FAIL: exposure — $LEDGER has a Human date this script cannot parse: $NEWEST"
101
+ exit 1
102
+ fi
103
+ SINCE="$DAYS days since the last human confirmation"
104
+ fi
105
+
106
+ # ---------- releases: what has gone out on top of it ----------
107
+ # Tags, not commits: a release is what an operator feels. Counted since the newest
108
+ # confirmation date where there is one, and over the whole tag list where there is not —
109
+ # because "no release has ever followed a confirmation" and "no confirmation exists" are
110
+ # the same fact from two directions.
111
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
112
+ if [ -z "$NEWEST" ]; then
113
+ RELEASES=$(git tag --list 'v*' 2>/dev/null | grep -c '') || RELEASES=0
114
+ else
115
+ RELEASES=$(git log --tags --simplify-by-decoration --since="$NEWEST" \
116
+ --pretty='%d' 2>/dev/null | grep -o 'tag: v[0-9][^,)]*' | sort -u | grep -c '') || RELEASES=0
117
+ fi
118
+ else
119
+ RELEASES="?"
120
+ fi
121
+
122
+ LINE="exposure: $UNVERIFIED unverified · $SINCE · $RELEASES releases carry one"
123
+
124
+ # ---------- the guard the doctrine names ----------
125
+ # Not decoration: a later hand adding "(N%)" here is exactly how the estimate-wearing-a-
126
+ # measurement's-clothes returns, and it would return looking helpful.
127
+ case "$LINE" in
128
+ *%*) echo "FAIL: exposure — the line contains a percentage: $LINE"
129
+ echo " references/exposure.md: no percentage, ever. A single score invites a"
130
+ echo " threshold, and a threshold here is a target on \`never\`."
131
+ exit 1 ;;
132
+ esac
133
+
134
+ echo "$LINE"
135
+
136
+ # ---------- the check-list: a number without it says there is a problem, not where ----------
137
+ if [ "$UNVERIFIED" -eq 0 ]; then
138
+ echo " every shipped row carries a human confirmation"
139
+ exit 0
140
+ fi
141
+
142
+ # Oldest first by `Shipped in`, which is the only ordering this repository can defend.
143
+ # Version sort, so v1.9.0 precedes v1.10.0 — a lexical sort puts the newer one first and
144
+ # hands the operator the wrong end of the list.
145
+ awk -F'|' '{ req=$2; ship=$5; what=$3;
146
+ gsub(/^[ \t]+|[ \t]+$/, "", req);
147
+ gsub(/^[ \t]+|[ \t]+$/, "", ship);
148
+ gsub(/^[ \t]+|[ \t]+$/, "", what);
149
+ # Truncated on a WORD boundary, never mid-character: awk counts bytes
150
+ # here, so `substr(what, 1, 56)` cut a Cyrillic letter in half and printed
151
+ # a replacement glyph. Appending whole words cannot land inside one.
152
+ short = ""; n = split(what, w, " ");
153
+ for (i = 1; i <= n; i++) {
154
+ if (length(short) + length(w[i]) + 1 > 56) break;
155
+ short = (short == "" ? w[i] : short " " w[i]);
156
+ }
157
+ if (short == "") short = substr(what, 1, 20);
158
+ printf "%s\t%s\t%s\n", ship, req, short }' \
159
+ "$TMP/unverified" | LC_ALL=C sort -t"$(printf '\t')" -k1,1V > "$TMP/sorted"
160
+ # LC_ALL=C, and not for speed: the `What` column carries em-dashes and Cyrillic, and BSD
161
+ # sort exits with `Illegal byte sequence` on them under a UTF-8 locale — printing the
162
+ # error to stderr, producing an EMPTY list, and leaving the exposure line above it
163
+ # looking perfectly fine. A check-list that silently becomes empty is worse than no
164
+ # check-list, because the number above it still says there is work.
165
+ # The delimiter is a literal tab: the first draft said `-t.` and version-sorted on
166
+ # nothing.
167
+
168
+ echo " oldest first, by the release that shipped it:"
169
+ head -"$LIST_MAX" "$TMP/sorted" | while IFS="$(printf '\t')" read -r ship req what; do
170
+ # Blast radius from the board where it carries a row, reusing its stated input rather
171
+ # than inventing a weight here. Absent is printed as absent, never as a default.
172
+ blast=""
173
+ if [ -f "$BOARD" ]; then
174
+ blast=$(grep -m1 "| $req |" "$BOARD" 2>/dev/null | awk -F'|' '{ b=$5; gsub(/^[ \t]+|[ \t]+$/, "", b); print b }')
175
+ fi
176
+ if [ -n "$blast" ]; then
177
+ printf ' %-10s %-12s %s [blast %s]\n' "$ship" "$req" "$what" "$blast"
178
+ else
179
+ printf ' %-10s %-12s %s\n' "$ship" "$req" "$what"
180
+ fi
181
+ done
182
+
183
+ REST=$((UNVERIFIED - LIST_MAX))
184
+ if [ "$REST" -gt 0 ]; then
185
+ echo " … and $REST more — the full list is \`/task-pipeline checkup\`"
186
+ fi
187
+ exit 0