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.
- package/CHANGELOG.md +87 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +2 -1
- package/SKILL-CARD.md +1 -1
- package/bin/task-pipeline.js +70 -9
- package/evals/cases/evidence-docs.json +188 -0
- package/evals/cases/project-audit.json +188 -0
- package/evals/cases/task-pipeline.json +191 -0
- package/package.json +5 -4
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +16 -11
- package/plugins/task-pipeline/skills/evidence-docs/references/GENERATED.md +8 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/documentation.md +472 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/gates.md +645 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +279 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/learned.md +292 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/retrospective.md +551 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/setup.md +149 -0
- package/plugins/task-pipeline/skills/evidence-docs/templates/decisions.md +50 -0
- package/plugins/task-pipeline/skills/evidence-docs/templates/docgate.sh +537 -0
- package/plugins/task-pipeline/skills/project-audit/SKILL.md +66 -25
- package/plugins/task-pipeline/skills/project-audit/scripts/audit.py +11 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +71 -54
- package/plugins/task-pipeline/skills/task-pipeline/execution-attempt.schema.json +68 -0
- package/plugins/task-pipeline/skills/task-pipeline/execution-packet.example.json +42 -0
- package/plugins/task-pipeline/skills/task-pipeline/execution-packet.schema.json +217 -0
- package/plugins/task-pipeline/skills/task-pipeline/execution-result.example.json +49 -0
- package/plugins/task-pipeline/skills/task-pipeline/execution-result.schema.json +261 -0
- package/plugins/task-pipeline/skills/task-pipeline/graph.example.json +10 -1
- package/plugins/task-pipeline/skills/task-pipeline/graph.schema.json +172 -2
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +50 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/acceptance.md +7 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +23 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/audit.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/backlog.md +8 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/build.md +32 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +14 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/decomposition.md +83 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +53 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/documentation.md +3 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/grill.md +27 -8
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +10 -5
- package/plugins/task-pipeline/skills/task-pipeline/references/model-tiering.md +19 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/planning.md +203 -26
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +26 -8
- package/plugins/task-pipeline/skills/task-pipeline/references/work-graph.md +7 -1
- package/plugins/task-pipeline/skills/task-pipeline/scripts/context_packets.py +686 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/execution_authority.py +271 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +415 -18
- package/plugins/task-pipeline/skills/task-pipeline/scripts/packet.py +400 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +2 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/browser-claims.json +54 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/finding-evidence.json +42 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/hooks.example.json +2 -2
- 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)
|