@joshuafolkken/kit 1.1453.0 → 1.1471.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/skills/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +87 -148
- package/.claude/skills/epic-commands/epic-bundle.md +45 -77
- package/.claude/skills/epic-commands/execution-waves.md +44 -81
- package/.claude/skills/verify-ui/SKILL.md +25 -50
- package/.claude/skills/workflow-commands/background-commands.md +36 -47
- package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
- package/.claude/skills/workflow-commands/backlogrun-handoff-report.md +1 -1
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
- package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
- package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
- package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
- package/.claude/skills/workflow-commands/chain-rule.md +24 -34
- package/.claude/skills/workflow-commands/followup.md +57 -76
- package/.claude/skills/workflow-commands/halfrun.md +2 -1
- package/.claude/skills/workflow-commands/issue-comments.md +43 -60
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- package/.claude/skills/workflow-commands/observation-filing.md +62 -107
- package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
- package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
- package/.claude/skills/workflow-commands/prerequisite.md +10 -15
- package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
- package/.claude/skills/workflow-commands/prrun.md +2 -1
- package/.claude/skills/workflow-commands/retrospective.md +31 -50
- package/.claude/skills/workflow-commands/split-assessment.md +34 -58
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
- package/CLAUDE.md +7 -6
- package/README.md +2 -9
- package/dist/CLAUDE.md +7 -6
- package/dist/hooks/batch-guard.js +6 -6
- package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
- package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
- package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
- package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
- package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
- package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
- package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
- package/dist/hooks/chunk-SHGPANFB.js +56 -0
- package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
- package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
- package/dist/hooks/codex-hook-adapter.js +13 -13
- package/dist/hooks/duplicate-read-guard.js +7 -7
- package/dist/hooks/format-edited.js +3 -3
- package/dist/hooks/inputs.json +251 -247
- package/dist/hooks/investigation-guard.js +7 -7
- package/dist/hooks/pretool-guard.js +12 -12
- package/dist/hooks/session-lang.js +6 -5
- package/dist/hooks/stop-guard.js +75 -18
- package/dist/josh.js +6 -7
- package/dist/managed-marker/index.js +1 -2
- package/dist/version/index.js +15 -12
- package/eslint/base.js +2 -1
- package/eslint/rules/unicorn.js +16 -0
- package/package.json +3 -3
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +26 -44
- package/prompts/collaboration-workflow/gh-rest.md +20 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +7 -13
- package/prompts/collaboration-workflow/issue-template.md +29 -29
- package/prompts/collaboration-workflow/operating-rules.md +35 -44
- package/prompts/collaboration-workflow/output-bounds.md +15 -15
- package/prompts/collaboration-workflow/overview.md +14 -19
- package/prompts/collaboration-workflow/plan-comment.md +13 -13
- package/prompts/collaboration-workflow/principles.md +50 -53
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +32 -49
- package/prompts/collaboration-workflow/residency.md +10 -10
- package/prompts/collaboration-workflow/rule-delivery.md +9 -9
- package/prompts/collaboration-workflow/shell-body.md +11 -21
- package/prompts/collaboration-workflow/turn-batching.md +8 -14
- package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
- package/prompts/collaboration-workflow/wip-cap.md +24 -30
- package/prompts/collaboration-workflow.md +24 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +111 -141
- package/prompts/sonar-hotspot-handling.md +22 -50
- package/prompts/testing-guide.md +48 -99
- package/scripts/adopt/adopt-toolkits.ts +2 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- package/scripts/agent/codex-agent-argv.ts +7 -2
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-drive-carried.ts +26 -0
- package/scripts/backlog/backlog-drive-cli.ts +10 -5
- package/scripts/backlog/backlog-drive-named.ts +2 -4
- package/scripts/backlog/backlog-drive-restore.ts +19 -1
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-ready.ts +13 -6
- package/scripts/behavior/behavior-rules.ts +3 -2
- package/scripts/cost-runtime/codex-usage.ts +4 -2
- package/scripts/cost-runtime/cost-cli.ts +1 -2
- package/scripts/delegation/investigation-guard.ts +1 -1
- package/scripts/delegation/investigation-reads.ts +1 -1
- package/scripts/discovery/repo-discovery.ts +4 -2
- package/scripts/discovery/repo-origin.ts +2 -4
- package/scripts/doctor/doctor-logic.ts +6 -3
- package/scripts/document/document-byte-budget.ts +27 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/entry-read-budget.ts +2 -2
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- package/scripts/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- package/scripts/gate/verification-gate.ts +7 -2
- package/scripts/gh/git-pr-ai-review.ts +12 -1
- package/scripts/gh/git-pr-confirmation.ts +11 -7
- package/scripts/gh/git-pr.ts +6 -2
- package/scripts/git/git-common-directory.ts +3 -3
- package/scripts/git/git-diff-reads.ts +16 -9
- package/scripts/git/git-execa-error.ts +1 -2
- package/scripts/hooks/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +23 -29
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- package/scripts/issue/issue-scout-cli.ts +3 -2
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- package/scripts/josh/josh-commands-workflow.ts +1 -1
- package/scripts/lane/lane-await.ts +3 -2
- package/scripts/lane/lane-child-invocation.ts +14 -1
- package/scripts/lane/lane-guard-policy.ts +4 -4
- package/scripts/lane/lane-ledger.ts +9 -0
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +2 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +8 -3
- package/scripts/lane/lane-session-broker.ts +29 -5
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- package/scripts/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/confirmation-options.ts +59 -0
- package/scripts/notify/notify-opt-in.ts +33 -0
- package/scripts/notify/telegram-notify.ts +25 -8
- package/scripts/notify/telegram-test-logic.ts +33 -1
- package/scripts/notify/telegram-test.ts +4 -2
- package/scripts/overrides/lockfile-overrides.ts +1 -3
- package/scripts/propagate/propagate.ts +1 -2
- package/scripts/release/release-progress.ts +3 -2
- package/scripts/report/report-lint.ts +7 -4
- package/scripts/review/review-record-cli.ts +6 -2
- package/scripts/rules/destructive-command.ts +6 -9
- package/scripts/rules/direct-filing.ts +3 -7
- package/scripts/rules/early-heartbeat.ts +7 -18
- package/scripts/rules/file-body.ts +6 -13
- package/scripts/rules/filing-cap.ts +3 -6
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +5 -15
- package/scripts/rules/josh-git-bare.ts +3 -6
- package/scripts/rules/lane-background.ts +10 -21
- package/scripts/rules/lane-carry-conflict.ts +7 -17
- package/scripts/rules/lane-interactive-ask.ts +6 -11
- package/scripts/rules/lane-park.ts +5 -13
- package/scripts/rules/lane-split-park.ts +2 -7
- package/scripts/rules/lane-switch-main.ts +9 -12
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/piped-verification.ts +81 -31
- package/scripts/rules/poll-loop.ts +7 -17
- package/scripts/rules/pre-gate-cut.ts +7 -21
- package/scripts/rules/prior-comment-read.ts +7 -5
- package/scripts/rules/protected-files.ts +5 -9
- package/scripts/rules/raw-field-body.ts +3 -7
- package/scripts/rules/rule-body-guard.ts +4 -11
- package/scripts/rules/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +7 -8
- package/scripts/rules/shell-segments.ts +7 -0
- package/scripts/rules/stop-rules.ts +17 -29
- package/scripts/rules/test-declared-commit.ts +4 -10
- package/scripts/rules/third-party-write.ts +5 -10
- package/scripts/rules/worktree-guard.ts +5 -12
- package/scripts/run/board/run-board-github.ts +11 -3
- package/scripts/run/board/run-board-idle.ts +31 -0
- package/scripts/run/board/run-board-read.ts +9 -12
- package/scripts/run/board/run-board-reason.ts +7 -3
- package/scripts/run/board/run-board-state.ts +4 -0
- package/scripts/run/board/run-board-status.ts +18 -3
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +32 -7
- package/scripts/run/carry/run-carry-args.ts +1 -3
- package/scripts/run/carry/run-carry-conversation.ts +7 -2
- package/scripts/run/cut/run-cut-cli.ts +15 -20
- package/scripts/run/cut/run-cut-decline.ts +72 -0
- package/scripts/run/cut/run-cut-ready.ts +74 -0
- package/scripts/run/cut/run-cut-report.ts +13 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- package/scripts/run/entry/run-entry-stop.ts +6 -3
- package/scripts/run/hold/run-hold-cli.ts +14 -3
- package/scripts/run/hold/run-hold.ts +6 -3
- package/scripts/run/merge/run-merge-cli.ts +30 -7
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- package/scripts/run/merge/run-merge.ts +3 -2
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-stop-notify.ts +3 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- package/scripts/run/ship/run-ship-review-steps.ts +13 -4
- package/scripts/run/ship/run-ship-review.ts +15 -2
- package/scripts/run/tidy/run-tidy-cli.ts +3 -3
- package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
- package/scripts/safe-chain/project-config.ts +7 -2
- package/scripts/sync/sync-hook-safety.ts +8 -2
- package/scripts/test/unit-worker-share.ts +3 -2
- package/scripts/time-runtime/time-batch-guard.ts +3 -3
- package/scripts/time-runtime/time-hook-transcript.ts +3 -2
- package/scripts/time-runtime/time-shell.ts +10 -0
- package/scripts/version/effective-upstream.ts +3 -4
- package/scripts/version/kit-version-config.ts +8 -2
- package/scripts/version/latest-corepack.ts +1 -2
- package/scripts/version/release-hold.ts +11 -4
- package/scripts/version/version-commands.ts +12 -4
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -7,7 +7,9 @@ import type { GuardRun } from '#scripts/josh/hook-decision'
|
|
|
7
7
|
import { lane_child_marker, type MarkerSource } from '#scripts/lane/lane-child-marker'
|
|
8
8
|
import { lane_paths } from '#scripts/lane/lane-paths'
|
|
9
9
|
import { run_cut, type RunCut } from '#scripts/run/cut/run-cut'
|
|
10
|
+
import { run_cut_decline } from '#scripts/run/cut/run-cut-decline'
|
|
10
11
|
import { run_cut_handoff } from '#scripts/run/cut/run-cut-handoff'
|
|
12
|
+
import { run_cut_ready } from '#scripts/run/cut/run-cut-ready'
|
|
11
13
|
import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
|
|
12
14
|
import { bash_triggers } from './bash-triggers'
|
|
13
15
|
import { implementation_cut_verdict } from './implementation-cut-verdict'
|
|
@@ -62,6 +64,16 @@ interface LaneCostState {
|
|
|
62
64
|
// held outside a lane is told apart from a person's tree.
|
|
63
65
|
held_issue: () => string | undefined
|
|
64
66
|
verdict: () => CostVerdict
|
|
67
|
+
// Whether `run:cut --impl` could take the cut for this issue now — see `can_cut_sync`.
|
|
68
|
+
can_cut: (issue: string) => boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// **The guard asks for a cut only where `run:cut --impl` could take one** (joshuafolkken/kit#3881).
|
|
72
|
+
// A run held on the default branch, or one whose cut was already answered `not-a-lane` / `unready`
|
|
73
|
+
// under this hold, can only be refused again, so each refusal was a round trip that cut nothing. The
|
|
74
|
+
// decline record is a file read and comes first; the tree read spawns git and comes second.
|
|
75
|
+
function can_cut_sync(issue: string): boolean {
|
|
76
|
+
return !run_cut_decline.is_declined_sync(issue) && run_cut_ready.is_ready_sync()
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
function current_state(): LaneCostState {
|
|
@@ -75,6 +87,7 @@ function current_state(): LaneCostState {
|
|
|
75
87
|
// the window collapses one turn's burst of edits to a single whole-transcript price.
|
|
76
88
|
verdict: (): CostVerdict =>
|
|
77
89
|
implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict()),
|
|
90
|
+
can_cut: can_cut_sync,
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -140,14 +153,19 @@ function warrants_the_cut(state: LaneCostState): boolean {
|
|
|
140
153
|
// **The unmeasurable direction matches the pre-gate cut**. A session that
|
|
141
154
|
// cannot be priced reads as warranting the cut, the same safety-net direction `pre-gate-cut.ts`'s
|
|
142
155
|
// `warrants_the_cut` takes (`verdict !== UNDER_VERDICT`): the two rules read one statistic against one
|
|
143
|
-
// threshold, so they must not disagree about whether an unmeasurable session is due a cut.
|
|
156
|
+
// threshold, so they must not disagree about whether an unmeasurable session is due a cut. Whether the
|
|
157
|
+
// cut could be taken is asked only once it is warranted, so an under-threshold edit spawns no git.
|
|
144
158
|
function is_over_threshold_edit(
|
|
145
159
|
call: GuardedCall,
|
|
146
160
|
state: LaneCostState = current_state(),
|
|
147
161
|
): boolean {
|
|
148
|
-
if (!is_edit_tool(call.name)
|
|
162
|
+
if (!is_edit_tool(call.name)) return false
|
|
163
|
+
|
|
164
|
+
const issue = uncut_run_issue(state)
|
|
165
|
+
|
|
166
|
+
if (issue === undefined) return false
|
|
149
167
|
|
|
150
|
-
return warrants_the_cut(state)
|
|
168
|
+
return warrants_the_cut(state) && state.can_cut(issue)
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
// **The threshold in the refusal text is assembled from the constant, never spelled**.
|
|
@@ -155,36 +173,14 @@ function is_over_threshold_edit(
|
|
|
155
173
|
// building it from `CONTEXT_CUT_THRESHOLD` keeps the two from drifting. `cost_format.format_tokens` renders it the way every cost report does.
|
|
156
174
|
const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
|
|
157
175
|
|
|
158
|
-
// The instruction in the shape a refusal can carry:
|
|
159
|
-
// the
|
|
160
|
-
//
|
|
161
|
-
// read which of the others leave it implementing — the same reason `pre-gate-cut.ts` spells them out.
|
|
176
|
+
// The instruction in the shape a refusal can carry: the handoff in the format the parser reads, the
|
|
177
|
+
// cut, and the one verdict that ends the turn — `cut` is the only one, so a run told merely to "take
|
|
178
|
+
// the cut" is not left to look it up.
|
|
162
179
|
const IMPLEMENTATION_CUT_REASON =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`
|
|
166
|
-
'
|
|
167
|
-
`First write a handoff file at \`${run_cut_handoff.HANDOFF_PATH}\` (ignored, so it is never committed) with the Write tool — the user’s instruction verbatim, what you have ` +
|
|
168
|
-
'completed, what remains, and what you deliberately did not touch, as ' +
|
|
169
|
-
`${run_cut_handoff.HANDOFF_FORMAT} (Markdown is refused) — and pass it as \`--handoff <path>\`, ` +
|
|
170
|
-
'so the fresh process resumes on the original instruction rather than the working tree alone ' +
|
|
171
|
-
'(joshuafolkken/kit#2354); a resume that finds no instruction is refused `incomplete` rather than ' +
|
|
172
|
-
'continuing blind. ' +
|
|
173
|
-
'`pnpm josh run:cut --impl <N> --handoff <path>` ends this process and relaunches a fresh one that resumes back into ' +
|
|
174
|
-
'implementation (`resume-impl`), dropping the accumulated context rather than carrying it — ' +
|
|
175
|
-
'joshuafolkken/kit#1933 built the boundary and joshuafolkken/kit#2310 measured it firing 0 times ' +
|
|
176
|
-
'because the verdict was only ever read at session entry, where the context has not yet grown. The ' +
|
|
177
|
-
"verdict is `pnpm josh cost --cut`'s exactly — the same per-request billed-input statistic against the " +
|
|
178
|
-
'same threshold, never a second measurement. Issue `pnpm josh run:cut --impl <N> --handoff <path>` now and read the ' +
|
|
179
|
-
'verdict: on `cut`, read what the command printed. Where it relaunched a fresh process, **end the turn ' +
|
|
180
|
-
'immediately** — that process owns the run and continues implementing, so it must not be waited for, ' +
|
|
181
|
-
'and **no Telegram is owed**: a cut that resumes itself leaves a person nothing to act on. Where it printed that nothing was relaunched, do what it prints, then end the turn; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
|
|
182
|
-
'`unknown`, this process carries the run on and the edit is simply the next call. Never relaunch a ' +
|
|
183
|
-
'second process after `busy`. This fired because the dispatch mark names this lane or the run hold ' +
|
|
184
|
-
'names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The ' +
|
|
185
|
-
'procedure is `.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue this edit once the cut has ' +
|
|
186
|
-
'answered — an edit reissued right after this refusal passes, so `busy` / `failed` cannot wedge the ' +
|
|
187
|
-
'run edit after edit; this fires again on the next threshold crossing rather than once per run.'
|
|
180
|
+
`⛔ implementation-phase cut: refused on every edit past the ${THRESHOLD_TEXT} line. ` +
|
|
181
|
+
`Write \`${run_cut_handoff.HANDOFF_PATH}\` as ${run_cut_handoff.HANDOFF_FORMAT}, run ` +
|
|
182
|
+
'`pnpm josh run:cut --impl <N> --handoff <path>`; `cut`: end the turn immediately, no Telegram ' +
|
|
183
|
+
'is owed; else reissue the edit (`.claude/skills/workflow-commands/pre-gate-cut.md`).'
|
|
188
184
|
|
|
189
185
|
// **A refusal younger than this window is this edit's own refusal being answered**.
|
|
190
186
|
// A `decide` row that refused every over-threshold edit would wedge a run whose cut came back `busy` /
|
|
@@ -201,7 +197,8 @@ function is_reissued_refusal(now_ms: number, delivered_at_ms: number): boolean {
|
|
|
201
197
|
// silenced the row for the rest of a process the moment its first refusal landed — and a `busy` /
|
|
202
198
|
// `failed` / `unready` verdict, or an edit reissued unchanged, left the context to grow unwatched to
|
|
203
199
|
// 282,747 tokens with the cut never taken. `decide` re-asks on every over-threshold edit; the reissue
|
|
204
|
-
// window
|
|
200
|
+
// window keeps that from wedging a run whose cut came back `busy` / `failed`, and a run that cannot
|
|
201
|
+
// cut at all never reaches here — `can_cut_sync` keeps the trigger silent for it. `delivered_at_ms` is this
|
|
205
202
|
// row's own last refusal, written by `fire_once` only when a refusal actually fired, so a passed
|
|
206
203
|
// reissue does not move it.
|
|
207
204
|
function decide(
|
|
@@ -213,8 +210,8 @@ function decide(
|
|
|
213
210
|
return !is_reissued_refusal(run.now_ms, delivered_at_ms)
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` fires it per threshold crossing
|
|
217
|
-
// `keeps` is the implementation cut command, read from a `Bash` call the same
|
|
213
|
+
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` fires it per threshold crossing
|
|
214
|
+
// in a run that can cut; `keeps` is the implementation cut command, read from a `Bash` call the same
|
|
218
215
|
// way `pre-gate-cut`'s is.
|
|
219
216
|
const ROW = {
|
|
220
217
|
id: 'implementation-cut',
|
|
@@ -222,6 +219,11 @@ const ROW = {
|
|
|
222
219
|
reason: IMPLEMENTATION_CUT_REASON,
|
|
223
220
|
decide,
|
|
224
221
|
keeps: bash_triggers.on_bash_command(takes_the_impl_cut),
|
|
222
|
+
// The opening the reason carried before it was cut to the action and the next command.
|
|
223
|
+
former_reasons: [
|
|
224
|
+
'⛔ implementation-phase cut: this checkout is a lane dispatched',
|
|
225
|
+
"⛔ implementation-phase cut: this run's context crossed the",
|
|
226
|
+
],
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
const implementation_cut = {
|
|
@@ -51,13 +51,10 @@ function is_index_mutation(command: string): boolean {
|
|
|
51
51
|
|
|
52
52
|
const INDEX_MUTATION_REASON =
|
|
53
53
|
'⛔ git index mutation: `git add` / `stage` / `commit` / `reset` / `rm` / `mv` and `git restore ' +
|
|
54
|
-
"--staged` overwrite the user's staged snapshot
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'A commit goes through `pnpm josh git -y`; any other staging needs explicit current-turn user ' +
|
|
59
|
-
'instruction, run by the user in their own terminal. **This rule fires on every occurrence, not once ' +
|
|
60
|
-
'per run.**'
|
|
54
|
+
"--staged` overwrite the user's staged snapshot. Commit with `pnpm josh git -y`; any other staging " +
|
|
55
|
+
"is the user's, on explicit instruction. It fires on every occurrence " +
|
|
56
|
+
'(`prompts/collaboration-workflow/operating-rules.md` → ' +
|
|
57
|
+
'"no-self-staging").'
|
|
61
58
|
|
|
62
59
|
const ROW = {
|
|
63
60
|
id: 'index-mutation',
|
|
@@ -89,21 +89,11 @@ function reads_issue_comments(command: string): boolean {
|
|
|
89
89
|
// the fix. The label keeps `time_transcript_line.guard_from_refusal` naming this guard, not the command.
|
|
90
90
|
const ISSUE_COMMENTS_REASON =
|
|
91
91
|
'⛔ issue comments: pnpm josh issue:read <N>\n' +
|
|
92
|
-
"Run that in place of this read
|
|
93
|
-
'the
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
'diagnosis (joshuafolkken/kit#1537), a changed default and an added acceptance criterion ' +
|
|
98
|
-
'(joshuafolkken/kit#1520), a scope handed to another Issue (joshuafolkken/kit#1304) — and ' +
|
|
99
|
-
'nothing in the body says it was superseded, so a body-only reader builds the wrong thing and ' +
|
|
100
|
-
'sees no contradiction. ' +
|
|
101
|
-
'Then the later text is the agreement in force — a comment supersedes the body it contradicts — ' +
|
|
102
|
-
"except for two answers that are not the run's to make: work a comment reassigns to another " +
|
|
103
|
-
'Issue is out of scope and is not implemented, and a comment saying the Issue no longer has a ' +
|
|
104
|
-
'reason to exist stops the run with a `confirmation` Telegram. The procedure is ' +
|
|
105
|
-
'`.claude/skills/workflow-commands/issue-comments.md`. ' +
|
|
106
|
-
'Every body-only read is refused until the comments are read.'
|
|
92
|
+
"Run that in place of this read — an Issue's comments are part of it, and a later comment " +
|
|
93
|
+
'supersedes the body it contradicts; refused until that command is on the transcript. Without ' +
|
|
94
|
+
'josh, read ' +
|
|
95
|
+
'`gh api repos/{owner}/{repo}/issues/<N>/comments` beside the body ' +
|
|
96
|
+
'(`.claude/skills/workflow-commands/issue-comments.md`).'
|
|
107
97
|
|
|
108
98
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry. `already_satisfied` stands it
|
|
109
99
|
// down once the comments are on the transcript tail, and until then it refuses every body-only read.
|
|
@@ -52,12 +52,9 @@ function is_bare_josh_git(command: string): boolean {
|
|
|
52
52
|
// sibling reasons do.
|
|
53
53
|
const JOSH_GIT_BARE_REASON =
|
|
54
54
|
'⛔ bare `pnpm josh git`: this prompts to confirm the staging, and with no TTY the prompt falls to ' +
|
|
55
|
-
'its cancel branch
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'`pnpm josh git -y "<title> #<N>"`. The recovery form that opens the PR for a push that already ' +
|
|
59
|
-
'landed is `pnpm josh git -y --skip-commit --skip-push`. This rule fires on every occurrence, not ' +
|
|
60
|
-
'once per run.'
|
|
55
|
+
'its cancel branch. Run `pnpm josh git -y "<title> #<N>"`, or ' +
|
|
56
|
+
'`pnpm josh git -y --skip-commit --skip-push` to open the PR for a push that already landed. It ' +
|
|
57
|
+
'fires on every occurrence.'
|
|
61
58
|
|
|
62
59
|
// The row itself, so `delivered-rules.ts` spreads one entry. `is_trigger` reads the input through
|
|
63
60
|
// `on_bash_command` — the `Bash` tool-name gate every command row shares — and `decide` returns true so
|
|
@@ -109,9 +109,9 @@ function foreground_input(
|
|
|
109
109
|
call: GuardedCall,
|
|
110
110
|
is_lane_child: () => boolean = () => lane_child_marker.is_child_of(process.cwd()),
|
|
111
111
|
): Record<string, unknown> | undefined {
|
|
112
|
-
if (!is_backgrounded_ship(call) || !json_value.is_record(call.input))
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
if (!is_backgrounded_ship(call) || !json_value.is_record(call.input) || !is_lane_child()) {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
115
|
|
|
116
116
|
return { ...call.input, [BACKGROUND_KEY]: false, [TIMEOUT_KEY]: FOREGROUND_TIMEOUT_MS }
|
|
117
117
|
}
|
|
@@ -119,7 +119,7 @@ function foreground_input(
|
|
|
119
119
|
const FOREGROUND_NOTE =
|
|
120
120
|
'lane background: this lane child backgrounded `josh ship`, which would die with the headless turn ' +
|
|
121
121
|
'before its supervisor exists — so it was run in the foreground instead, where it detaches itself ' +
|
|
122
|
-
'after the preflight. End the turn on `launched` / `busy
|
|
122
|
+
'after the preflight. End the turn on `launched` / `busy`.'
|
|
123
123
|
|
|
124
124
|
// The launches `launched_ids` reads off the transcript tail that never saw their finish. A launch is
|
|
125
125
|
// read off its own tool result and a finish off the harness notice, both by `time_transcript_line`.
|
|
@@ -149,26 +149,15 @@ function pending_agent_ids(tail: string): ReadonlyArray<string> {
|
|
|
149
149
|
|
|
150
150
|
const LANE_BACKGROUND_REASON =
|
|
151
151
|
'⛔ lane background: this session is a dispatched lane child (`JOSH_LANE_CHILD`), a headless ' +
|
|
152
|
-
'
|
|
153
|
-
'`
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'supervisor instead: `pnpm josh ship --detach --review "<title> #<N>"` (add `--cite <N>`) issued in ' +
|
|
157
|
-
'the foreground — its preflight runs in this process before the supervisor exists, so a backgrounded ' +
|
|
158
|
-
'`ship` dies with the turn (joshuafolkken/kit#3027; a backgrounded `ship` issued alone is moved to ' +
|
|
159
|
-
'the foreground for you, joshuafolkken/kit#3154) — then end the turn on `launched` / `busy`. ' +
|
|
160
|
-
'`.claude/skills/workflow-commands/chain-rule.md` step 0 is the single source. A ' +
|
|
161
|
-
'single check you must read before continuing runs in the foreground within the tool timeout; the ' +
|
|
162
|
-
'commit-push step does not — in a child it too belongs to the detached ship, in either spelling. This ' +
|
|
163
|
-
'rule fires on every occurrence, not once per run.'
|
|
152
|
+
'process whose background tasks die when the turn ends. Hand over to ' +
|
|
153
|
+
'`pnpm josh ship --detach --review "<title> #<N>"`, issued in the foreground, then end the turn on ' +
|
|
154
|
+
'`launched` / `busy`. It fires on every occurrence ' +
|
|
155
|
+
'(`.claude/skills/workflow-commands/chain-rule.md` step 0).'
|
|
164
156
|
|
|
165
157
|
const LANE_BACKGROUND_STOP_REASON =
|
|
166
158
|
'⛔ lane background stop: this is a dispatched lane child (`JOSH_LANE_CHILD`) and a background task ' +
|
|
167
|
-
'it launched has not finished
|
|
168
|
-
'
|
|
169
|
-
'in the foreground and act on its result, or re-issue the work through `pnpm josh ship --detach`, ' +
|
|
170
|
-
'whose supervisor outlives the turn (`.claude/skills/workflow-commands/chain-rule.md` step 0). Do ' +
|
|
171
|
-
'not repeat your previous reply.'
|
|
159
|
+
'it launched has not finished, and ending this turn kills it. Wait for it in the foreground, or ' +
|
|
160
|
+
're-issue the work through `pnpm josh ship --detach` — do not notify, stop or repeat your reply.'
|
|
172
161
|
|
|
173
162
|
// The row itself, so `delivered-rules.ts` spreads one entry. It fires on every occurrence (`decide`
|
|
174
163
|
// returns true): a child must never background these, so the route is always the detached ship, never
|
|
@@ -89,25 +89,15 @@ function is_carry_conflict(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// The instruction in the shape a refusal can carry: what the child cannot do, why the record is not a
|
|
92
|
-
// conflict, and the mechanical branch to take instead — continue, never park or ask.
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// avoided so the single-quoted literal needs no escaping, as the sibling reasons do.
|
|
92
|
+
// conflict, and the mechanical branch to take instead — continue, never park or ask. Its history is
|
|
93
|
+
// `docs/maintainers/rule-delivery-rationale.md` → "Why a refusal is the action and the next command".
|
|
94
|
+
// Apostrophes are avoided so the single-quoted literal needs no escaping, as the sibling reasons do.
|
|
96
95
|
const LANE_CARRY_CONFLICT_REASON =
|
|
97
96
|
'⛔ lane child carry conflict: this session is a dispatched lane child, and ' +
|
|
98
|
-
'`
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'parent (joshuafolkken/kit#2267, measured 2026-09-21 in backlogrun #2252 — a child read run:merge ' +
|
|
103
|
-
'`busy` as a competitor and parked #2258 unimplemented). Do not run this command, and do not treat ' +
|
|
104
|
-
'it as a conflict: no `needs-decision`, no `confirmation` Telegram, no question to a person — a ' +
|
|
105
|
-
'dispatched child cannot ask, and there is nothing to decide. Continue implementing this Issue; the ' +
|
|
106
|
-
'parent owns the budget and counts the outcome at the return. The procedure is ' +
|
|
107
|
-
'`.claude/skills/workflow-commands/pre-gate-cut.md` and `backlogrun-progress.md` → "The hand-off". ' +
|
|
108
|
-
'Live evidence of `run:carry` is taken with a fresh `JOSH_TEMP_ROOT` prefixed to the call, which this ' +
|
|
109
|
-
'rule lets through (joshuafolkken/kit#3458); `run:merge` is refused under any root, because it also ' +
|
|
110
|
-
'syncs main, closes lanes and writes to GitHub. This rule fires on every occurrence, not once per run.'
|
|
97
|
+
'`run:merge` / `run:carry` are the parent budget commands; the live record belongs to the parent, ' +
|
|
98
|
+
'not to a competing run. Continue implementing this Issue: no `needs-decision`, no Telegram. ' +
|
|
99
|
+
'Live `run:carry` evidence takes a fresh `JOSH_TEMP_ROOT`; `run:merge` is refused under any root, ' +
|
|
100
|
+
'on every occurrence.'
|
|
111
101
|
|
|
112
102
|
// The row itself, so `delivered-rules.ts` spreads one entry. `is_trigger` reads the input through
|
|
113
103
|
// `on_bash_command` — the `Bash` tool-name gate every command row shares — and `decide` returns true
|
|
@@ -41,17 +41,12 @@ function is_interactive_ask(
|
|
|
41
41
|
// `backlogrun-park.md` → "park and continue", and a second copy here would be the clone `CLAUDE.md`
|
|
42
42
|
// prohibits. The one command is spelled so the child is not left to reconstruct it from a doc read.
|
|
43
43
|
const LANE_INTERACTIVE_ASK_REASON =
|
|
44
|
-
'⛔ lane child interactive ask: this session is a dispatched lane child and cannot ask a
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
'`
|
|
49
|
-
|
|
50
|
-
'`pnpm josh issue:comment <N> --body-file <path>` — then send the ' +
|
|
51
|
-
'`confirmation` Telegram and stop. The procedure is ' +
|
|
52
|
-
'`.claude/skills/workflow-commands/pre-gate-cut.md` → "A lane child records its park before it stops", ' +
|
|
53
|
-
'and the park itself is `backlogrun-park.md` → "park and continue". This rule fires on every ' +
|
|
54
|
-
'occurrence, not once per run.'
|
|
44
|
+
'⛔ lane child interactive ask: this session is a dispatched lane child and cannot ask a ' +
|
|
45
|
+
'person, so park instead: ' +
|
|
46
|
+
"`gh api repos/{owner}/{repo}/issues/<N>/labels -f 'labels[]=needs-decision'`, " +
|
|
47
|
+
'`pnpm josh issue:comment <N> --body-file <path>`, ' +
|
|
48
|
+
'the `confirmation` Telegram, stop. It fires on every occurrence ' +
|
|
49
|
+
'(`.claude/skills/workflow-commands/pre-gate-cut.md`, `backlogrun-park.md`).'
|
|
55
50
|
|
|
56
51
|
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
|
|
57
52
|
// occurrence; it declares no `keeps` — not asking is the absence of a call, not a call, so the row is
|
|
@@ -108,19 +108,11 @@ function records_the_park(command: string): boolean {
|
|
|
108
108
|
// single source is `backlogrun-park.md` → "park and continue", and a second copy here would be the
|
|
109
109
|
// clone `CLAUDE.md` prohibits.
|
|
110
110
|
const LANE_PARK_REASON =
|
|
111
|
-
'⛔ lane child park: this session is a dispatched lane child,
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'the parent to guess from a log. Before you notify and stop: apply `needs-decision` and post a ' +
|
|
117
|
-
'comment carrying the question, the options, and whether work was stashed — ' +
|
|
118
|
-
"`gh api repos/{owner}/{repo}/issues/<N>/labels -f 'labels[]=needs-decision'` then " +
|
|
119
|
-
'`pnpm josh issue:comment <N> --body-file <path>`. If this stop is a ' +
|
|
120
|
-
'`needs-human-review` or `already-done` one, that label is already on the Issue and there is nothing ' +
|
|
121
|
-
'to add. Then reissue this notify — it fires once per run and cannot repeat on the call in hand. The ' +
|
|
122
|
-
'procedure is `.claude/skills/workflow-commands/pre-gate-cut.md` → "A lane child records its park ' +
|
|
123
|
-
'before it stops", and the park itself is `backlogrun-park.md` → "park and continue".'
|
|
111
|
+
'⛔ lane child park: this session is a dispatched lane child, so a stop for a person parks first: ' +
|
|
112
|
+
"`gh api repos/{owner}/{repo}/issues/<N>/labels -f 'labels[]=needs-decision'`; " +
|
|
113
|
+
'`pnpm josh issue:comment <N> --body-file <path>` (question, options, stash); a ' +
|
|
114
|
+
'`needs-human-review` / `already-done` stop already is. Reissue this notify, once per run ' +
|
|
115
|
+
'(`.claude/skills/workflow-commands/pre-gate-cut.md`).'
|
|
124
116
|
|
|
125
117
|
// The row itself, so `delivered-rules.ts` spreads one entry. **Once per run rather than `decide`, and
|
|
126
118
|
// no `already_satisfied`.** The stop ends the turn, so it is not the recurring act `decide` is for. It carries no `already_satisfied` because the park it asks for is a GitHub label a
|
|
@@ -101,13 +101,8 @@ function decide(_call: unknown, run: GuardRun): boolean {
|
|
|
101
101
|
|
|
102
102
|
const LANE_SPLIT_PARK_REASON =
|
|
103
103
|
'⛔ lane split park: this session is a dispatched lane child that promoted its own Issue to an epic ' +
|
|
104
|
-
'this run, and a split is not a park
|
|
105
|
-
'
|
|
106
|
-
'continues through its children; a question here has nobody to answer it (joshuafolkken/kit#3296, ' +
|
|
107
|
-
'measured 2026-10-06 when the child for #3255 parked its promoted epic). If what remains of the ' +
|
|
108
|
-
'original work needs a person, park that child issue instead, then end the turn. The procedure is ' +
|
|
109
|
-
'`.claude/skills/workflow-commands/backlogrun-park.md` → "Splitting a child mid-run". This rule fires ' +
|
|
110
|
-
'on every occurrence, not once per run.'
|
|
104
|
+
'this run, and a split is not a park: no `needs-decision` on the epic, no `confirmation` Telegram, ' +
|
|
105
|
+
'on every occurrence. Park a child issue if the rest needs a person, else end the turn (`.claude/skills/workflow-commands/backlogrun-park.md` → "Splitting a child mid-run").'
|
|
111
106
|
|
|
112
107
|
// It declares no `keeps`: not parking is the absence of a call, so the row is reported unmeasured
|
|
113
108
|
// rather than scored on an act that does not exist (`lane-carry-conflict.ts`).
|
|
@@ -61,8 +61,12 @@ const FLAG_PREFIX = '-'
|
|
|
61
61
|
function switch_target(segment: string): string | undefined {
|
|
62
62
|
const call = git_argv.parse(segment)
|
|
63
63
|
|
|
64
|
-
if (
|
|
65
|
-
|
|
64
|
+
if (
|
|
65
|
+
call?.subcommand !== SWITCH_SUBCOMMAND ||
|
|
66
|
+
call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))
|
|
67
|
+
) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
66
70
|
|
|
67
71
|
return call.args.find((argument) => !argument.startsWith(FLAG_PREFIX))
|
|
68
72
|
}
|
|
@@ -103,16 +107,9 @@ function is_switch_away_from_lane(
|
|
|
103
107
|
// prohibits. Apostrophes are avoided so the single-quoted literal needs no escaping, as the siblings do.
|
|
104
108
|
const LANE_SWITCH_MAIN_REASON =
|
|
105
109
|
'⛔ lane child on `git switch main`: this session is a dispatched lane child, and switching to the ' +
|
|
106
|
-
'default branch here always fails
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'switch: the lane is already on its own issue branch, branched from a fresh default before it opened, ' +
|
|
110
|
-
'and the latest default is brought in during the gate by `pnpm josh main:merge`. So skip this step ' +
|
|
111
|
-
'and keep implementing on the current branch — no `git switch`, no `git pull`. `pnpm josh main:sync` ' +
|
|
112
|
-
'(`josh ms`) is refused in a lane for the same reason, and a lane is finished with `pnpm josh ' +
|
|
113
|
-
'lane:close <issue-number>`. The parent runs `pnpm josh ms` in the primary checkout ' +
|
|
114
|
-
'before each lane opens, never the child — `backlogrun-lanes.md` and `backlogrun-child.md` are the ' +
|
|
115
|
-
'single sources. This rule fires on every occurrence, not once per run.'
|
|
110
|
+
'default branch here always fails (`already used by worktree`: the primary checkout holds it). Skip ' +
|
|
111
|
+
'this step and keep implementing on the current branch; `pnpm josh main:merge` brings the default ' +
|
|
112
|
+
'in during the gate. Refused on every occurrence (`backlogrun-lanes.md`, `backlogrun-child.md`).'
|
|
116
113
|
|
|
117
114
|
// The row itself, so `delivered-rules.ts` spreads one entry. `is_trigger` reads the input through
|
|
118
115
|
// `on_bash_command` — the `Bash` tool-name gate every command row shares — and `decide` returns true so
|
|
@@ -48,11 +48,10 @@ function command_line(oracle: Oracle): string {
|
|
|
48
48
|
// the single source of the procedure.
|
|
49
49
|
function reason_for(oracle: Oracle, firing_point: FiringPoint): string {
|
|
50
50
|
return (
|
|
51
|
-
`⛔ decision oracle not consulted: this is ${firing_point.describes}
|
|
52
|
-
`\`${command_line(oracle)}\` first —
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
`the transcript.`
|
|
51
|
+
`⛔ decision oracle not consulted: this is ${firing_point.describes}. Run ` +
|
|
52
|
+
`\`${command_line(oracle)}\` first — it decides ${oracle.decision.toLowerCase()} and answers ` +
|
|
53
|
+
`${oracle.vocabulary.join(' | ')} (${oracle.single_source}). Every such call is refused until ` +
|
|
54
|
+
`that command is on the transcript.`
|
|
56
55
|
)
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -73,20 +73,16 @@ function is_masked_verification(command: string): boolean {
|
|
|
73
73
|
return time_shell.discarded_commands(command).some((segment) => is_verification_command(segment))
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// The masking
|
|
76
|
+
// The masking and the ways out of it, in the shape a refusal can carry; the boundary is the topic file's. **The way
|
|
77
77
|
// out travels with the refusal rather than being named**, because a delivery saying only "do not pipe
|
|
78
78
|
// it" leaves the caller with the same long output and no sanctioned way to read it, which is what put
|
|
79
79
|
// the pipe there in the first place.
|
|
80
80
|
const PIPED_VERIFICATION_REASON =
|
|
81
81
|
"⛔ piped verification: a pipeline exits with its last command's status, so `pnpm josh gate | " +
|
|
82
|
-
'tail` reports success on a gate that failed
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
"the check's status. Read the printed verdict either way, never the exit code alone. Read-only " +
|
|
87
|
-
'listings are untouched — this fires only on a command whose result means pass or fail. The rule ' +
|
|
88
|
-
'is in `prompts/collaboration-workflow/output-bounds.md`. Reissue this call with no pipe — it ' +
|
|
89
|
-
'fires once per run and cannot repeat on the call in hand.'
|
|
82
|
+
'tail` reports success on a gate that failed. Reissue this call with no pipe (josh prints its ' +
|
|
83
|
+
'verdict last), or redirect it to a file and read ranges from that file, or prefix ' +
|
|
84
|
+
'`set -o pipefail`; read the verdict line, never the exit code alone ' +
|
|
85
|
+
'(`prompts/collaboration-workflow/output-bounds.md`).'
|
|
90
86
|
|
|
91
87
|
// Every command position on the line: the chain cut `shell_segments` makes, each piece of a pipeline
|
|
92
88
|
// inside it taken on its own.
|
|
@@ -129,26 +125,34 @@ function keeps_verdict_intact(command: string): boolean {
|
|
|
129
125
|
// `grep` stopping at its first match — closes the pipe on a check still writing, and under `pipefail` the
|
|
130
126
|
// check's SIGPIPE turns a passing run into exit 141; `head` also cuts the verdict line josh prints last.
|
|
131
127
|
//
|
|
132
|
-
// **
|
|
133
|
-
//
|
|
134
|
-
// `
|
|
128
|
+
// **Each masked check is wrapped on its own, and the rest of the line runs as typed.** Most of the
|
|
129
|
+
// refusals left after #3570 were a check chained beside other work — `vitest …; pnpm josh lint:related
|
|
130
|
+
// | tail -5; git status` — so the check's own pipeline becomes `(set -o pipefail; …)` and nothing else
|
|
131
|
+
// on the line is put under a `pipefail` it was not written for (joshuafolkken/kit#3875). The envelope
|
|
132
|
+
// carries no permission decision (`hook_decision.flow_rewrite_envelope`), so whatever else the line runs
|
|
133
|
+
// meets its own permission prompt exactly as the typed call would have.
|
|
135
134
|
//
|
|
136
|
-
// **The
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
135
|
+
// **The check's own pipeline is read for what the cut cannot see.** `parts_of` does not split on a lone
|
|
136
|
+
// `&`, so `| tail & curl …`, `| tail > ~/.zshrc` and `| tail -n $(cmd)` look like a filter after a check.
|
|
137
|
+
// Any `&`, redirection, backtick or `$(` there beyond the check's own `2>&1` leaves the line to the
|
|
138
|
+
// refusal, as does a heredoc anywhere on it, whose body lines the cut would read as commands. An
|
|
139
|
+
// unquoted `(` or `)` does too: the cut splits inside a group, so `(cd x && pnpm josh lint) | tail` would
|
|
140
|
+
// wrap the inner check and leave the group's own pipe without `pipefail`.
|
|
141
|
+
const PIPEFAIL_OPEN = '(set -o pipefail; '
|
|
142
|
+
const PIPEFAIL_CLOSE = ')'
|
|
143
|
+
const COMMENT_START = /(?:^|\s)#/u
|
|
141
144
|
const FULL_READING_FILTER = /^(?:tail|grep)\b/u
|
|
142
145
|
const EARLY_EXIT_GREP =
|
|
143
146
|
/^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u
|
|
144
147
|
|
|
145
148
|
const ALLOWED_OPERATORS = /2>&1|&&/gu
|
|
146
149
|
const SIDE_EFFECT_SYNTAX = /[&<>`]|\$\(/u
|
|
150
|
+
const GROUP_SYNTAX = /[()]/u
|
|
147
151
|
|
|
148
152
|
const PIPEFAIL_NOTE =
|
|
149
|
-
'↻ piped verification: the josh check was piped, so
|
|
150
|
-
"
|
|
151
|
-
'alone — `prompts/collaboration-workflow/output-bounds.md
|
|
153
|
+
'↻ piped verification: the josh check was piped, so its pipeline was wrapped in ' +
|
|
154
|
+
"`(set -o pipefail; …)` to carry the check's status, and the rest of the line ran as typed. Read " +
|
|
155
|
+
'the printed verdict, never the exit code alone — `prompts/collaboration-workflow/output-bounds.md`.'
|
|
152
156
|
|
|
153
157
|
function reads_to_the_end(piece: string): boolean {
|
|
154
158
|
return FULL_READING_FILTER.test(piece) && !EARLY_EXIT_GREP.test(piece)
|
|
@@ -164,23 +168,69 @@ function has_side_effect_syntax(command: string): boolean {
|
|
|
164
168
|
return SIDE_EFFECT_SYNTAX.test(command.replaceAll(ALLOWED_OPERATORS, ' '))
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
171
|
+
// The side-effect syntax is read on the typed part, since a double-quoted `$(cmd)` still runs; the pipe
|
|
172
|
+
// cut and the grouping are read on the blanked one, since a quoted `|` or `(` is text.
|
|
173
|
+
function is_rewritable_part(raw: string, judged: string): boolean {
|
|
174
|
+
const pipeline = judged.split('|').map((piece) => piece.trim())
|
|
175
|
+
|
|
176
|
+
return !has_side_effect_syntax(raw) && !GROUP_SYNTAX.test(judged) && is_filtered_check(pipeline)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Where the part's code ends: an unquoted `#` opens a comment, which would swallow the closing `)`.
|
|
180
|
+
function code_end(judged: string): number {
|
|
181
|
+
const comment = COMMENT_START.exec(judged)
|
|
182
|
+
|
|
183
|
+
return comment === null ? judged.length : comment.index
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The part with its surrounding whitespace and any trailing comment kept outside the subshell, so the
|
|
187
|
+
// line reads as typed.
|
|
188
|
+
function wrapped(raw: string, judged: string): string {
|
|
189
|
+
const code = raw.slice(0, code_end(judged))
|
|
190
|
+
const core = code.trim()
|
|
191
|
+
const lead = code.slice(0, code.indexOf(core))
|
|
192
|
+
|
|
193
|
+
return `${lead}${PIPEFAIL_OPEN}${core}${PIPEFAIL_CLOSE}${raw.slice(lead.length + core.length)}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// One part of the line as it will run: as typed where it masks nothing, wrapped where it masks a check a
|
|
197
|
+
// wrapper can carry, and `undefined` where it masks one no wrapper can.
|
|
198
|
+
function rewritten_part(raw: string, judged: string): string | undefined {
|
|
199
|
+
if (!is_masked_verification(judged)) return raw
|
|
200
|
+
|
|
201
|
+
return is_rewritable_part(raw, judged) ? wrapped(raw, judged) : undefined
|
|
173
202
|
}
|
|
174
203
|
|
|
175
|
-
// The
|
|
204
|
+
// The command with every masked check wrapped, judged on the quote-blanked line and cut from the typed
|
|
205
|
+
// one at the same offsets — a `|` inside `grep -E "a|b"` is text, and the typed line keeps its quotes.
|
|
206
|
+
function pipefail_command(command: string): string | undefined {
|
|
207
|
+
const judged = time_shell.blanked(command)
|
|
208
|
+
|
|
209
|
+
if (judged === undefined || !is_masked_verification(command)) return undefined
|
|
210
|
+
|
|
211
|
+
const offset = { at: 0 }
|
|
212
|
+
const parts = shell_segments.parts_of(judged).map((part) => {
|
|
213
|
+
const raw = command.slice(offset.at, offset.at + part.length)
|
|
214
|
+
|
|
215
|
+
offset.at += part.length
|
|
216
|
+
|
|
217
|
+
return rewritten_part(raw, part)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const rewritten = parts.includes(undefined) ? undefined : parts.join('')
|
|
221
|
+
|
|
222
|
+
// A line masked as a whole but in no part wraps nothing, so no note may claim a wrap.
|
|
223
|
+
return rewritten === command ? undefined : rewritten
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The call's input with each masked check wrapped, or `undefined` where the call is not a rewritable
|
|
227
|
+
// masking.
|
|
176
228
|
function pipefail_input(call: GuardedCall): Record<string, unknown> | undefined {
|
|
177
229
|
if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return undefined
|
|
178
230
|
|
|
179
|
-
const command = time_shell.bash_command(call.input)
|
|
180
|
-
|
|
181
|
-
if (!is_pipefail_rewritable(command)) return undefined
|
|
231
|
+
const command = pipefail_command(time_shell.bash_command(call.input))
|
|
182
232
|
|
|
183
|
-
return { ...call.input, command
|
|
233
|
+
return command === undefined ? undefined : { ...call.input, command }
|
|
184
234
|
}
|
|
185
235
|
|
|
186
236
|
const piped_verification = {
|