task-pipeline-skill 1.49.1 → 1.50.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 +92 -0
- package/SKILL-CARD.md +1 -1
- package/package.json +3 -2
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/hooks/hooks.json +19 -0
- package/plugins/task-pipeline/hooks/release-gate.sh +143 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +10 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.50.0 — the stage-7 gate stops being a sentence somebody reads
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`plugins/task-pipeline/hooks/release-gate.sh` — the stage-7 rule, enforced at
|
|
8
|
+
agent time.** `stages.md` has always said a release does not leave stage 7 until
|
|
9
|
+
the full suite is green at stage 6. That was a sentence an agent reads and a
|
|
10
|
+
person hopes was obeyed, checked — when it was checked — after the tag was
|
|
11
|
+
already public. A `PreToolUse` hook now refuses `git tag`, a tag push,
|
|
12
|
+
`gh release create` and `npm publish` while the run ledger records no
|
|
13
|
+
`stage: 6 … verdict pass`.
|
|
14
|
+
|
|
15
|
+
Three narrownesses are the whole design, and each is the difference between a
|
|
16
|
+
gate people keep and a gate people rip out:
|
|
17
|
+
|
|
18
|
+
1. **Only outward acts.** Ordinary commits are how stage 5 works; gating them
|
|
19
|
+
would fight the pipeline's own build loop and be gone within a day.
|
|
20
|
+
2. **Only where a pipeline runs.** No `.task-pipeline/run.md` means exit 0
|
|
21
|
+
before anything else is read, so enabling the plugin changes nothing in any
|
|
22
|
+
other repository on the machine.
|
|
23
|
+
3. **Only what the ledger says.** Nothing reruns a suite or believes a claim —
|
|
24
|
+
`progress.md` already makes the ledger append-only, and this reads it.
|
|
25
|
+
|
|
26
|
+
The refusal names the act, the ledger to record the stage in, and the opt-out.
|
|
27
|
+
A refusal with no next step is how an operator learns to remove a gate.
|
|
28
|
+
|
|
29
|
+
Fail-closed by construction: an internal failure exits 2 as well, because every
|
|
30
|
+
non-zero code other than 2 is non-blocking in Claude Code, and a crashing gate
|
|
31
|
+
that fails open is worse than no gate — it reads as one.
|
|
32
|
+
|
|
33
|
+
- **`test/release_gate_test.py`** — 16 fixtures, run as a process with real JSON on
|
|
34
|
+
stdin, wired into `npm run test:all` and CI. Eight of them were watched failing:
|
|
35
|
+
the first implementation fed its own python source to `python3 -` through a
|
|
36
|
+
heredoc **and** tried to read the payload from stdin, so `sys.stdin.read()` came
|
|
37
|
+
back empty, every release was classified as "not a release", and the gate
|
|
38
|
+
allowed everything while looking installed. The payload now travels in the
|
|
39
|
+
environment.
|
|
40
|
+
|
|
41
|
+
### Notes
|
|
42
|
+
|
|
43
|
+
- The hook lives in the plugin's `hooks/hooks.json` rather than in `SKILL.md`
|
|
44
|
+
front matter. Front-matter hooks are scoped to a skill's activation; a release
|
|
45
|
+
gate has to hold for the whole run, across turns where the skill is not the
|
|
46
|
+
thing being invoked. `agent-sync` already enforces its leases from the same
|
|
47
|
+
channel, so this is the family's proven path rather than a new one.
|
|
48
|
+
|
|
49
|
+
Guards: 309 → **310**. Property checks: 9 → 9. The new guard is the negative
|
|
50
|
+
self-test that disarms the release gate — it blanks the payload handoff, which is
|
|
51
|
+
how the gate was really broken for its first eight fixtures — and requires the
|
|
52
|
+
suite to notice. `test/negatives.py`'s floor moved with it in the same change,
|
|
53
|
+
because a floor below the count cannot notice losing the difference.
|
|
54
|
+
|
|
55
|
+
## v1.49.2 — four stray table rows, and a list that promised three and delivered two
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **The body shipped a rendering defect.** Four rows of the built-in-doctrine
|
|
60
|
+
index — `artifacts.md`, `companion-skills.md`, `conventions.md`,
|
|
61
|
+
`model-tiering.md` — sat between the grill's bullet list and `## How to run`
|
|
62
|
+
with no header row above them. GFM needs a delimiter row to open a table, so an
|
|
63
|
+
agent reading `SKILL.md` saw four lines of literal pipes. Moved back into the
|
|
64
|
+
index where their shape says they belong.
|
|
65
|
+
|
|
66
|
+
- **"Three things the grill does beyond clarifying the request" listed two.** The
|
|
67
|
+
displaced rows had taken the third with them. Restored from
|
|
68
|
+
`references/grill.md`, which still carries it in full: **the design
|
|
69
|
+
destination** — which Figma file, in which team, decided at stage 0 rather than
|
|
70
|
+
at drawing time. Restored rather than renumbered, because the reference proves
|
|
71
|
+
what was lost; renumbering to "two" would have made the body agree with itself
|
|
72
|
+
and disagree with the doctrine.
|
|
73
|
+
|
|
74
|
+
Guards: 309 → **309**. Property checks: 9 → 9. This release restores content and
|
|
75
|
+
adds no enforcement — the defect was structural markdown, which no guard here
|
|
76
|
+
reads, and the count is stated because two probes read the newest section.
|
|
77
|
+
|
|
78
|
+
### Note on the token budget
|
|
79
|
+
|
|
80
|
+
The body is **5969 tokens against a 5000 cap**, up 108: the restored bullet costs
|
|
81
|
+
more than the four moved rows save. Correctness first — a body promising three
|
|
82
|
+
items and delivering two, with four lines rendering as garbage, is worse than
|
|
83
|
+
being over a soft cap it was already over.
|
|
84
|
+
|
|
85
|
+
The gap is now measured rather than estimated, so it stops being re-litigated by
|
|
86
|
+
prose trims. Three attempts at trimming prose produced 35 tokens between them. The
|
|
87
|
+
whole doctrine index is 555 tokens — 320 in the descriptive column, 235 in the
|
|
88
|
+
reference names — so compressing every description to nothing closes at most **26%
|
|
89
|
+
of the 1219-token gap**, and less than that if the stage numbers stay, which are
|
|
90
|
+
the navigational value. The remainder lives in the Stages table's `Gate` column
|
|
91
|
+
(`grep -c '^\*\*Gate' references/stages.md` → **0**: the body is its only home)
|
|
92
|
+
and in the five-step operating procedure. Closing the gap therefore means moving
|
|
93
|
+
content into references — a restructuring of the skill, not a trim.
|
|
94
|
+
|
|
3
95
|
## v1.49.1 — the entry states the count its own guard reads
|
|
4
96
|
|
|
5
97
|
### 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.
|
|
15
|
+
| **Version** | 1.50.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.
|
|
3
|
+
"version": "1.50.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"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"test": "python3 test/validate.py",
|
|
10
10
|
"test:negatives": "python3 test/negatives.py",
|
|
11
11
|
"test:probe": "python3 test/probe.py --self-test",
|
|
12
|
-
"test:all": "python3 test/validate.py && python3 test/negatives.py && npm run test:probe"
|
|
12
|
+
"test:all": "python3 test/validate.py && python3 test/negatives.py && npm run test:probe && npm run test:hooks",
|
|
13
|
+
"test:hooks": "python3 test/release_gate_test.py"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"bin",
|
|
@@ -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.
|
|
5
|
+
"version": "1.50.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ssheleg",
|
|
8
8
|
"url": "https://x.com/sshlg93"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "task-pipeline — the stage-7 gate, enforced at agent time. 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
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"shell": "bash",
|
|
11
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/release-gate.sh\"",
|
|
12
|
+
"statusMessage": "stage 7: has stage 6 passed?",
|
|
13
|
+
"timeout": 20
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse — the stage-7 gate, made mechanical.
|
|
3
|
+
#
|
|
4
|
+
# `stages.md` already says a release does not leave stage 7 until the full suite
|
|
5
|
+
# is green at stage 6. Until now that was a sentence an agent reads and a person
|
|
6
|
+
# hopes was obeyed; the tag is public before anybody can check. This refuses the
|
|
7
|
+
# irreversible act while the run's own ledger records no passing stage 6.
|
|
8
|
+
#
|
|
9
|
+
# THREE DELIBERATE NARROWNESSES, each one the difference between a gate people
|
|
10
|
+
# keep and a gate people rip out:
|
|
11
|
+
#
|
|
12
|
+
# 1. Only OUTWARD acts. `git tag`, a tag push, `gh release create`, `npm
|
|
13
|
+
# publish`. Ordinary commits are how stage 5 works — gating them would fight
|
|
14
|
+
# the pipeline's own build loop and be gone within a day.
|
|
15
|
+
# 2. Only in a project that is running a pipeline. No `.task-pipeline/run.md`
|
|
16
|
+
# means exit 0 before anything else is read, so installing this plugin
|
|
17
|
+
# changes nothing anywhere else.
|
|
18
|
+
# 3. Only what the ledger SAYS. Nothing here reruns a suite or believes a
|
|
19
|
+
# claim; `progress.md` makes the ledger append-only, and this reads it.
|
|
20
|
+
#
|
|
21
|
+
# Exit 2 blocks the call and shows stderr as the reason. Any other non-zero code
|
|
22
|
+
# is NON-blocking in Claude Code, so an internal failure exits 2 as well: a
|
|
23
|
+
# crashing gate that fails open is worse than no gate, because it reads as one.
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
input=$(cat 2>/dev/null || true)
|
|
27
|
+
|
|
28
|
+
project="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
29
|
+
ledger="$project/.task-pipeline/run.md"
|
|
30
|
+
|
|
31
|
+
decide() {
|
|
32
|
+
# The payload travels in the environment, NOT on stdin: the heredoc below IS
|
|
33
|
+
# stdin for `python3 -`, so a script fed that way can never also read the hook's
|
|
34
|
+
# JSON from there. Watched failing — the gate allowed every release, silently,
|
|
35
|
+
# because `sys.stdin.read()` came back empty and an empty payload is a skip.
|
|
36
|
+
HOOK_INPUT="$input" python3 - "$ledger" <<'PY'
|
|
37
|
+
import json, shlex, sys, os, re
|
|
38
|
+
|
|
39
|
+
ledger = sys.argv[1]
|
|
40
|
+
raw = os.environ.get("HOOK_INPUT", "")
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(raw)
|
|
43
|
+
except Exception:
|
|
44
|
+
print("skip"); raise SystemExit(0)
|
|
45
|
+
|
|
46
|
+
cmd = ((data.get("tool_input") or {}).get("command") or "")
|
|
47
|
+
try:
|
|
48
|
+
tokens = shlex.split(cmd)
|
|
49
|
+
except ValueError:
|
|
50
|
+
tokens = cmd.split()
|
|
51
|
+
|
|
52
|
+
def outward(tokens):
|
|
53
|
+
"""Is this an act that other people can see the moment it succeeds?"""
|
|
54
|
+
low = [t.lower() for t in tokens]
|
|
55
|
+
for i, t in enumerate(low):
|
|
56
|
+
rest = low[i + 1:]
|
|
57
|
+
# `git -C dir -c k=v tag v1` — the subcommand is the first token after
|
|
58
|
+
# git that is not a flag or a flag's value. Tokenised rather than matched
|
|
59
|
+
# as a substring: `git log --grep=tag` must not count.
|
|
60
|
+
if t.endswith("git"):
|
|
61
|
+
j = 0
|
|
62
|
+
while j < len(rest):
|
|
63
|
+
if rest[j] in ("-C", "-c", "--git-dir", "--work-tree"):
|
|
64
|
+
j += 2; continue
|
|
65
|
+
if rest[j].startswith("-"):
|
|
66
|
+
j += 1; continue
|
|
67
|
+
break
|
|
68
|
+
if j < len(rest):
|
|
69
|
+
sub = rest[j]
|
|
70
|
+
args = rest[j + 1:]
|
|
71
|
+
if sub == "tag" and not any(a in ("-d", "--delete", "-l", "--list") for a in args):
|
|
72
|
+
# A bare `git tag` lists; a tag with a name creates one.
|
|
73
|
+
if any(not a.startswith("-") for a in args):
|
|
74
|
+
return "git tag"
|
|
75
|
+
if sub == "push":
|
|
76
|
+
if any(a in ("--tags", "--follow-tags") for a in args):
|
|
77
|
+
return "git push --tags"
|
|
78
|
+
if any(a.startswith("refs/tags/") or re.fullmatch(r"v?\d+\.\d+\.\d+.*", a) for a in args):
|
|
79
|
+
return "git push <tag>"
|
|
80
|
+
if t.endswith("gh") and rest[:2] == ["release", "create"]:
|
|
81
|
+
return "gh release create"
|
|
82
|
+
if t.endswith("npm") and "publish" in rest:
|
|
83
|
+
return "npm publish"
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
act = outward(tokens)
|
|
87
|
+
if not act:
|
|
88
|
+
print("skip"); raise SystemExit(0)
|
|
89
|
+
|
|
90
|
+
if not os.path.exists(ledger):
|
|
91
|
+
print("skip"); raise SystemExit(0)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
text = open(ledger, encoding="utf-8").read()
|
|
95
|
+
except Exception:
|
|
96
|
+
# The ledger exists and cannot be read: this project IS governed and the
|
|
97
|
+
# gate cannot tell. Fail closed, and say which file to look at.
|
|
98
|
+
print("block\t%s\tthe run ledger could not be read" % act); raise SystemExit(0)
|
|
99
|
+
|
|
100
|
+
passed = False
|
|
101
|
+
for line in text.splitlines():
|
|
102
|
+
line = line.strip()
|
|
103
|
+
if not line.startswith("stage:"):
|
|
104
|
+
continue
|
|
105
|
+
m = re.match(r"stage:\s*6\b", line)
|
|
106
|
+
if m and re.search(r"verdict\s+pass", line):
|
|
107
|
+
passed = True
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
print("ok\t%s" % act if passed else "block\t%s\tno `stage: 6 … verdict pass` line in the ledger" % act)
|
|
111
|
+
PY
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
verdict=$(decide 2>/dev/null)
|
|
115
|
+
status=$?
|
|
116
|
+
|
|
117
|
+
# A gate that cannot reach its own decision must not wave the release through.
|
|
118
|
+
if [ $status -ne 0 ] || [ -z "$verdict" ]; then
|
|
119
|
+
[ -f "$ledger" ] || exit 0
|
|
120
|
+
echo "task-pipeline: the stage-7 gate could not run (python3 missing or failed), and this project has a run in flight ($ledger). Refusing an outward act rather than failing open." >&2
|
|
121
|
+
exit 2
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
state=$(printf '%s' "$verdict" | cut -f1)
|
|
125
|
+
act=$(printf '%s' "$verdict" | cut -f2)
|
|
126
|
+
why=$(printf '%s' "$verdict" | cut -f3)
|
|
127
|
+
|
|
128
|
+
case "$state" in
|
|
129
|
+
skip|ok) exit 0 ;;
|
|
130
|
+
block)
|
|
131
|
+
cat >&2 <<EOF
|
|
132
|
+
task-pipeline: \`$act\` is an outward, irreversible act and stage 6 has not passed
|
|
133
|
+
in this run — $why.
|
|
134
|
+
|
|
135
|
+
The tag is public the moment it lands; reading the verdict afterwards is not a
|
|
136
|
+
gate. Run the full suite, record stage 6 in $ledger, then release.
|
|
137
|
+
|
|
138
|
+
To release deliberately without the pipeline, remove the ledger or say
|
|
139
|
+
«без пайплайна» and take the route by hand.
|
|
140
|
+
EOF
|
|
141
|
+
exit 2 ;;
|
|
142
|
+
*) exit 0 ;;
|
|
143
|
+
esac
|
|
@@ -73,6 +73,10 @@ gate stops until it is installed.
|
|
|
73
73
|
| run-wide · the work-list **between** runs, and how its priority is computed | `references/backlog.md` |
|
|
74
74
|
| run-wide · whether a **human** ever confirmed what shipped, and when | `references/verification.md` |
|
|
75
75
|
| run-wide · how much unconfirmed work has piled up, and what to look at first | `references/exposure.md` |
|
|
76
|
+
| any stage · Where each artifact belongs | `references/artifacts.md` |
|
|
77
|
+
| preflight · Companion skills and their fallbacks | `references/companion-skills.md` |
|
|
78
|
+
| 6–10 · How the host project's CLAUDE.md is read | `references/conventions.md` |
|
|
79
|
+
| preflight · Model map, ids and the override | `references/model-tiering.md` |
|
|
76
80
|
|
|
77
81
|
**Optional bridge.** An equivalent skill set the operator already runs (e.g.
|
|
78
82
|
`superpowers:brainstorming` / `writing-plans` / `subagent-driven-development` /
|
|
@@ -152,10 +156,12 @@ Three things the grill does beyond clarifying the request:
|
|
|
152
156
|
mid-flight (test/lint/deploy commands, branch policy, log locations, docs
|
|
153
157
|
targets, the model decision, deploy authorization). Autonomy is bought here or
|
|
154
158
|
not at all — an unasked question is a scheduled interruption.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
- **The design destination**, when the project designs in Figma: *which* file, in
|
|
160
|
+
which team — a stage-0 decision, never a stage-3 side effect. Left to drawing
|
|
161
|
+
time the question is answered by whoever is holding the brush, and the answer is
|
|
162
|
+
usually *create a new file* — which is how a project ends up with three files
|
|
163
|
+
called some variation of "Design", each with real work in it and no way to tell
|
|
164
|
+
which one the team opens.
|
|
159
165
|
|
|
160
166
|
## How to run
|
|
161
167
|
|
|
@@ -17,6 +17,7 @@ that acts *while the agent is working* rather than after the commit.
|
|
|
17
17
|
- A worked example
|
|
18
18
|
- Debugging
|
|
19
19
|
- Removing them
|
|
20
|
+
- The one this skill ships
|
|
20
21
|
- Leases are not reimplemented here
|
|
21
22
|
- Rationalizations
|
|
22
23
|
|
|
@@ -207,6 +208,33 @@ hooks enforced is still available as a command and still stated in the doctrine
|
|
|
207
208
|
and the run is **`ungated`** from then on, which is a thing to say out loud rather
|
|
208
209
|
than a detail to omit.
|
|
209
210
|
|
|
211
|
+
## The one this skill ships
|
|
212
|
+
|
|
213
|
+
Since v1.50.0 the plugin carries a hook of its own —
|
|
214
|
+
`hooks/release-gate.sh`, wired at `PreToolUse` on `Bash`. It refuses an
|
|
215
|
+
**outward, irreversible act** (`git tag`, a tag push, `gh release create`,
|
|
216
|
+
`npm publish`) while the run ledger records no `stage: 6 … verdict pass`.
|
|
217
|
+
|
|
218
|
+
It is the worked example above, made real, and its three narrownesses are the
|
|
219
|
+
reusable part:
|
|
220
|
+
|
|
221
|
+
| Narrowness | Why it is not a smaller feature |
|
|
222
|
+
|---|---|
|
|
223
|
+
| Only outward acts, never ordinary commits | stage 5 commits per task by design; a gate that fights the build loop is removed within a day |
|
|
224
|
+
| Silent where no `.task-pipeline/run.md` exists | enabling the plugin must change nothing in a repository that runs no pipeline |
|
|
225
|
+
| Reads the ledger, never reruns a suite | `progress.md` already makes the ledger append-only; a second source of truth about "did stage 6 pass" is the failure this file warns about below |
|
|
226
|
+
|
|
227
|
+
**Fail-closed, deliberately.** Every non-zero exit code other than `2` is
|
|
228
|
+
non-blocking, so an internal failure exits `2` as well. A crashing gate that fails
|
|
229
|
+
open is worse than no gate: it reads as one.
|
|
230
|
+
|
|
231
|
+
**The defect worth remembering** — the first implementation fed its own python
|
|
232
|
+
source to `python3 -` through a heredoc *and* read the payload from stdin. The
|
|
233
|
+
heredoc **is** stdin, so the payload came back empty, every act classified as "not
|
|
234
|
+
a release", and the gate allowed everything while looking installed. Eight fixtures
|
|
235
|
+
caught it. A hook that cannot see its own input is indistinguishable, from the
|
|
236
|
+
outside, from a hook that approves.
|
|
237
|
+
|
|
210
238
|
## Leases are not reimplemented here
|
|
211
239
|
|
|
212
240
|
Guarded registers and lease arbitration belong to a coordination tool
|