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,400 @@
1
+ #!/usr/bin/env python3
2
+ """Validate an execution packet — the family's versioned task/context contract.
3
+
4
+ `execution-packet.schema.json` states the shape; this script is the
5
+ dependency-free runtime half every consumer runs BEFORE acting on a packet
6
+ (CTX-01). Python stdlib only, like everything else here: a validator that
7
+ needs a package install is a validator half the hosts never run.
8
+
9
+ python3 scripts/packet.py validate <packet.json> # exit 0 clean, 1 rejected
10
+ python3 scripts/packet.py canon <packet.json> # canonical bytes digest
11
+ python3 scripts/packet.py validate-result <envelope.json> \\
12
+ [--current-revision N] [--current-fence F] # the answer half (CTX-01.02)
13
+
14
+ Three rejections are the contract's whole point, each watched failing in
15
+ `test/audit_regressions/ctx-01.01.py`:
16
+
17
+ * an UNKNOWN MANDATORY MAJOR — a consumer that does not know
18
+ `execution-packet/9` must refuse the packet, not skim the fields it likes;
19
+ * a MISSING REF/DIGEST — an input or baseline without its sha256 cannot be
20
+ checked fresh, and stale context must block, never silently pass;
21
+ * an UNBOUND DECISION — a decision named without address+digest is a rumour,
22
+ and two agents reading rumours build two different things from one plan.
23
+
24
+ Round-trip: `canon` serializes with sorted keys and stable separators, so the
25
+ same packet always hashes the same — the id a receipt can carry.
26
+
27
+ The RESULT half (`execution-result.schema.json`) answers with an AttemptGrant
28
+ and a ResultEnvelope, and two rejections carry that contract's whole point:
29
+ a bare boolean is NOT a grant (it names no issuer, revision or fence), and a
30
+ candidate built against an older revision than the current one is STALE — it
31
+ re-plans, it never lands as current. A grant whose fence the authority has
32
+ since superseded is rejected the same way.
33
+ """
34
+ import hashlib
35
+ import json
36
+ import re
37
+ import sys
38
+
39
+ KNOWN_MAJORS = {1}
40
+ SHA_RE = re.compile(r"^[0-9a-f]{64}$")
41
+ EDIT_MODES = {"Edit", "Create", "Create_or_extend"}
42
+ DEP_KINDS = {"data", "control", "resource"}
43
+
44
+
45
+ OUTPUT_KINDS = {"artifact", "report", "decision", "metric"}
46
+
47
+
48
+ def _ref_problems(ref, where, need_id=False):
49
+ out = []
50
+ if not isinstance(ref, dict):
51
+ return [f"{where}: not an object"]
52
+ if need_id and not ref.get("id"):
53
+ out.append(f"{where}: decision without an id")
54
+ if not ref.get("address"):
55
+ out.append(f"{where}: missing address — a ref that points nowhere")
56
+ sha = ref.get("sha256", "")
57
+ if not sha:
58
+ out.append(f"{where}: missing sha256 digest — freshness cannot be checked, "
59
+ "so this blocks dispatch")
60
+ elif not SHA_RE.match(str(sha)):
61
+ out.append(f"{where}: sha256 is not 64 hex chars")
62
+ return out
63
+
64
+
65
+ def problems(packet):
66
+ """Every reason this packet must not dispatch. Empty list = valid."""
67
+ out = []
68
+ if not isinstance(packet, dict):
69
+ return ["the packet is not a JSON object"]
70
+
71
+ version = str(packet.get("schema_version", ""))
72
+ m = re.match(r"^execution-packet/(\d+)$", version)
73
+ if not m:
74
+ out.append(f"schema_version {version!r} is not 'execution-packet/<major>' — "
75
+ "a packet without a mandatory version is unversioned, rejected")
76
+ elif int(m.group(1)) not in KNOWN_MAJORS:
77
+ out.append(f"schema_version major {m.group(1)} is unknown to this consumer "
78
+ f"(knows: {sorted(KNOWN_MAJORS)}) — rejected, never skimmed; "
79
+ "upgrade the consumer or re-issue the packet at a known major")
80
+
81
+ for field in ("id", "module", "intent"):
82
+ if not packet.get(field):
83
+ out.append(f"missing {field}")
84
+
85
+ inputs = packet.get("inputs")
86
+ if not isinstance(inputs, list):
87
+ out.append("inputs must be a list (empty is allowed, absent is not)")
88
+ else:
89
+ for i, ref in enumerate(inputs):
90
+ out.extend(_ref_problems(ref, f"inputs[{i}]"))
91
+
92
+ decisions = packet.get("decision_refs")
93
+ if not isinstance(decisions, list):
94
+ out.append("decision_refs must be a list (empty is allowed, absent is not)")
95
+ else:
96
+ for i, ref in enumerate(decisions):
97
+ out.extend(_ref_problems(ref, f"decision_refs[{i}]", need_id=True))
98
+
99
+ scope = packet.get("source_scope")
100
+ if not isinstance(scope, dict) or not isinstance(scope.get("edit_targets"), list):
101
+ out.append("source_scope.edit_targets must be a list — a packet that names no "
102
+ "files it may touch has an unbounded blast radius")
103
+ else:
104
+ for i, t in enumerate(scope["edit_targets"]):
105
+ where = f"source_scope.edit_targets[{i}]"
106
+ if not isinstance(t, dict) or not t.get("address"):
107
+ out.append(f"{where}: missing address")
108
+ continue
109
+ mode = t.get("mode")
110
+ if mode not in EDIT_MODES:
111
+ out.append(f"{where}: mode {mode!r} is not one of {sorted(EDIT_MODES)}")
112
+ if mode == "Edit":
113
+ sha = t.get("baseline_sha256", "")
114
+ if not sha:
115
+ out.append(f"{where}: an Edit target without baseline_sha256 — "
116
+ "'the source moved under the plan' would be undetectable")
117
+ elif not SHA_RE.match(str(sha)):
118
+ out.append(f"{where}: baseline_sha256 is not 64 hex chars")
119
+
120
+ budgets = packet.get("budgets")
121
+ if not isinstance(budgets, dict) or not budgets:
122
+ out.append("budgets must carry at least one bound")
123
+ else:
124
+ cb = budgets.get("context_bytes")
125
+ if cb is not None and (not isinstance(cb, int) or cb < 1):
126
+ out.append("budgets.context_bytes must be a positive integer")
127
+
128
+ seen_edges = set()
129
+ for i, dep in enumerate(packet.get("dependencies") or []):
130
+ where = f"dependencies[{i}]"
131
+ if not isinstance(dep, dict) or not dep.get("task_id"):
132
+ out.append(f"{where}: missing task_id")
133
+ continue
134
+ if dep.get("kind") not in DEP_KINDS:
135
+ out.append(f"{where}: kind {dep.get('kind')!r} is not one of {sorted(DEP_KINDS)}")
136
+ if not dep.get("rationale"):
137
+ out.append(f"{where}: missing rationale — an edge nobody can explain is an "
138
+ "edge nobody dares remove or trust")
139
+ edge = (dep["task_id"], dep.get("kind"))
140
+ if edge in seen_edges:
141
+ out.append(f"{where}: duplicate edge to {dep['task_id']} ({dep.get('kind')})")
142
+ seen_edges.add(edge)
143
+ # A CONTROL edge with no satisfaction is VALID and PRESERVED: ordering is
144
+ # its whole payload. Only data/resource edges owe a satisfaction.
145
+ if dep.get("kind") in ("data", "resource") and not dep.get("satisfaction"):
146
+ out.append(f"{where}: a {dep['kind']} edge without satisfaction — what would "
147
+ "mark it met? A control edge may omit this; a payload edge may not")
148
+
149
+ return out
150
+
151
+
152
+ RESULT_MAJORS = {1}
153
+ CHECK_STATUSES = {"PASS", "FAIL", "ERROR", "NOT_RUN"}
154
+ EVIDENCE_CLASSES = {"fact", "defect", "unknown_effect"}
155
+ RESULT_STATUSES = {"completed", "partial", "blocked", "abandoned"}
156
+
157
+
158
+ def result_problems(env, current_revision=None, current_fence=None):
159
+ """Every reason this envelope must not land. Empty list = valid."""
160
+ out = []
161
+ if not isinstance(env, dict):
162
+ return ["the envelope is not a JSON object"]
163
+
164
+ version = str(env.get("schema_version", ""))
165
+ m = re.match(r"^execution-result/(\d+)$", version)
166
+ if not m:
167
+ out.append(f"schema_version {version!r} is not 'execution-result/<major>' — rejected")
168
+ elif int(m.group(1)) not in RESULT_MAJORS:
169
+ out.append(f"schema_version major {m.group(1)} is unknown to this consumer "
170
+ f"(knows: {sorted(RESULT_MAJORS)}) — rejected, never skimmed")
171
+
172
+ grant = env.get("grant")
173
+ if isinstance(grant, bool) or grant is None or not isinstance(grant, dict):
174
+ out.append(f"grant is {grant!r} — a boolean confirmation is not a grant: it names "
175
+ "no issuer, no revision, no fence; nothing a later reader could check")
176
+ else:
177
+ for field in ("grant_id", "packet_id", "holder"):
178
+ if not grant.get(field):
179
+ out.append(f"grant.{field} is missing")
180
+ for field in ("revision", "fence"):
181
+ if not isinstance(grant.get(field), int):
182
+ out.append(f"grant.{field} must be an integer — the grant is checkable "
183
+ "or it is not a grant")
184
+ if env.get("packet_id") and grant.get("packet_id") \
185
+ and env["packet_id"] != grant["packet_id"]:
186
+ out.append("the envelope and its grant name different packets")
187
+ if current_fence is not None and isinstance(grant.get("fence"), int) \
188
+ and grant["fence"] < current_fence:
189
+ out.append(f"grant fence {grant['fence']} is superseded (current {current_fence}) "
190
+ "— exclusivity was lost mid-flight; this result must not land")
191
+
192
+ rev = env.get("built_against_revision")
193
+ if not isinstance(rev, int):
194
+ out.append("built_against_revision must be an integer")
195
+ elif current_revision is not None and rev < current_revision:
196
+ out.append(f"STALE candidate: built against revision {rev}, current is "
197
+ f"{current_revision} — re-plan, never land as current")
198
+
199
+ cand = env.get("candidate")
200
+ if not isinstance(cand, dict) or not isinstance(cand.get("changed"), list):
201
+ out.append("candidate.changed must be a list of content-addressed paths")
202
+ else:
203
+ for i, row in enumerate(cand["changed"]):
204
+ if not isinstance(row, dict) or not row.get("address"):
205
+ out.append(f"candidate.changed[{i}]: missing address")
206
+ elif not SHA_RE.match(str(row.get("sha256", ""))):
207
+ out.append(f"candidate.changed[{i}]: missing or malformed sha256")
208
+
209
+ checks = env.get("checks")
210
+ failing = 0
211
+ if not isinstance(checks, list):
212
+ out.append("checks must be a list (empty is allowed, absent is not)")
213
+ else:
214
+ for i, c in enumerate(checks):
215
+ if not isinstance(c, dict) or not c.get("name") or not c.get("command"):
216
+ out.append(f"checks[{i}]: needs name and command — an unnamed check "
217
+ "cannot be re-run")
218
+ continue
219
+ if c.get("status") not in CHECK_STATUSES:
220
+ out.append(f"checks[{i}]: status {c.get('status')!r} is not one of "
221
+ f"{sorted(CHECK_STATUSES)} — NOT_RUN is a status, not a gap")
222
+ elif c["status"] in ("FAIL", "ERROR"):
223
+ failing += 1
224
+
225
+ for i, row in enumerate(env.get("evidence") or []):
226
+ if not isinstance(row, dict) or row.get("class") not in EVIDENCE_CLASSES:
227
+ out.append(f"evidence[{i}]: class must be one of {sorted(EVIDENCE_CLASSES)} — "
228
+ "fact, defect and unknown effect are three verdicts, never blended")
229
+
230
+ secretish = re.compile(r"(?i)^(?:.*[_-])?(token|secret|password|passwd|api[_-]?key|"
231
+ r"apikey|credential|authorization|cookie|private[_-]?key)s?$")
232
+ for i, row in enumerate(env.get("outputs") or []):
233
+ if not isinstance(row, dict) or not row.get("name") or not row.get("address"):
234
+ out.append(f"outputs[{i}]: needs name, kind, address, sha256 — an untyped "
235
+ "output cannot be consumed by digest")
236
+ continue
237
+ if row.get("kind") not in OUTPUT_KINDS:
238
+ out.append(f"outputs[{i}]: kind {row.get('kind')!r} is not one of "
239
+ f"{sorted(OUTPUT_KINDS)}")
240
+ if not SHA_RE.match(str(row.get("sha256", ""))):
241
+ out.append(f"outputs[{i}]: missing or malformed sha256")
242
+ if secretish.match(str(row["name"])):
243
+ out.append(f"outputs[{i}]: {row['name']!r} names a credential — a result "
244
+ "outlives its session exactly like the packet; reference the "
245
+ "store by name, never carry the value (FIX-PF-05.03)")
246
+ pd = env.get("packet_digest")
247
+ if pd is not None and not SHA_RE.match(str(pd)):
248
+ out.append("packet_digest: malformed — the tie to the dispatch packet must be "
249
+ "a sha256 or absent, never a guess")
250
+ for i, row in enumerate(env.get("consumed") or []):
251
+ if not isinstance(row, dict) or not row.get("name") or not SHA_RE.match(str(row.get("sha256", ""))):
252
+ out.append(f"consumed[{i}]: needs name + sha256 — freshness is a comparison, "
253
+ "and an unpinned consumption cannot be compared")
254
+
255
+ status = env.get("status")
256
+ if status not in RESULT_STATUSES:
257
+ out.append(f"status {status!r} is not one of {sorted(RESULT_STATUSES)}")
258
+ elif status == "completed" and failing:
259
+ out.append(f"status 'completed' beside {failing} failing check(s) — a completed "
260
+ "attempt with red checks is a contradiction, not a nuance")
261
+
262
+ return out
263
+
264
+
265
+ def consumed_stale(env, current_outputs):
266
+ """Which of this result's consumed inputs have moved (FIX-PF-05.03).
267
+
268
+ `current_outputs`: {name: sha256} — the predecessors' outputs as they are
269
+ NOW. A name whose digest differs is returned; a non-empty list means this
270
+ result is STALE and the node rebuilds at a new revision. A consumed name
271
+ the predecessors no longer produce is stale too — an input that vanished
272
+ is not fresher than one that changed.
273
+ """
274
+ stale = []
275
+ for row in env.get("consumed") or []:
276
+ name = row.get("name")
277
+ if current_outputs.get(name) != row.get("sha256"):
278
+ stale.append(name)
279
+ return stale
280
+
281
+
282
+ def graph_problems(packets):
283
+ """Closure over a packet SET: unique ids, every edge resolves, no cycles.
284
+ A control edge participates in the cycle check like any other — ordering
285
+ that loops is still a loop."""
286
+ out = []
287
+ if not isinstance(packets, list) or not packets:
288
+ return ["the graph is not a non-empty list of packets"]
289
+ ids = [p.get("id") for p in packets if isinstance(p, dict)]
290
+ for dup in sorted({i for i in ids if ids.count(i) > 1}):
291
+ out.append(f"duplicate packet id {dup!r} — two packets, one name, no arbitration")
292
+ known = set(ids)
293
+ edges = {}
294
+ for p in packets:
295
+ if not isinstance(p, dict):
296
+ continue
297
+ edges[p.get("id")] = []
298
+ for dep in p.get("dependencies") or []:
299
+ tid = dep.get("task_id") if isinstance(dep, dict) else None
300
+ if tid is None:
301
+ continue
302
+ if tid not in known:
303
+ out.append(f"{p.get('id')}: depends on {tid!r}, which is not in the graph")
304
+ continue
305
+ edges[p.get("id")].append(tid)
306
+ state = {}
307
+ def visit(node, stack):
308
+ state[node] = "visiting"
309
+ for nxt in edges.get(node, []):
310
+ if state.get(nxt) == "visiting":
311
+ cycle = stack[stack.index(nxt):] + [nxt] if nxt in stack else [node, nxt]
312
+ out.append("cycle: " + " -> ".join(cycle))
313
+ continue
314
+ if nxt not in state:
315
+ visit(nxt, stack + [nxt])
316
+ state[node] = "done"
317
+ for node in edges:
318
+ if node not in state:
319
+ visit(node, [node])
320
+ return out
321
+
322
+
323
+ def canon(packet):
324
+ """Canonical bytes: sorted keys, stable separators — same packet, same hash."""
325
+ return json.dumps(packet, sort_keys=True, ensure_ascii=False,
326
+ separators=(",", ":")).encode("utf-8")
327
+
328
+
329
+ def main(argv):
330
+ if len(argv) < 3 or argv[1] not in ("validate", "canon", "validate-result",
331
+ "validate-graph"):
332
+ print(__doc__.strip().splitlines()[0])
333
+ print("usage: packet.py validate <packet.json> | packet.py canon <packet.json> | "
334
+ "packet.py validate-result <envelope.json> [--current-revision N] "
335
+ "[--current-fence F]")
336
+ return 2
337
+ try:
338
+ with open(argv[2], encoding="utf-8") as fh:
339
+ packet = json.load(fh)
340
+ except (OSError, json.JSONDecodeError) as exc:
341
+ print(f"REJECTED: cannot read the packet — {exc}")
342
+ return 1
343
+ if argv[1] == "validate-graph":
344
+ found = graph_problems(packet)
345
+ for p_ in packet if isinstance(packet, list) else []:
346
+ for pr in problems(p_) if isinstance(p_, dict) else []:
347
+ found.append(f"{p_.get('id')}: {pr}")
348
+ if found:
349
+ for pr in found:
350
+ print(f"REJECTED: {pr}")
351
+ return 1
352
+ n_ctrl = sum(1 for p_ in packet for dep in (p_.get("dependencies") or [])
353
+ if dep.get("kind") == "control")
354
+ print(f"ok: {len(packet)} packet(s), acyclic, every edge resolves; "
355
+ f"{n_ctrl} control edge(s) preserved")
356
+ return 0
357
+ if argv[1] == "validate-result":
358
+ opts = argv[3:]
359
+ current_revision = current_fence = None
360
+ while opts:
361
+ flag = opts.pop(0)
362
+ if flag == "--current-revision" and opts:
363
+ current_revision = int(opts.pop(0))
364
+ elif flag == "--current-fence" and opts:
365
+ current_fence = int(opts.pop(0))
366
+ else:
367
+ print(f"unknown option: {flag}")
368
+ return 2
369
+ found = result_problems(packet, current_revision, current_fence)
370
+ if found:
371
+ for pr in found:
372
+ print(f"REJECTED: {pr}")
373
+ return 1
374
+ checks = packet.get("checks") or []
375
+ not_run = sum(1 for c in checks if c.get("status") == "NOT_RUN")
376
+ green = sum(1 for c in checks if c.get("status") == "PASS")
377
+ print(f"ok: {packet['packet_id']} ({packet['status']}) — {green} PASS, "
378
+ f"{not_run} NOT_RUN of {len(checks)} check(s); "
379
+ f"revision {packet['built_against_revision']}, fence {packet['grant']['fence']}")
380
+ return 0
381
+ if argv[1] == "canon":
382
+ bytes_ = canon(packet)
383
+ if json.loads(bytes_.decode("utf-8")) != packet:
384
+ print("REJECTED: the packet does not round-trip canonically")
385
+ return 1
386
+ print(hashlib.sha256(bytes_).hexdigest())
387
+ return 0
388
+ found = problems(packet)
389
+ if found:
390
+ for p in found:
391
+ print(f"REJECTED: {p}")
392
+ return 1
393
+ print(f"ok: {packet['id']} ({packet['schema_version']}) — "
394
+ f"{len(packet['inputs'])} input(s), {len(packet['decision_refs'])} decision(s), "
395
+ f"{len(packet['source_scope']['edit_targets'])} edit target(s)")
396
+ return 0
397
+
398
+
399
+ if __name__ == "__main__":
400
+ sys.exit(main(sys.argv))
@@ -25,6 +25,8 @@ from `super-ux`.
25
25
  | `hygiene.sh` | `scripts/check-hygiene.sh` | 0 seeds it · **5 runs it after every task** · 6 and 9 run it · 10 proves it |
26
26
  | `stage-coverage.sh` | `scripts/stage-coverage.sh` | 0 seeds it · **10 runs it before the coverage table** — every stage the flow declares must carry a verdict, or the flow stops declaring one it merges |
27
27
  | `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
28
+ | `browser-claims.json` | stage 6, beside the look — one row per browser claim | REQ/scenario id + state + kind (look/suite/library); validated by `test/browser_claims_test.py`, stdlib only |
29
+ | `finding-evidence.json` | one record per audit finding, wherever the finding lives | the five-axes evidence schema: mechanism / reproduction / exposure / incidence / impact uncertainty, observations apart from assumptions |
28
30
  | `routing-rule.md` | the operator's `CLAUDE.md` — **offered by `setup`, never written silently** | 0 / `setup` |
29
31
  | `retro.md` | `docs/evidence/retro.md` — **one per project, not per run** | 10 writes (stamp → prune → entry), 0 reads it in full |
30
32
  | `retro-archive.md` | `docs/evidence/retro/YYYY-QN.md` | 10 rotates into it, 0 **queries** it |
@@ -0,0 +1,54 @@
1
+ {
2
+ "schema_version": "browser-claims/1",
3
+ "note": "One row = one browser claim: which REQ/scenario it serves, which STATE it was captured in, and which KIND of check produced it — the look, the spec suite or the library script (the browser.md split). A functional PASS never yields a visual PASS; an artifact closes only the state it was captured in (the initial screenshot does not close an opened/error claim); no browser channel means status NOT_RUN with the reason, never a quiet green. python3 test/browser_claims_test.py validates a filled copy with stdlib only.",
4
+ "claims": [
5
+ {
6
+ "id": "BC-01",
7
+ "req": "REQ-3",
8
+ "scenario": "SCN-004",
9
+ "component": "nav-menu",
10
+ "state": "initial",
11
+ "kind": "look",
12
+ "artifact": "shots/nav-menu-initial.png",
13
+ "artifact_state": "initial",
14
+ "status": "NOT_RUN",
15
+ "reason": "template row — replace with a real capture"
16
+ },
17
+ {
18
+ "id": "BC-02",
19
+ "req": "REQ-3",
20
+ "scenario": "SCN-004",
21
+ "component": "nav-menu",
22
+ "state": "opened",
23
+ "kind": "look",
24
+ "artifact": "shots/nav-menu-opened.png",
25
+ "artifact_state": "opened",
26
+ "status": "NOT_RUN",
27
+ "reason": "template row — replace with a real capture"
28
+ },
29
+ {
30
+ "id": "BC-03",
31
+ "req": "REQ-3",
32
+ "scenario": "SCN-004",
33
+ "component": "nav-menu",
34
+ "state": "closed-again",
35
+ "kind": "look",
36
+ "artifact": "shots/nav-menu-closed-again.png",
37
+ "artifact_state": "closed-again",
38
+ "status": "NOT_RUN",
39
+ "reason": "the full toggle cycle: initial -> opened -> closed-again, each with its own artifact"
40
+ },
41
+ {
42
+ "id": "BC-04",
43
+ "req": "REQ-3",
44
+ "scenario": "SCN-004",
45
+ "component": "nav-menu",
46
+ "state": "any",
47
+ "kind": "suite",
48
+ "artifact": null,
49
+ "artifact_state": null,
50
+ "status": "NOT_RUN",
51
+ "reason": "the spec suite half — its PASS counts for functional claims only"
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "schema": "finding-evidence/1",
3
+ "doc": "Minimal evidence record for one finding. The five axes are SEPARATE: a defect proven by reproduction stands with incidence unknown; UNKNOWN is not 0; absent telemetry lowers exposure claims, never mechanism truth; a documented exception never turns a failed invariant into PASS; unknown attacker control lowers exploitability confidence, not the observed behaviour. Keep observations and assumptions apart — a reader must be able to tell what was WATCHED from what was inferred.",
4
+ "fields": {
5
+ "mechanism_status": {
6
+ "one_of": ["confirmed", "suspected"],
7
+ "doc": "What the code/artefact was seen to do. Confirmed needs evidence (file:line, a command and its output, a reproduction)."
8
+ },
9
+ "reproduction": {
10
+ "one_of": ["reproduced", "not-attempted", "attempted-failed"],
11
+ "doc": "A local reproduction is proof BEFORE any incident — it licenses the finding on its own."
12
+ },
13
+ "exposure": {
14
+ "doc": "Who or what can reach the mechanism, or 'unknown'. This is the axis absent telemetry actually lowers."
15
+ },
16
+ "observed_incidence": {
17
+ "doc": "'unknown' | 'never-observed' | a count with its window. UNKNOWN is a word, never a zero.",
18
+ "default": "unknown"
19
+ },
20
+ "impact_uncertainty": {
21
+ "doc": "What is NOT known about the blast, stated — so a rare catastrophic path is not priced as a common trivial one."
22
+ },
23
+ "observed_scope": {
24
+ "doc": "What was actually looked at: 'local checkout', a deployment name, a log window."
25
+ },
26
+ "observed_at": {
27
+ "doc": "ISO-8601 UTC time of the observation."
28
+ },
29
+ "observations": {
30
+ "doc": "What was watched happening, each with its receipt."
31
+ },
32
+ "assumptions": {
33
+ "doc": "What was inferred, each named as such — never mixed into observations."
34
+ }
35
+ },
36
+ "rules": [
37
+ "a proven defect may have observed_incidence 'unknown' and remains a finding",
38
+ "observed_incidence 'unknown' never lowers mechanism_status",
39
+ "an operator interview is asked only when the unknown would change the action",
40
+ "a documented exception changes the decision axis, never the validity axis"
41
+ ]
42
+ }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "_note": "EXAMPLE — copy this block into the project's .claude/settings.json. Doctrine: references/hooks.md. Hooks exist ONLY in Claude Code; on any other agent the same rule runs as a self-check and the run is recorded 'ungated'. Never describe a project as protected when its agents run elsewhere.",
3
3
  "_contract": "A PreToolUse hook blocks in one of two ways: exit 2 with the reason on stderr (stdout is ignored), or exit 0 with {\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"...\"}} on stdout. ANY OTHER EXIT CODE IS NON-BLOCKING, so a crashing guard fails open and stops guarding without announcing it. That is why the command below ends in '|| exit 2' — the gate's own 'exit 1' would otherwise land in the non-blocking branch and the commit would proceed.",
4
+ "_filter_note": "Refuse a commit while the documentation gate is red. Matched narrowly: the matcher is free, the script is not. The `if` filter sits ON THE HANDLER, beside `type`/`command` — never beside `matcher`: a matcher group is only `matcher` + `hooks`, and a key Claude Code does not know there is ignored (and reported at session start from 2.1.270). Until v1.86.2 this template carried `if` at group level, so the gate ran on EVERY Bash call in every project that copied it, and a red docs gate refused every shell command rather than the commit.",
4
5
  "hooks": {
5
6
  "PreToolUse": [
6
7
  {
7
- "_note": "Refuse a commit while the documentation gate is red. Matched narrowly: the matcher is free, the script is not.",
8
8
  "matcher": "Bash",
9
- "if": "Bash(git commit *)",
10
9
  "hooks": [
11
10
  {
12
11
  "type": "command",
12
+ "if": "Bash(git commit *)",
13
13
  "shell": "bash",
14
14
  "timeout": 60,
15
15
  "command": "bash scripts/check-docs.sh >&2 || exit 2"
@@ -20,7 +20,7 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
20
20
 
21
21
  ## `read:` — which doctrine this run actually opened
22
22
 
23
- The bundle is 38 reference files and nothing recorded which of them a run read, so **a
23
+ The bundle is 39 reference files and nothing recorded which of them a run read, so **a
24
24
  skipped file and a read one were indistinguishable** — the class every guard in this
25
25
  pipeline exists to catch, left standing over the doctrine itself.
26
26
 
@@ -48,7 +48,7 @@ same claim one level down.
48
48
  |---|---|---|
49
49
  | `unmeasured — no run ledger` | there is no ledger | nothing to read from |
50
50
  | `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
51
- | `N of 38 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
51
+ | `N of 39 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
52
52
 
53
53
  **It is a disclosure: no floor, no direction, never a target.** A run that needs four files
54
54
  and reads four is not worse than one that reads thirty — and the moment the number becomes