task-pipeline-skill 1.51.0 → 1.52.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,52 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.52.0 — the three moments the run's own record could not show
4
+
5
+ ### Added
6
+
7
+ - **`hooks/run-lifecycle.sh`** — one line shape for three events the ledger was
8
+ blind to:
9
+
10
+ ```
11
+ event: <compact|session-end|subagent> — <detail> — <ISO-8601>
12
+ ```
13
+
14
+ - `compact` marks the boundary **the ledger exists because of** —
15
+ `templates/run.md` says so in its own header, and until now that boundary was
16
+ the one thing the file could not show. A resumed run could not tell "the
17
+ context was compacted here" from "nothing happened".
18
+ - `session-end` marks a run whose session ended without reaching acceptance.
19
+ That is precisely what `/task-pipeline checkup` looks for, and it was
20
+ invisible: the ledger simply stopped, which is indistinguishable from a run
21
+ still in progress. A run that *did* reach acceptance is not filed as
22
+ abandoned, or the report fills with runs that closed exactly as intended.
23
+ - `subagent` records one finishing, so the `hand:` count has something to be
24
+ checked against other than itself.
25
+
26
+ **It never writes a `hand:` line, and that is not a shortcut.** That shape
27
+ carries `done`, `surfaced`, `decisions` and `amb` — judgements only the agent
28
+ holds. A hook filling them in would fabricate the evidence the line exists to
29
+ provide. It records what it can see and leaves the accounting to whoever can
30
+ account.
31
+
32
+ One shape rather than three: a ledger grammar is read by four documents and
33
+ several hooks, and every shape added is a shape each of them must learn.
34
+
35
+ - **`hooks/build-gate.sh`** — editing the product before the plan is agreed now
36
+ asks. Stage 5 is where code is written; editing during intake, docs, brainstorm,
37
+ spec or plan is the pipeline's discipline being skipped, and it is the skip
38
+ nobody notices because the work looks like progress.
39
+
40
+ `ask`, never `deny`: the routing boundary says a typo, a one-line fix or a
41
+ mechanical rename never went through the pipeline anyway, and no hook can tell a
42
+ typo from a feature. **The build stage is resolved by role, never by number** —
43
+ the same lesson v1.51.0 learned from the release gate, applied before it could
44
+ be repeated. **The pipeline's own artefacts are never gated**: `docs/`,
45
+ `.task-pipeline/`, README and CHANGELOG are what stages 0-4 are *for*.
46
+
47
+ Guards: 311 → **312**. Property checks: 9 → 9. The release-gate suite is 29 → 43
48
+ fixtures, all run as processes.
49
+
3
50
  ## v1.51.0 — the gate stopped being keyed to a number, and stopped believing the agent
4
51
 
5
52
  ### Fixed
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.51.0 |
15
+ | **Version** | 1.52.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. 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.51.0",
3
+ "version": "1.52.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.51.0",
5
+ "version": "1.52.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse — editing the product before the plan is agreed.
3
+ #
4
+ # `stages.md` says no stage advances until its gate passes, and stage 5 is where
5
+ # code gets written. Editing the product during intake, docs, brainstorm, spec or
6
+ # plan is the pipeline's own discipline being skipped — and it is the skip nobody
7
+ # notices, because the work looks like progress.
8
+ #
9
+ # **`ask`, never `deny`, and the reason is this file's own doctrine.** The routing
10
+ # boundary says a typo, a one-line edit or a mechanical rename does not go through
11
+ # the pipeline, and no hook can tell a typo from a feature. A refusal here would
12
+ # fight the honest cases daily and be removed inside a week; a question answered
13
+ # once costs a keystroke.
14
+ #
15
+ # **The build stage is resolved by ROLE, never by number.** v1.50.0 matched
16
+ # `stage: 6` literally and blocked every release in a six-stage project; the same
17
+ # mistake here would put a prompt in front of every edit in any project whose flow
18
+ # is numbered differently. `pipeline.json` → a stage whose `state` is `build`, else
19
+ # one whose name says build. Unresolvable → silence, because a question nobody can
20
+ # act on is worse than none.
21
+ #
22
+ # **The pipeline's own artefacts are never gated.** Stages 0-4 exist to WRITE
23
+ # things — the brief, the spec, the plan, the ledger. A gate that asked about those
24
+ # would fire on the very work it is protecting.
25
+ set -uo pipefail
26
+
27
+ input=$(cat 2>/dev/null || true)
28
+ project="${CLAUDE_PROJECT_DIR:-$PWD}"
29
+ ledger="$project/.task-pipeline/run.md"
30
+ [ -f "$ledger" ] || exit 0
31
+
32
+ HOOK_INPUT="$input" python3 - "$ledger" "$project" <<'PY' 2>/dev/null || exit 0
33
+ import json, os, re, sys
34
+
35
+ ledger, project = sys.argv[1], sys.argv[2]
36
+ try:
37
+ data = json.loads(os.environ.get("HOOK_INPUT", ""))
38
+ except Exception:
39
+ raise SystemExit(0)
40
+
41
+ ti = data.get("tool_input") or {}
42
+ path = ti.get("file_path") or ti.get("notebook_path") or ""
43
+ if not path:
44
+ raise SystemExit(0)
45
+
46
+ rel = os.path.relpath(path, project) if os.path.isabs(path) else path
47
+ rel = rel.replace(os.sep, "/")
48
+ # The run writes these; gating them would fire on the work stages 0-4 are for.
49
+ if rel.startswith("..") or re.match(r"^(docs/|\.task-pipeline/|\.claude/|CHANGELOG\.md|README\.md)", rel):
50
+ raise SystemExit(0)
51
+
52
+ try:
53
+ text = open(ledger, encoding="utf-8").read()
54
+ except Exception:
55
+ raise SystemExit(0)
56
+
57
+ stage_lines = [l.strip() for l in text.splitlines() if l.strip().startswith("stage:")]
58
+ if not stage_lines:
59
+ raise SystemExit(0)
60
+
61
+
62
+ def build_stage_id():
63
+ """By role, never by number — the mistake v1.50.0 shipped in the release gate."""
64
+ try:
65
+ cfg = json.load(open(os.path.join(project, "pipeline.json"), encoding="utf-8"))
66
+ for s in cfg.get("stages") or []:
67
+ if isinstance(s, dict) and s.get("state") == "build":
68
+ return str(s.get("id"))
69
+ except Exception:
70
+ pass
71
+ for l in stage_lines:
72
+ m = re.match(r"stage:\s*(\S+)\s+([^—]*)", l)
73
+ if m and re.search(r"build|dev\b", m.group(2), re.I):
74
+ return m.group(1)
75
+ return None
76
+
77
+
78
+ build_id = build_stage_id()
79
+ if build_id is None:
80
+ raise SystemExit(0) # unresolvable: a question nobody can act on
81
+
82
+ # Has the run entered the build stage at all? Entering is enough — the gate is
83
+ # about editing BEFORE the plan is agreed, not about the build's own verdict.
84
+ entered = any(re.match(r"stage:\s*%s\b" % re.escape(build_id), l) for l in stage_lines)
85
+ if entered:
86
+ raise SystemExit(0)
87
+
88
+ last = stage_lines[-1]
89
+ m = re.match(r"stage:\s*(\S+)\s+([^—]*)", last)
90
+ now = "%s %s" % (m.group(1), m.group(2).strip()) if m else "an early stage"
91
+
92
+ print(json.dumps({"hookSpecificOutput": {
93
+ "hookEventName": "PreToolUse",
94
+ "permissionDecision": "ask",
95
+ "permissionDecisionReason":
96
+ "This run is at stage %s and has not entered the build stage (%s). Editing "
97
+ "the product before the plan is agreed is the pipeline's own discipline "
98
+ "being skipped — and it is the skip nobody notices, because the work looks "
99
+ "like progress.\n\n"
100
+ "Allow if this is a typo, a one-line fix or a mechanical rename, which the "
101
+ "routing boundary says never went through the pipeline anyway. Otherwise "
102
+ "finish the plan and record stage %s in %s.\n\n"
103
+ "The pipeline's own artefacts — docs/, .task-pipeline/, README, CHANGELOG — "
104
+ "are never gated." % (now, build_id, build_id, ledger),
105
+ }}))
106
+ PY
107
+ exit 0
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "task-pipeline — the stage-7 gate and the observation it rests on. Every hook exits 0 immediately when the project has no .task-pipeline/run.md, so installing the plugin globally changes nothing in a repository that is not running a pipeline.",
2
+ "description": "task-pipeline — the gates and the observations they rest on. Every hook exits 0 immediately when the project has no .task-pipeline/run.md, so installing the plugin globally changes nothing in a repository that is not running a pipeline.",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -13,6 +13,17 @@
13
13
  "timeout": 20
14
14
  }
15
15
  ]
16
+ },
17
+ {
18
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "shell": "bash",
23
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/build-gate.sh\"",
24
+ "timeout": 20
25
+ }
26
+ ]
16
27
  }
17
28
  ],
18
29
  "PostToolUse": [
@@ -40,6 +51,42 @@
40
51
  }
41
52
  ]
42
53
  }
54
+ ],
55
+ "PreCompact": [
56
+ {
57
+ "hooks": [
58
+ {
59
+ "type": "command",
60
+ "shell": "bash",
61
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-lifecycle.sh\"",
62
+ "timeout": 15
63
+ }
64
+ ]
65
+ }
66
+ ],
67
+ "SubagentStop": [
68
+ {
69
+ "hooks": [
70
+ {
71
+ "type": "command",
72
+ "shell": "bash",
73
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-lifecycle.sh\"",
74
+ "timeout": 15
75
+ }
76
+ ]
77
+ }
78
+ ],
79
+ "SessionEnd": [
80
+ {
81
+ "hooks": [
82
+ {
83
+ "type": "command",
84
+ "shell": "bash",
85
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-lifecycle.sh\"",
86
+ "timeout": 10
87
+ }
88
+ ]
89
+ }
43
90
  ]
44
91
  }
45
92
  }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # PreCompact · SessionEnd · SubagentStop — the three moments the run's own record
3
+ # cannot see, written down as they happen.
4
+ #
5
+ # One line shape for all three:
6
+ #
7
+ # event: <kind> — <detail> — <ISO-8601>
8
+ #
9
+ # **Why one shape and not three.** A ledger grammar is read by four documents and
10
+ # two hooks; every shape added is a shape each of them must learn. These three are
11
+ # the same kind of fact — something happened to the RUN rather than to a stage —
12
+ # and a `kind` field costs one word against three grammars.
13
+ #
14
+ # WHAT EACH ONE IS FOR
15
+ #
16
+ # compact — the ledger exists because compaction happens; `templates/run.md`
17
+ # says so in its own header. Until now the boundary itself was the
18
+ # one event the file could not show, so a resumed run could not
19
+ # tell "the context was compacted here" from "nothing happened".
20
+ # session-end — a run whose session ended without reaching acceptance is
21
+ # ABANDONED, and abandoned runs are exactly what
22
+ # `/task-pipeline checkup` exists to surface. Before this they
23
+ # were invisible: the ledger simply stopped, which looks identical
24
+ # to a run still in progress.
25
+ # subagent — stage 5 dispatches implementers as subagents, and the `hand:`
26
+ # line counts them. Both sides of that count are written by the
27
+ # same agent from the same memory, so the audit comparing them
28
+ # compares a number with itself.
29
+ #
30
+ # **It does NOT write `hand:` lines, and that is not a shortcut.** That shape
31
+ # carries `done`, `surfaced`, `decisions` and `amb` — judgements only the agent
32
+ # holds. A hook filling them in would be fabricating the very evidence the line
33
+ # exists to provide. So it records what it can actually see (a subagent of this
34
+ # type stopped) and leaves the accounting to whoever can account.
35
+ #
36
+ # Silent with no ledger, and never blocking: none of these events should ever cost
37
+ # a session. `SessionEnd` hooks share a 1.5-second budget, so this appends one line
38
+ # and exits.
39
+ set -uo pipefail
40
+
41
+ input=$(cat 2>/dev/null || true)
42
+ project="${CLAUDE_PROJECT_DIR:-$PWD}"
43
+ ledger="$project/.task-pipeline/run.md"
44
+ [ -f "$ledger" ] || exit 0
45
+
46
+ HOOK_INPUT="$input" python3 - "$ledger" <<'PY' 2>/dev/null || true
47
+ import json, os, sys, datetime, re
48
+
49
+ ledger = sys.argv[1]
50
+ try:
51
+ data = json.loads(os.environ.get("HOOK_INPUT", ""))
52
+ except Exception:
53
+ raise SystemExit(0)
54
+
55
+ event = data.get("hook_event_name")
56
+
57
+
58
+ def clean(text, limit=90):
59
+ """One line, no separators that would break the ledger's own grammar."""
60
+ s = re.sub(r"\s+", " ", str(text or "")).replace("—", "-").strip()
61
+ return s[:limit]
62
+
63
+
64
+ if event == "PreCompact":
65
+ kind, detail = "compact", clean(data.get("trigger") or "unknown")
66
+ elif event == "SessionEnd":
67
+ # A run that reached acceptance is finished, not abandoned. Recording an end
68
+ # for it would fill `checkup` with runs that closed exactly as intended.
69
+ try:
70
+ text = open(ledger, encoding="utf-8").read()
71
+ except Exception:
72
+ raise SystemExit(0)
73
+ stages = [l.strip() for l in text.splitlines() if l.strip().startswith("stage:")]
74
+ closed = any(re.search(r"verdict\s+pass", l) and re.search(r"accept", l, re.I) for l in stages)
75
+ if closed:
76
+ raise SystemExit(0)
77
+ kind = "session-end"
78
+ detail = "%s - run not closed, no acceptance recorded" % clean(data.get("reason") or "other", 40)
79
+ elif event == "SubagentStop":
80
+ kind = "subagent"
81
+ detail = clean(data.get("agent_type") or "unknown", 40)
82
+ else:
83
+ raise SystemExit(0)
84
+
85
+ stamp = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
86
+ with open(ledger, "a", encoding="utf-8") as fh:
87
+ fh.write('event: %s — %s — %s\n' % (kind, detail, stamp))
88
+ PY
89
+ exit 0
@@ -31,6 +31,7 @@ maintains them and the next run reads them as current.
31
31
  - Absent is a word, never a zero
32
32
  - The `holds:` line — what the run is still holding
33
33
  - The observation beside the claim
34
+ - The run's own lifecycle
34
35
  - The run ledger this reads from
35
36
  - Rationalizations
36
37
 
@@ -336,6 +337,20 @@ written from memory is a summary that is confidently wrong exactly when it matte
336
337
  a gate that reads a verdict typed by the agent it constrains is the same shape
337
338
  again, and it looks like enforcement while being a mirror.
338
339
 
340
+ ## The run's own lifecycle
341
+
342
+ Three moments the rail cannot show, recorded by `hooks/run-lifecycle.sh` as
343
+
344
+ ```
345
+ event: <compact|session-end|subagent> — <detail> — <ISO-8601>
346
+ ```
347
+
348
+ The rail reads none of them; `checkup` reads `session-end`, which is how an
349
+ abandoned run stops being invisible. Before this the ledger simply stopped at
350
+ whatever stage the session died on — and a stopped ledger is indistinguishable
351
+ from a run still in progress, which is the exact shape *absent is a word, never a
352
+ zero* exists to refuse.
353
+
339
354
  ## The run ledger this reads from
340
355
 
341
356
  `.task-pipeline/run.md`, seeded at stage 0 from
@@ -29,6 +29,7 @@ touch: <file> — pass <N> (<stage|round|module>) — reason: <finding id / gate
29
29
  hand: <N|10> — task "<quoted>" — done <n> — surfaced <n> — decisions <n> — amb <n> (<ids or "— no register">)
30
30
  holds: <stage id> — <n> (<class: what, owner>; … or "none") — enumerated <n>/8 classes, <unlooked: classes not enumerable>
31
31
  gate: <stage id> — command "<cmd>" — exit <N> — <ISO-8601>
32
+ event: <compact|session-end|subagent> — <detail> — <ISO-8601>
32
33
  ```
33
34
 
34
35
  - **`stage:`** — written when a gate **returns**, not when the stage is entered. The
@@ -42,6 +43,19 @@ gate: <stage id> — command "<cmd>" — exit <N> — <ISO-8601>
42
43
  constrains and confirms an assertion with itself. Absent where the project
43
44
  declares no command, and the release gate then degrades to the claim alone.
44
45
 
46
+ - **`event:`** — written by `hooks/run-lifecycle.sh`, the three moments this file
47
+ otherwise cannot show. `compact` marks the boundary the ledger exists *because
48
+ of* — without it a resumed run cannot tell a compaction from nothing happening.
49
+ `session-end` marks a run whose session ended without reaching acceptance, which
50
+ is what `/task-pipeline checkup` looks for and what was previously invisible: the
51
+ ledger simply stopped, and a stopped ledger looks exactly like a run still in
52
+ progress. `subagent` records one finishing, so the `hand:` count below has
53
+ something to be checked against other than itself.
54
+
55
+ **It never writes a `hand:` line.** That shape carries `done`, `surfaced`,
56
+ `decisions` and `amb` — judgements only the agent holds, and a hook filling them
57
+ in would fabricate the evidence the line exists to provide.
58
+
45
59
  - **`iter:`** — one line per iteration closed. The progress line's counter is
46
60
  `grep -c '^iter:'`, never a number anyone remembers.
47
61
  - **`hand:`** — one per hand-back, at an iteration's close and at stage 10
@@ -69,6 +83,8 @@ stage: 1 Docs study — gate auto — verdict pass — 2026-08-10T11:31Z
69
83
  touch: src/export.ts — pass 1 (stage 5) — reason: TASK-3
70
84
  touch: src/export.ts — pass 2 (stage 5) — reason: F-014
71
85
  touch: src/export.ts — pass 3 (stage 5) — reason: F-014
86
+ event: compact — auto — 2026-08-10T11:58Z
87
+ event: subagent — general-purpose — 2026-08-10T12:00Z
72
88
  gate: 6 — command "npm test" — exit 0 — 2026-08-10T12:02Z
73
89
  stage: 6 Tests — gate manual — verdict pass — 2026-08-10T12:03Z
74
90
  hand: 3 — task "add CSV export to the orders table" — done 2 — surfaced 1 — decisions 1 — amb 2 (OQ-0007, ledger row 4)