@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.
- package/.claude/settings.json +6 -6
- package/.claude/skills/epic-commands/SKILL.md +2 -14
- package/.claude/skills/epic-commands/epic-bundle.md +9 -5
- package/.claude/skills/epic-commands/execution-waves.md +1 -2
- package/.claude/skills/verify-ui/SKILL.md +1 -2
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +3 -7
- package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
- package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
- package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
- package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +7 -6
- package/.claude/skills/workflow-commands/delegation.md +12 -8
- package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
- package/.claude/skills/workflow-commands/followup.md +3 -6
- package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
- package/.claude/skills/workflow-commands/fullrun.md +4 -6
- package/.claude/skills/workflow-commands/halfrun.md +3 -7
- package/.claude/skills/workflow-commands/into-target.md +1 -2
- package/.claude/skills/workflow-commands/issue-comments.md +3 -3
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/latest-gate.md +2 -6
- package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
- package/.claude/skills/workflow-commands/observation-filing.md +0 -14
- package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
- package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
- package/.claude/skills/workflow-commands/prerequisite.md +1 -2
- package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +4 -4
- package/.claude/skills/workflow-commands/split-assessment.md +1 -3
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
- package/.codex/hooks.json +6 -6
- package/.github/actions/setup-node/action.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CLAUDE.md +1 -1
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +1 -1
- package/dist/commands/document-section-cli.js +67 -34
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/chunk-4KKD2UTK.js +66 -0
- package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
- package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
- package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
- package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
- package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
- package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
- package/dist/hooks/codex-hook-adapter.js +17 -14
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +5 -4
- package/dist/hooks/inputs.json +317 -264
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +370 -25
- package/dist/hooks/stop-guard.js +2576 -176
- package/dist/josh.js +2124 -277
- package/lefthook/base.yml +5 -3
- package/package.json +12 -10
- package/prompts/collaboration-workflow/file-edits.md +1 -3
- package/prompts/collaboration-workflow/gh-rest.md +1 -2
- package/prompts/collaboration-workflow/issue-citation.md +2 -4
- package/prompts/collaboration-workflow/issue-template.md +10 -2
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +2 -6
- package/prompts/collaboration-workflow/output-bounds.md +3 -3
- package/prompts/collaboration-workflow/overview.md +1 -1
- package/prompts/collaboration-workflow/principles.md +14 -8
- package/prompts/collaboration-workflow/report-format.md +9 -0
- package/prompts/collaboration-workflow/residency.md +12 -8
- package/prompts/collaboration-workflow/rule-delivery.md +3 -11
- package/prompts/collaboration-workflow/shell-body.md +6 -6
- package/prompts/collaboration-workflow/turn-batching.md +5 -5
- package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
- package/prompts/collaboration-workflow/wip-cap.md +6 -6
- package/prompts/collaboration-workflow.md +1 -0
- package/prompts/review.md +4 -5
- package/prompts/testing-guide.md +2 -2
- package/scripts/agent/agent-argv.ts +20 -4
- package/scripts/agent/agent-role-profile.ts +101 -45
- package/scripts/agent/agent-safe-text.ts +60 -0
- package/scripts/agent/claude-agent-argv.ts +14 -0
- package/scripts/agent/codex-agent-argv.ts +2 -0
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget.ts +4 -4
- package/scripts/backlog/backlog-drive-named.ts +1 -1
- package/scripts/backlog/backlog-drive-restore.ts +9 -1
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +47 -10
- package/scripts/delegation/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +0 -1
- package/scripts/document/document-byte-budget.ts +6 -5
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-evidence.ts +49 -26
- package/scripts/epic/epic-bundle-referenced.ts +14 -3
- package/scripts/epic/epic-bundle.ts +17 -9
- package/scripts/followup/git-followup-workflow.ts +19 -1
- package/scripts/gate/check-inputs.ts +129 -0
- package/scripts/gate/check-reuse.ts +162 -0
- package/scripts/gate/core-budget.ts +93 -44
- package/scripts/gate/core-wait.ts +130 -0
- package/scripts/gate/free-cores.ts +49 -0
- package/scripts/gate/gate-ledger.ts +17 -3
- package/scripts/gate/gate-plan.ts +23 -1
- package/scripts/gate/gate-report.ts +15 -2
- package/scripts/gate/gate-run-steps.ts +150 -0
- package/scripts/gate/held-mark.ts +55 -0
- package/scripts/gate/machine-capacity.ts +70 -11
- package/scripts/gate/pre-commit-type-check.ts +14 -1
- package/scripts/gate/verification-gate.ts +58 -125
- package/scripts/gh/git-gh-issue-rest.ts +1 -0
- package/scripts/gh/git-gh-issue-write.ts +24 -2
- package/scripts/gh/git-pr.ts +13 -1
- package/scripts/git/main-merge-regenerate.ts +78 -0
- package/scripts/git/main-merge.ts +14 -5
- package/scripts/hooks/batch-guard.ts +5 -8
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +1 -1
- package/scripts/hooks/pretool-guard.ts +27 -52
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/issue/expected-effect-lint.ts +59 -0
- package/scripts/issue/issue-cite-cli.ts +35 -15
- package/scripts/issue/issue-classification.ts +1 -1
- package/scripts/issue/issue-lint-cli.ts +5 -3
- package/scripts/issue/issue-scout-cli.ts +19 -14
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/josh-command-map.ts +2 -0
- package/scripts/josh/josh-command-types.ts +55 -9
- package/scripts/josh/josh-commands-ai.ts +13 -0
- package/scripts/josh/josh-commands-development.ts +1 -1
- package/scripts/josh/josh-commands-hooks.ts +6 -0
- package/scripts/josh/josh-commands-lane.ts +10 -1
- package/scripts/josh/josh-commands-workflow.ts +5 -0
- package/scripts/josh/josh-logic.ts +6 -15
- package/scripts/josh/josh-usage-automation.ts +121 -0
- package/scripts/josh/josh-usage-backlog.ts +180 -0
- package/scripts/josh/josh-usage-developer.ts +230 -0
- package/scripts/josh/josh-usage-hooks.ts +38 -0
- package/scripts/josh/josh-usage-issue.ts +205 -0
- package/scripts/josh/josh-usage-review.ts +101 -0
- package/scripts/josh/josh-usage-run.ts +252 -0
- package/scripts/josh/josh-usage-session.ts +238 -0
- package/scripts/josh/josh-usage-workflow.ts +212 -0
- package/scripts/josh/josh.ts +14 -5
- package/scripts/josh/package-directory.ts +20 -0
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +35 -4
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +2 -2
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +43 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-registry.ts +9 -7
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +57 -138
- package/scripts/lane/lane-withdraw-cli.ts +71 -0
- package/scripts/lane/lane-withdraw.ts +182 -0
- package/scripts/lint/format.ts +4 -4
- package/scripts/lint/lint-parallel.ts +10 -3
- package/scripts/lint/lint-related-scope.ts +4 -1
- package/scripts/notify/telegram-test.ts +10 -5
- package/scripts/report/report-format-reference.ts +2 -0
- package/scripts/review/acceptance-map-check.ts +61 -0
- package/scripts/review/acceptance-map.ts +205 -0
- package/scripts/review/review-brief-cli.ts +1 -1
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -2
- package/scripts/rules/filing-cap.ts +1 -0
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/issue-comments.ts +67 -6
- package/scripts/rules/last-prompt.ts +50 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +3 -1
- package/scripts/rules/rule-list.ts +4 -1
- package/scripts/rules/rule-registry-rows.ts +10 -0
- package/scripts/rules/rule-registry.ts +7 -4
- package/scripts/rules/rule-rewrite.ts +114 -0
- package/scripts/rules/rule-value.ts +7 -1
- package/scripts/rules/shell-segments.ts +15 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +147 -47
- package/scripts/rules/tail-commands.ts +15 -1
- package/scripts/run/board/run-board-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +9 -1
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-labels.ts +2 -0
- package/scripts/run/board/run-board-layout.ts +1 -1
- package/scripts/run/board/run-board-read.ts +16 -0
- package/scripts/run/board/run-board-reason.ts +10 -1
- package/scripts/run/board/run-board-render-legend.ts +1 -0
- package/scripts/run/board/run-board-render-slot.ts +1 -1
- package/scripts/run/board/run-board-render-waits.ts +41 -0
- package/scripts/run/board/run-board-render.ts +6 -1
- package/scripts/run/board/run-board-row-yield.ts +1 -0
- package/scripts/run/board/run-board-state.ts +5 -0
- package/scripts/run/board/run-board-status.ts +72 -21
- package/scripts/run/board/run-board-tick.ts +5 -1
- package/scripts/run/cut/run-cut-cli.ts +3 -2
- package/scripts/run/cut/run-cut-report.ts +16 -0
- package/scripts/run/detached-launch.ts +23 -4
- package/scripts/run/entry/run-entry-cli.ts +8 -1
- package/scripts/run/event/run-event-render.ts +1 -0
- package/scripts/run/event/run-event-scope.ts +19 -1
- package/scripts/run/event/run-event-stream-emit.ts +13 -6
- package/scripts/run/event/run-event-stream.ts +4 -0
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +7 -1
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -65
- package/scripts/run/merge/run-merge-signals.ts +99 -0
- package/scripts/run/merge/run-merge-steps.ts +11 -1
- package/scripts/run/merge/run-merge.ts +21 -9
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/ship/run-ship-cli.ts +5 -12
- package/scripts/run/ship/run-ship-detach.ts +9 -0
- package/scripts/run/ship/run-ship-next.ts +10 -2
- package/scripts/run/ship/run-ship-overlap.ts +195 -0
- package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
- package/scripts/run/ship/run-ship-preflight.ts +28 -2
- package/scripts/run/ship/run-ship-return.ts +31 -7
- package/scripts/run/ship/run-ship-review-steps.ts +12 -10
- package/scripts/run/ship/run-ship-steps.ts +1 -1
- package/scripts/run/ship/run-ship-stop-context.ts +206 -0
- package/scripts/run/ship/run-ship-sync.ts +14 -6
- package/scripts/test/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -4
- package/scripts/time-runtime/time-shell.ts +29 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -0
- package/templates/gitignore +6 -2
- 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
|
-
|
|
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.
|
|
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
|
|
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 }
|
|
@@ -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`),
|
|
13
|
-
// lane-child interactive-ask row triggers on `AskUserQuestion
|
|
14
|
-
// be delivered at the moment it binds; anything
|
|
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 }
|