@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
|
@@ -3,11 +3,11 @@ import { investigation_outcome } from '#scripts/delegation/investigation-guard'
|
|
|
3
3
|
import { issue_watch_hook } from '#scripts/issue/issue-watch-hook'
|
|
4
4
|
import { hook_decision, type GuardOutcome } from '#scripts/josh/hook-decision'
|
|
5
5
|
import { lane_phase } from '#scripts/lane/lane-phase'
|
|
6
|
-
import { json_value } from '#scripts/lib/json-value'
|
|
7
6
|
import { delivered_rules } from '#scripts/rules/delivered-rules'
|
|
8
7
|
import { lane_background } from '#scripts/rules/lane-background'
|
|
9
|
-
import {
|
|
8
|
+
import { rule_rewrite, type RuleRewrite } from '#scripts/rules/rule-rewrite'
|
|
10
9
|
import { run_parent_cut_hook } from '#scripts/run/run-parent-cut-hook'
|
|
10
|
+
import { run_poll_hook } from '#scripts/run/run-poll-hook'
|
|
11
11
|
import { run_watcher_hook } from '#scripts/run/run-watcher-hook'
|
|
12
12
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
13
13
|
import { batch_outcome } from './batch-guard'
|
|
@@ -22,11 +22,12 @@ import { step_zero_notice } from './step-zero-notice'
|
|
|
22
22
|
//
|
|
23
23
|
// **The verdict is identical to running the three in turn**, and that is the whole point. Each guard
|
|
24
24
|
// self-gates on the tool name inside its own `is_candidate` / trigger, so running all three on the
|
|
25
|
-
// union matcher (`Bash|Edit|Read|Write|AskUserQuestion`) refuses exactly what the three
|
|
26
|
-
// entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask
|
|
27
|
-
// and the batching and investigation guards self-gate
|
|
28
|
-
// refusal from any guard wins first; only when none refuses is a guard's non-blocking
|
|
29
|
-
// (the whole-file-write notice, a lane child's
|
|
25
|
+
// union matcher (`Bash|Edit|Read|Write|AskUserQuestion|Agent|Task`) refuses exactly what the three
|
|
26
|
+
// separate entries did — `AskUserQuestion` is matched for the rule guard's lane-child interactive-ask
|
|
27
|
+
// row and `Agent|Task` for its `agent-model` row, and the batching and investigation guards self-gate
|
|
28
|
+
// away from both. A refusal from any guard wins first; only when none refuses is a guard's non-blocking
|
|
29
|
+
// notice or fault (the whole-file-write notice, a lane child's duplicate-read notice) emitted. Each
|
|
30
|
+
// guard still honours its own
|
|
30
31
|
// switch (`JOSH_BATCH_GUARD` / `JOSH_INVESTIGATION_GUARD` / `JOSH_RULE_GUARD`) internally, so this
|
|
31
32
|
// process needs no switch of its own.
|
|
32
33
|
|
|
@@ -96,12 +97,14 @@ async function mark_phase(outcome: GuardOutcome): Promise<void> {
|
|
|
96
97
|
// **The parent hand-off guard is the second asynchronous rule, asked after the watcher's**.
|
|
97
98
|
// It reads the carry record to tell the `backlogrun` parent apart, so it sits
|
|
98
99
|
// beside the watcher rather than among the synchronous rows; a stale watcher is the cheaper fix and is
|
|
99
|
-
// surfaced first.
|
|
100
|
+
// surfaced first. **The poll guard is third**: it reads the same carry record, and only for a call that
|
|
101
|
+
// repeats a state read, so the hand-off still wins over it.
|
|
100
102
|
async function async_reason(raw_payload: string): Promise<string | undefined> {
|
|
101
103
|
const watcher = await run_watcher_hook.watcher_hook_reason(raw_payload)
|
|
102
104
|
const cut = watcher ?? (await run_parent_cut_hook.parent_cut_reason(raw_payload))
|
|
105
|
+
const poll = cut ?? (await run_poll_hook.safe_poll_reason(raw_payload))
|
|
103
106
|
|
|
104
|
-
return
|
|
107
|
+
return poll ?? (await issue_watch_hook.watch_reason())
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
async function pretool_outcome_async(raw_payload: string): Promise<GuardOutcome> {
|
|
@@ -159,59 +162,32 @@ function foreground_rewrite(
|
|
|
159
162
|
)
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
input: Record<string, unknown>
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// A piped josh check the hook runs under `set -o pipefail` instead of refusing it:
|
|
168
|
-
// the rewritten input, and the payload that carries it. The rewrite is the
|
|
169
|
-
// rule guard's delivery, so the rule guard's switch turns it off too.
|
|
170
|
-
function pipefail_rewrite(raw_payload: string): PipefailRewrite | undefined {
|
|
171
|
-
if (!delivered_rules.is_enabled()) return undefined
|
|
172
|
-
|
|
173
|
-
const call = guarded_call(raw_payload)
|
|
174
|
-
const input = call === undefined ? undefined : piped_verification.pipefail_input(call)
|
|
175
|
-
const payload = json_value.parse_or_undefined(raw_payload)
|
|
176
|
-
|
|
177
|
-
if (input === undefined || !json_value.is_record(payload)) return undefined
|
|
178
|
-
|
|
179
|
-
return { payload: JSON.stringify({ ...payload, tool_input: input }), input }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function pipefail_envelope(
|
|
165
|
+
// The rows' rewrite carries no permission decision: whatever else the line runs meets its own prompt.
|
|
166
|
+
function rule_envelope(
|
|
183
167
|
outcome: GuardOutcome,
|
|
184
|
-
|
|
168
|
+
rewrite: RuleRewrite | undefined,
|
|
185
169
|
): string | undefined {
|
|
186
|
-
if (
|
|
170
|
+
if (rewrite === undefined || outcome.reason !== undefined) return undefined
|
|
187
171
|
|
|
188
|
-
return hook_decision.flow_rewrite_envelope(
|
|
189
|
-
input,
|
|
190
|
-
rewrite_context(piped_verification.PIPEFAIL_NOTE, outcome),
|
|
191
|
-
)
|
|
172
|
+
return hook_decision.flow_rewrite_envelope(rewrite.input, rewrite_context(rewrite.note, outcome))
|
|
192
173
|
}
|
|
193
174
|
|
|
194
175
|
// The hook's one write: a rewrite where one applies, the outcome's own envelope otherwise.
|
|
195
|
-
function emit(
|
|
196
|
-
|
|
197
|
-
outcome: GuardOutcome,
|
|
198
|
-
pipefail_input?: Record<string, unknown>,
|
|
199
|
-
): void {
|
|
200
|
-
const rewrite =
|
|
201
|
-
pipefail_envelope(outcome, pipefail_input) ?? foreground_rewrite(outcome, raw_payload)
|
|
176
|
+
function emit(raw_payload: string, outcome: GuardOutcome, rewrite?: RuleRewrite): void {
|
|
177
|
+
const envelope = rule_envelope(outcome, rewrite) ?? foreground_rewrite(outcome, raw_payload)
|
|
202
178
|
|
|
203
|
-
if (
|
|
204
|
-
else process.stdout.write(`${
|
|
179
|
+
if (envelope === undefined) hook_decision.emit_outcome(outcome)
|
|
180
|
+
else process.stdout.write(`${envelope}\n`)
|
|
205
181
|
}
|
|
206
182
|
|
|
207
|
-
// **The guards judge the call that will run.** A
|
|
208
|
-
//
|
|
209
|
-
//
|
|
183
|
+
// **The guards judge the call that will run.** A rewritten call reaches every guard already made
|
|
184
|
+
// right, so the rewriting row stays quiet on it while any other guard's refusal still wins over the
|
|
185
|
+
// rewrite. The Codex adapter cannot rewrite a call, so it goes on judging the call as typed.
|
|
210
186
|
async function respond(raw_payload: string): Promise<void> {
|
|
211
|
-
const
|
|
212
|
-
const judged =
|
|
187
|
+
const rewrite = rule_rewrite.rewrite(raw_payload)
|
|
188
|
+
const judged = rewrite?.payload ?? raw_payload
|
|
213
189
|
|
|
214
|
-
emit(judged, await pretool_outcome_async(judged),
|
|
190
|
+
emit(judged, await pretool_outcome_async(judged), rewrite)
|
|
215
191
|
}
|
|
216
192
|
|
|
217
193
|
const pretool_guard = {
|
|
@@ -219,7 +195,6 @@ const pretool_guard = {
|
|
|
219
195
|
emit,
|
|
220
196
|
foreground_rewrite,
|
|
221
197
|
mark_phase,
|
|
222
|
-
pipefail_rewrite,
|
|
223
198
|
pretool_outcome,
|
|
224
199
|
respond,
|
|
225
200
|
}
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
# A hook command in `.claude/settings.json` / `.codex/hooks.json` only moves to the project root
|
|
6
6
|
# (`scripts/init/hook-launch.ts`) and calls this script, so everything every hook shares lives here once:
|
|
7
7
|
#
|
|
8
|
-
# - The git location variables a git hook exports
|
|
9
|
-
#
|
|
10
|
-
# them — git metadata outside the work tree.
|
|
8
|
+
# - The root prefix has already cleared the git location variables a git hook exports, or kept them
|
|
9
|
+
# when the checkout needs them; this script starts no `git` of its own (joshuafolkken/kit#3946).
|
|
11
10
|
# - The pre-built bundle `dist/hooks/<name>.js` runs once its gate passes; otherwise the live source
|
|
12
11
|
# runs through josh, whose command is the name with its first `-` turned into `:`
|
|
13
12
|
# (`pretool-guard` → `pretool:guard`).
|
|
@@ -16,27 +15,26 @@
|
|
|
16
15
|
# starts node once (joshuafolkken/kit#3327). In an installed package it is a presence check: the
|
|
17
16
|
# published bundles cannot be stale against a source the consumer never edits, and Node strips no
|
|
18
17
|
# types under `node_modules`.
|
|
18
|
+
# - Nothing is spawned that the shell can do itself: the directory is a parameter expansion rather than
|
|
19
|
+
# `dirname`, and `node` replaces this shell through `exec` rather than running beside it.
|
|
19
20
|
# - An `if`/`else`, never `&&`/`||`: a guard's refusal is a non-zero exit, and a chain would read it as
|
|
20
21
|
# a missing bundle and run the hook twice.
|
|
21
22
|
|
|
22
|
-
location_variables='GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_DIR GIT_INDEX_FILE GIT_NAMESPACE GIT_OBJECT_DIRECTORY GIT_WORK_TREE'
|
|
23
|
-
unset_options=''
|
|
24
|
-
for variable in $location_variables; do unset_options="$unset_options -u $variable"; done
|
|
25
|
-
# shellcheck disable=SC2086
|
|
26
|
-
if env $unset_options git rev-parse --show-toplevel >/dev/null 2>&1; then unset $location_variables; fi
|
|
27
|
-
|
|
28
23
|
hook_name=$1
|
|
29
24
|
shift
|
|
30
|
-
|
|
25
|
+
case $0 in
|
|
26
|
+
*/*) hooks_dir=${0%/*} ;;
|
|
27
|
+
*) hooks_dir=. ;;
|
|
28
|
+
esac
|
|
31
29
|
package_dir="$hooks_dir/../.."
|
|
32
30
|
bundle="$package_dir/dist/hooks/$hook_name.js"
|
|
33
31
|
josh_command="${hook_name%%-*}:${hook_name#*-}"
|
|
34
32
|
|
|
35
33
|
case $0 in
|
|
36
34
|
*node_modules/*)
|
|
37
|
-
if [ -d "$package_dir/dist/hooks" ]; then node "$bundle" "$@"; else node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
|
|
35
|
+
if [ -d "$package_dir/dist/hooks" ]; then exec node "$bundle" "$@"; else exec node "$package_dir/dist/josh.js" "$josh_command" "$@"; fi
|
|
38
36
|
;;
|
|
39
37
|
*)
|
|
40
|
-
node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
|
|
38
|
+
exec node --disable-warning=ExperimentalWarning "$hooks_dir/hook-bundle-ready.ts" "$hook_name" "$josh_command" "$@"
|
|
41
39
|
;;
|
|
42
40
|
esac
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { git_gh_repo } from '#scripts/gh/git-gh-repo'
|
|
2
|
+
import { issue_cite } from '#scripts/issue/issue-cite'
|
|
3
|
+
import { issue_cite_cli } from '#scripts/issue/issue-cite-cli'
|
|
4
|
+
import { session_cite } from '#scripts/issue/session-cite'
|
|
5
|
+
import { bounded_pool } from '#scripts/lib/bounded-pool'
|
|
6
|
+
import { telegram_test } from '#scripts/notify/telegram-test'
|
|
7
|
+
import type { CliValues } from '#scripts/notify/telegram-test-logic'
|
|
8
|
+
import { issue_citation } from '#scripts/rules/issue-citation'
|
|
9
|
+
import { stop_rules, type StopAction, type StopOutcome } from '#scripts/rules/stop-rules'
|
|
10
|
+
import { run_hold } from '#scripts/run/hold/run-hold'
|
|
11
|
+
import { run_hold_cli } from '#scripts/run/hold/run-hold-cli'
|
|
12
|
+
import { run_hold_session } from '#scripts/run/hold/run-hold-session'
|
|
13
|
+
|
|
14
|
+
type NotifyAction = Extract<StopAction, { kind: 'notify' }>
|
|
15
|
+
|
|
16
|
+
// The `Stop` hook running the operations whose answer is fixed (joshuafolkken/kit#3919): the
|
|
17
|
+
// `confirmation` notify, the hold release and the citation lines, through the very calls
|
|
18
|
+
// `josh notify`, `josh run:release` and `josh issue:cite` make — in process, since three `pnpm`
|
|
19
|
+
// launches would spend most of the hook's 20-second budget. What each did reaches the person as a
|
|
20
|
+
// `systemMessage`; one that did not take falls back to the refusal it replaced (`stop_rules.fallback_reason`).
|
|
21
|
+
|
|
22
|
+
const SUCCESS_EXIT_CODE = 0
|
|
23
|
+
const NOTICE_SEPARATOR = '\n'
|
|
24
|
+
// The reply is the stop's reason, but a Telegram is read at a glance — its opening is enough.
|
|
25
|
+
const SUMMARY_CHARS = 600
|
|
26
|
+
// A `#N`, `owner/repo#N` or URL is a run of printable ASCII; a cut inside one would link the wrong Issue.
|
|
27
|
+
const TOKEN_CHAR = /^[!-~]$/u
|
|
28
|
+
const EMPTY_SUMMARY = 'The run stopped and is waiting for you.'
|
|
29
|
+
|
|
30
|
+
interface Settled {
|
|
31
|
+
notices: ReadonlyArray<string>
|
|
32
|
+
failure: string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// One action at a time — the order is the rule order.
|
|
36
|
+
const SEQUENTIAL = 1
|
|
37
|
+
|
|
38
|
+
// Where the run of token characters `text` ends with starts.
|
|
39
|
+
function token_start(text: string): number {
|
|
40
|
+
let index = text.length
|
|
41
|
+
|
|
42
|
+
while (index > 0 && TOKEN_CHAR.test(text.charAt(index - 1))) index -= 1
|
|
43
|
+
|
|
44
|
+
return index
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cut before linking, on a boundary that keeps every reference whole: the character past the cut shows
|
|
48
|
+
// whether the last token ran on, and a token that did is dropped rather than halved.
|
|
49
|
+
function summary_of(message: string): string {
|
|
50
|
+
const trimmed = message.trim()
|
|
51
|
+
if (trimmed === '') return EMPTY_SUMMARY
|
|
52
|
+
if (trimmed.length <= SUMMARY_CHARS) return trimmed
|
|
53
|
+
|
|
54
|
+
const overshoot = trimmed.slice(0, SUMMARY_CHARS + 1)
|
|
55
|
+
const cut = overshoot.slice(0, Math.min(token_start(overshoot), SUMMARY_CHARS))
|
|
56
|
+
|
|
57
|
+
return cut === '' ? trimmed.slice(0, SUMMARY_CHARS) : cut
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The held Issue's URL, or `undefined` for a `new` entry or a repository that cannot be read.
|
|
61
|
+
async function issue_url_of(issue: string): Promise<string | undefined> {
|
|
62
|
+
if (issue === run_hold.UNNUMBERED_ISSUE) return undefined
|
|
63
|
+
|
|
64
|
+
const slug = await git_gh_repo.repo_get_name_with_owner()
|
|
65
|
+
|
|
66
|
+
return slug === undefined ? undefined : issue_cite.issue_url(slug, issue)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The body is the reply's opening copied into a Telegram, so its bare `#N` are linked (`issue-citation.md`).
|
|
70
|
+
// The choices are the reply's own decision lines (joshuafolkken/kit#3961).
|
|
71
|
+
function notify_values(issue_url: string | undefined, action: NotifyAction): CliValues {
|
|
72
|
+
const { recommended, alternative } = action.decision
|
|
73
|
+
const values: CliValues = {
|
|
74
|
+
'task-type': 'confirmation',
|
|
75
|
+
body: session_cite.text(summary_of(action.message)),
|
|
76
|
+
recommended,
|
|
77
|
+
...(alternative !== undefined && { alternative: [alternative] }),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return issue_url === undefined ? values : { ...values, 'issue-url': issue_url }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The send is stamped on the hold, so a later stop of the same pause does not announce it again. A
|
|
84
|
+
// stamp that fails does not fail the send: the Telegram is out, and a refused stop would send it twice.
|
|
85
|
+
async function mark_notified(): Promise<void> {
|
|
86
|
+
try {
|
|
87
|
+
const directory = await run_hold.worktree_directory()
|
|
88
|
+
|
|
89
|
+
if (directory !== undefined) run_hold_session.mark_notified(run_hold.hold_path(directory))
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`stop:guard could not stamp the notify on the hold: ${String(error)}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function notify(action: NotifyAction): Promise<string> {
|
|
96
|
+
const issue_url = await issue_url_of(action.issue)
|
|
97
|
+
|
|
98
|
+
await telegram_test.send_values(notify_values(issue_url, action))
|
|
99
|
+
await mark_notified()
|
|
100
|
+
|
|
101
|
+
return `📱 stop:guard sent the confirmation notify (${issue_url ?? action.issue}).`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function release(issue: string): Promise<string | undefined> {
|
|
105
|
+
const argv = issue === run_hold.UNNUMBERED_ISSUE ? ['--release'] : ['--release', issue]
|
|
106
|
+
const code = await run_hold_cli.run(argv)
|
|
107
|
+
|
|
108
|
+
return code === SUCCESS_EXIT_CODE ? '🔓 stop:guard released the run:hold record.' : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Every line or none: a citation with a number missing is the half-fix the refusal exists to avoid.
|
|
112
|
+
async function cite(references: ReadonlyArray<string>): Promise<string | undefined> {
|
|
113
|
+
const cited = await issue_cite_cli.cite(issue_citation.cite_arguments(references))
|
|
114
|
+
if (cited === undefined || cited.failures.length > 0) return undefined
|
|
115
|
+
|
|
116
|
+
return ['Issue citations:', ...cited.lines].join(NOTICE_SEPARATOR)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// An action's line for the person, or `undefined` when it did not take.
|
|
120
|
+
async function run_action(action: StopAction): Promise<string | undefined> {
|
|
121
|
+
if (action.kind === 'notify') return await notify(action)
|
|
122
|
+
if (action.kind === 'release') return await release(action.issue)
|
|
123
|
+
|
|
124
|
+
return await cite(action.references)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// **The hook's stdout is the decision envelope, so nothing else may reach it**: the reused calls report
|
|
128
|
+
// on stdout (`josh run:release`'s verdict token, the notify's sent line) and are sent to stderr instead.
|
|
129
|
+
type StreamWrite = typeof process.stdout.write
|
|
130
|
+
|
|
131
|
+
function set_stdout_write(stream_write: StreamWrite): void {
|
|
132
|
+
process.stdout.write = stream_write
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function on_stderr<T>(work: () => Promise<T>): Promise<T> {
|
|
136
|
+
const saved = { write: process.stdout.write.bind(process.stdout) }
|
|
137
|
+
|
|
138
|
+
set_stdout_write(process.stderr.write.bind(process.stderr))
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return await work()
|
|
142
|
+
} finally {
|
|
143
|
+
set_stdout_write(saved.write)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A throw is an action that did not take, the same as one that answered it had failed.
|
|
148
|
+
async function attempt(action: StopAction): Promise<string | undefined> {
|
|
149
|
+
try {
|
|
150
|
+
return await run_action(action)
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function is_notice(notice: string | undefined): notice is string {
|
|
157
|
+
return notice !== undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// In order, one at a time: the notify reports the hold the release then ends. The first failure is the
|
|
161
|
+
// one refused with, as the rule order put it first.
|
|
162
|
+
async function settle(actions: ReadonlyArray<StopAction>): Promise<Settled> {
|
|
163
|
+
const notices = await on_stderr(
|
|
164
|
+
async () => await bounded_pool.bounded_map(actions, SEQUENTIAL, attempt),
|
|
165
|
+
)
|
|
166
|
+
const failed = actions.find((_action, index) => notices[index] === undefined)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
notices: notices.filter((notice) => is_notice(notice)),
|
|
170
|
+
failure: failed === undefined ? undefined : stop_rules.fallback_reason(failed),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function joined(notices: ReadonlyArray<string>): string | undefined {
|
|
175
|
+
return notices.length === 0 ? undefined : notices.join(NOTICE_SEPARATOR)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A failure refuses only where the outcome allows it — never past the loop-breaker.
|
|
179
|
+
function settled_envelope(
|
|
180
|
+
settled: Settled,
|
|
181
|
+
is_blocking: boolean,
|
|
182
|
+
lang: string,
|
|
183
|
+
): string | undefined {
|
|
184
|
+
const notice = joined(settled.notices)
|
|
185
|
+
|
|
186
|
+
if (is_blocking && settled.failure !== undefined) {
|
|
187
|
+
return stop_rules.block_envelope(settled.failure, lang, notice)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return notice === undefined ? undefined : stop_rules.notice_envelope(notice)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// What the hook writes for one stop's outcome, or `undefined` for a stop that writes nothing.
|
|
194
|
+
async function envelope_of(outcome: StopOutcome, lang: string): Promise<string | undefined> {
|
|
195
|
+
if (outcome.reason !== undefined) return stop_rules.block_envelope(outcome.reason, lang)
|
|
196
|
+
|
|
197
|
+
const settled = await settle(outcome.actions)
|
|
198
|
+
|
|
199
|
+
return settled_envelope(settled, outcome.blocks_on_failure, lang)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const stop_actions = { envelope_of, summary_of }
|
|
203
|
+
|
|
204
|
+
export { stop_actions }
|
|
@@ -5,6 +5,7 @@ import { agent_headless } from '#scripts/agent/agent-headless'
|
|
|
5
5
|
import { backlog_ready } from '#scripts/backlog/backlog-ready'
|
|
6
6
|
import { backlog_stalled_detect } from '#scripts/backlog/backlog-stalled-detect'
|
|
7
7
|
import { repo_party } from '#scripts/discovery/repo-party'
|
|
8
|
+
import { stop_actions } from '#scripts/hooks/stop-actions'
|
|
8
9
|
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
9
10
|
import { session_language } from '#scripts/josh/session-language'
|
|
10
11
|
import { lane_child_marker } from '#scripts/lane/lane-child-marker'
|
|
@@ -13,37 +14,64 @@ import { filing_cap } from '#scripts/rules/filing-cap'
|
|
|
13
14
|
import { lane_background } from '#scripts/rules/lane-background'
|
|
14
15
|
import { lane_park } from '#scripts/rules/lane-park'
|
|
15
16
|
import { last_prompt } from '#scripts/rules/last-prompt'
|
|
17
|
+
import { pre_gate_cut } from '#scripts/rules/pre-gate-cut'
|
|
18
|
+
import { stop_decision } from '#scripts/rules/stop-decision'
|
|
16
19
|
import { stop_rules, type StopContext, type StopOutcome } from '#scripts/rules/stop-rules'
|
|
20
|
+
import { tail_commands } from '#scripts/rules/tail-commands'
|
|
17
21
|
import { run_carry } from '#scripts/run/carry/run-carry'
|
|
18
22
|
import { run_cut } from '#scripts/run/cut/run-cut'
|
|
19
|
-
import {
|
|
23
|
+
import { run_cut_report } from '#scripts/run/cut/run-cut-report'
|
|
24
|
+
import { run_hold, type RunHold } from '#scripts/run/hold/run-hold'
|
|
25
|
+
import { run_hold_session } from '#scripts/run/hold/run-hold-session'
|
|
20
26
|
import { run_halfrun_resume } from '#scripts/run/run-halfrun-resume'
|
|
21
27
|
import { run_headless } from '#scripts/run/run-headless'
|
|
22
28
|
import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
|
|
23
29
|
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
24
30
|
import { time_hook_transcript } from '#scripts/time-runtime/time-hook-transcript'
|
|
25
31
|
|
|
26
|
-
// The `Stop` hook entry: one process that delivers the
|
|
32
|
+
// The `Stop` hook entry: one process that delivers the stop-time rules, running the fixed ones itself
|
|
33
|
+
// (`stop-actions.ts`).
|
|
27
34
|
// It is the stop-time counterpart of `pretool-guard.ts` — a second entry on the same `hook_decision`
|
|
28
35
|
// foundation, not a second copy of it.
|
|
29
36
|
//
|
|
30
37
|
// **Every failure allows the stop.** A missing transcript, an unreadable hold record, a git status
|
|
31
38
|
// that will not read — each ends as "no block", for the same reason the PreToolUse guard fails open:
|
|
32
|
-
// a hook that failed closed would wedge a run over its own plumbing. The
|
|
33
|
-
//
|
|
39
|
+
// a hook that failed closed would wedge a run over its own plumbing. The hook runs the fixed
|
|
40
|
+
// operations itself (send the notify, release the hold) and `stop_hook_active` is the backstop, so failing
|
|
34
41
|
// open here costs at most a missed nudge, never a stuck run.
|
|
35
42
|
|
|
36
|
-
// The hold record for this working tree
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
async function hold_facts(): Promise<Pick<StopContext, 'hold_present' | 'prrun_stopped'>> {
|
|
43
|
+
// The hold record for this working tree. `unreadable` reads as absent: the safe direction for a stop
|
|
44
|
+
// guard is not to act, and an unreadable record is not proof a run is holding.
|
|
45
|
+
async function held_record(): Promise<RunHold | undefined> {
|
|
40
46
|
const directory = await run_hold.worktree_directory()
|
|
41
|
-
|
|
42
|
-
directory === undefined
|
|
43
|
-
? undefined
|
|
44
|
-
: run_halfrun_resume.hold_of(run_hold.read_hold(run_hold.hold_path(directory)))
|
|
47
|
+
if (directory === undefined) return undefined
|
|
45
48
|
|
|
46
|
-
return
|
|
49
|
+
return run_halfrun_resume.hold_of(run_hold.read_hold(run_hold.hold_path(directory)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The pause was announced: by a `josh notify` on the tail, or by this hook's own send, recorded on the
|
|
53
|
+
// hold since the pause began (joshuafolkken/kit#3961).
|
|
54
|
+
function was_notified(hold: RunHold | undefined, tail: string): boolean {
|
|
55
|
+
if (lane_park.mentions_confirmation_notify(tail)) return true
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
hold !== undefined && run_hold_session.is_notified_since(hold, last_prompt.pause_start(tail))
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The Issue the hold names, whether it is a `prrun` stop's and this session's own, and whether its
|
|
63
|
+
// pause was already announced.
|
|
64
|
+
async function hold_facts(
|
|
65
|
+
tail: string,
|
|
66
|
+
): Promise<Pick<StopContext, 'hold_issue' | 'hold_owned' | 'notified' | 'prrun_stopped'>> {
|
|
67
|
+
const hold = await held_record()
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
hold_issue: hold?.issue,
|
|
71
|
+
hold_owned: hold === undefined || run_hold_session.is_session_hold(hold),
|
|
72
|
+
notified: was_notified(hold, tail),
|
|
73
|
+
prrun_stopped: hold?.prrun_stop_head !== undefined,
|
|
74
|
+
}
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
// A `confirmation` notify sits on this run's transcript tail. The tail is derived exactly as the
|
|
@@ -60,6 +88,19 @@ function was_filed(tail: string): boolean {
|
|
|
60
88
|
return filing_cap.turn_filing_count(tail) > 0
|
|
61
89
|
}
|
|
62
90
|
|
|
91
|
+
// This turn ended at a cut — a record still `carried`, or this turn's `run:cut` relaunching a successor.
|
|
92
|
+
// The second covers the successor's resume clearing the record before this stop (joshuafolkken/kit#3936);
|
|
93
|
+
// the held cut outside a lane prints no marker, so its person is still notified.
|
|
94
|
+
function is_cut_stop(tail: string): boolean {
|
|
95
|
+
if (run_cut.carried_cut_sync() !== undefined) return true
|
|
96
|
+
|
|
97
|
+
return tail_commands.answered(
|
|
98
|
+
filing_cap.current_turn(tail),
|
|
99
|
+
pre_gate_cut.takes_the_cut,
|
|
100
|
+
run_cut_report.RELAUNCHED_CUT_MARKER,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
63
104
|
// The session language the refusal is written for — `write_stop_decision` has already loaded the `.env`.
|
|
64
105
|
function session_lang(): string {
|
|
65
106
|
return session_language.resolve_session_lang().lang
|
|
@@ -85,13 +126,13 @@ async function build_context(
|
|
|
85
126
|
const tail = transcript_tail(transcript_path)
|
|
86
127
|
|
|
87
128
|
return {
|
|
88
|
-
...(await hold_facts()),
|
|
129
|
+
...(await hold_facts(tail)),
|
|
89
130
|
tree_clean: !(await run_hold.is_tree_dirty()),
|
|
90
|
-
notified: lane_park.mentions_confirmation_notify(tail),
|
|
91
131
|
message: payload_tail.message,
|
|
92
132
|
prompt: last_prompt.prompt_text(tail),
|
|
133
|
+
answers_question: stop_decision.is_question(last_prompt.turn_prompt(tail)),
|
|
93
134
|
stop_hook_active: payload_tail.stop_hook_active,
|
|
94
|
-
cut_pending:
|
|
135
|
+
cut_pending: is_cut_stop(tail),
|
|
95
136
|
filed: was_filed(tail),
|
|
96
137
|
session_owner: repo_party.current_owner(),
|
|
97
138
|
...(await session_role_facts()),
|
|
@@ -167,17 +208,17 @@ async function run_report_checks(): Promise<void> {
|
|
|
167
208
|
await run_stranded_detect.run_stranded_check()
|
|
168
209
|
}
|
|
169
210
|
|
|
170
|
-
// Nothing reaches stdout on an ordinary stop, so what the harness parses stays empty unless the stop
|
|
171
|
-
//
|
|
211
|
+
// Nothing reaches stdout on an ordinary stop, so what the harness parses stays empty unless the stop is
|
|
212
|
+
// held or the hook acted on it (`stop-actions.ts`).
|
|
172
213
|
async function write_stop_decision(raw_payload: string): Promise<void> {
|
|
173
214
|
hook_decision.load_environment_file()
|
|
174
215
|
await run_report_checks()
|
|
175
216
|
|
|
176
|
-
const
|
|
217
|
+
const envelope = await stop_actions.envelope_of(await outcome_of(raw_payload), session_lang())
|
|
177
218
|
|
|
178
|
-
if (
|
|
219
|
+
if (envelope === undefined) return
|
|
179
220
|
|
|
180
|
-
process.stdout.write(`${
|
|
221
|
+
process.stdout.write(`${envelope}\n`)
|
|
181
222
|
}
|
|
182
223
|
|
|
183
224
|
const stop_guard = { outcome_of, stop_outcome_for_payload }
|
|
@@ -9,18 +9,23 @@ import { GIT_LOCATION_VARIABLES } from '#scripts/git/git-location-environment'
|
|
|
9
9
|
// Every path after the prefix is relative, so a hook fired from a
|
|
10
10
|
// subdirectory would otherwise miss its script. The root is git's, resolved with the location
|
|
11
11
|
// variables a git hook exports cleared first, and resolved with them only when that fails — git
|
|
12
|
-
// metadata outside the work tree.
|
|
13
|
-
// so
|
|
12
|
+
// metadata outside the work tree. When the cleared lookup succeeds the variables are cleared for the
|
|
13
|
+
// hook too, so its own git calls act on the checkout it stands in. The consumer rewrite
|
|
14
|
+
// (`hook-command-rewrite.ts`) keeps this prefix, so both sides stay one mechanism for Claude Code and
|
|
15
|
+
// Codex alike.
|
|
14
16
|
//
|
|
15
|
-
// **
|
|
16
|
-
//
|
|
17
|
-
//
|
|
17
|
+
// **A hook runs on every tool call, so its launch spawns as little as it can** (joshuafolkken/kit#3946):
|
|
18
|
+
// the shell's own `unset` clears the variables rather than an `env` process, and that one lookup
|
|
19
|
+
// both finds the root and decides whether the variables stay — `git` starts once.
|
|
20
|
+
//
|
|
21
|
+
// **Everything after the root is one script** (`scripts/hooks/run-hook.sh`): the bundle-ready gate and
|
|
22
|
+
// the live-source fallback live there once, so each hook command names only its hook.
|
|
18
23
|
|
|
19
24
|
const HOOK_DIST_DIR = 'dist/hooks'
|
|
20
25
|
const RUN_HOOK_SCRIPT = 'scripts/hooks/run-hook.sh'
|
|
21
|
-
const
|
|
26
|
+
const LOCATION_VARIABLES = GIT_LOCATION_VARIABLES.join(' ')
|
|
22
27
|
const ROOT_LOOKUP = 'git rev-parse --show-toplevel'
|
|
23
|
-
const PROJECT_ROOT_PREFIX = `project_root="$(
|
|
28
|
+
const PROJECT_ROOT_PREFIX = `if project_root="$(unset ${LOCATION_VARIABLES}; ${ROOT_LOOKUP} 2>/dev/null)"; then unset ${LOCATION_VARIABLES}; else project_root="$(${ROOT_LOOKUP})"; fi && cd "$project_root" && `
|
|
24
29
|
|
|
25
30
|
// `name` is the hook's bundle under `dist/hooks` without `.js` (e.g. `pretool-guard`), followed by any
|
|
26
31
|
// arguments it takes (e.g. `codex-hook-adapter pretool`).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { markdown_section } from './markdown-section'
|
|
2
|
+
|
|
3
|
+
// joshuafolkken/kit#3909: a run-time-shortening proposal was filed and implemented on a criterion that
|
|
4
|
+
// never weighed how often the speed-up applies against what it costs when it does not. An Issue that
|
|
5
|
+
// declares that aim must therefore state its expected effect — the hit rate, the gain per hit, the loss
|
|
6
|
+
// per miss and where each number came from — before `issue:lint` and `issue:file` accept it. The
|
|
7
|
+
// declaration, the heading and the item labels are the template's, pinned by the document test.
|
|
8
|
+
|
|
9
|
+
const DECLARATION_LINE = '- 目的: 時間短縮'
|
|
10
|
+
const BACKGROUND_HEADING = '## 背景'
|
|
11
|
+
const HEADING = '## 期待効果'
|
|
12
|
+
const ITEM_LABELS: ReadonlyArray<string> = ['効く率', '1 回あたりの得', '外れたときの損', '出典']
|
|
13
|
+
// A template placeholder value — the `<...>` guidance a filed Issue replaces. It is not an estimate, but a
|
|
14
|
+
// value that only opens with `<` (an upper bound such as `<1 min`) is one, so the placeholder must close with `>` too.
|
|
15
|
+
const PLACEHOLDER = /^<.*>$/u
|
|
16
|
+
|
|
17
|
+
// Whether the body declares a run-time-shortening aim under its background heading.
|
|
18
|
+
function is_target(body: string): boolean {
|
|
19
|
+
const background = markdown_section.section_lines(body, BACKGROUND_HEADING).join('\n')
|
|
20
|
+
|
|
21
|
+
return markdown_section.has_unfenced_line(background, DECLARATION_LINE)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function item_value(line: string, label: string): string | undefined {
|
|
25
|
+
const prefix = `- ${label}:`
|
|
26
|
+
|
|
27
|
+
if (!line.startsWith(prefix)) return undefined
|
|
28
|
+
|
|
29
|
+
return line.slice(prefix.length).trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function has_item(lines: ReadonlyArray<string>, label: string): boolean {
|
|
33
|
+
return lines.some((line) => {
|
|
34
|
+
const value = item_value(line.trim(), label)
|
|
35
|
+
|
|
36
|
+
return value !== undefined && value.length > 0 && !PLACEHOLDER.test(value)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Every expected-effect problem a body has, or an empty array. A body without the declaration has none.
|
|
41
|
+
function problems(body: string): ReadonlyArray<string> {
|
|
42
|
+
if (!is_target(body)) return []
|
|
43
|
+
if (!markdown_section.has_line(body, HEADING)) return [`missing heading: ${HEADING}`]
|
|
44
|
+
const section = markdown_section.section_lines(body, HEADING).join('\n')
|
|
45
|
+
const lines = markdown_section.unfenced_lines(section)
|
|
46
|
+
|
|
47
|
+
return ITEM_LABELS.filter((label) => !has_item(lines, label)).map(
|
|
48
|
+
(label) => `missing expected-effect item: - ${label}: <value>`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const expected_effect_lint = {
|
|
53
|
+
DECLARATION_LINE,
|
|
54
|
+
HEADING,
|
|
55
|
+
ITEM_LABELS,
|
|
56
|
+
problems,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { expected_effect_lint }
|