@joshuafolkken/kit 1.1471.0 → 1.1514.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 (267) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/epic-commands/SKILL.md +2 -14
  3. package/.claude/skills/epic-commands/epic-bundle.md +9 -5
  4. package/.claude/skills/epic-commands/execution-waves.md +1 -2
  5. package/.claude/skills/verify-ui/SKILL.md +1 -2
  6. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  7. package/.claude/skills/workflow-commands/background-commands.md +3 -7
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
  14. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  15. package/.claude/skills/workflow-commands/chain-rule.md +7 -6
  16. package/.claude/skills/workflow-commands/delegation.md +12 -8
  17. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  18. package/.claude/skills/workflow-commands/followup.md +3 -6
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  20. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  21. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  22. package/.claude/skills/workflow-commands/into-target.md +1 -2
  23. package/.claude/skills/workflow-commands/issue-comments.md +3 -3
  24. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  25. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  27. package/.claude/skills/workflow-commands/observation-filing.md +0 -14
  28. package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
  30. package/.claude/skills/workflow-commands/prerequisite.md +1 -2
  31. package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
  32. package/.claude/skills/workflow-commands/prrun.md +3 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +4 -4
  34. package/.claude/skills/workflow-commands/split-assessment.md +1 -3
  35. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
  37. package/.codex/hooks.json +6 -6
  38. package/.github/actions/setup-node/action.yml +1 -1
  39. package/.github/workflows/ci.yml +1 -1
  40. package/.github/workflows/publish.yml +2 -2
  41. package/CLAUDE.md +1 -1
  42. package/cspell/index.yaml +7 -0
  43. package/dist/CLAUDE.md +1 -1
  44. package/dist/commands/document-section-cli.js +67 -34
  45. package/dist/hooks/batch-guard.js +6 -6
  46. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  47. package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
  48. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  49. package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
  50. package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
  51. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  52. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  53. package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
  54. package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
  55. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  56. package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
  57. package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
  58. package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
  59. package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
  60. package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
  61. package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
  62. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  63. package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
  64. package/dist/hooks/codex-hook-adapter.js +17 -14
  65. package/dist/hooks/duplicate-read-guard.js +7 -7
  66. package/dist/hooks/format-edited.js +5 -4
  67. package/dist/hooks/inputs.json +317 -264
  68. package/dist/hooks/investigation-guard.js +8 -11
  69. package/dist/hooks/pretool-guard.js +16 -13
  70. package/dist/hooks/session-lang.js +370 -25
  71. package/dist/hooks/stop-guard.js +2576 -176
  72. package/dist/josh.js +2124 -277
  73. package/lefthook/base.yml +5 -3
  74. package/package.json +12 -10
  75. package/prompts/collaboration-workflow/file-edits.md +1 -3
  76. package/prompts/collaboration-workflow/gh-rest.md +1 -2
  77. package/prompts/collaboration-workflow/issue-citation.md +2 -4
  78. package/prompts/collaboration-workflow/issue-template.md +10 -2
  79. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  80. package/prompts/collaboration-workflow/operating-rules.md +2 -6
  81. package/prompts/collaboration-workflow/output-bounds.md +3 -3
  82. package/prompts/collaboration-workflow/overview.md +1 -1
  83. package/prompts/collaboration-workflow/principles.md +14 -8
  84. package/prompts/collaboration-workflow/report-format.md +9 -0
  85. package/prompts/collaboration-workflow/residency.md +12 -8
  86. package/prompts/collaboration-workflow/rule-delivery.md +3 -11
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +5 -5
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +6 -6
  91. package/prompts/collaboration-workflow.md +1 -0
  92. package/prompts/review.md +4 -5
  93. package/prompts/testing-guide.md +2 -2
  94. package/scripts/agent/agent-argv.ts +20 -4
  95. package/scripts/agent/agent-role-profile.ts +101 -45
  96. package/scripts/agent/agent-safe-text.ts +60 -0
  97. package/scripts/agent/claude-agent-argv.ts +14 -0
  98. package/scripts/agent/codex-agent-argv.ts +2 -0
  99. package/scripts/agent/lane-instructions.ts +144 -0
  100. package/scripts/backlog/backlog-budget.ts +4 -4
  101. package/scripts/backlog/backlog-drive-named.ts +1 -1
  102. package/scripts/backlog/backlog-drive-restore.ts +9 -1
  103. package/scripts/backlog/backlog-occupied.ts +56 -0
  104. package/scripts/backlog/backlog-ready.ts +47 -10
  105. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  106. package/scripts/delegation/investigation-guard.ts +15 -65
  107. package/scripts/delegation/investigation-reads.ts +0 -1
  108. package/scripts/document/document-byte-budget.ts +6 -5
  109. package/scripts/document/document-section-cli.ts +17 -5
  110. package/scripts/document/entry-read-budget.ts +2 -2
  111. package/scripts/document/read-set-cli.ts +4 -1
  112. package/scripts/epic/epic-audit-orphans.ts +5 -8
  113. package/scripts/epic/epic-audit.ts +52 -1
  114. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  115. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  116. package/scripts/epic/epic-bundle.ts +17 -9
  117. package/scripts/followup/git-followup-workflow.ts +19 -1
  118. package/scripts/gate/check-inputs.ts +129 -0
  119. package/scripts/gate/check-reuse.ts +162 -0
  120. package/scripts/gate/core-budget.ts +93 -44
  121. package/scripts/gate/core-wait.ts +130 -0
  122. package/scripts/gate/free-cores.ts +49 -0
  123. package/scripts/gate/gate-ledger.ts +17 -3
  124. package/scripts/gate/gate-plan.ts +23 -1
  125. package/scripts/gate/gate-report.ts +15 -2
  126. package/scripts/gate/gate-run-steps.ts +150 -0
  127. package/scripts/gate/held-mark.ts +55 -0
  128. package/scripts/gate/machine-capacity.ts +70 -11
  129. package/scripts/gate/pre-commit-type-check.ts +14 -1
  130. package/scripts/gate/verification-gate.ts +58 -125
  131. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  132. package/scripts/gh/git-gh-issue-write.ts +24 -2
  133. package/scripts/gh/git-pr.ts +13 -1
  134. package/scripts/git/main-merge-regenerate.ts +78 -0
  135. package/scripts/git/main-merge.ts +14 -5
  136. package/scripts/hooks/batch-guard.ts +5 -8
  137. package/scripts/hooks/pre-commit-lint.ts +91 -0
  138. package/scripts/hooks/pre-push-unit.ts +1 -1
  139. package/scripts/hooks/pretool-guard.ts +27 -52
  140. package/scripts/hooks/reserved-run.ts +1 -0
  141. package/scripts/hooks/run-hook.sh +10 -12
  142. package/scripts/hooks/stop-actions.ts +204 -0
  143. package/scripts/hooks/stop-guard.ts +62 -21
  144. package/scripts/init/hook-launch.ts +12 -7
  145. package/scripts/issue/expected-effect-lint.ts +59 -0
  146. package/scripts/issue/issue-cite-cli.ts +35 -15
  147. package/scripts/issue/issue-classification.ts +1 -1
  148. package/scripts/issue/issue-lint-cli.ts +5 -3
  149. package/scripts/issue/issue-scout-cli.ts +19 -14
  150. package/scripts/issue/markdown-section.ts +19 -5
  151. package/scripts/josh/command-usage.ts +110 -0
  152. package/scripts/josh/josh-command-map.ts +2 -0
  153. package/scripts/josh/josh-command-types.ts +55 -9
  154. package/scripts/josh/josh-commands-ai.ts +13 -0
  155. package/scripts/josh/josh-commands-development.ts +1 -1
  156. package/scripts/josh/josh-commands-hooks.ts +6 -0
  157. package/scripts/josh/josh-commands-lane.ts +10 -1
  158. package/scripts/josh/josh-commands-workflow.ts +5 -0
  159. package/scripts/josh/josh-logic.ts +6 -15
  160. package/scripts/josh/josh-usage-automation.ts +121 -0
  161. package/scripts/josh/josh-usage-backlog.ts +180 -0
  162. package/scripts/josh/josh-usage-developer.ts +230 -0
  163. package/scripts/josh/josh-usage-hooks.ts +38 -0
  164. package/scripts/josh/josh-usage-issue.ts +205 -0
  165. package/scripts/josh/josh-usage-review.ts +101 -0
  166. package/scripts/josh/josh-usage-run.ts +252 -0
  167. package/scripts/josh/josh-usage-session.ts +238 -0
  168. package/scripts/josh/josh-usage-workflow.ts +212 -0
  169. package/scripts/josh/josh.ts +14 -5
  170. package/scripts/josh/package-directory.ts +20 -0
  171. package/scripts/lane/lane-capacity.ts +8 -2
  172. package/scripts/lane/lane-check-stats.ts +56 -0
  173. package/scripts/lane/lane-child-invocation.ts +35 -4
  174. package/scripts/lane/lane-dispatch.ts +3 -3
  175. package/scripts/lane/lane-guard-policy.ts +2 -2
  176. package/scripts/lane/lane-handoff.ts +8 -1
  177. package/scripts/lane/lane-install.ts +12 -4
  178. package/scripts/lane/lane-ledger.ts +43 -1
  179. package/scripts/lane/lane-limit-cli.ts +8 -9
  180. package/scripts/lane/lane-measure-cli.ts +2 -0
  181. package/scripts/lane/lane-preserve.ts +29 -1
  182. package/scripts/lane/lane-registry.ts +9 -7
  183. package/scripts/lane/lane-restart.ts +5 -26
  184. package/scripts/lane/lane-session-action.ts +146 -0
  185. package/scripts/lane/lane-session-broker.ts +57 -138
  186. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  187. package/scripts/lane/lane-withdraw.ts +182 -0
  188. package/scripts/lint/format.ts +4 -4
  189. package/scripts/lint/lint-parallel.ts +10 -3
  190. package/scripts/lint/lint-related-scope.ts +4 -1
  191. package/scripts/notify/telegram-test.ts +10 -5
  192. package/scripts/report/report-format-reference.ts +2 -0
  193. package/scripts/review/acceptance-map-check.ts +61 -0
  194. package/scripts/review/acceptance-map.ts +205 -0
  195. package/scripts/review/review-brief-cli.ts +1 -1
  196. package/scripts/rules/agent-model.ts +59 -0
  197. package/scripts/rules/delivered-rules.ts +11 -2
  198. package/scripts/rules/filing-cap.ts +1 -0
  199. package/scripts/rules/firing-point.ts +6 -5
  200. package/scripts/rules/guard-remedy.ts +117 -0
  201. package/scripts/rules/issue-comments.ts +67 -6
  202. package/scripts/rules/last-prompt.ts +50 -5
  203. package/scripts/rules/oracle-list-cli.ts +7 -0
  204. package/scripts/rules/piped-verification.ts +3 -1
  205. package/scripts/rules/rule-list.ts +4 -1
  206. package/scripts/rules/rule-registry-rows.ts +10 -0
  207. package/scripts/rules/rule-registry.ts +7 -4
  208. package/scripts/rules/rule-rewrite.ts +114 -0
  209. package/scripts/rules/rule-value.ts +7 -1
  210. package/scripts/rules/shell-segments.ts +15 -0
  211. package/scripts/rules/stop-decision.ts +77 -0
  212. package/scripts/rules/stop-rules.ts +147 -47
  213. package/scripts/rules/tail-commands.ts +15 -1
  214. package/scripts/run/board/run-board-cli.ts +2 -0
  215. package/scripts/run/board/run-board-closed.ts +8 -2
  216. package/scripts/run/board/run-board-github.ts +9 -1
  217. package/scripts/run/board/run-board-header.ts +4 -1
  218. package/scripts/run/board/run-board-labels.ts +2 -0
  219. package/scripts/run/board/run-board-layout.ts +1 -1
  220. package/scripts/run/board/run-board-read.ts +16 -0
  221. package/scripts/run/board/run-board-reason.ts +10 -1
  222. package/scripts/run/board/run-board-render-legend.ts +1 -0
  223. package/scripts/run/board/run-board-render-slot.ts +1 -1
  224. package/scripts/run/board/run-board-render-waits.ts +41 -0
  225. package/scripts/run/board/run-board-render.ts +6 -1
  226. package/scripts/run/board/run-board-row-yield.ts +1 -0
  227. package/scripts/run/board/run-board-state.ts +5 -0
  228. package/scripts/run/board/run-board-status.ts +72 -21
  229. package/scripts/run/board/run-board-tick.ts +5 -1
  230. package/scripts/run/cut/run-cut-cli.ts +3 -2
  231. package/scripts/run/cut/run-cut-report.ts +16 -0
  232. package/scripts/run/detached-launch.ts +23 -4
  233. package/scripts/run/entry/run-entry-cli.ts +8 -1
  234. package/scripts/run/event/run-event-render.ts +1 -0
  235. package/scripts/run/event/run-event-scope.ts +19 -1
  236. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  237. package/scripts/run/event/run-event-stream.ts +4 -0
  238. package/scripts/run/hold/run-hold-session.ts +46 -0
  239. package/scripts/run/hold/run-hold.ts +7 -1
  240. package/scripts/run/launch-priority.ts +32 -0
  241. package/scripts/run/merge/run-merge-cli.ts +16 -65
  242. package/scripts/run/merge/run-merge-signals.ts +99 -0
  243. package/scripts/run/merge/run-merge-steps.ts +11 -1
  244. package/scripts/run/merge/run-merge.ts +21 -9
  245. package/scripts/run/run-liveness.ts +1 -1
  246. package/scripts/run/run-poll-hook.ts +157 -0
  247. package/scripts/run/run-step-cli.ts +22 -15
  248. package/scripts/run/run-step-section.ts +85 -0
  249. package/scripts/run/ship/run-ship-cli.ts +5 -12
  250. package/scripts/run/ship/run-ship-detach.ts +9 -0
  251. package/scripts/run/ship/run-ship-next.ts +10 -2
  252. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  253. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  254. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  255. package/scripts/run/ship/run-ship-return.ts +31 -7
  256. package/scripts/run/ship/run-ship-review-steps.ts +12 -10
  257. package/scripts/run/ship/run-ship-steps.ts +1 -1
  258. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  259. package/scripts/run/ship/run-ship-sync.ts +14 -6
  260. package/scripts/test/test-e2e-guard.ts +30 -5
  261. package/scripts/test/test-unit-guard.ts +16 -1
  262. package/scripts/test/unit-worker-share.ts +26 -4
  263. package/scripts/time-runtime/time-shell.ts +29 -4
  264. package/scripts/time-runtime/time-spans.ts +1 -0
  265. package/scripts/time-runtime/time-tool-call.ts +7 -0
  266. package/templates/gitignore +6 -2
  267. package/dist/hooks/chunk-K4LMBQC2.js +0 -68
@@ -112,6 +112,14 @@ async function resolve_context(values: CliValues): Promise<ResolvedContext> {
112
112
  return { repo_name, issue_title }
113
113
  }
114
114
 
115
+ // The send behind the command line, for a caller that already holds the values — the `Stop` hook's
116
+ // own confirmation (`stop-actions.ts`). It throws as `telegram_notify.send` does.
117
+ async function send_values(values: CliValues): Promise<void> {
118
+ const context = await resolve_context(values)
119
+
120
+ await telegram_notify.send(telegram_test_logic.build_input({ values, context }))
121
+ }
122
+
115
123
  async function main(): Promise<void> {
116
124
  josh_environment_file.load_environment_file()
117
125
  const values = parse_cli_arguments()
@@ -122,10 +130,7 @@ async function main(): Promise<void> {
122
130
  return
123
131
  }
124
132
 
125
- const context = await resolve_context(values)
126
- const input = telegram_test_logic.build_input({ values, context })
127
-
128
- await telegram_notify.send(input)
133
+ await send_values(values)
129
134
  }
130
135
 
131
136
  // **A notification nobody received must not read as success**. This command
@@ -148,6 +153,6 @@ async function run_cli(): Promise<void> {
148
153
 
149
154
  if (process.argv[1] === fileURLToPath(import.meta.url)) await run_cli()
150
155
 
151
- const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli }
156
+ const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli, send_values }
152
157
 
153
158
  export { telegram_test }
@@ -8,6 +8,7 @@ const REPORT_FORMAT_PATH = 'prompts/collaboration-workflow/report-format.md'
8
8
  const SUMMARY_RULES_HEADING = '作業前サマリはいつ・どう出すか'
9
9
  const SUMMARY_TEMPLATE_HEADING = '作業前サマリ(セッション向け・`JOSH_SESSION_LANG` の言語)'
10
10
  const COMPLETION_REPORT_HEADING = '完了報告(セッション向け)'
11
+ const ACCEPTANCE_MAP_HEADING = 'PR 本文の受け入れ条件の対応表'
11
12
 
12
13
  // The heading goes in single quotes in the runnable form: the template heading carries backticks,
13
14
  // which a double-quoted shell argument would run as a command substitution.
@@ -16,6 +17,7 @@ function pointer(heading: string): string {
16
17
  }
17
18
 
18
19
  const report_format_reference = {
20
+ ACCEPTANCE_MAP_HEADING,
19
21
  COMPLETION_REPORT_HEADING,
20
22
  pointer,
21
23
  REPORT_FORMAT_PATH,
@@ -0,0 +1,61 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { session_cite } from '#scripts/issue/session-cite'
3
+ import { acceptance_map, type Deferral } from './acceptance-map'
4
+
5
+ // The reads behind `acceptance_map`: the Issue as it reads at the merge, the pull request body, and
6
+ // whether each issue a deferral row names exists. Kept apart so the comparison stays pure.
7
+
8
+ const ISSUE_SEPARATOR = '#'
9
+
10
+ // `owner/repo#12` reads that repository's issue; a bare `#12` reads this one's.
11
+ async function is_found(deferral: Deferral): Promise<boolean> {
12
+ const [repo, number = ''] = deferral.issue.split(ISSUE_SEPARATOR)
13
+ const read = await git_gh_command.issue_get_body_classified(
14
+ number,
15
+ repo === '' ? undefined : repo,
16
+ )
17
+
18
+ return read.kind === 'read'
19
+ }
20
+
21
+ async function absent_deferrals(
22
+ deferrals: ReadonlyArray<Deferral>,
23
+ ): Promise<ReadonlyArray<string>> {
24
+ const found = await Promise.all(deferrals.map(async (deferral) => await is_found(deferral)))
25
+
26
+ return deferrals
27
+ .filter((_deferral, index) => found[index] !== true)
28
+ .map((deferral) => `- \`${deferral.id}\` names ${deferral.issue}`)
29
+ }
30
+
31
+ function unreadable_message(issue: string): string {
32
+ return `Issue ${session_cite.issue(issue)} could not be re-read to check its acceptance criteria, so the merge is refused; re-run once it can be.`
33
+ }
34
+
35
+ // **An Issue that cannot be re-read is a refusal, not a pass** — the criteria it would have named are
36
+ // exactly what this gate exists to see. An Issue with no acceptance section merges as before.
37
+ // `josh ship`'s preflight asks the body it is about to deliver, so a missing table stops it before the
38
+ // review rather than at the merge.
39
+ async function body_refusal(
40
+ issue: string,
41
+ pr_body: string | undefined,
42
+ ): Promise<string | undefined> {
43
+ const issue_read = await git_gh_command.issue_get_body_classified(issue)
44
+
45
+ if (issue_read.kind !== 'read') return unreadable_message(issue)
46
+ const criteria = acceptance_map.criteria_of(issue_read.text)
47
+ const comparison = acceptance_map.compare(criteria, acceptance_map.rows_of(pr_body))
48
+ const absent = await absent_deferrals(comparison.deferrals)
49
+
50
+ if (comparison.missing.length === 0 && absent.length === 0) return undefined
51
+
52
+ return acceptance_map.refusal_message(issue, comparison, absent)
53
+ }
54
+
55
+ async function refusal(issue: string, branch_name: string): Promise<string | undefined> {
56
+ return await body_refusal(issue, await git_gh_command.pr_get_body(branch_name))
57
+ }
58
+
59
+ const acceptance_map_check = { body_refusal, refusal }
60
+
61
+ export { acceptance_map_check }
@@ -0,0 +1,205 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { epic_audit_logic } from '#scripts/epic/epic-audit'
3
+ import { markdown_section } from '#scripts/issue/markdown-section'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { report_format_reference } from '#scripts/report/report-format-reference'
6
+
7
+ // A green gate and a clean review say the diff is sound, never that it delivers what the Issue asked
8
+ // for. **This is the seam where each acceptance criterion is checked one by one**: the pull request
9
+ // body carries one row per criterion — met, with its evidence, or deferred to a split-off issue — and
10
+ // `josh followup` re-reads the Issue just before the merge and refuses a criterion with no row.
11
+ //
12
+ // **A criterion is matched by an identifier built from its own text**, so a criterion added or
13
+ // reworded after the body was written has an identifier no row carries, and is refused as unmapped
14
+ // rather than passing on a stale row. The checkbox is stripped first: ticking it changes nothing.
15
+ //
16
+ // The criteria are read through `epic_audit_logic.acceptance_section`, the one place the acceptance
17
+ // headings are defined.
18
+
19
+ const MAP_HEADING = '## 受け入れ条件の対応'
20
+ const MET = '満たした'
21
+ const DEFERRED = '見送り'
22
+ const PENDING = '未記入'
23
+ const PENDING_SUFFIX = `— ${PENDING}`
24
+ const ID_PREFIX = 'ac-'
25
+ const ID_HEX_LENGTH = 8
26
+ const BACKTICK = '`'
27
+ // A top-level list item only: a nested item is detail under a criterion, not one of its own.
28
+ const CRITERION_PATTERN = /^(?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s+)?(?<text>\S.*)$/u
29
+ // A list item, heading, table row or HTML comment opens a block of its own, never a wrapped line.
30
+ const BLOCK_START_PATTERN = /^\s*(?:(?:[-*+]|\d+\.)\s|#|\||<!--)/u
31
+ // The criterion's text may sit between the identifier and the status for the reader, so the status is
32
+ // the last status word on the line — a criterion quoting the row format stays text.
33
+ const ROW_PATTERN = new RegExp(
34
+ String.raw`^[-*]\s+${BACKTICK}(?<id>${ID_PREFIX}[0-9a-f]{${String(ID_HEX_LENGTH)}})${BACKTICK}(?:\s.*)?\s(?<kind>${MET}|${DEFERRED}):\s*(?<rest>\S.*)$`,
35
+ 'u',
36
+ )
37
+ // A deferral names the split-off issue first, then why it was deferred.
38
+ const DEFERRAL_PATTERN = /^(?<issue>(?:[\w.-]+\/[\w.-]+)?#\d+)\s+\S/u
39
+ const ROW_FORMAT = `under ${MAP_HEADING}, one line per acceptance criterion: \`- \`<id>\` <criterion> — ${MET}: <test name, file:line or command output>\` or \`- \`<id>\` <criterion> — ${DEFERRED}: #<split-off issue> <reason>\``
40
+
41
+ interface Criterion {
42
+ id: string
43
+ text: string
44
+ }
45
+
46
+ interface Deferral {
47
+ id: string
48
+ issue: string
49
+ }
50
+
51
+ interface Row {
52
+ id: string
53
+ kind: string
54
+ rest: string
55
+ }
56
+
57
+ interface MapRows {
58
+ met: ReadonlySet<string>
59
+ deferrals: ReadonlyArray<Deferral>
60
+ ids: ReadonlySet<string>
61
+ }
62
+
63
+ interface MapComparison {
64
+ missing: ReadonlyArray<Criterion>
65
+ deferrals: ReadonlyArray<Deferral>
66
+ stale: ReadonlyArray<string>
67
+ }
68
+
69
+ function criterion_id(text: string): string {
70
+ const normalized = text.replaceAll(/\s+/gu, ' ').trim()
71
+ const digest = createHash('sha256').update(normalized).digest('hex')
72
+
73
+ return `${ID_PREFIX}${digest.slice(0, ID_HEX_LENGTH)}`
74
+ }
75
+
76
+ function joins(last: string | undefined, line: string): boolean {
77
+ const is_continuation = line.trim() !== '' && !BLOCK_START_PATTERN.test(line)
78
+
79
+ return is_continuation && last !== undefined && CRITERION_PATTERN.test(last)
80
+ }
81
+
82
+ // A criterion wrapped onto continuation lines is one criterion, hashed whole — rewording any of its
83
+ // lines changes its identifier. A blank line or a nested item ends it.
84
+ function unwrapped(lines: ReadonlyArray<string>): ReadonlyArray<string> {
85
+ const items: Array<string> = []
86
+
87
+ for (const line of lines) {
88
+ items.push(joins(items.at(-1), line) ? `${items.pop() ?? ''} ${line.trim()}` : line)
89
+ }
90
+
91
+ return items
92
+ }
93
+
94
+ function criteria_of(issue_body: string | undefined): ReadonlyArray<Criterion> {
95
+ // A list item inside a code fence is an example the Issue quotes, not a criterion. An indented line
96
+ // outside one is a wrapped criterion's continuation, so it is kept.
97
+ const lines = markdown_section.fence_free_lines(epic_audit_logic.acceptance_section(issue_body))
98
+ const texts = unwrapped(lines).map((line) => CRITERION_PATTERN.exec(line)?.groups?.['text'])
99
+
100
+ return texts
101
+ .filter((text): text is string => text !== undefined)
102
+ .map((text) => ({ id: criterion_id(text), text: text.trim() }))
103
+ }
104
+
105
+ // A deferral row with no issue or no reason is no row at all, so its criterion reads as unmapped.
106
+ function deferral_of(id: string, rest: string): Deferral | undefined {
107
+ const issue = DEFERRAL_PATTERN.exec(rest)?.groups?.['issue']
108
+
109
+ return issue === undefined ? undefined : { id, issue }
110
+ }
111
+
112
+ // Every group in `ROW_PATTERN` is mandatory, so a match always carries the name.
113
+ function group(groups: Readonly<Record<string, string>>, name: string): string {
114
+ return groups[name] ?? ''
115
+ }
116
+
117
+ // A pending row is no row even when the criterion it quotes carries a status word — otherwise the
118
+ // skeleton's own unfilled row would read as met.
119
+ function row_of(line: string): Row | undefined {
120
+ const trimmed = line.trim()
121
+
122
+ if (trimmed.endsWith(PENDING_SUFFIX)) return undefined
123
+ const groups = ROW_PATTERN.exec(trimmed)?.groups
124
+
125
+ if (groups === undefined) return undefined
126
+
127
+ return { id: group(groups, 'id'), kind: group(groups, 'kind'), rest: group(groups, 'rest') }
128
+ }
129
+
130
+ function rows_of(pr_body = ''): MapRows {
131
+ const rows = markdown_section
132
+ .section_lines(pr_body, MAP_HEADING)
133
+ .map((line) => row_of(line))
134
+ .filter((row): row is Row => row !== undefined)
135
+ const met = rows.filter((row) => row.kind === MET).map((row) => row.id)
136
+ const deferrals = rows
137
+ .filter((row) => row.kind === DEFERRED)
138
+ .map((row) => deferral_of(row.id, row.rest))
139
+ .filter((deferral): deferral is Deferral => deferral !== undefined)
140
+
141
+ return { met: new Set(met), deferrals, ids: new Set([...met, ...deferrals.map((row) => row.id)]) }
142
+ }
143
+
144
+ function compare(criteria: ReadonlyArray<Criterion>, rows: MapRows): MapComparison {
145
+ const current = new Set(criteria.map((criterion) => criterion.id))
146
+
147
+ return {
148
+ missing: criteria.filter((criterion) => !rows.ids.has(criterion.id)),
149
+ deferrals: rows.deferrals.filter((deferral) => current.has(deferral.id)),
150
+ stale: [...rows.ids].filter((id) => !current.has(id)),
151
+ }
152
+ }
153
+
154
+ // What `josh pr` writes when the caller's body carries no table: every criterion as a pending row,
155
+ // which the merge refuses until it is filled in.
156
+ function skeleton(issue_body: string | undefined): string | undefined {
157
+ const criteria = criteria_of(issue_body)
158
+
159
+ if (criteria.length === 0) return undefined
160
+ const rows = criteria.map(
161
+ (criterion) => `- \`${criterion.id}\` ${criterion.text} ${PENDING_SUFFIX}`,
162
+ )
163
+
164
+ return [MAP_HEADING, '', ...rows].join('\n')
165
+ }
166
+
167
+ function has_map(pr_body = ''): boolean {
168
+ return markdown_section.has_unfenced_line(pr_body, MAP_HEADING)
169
+ }
170
+
171
+ function listed(title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> {
172
+ return lines.length === 0 ? [] : [title, ...lines.map((line) => ` ${line}`)]
173
+ }
174
+
175
+ function refusal_message(
176
+ issue: string,
177
+ comparison: MapComparison,
178
+ absent: ReadonlyArray<string>,
179
+ ): string {
180
+ return [
181
+ `The pull request body does not map every acceptance criterion of ${session_cite.issue(issue)} as it reads now, so the merge is refused.`,
182
+ ...listed(
183
+ 'Criteria with no row (added or reworded since the body was written, or never mapped):',
184
+ comparison.missing.map((criterion) => `- \`${criterion.id}\` ${criterion.text}`),
185
+ ),
186
+ ...listed('Deferral rows naming an issue that could not be found:', absent),
187
+ ...listed('Rows naming no current criterion:', comparison.stale),
188
+ `Write ${ROW_FORMAT} — format: ${report_format_reference.pointer(report_format_reference.ACCEPTANCE_MAP_HEADING)}`,
189
+ 'Write it into the `--body-file` before `josh ship`, or update an open PR with `gh api -X PATCH repos/{owner}/{repo}/pulls/<N> -F body=@<path>`, then re-run.',
190
+ ].join('\n')
191
+ }
192
+
193
+ const acceptance_map = {
194
+ MAP_HEADING,
195
+ compare,
196
+ criteria_of,
197
+ criterion_id,
198
+ has_map,
199
+ refusal_message,
200
+ rows_of,
201
+ skeleton,
202
+ }
203
+
204
+ export type { Criterion, Deferral, MapComparison }
205
+ export { acceptance_map }
@@ -330,7 +330,7 @@ async function run_review(round: number, reading: ChangeReading): Promise<number
330
330
  const refusal = scoped_green.refusal_for(tree, commit)
331
331
 
332
332
  if (refusal !== undefined) return report_error(refusal)
333
- const resolved = agent_role_profile.resolve(agent_role_profile.REVIEWER)
333
+ const resolved = agent_role_profile.resolve_review(round)
334
334
 
335
335
  if (resolved.kind === 'rejected') return report_error(resolved.note)
336
336
  agent_role_profile.warn_of_default()
@@ -0,0 +1,59 @@
1
+ import { json_value } from '#scripts/lib/json-value'
2
+ import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
3
+
4
+ // A `general-purpose` subagent dispatched without a `model` is refused until it names one
5
+ // (joshuafolkken/kit#3927).
6
+ //
7
+ // **The omission is the cost, not the dispatch.** A subagent with no `model` inherits the parent's,
8
+ // so a routine unit runs on the main line's model — measured at 94 such dispatches on Opus. A named
9
+ // agent type carries its own `model` in its definition, so only the catch-all type is in scope; an
10
+ // absent `subagent_type` is that type, the harness's default.
11
+ //
12
+ // **It fires on every occurrence**: the remedy is one field on the same call, so a refusal costs one
13
+ // round trip and never wedges the run.
14
+
15
+ // Both spellings of the subagent tool are in use: `Task` (published) and `Agent` (this harness).
16
+ const SUBAGENT_TOOLS: ReadonlySet<string> = new Set(['Agent', 'Task'])
17
+ const GENERAL_PURPOSE = 'general-purpose'
18
+
19
+ // A dispatch of the catch-all type — the one whose model is otherwise inherited.
20
+ function is_general_purpose(call: GuardedCall): boolean {
21
+ if (!SUBAGENT_TOOLS.has(call.name) || !json_value.is_record(call.input)) return false
22
+
23
+ const type = call.input['subagent_type']
24
+
25
+ return type === undefined || type === GENERAL_PURPOSE
26
+ }
27
+
28
+ function names_model(call: GuardedCall): boolean {
29
+ const model = json_value.is_record(call.input) ? call.input['model'] : undefined
30
+
31
+ return typeof model === 'string' && model.length > 0
32
+ }
33
+
34
+ function is_unnamed_model(call: GuardedCall): boolean {
35
+ return is_general_purpose(call) && !names_model(call)
36
+ }
37
+
38
+ function keeps_model(call: GuardedCall): boolean {
39
+ return is_general_purpose(call) && names_model(call)
40
+ }
41
+
42
+ const AGENT_MODEL_REASON =
43
+ '⛔ agent model: a `general-purpose` subagent with no `model` inherits the parent model. ' +
44
+ 'Reissue the call with `model` set — `sonnet` for routine work, `opus` for judgment such as a review. ' +
45
+ 'It fires on every occurrence (`.claude/skills/workflow-commands/delegation.md`).'
46
+
47
+ // `reaches` is every catch-all dispatch, so `rule-value.ts` scores the named-model ones against it.
48
+ const ROW = {
49
+ id: 'agent-model',
50
+ is_trigger: is_unnamed_model,
51
+ reason: AGENT_MODEL_REASON,
52
+ decide: (): boolean => true,
53
+ keeps: keeps_model,
54
+ reaches: is_general_purpose,
55
+ }
56
+
57
+ const agent_model = { AGENT_MODEL_REASON, ROW, is_unnamed_model }
58
+
59
+ export { agent_model }
@@ -2,6 +2,7 @@ import { investigation_reads } from '#scripts/delegation/investigation-reads'
2
2
  import { hook_decision, type GuardRun, type TranscriptGuardSpec } from '#scripts/josh/hook-decision'
3
3
  import { lane_guard_policy } from '#scripts/lane/lane-guard-policy'
4
4
  import { time_batch_guard, type GuardedCall } from '#scripts/time-runtime/time-batch-guard'
5
+ import { agent_model } from './agent-model'
5
6
  import { bash_triggers } from './bash-triggers'
6
7
  import { direct_filing } from './direct-filing'
7
8
  import { early_heartbeat } from './early-heartbeat'
@@ -46,6 +47,9 @@ type CallTest = (
46
47
  run: ReadonlyArray<GuardedCall>,
47
48
  ) => boolean
48
49
 
50
+ // The tail is read lazily: most calls no rewrite applies to never need it.
51
+ type CallRewrite = (call: GuardedCall, tail: () => string) => Record<string, unknown> | undefined
52
+
49
53
  // `id` keys the rule's own refusal record, so one rule firing never spends another's budget.
50
54
  interface DeliveredRule {
51
55
  id: string
@@ -63,6 +67,9 @@ interface DeliveredRule {
63
67
  // The occasion the rule governs, for a row whose trigger is the violation itself (else `is_trigger`
64
68
  // would only count runs that broke it).
65
69
  reaches?: CallTest
70
+ // The call made right, for a row `guard-remedy.ts` classes as a rewrite: `rule-rewrite.ts` runs it
71
+ // in place of the refusal. `undefined` where this call has no mechanical fix, so it is refused.
72
+ rewrite?: CallRewrite
66
73
  // The note a hook attaches where it rewrites a violating call instead of refusing it, so
67
74
  // `rule-value.ts` counts the rewrite as this row's delivery.
68
75
  rewrite_note?: string
@@ -107,7 +114,8 @@ const DELIVERED_RULES: ReadonlyArray<DeliveredRule> = [
107
114
  reason: piped_verification.PIPED_VERIFICATION_REASON,
108
115
  keeps: on_bash_command(piped_verification.keeps_verdict_intact),
109
116
  reaches: on_bash_command(piped_verification.runs_verification),
110
- // A `| tail` / `| grep` masking is rewritten under `pipefail` by `pretool-guard.ts`, never refused.
117
+ // A masking whose filter reads to the end is rewritten under `pipefail`, never refused.
118
+ rewrite: piped_verification.pipefail_input,
111
119
  rewrite_note: piped_verification.PIPEFAIL_NOTE,
112
120
  },
113
121
  {
@@ -146,6 +154,7 @@ const DELIVERED_RULES: ReadonlyArray<DeliveredRule> = [
146
154
  lane_split_park.ROW,
147
155
  lane_park.ROW,
148
156
  lane_interactive_ask.ROW,
157
+ agent_model.ROW,
149
158
  lane_carry_conflict.ROW,
150
159
  lane_switch_main.ROW,
151
160
  // Disjoint from `run-tail` by the `-y` flag.
@@ -304,5 +313,5 @@ const delivered_rules = {
304
313
  is_issue_filing,
305
314
  }
306
315
 
307
- export type { DeliveredRule, MeasuredRule }
316
+ export type { CallRewrite, DeliveredRule, MeasuredRule }
308
317
  export { delivered_rules }
@@ -123,6 +123,7 @@ const filing_cap = {
123
123
  FILING_CAP,
124
124
  FILING_CAP_REASON,
125
125
  ROW,
126
+ current_turn,
126
127
  is_prompt_line,
127
128
  prior_filing_count,
128
129
  turn_filing_count,
@@ -9,15 +9,18 @@
9
9
  // time is the whole point of the check.
10
10
 
11
11
  // **The hook-deliverable set — the single source is the list `pnpm josh rule:list` renders.** The
12
- // `PreToolUse` guard is wired to `Bash|Edit|Read|Write` (`scripts/hooks/pretool-guard.ts`), and the
13
- // lane-child interactive-ask row triggers on `AskUserQuestion`. A firing point naming one of these can
14
- // be delivered at the moment it binds; anything else cannot.
12
+ // `PreToolUse` guard is wired to `Bash|Edit|Read|Write` (`scripts/hooks/pretool-guard.ts`), the
13
+ // lane-child interactive-ask row triggers on `AskUserQuestion`, and the `agent-model` row on
14
+ // `Agent|Task`. A firing point naming one of these can be delivered at the moment it binds; anything
15
+ // else cannot.
15
16
  const HOOK_DELIVERABLE_TOOLS: ReadonlyArray<string> = [
16
17
  'Bash',
17
18
  'Edit',
18
19
  'Read',
19
20
  'Write',
20
21
  'AskUserQuestion',
22
+ 'Agent',
23
+ 'Task',
21
24
  ]
22
25
 
23
26
  // **Known Claude Code tool calls that a hook cannot deliver a rule on.** They are real tools, so a
@@ -31,8 +34,6 @@ const KNOWN_UNDELIVERABLE_TOOLS: ReadonlyArray<string> = [
31
34
  'WebFetch',
32
35
  'WebSearch',
33
36
  'TodoWrite',
34
- 'Task',
35
- 'Agent',
36
37
  'NotebookEdit',
37
38
  ]
38
39
 
@@ -0,0 +1,117 @@
1
+ // Every `PreToolUse` and `Stop` guard, classed by what its refusal asks for (joshuafolkken/kit#3920),
2
+ // with the reason it cannot run itself instead.
3
+ //
4
+ // **A refusal whose right form follows from the call alone is a wasted round trip.** The model reissues
5
+ // the call the refusal already described, at the run's whole context. Where that form is mechanical
6
+ // and stays the same tool, the hook runs it instead (`rule-rewrite.ts`) and says what it changed. Every
7
+ // other refusal asks for something the call does not carry: another tool, another command and its
8
+ // arguments, or a decision.
9
+ //
10
+ // `rewrite` means the row rewrites where it can and refuses where it cannot; `judgment` means it always
11
+ // refuses. **`why` is required**: a guard is the third rung of `principles.md` → "automate-first", so
12
+ // each row says why a command or the hook running the step cannot replace it. `guard-remedy.test.ts`
13
+ // pins that every guard is classed with a non-empty `why` and that each `rewrite` row has one.
14
+
15
+ type Remedy = 'rewrite' | 'judgment'
16
+
17
+ interface GuardRemedy {
18
+ remedy: Remedy
19
+ // Why the hook cannot make the step itself — printed by `rule:list` as `not automated`.
20
+ why: string
21
+ }
22
+
23
+ const REWRITTEN =
24
+ 'the hook makes the same call right itself; it refuses only a spelling it cannot rewrite'
25
+ const ANOTHER_TOOL = 'the right form is another tool, which `updatedInput` cannot switch to'
26
+ const ANOTHER_COMMAND = 'the right act is another command, whose arguments the call does not carry'
27
+ const NO_RUN = "the call must not run at all, or only on a person's word — refusing is the remedy"
28
+ // The `Stop` rows that already run their step: each blocks only on the case the hook cannot finish.
29
+ const STOP_NOTIFY = 'the hook sends the notify itself; it blocks only when the send fails'
30
+ const HOLD_RELEASE = 'the hook releases the hold itself; it blocks only when that release fails'
31
+ const ISSUE_CITATION =
32
+ 'the hook prints the citation lines itself; it blocks only on a number it cannot cite'
33
+ const FILING_OFFER = 'the filing needs a title and a body only the model can write'
34
+
35
+ function rewrite(): GuardRemedy {
36
+ return { remedy: 'rewrite', why: REWRITTEN }
37
+ }
38
+
39
+ function judgment(why: string): GuardRemedy {
40
+ return { remedy: 'judgment', why }
41
+ }
42
+
43
+ const GUARD_REMEDIES: Readonly<Record<string, GuardRemedy>> = {
44
+ // The rewrites: each fix is the same call made right.
45
+ 'piped-verification': rewrite(), // wrapped in `(set -o pipefail; …)`
46
+ 'issue-comments': rewrite(), // the same Issue's comments read on the same line
47
+ 'lane-background': rewrite(), // a lane child's `ship` run in the foreground (`pretool-guard.ts`)
48
+ 'file-body': judgment(ANOTHER_TOOL), // Edit or Write
49
+ 'shell-body': judgment(
50
+ "which part of the body the shell was meant to expand is the author's, not the hook's",
51
+ ),
52
+ 'raw-field-body': judgment('the body moves to a file the call does not name'),
53
+ 'rule-body': judgment('a trigger-plus-pointer line is written, not derived'),
54
+ 'direct-filing': judgment(ANOTHER_COMMAND),
55
+ 'direct-pr-create': judgment(ANOTHER_COMMAND),
56
+ 'josh-git-bare': judgment(ANOTHER_COMMAND),
57
+ 'run-tail': judgment(ANOTHER_COMMAND),
58
+ 'pre-gate-cut': judgment(ANOTHER_COMMAND),
59
+ 'implementation-cut': judgment(ANOTHER_COMMAND),
60
+ 'early-heartbeat': judgment(ANOTHER_COMMAND),
61
+ 'poll-loop': judgment(ANOTHER_COMMAND),
62
+ 'lane-park': judgment(ANOTHER_COMMAND),
63
+ 'lane-split-park': judgment(ANOTHER_COMMAND),
64
+ 'lane-interactive-ask': judgment(ANOTHER_COMMAND),
65
+ 'agent-model': judgment("which model fits the unit is the author's call"),
66
+ 'lane-carry-conflict': judgment(ANOTHER_COMMAND),
67
+ 'lane-switch-main': judgment(ANOTHER_COMMAND),
68
+ 'oracle-consulted': judgment(ANOTHER_COMMAND),
69
+ 'test-declared': judgment(ANOTHER_COMMAND),
70
+ batching: judgment(ANOTHER_COMMAND),
71
+ investigation: judgment(ANOTHER_COMMAND),
72
+ 'duplicate-read': judgment(ANOTHER_COMMAND),
73
+ 'run-watcher': judgment(ANOTHER_COMMAND),
74
+ 'parent-cut': judgment(ANOTHER_COMMAND),
75
+ 'issue-watch': judgment(ANOTHER_COMMAND),
76
+ 'third-party-write': judgment(NO_RUN),
77
+ 'filing-cap': judgment(NO_RUN),
78
+ 'git-force': judgment(NO_RUN),
79
+ 'worktree-mutation': judgment(NO_RUN),
80
+ 'index-mutation': judgment(NO_RUN),
81
+ 'destructive-command': judgment(NO_RUN),
82
+ 'protected-file': judgment(NO_RUN),
83
+ // The `Stop` rows: the hook judges a finished reply, so it has no call to rewrite.
84
+ 'stop-notify': judgment(STOP_NOTIFY),
85
+ 'issue-citation': judgment(ISSUE_CITATION),
86
+ 'filing-offer': judgment(FILING_OFFER),
87
+ 'hold-release': judgment(HOLD_RELEASE),
88
+ }
89
+
90
+ // A row family shares one class: `oracle-consulted:pkg:scout` is an `oracle-consulted` row.
91
+ const FAMILY_SEPARATOR = ':'
92
+
93
+ function entry_of(id: string): GuardRemedy | undefined {
94
+ return GUARD_REMEDIES[id] ?? GUARD_REMEDIES[id.split(FAMILY_SEPARATOR)[0] ?? '']
95
+ }
96
+
97
+ function remedy_of(id: string): Remedy | undefined {
98
+ return entry_of(id)?.remedy
99
+ }
100
+
101
+ function why_of(id: string): string | undefined {
102
+ return entry_of(id)?.why
103
+ }
104
+
105
+ function is_rewrite(id: string): boolean {
106
+ return remedy_of(id) === 'rewrite'
107
+ }
108
+
109
+ const guard_remedy = {
110
+ GUARD_REMEDIES,
111
+ is_rewrite,
112
+ remedy_of,
113
+ why_of,
114
+ }
115
+
116
+ export type { GuardRemedy, Remedy }
117
+ export { guard_remedy }