task-pipeline-skill 1.93.2 → 1.94.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 +151 -1
- package/README.md +42 -9
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +64 -33
- package/cursor/rules/task-pipeline.mdc +10 -5
- package/evals/RESULTS.md +1 -1
- package/evals/task-pipeline.evals.json +16 -0
- package/package.json +3 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/verifier.md +4 -1
- package/plugins/task-pipeline/commands/task-pipeline.md +22 -8
- package/plugins/task-pipeline/hooks/memory-nudge.sh +45 -3
- package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +6 -3
- package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +2 -1
- package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +26 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +13 -11
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +89 -4
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +7 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +25 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +45 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/continuity.md +95 -26
- package/plugins/task-pipeline/skills/task-pipeline/references/deploy-targets.md +30 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/gates.md +6 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +2 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/iteration.md +387 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/knowledge-sources.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +4 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +35 -11
- package/plugins/task-pipeline/skills/task-pipeline/references/waves.md +80 -36
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +54 -5
- package/plugins/task-pipeline/skills/task-pipeline/scripts/backlog_sources.py +212 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +3 -3
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +205 -22
- package/plugins/task-pipeline/skills/task-pipeline/scripts/lessons.py +1137 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/stage_checkpoint.py +120 -15
- package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +546 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/backlog-sources.schema.json +96 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/canvas.md +97 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/dossier.md +82 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/inventory.schema.json +94 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/lesson.schema.json +221 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/questions.md +45 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/retro.md +4 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
|
@@ -15,8 +15,10 @@ deterministic halves around that call, so nothing about the checkpoint is left t
|
|
|
15
15
|
prints the tool's arguments as JSON, from the ledger's last `stage:` line
|
|
16
16
|
stage_checkpoint.py record --answer FILE|- [--ledger PATH]
|
|
17
17
|
reads the tool's answer and keeps `workflowId` and `leaseId` for the next boundary
|
|
18
|
-
stage_checkpoint.py record --unavailable "<reason>" [--ledger PATH]
|
|
19
|
-
the tools are absent or failed: one ledger line, exit 0, the run continues
|
|
18
|
+
stage_checkpoint.py record --unavailable "<reason>" [--for-run] [--ledger PATH]
|
|
19
|
+
the tools are absent or failed: one ledger line, exit 0, the run continues.
|
|
20
|
+
--for-run says it once for the whole run: a marker in memory.json silences
|
|
21
|
+
hooks/memory-nudge.sh until a later `record --answer` clears it
|
|
20
22
|
stage_checkpoint.py state [--ledger PATH]
|
|
21
23
|
the workflow this run writes to, for `observatory_checkpoint_latest` after a resume
|
|
22
24
|
|
|
@@ -45,6 +47,7 @@ Ledger lines it appends, all of the existing `event:` shape:
|
|
|
45
47
|
event: memory — checkpoint <stepId> rev <n> <workflowId> — <ISO-8601>
|
|
46
48
|
event: memory — refused <error> (kept as <id>) — <ISO-8601>
|
|
47
49
|
event: memory — unavailable — <reason> — <ISO-8601>
|
|
50
|
+
event: memory — unavailable for this run — <reason> — <ISO-8601>
|
|
48
51
|
|
|
49
52
|
Exit codes: 0 done (including "unavailable", which is a state, not a failure); 2 the ledger
|
|
50
53
|
or the answer cannot be read.
|
|
@@ -71,6 +74,10 @@ TOPIC = re.compile(r"^Run:\s*`([^`]+)`")
|
|
|
71
74
|
# passes a surface took is measured at the boundary rather than remembered at the end.
|
|
72
75
|
REVIEW = re.compile(r"^review:\s*(\d+)\s+—\s+surface\s+(.+?)\s+—\s+rounds\s+(\d+)\s+—\s+(\S+)\s+—\s+\S+\s*$")
|
|
73
76
|
STATUS = {"pass": "done", "skip": "done", "fail": "blocked"}
|
|
77
|
+
# What `handoff` counts for its idempotency key: the memory events this script appended
|
|
78
|
+
# (checkpoint, refused, handoff offered) — never `unavailable`, which records no state change.
|
|
79
|
+
MEMORY_EVENT = re.compile(r"^event:\s*memory\s+—\s")
|
|
80
|
+
MEMORY_UNAVAILABLE = re.compile(r"^event:\s*memory\s+—\s+unavailable\b")
|
|
74
81
|
|
|
75
82
|
|
|
76
83
|
def _stages() -> dict[int, str]:
|
|
@@ -99,16 +106,44 @@ def _reviews(path: pathlib.Path) -> dict[int, list[str]]:
|
|
|
99
106
|
return out
|
|
100
107
|
|
|
101
108
|
|
|
109
|
+
def run_line(lines: list[str]) -> str | None:
|
|
110
|
+
"""The run's `Run:` line — the first one naming a real topic, not the template's.
|
|
111
|
+
|
|
112
|
+
ONE helper for everything that asks "which run is this": `_read_ledger` takes the topic
|
|
113
|
+
from it, and `run_key` hashes it for the run-level memory marker and for
|
|
114
|
+
`hooks/memory-nudge.sh`'s once-per-run pointer, which imports this function."""
|
|
115
|
+
for line in lines:
|
|
116
|
+
m = TOPIC.match(line)
|
|
117
|
+
if m and "<topic>" not in m.group(1):
|
|
118
|
+
return line.strip()
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
STARTED = re.compile(r"started\s*`([^`]+)`")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_key(lines: list[str]) -> str | None:
|
|
126
|
+
"""16 hex of the run's topic and start date — not the whole `Run:` line, whose module-map
|
|
127
|
+
field an agent may fill in mid-run. The same topic started on another day is another run;
|
|
128
|
+
a same-day restart is told apart by `marker_for_this_run`'s ledger-line test."""
|
|
129
|
+
line = run_line(lines)
|
|
130
|
+
if not line:
|
|
131
|
+
return None
|
|
132
|
+
topic = TOPIC.match(line).group(1)
|
|
133
|
+
started = STARTED.search(line)
|
|
134
|
+
ident = f"{topic}\n{started.group(1) if started else ''}"
|
|
135
|
+
return hashlib.sha256(ident.encode()).hexdigest()[:16]
|
|
136
|
+
|
|
137
|
+
|
|
102
138
|
def _read_ledger(path: pathlib.Path) -> tuple[str, list[tuple]]:
|
|
103
139
|
try:
|
|
104
140
|
text = path.read_text(encoding="utf-8")
|
|
105
141
|
except OSError as exc:
|
|
106
142
|
raise SystemExit(f"stage_checkpoint: the ledger {path} cannot be read ({type(exc).__name__})")
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
topic = m.group(1)
|
|
143
|
+
lines = text.splitlines()
|
|
144
|
+
rl = run_line(lines)
|
|
145
|
+
topic, stages = (TOPIC.match(rl).group(1) if rl else None), []
|
|
146
|
+
for line in lines:
|
|
112
147
|
s = STAGE.match(line)
|
|
113
148
|
if s:
|
|
114
149
|
stages.append((int(s.group(1)), s.group(2), s.group(3), s.group(4), s.group(5), line))
|
|
@@ -117,6 +152,30 @@ def _read_ledger(path: pathlib.Path) -> tuple[str, list[tuple]]:
|
|
|
117
152
|
return topic, stages
|
|
118
153
|
|
|
119
154
|
|
|
155
|
+
class UsageError(Exception):
|
|
156
|
+
"""A call the script cannot act on as given: exit 2, nothing written."""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def marker_for_this_run(ledger: pathlib.Path, state: dict) -> dict | None:
|
|
160
|
+
"""The run-level "memory unavailable" marker, only when it was written for THIS run.
|
|
161
|
+
|
|
162
|
+
Two tests, both needed: the run key (topic and start date) matches, AND the marker's own
|
|
163
|
+
`unavailable for this run` line is still in the ledger. A ledger reseeded at stage 0 the
|
|
164
|
+
same day carries an identical `Run:` line but not that line, so a restart never inherits
|
|
165
|
+
the previous run's silence (verification round 3)."""
|
|
166
|
+
gone = (state.get("memory") or {}).get("unavailable") or {}
|
|
167
|
+
if not gone:
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
lines = ledger.read_text(encoding="utf-8").splitlines()
|
|
171
|
+
except OSError:
|
|
172
|
+
return None
|
|
173
|
+
key = run_key(lines)
|
|
174
|
+
if key is None or gone.get("run") != key or gone.get("ledger_line") not in lines:
|
|
175
|
+
return None
|
|
176
|
+
return gone
|
|
177
|
+
|
|
178
|
+
|
|
120
179
|
def _state_path(ledger: pathlib.Path) -> pathlib.Path:
|
|
121
180
|
return ledger.parent / STATE_NAME
|
|
122
181
|
|
|
@@ -217,10 +276,36 @@ def emit(ledger: pathlib.Path, project: str | None, constraints: list[str],
|
|
|
217
276
|
return args
|
|
218
277
|
|
|
219
278
|
|
|
220
|
-
def record(ledger: pathlib.Path, answer_text: str | None, unavailable: str | None
|
|
279
|
+
def record(ledger: pathlib.Path, answer_text: str | None, unavailable: str | None,
|
|
280
|
+
for_run: bool = False) -> str:
|
|
221
281
|
if unavailable is not None:
|
|
222
282
|
reason = " ".join(unavailable.split())[:200] or "no reason given"
|
|
223
|
-
|
|
283
|
+
if for_run:
|
|
284
|
+
# ONE marker for the run (CHANGELOG, Unreleased). Without it a run on a machine with no
|
|
285
|
+
# Observatory paid one nudge and one ledger line per gate — about eleven of
|
|
286
|
+
# each — to restate a fact that does not change between gates.
|
|
287
|
+
# The marker carries the run key (topic and start date, hashed): the hook and
|
|
288
|
+
# `state` honour it only while the ledger names the same run AND still carries this
|
|
289
|
+
# line, so a marker left on disk — even after a same-day reseed — cannot silence
|
|
290
|
+
# the next run.
|
|
291
|
+
try:
|
|
292
|
+
key = run_key(ledger.read_text(encoding="utf-8").splitlines())
|
|
293
|
+
except OSError:
|
|
294
|
+
key = None
|
|
295
|
+
if key is None:
|
|
296
|
+
raise UsageError("--for-run needs the ledger's `Run:` line: the marker is "
|
|
297
|
+
"keyed to the run, and a ledger that names no run has none")
|
|
298
|
+
line = f"event: memory — unavailable for this run — {reason} — {_now()}"
|
|
299
|
+
state = _load_state(ledger)
|
|
300
|
+
state["memory"] = {"unavailable": {"reason": reason, "at": _now(), "ledger_line": line,
|
|
301
|
+
"run": key}}
|
|
302
|
+
_save_state(ledger, state)
|
|
303
|
+
else:
|
|
304
|
+
line = f"event: memory — unavailable — {reason} — {_now()}"
|
|
305
|
+
state = _load_state(ledger)
|
|
306
|
+
if state.get("memory") and marker_for_this_run(ledger, state) is None:
|
|
307
|
+
state.pop("memory") # another run's marker: cleared, not inherited
|
|
308
|
+
_save_state(ledger, state)
|
|
224
309
|
_append(ledger, line)
|
|
225
310
|
return line
|
|
226
311
|
try:
|
|
@@ -230,6 +315,9 @@ def record(ledger: pathlib.Path, answer_text: str | None, unavailable: str | Non
|
|
|
230
315
|
if not isinstance(answer, dict):
|
|
231
316
|
raise SystemExit("stage_checkpoint: the answer is not an object")
|
|
232
317
|
state = _load_state(ledger)
|
|
318
|
+
# Any answer — a checkpoint or a refusal — means the tools answered: the run-level
|
|
319
|
+
# "unavailable" marker no longer holds, and the nudge comes back.
|
|
320
|
+
cleared = state.pop("memory", None) is not None
|
|
233
321
|
if answer.get("error"):
|
|
234
322
|
kept = answer.get("keptAs")
|
|
235
323
|
if answer["error"] in ("LeaseLost", "WorkflowClosed"):
|
|
@@ -240,9 +328,13 @@ def record(ledger: pathlib.Path, answer_text: str | None, unavailable: str | Non
|
|
|
240
328
|
_save_state(ledger, state)
|
|
241
329
|
line = (f"event: memory — refused {answer['error']}"
|
|
242
330
|
f"{f' (kept as {kept})' if kept else ''} — {_now()}")
|
|
331
|
+
if cleared:
|
|
332
|
+
_save_state(ledger, state)
|
|
243
333
|
_append(ledger, line)
|
|
244
334
|
return line
|
|
245
335
|
if answer.get("handoffId") and not answer.get("leaseId"):
|
|
336
|
+
if cleared:
|
|
337
|
+
_save_state(ledger, state)
|
|
246
338
|
# An offer: the lease stays with this run until the successor accepts.
|
|
247
339
|
line = f"event: memory — handoff offered {answer['handoffId']} — {_now()}"
|
|
248
340
|
_append(ledger, line)
|
|
@@ -276,11 +368,15 @@ def handoff(ledger: pathlib.Path, provider: str, model: str | None, reason: str,
|
|
|
276
368
|
"(emit → observatory_checkpoint_write → record)")
|
|
277
369
|
to = {"provider": provider, **({"model": model} if model else {})}
|
|
278
370
|
# Observatory digests the whole request, TTL included, so the TTL is in the key (another
|
|
279
|
-
# TTL is another offer, not an idempotency conflict). So is the
|
|
280
|
-
# offer is recorded
|
|
281
|
-
# the same key would replay the first, expired
|
|
371
|
+
# TTL is another offer, not an idempotency conflict). So is the count of memory events the
|
|
372
|
+
# ledger holds: once an offer (or a checkpoint) is recorded that count grows, and offering
|
|
373
|
+
# again after it lapsed is a new offer — the same key would replay the first, expired
|
|
374
|
+
# answer. ONLY memory events count. The whole ledger length did, until 1.93.3: a `gate:`
|
|
375
|
+
# line from hooks/gate-observer.sh or a `record --unavailable` line between two retries
|
|
376
|
+
# made the retry a NEW offer, and Observatory replaces an unaccepted older offer with it.
|
|
282
377
|
try:
|
|
283
|
-
seen =
|
|
378
|
+
seen = sum(1 for ln in ledger.read_text(encoding="utf-8").splitlines()
|
|
379
|
+
if MEMORY_EVENT.match(ln) and not MEMORY_UNAVAILABLE.match(ln))
|
|
284
380
|
except OSError:
|
|
285
381
|
seen = 0
|
|
286
382
|
key = hashlib.sha256(f"{state['workflowId']}\n{json.dumps(to, sort_keys=True)}\n{reason}"
|
|
@@ -318,6 +414,8 @@ def main(argv: list[str]) -> int:
|
|
|
318
414
|
g = r.add_mutually_exclusive_group(required=True)
|
|
319
415
|
g.add_argument("--answer", help="a file with the tool's JSON answer, or - for stdin")
|
|
320
416
|
g.add_argument("--unavailable", help="why the tools could not be used")
|
|
417
|
+
r.add_argument("--for-run", action="store_true",
|
|
418
|
+
help="with --unavailable: once for the whole run; silences the nudge until an answer")
|
|
321
419
|
s = sub.add_parser("state", help="the workflow this run writes to; never the token")
|
|
322
420
|
s.add_argument("--ledger", default=DEFAULT_LEDGER)
|
|
323
421
|
h = sub.add_parser("handoff", help="observatory_handoff_create arguments: a PLANNED executor change")
|
|
@@ -346,12 +444,19 @@ def main(argv: list[str]) -> int:
|
|
|
346
444
|
if a.cmd == "state":
|
|
347
445
|
st = _load_state(ledger)
|
|
348
446
|
print(json.dumps({"workflowId": st.get("workflowId"), "holdsLease": bool(st.get("leaseId")),
|
|
349
|
-
"stale": st.get("stale")
|
|
447
|
+
"stale": st.get("stale"),
|
|
448
|
+
"memoryUnavailable": (marker_for_this_run(ledger, st) or {}).get("reason")}))
|
|
350
449
|
return 0
|
|
351
450
|
text = None
|
|
352
451
|
if a.answer is not None:
|
|
353
452
|
text = sys.stdin.read() if a.answer == "-" else pathlib.Path(a.answer).read_text(encoding="utf-8")
|
|
354
|
-
|
|
453
|
+
if a.for_run and a.unavailable is None:
|
|
454
|
+
p.error("--for-run goes with --unavailable")
|
|
455
|
+
try:
|
|
456
|
+
print(record(ledger, text, a.unavailable, a.for_run))
|
|
457
|
+
except UsageError as exc:
|
|
458
|
+
print(f"stage_checkpoint: {exc}", file=sys.stderr)
|
|
459
|
+
return 2
|
|
355
460
|
return 0
|
|
356
461
|
|
|
357
462
|
|