@oneie/claude 0.8.0 → 0.10.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.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -23,6 +23,8 @@ Usage:
23
23
  do-rank.py human table
24
24
  do-rank.py --json machine queue for the fleet
25
25
  do-rank.py --top N the N highest READY slugs, one per line
26
+ do-rank.py --board-check perform the live board read (floors print as `>=N`)
27
+ do-rank.py --untriaged [ws] the triage queue as JSON — tasks:board, every page
26
28
  """
27
29
  import json
28
30
  import os
@@ -392,6 +394,60 @@ def _gateway_key():
392
394
  return found["one-"] or found["osk_"] or found["gw"]
393
395
 
394
396
 
397
+ # ── the cap, and whether it bit ───────────────────────────────────────────────
398
+ # Both board doors can hand back a PAGE and let it read as the board, and until
399
+ # 2026-09-13 neither caller here looked. The two doors do not mean the same thing
400
+ # by `total`, so they are not read the same way:
401
+ # tasks:everywhere — `total` is "visible rows BEFORE the limit was applied"
402
+ # (receivers.ts) and `truncated` is true when more exist. Both usable.
403
+ # /api/things — `total` is `tasks.length` of the slice it JUST CUT
404
+ # (api/things/index.ts:396: "We do NOT claim a grand total we never
405
+ # measured"). Only its `truncated` object is usable; printing its `total`
406
+ # as a board total would be a NEW dishonesty, so it is deliberately dropped.
407
+ # What this records is what the last read can honestly say. A number that is a
408
+ # floor prints as a floor — `>=N` — because the rows past the cap were never seen.
409
+ LAST_BOARD_READ = {"door": "", "returned": 0, "total": None, "truncated": False, "why": ""}
410
+
411
+
412
+ def _note_board_read(door, returned, total=None, truncated=False, asked=None):
413
+ """Record what the last board read can honestly say about its own cap. Called by
414
+ EVERY board read, including the failing ones, so a stale note can never be read
415
+ as this read's answer.
416
+
417
+ `asked` is the limit WE SENT, and it is the load-bearing argument. Measured
418
+ 2026-09-13 against prod: `tasks:everywhere` returned exactly 200 rows — our own
419
+ limit — with NO `total` and NO `truncated`, because those two fields are
420
+ declared in receivers.ts and the deployed worker does not yet send them. So a
421
+ cap that reports itself is a contract, not a fact, and trusting it would have
422
+ printed a silent floor as a clean count: exactly the defect this exists to fix.
423
+ A full page is structural evidence of a cap — it is derived from what we asked
424
+ for, so it holds whether or not the door is honest yet."""
425
+ truncated = bool(truncated)
426
+ why = "the door reported a cap" if truncated else ""
427
+ if not truncated and isinstance(asked, int) and asked > 0 and returned >= asked:
428
+ truncated, why = True, f"the page came back FULL at our own limit of {asked}, and the door reported no cap"
429
+ LAST_BOARD_READ.update(
430
+ door=door, returned=returned,
431
+ total=total if isinstance(total, int) else None,
432
+ truncated=truncated, why=why,
433
+ )
434
+ return LAST_BOARD_READ
435
+
436
+
437
+ def board_count_phrase(n=None):
438
+ """How many rows the last board read actually PROVES, as words.
439
+
440
+ Uncapped → the count. Capped with a usable pre-limit total → `N of T (capped)`.
441
+ Capped with no honest total → `>=N (capped)`. Never a bare count over a cap."""
442
+ r = LAST_BOARD_READ
443
+ n = r["returned"] if n is None else n
444
+ if not r["truncated"]:
445
+ return str(n)
446
+ if isinstance(r["total"], int) and r["total"] > n:
447
+ return f"{n} of {r['total']} (CAPPED — {r['total'] - n} row(s) never read)"
448
+ return f">={n} (CAPPED — {r['why'] or 'the door returned a page, not the board'})"
449
+
450
+
395
451
  def fetch_tasks_everywhere(timeout=None):
396
452
  """Board-origin open tasks — the substrate half of the ranked queue (tasks-do C2).
397
453
 
@@ -470,7 +526,11 @@ def fetch_tasks_everywhere(timeout=None):
470
526
  return [], f"receiver refused: {err}{hint}"
471
527
  rows = inner.get("tasks", [])
472
528
  if not isinstance(rows, list):
529
+ _note_board_read("tasks:everywhere", 0)
473
530
  return [], "malformed response (tasks is not a list)"
531
+ # The receiver declares both (receivers.ts § tasks:everywhere) and this dropped
532
+ # them: a 595-row queue answered as 200 and read as the whole board.
533
+ _note_board_read("tasks:everywhere", len(rows), inner.get("total"), inner.get("truncated"), asked=200)
474
534
  # tasks:everywhere answers "workspaces I BELONG TO" — assignment/follow/ownership. A key
475
535
  # whose actor belongs to nothing (the MCP bootstrap agent is one) gets ok:true and an
476
536
  # empty list while the operator's own board is full. That green-empty read is worse than
@@ -518,10 +578,125 @@ def fetch_workspace_board(timeout=None, workspace=None):
518
578
  return [], f"unreachable ({type(e).__name__})"
519
579
  rows = payload.get("things") if isinstance(payload, dict) else None
520
580
  if not isinstance(rows, list):
581
+ _note_board_read("/api/things", 0)
521
582
  return [], "malformed response (things is not a list)"
583
+ # `limit=200` above is a HARD cap and this read is the ranker's last word on
584
+ # what is on the board. /api/things reports its own cap as a `truncated`
585
+ # OBJECT (absent = nothing was cut) — truthy is the whole test. Its `total` is
586
+ # the length of the slice, so it is NOT passed on as a board total.
587
+ _note_board_read("/api/things", len(rows), None, bool(payload.get("truncated")), asked=200)
522
588
  return rows, ""
523
589
 
524
590
 
591
+ def board_ask(receiver, data, timeout=None):
592
+ """POST one receiver through the same door fetch_tasks_everywhere uses, with the
593
+ /api/ask envelope unwrapped. Returns (inner, reason) — never a silent empty."""
594
+ if timeout is None:
595
+ timeout = float(os.environ.get("DO_RANK_BOARD_TIMEOUT", "25"))
596
+ secret = _gateway_key()
597
+ if not secret:
598
+ return None, "no GATEWAY_API_KEY in one.ie/web/.env"
599
+ url = os.environ.get("DO_SIGNAL_URL", "https://one.ie") + "/api/ask/" + receiver
600
+ req = urllib.request.Request(
601
+ url, data=json.dumps({"data": data}).encode(), method="POST",
602
+ headers={
603
+ "Content-Type": "application/json",
604
+ "Authorization": f"Bearer {secret}",
605
+ # Cloudflare 403s the default urllib UA before the Worker runs.
606
+ "User-Agent": "one-do-rank/1.0 (+https://one.ie)",
607
+ },
608
+ )
609
+ try:
610
+ with urllib.request.urlopen(req, timeout=timeout) as r:
611
+ payload = json.loads(r.read().decode())
612
+ except urllib.error.HTTPError as e:
613
+ detail = "CF bot block (UA rejected)" if e.code == 403 else ""
614
+ return None, f"HTTP {e.code}{' — ' + detail if detail else ''}"
615
+ except Exception as e:
616
+ return None, f"unreachable ({type(e).__name__})"
617
+ if not isinstance(payload, dict):
618
+ return None, "malformed response (not an object)"
619
+ inner = payload.get("result")
620
+ inner = inner if isinstance(inner, dict) else payload
621
+ if not inner.get("ok"):
622
+ return None, str(inner.get("error") or payload.get("outcome") or "unknown")
623
+ return inner, ""
624
+
625
+
626
+ # What triage treats as already-shaped. A row whose prose goal opens with this has
627
+ # a one-line check that proves it done, which is the whole output of a triage pass.
628
+ TRIAGED_PREFIX = "accept:"
629
+
630
+
631
+ def fetch_untriaged(workspace=None, pages=None, timeout=None):
632
+ """Every OPEN row whose notes do NOT begin `accept:` — the triage queue, read
633
+ deterministically.
634
+
635
+ Why this is a script and not a prompt: triage.js used to hand a model the ONE
636
+ LINE `--board-check` prints and ask it for `{id,title,notes}[]`. That line
637
+ carries no rows, so the only possible answers were an invented list or an empty
638
+ one, and the only guard was the words "Do not invent rows." A fact the harness
639
+ needs gets a script (.claude/CLAUDE.md).
640
+
641
+ ONE door — tasks:board — followed to the LAST page. `nextCursor` is present iff
642
+ more rows match, so stopping at page one reports a page as the board; that is
643
+ the exact failure this mode exists to refuse. `include:["notes"]` is required:
644
+ a compact row carries no notes and the accept: test IS the filter. There is no
645
+ server-side notes filter, so the test runs here, over every page.
646
+
647
+ `truncated` is a DIFFERENT cut from paging — it means the snapshot itself hit a
648
+ budget, so `total` is only a floor. It is carried through verbatim."""
649
+ ws = workspace or os.environ.get("CC_TASKS_WORKSPACE", "one")
650
+ # A ceiling on the cursor walk so a runaway board cannot loop forever. 20 pages
651
+ # x 500 rows is 10,000 — far past any real board, and reported when it bites.
652
+ pages = int(pages or os.environ.get("DO_RANK_UNTRIAGED_PAGES", "20"))
653
+ out, cursor, total, truncated, seen, page = [], None, None, None, 0, 0
654
+ while page < pages:
655
+ req = {"workspace": ws, "status": "open", "view": "rows",
656
+ "include": ["notes"], "limit": 500, "sort": "priority"}
657
+ if cursor:
658
+ req["cursor"] = cursor
659
+ inner, reason = board_ask("tasks:board", req, timeout=timeout)
660
+ if reason:
661
+ # A failure mid-walk leaves a PARTIAL answer, and it must never read as
662
+ # the whole board: truncated=True is the honest state, always a 3-tuple.
663
+ _note_board_read("tasks:board", seen, total, True)
664
+ return out, f"page {page + 1}: {reason}", {
665
+ "workspace": ws, "scanned": seen, "total": total,
666
+ "truncated": truncated or None, "pagesExhausted": True, "pages": page,
667
+ }
668
+ rows = inner.get("tasks")
669
+ rows = rows if isinstance(rows, list) else []
670
+ if total is None and isinstance(inner.get("total"), int):
671
+ total = inner["total"]
672
+ if inner.get("truncated") and truncated is None:
673
+ truncated = inner["truncated"]
674
+ seen += len(rows)
675
+ for r in rows:
676
+ notes = str(r.get("notes") or "")
677
+ if notes.lstrip().lower().startswith(TRIAGED_PREFIX):
678
+ continue
679
+ out.append({"id": r.get("tid", ""), "title": r.get("name", ""),
680
+ "notes": notes, "priority": r.get("priority", 0),
681
+ "assignee": r.get("assignee", ""), "tags": r.get("tags") or []})
682
+ page += 1
683
+ cursor = inner.get("nextCursor")
684
+ if not cursor:
685
+ break
686
+ # Two independent cuts, reported separately: `truncated` = the snapshot was
687
+ # capped (total is a floor); `pagesExhausted` = WE stopped following cursors.
688
+ # `asked` is the third and it needs no cooperation from the door: a LAST page
689
+ # that came back full with no nextCursor is a cap that did not announce itself
690
+ # (measured on tasks:everywhere the day this was written — it returned exactly
691
+ # our limit and declared neither field).
692
+ pages_exhausted = bool(cursor)
693
+ _note_board_read("tasks:board", seen, total, bool(truncated) or pages_exhausted,
694
+ asked=None if cursor else 500 * page or None)
695
+ return out, "", {"workspace": ws, "scanned": seen, "total": total,
696
+ "truncated": truncated or None, "pagesExhausted": pages_exhausted,
697
+ "pages": page, "capNote": LAST_BOARD_READ["why"] or None}
698
+
699
+
525
700
  def board_task_entry(row):
526
701
  """Shape one tasks:everywhere row as a plan-like dict — the same keys tasks_view()/
527
702
  tasks_seed() already read off a local todo's `plans[slug]` entry, so a board-created
@@ -549,13 +724,17 @@ def merge_board_tasks(active, plans):
549
724
  local match is a pure board-created task and is simply added. Empty/failed fetch is a
550
725
  no-op — file-only ranking, unchanged."""
551
726
  rows, reason = fetch_tasks_everywhere()
727
+ door = LAST_BOARD_READ["door"] or "tasks:everywhere"
552
728
  if not rows:
553
729
  why = reason or "board is empty (0 open tasks)"
554
- print(f"[do-rank] tasks:everywhere: 0 rows — {why} — file-only fallback", file=sys.stderr)
730
+ print(f"[do-rank] {door}: 0 rows — {why} — file-only fallback", file=sys.stderr)
555
731
  return active
556
732
  board_entries = [board_task_entry(r) for r in rows]
557
733
  matched_slugs = {e["slug"] for e in board_entries if e["slug"] in plans}
558
- print(f"[do-rank] tasks:everywhere: merged {len(board_entries)} row(s), "
734
+ # The door is named because the fallback inside fetch_tasks_everywhere can
735
+ # answer from /api/things, and this line said "tasks:everywhere" either way.
736
+ # The count is a FLOOR whenever that door capped — say so, don't round it off.
737
+ print(f"[do-rank] {door}: merged {board_count_phrase(len(board_entries))} row(s), "
559
738
  f"{len(matched_slugs)} superseding a local todo", file=sys.stderr)
560
739
  return [p for p in active if p["slug"] not in matched_slugs] + board_entries
561
740
 
@@ -1324,9 +1503,27 @@ def main():
1324
1503
  if reason:
1325
1504
  print(f"[board-check] RED — {target}: {reason}", file=sys.stderr)
1326
1505
  return 1
1327
- print(f"[board-check] OK — {target}: {len(rows)} open task(s) readable")
1506
+ door = LAST_BOARD_READ["door"] or "tasks:everywhere"
1507
+ print(f"[board-check] OK — {target}: {board_count_phrase(len(rows))} "
1508
+ f"open task(s) readable (door: {door})")
1328
1509
  return 0
1329
1510
 
1511
+ # `--untriaged [workspace]` — the triage queue as JSON on stdout, read through
1512
+ # tasks:board and followed to the last page. This is what .claude/workflows/
1513
+ # triage.js consumes: it used to ask a model to enumerate rows out of
1514
+ # --board-check's single summary line, which carries none. Deterministic fact,
1515
+ # deterministic script. Always prints an object; `ok:false` carries the reason.
1516
+ if "--untriaged" in args:
1517
+ i = args.index("--untriaged")
1518
+ ws = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("-") else None
1519
+ rows, reason, meta = fetch_untriaged(workspace=ws)
1520
+ out = {"ok": not reason, "tasks": rows, "returned": len(rows)}
1521
+ out.update(meta)
1522
+ if reason:
1523
+ out["error"] = reason
1524
+ print(json.dumps(out))
1525
+ return 0 if not reason else 1
1526
+
1330
1527
  # `--lint` — mechanical deliverables hygiene across all OPEN plans. Flags phantom
1331
1528
  # paths (under-qualified/wrong-rooted) that make a plan a fleet false-parallel, and
1332
1529
  # plans with NO parseable deliverables (a fleet hazard). Exit 1 if any plan is dirty.
@@ -324,23 +324,84 @@ case "$CANON" in
324
324
  _fp=$(tsc_tree_fingerprint "$folder" $_fp_extra)
325
325
  _cache="$GOVERN_DIR/tsc-$(printf '%s' "$folder" | tr '/' '_').$_fp"
326
326
 
327
+ # Set HERE, outside the branches, so the rc=3 handler below can never read
328
+ # a value left by an earlier folder or an unset variable.
329
+ _tsc_rc=0
327
330
  if [ -s "$_cache" ]; then
328
331
  current=$(cat "$_cache" | tr -d '[:space:]') # someone already computed it
329
- elif gate_lock "tsc-$(printf '%s' "$folder" | tr '/' '_')" "${RECONCILE_TSC_WAIT:-900}"; then
330
- trap 'gate_release_all' EXIT INT TERM
331
- if [ -s "$_cache" ]; then
332
+ else
333
+ # ── SLOT BEFORE LOCK ─────────────────────────────────────────────────
334
+ # This block used to take the gate_lock HERE and then queue for a slot
335
+ # while holding it. tsc-cached.sh had the same inversion, on the SAME
336
+ # lock name (`tsc-<folder>` — the shared memo namespace is deliberate),
337
+ # and between them they deadlocked the whole box three times in ~35
338
+ # minutes on 2026-09-13: one side held a slot and waited for this lock,
339
+ # the other held this lock and waited for a slot. Neither owner was
340
+ # dead, so nothing reaped it, and killing the LOCK holder only handed
341
+ # the lock to the next waiter and re-formed the cycle within seconds.
342
+ # tsc-cached.sh § SLOT BEFORE LOCK has the full account; the proof is
343
+ # `bash .claude/scripts/govern-order-check.sh`.
344
+ #
345
+ # So the lock is taken INSIDE the gate-run command, never around it: a
346
+ # slot is held before anyone waits on this lock. The inner shell's exit
347
+ # code carries which of the three things happened — 3 = could not lock,
348
+ # 4 = someone else's answer appeared while we queued, 0 = we computed.
349
+ #
350
+ # Resolve gate-run from THIS script's own directory. An earlier version
351
+ # used "$ROOT/..." -- ROOT is never set in this file, and under `set -u`
352
+ # that is a hard "unbound variable" that killed the whole `types` canon.
353
+ # Caught by tests/integration/reconcile.test.ts, which is the reason
354
+ # that test exists.
355
+ _tsc_tmp=$(mktemp)
356
+ _tsc_rc=0
357
+ _gr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
358
+ _gv="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/govern.sh"
359
+ # The compute is passed as DATA, not as nested quoting: three levels of
360
+ # `bash -c "... \"...\" ..."` is how a pipeline silently loses its
361
+ # redirect. Everything the inner shells read arrives through the
362
+ # environment instead.
363
+ _RC_SCRIPT='cd "$_RC_FOLDER" && bunx tsc --noEmit 2>&1 | grep -c "error TS" > "$_RC_TMP"'
364
+ _RC_FOLDER="$folder" _RC_TMP="$_tsc_tmp" _RC_CACHE="$_cache" _RC_SCRIPT="$_RC_SCRIPT" \
365
+ _RC_LOCK="tsc-$(printf '%s' "$folder" | tr '/' '_')" _RC_GV="$_gv" \
366
+ _RC_WAIT="${RECONCILE_TSC_WAIT:-900}" _RC_TIMEOUT="${RECONCILE_TSC_TIMEOUT:-600}" \
367
+ GOVERN_GATE_TIMEOUT=$(( ${RECONCILE_TSC_TIMEOUT:-600} + ${RECONCILE_TSC_WAIT:-900} + 60 )) \
368
+ bash "$_gr" "tsc-reconcile:$folder" -- bash -c '
369
+ . "$_RC_GV"
370
+ gate_lock "$_RC_LOCK" "$_RC_WAIT" || exit 3
371
+ trap "gate_release_all" EXIT INT TERM
372
+ [ -s "$_RC_CACHE" ] && exit 4
373
+ run_bounded "$_RC_TIMEOUT" bash -c "$_RC_SCRIPT" || true
374
+ exit 0' || _tsc_rc=$?
375
+ # `|| _tsc_rc=$?`, never a bare `$?` on the next line: this file runs
376
+ # under `set -e`, so an exit 3 or 4 from the inner shell would kill the
377
+ # canon before the code could be read. The `|| true` this replaces hid
378
+ # every one of those codes.
379
+ if [ "$_tsc_rc" = "4" ] && [ -s "$_cache" ]; then
332
380
  current=$(cat "$_cache" | tr -d '[:space:]') # computed while we queued
333
- else
334
- _tsc_tmp=$(mktemp)
335
- run_bounded "${RECONCILE_TSC_TIMEOUT:-600}" \
336
- bash -c "cd '$folder' && bunx tsc --noEmit 2>&1 | grep -c 'error TS' > '$_tsc_tmp'" \
337
- 2>/dev/null || true
381
+ rm -f "$_tsc_tmp"
382
+ elif [ "$_tsc_rc" != "3" ]; then
338
383
  current=$(cat "$_tsc_tmp" 2>/dev/null | tr -d '[:space:]'); rm -f "$_tsc_tmp"
339
- current=${current:-0}
340
- printf '%s' "$current" > "$_cache"
384
+ # AN UNRUN CHECK IS NEVER MEMOISED. This canon reports a DELTA and has
385
+ # always fallen open to 0 when it could not measure — that is its own
386
+ # call and is unchanged. What must not happen is that 0 being SHARED:
387
+ # this file is `$GOVERN_DIR/tsc-<folder>.<fp>`, the exact object
388
+ # tsc-cached.sh's `_cache_ok` accepts as a reusable PASS, so an unrun
389
+ # typecheck would become a green gate for every later reader on that
390
+ # tree, in every session and worktree.
391
+ #
392
+ # Reachable, and this is the line that made it likely: the slot queue
393
+ # is now the FIRST thing a miss does, so `gate-run … no slot after
394
+ # 1800s` on a saturated box lands here with an empty count file.
395
+ # Measured 2026-09-13 in a sandbox: GOVERN_QUEUE_WAIT=8 with the one
396
+ # slot held printed `OK — tsc delta=0` and wrote a `0` stamp.
397
+ case "$current" in
398
+ ''|*[!0-9]*) current=0 ;; # could not measure: no stamp
399
+ *) printf '%s' "$current" > "$_cache" ;; # a real count is shareable
400
+ esac
341
401
  fi
342
- gate_release_all; trap - EXIT INT TERM
343
- else
402
+ fi
403
+ if [ "${_tsc_rc:-0}" = "3" ]; then
404
+ rm -f "${_tsc_tmp:-}"
344
405
  # Lock held for the full wait. Re-check the cache once — the holder may
345
406
  # have finished and released between our last poll and now.
346
407
  if [ -s "$_cache" ]; then
@@ -13,6 +13,7 @@
13
13
  # do-signal.sh [--print] --task-reap
14
14
  # do-signal.sh [--print] --task-link <slug> [docs=a,b,c]
15
15
  # do-signal.sh [--print] --task-heartbeat <slug> <body>
16
+ # do-signal.sh [--print] --outcome <slug> tags=a,b actor=<aid> success=<bool> [weight=<n>]
16
17
  #
17
18
  # --print: write JSON to stdout without POSTing (tests + dry runs).
18
19
  # Extra key=value pairs are merged into the world:do-event payload.
@@ -117,6 +118,23 @@ _jsonesc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t
117
118
  # session happened to be attested.
118
119
  CC_TASKS_WORKSPACE="${CC_TASKS_WORKSPACE:-one}"
119
120
 
121
+ # CC_TASKS_AUTHOR — WHO the comment is from, when it is not simply "the
122
+ # workspace". tasks:comment records an author on the message and the inbox draws
123
+ # it (three directors on one task read as three speakers, not one anonymous
124
+ # run), but a field no caller supplies is a field production never sets: this
125
+ # script and the MCP door are the two writers, and both built their payload with
126
+ # tid/workspace/body only, so every /do close and every wave heartbeat landed
127
+ # `authorKind:"staff"`. The receiver honours the field only for an ATTESTED
128
+ # staff caller — which is exactly what GATEWAY_API_KEY auth above makes this
129
+ # script — and validates the slug shape; a caller that cannot be attested names
130
+ # nobody. UNSET is the default and emits the byte-identical payload this script
131
+ # has always sent, so nothing changes for a harness that does not set it.
132
+ CC_TASKS_AUTHOR="${CC_TASKS_AUTHOR:-}"
133
+ _author_json() { # emits `,"author":"<slug>"` or nothing
134
+ [ -n "$CC_TASKS_AUTHOR" ] || return 0
135
+ printf ',"author":"%s"' "$CC_TASKS_AUTHOR"
136
+ }
137
+
120
138
  # signal_tasks_create — plan arm: files an open task tagged slug:<slug>, so the
121
139
  # plan appears in tasks:everywhere the moment it's armed (C1/C2 interface contract).
122
140
  signal_tasks_create() { # signal_tasks_create <slug> <title> [tags=a,b,c] [notes=...]
@@ -152,25 +170,16 @@ signal_tasks_create() { # signal_tasks_create <slug> <title> [tags=a,b,c] [note
152
170
  # The cycle list is read from do-plan-json.sh, which is a parser, not a model.
153
171
  # tasks:create dedupes on the slug tag, so re-running a plan is idempotent.
154
172
  signal_tasks_plan() { # signal_tasks_plan <slug>
155
- local slug="$1" plan cid title n=0
156
- plan="$(bash "$(dirname "${BASH_SOURCE[0]}")/do-plan-json.sh" "$slug" 2>/dev/null)" || {
157
- printf 'do-signal: cannot parse plan %s — no cycle tasks filed\n' "$slug" >&2; return 1; }
158
- # one task per cycle, tagged so the router can fan on any of them
159
- while IFS=$'\t' read -r cid title; do
160
- [ -n "$cid" ] && [ "$cid" != "null" ] || continue
161
- if $PRINT; then
162
- signal_tasks_create "${slug}-${cid}" "${title:-$slug $cid}" "tags=do:cycle,cycle:${cid#C},slug:${slug}"
163
- else
164
- signal_tasks_create "${slug}-${cid}" "${title:-$slug $cid}" "tags=do:cycle,cycle:${cid#C},slug:${slug}" >/dev/null || true
165
- fi
166
- n=$((n+1))
167
- # `.cycles` is an OBJECT KEYED BY CYCLE ID, not an array. The first draft
168
- # used `.cycles[]? | .id` and got null for every id -- it still reported
169
- # "filed 2 cycle task(s)", so the count looked right while every task was
170
- # named `<slug>-` and tagged `cycle:`. A count is not a check.
171
- done < <(printf '%s' "$plan" | jq -r '.cycles | to_entries[] | "\(.key)\t\(.value.title // .key)"' 2>/dev/null)
172
- [ "$n" -gt 0 ] || { printf 'do-signal: %s parsed but yielded ZERO cycles — refusing to report success\n' "$slug" >&2; return 1; }
173
- printf 'do-signal: filed %d cycle task(s) for %s\n' "$n" "$slug"
173
+ # ONE MINTER (2026-09-12). This used to file the cycles itself — flat, unchained,
174
+ # tagged `slug:<slug>-C1` in UPPER case. SLUG_RE is lowercase-only, so those rows
175
+ # never deduped on re-arm and `--task-cycle` (which lowercases) closed a DIFFERENT
176
+ # row: the armed rows sat open forever. do-board.sh mints the same rows chained
177
+ # (`tasks:subtask` + blockedBy from the batch DAG), tagged `slug:<slug>-c<n>` —
178
+ # the exact tag _tid_for_slug resolves so the existing --task-cycle close moves
179
+ # THESE rows and the gate unblocks the next batch by itself.
180
+ local slug="$1"
181
+ if $PRINT; then bash "$_ROOT/.claude/scripts/do-board.sh" "$slug" --dry-run; return $?; fi
182
+ bash "$_ROOT/.claude/scripts/do-board.sh" "$slug"
174
183
  }
175
184
 
176
185
  # signal_tasks_claim — cycle open: leases the plan's substrate task so a fleet
@@ -224,7 +233,15 @@ signal_tasks_claim() { # signal_tasks_claim <slug>
224
233
  # task:<traceId>, so "task:<slug>" never matched — claim silently 404'd for every /do
225
234
  # run until this. The server resolves slug → tid (resolvers/tasks.ts resolveTaskBySlug).
226
235
  local slug="$1" tid
227
- tid=$(_tid_for_slug "$slug") || true
236
+ # THE LEASE MOVES TO THE CYCLE. Once do-board.sh hangs cycle rows off the plan
237
+ # row (`containment`), blockersResolved refuses `picked` on the parent while any
238
+ # child is open — so a claim on the plan row reads `blocked` for the whole run.
239
+ # Claim the FIRST OPEN CYCLE's row instead: the thing actually in flight, gated by
240
+ # its own batch predecessors, which is the DAG doing its job. Falls back to the
241
+ # plan row for an unmirrored plan.
242
+ tid=$(DO_PLAN_TODO="${DO_PLAN_TODO:-}" bash "$_ROOT/.claude/scripts/do-plan-json.sh" "$slug" 2>/dev/null \
243
+ | jq -r '.cycleRows[.openCycles[0]] // empty' 2>/dev/null) || true
244
+ [ -n "$tid" ] || tid=$(_tid_for_slug "$slug") || true
228
245
  local payload
229
246
  if [ -n "$tid" ]; then
230
247
  payload="{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\"}}"
@@ -248,6 +265,65 @@ signal_tasks_reap() { # signal_tasks_reap
248
265
  # signal_tasks_link — cycle close: records the handoff-completion back on the
249
266
  # origin task (provenance line + task-status:done), same shape lifecycle.ts's
250
267
  # tasks_link MCP tool posts.
268
+ # signal_outcome — /close's loop-closer, and the reason it is world:outcome.
269
+ #
270
+ # THE NAMING GAP, measured. `/api/receivers/signals` ranks every emitted signal
271
+ # name as declared | candidate | unhomed against packages/sdk/src/receivers.ts,
272
+ # because 35 distinct event names were emitted in 90 days and exactly ONE
273
+ # (`world:in`) was a declared receiver. /close was a top contributor: measured
274
+ # 2026-09-09, `do:close` 0, `loop:feedback` 0, `world:do-event` 0 declarations —
275
+ # three of the four names it emitted could not be answered by anything, and it
276
+ # reported success anyway.
277
+ #
278
+ # `world:outcome` is not a new contract written for this. It already says exactly
279
+ # what /close does: "whoever consumed a route reports success/failure, moving the
280
+ # same tag->receiver paths the router reads - success marks, failure warns."
281
+ # So the fix converges four names to one and designs nothing.
282
+ #
283
+ # THE VERB IS READ BACK, NEVER ASSUMED. The receiver answers
284
+ # {ok, outcome:"marked"|"warned", ...}; a caller that prints "marked" because it
285
+ # sent success:true is reporting its own intent, which is the class of defect the
286
+ # whole four-outcome grammar exists to stop.
287
+ #
288
+ # TAGS ARE BARE WORDS. `marketing`, never `lifecycle:marketing` - a namespaced
289
+ # tag matches zero subscribers. `slug:`/`cycle:` are provenance, not stakes, and
290
+ # are appended separately so they can never be mistaken for a routable tag.
291
+ signal_outcome() { # signal_outcome <slug> tags=a,b actor=<aid> success=<bool> weight=<n> [workspace=<ws>]
292
+ local slug="$1"; shift
293
+ local tags="" actor="" success="true" weight="" ws="${CC_TASKS_WORKSPACE}"
294
+ local kv
295
+ for kv in "$@"; do
296
+ case "$kv" in
297
+ tags=*) tags="${kv#*=}" ;;
298
+ actor=*) actor="${kv#*=}" ;;
299
+ success=*) success="${kv#*=}" ;;
300
+ weight=*) weight="${kv#*=}" ;;
301
+ workspace=*) ws="${kv#*=}" ;;
302
+ esac
303
+ done
304
+ [ -n "$slug" ] || { echo "signal_outcome: slug required" >&2; return 2; }
305
+ [ -n "$actor" ] || { echo "signal_outcome: actor= required (world:outcome moves that actor's paths)" >&2; return 2; }
306
+ case "$success" in true|false) ;; *) echo "signal_outcome: success must be true|false" >&2; return 2 ;; esac
307
+
308
+ # A namespaced word here would route to nobody. Refuse it loudly rather than
309
+ # emit a signal that matches zero subscribers and still exits 0.
310
+ local t
311
+ for t in ${tags//,/ }; do
312
+ case "$t" in
313
+ slug:*|cycle:*) ;;
314
+ *:*) echo "signal_outcome: '$t' is namespaced - stake tags are BARE words (see subscribe-tags-parity.test.ts)" >&2; return 2 ;;
315
+ esac
316
+ done
317
+
318
+ tags="${tags:+${tags},}slug:${slug}"
319
+ local tags_json
320
+ tags_json=$(printf '%s' "$tags" | awk -F',' '{for(i=1;i<=NF;i++){if($i!=""){printf "%s\"%s\"", (n++?",":""), $i}}}')
321
+
322
+ local payload="{\"data\":{\"tags\":[${tags_json}],\"actor\":\"$(_jsonesc "$actor")\",\"success\":${success}${weight:+,\"weight\":${weight}},\"slug\":\"$(_jsonesc "$slug")\",\"workspace\":\"$(_jsonesc "$ws")\"}}"
323
+ if $PRINT; then printf '%s\n' "$payload"; return 0; fi
324
+ _post_ask "world:outcome" "$payload"
325
+ }
326
+
251
327
  signal_tasks_link() { # signal_tasks_link <slug> [docs=a,b,c]
252
328
  # Slug-addressed like claim — the server resolves slug → the origin tid create wrote.
253
329
  local slug="$1" docs=""
@@ -269,7 +345,7 @@ signal_tasks_link() { # signal_tasks_link <slug> [docs=a,b,c]
269
345
  if _post_ask "tasks:link" "$payload"; then return 0; fi
270
346
  [ -n "$tid" ] || return 1
271
347
  _post_ask "tasks:status" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"status\":\"done\"}}" >/dev/null || return 1
272
- _post_ask "tasks:comment" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"/do close — ${slug}${docs:+ (docs: ${docs})}\"}}"
348
+ _post_ask "tasks:comment" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"/do close — ${slug}${docs:+ (docs: ${docs})}\"$(_author_json)}}"
273
349
  }
274
350
 
275
351
  # signal_tasks_heartbeat — batch/wave close: posts tasks:comment so the lease
@@ -285,9 +361,9 @@ signal_tasks_heartbeat() { # signal_tasks_heartbeat <slug> <body>
285
361
  # accompanied, when the tid is known — otherwise the broken resolver runs anyway.
286
362
  local payload
287
363
  if [ -n "$tid" ]; then
288
- payload="{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"}}"
364
+ payload="{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"$(_author_json)}}"
289
365
  else
290
- payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"}}"
366
+ payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"$(_author_json)}}"
291
367
  fi
292
368
  if $PRINT; then printf '%s\n' "$payload"; return 0; fi
293
369
  _post_ask "tasks:comment" "$payload"
@@ -315,6 +391,7 @@ while [ "$#" -gt 0 ]; do
315
391
  --task-reap) shift; signal_tasks_reap "$@"; exit $? ;;
316
392
  --task-link) shift; signal_tasks_link "$@"; exit $? ;;
317
393
  --task-heartbeat) shift; signal_tasks_heartbeat "$@"; exit $? ;;
394
+ --outcome) shift; signal_outcome "$@"; exit $? ;;
318
395
  *) break ;;
319
396
  esac
320
397
  done
@@ -322,6 +399,7 @@ done
322
399
  if [ "$#" -lt 4 ]; then
323
400
  echo "usage: do-signal.sh [--print] <type> <slug> <tier> <cycle> [key=value ...]" >&2
324
401
  echo " do-signal.sh [--print] --task-create|--task-claim|--task-link|--task-heartbeat <slug> ..." >&2
402
+ echo " do-signal.sh [--print] --outcome <slug> tags=a,b actor=<aid> success=<bool> [weight=<n>]" >&2
325
403
  exit 1
326
404
  fi
327
405
 
@@ -4,6 +4,23 @@
4
4
  # the seven-canon predicate (do-reconcile.sh) + navigation reachable/orphan fixtures.
5
5
  # Exits 0 only if ALL pass.
6
6
  set -uo pipefail
7
+
8
+ # _GATE — route a heavy compute through the machine governor. A gate_lock only
9
+ # dedupes IDENTICAL work; a SLOT is what bounds N worktrees each running one of
10
+ # these at once (measured 2026-09-07: three concurrent 2.5GB typecheckers, every
11
+ # lock uncontended, load 171). Empty when already inside a gate, so a nested call
12
+ # inherits the outer slot rather than taking a second one.
13
+ #
14
+ # Resolves gate-run.sh from its OWN directory, deliberately: an earlier version
15
+ # keyed off $ROOT and got inserted above the line that sets it, so _GATE was
16
+ # silently empty and every call ran ungoverned -- a fail-OPEN, which is the exact
17
+ # defect this preamble exists to close.
18
+ _GATE=()
19
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
20
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
21
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
22
+ fi
23
+
7
24
  cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 2
8
25
  S=.claude/scripts
9
26
  fail=0
@@ -61,7 +78,7 @@ echo "== C2 do-reconcile — types canon =="
61
78
  # Use a temp file to capture grep output — avoids pipefail swallowing the value when tsc
62
79
  # exits non-zero (which it does whenever there are any type errors).
63
80
  _tsc_tmp=$(mktemp)
64
- ( cd one.ie/web 2>/dev/null && bunx tsc --noEmit 2>&1 | grep -c 'error TS' > "$_tsc_tmp" ) 2>/dev/null || true
81
+ ( cd one.ie/web 2>/dev/null && "${_GATE[@]}" bunx tsc --noEmit 2>&1 | grep -c 'error TS' > "$_tsc_tmp" ) 2>/dev/null || true
65
82
  _tsc_now=$(cat "$_tsc_tmp" 2>/dev/null | tr -d '[:space:]'); rm -f "$_tsc_tmp"
66
83
  echo "{\"tscErrors\": ${_tsc_now:-0}}" > .w0-baseline.json
67
84
  "$S/do-reconcile.sh" types >/dev/null 2>&1 && ok "types delta=0 → exit 0" || no "types should pass when no delta"
@@ -49,6 +49,16 @@
49
49
  # because a skip is unrun, not pass; never set it for real.
50
50
  set -euo pipefail
51
51
 
52
+ # _GATE — see governor-doors-check.sh "compute sites". A gate_lock dedupes
53
+ # identical work; only a SLOT bounds N worktrees running N computes at once.
54
+ # Empty inside an existing gate so the outer slot is inherited, never doubled.
55
+ _GATE=()
56
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
57
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
58
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
59
+ fi
60
+
61
+
52
62
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
53
63
  S="$ROOT/.claude/scripts"
54
64
  RESULTS_FILE=""
@@ -265,7 +275,7 @@ _gates() {
265
275
  if [ -z "$base" ]; then
266
276
  _add tsc-delta unrun ".w0-baseline.json has no tscErrors"
267
277
  else
268
- set +e; now="$(cd "$ROOT" && bunx tsc --noEmit 2>&1 | grep -c 'error TS')"; set -e
278
+ set +e; now="$(cd "$ROOT" && "${_GATE[@]}" bunx tsc --noEmit 2>&1 | grep -c 'error TS')"; set -e
269
279
  if [ "$now" -le "$base" ]; then _add tsc-delta pass "baseline=$base now=$now"
270
280
  else _add tsc-delta fail "baseline=$base now=$now — new type errors"; fi
271
281
  fi