task-pipeline-skill 1.60.1 → 1.61.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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.61.0 — the command that was built, parked, and lost
4
+
5
+ **The exposure command lands, two days after it was declared built.** B-43 recorded
6
+ `templates/exposure.sh` and its fixtures as finished and parked in a session scratchpad,
7
+ blocked only on a concurrent release. The scratchpad was cleaned; nothing on disk, in git
8
+ history, in a stash or in a dangling object held any of it. **The row described work that
9
+ no longer existed**, and nothing would have said so until someone went to merge it.
10
+
11
+ So it is rebuilt, and this time the validator asserts it: a seeded shell script must
12
+ exist, must carry a shebang, and must still be named by the doctrine that tells a project
13
+ to copy it. All three watched failing.
14
+
15
+ `exposure.sh` turns [`verification.md`](plugins/task-pipeline/skills/task-pipeline/references/verification.md)
16
+ into the line and the check-list `references/exposure.md` specifies, so a host project
17
+ gets the number without an agent in the room. **It exits 0 whatever the number is** — a
18
+ threshold here would be a target on `never`, and the cheapest way to satisfy such a
19
+ target is a date nobody earned.
20
+
21
+ Three of its fourteen fixtures caught real defects in the first draft, all of them lying
22
+ in the reassuring direction:
23
+
24
+ - `$(grep -c "" f || echo 0)` prints **two** zeroes when nothing matches — grep prints its
25
+ own 0 and exits 1, so the fallback runs as well. The variable became `"0\n0"`, every
26
+ numeric test after it died with *integer expression expected*, and the script fell over
27
+ precisely in the case that means "everything is confirmed".
28
+ - BSD `sort` exits with *Illegal byte sequence* on the non-ASCII `What` column under a
29
+ UTF-8 locale. The error went to stderr, the check-list came out **empty**, and the
30
+ count above it still said 126 unverified. A list that silently empties is worse than no
31
+ list.
32
+ - Byte-wise `substr` cut a Cyrillic letter in half. Truncation is by whole words now, which
33
+ cannot land inside a character.
34
+
35
+ Guards: 344 → **347**. Property checks: 9 → 9. Three new plants: the seeded script
36
+ disappearing, the doctrine that stops naming it, and a percentage reaching the exposure
37
+ line — the last because the doctrine says *no percentage, ever*, and a later hand adding
38
+ `(N%)` would be adding it to look helpful.
39
+
3
40
  ## v1.60.1 — the gate can see an invariant it breaks elsewhere
4
41
 
5
42
  **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.61.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.61.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.61.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
@@ -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 |
@@ -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