@event4u/agent-config 10.4.0 → 12.0.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 (303) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +73 -182
  3. package/dist/agent-src/commands/agent-handoff.md +0 -1
  4. package/dist/agent-src/commands/agent-status.md +0 -1
  5. package/dist/agent-src/commands/agents/audit.md +0 -1
  6. package/dist/agent-src/commands/agents/init.md +0 -1
  7. package/dist/agent-src/commands/agents/optimize.md +0 -1
  8. package/dist/agent-src/commands/agents/user/accept.md +0 -1
  9. package/dist/agent-src/commands/agents/user/delete.md +0 -1
  10. package/dist/agent-src/commands/agents/user/init.md +0 -1
  11. package/dist/agent-src/commands/agents/user/review.md +0 -1
  12. package/dist/agent-src/commands/agents/user/show.md +0 -1
  13. package/dist/agent-src/commands/agents/user/update.md +0 -1
  14. package/dist/agent-src/commands/agents/user.md +0 -1
  15. package/dist/agent-src/commands/agents.md +0 -1
  16. package/dist/agent-src/commands/analytics/prune.md +0 -1
  17. package/dist/agent-src/commands/analytics/show.md +0 -1
  18. package/dist/agent-src/commands/analytics.md +0 -1
  19. package/dist/agent-src/commands/analyze/conformance.md +0 -1
  20. package/dist/agent-src/commands/analyze/decision.md +0 -1
  21. package/dist/agent-src/commands/analyze/inbox.md +0 -1
  22. package/dist/agent-src/commands/analyze/incident.md +0 -1
  23. package/dist/agent-src/commands/analyze/near-miss.md +0 -1
  24. package/dist/agent-src/commands/analyze/postmortem.md +0 -1
  25. package/dist/agent-src/commands/analyze/premortem.md +0 -1
  26. package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
  27. package/dist/agent-src/commands/analyze.md +0 -1
  28. package/dist/agent-src/commands/brand/identity.md +0 -1
  29. package/dist/agent-src/commands/brand/review.md +0 -1
  30. package/dist/agent-src/commands/brand/strategy.md +0 -1
  31. package/dist/agent-src/commands/brand/tokens.md +0 -1
  32. package/dist/agent-src/commands/brand/voice.md +0 -1
  33. package/dist/agent-src/commands/brand.md +0 -1
  34. package/dist/agent-src/commands/bug/fix.md +0 -1
  35. package/dist/agent-src/commands/bug/investigate.md +0 -1
  36. package/dist/agent-src/commands/bug.md +0 -1
  37. package/dist/agent-src/commands/challenge-me/vision.md +0 -1
  38. package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
  39. package/dist/agent-src/commands/challenge-me.md +0 -1
  40. package/dist/agent-src/commands/chat-history/import.md +0 -1
  41. package/dist/agent-src/commands/chat-history.md +0 -1
  42. package/dist/agent-src/commands/check-current-md.md +0 -1
  43. package/dist/agent-src/commands/commit/in-chunks.md +0 -1
  44. package/dist/agent-src/commands/commit.md +0 -1
  45. package/dist/agent-src/commands/condense.md +0 -1
  46. package/dist/agent-src/commands/context/create.md +0 -1
  47. package/dist/agent-src/commands/context/refactor.md +0 -1
  48. package/dist/agent-src/commands/context.md +0 -1
  49. package/dist/agent-src/commands/contribution-precheck.md +0 -1
  50. package/dist/agent-src/commands/cost/profile.md +0 -1
  51. package/dist/agent-src/commands/cost/report.md +0 -1
  52. package/dist/agent-src/commands/cost.md +0 -1
  53. package/dist/agent-src/commands/council/analysis.md +0 -1
  54. package/dist/agent-src/commands/council/debate.md +13 -5
  55. package/dist/agent-src/commands/council/default.md +26 -12
  56. package/dist/agent-src/commands/council/design.md +0 -1
  57. package/dist/agent-src/commands/council/optimize.md +0 -1
  58. package/dist/agent-src/commands/council/pr.md +0 -1
  59. package/dist/agent-src/commands/council.md +1 -2
  60. package/dist/agent-src/commands/design-system/capture.md +0 -1
  61. package/dist/agent-src/commands/design-system/generate.md +23 -2
  62. package/dist/agent-src/commands/design-system/import.md +0 -1
  63. package/dist/agent-src/commands/design-system.md +0 -1
  64. package/dist/agent-src/commands/estimate-ticket.md +0 -1
  65. package/dist/agent-src/commands/explain-run.md +0 -1
  66. package/dist/agent-src/commands/feature/dev.md +0 -1
  67. package/dist/agent-src/commands/feature/explore.md +0 -1
  68. package/dist/agent-src/commands/feature/plan.md +0 -1
  69. package/dist/agent-src/commands/feature/refactor.md +0 -1
  70. package/dist/agent-src/commands/feature/roadmap.md +0 -1
  71. package/dist/agent-src/commands/feature.md +0 -1
  72. package/dist/agent-src/commands/fix/ci.md +0 -1
  73. package/dist/agent-src/commands/fix/comments.md +0 -1
  74. package/dist/agent-src/commands/fix/portability.md +0 -1
  75. package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
  76. package/dist/agent-src/commands/fix/pr-comments.md +0 -1
  77. package/dist/agent-src/commands/fix/quality.md +0 -1
  78. package/dist/agent-src/commands/fix/refs.md +0 -1
  79. package/dist/agent-src/commands/fix/route.md +0 -1
  80. package/dist/agent-src/commands/fix/seeder.md +0 -1
  81. package/dist/agent-src/commands/fix.md +0 -1
  82. package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
  83. package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
  84. package/dist/agent-src/commands/ghostwriter/list.md +0 -1
  85. package/dist/agent-src/commands/ghostwriter/show.md +0 -1
  86. package/dist/agent-src/commands/ghostwriter/write.md +0 -1
  87. package/dist/agent-src/commands/ghostwriter.md +0 -1
  88. package/dist/agent-src/commands/grill-me.md +0 -1
  89. package/dist/agent-src/commands/humanize.md +0 -1
  90. package/dist/agent-src/commands/image/analyse.md +0 -1
  91. package/dist/agent-src/commands/image/create.md +0 -1
  92. package/dist/agent-src/commands/image/verify.md +0 -1
  93. package/dist/agent-src/commands/image.md +0 -1
  94. package/dist/agent-src/commands/implement-ticket.md +0 -1
  95. package/dist/agent-src/commands/jira-ticket.md +0 -1
  96. package/dist/agent-src/commands/judge/on-diff.md +0 -1
  97. package/dist/agent-src/commands/judge/solo.md +0 -1
  98. package/dist/agent-src/commands/judge/steps.md +0 -1
  99. package/dist/agent-src/commands/judge.md +0 -1
  100. package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
  101. package/dist/agent-src/commands/knowledge/forget.md +0 -1
  102. package/dist/agent-src/commands/knowledge/ingest.md +0 -1
  103. package/dist/agent-src/commands/knowledge/list.md +0 -1
  104. package/dist/agent-src/commands/knowledge.md +0 -1
  105. package/dist/agent-src/commands/memory/add.md +0 -1
  106. package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
  107. package/dist/agent-src/commands/memory/load.md +0 -1
  108. package/dist/agent-src/commands/memory/mine-session.md +0 -1
  109. package/dist/agent-src/commands/memory/promote.md +0 -1
  110. package/dist/agent-src/commands/memory/propose.md +0 -1
  111. package/dist/agent-src/commands/memory.md +0 -1
  112. package/dist/agent-src/commands/mission/upgrade.md +0 -1
  113. package/dist/agent-src/commands/mode.md +0 -1
  114. package/dist/agent-src/commands/module/create.md +0 -1
  115. package/dist/agent-src/commands/module/explore.md +0 -1
  116. package/dist/agent-src/commands/module.md +0 -1
  117. package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
  118. package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
  119. package/dist/agent-src/commands/optimize/deep.md +0 -1
  120. package/dist/agent-src/commands/optimize/project.md +0 -1
  121. package/dist/agent-src/commands/optimize/prompt.md +0 -1
  122. package/dist/agent-src/commands/optimize/rtk.md +0 -1
  123. package/dist/agent-src/commands/optimize/skills.md +0 -1
  124. package/dist/agent-src/commands/optimize.md +0 -1
  125. package/dist/agent-src/commands/orchestrate.md +0 -1
  126. package/dist/agent-src/commands/override/create.md +0 -1
  127. package/dist/agent-src/commands/override/manage.md +0 -1
  128. package/dist/agent-src/commands/override.md +0 -1
  129. package/dist/agent-src/commands/package/reset.md +0 -1
  130. package/dist/agent-src/commands/package/test.md +0 -1
  131. package/dist/agent-src/commands/package.md +0 -1
  132. package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
  133. package/dist/agent-src/commands/post-as/me.md +0 -1
  134. package/dist/agent-src/commands/post-as.md +0 -1
  135. package/dist/agent-src/commands/pr/create/description-only.md +0 -1
  136. package/dist/agent-src/commands/pr/create.md +67 -28
  137. package/dist/agent-src/commands/prediction-pool.md +0 -1
  138. package/dist/agent-src/commands/prepare-for-review.md +0 -1
  139. package/dist/agent-src/commands/profile/activate.md +0 -1
  140. package/dist/agent-src/commands/profile/deactivate.md +0 -1
  141. package/dist/agent-src/commands/profile/show.md +0 -1
  142. package/dist/agent-src/commands/profile.md +0 -1
  143. package/dist/agent-src/commands/project/analyze.md +0 -1
  144. package/dist/agent-src/commands/project/health.md +0 -1
  145. package/dist/agent-src/commands/project.md +0 -1
  146. package/dist/agent-src/commands/refine-ticket.md +0 -1
  147. package/dist/agent-src/commands/research/deep.md +0 -1
  148. package/dist/agent-src/commands/research/report.md +0 -1
  149. package/dist/agent-src/commands/research.md +0 -1
  150. package/dist/agent-src/commands/review/changes.md +0 -1
  151. package/dist/agent-src/commands/review/routing.md +0 -1
  152. package/dist/agent-src/commands/review.md +0 -1
  153. package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
  154. package/dist/agent-src/commands/roadmap/create.md +0 -1
  155. package/dist/agent-src/commands/roadmap/materialize.md +0 -1
  156. package/dist/agent-src/commands/roadmap/next.md +0 -1
  157. package/dist/agent-src/commands/roadmap/process-full.md +0 -1
  158. package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
  159. package/dist/agent-src/commands/roadmap/process-step.md +0 -1
  160. package/dist/agent-src/commands/roadmap.md +0 -1
  161. package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
  162. package/dist/agent-src/commands/security-audit-config.md +0 -1
  163. package/dist/agent-src/commands/skill/preview.md +0 -1
  164. package/dist/agent-src/commands/skill.md +0 -1
  165. package/dist/agent-src/commands/skills/discover.md +0 -1
  166. package/dist/agent-src/commands/skills.md +0 -1
  167. package/dist/agent-src/commands/sync/agent-settings.md +0 -1
  168. package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
  169. package/dist/agent-src/commands/sync/gitignore.md +0 -1
  170. package/dist/agent-src/commands/sync.md +0 -1
  171. package/dist/agent-src/commands/tdd/green.md +0 -1
  172. package/dist/agent-src/commands/tdd/red.md +0 -1
  173. package/dist/agent-src/commands/tdd/refactor.md +0 -1
  174. package/dist/agent-src/commands/tdd.md +0 -1
  175. package/dist/agent-src/commands/team/adversarial.md +0 -1
  176. package/dist/agent-src/commands/team/delegate.md +0 -1
  177. package/dist/agent-src/commands/team/review.md +0 -1
  178. package/dist/agent-src/commands/team/status.md +0 -1
  179. package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
  180. package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
  181. package/dist/agent-src/commands/team-knowledge.md +0 -1
  182. package/dist/agent-src/commands/team.md +0 -1
  183. package/dist/agent-src/commands/tests/create.md +0 -1
  184. package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
  185. package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
  186. package/dist/agent-src/commands/tests/execute.md +0 -1
  187. package/dist/agent-src/commands/tests.md +0 -1
  188. package/dist/agent-src/commands/threat-model.md +0 -1
  189. package/dist/agent-src/commands/update-form-request-messages.md +0 -1
  190. package/dist/agent-src/commands/upstream-contribute.md +0 -1
  191. package/dist/agent-src/commands/video/from-script.md +0 -1
  192. package/dist/agent-src/commands/video/from-song.md +0 -1
  193. package/dist/agent-src/commands/video/scene.md +0 -1
  194. package/dist/agent-src/commands/video/stitch.md +0 -1
  195. package/dist/agent-src/commands/video/storyboard.md +0 -1
  196. package/dist/agent-src/commands/video.md +0 -1
  197. package/dist/agent-src/commands/work.md +0 -1
  198. package/dist/agent-src/commands/worktree/cleanup.md +0 -1
  199. package/dist/agent-src/commands/worktree/create.md +5 -4
  200. package/dist/agent-src/commands/worktree/status.md +0 -1
  201. package/dist/agent-src/commands/worktree/verify.md +0 -1
  202. package/dist/agent-src/commands/worktree.md +0 -1
  203. package/dist/agent-src/rules/settings-ask-protocol.md +14 -9
  204. package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
  205. package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
  206. package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
  207. package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
  208. package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
  209. package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
  210. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
  211. package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
  212. package/dist/agent-src/skills/git-workflow/SKILL.md +17 -3
  213. package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
  214. package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
  215. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
  216. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
  217. package/dist/agent-src/templates/agent-settings.md +0 -1
  218. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  219. package/dist/cli/commands/commands.candidates.test.js +1 -1
  220. package/dist/cli/commands/commands.candidates.test.js.map +1 -1
  221. package/dist/cli/commands/commands.js +9 -15
  222. package/dist/cli/commands/commands.js.map +1 -1
  223. package/dist/cli/commands/profiles.test.js +7 -7
  224. package/dist/cli/commands/profiles.test.js.map +1 -1
  225. package/dist/cli/discovery/loadManifest.js.map +1 -1
  226. package/dist/cli/main.js +2 -2
  227. package/dist/cli/main.js.map +1 -1
  228. package/dist/cli-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
  229. package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
  230. package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
  231. package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
  232. package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
  233. package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
  234. package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
  235. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  236. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  237. package/dist/cli-delegate/cmd_conformance.js +16 -4
  238. package/dist/cli-delegate/cmd_doctor.js +2 -2
  239. package/dist/cli-delegate/cmd_explain.js +2 -2
  240. package/dist/cli-delegate/cmd_export.js +4 -4
  241. package/dist/cli-delegate/cmd_handoff.js +2 -2
  242. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  243. package/dist/cli-delegate/cmd_migrate.js +2 -2
  244. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  245. package/dist/cli-delegate/cmd_prune.js +4 -4
  246. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  247. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  248. package/dist/cli-delegate/cmd_session_recycle.js +1 -1
  249. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  250. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  251. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  252. package/dist/cli-delegate/cmd_sync.js +4 -4
  253. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  254. package/dist/cli-delegate/cmd_update.js +1 -1
  255. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  256. package/dist/cli-delegate/cmd_validate.js +4 -4
  257. package/dist/cli-delegate/cmd_versions.js +1 -1
  258. package/dist/discovery/deprecation-report.md +1 -1
  259. package/dist/discovery/discovery-manifest.json +216 -415
  260. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  261. package/dist/discovery/discovery-manifest.summary.md +2 -2
  262. package/dist/discovery/dormancy-report.md +1 -1
  263. package/dist/discovery/orphan-report.md +1 -1
  264. package/dist/discovery/packs.json +3 -3
  265. package/dist/discovery/trust-report.md +1 -1
  266. package/dist/discovery/workspaces.json +3 -3
  267. package/dist/hooks/dispatch.js +68 -14
  268. package/dist/install/install.mjs +5 -7
  269. package/dist/mcp/registry-manifest.json +3 -3
  270. package/dist/mcp/server.json +2 -2
  271. package/dist/mcp/server.mjs +48 -6
  272. package/dist/scripts/_lib/agent_settings.js +8 -0
  273. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  274. package/dist/server/schemas/settings.js +3 -4
  275. package/dist/server/schemas/settings.js.map +1 -1
  276. package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
  277. package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
  278. package/dist/ui/index.html +1 -1
  279. package/docs/MIGRATION.md +1 -0
  280. package/docs/contracts/settings-classes.md +53 -9
  281. package/package.json +1 -1
  282. package/src/config/agent-settings.template.yml +49 -18
  283. package/src/config/gate-coverage.yml +13 -3
  284. package/src/config/gate-violation-baselines.json +4 -4
  285. package/src/config/pack-size-budget.json +3 -2
  286. package/src/scripts/_dispatch.bash +42 -5
  287. package/src/scripts/_lib/agent_settings.ts +8 -0
  288. package/src/scripts/audit_command_surface.ts +21 -15
  289. package/src/scripts/build_catalog_index.ts +1 -1
  290. package/src/scripts/build_discovery_manifest.ts +30 -34
  291. package/src/scripts/check_branch_freshness.ts +393 -12
  292. package/src/scripts/external_sources_denylist.json +7 -2
  293. package/src/scripts/gen_discovery_baseline.ts +3 -3
  294. package/src/scripts/hook_manifest.yaml +20 -0
  295. package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
  296. package/src/scripts/lint_command_tiers.ts +29 -75
  297. package/src/scripts/lint_originality.ts +101 -14
  298. package/src/scripts/schemas/command.schema.json +4 -9
  299. package/src/scripts/schemas/skill.schema.json +10 -1
  300. package/src/scripts/session_register_hook.ts +120 -10
  301. package/src/scripts/skill_linter.ts +82 -2
  302. package/src/scripts/worktree_cleanup_check.ts +111 -7
  303. package/src/server/schemas/settings.ts +3 -6
@@ -31,6 +31,33 @@
31
31
  * It never fetches, merges, or rebases for you. Choosing how to catch up is a
32
32
  * git-history decision (`git-history-discipline`), and a gate that rewrites
33
33
  * history to make itself pass is a worse failure than the one it reports.
34
+ *
35
+ * WHICH BASE — asked of the forge, not assumed.
36
+ *
37
+ * The first version hardcoded `main`. That is right for most branches here and
38
+ * wrong for every branch whose PR targets something else: a stacked PR, a
39
+ * release line, a consumer repo on `master`. On those the gate produced a false
40
+ * GREEN — "current with origin/main" while the branch sat behind the base it
41
+ * would actually merge into — which is worse than no gate, because it answers a
42
+ * question nobody asked in the voice of the one they did.
43
+ *
44
+ * So the base is resolved, in order: an explicit `--base`, then the base of the
45
+ * OPEN PR for this branch as the forge reports it, then the repo's default
46
+ * branch AS THE SERVER REPORTS IT (`ls-remote --symref`), then the local
47
+ * `origin/HEAD`, then `main`. Every message names the base AND where it came
48
+ * from, because "I checked main because that is the PR's base" and "I checked
49
+ * main because I could not ask" must never print the same.
50
+ *
51
+ * The server symref matters for the same reason `remoteHead` exists: the local
52
+ * `refs/remotes/origin/HEAD` is written at clone time and never refreshed, so
53
+ * taking the base NAME from it would reintroduce the stale-local-state failure
54
+ * this module argues against everywhere else. It is kept only as the labelled
55
+ * fallback for when the server cannot be reached.
56
+ *
57
+ * When the forge cannot be asked (no `gh`, not authenticated, no GitHub remote)
58
+ * the run says so out loud even under `--quiet`: it has verified the default
59
+ * base and has NOT ruled out an open PR against a different one. That is a
60
+ * partial answer, and partial answers are reported as partial.
34
61
  */
35
62
  import { execFileSync } from "node:child_process";
36
63
  import fs from "node:fs";
@@ -43,18 +70,254 @@ import { reportScanned } from "./_lib/scan_scope.js";
43
70
 
44
71
  // ledger-exempt: single remote-ref probe — the entire scope is ONE ls-remote answer (0 or 1 refs) resolved to one aggregate ancestor verdict, and every empty path already publishes its reason via reportScanned allowEmpty; there is no per-target collection to account.
45
72
  const DEFAULT_BASE = "main";
46
- const SELF_TEST_MIN_CASES = 3;
47
- const SELF_TEST_MIN_REJECT = 1;
73
+ const SELF_TEST_MIN_CASES = 5;
74
+ const SELF_TEST_MIN_REJECT = 2;
48
75
  const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
76
+ /**
77
+ * One network round trip on the push path — long enough for a cold `gh`, short
78
+ * enough that the worst case stays under budget.
79
+ *
80
+ * It bounds `git` as well as `gh`, and that is the point rather than tidiness:
81
+ * with a timeout on `gh` only, the two `ls-remote` calls could hang unbounded
82
+ * against an unresponsive SSH remote, so the stated worst case was not one. The
83
+ * slowest path makes three calls (forge, symref, base head), so the arithmetic
84
+ * that keeps this under the 25s `pre_push_budget_seconds` ceiling is 3 x 8s.
85
+ */
86
+ const NETWORK_TIMEOUT_MS = 8_000;
49
87
 
50
88
  function git(args: string[]): string | null {
51
89
  try {
52
- return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
90
+ return execFileSync("git", args, {
91
+ encoding: "utf8",
92
+ stdio: ["ignore", "pipe", "ignore"],
93
+ timeout: NETWORK_TIMEOUT_MS,
94
+ }).trim();
53
95
  } catch {
54
96
  return null;
55
97
  }
56
98
  }
57
99
 
100
+ export type GhResult = { ok: true; stdout: string } | { ok: false; reason: string };
101
+ export type GhRunner = (args: string[]) => GhResult;
102
+
103
+ /**
104
+ * What the forge said about an open PR for this branch.
105
+ *
106
+ * Three-valued on purpose. `none` (the forge answered: no open PR) and
107
+ * `unavailable` (the forge could not be asked) lead to the same base and to
108
+ * DIFFERENT confidence, and collapsing them is exactly how a gate reports a
109
+ * green it did not earn.
110
+ */
111
+ export type ForgeAnswer =
112
+ | { kind: "pr"; base: string; number?: number }
113
+ | { kind: "none" }
114
+ | { kind: "unavailable"; reason: string };
115
+
116
+ /** Where the base name came from — printed with every verdict. */
117
+ export type BaseSource =
118
+ | "flag"
119
+ | "pull-request"
120
+ | "origin-symref"
121
+ | "origin-head"
122
+ | "fallback";
123
+
124
+ export interface BaseResolution {
125
+ readonly base: string;
126
+ readonly source: BaseSource;
127
+ readonly pr?: number;
128
+ /** Present iff an open PR against a different base could not be ruled out. */
129
+ readonly unverified?: string;
130
+ }
131
+
132
+ function gh(args: string[]): GhResult {
133
+ try {
134
+ const out = execFileSync("gh", args, {
135
+ encoding: "utf8",
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ timeout: NETWORK_TIMEOUT_MS,
138
+ });
139
+ return { ok: true, stdout: out.toString() };
140
+ } catch (err) {
141
+ const code = (err as NodeJS.ErrnoException).code;
142
+ if (code === "ENOENT") {
143
+ return { ok: false, reason: "the gh CLI is not installed" };
144
+ }
145
+ if (code === "ETIMEDOUT") {
146
+ return { ok: false, reason: `gh did not answer within ${NETWORK_TIMEOUT_MS / 1000}s` };
147
+ }
148
+ return { ok: false, reason: "gh could not answer — not authenticated, or no GitHub remote" };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * The base of the open PR whose head is `branch`.
154
+ *
155
+ * `pr list --head` rather than `pr view`: `pr view` will happily resolve a
156
+ * CLOSED PR for the same branch, and a merged PR's base is not the base the
157
+ * next push has to be current with.
158
+ */
159
+ export function askForgeForBase(branch: string, run: GhRunner = gh): ForgeAnswer {
160
+ const res = run([
161
+ "pr", "list", "--head", branch, "--state", "open",
162
+ "--json", "number,baseRefName", "--limit", "1",
163
+ ]);
164
+ if (!res.ok) {
165
+ return { kind: "unavailable", reason: res.reason };
166
+ }
167
+ let rows: unknown;
168
+ try {
169
+ rows = JSON.parse(res.stdout);
170
+ } catch {
171
+ return { kind: "unavailable", reason: "gh returned output that is not JSON" };
172
+ }
173
+ if (!Array.isArray(rows) || rows.length === 0) {
174
+ return { kind: "none" };
175
+ }
176
+ const row = rows[0] as { baseRefName?: unknown; number?: unknown };
177
+ const base = typeof row.baseRefName === "string" ? row.baseRefName.trim() : "";
178
+ if (base === "") {
179
+ return { kind: "unavailable", reason: "gh answered without a baseRefName" };
180
+ }
181
+ // The number is cosmetic — it names the PR in the verdict line — while the
182
+ // base is the answer the gate exists to get. An unusable number therefore
183
+ // does NOT discard the base: doing that made the gate check the repo default
184
+ // instead and warn "could not ask the forge" when it had asked and been
185
+ // answered, trading a correct verdict for a tidier message. `describeBase`
186
+ // omits the number rather than rendering the impossible `#0`.
187
+ const usable =
188
+ typeof row.number === "number" && Number.isInteger(row.number) && row.number > 0;
189
+ return usable ? { kind: "pr", base, number: row.number as number } : { kind: "pr", base };
190
+ }
191
+
192
+ /**
193
+ * The default branch as the LOCAL clone believes it — no network.
194
+ *
195
+ * `refs/remotes/origin/HEAD` is written at clone time and never refreshed
196
+ * without `git remote set-head`, so this is exactly the class of stale local
197
+ * state this module argues against everywhere else. It has exactly one caller,
198
+ * `serverDefaultBase`, and is reached only when the server could not be asked —
199
+ * where `describeBase` labels it as possibly stale. It decides no verdict on
200
+ * its own; a short-circuit that let it do so is the regression documented in
201
+ * `resolveBase`.
202
+ */
203
+ export function localDefaultBase(): { base: string; source: "origin-head" | "fallback" } {
204
+ const ref = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
205
+ if (ref !== null && ref.startsWith("origin/")) {
206
+ const name = ref.slice("origin/".length).trim();
207
+ if (name !== "") {
208
+ return { base: name, source: "origin-head" };
209
+ }
210
+ }
211
+ return { base: DEFAULT_BASE, source: "fallback" };
212
+ }
213
+
214
+ /**
215
+ * The default branch as the SERVER reports it right now.
216
+ *
217
+ * Same discipline as `remoteHead`: a gate whose thesis is "ask the remote"
218
+ * must not take the branch NAME from a tracking ref either. Costs one
219
+ * `ls-remote`, the round trip this gate already pays, and degrades to the
220
+ * local ref (labelled as such) when the server cannot be reached.
221
+ */
222
+ export function serverDefaultBase(): {
223
+ base: string;
224
+ source: "origin-symref" | "origin-head" | "fallback";
225
+ } {
226
+ const out = git(["ls-remote", "--symref", "origin", "HEAD"]);
227
+ if (out !== null) {
228
+ for (const line of out.split("\n")) {
229
+ const trimmed = line.trim();
230
+ if (trimmed.startsWith("ref:")) {
231
+ const name = trimmed.slice("ref:".length).trim().split(/\s+/)[0] ?? "";
232
+ if (name.startsWith("refs/heads/")) {
233
+ const short = name.slice("refs/heads/".length).trim();
234
+ if (short !== "") {
235
+ return { base: short, source: "origin-symref" };
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ return localDefaultBase();
242
+ }
243
+
244
+ /**
245
+ * The base requested on the command line, or null when none was.
246
+ *
247
+ * Both spellings are accepted. `--base=develop` used to be dropped silently by
248
+ * an `indexOf("--base")` lookup, and a dropped request is worse than a rejected
249
+ * one here: the gate then auto-resolves some OTHER base and reports a green
250
+ * about a base the caller never asked about.
251
+ */
252
+ export function explicitBase(argv: readonly string[]): string | null {
253
+ const joined = argv.find((a) => a.startsWith("--base="));
254
+ if (joined !== undefined) {
255
+ const value = joined.slice("--base=".length);
256
+ return value === "" ? null : value;
257
+ }
258
+ const flag = argv.indexOf("--base");
259
+ if (flag < 0) {
260
+ return null;
261
+ }
262
+ const explicit = argv[flag + 1];
263
+ // A bare trailing `--base`, or one followed by another flag, is a typo rather
264
+ // than a base name. Accepting it produced `remoteHead("--quiet")`, which fails
265
+ // and lands on the exit-0 NOT-VERIFIED path — a typo reading as a pass.
266
+ if (explicit === undefined || explicit === "" || explicit.startsWith("-")) {
267
+ return null;
268
+ }
269
+ return explicit;
270
+ }
271
+
272
+ /** `--base` wins, then the open PR's base, then the repo default. */
273
+ export function resolveBase(
274
+ argv: readonly string[],
275
+ branch: string,
276
+ forge: (b: string) => ForgeAnswer = (b) => askForgeForBase(b),
277
+ ): BaseResolution {
278
+ const explicit = explicitBase(argv);
279
+ if (explicit !== null) {
280
+ return { base: explicit, source: "flag" };
281
+ }
282
+ // There is deliberately no cheap short-circuit for "the branch looks like the
283
+ // default". It was tried and reverted: it decided a VERDICT from
284
+ // `localDefaultBase`, the stale clone-time ref this module argues against, so
285
+ // a branch whose name collided with a stale or fallback local default was
286
+ // skipped without any check at all. The premise it rested on — that
287
+ // `gh pr list --head main` can never usefully answer — is false: a PR from the
288
+ // default branch into a release line or a parent PR is exactly that answer,
289
+ // and it is the case this gate exists for. Standing on the real base is
290
+ // short-circuited one layer up, in `main`, AFTER the base is known.
291
+ const answer = forge(branch);
292
+ if (answer.kind === "pr") {
293
+ return answer.number === undefined
294
+ ? { base: answer.base, source: "pull-request" }
295
+ : { base: answer.base, source: "pull-request", pr: answer.number };
296
+ }
297
+ const fallback = serverDefaultBase();
298
+ return answer.kind === "unavailable"
299
+ ? { base: fallback.base, source: fallback.source, unverified: answer.reason }
300
+ : { base: fallback.base, source: fallback.source };
301
+ }
302
+
303
+ /** Human-readable provenance — never omitted, so no verdict is ambiguous about what it checked. */
304
+ export function describeBase(r: BaseResolution): string {
305
+ switch (r.source) {
306
+ case "flag":
307
+ return "base given on the command line";
308
+ case "pull-request":
309
+ return r.pr === undefined
310
+ ? "base of the open PR for this branch"
311
+ : `base of open PR #${r.pr}`;
312
+ case "origin-symref":
313
+ return "repo default branch, as the server reports it";
314
+ case "origin-head":
315
+ return "repo default branch from the LOCAL origin/HEAD ref, which may be stale";
316
+ case "fallback":
317
+ return `repo default (assumed ${DEFAULT_BASE} — origin/HEAD is not set)`;
318
+ }
319
+ }
320
+
58
321
  /** The SHA the server reports for `<base>` right now — never a tracking ref. */
59
322
  export function remoteHead(base: string, remote = "origin"): string | null {
60
323
  const out = git(["ls-remote", "--heads", remote, base]);
@@ -93,7 +356,15 @@ export function containsCommit(sha: string): boolean | null {
93
356
  * gate reading the tracking ref would pass it — which is exactly the false green
94
357
  * this gate exists to remove.
95
358
  */
96
- function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
359
+ interface Scaffold {
360
+ origin: string;
361
+ work: string;
362
+ g: (args: string[], at: string) => void;
363
+ write: (at: string, name: string, body: string) => void;
364
+ }
365
+
366
+ /** The bare origin + working clone every fixture below starts from. */
367
+ function scaffold(root: string): Scaffold {
97
368
  const origin = path.join(root, "origin.git");
98
369
  const work = path.join(root, "work");
99
370
  fs.mkdirSync(origin, { recursive: true });
@@ -110,6 +381,11 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
110
381
  g(["remote", "add", "origin", origin], work);
111
382
  write(work, "a.txt", "one");
112
383
  g(["push", "-u", "origin", DEFAULT_BASE], work);
384
+ return { origin, work, g, write };
385
+ }
386
+
387
+ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
388
+ const { origin, work, g, write } = scaffold(root);
113
389
  g(["checkout", "-b", "feat/x"], work);
114
390
  write(work, "b.txt", "two");
115
391
  if (opts.advanceRemote) {
@@ -121,6 +397,58 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
121
397
  return work;
122
398
  }
123
399
 
400
+ /**
401
+ * A repo where the branch's real base is NOT `main`: `feat/x` is cut from
402
+ * `develop`, `develop` then advances on the remote, and `main` never moves.
403
+ * The SERVER's `HEAD` is pointed at `serverDefault`.
404
+ *
405
+ * The base is left for the gate to resolve — no `--base`. That is the whole
406
+ * point of the fixture, and the first version got it wrong: it passed
407
+ * `--base` on both cases, which exercises the flag branch that existed before
408
+ * this change. Deleting the entire forge/symref resolution would have left both
409
+ * cases green, so the pair proved nothing about the new behaviour and the raised
410
+ * self-test floors rested on coverage that did not exist. (R2 finding 2.)
411
+ *
412
+ * With `serverDefault = "develop"` the gate must refuse: that is the base this
413
+ * branch is actually behind. With `serverDefault = "main"` the same tree passes.
414
+ * A regression that reinstates a hardcoded `main` flips the first case, because
415
+ * nothing but the resolution decides which base is asked about.
416
+ */
417
+ function buildTwoBaseFixture(root: string, serverDefault: string): string {
418
+ const { origin, work, g, write } = scaffold(root);
419
+ g(["checkout", "-b", "develop"], work);
420
+ g(["push", "-u", "origin", "develop"], work);
421
+ g(["checkout", "-b", "feat/x"], work);
422
+ write(work, "b.txt", "two");
423
+ // The bare repo's own HEAD is what `ls-remote --symref origin HEAD` reports,
424
+ // so this is the server-side default branch, not a local tracking ref.
425
+ g(["symbolic-ref", "HEAD", `refs/heads/${serverDefault}`], origin);
426
+ // A second clone advances `develop` only — `main` stays exactly where it was.
427
+ const other = path.join(root, "other");
428
+ g(["clone", origin, other], root);
429
+ g(["checkout", "develop"], other);
430
+ write(other, "c.txt", "three");
431
+ g(["push"], other);
432
+ return work;
433
+ }
434
+
435
+ /**
436
+ * Strip everything that could point the child's `gh` at a real forge.
437
+ *
438
+ * `runGateCli` spawns with the full inherited environment, and since the
439
+ * two-base cases dropped `--base` the resolution itself is what they assert on.
440
+ * An ambient `GH_REPO` can make `gh pr list --head feat/x` answer about a real
441
+ * PR in someone else's repository and silently move the base under test — the
442
+ * same ambient-credential hazard the `forge` seam closed for the unit tests,
443
+ * which would otherwise have stayed live in the only coverage of the new
444
+ * symref resolution. `CI` would no-op every case green.
445
+ */
446
+ function isolateChildEnv(): void {
447
+ for (const key of ["CI", "GITHUB_ACTIONS", "GH_REPO", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GH_CONFIG_DIR"]) {
448
+ delete process.env[key];
449
+ }
450
+ }
451
+
124
452
  export function selfTest(repoRoot: string): number {
125
453
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
126
454
  const run = (advanceRemote: boolean, onBase: boolean): number => {
@@ -129,9 +457,14 @@ export function selfTest(repoRoot: string): number {
129
457
  if (onBase) {
130
458
  execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
131
459
  }
132
- // The child must not inherit a CI flag, or every case would no-op green.
133
- delete process.env["CI"];
134
- delete process.env["GITHUB_ACTIONS"];
460
+ isolateChildEnv();
461
+ return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
462
+ };
463
+ const runTwoBase = (serverDefault: string): number => {
464
+ const dir = fs.mkdtempSync(path.join(root, "twobase-"));
465
+ const work = buildTwoBaseFixture(dir, serverDefault);
466
+ isolateChildEnv();
467
+ // No `--base`: the gate resolves it, which is the behaviour under test.
135
468
  return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
136
469
  };
137
470
  const cases: SelfTestCase[] = [
@@ -140,6 +473,16 @@ export function selfTest(repoRoot: string): number {
140
473
  expect: "reject",
141
474
  run: () => run(true, false),
142
475
  },
476
+ {
477
+ name: "the RESOLVED base decides: server default develop, branch behind it, REFUSED",
478
+ expect: "reject",
479
+ run: () => runTwoBase("develop"),
480
+ },
481
+ {
482
+ name: "same tree, server default main — passes, so the resolution is what moved the verdict",
483
+ expect: "accept",
484
+ run: () => runTwoBase(DEFAULT_BASE),
485
+ },
143
486
  {
144
487
  name: "a branch containing the remote base passes",
145
488
  expect: "accept",
@@ -181,13 +524,23 @@ function scanReport(scanned: number, allowEmpty?: string): void {
181
524
  });
182
525
  }
183
526
 
184
- export function main(argv: string[] = process.argv.slice(2)): number {
527
+ /**
528
+ * @param forge Seam for the forge lookup. Production passes nothing and gets the
529
+ * real `gh`. Tests MUST pass a stub: without this parameter the only way to
530
+ * reach the forge-unavailable branch was to not have `gh` installed, so the test
531
+ * for it could only assert conditionally — i.e. it passed whether or not the
532
+ * warning was emitted. It also stopped every fixture test from spawning `gh`
533
+ * against ambient credentials, where a `GH_REPO` override could return a real
534
+ * PR base for a same-named branch and silently change the base under test.
535
+ */
536
+ export function main(
537
+ argv: string[] = process.argv.slice(2),
538
+ forge?: (b: string) => ForgeAnswer,
539
+ ): number {
185
540
  if (argv.includes("--self-test")) {
186
541
  return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
187
542
  }
188
543
  const quiet = argv.includes("--quiet");
189
- const baseArg = argv.indexOf("--base");
190
- const base = baseArg >= 0 ? (argv[baseArg + 1] ?? DEFAULT_BASE) : DEFAULT_BASE;
191
544
 
192
545
  // CI merges or refuses on the server; a second opinion here would be noise.
193
546
  if (process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true") {
@@ -200,11 +553,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
200
553
  scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
201
554
  return 0;
202
555
  }
556
+
557
+ // Resolved AFTER the branch is known: the PR's base is a fact about THIS branch.
558
+ // `resolveBase`'s third parameter has a default, so passing `undefined`
559
+ // positionally already selects the real `gh` — no branch needed here.
560
+ const resolved = resolveBase(argv, branch, forge);
561
+ const base = resolved.base;
562
+
203
563
  if (branch === base) {
204
564
  scanReport(0, `standing on ${base} itself — a branch cannot be behind itself`);
205
565
  return 0;
206
566
  }
207
567
 
568
+ // Loud even under --quiet: this run verified the default base and did NOT
569
+ // rule out an open PR against a different one. A partial answer is reported
570
+ // as partial, never folded into the plain green below.
571
+ if (resolved.unverified !== undefined) {
572
+ process.stdout.write(
573
+ `⚠️ check_branch_freshness: could not ask the forge for this branch's PR base ` +
574
+ `(${resolved.unverified}) — checking origin/${base}, the ${describeBase(resolved)}. ` +
575
+ "If an open PR targets a different base, THAT base is NOT verified here.\n",
576
+ );
577
+ }
578
+
208
579
  const sha = remoteHead(base);
209
580
  if (sha === null) {
210
581
  // Loud, not silent. An offline run has not verified the invariant, and a
@@ -221,19 +592,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
221
592
  scanReport(1);
222
593
  if (containsCommit(sha) === true) {
223
594
  if (!quiet) {
224
- process.stdout.write(`✅ branch is current with origin/${base} (${sha.slice(0, 9)})` + "\n");
595
+ process.stdout.write(
596
+ `✅ branch is current with origin/${base} (${sha.slice(0, 9)}) — ${describeBase(resolved)}\n`,
597
+ );
225
598
  }
226
599
  return 0;
227
600
  }
228
601
 
229
- console.error(`❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}.`);
602
+ console.error(
603
+ `❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}. ` +
604
+ `(${describeBase(resolved)})`,
605
+ );
230
606
  console.error("");
231
607
  console.error(" Pushing now opens a PR that may conflict, and worse: another branch");
232
608
  console.error(" may already have shipped what this one is implementing.");
233
609
  console.error("");
234
610
  console.error(` git fetch origin && git merge origin/${base}`);
611
+ console.error(" ./agent-config roadmap:progress # regenerate AFTER every merge, not only on conflict");
235
612
  console.error(" then re-run the gates before pushing.");
236
613
  console.error("");
614
+ console.error(" The dashboard line is not optional housekeeping: the roadmap dashboard is a");
615
+ console.error(" GENERATED file, so a merge that lands cleanly still leaves it describing");
616
+ console.error(" neither side's roadmap set. A clean auto-merge of a generated file is still wrong.");
617
+ console.error("");
237
618
  console.error(
238
619
  " This gate asks the REMOTE, not your tracking ref — a fetch from earlier in",
239
620
  );
@@ -95,7 +95,10 @@
95
95
  "agents/roadmaps/archive/road-to-subagent-value-realization.md",
96
96
  "agents/roadmap-assets/road-to-image-brand-typography.assets.md",
97
97
  "CREDITS.md",
98
- "agents/roadmaps/archive/road-to-ecosystem-harvest-index.md"
98
+ "agents/roadmaps/archive/road-to-ecosystem-harvest-index.md",
99
+ "provenance/borrows.jsonl",
100
+ "docs/THIRD-PARTY-NOTICES.md",
101
+ "agents/evidence/reviews/*.review-input/diff.patch"
99
102
  ],
100
103
  "skip_reason": {
101
104
  "vendored_cluster": "design-intelligence/corpus-grounding/design-tokens/react-shadcn-ui/tailwind-engineer + cost/*.mjs ship vendored Apache/MIT code; their upstream attribution is license-required and must NOT be stripped (ADR-061). CREDITS.md is the repo-level attribution file FOR that same cluster — it names the upstreams because the licenses require it, which is the one case source-confidentiality explicitly exempts. It was missing here, so the gate flagged the very attribution the rule mandates.",
@@ -103,7 +106,9 @@
103
106
  "generated_bundles": "content.json* bundle the vendored-cluster skill bodies above.",
104
107
  "self": "the linter + its denylist + the source-confidentiality rule necessarily contain the deny tokens as data.",
105
108
  "validate_safe_paths": "telegraph-derived (MIT) ported code; upstream attribution is license-required.",
106
- "archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record."
109
+ "archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record.",
110
+ "provenance_ledger": "provenance/borrows.jsonl and the docs/THIRD-PARTY-NOTICES.md it generates are the license-attribution surfaces for CODE borrows, and code-provenance REQUIRES the upstream name in them: the ledger's `source_url` is a mandatory field (provenance/README.md), and lint_provenance fails when the notices file drifts from it. That is the same license-required-attribution case source-confidentiality already exempts for the vendored cluster and CREDITS.md — the rule forbids derivation-attribution, not the notice a license obliges. The collision only surfaced on 2026-08-13 because the ledger had been empty until its first record; before that there was no name to flag. Scope is deliberately these two paths and not `provenance/*`: the harvest ledger next door governs KNOWLEDGE claims, where anonymisation is the rule and no license compels a name.",
111
+ "r2_review_diff_capture": "The R2 completion-review dispatcher writes a verbatim `diff.patch` of the branch under review into a tracked input package. It is a MECHANICAL CAPTURE, not authored text: any source name inside it was already written somewhere else in the same branch, where this gate does see it. A branch that touches the vendored cluster's ATTRIBUTION.md — carved out here precisely because its license requires the name — therefore reproduces that name in the capture and fails the gate for content the gate already allowed at its source. Scope is the capture ONLY: `findings.md` and `prompt.md` are authored prose and stay under the gate, so a reviewer who names a harvest source in their findings is still caught."
107
112
  },
108
113
  "_excluded_tokens": {
109
114
  "agent-os": "collides with our own coined 'Agent-OS primitive' term (universal-skills) — too FP-prone to auto-deny; scrub external 'agent-os' mentions by review.",
@@ -247,19 +247,19 @@ export function main(): number {
247
247
  out.push('');
248
248
 
249
249
  out.push('## Sample commands (first 10)\n');
250
- out.push('| File | name | tier | cluster |');
250
+ out.push('| File | name | visibility | cluster |');
251
251
  out.push('|---|---|---|---|');
252
252
  for (const c of commands.slice(0, 10)) {
253
253
  const fm = _frontmatter(c);
254
254
  const rel = _relativeTo(c, SRC);
255
- out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.tier ?? ''} | ${fm.cluster ?? ''} |`);
255
+ out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.visibility ?? ''} | ${fm.cluster ?? ''} |`);
256
256
  }
257
257
  out.push('');
258
258
 
259
259
  out.push('## Notes\n');
260
260
  out.push('- Every skill carries `domain:` today — Phase 4 mapping uses it as primary signal.');
261
261
  out.push('- Rules use `type` (always | auto) + `tier` (safety-floor | kernel | …); no `domain:` today.');
262
- out.push('- Commands use `tier` + `cluster`; no `domain:` today.');
262
+ out.push('- Commands use `visibility` + `cluster`; no `domain:` today.');
263
263
  out.push('- Templates have no consistent frontmatter; Phase 4 quarantines them in `unassigned-artefacts.yml`.');
264
264
  out.push('');
265
265
 
@@ -590,6 +590,26 @@ concerns:
590
590
  # blocker: in-process Agent-tool subagents inherit the host process env and
591
591
  # cannot be marked per-spawn; the axis binds where the suite launches a CLI
592
592
  # session itself.
593
+ #
594
+ # Superseded in part, 2026-08-14 (road-to-subagent-lifecycle-integrity Phase 4
595
+ # Step 2). The env half above still stands — nothing marks an in-process spawn
596
+ # per-process. What has changed is that env is no longer the only candidate
597
+ # channel: `subagent_start` / `subagent_stop` are bound on this host
598
+ # (2.1.229, Phase 0 Step 1) and their payload carries `agent_id`, which
599
+ # `subagent-ledger` correlates on today. So a payload-bound role axis —
600
+ # `agent_id` present ⇒ role `worker` — is a live proposal (Phase 4 Step 1),
601
+ # not a closed question.
602
+ # It is NOT implemented, and the reason is a missing measurement rather than a
603
+ # missing decision: whether `agent_id` reaches a `PreToolUse` / `PostToolUse`
604
+ # payload inside a subagent is unverified on this host. That is Phase 0 Step 4,
605
+ # blocked on `raw-capture-needs-host-env` (the capture itself is already
606
+ # shipped — `dispatch_hook._maybe_capture_payload`). A neighbouring assertion
607
+ # from the same spike already came back negative: `agent_type` does NOT arrive
608
+ # on `SubagentStop` (18 of 25 stop records null), so the documented-field
609
+ # presence is not assumed for `agent_id` either.
610
+ # Until that spike lands the env path is the only one wired, and the
611
+ # pre_tool_use exemption above is unaffected either way — a payload-resolved
612
+ # `worker` still cannot drop a pre_tool_use concern.
593
613
  roles:
594
614
  worker:
595
615
  # Orchestrator-only concerns (Phase 2.3): a worker never talks to the
@@ -73,8 +73,9 @@
73
73
  * a `subagent_reaped` line is written for it. That line is not bookkeeping —
74
74
  * it is the closest thing this instrument has to a direct observation of a
75
75
  * never-returning dispatch, which is why the leak is reported rather than
76
- * quietly swept. `session_id` rides on the record so a future sweep can scope
77
- * by session; nothing reads it yet and it is not claimed to.
76
+ * quietly swept. `session_id` rides on the open record AND on both dispatch
77
+ * lines so a rate can be scoped by session; nothing reads it yet and it is not
78
+ * claimed to.
78
79
  *
79
80
  * ── Depth and concurrency (Step 3) ────────────────────────────────────────
80
81
  *
@@ -598,6 +599,13 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
598
599
  event: 'subagent_start',
599
600
  ts: nowIso,
600
601
  ref,
602
+ // Phase 1 Step 4 correction (b): without this the dispatch denominator
603
+ // is not per-session. Three sessions share one ledger file, so a rate
604
+ // computed over the file aggregates strangers — the measured window
605
+ // held 7 starts against 25 stops, one of them an agent type this
606
+ // session never dispatched. The open record already carried it; the
607
+ // appended line did not, and the line is what a rate is computed from.
608
+ session_id: sessionId,
601
609
  agent_type: rec.agent_type,
602
610
  parent_ref: parentRef,
603
611
  depth,
@@ -606,7 +614,7 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
606
614
  });
607
615
  }
608
616
 
609
- function handleStop(root: string, payload: JsonObject, nowIso: string): void {
617
+ function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId: string | null): void {
610
618
  const agentId = str(payload, 'agent_id', 'agentId');
611
619
  if (agentId === null) {
612
620
  appendUnidentified(root, 'subagent_stop', nowIso, payload);
@@ -631,6 +639,11 @@ function handleStop(root: string, payload: JsonObject, nowIso: string): void {
631
639
  event: 'subagent_stop',
632
640
  ts: nowIso,
633
641
  ref,
642
+ // The session this stop was OBSERVED in, read from the same envelope
643
+ // position as the start's — never back-filled from `rec`, which would
644
+ // relabel a cross-session stop as belonging to the dispatching session
645
+ // and hide exactly the mismatch this field exists to expose.
646
+ session_id: sessionId,
634
647
  agent_type: rec?.agent_type ?? str(payload, 'agent_type', 'agentType', 'subagent_type', 'subagentType'),
635
648
  depth: rec?.depth ?? null,
636
649
  depth_basis: rec?.depth_basis ?? null,
@@ -661,7 +674,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string): numb
661
674
  reapStaleOpenRecords(consumerRoot, nowIso);
662
675
 
663
676
  if (event === 'subagent_start') handleStart(consumerRoot, payload, nowIso, sessionId);
664
- else handleStop(consumerRoot, payload, nowIso);
677
+ else handleStop(consumerRoot, payload, nowIso, sessionId);
665
678
  } catch {
666
679
  // Malformed payload, unreadable disk, anything — never disturb the run.
667
680
  return EXIT_ALLOW;