task-pipeline-skill 1.85.2 → 1.86.3

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +2 -1
  4. package/SKILL-CARD.md +1 -1
  5. package/bin/task-pipeline.js +70 -9
  6. package/evals/cases/evidence-docs.json +188 -0
  7. package/evals/cases/project-audit.json +188 -0
  8. package/evals/cases/task-pipeline.json +191 -0
  9. package/package.json +5 -4
  10. package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
  11. package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +16 -11
  12. package/plugins/task-pipeline/skills/evidence-docs/references/GENERATED.md +8 -0
  13. package/plugins/task-pipeline/skills/evidence-docs/references/documentation.md +472 -0
  14. package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +645 -0
  15. package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +279 -0
  16. package/plugins/task-pipeline/skills/evidence-docs/references/learned.md +292 -0
  17. package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +551 -0
  18. package/plugins/task-pipeline/skills/evidence-docs/references/setup.md +149 -0
  19. package/plugins/task-pipeline/skills/evidence-docs/templates/decisions.md +50 -0
  20. package/plugins/task-pipeline/skills/evidence-docs/templates/docgate.sh +537 -0
  21. package/plugins/task-pipeline/skills/project-audit/SKILL.md +66 -25
  22. package/plugins/task-pipeline/skills/project-audit/scripts/audit.py +11 -0
  23. package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +71 -54
  24. package/plugins/task-pipeline/skills/task-pipeline/execution-attempt.schema.json +68 -0
  25. package/plugins/task-pipeline/skills/task-pipeline/execution-packet.example.json +42 -0
  26. package/plugins/task-pipeline/skills/task-pipeline/execution-packet.schema.json +217 -0
  27. package/plugins/task-pipeline/skills/task-pipeline/execution-result.example.json +49 -0
  28. package/plugins/task-pipeline/skills/task-pipeline/execution-result.schema.json +261 -0
  29. package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +10 -1
  30. package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +172 -2
  31. package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +50 -1
  32. package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +7 -0
  33. package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +23 -0
  34. package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +6 -0
  35. package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +8 -1
  36. package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +8 -0
  37. package/plugins/task-pipeline/skills/task-pipeline/references/build.md +32 -0
  38. package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +14 -3
  39. package/plugins/task-pipeline/skills/task-pipeline/references/decomposition.md +83 -2
  40. package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +53 -0
  41. package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +3 -0
  42. package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +27 -8
  43. package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +10 -5
  44. package/plugins/task-pipeline/skills/task-pipeline/references/model-tiering.md +19 -0
  45. package/plugins/task-pipeline/skills/task-pipeline/references/planning.md +203 -26
  46. package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
  47. package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -8
  48. package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +7 -1
  49. package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +686 -0
  50. package/plugins/task-pipeline/skills/task-pipeline/scripts/execution_authority.py +271 -0
  51. package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +415 -18
  52. package/plugins/task-pipeline/skills/task-pipeline/scripts/packet.py +400 -0
  53. package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +2 -0
  54. package/plugins/task-pipeline/skills/task-pipeline/templates/browser-claims.json +54 -0
  55. package/plugins/task-pipeline/skills/task-pipeline/templates/finding-evidence.json +42 -0
  56. package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +2 -2
  57. package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env python3
2
+ """A durable, single-writer execution authority for the work graph (PF-01.02).
3
+
4
+ The defect this closes: `graph.py next` computes a FRONTIER and prints it, and
5
+ two independent `next` runs both read N-001 `pending` and both dispatched it.
6
+ `next` is advisory — it says what COULD run, not who MAY. Who may run a node is
7
+ a separate, durable decision, and it is this module's, not the frontier's.
8
+
9
+ `BEGIN IMMEDIATE` takes SQLite's write lock for the whole read-modify-write, so
10
+ two concurrent `claim`s serialise and exactly one wins. The winner gets an
11
+ attempt record — owner (a session/attempt identity), a monotonic FENCE token, a
12
+ node revision and an expiry — matching `execution-attempt.schema.json`. A stale
13
+ holder that wakes and writes carries an OLD fence and is refused; an expired
14
+ holder is reclaimable, and the reclaim mints a higher fence.
15
+
16
+ Deliberate boundaries, stated so a later reader does not assume more:
17
+
18
+ * **Local filesystem only.** SQLite's locking is a single-host guarantee; this
19
+ makes NO promise on a network share or NAS. Distributed execution is the
20
+ Fabric adapter's job, and `authority_for()` is the seam it will replace.
21
+ * **Fail-closed.** If the database cannot be opened or the arbitration cannot
22
+ run, `claim` RAISES rather than returning a grant. `graph.py` external mode
23
+ turns that into a blocked dispatch — no work starts on an authority that
24
+ could not answer.
25
+ * **The OS lock is not held for the LLM run.** `claim` takes the write lock for
26
+ milliseconds, records the grant, and releases. The lease (expiry + fence) is
27
+ what bounds the run, not a held file handle — a crashed holder frees its node
28
+ by expiry, not by a lock nobody will release.
29
+
30
+ Standard library only (`sqlite3`). No network, no third-party package.
31
+ """
32
+ import os
33
+ import sqlite3
34
+
35
+ SCHEMA_VERSION = "execution-attempt/1"
36
+
37
+ _DDL = """
38
+ CREATE TABLE IF NOT EXISTS attempts (
39
+ node TEXT PRIMARY KEY,
40
+ owner TEXT NOT NULL,
41
+ attempt INTEGER NOT NULL,
42
+ revision INTEGER NOT NULL,
43
+ fence INTEGER NOT NULL,
44
+ expiry REAL NOT NULL,
45
+ state TEXT NOT NULL,
46
+ installed_at REAL
47
+ );
48
+ CREATE TABLE IF NOT EXISTS fence_seq (
49
+ id INTEGER PRIMARY KEY CHECK (id = 1),
50
+ value INTEGER NOT NULL
51
+ );
52
+ INSERT OR IGNORE INTO fence_seq (id, value) VALUES (1, 0);
53
+ """
54
+
55
+
56
+ class AuthorityUnavailable(RuntimeError):
57
+ """Arbitration could not run. Fail-closed: the caller must NOT start work."""
58
+
59
+
60
+ class Authority:
61
+ def __init__(self, path):
62
+ self.path = path
63
+ try:
64
+ # isolation_level=None → explicit transaction control (BEGIN IMMEDIATE).
65
+ self._db = sqlite3.connect(path, timeout=30, isolation_level=None)
66
+ self._db.execute("PRAGMA journal_mode=WAL;")
67
+ self._db.executescript(_DDL)
68
+ except sqlite3.Error as e:
69
+ raise AuthorityUnavailable(f"cannot open authority at {path}: {e}") from e
70
+
71
+ # -- helpers ---------------------------------------------------------------
72
+
73
+ def _next_fence(self):
74
+ self._db.execute("UPDATE fence_seq SET value = value + 1 WHERE id = 1;")
75
+ return self._db.execute("SELECT value FROM fence_seq WHERE id = 1;").fetchone()[0]
76
+
77
+ @staticmethod
78
+ def _iso(ts):
79
+ # ISO-8601 UTC without pulling datetime.now (kept caller-supplied for testability).
80
+ import datetime
81
+ return datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)\
82
+ .strftime("%Y-%m-%dT%H:%M:%SZ")
83
+
84
+ def _grant(self, row):
85
+ node, owner, attempt, revision, fence, expiry, state, installed = row
86
+ g = {
87
+ "schema_version": SCHEMA_VERSION, "node": node, "owner": owner,
88
+ "attempt": attempt, "revision": revision, "fence": fence,
89
+ "expiry": self._iso(expiry), "state": state,
90
+ }
91
+ if installed is not None:
92
+ g["installed_at"] = self._iso(installed)
93
+ return g
94
+
95
+ # -- the one arbitrated decision ------------------------------------------
96
+
97
+ def claim(self, node, owner, revision, now, ttl_seconds=1800):
98
+ """Atomically move a node ready→claimed for ONE owner. Returns the grant
99
+ dict on success, or None when another owner holds a LIVE claim. Raises
100
+ AuthorityUnavailable if the transaction cannot run (fail-closed)."""
101
+ try:
102
+ self._db.execute("BEGIN IMMEDIATE;")
103
+ except sqlite3.Error as e:
104
+ raise AuthorityUnavailable(f"cannot begin arbitration: {e}") from e
105
+ try:
106
+ cur = self._db.execute(
107
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
108
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
109
+ if cur is not None:
110
+ _n, c_owner, c_attempt, _r, _f, c_expiry, c_state, _i = cur
111
+ live = c_state == "claimed" and now < c_expiry
112
+ if live and c_owner != owner:
113
+ self._db.execute("COMMIT;")
114
+ return None # a live claim by someone else: this run LOSES
115
+ attempt = c_attempt + 1 # reclaim of an expired/own hold increments
116
+ else:
117
+ attempt = 1
118
+ fence = self._next_fence()
119
+ expiry = now + ttl_seconds
120
+ self._db.execute(
121
+ "INSERT INTO attempts (node, owner, attempt, revision, fence, expiry, state, installed_at) "
122
+ "VALUES (?, ?, ?, ?, ?, ?, 'claimed', ?) "
123
+ "ON CONFLICT(node) DO UPDATE SET owner=excluded.owner, attempt=excluded.attempt, "
124
+ "revision=excluded.revision, fence=excluded.fence, expiry=excluded.expiry, "
125
+ "state='claimed', installed_at=excluded.installed_at;",
126
+ (node, owner, attempt, revision, fence, expiry, now))
127
+ row = self._db.execute(
128
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
129
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
130
+ self._db.execute("COMMIT;")
131
+ return self._grant(row)
132
+ except sqlite3.Error as e:
133
+ self._db.execute("ROLLBACK;")
134
+ raise AuthorityUnavailable(f"arbitration failed: {e}") from e
135
+
136
+ def renew(self, node, owner, fence, now, ttl_seconds=1800):
137
+ """Extend the lease only for the current fence-holder. A stale fence is
138
+ refused (the zombie guard). Returns the grant or None."""
139
+ self._db.execute("BEGIN IMMEDIATE;")
140
+ try:
141
+ cur = self._db.execute(
142
+ "SELECT fence, owner FROM attempts WHERE node = ?;", (node,)).fetchone()
143
+ if not cur or cur[0] != fence or cur[1] != owner:
144
+ self._db.execute("COMMIT;")
145
+ return None
146
+ self._db.execute("UPDATE attempts SET expiry = ? WHERE node = ?;",
147
+ (now + ttl_seconds, node))
148
+ row = self._db.execute(
149
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
150
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
151
+ self._db.execute("COMMIT;")
152
+ return self._grant(row)
153
+ except sqlite3.Error as e:
154
+ self._db.execute("ROLLBACK;")
155
+ raise AuthorityUnavailable(f"renew failed: {e}") from e
156
+
157
+ def release(self, node, owner, fence):
158
+ """Release a hold the caller actually owns (matching fence). Returns True
159
+ when a row was cleared."""
160
+ self._db.execute("BEGIN IMMEDIATE;")
161
+ try:
162
+ cur = self._db.execute(
163
+ "SELECT fence, owner FROM attempts WHERE node = ?;", (node,)).fetchone()
164
+ if not cur or cur[0] != fence or cur[1] != owner:
165
+ self._db.execute("COMMIT;")
166
+ return False
167
+ self._db.execute("DELETE FROM attempts WHERE node = ?;", (node,))
168
+ self._db.execute("COMMIT;")
169
+ return True
170
+ except sqlite3.Error as e:
171
+ self._db.execute("ROLLBACK;")
172
+ raise AuthorityUnavailable(f"release failed: {e}") from e
173
+
174
+ def recover(self, node, new_owner, revision, now, ttl_seconds=1800):
175
+ """Explicitly reclaim an EXPIRED (or absent) node for a NEW owner, minting
176
+ a higher fence. A still-live claim cannot be recovered — that would be
177
+ stealing a working node — so this returns None while the current lease
178
+ holds."""
179
+ self._db.execute("BEGIN IMMEDIATE;")
180
+ try:
181
+ cur = self._db.execute(
182
+ "SELECT attempt, expiry, state FROM attempts WHERE node = ?;", (node,)).fetchone()
183
+ if cur is not None:
184
+ c_attempt, c_expiry, c_state = cur
185
+ if c_state == "claimed" and now < c_expiry:
186
+ self._db.execute("COMMIT;")
187
+ return None # still live: not recoverable
188
+ attempt = c_attempt + 1
189
+ else:
190
+ attempt = 1
191
+ fence = self._next_fence()
192
+ self._db.execute(
193
+ "INSERT INTO attempts (node, owner, attempt, revision, fence, expiry, state, installed_at) "
194
+ "VALUES (?, ?, ?, ?, ?, ?, 'claimed', ?) "
195
+ "ON CONFLICT(node) DO UPDATE SET owner=excluded.owner, attempt=excluded.attempt, "
196
+ "revision=excluded.revision, fence=excluded.fence, expiry=excluded.expiry, "
197
+ "state='claimed', installed_at=excluded.installed_at;",
198
+ (node, new_owner, attempt, revision, fence, now + ttl_seconds, now))
199
+ row = self._db.execute(
200
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
201
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
202
+ self._db.execute("COMMIT;")
203
+ return self._grant(row)
204
+ except sqlite3.Error as e:
205
+ self._db.execute("ROLLBACK;")
206
+ raise AuthorityUnavailable(f"recover failed: {e}") from e
207
+
208
+ def complete(self, node, owner, fence, now):
209
+ """Record a node's completion — but ONLY from the CURRENT fence-holder.
210
+
211
+ This is the guard against the late worker: a run whose node was
212
+ reclaimed (expired, then recovered by another) carries an OLD fence, and
213
+ its result is refused here rather than overwriting the run that took
214
+ over. Idempotent: the current holder completing twice both succeed, so a
215
+ retried completion is safe. Returns the grant on success, None on a
216
+ stale/late/absent attempt."""
217
+ self._db.execute("BEGIN IMMEDIATE;")
218
+ try:
219
+ cur = self._db.execute(
220
+ "SELECT owner, fence, state FROM attempts WHERE node = ?;", (node,)).fetchone()
221
+ if not cur:
222
+ self._db.execute("COMMIT;")
223
+ return None # nothing to complete
224
+ c_owner, c_fence, c_state = cur
225
+ if c_owner != owner or c_fence != fence:
226
+ self._db.execute("COMMIT;")
227
+ return None # a LATE OLD worker: refused
228
+ if c_state == "completed":
229
+ row = self._db.execute(
230
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
231
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
232
+ self._db.execute("COMMIT;")
233
+ return self._grant(row) # already completed by me: idempotent
234
+ self._db.execute("UPDATE attempts SET state='completed' WHERE node = ?;", (node,))
235
+ row = self._db.execute(
236
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
237
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
238
+ self._db.execute("COMMIT;")
239
+ return self._grant(row)
240
+ except sqlite3.Error as e:
241
+ self._db.execute("ROLLBACK;")
242
+ raise AuthorityUnavailable(f"complete failed: {e}") from e
243
+
244
+ def cancel(self, node, owner, fence):
245
+ """Cancel a hold this run owns (matching fence), freeing the node. A
246
+ mismatch is a no-op — a late worker cannot cancel the run that replaced
247
+ it."""
248
+ return self.release(node, owner, fence)
249
+
250
+ def holder(self, node):
251
+ row = self._db.execute(
252
+ "SELECT node, owner, attempt, revision, fence, expiry, state, installed_at "
253
+ "FROM attempts WHERE node = ?;", (node,)).fetchone()
254
+ return self._grant(row) if row else None
255
+
256
+ def close(self):
257
+ try:
258
+ self._db.close()
259
+ except sqlite3.Error:
260
+ pass
261
+
262
+
263
+ def authority_for(path):
264
+ """The seam a Fabric adapter replaces for distributed execution. Today it
265
+ returns the local SQLite authority; fail-closed if it cannot be built."""
266
+ if not path:
267
+ raise AuthorityUnavailable("no authority path given — external mode needs one")
268
+ parent = os.path.dirname(os.path.abspath(path))
269
+ if parent and not os.path.isdir(parent):
270
+ raise AuthorityUnavailable(f"authority directory does not exist: {parent}")
271
+ return Authority(path)