@joshuafolkken/kit 1.1455.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/dependency-update/SKILL.md +26 -55
- package/.claude/skills/epic-commands/SKILL.md +83 -156
- package/.claude/skills/epic-commands/epic-bundle.md +51 -79
- package/.claude/skills/epic-commands/execution-waves.md +43 -81
- package/.claude/skills/verify-ui/SKILL.md +24 -50
- package/.claude/skills/workflow-commands/SKILL.md +7 -9
- package/.claude/skills/workflow-commands/background-commands.md +31 -46
- package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
- package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
- package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
- package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
- package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
- package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
- package/.claude/skills/workflow-commands/backlogrun.md +31 -50
- package/.claude/skills/workflow-commands/chain-rule.md +31 -40
- 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 +55 -77
- 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 +43 -60
- package/.claude/skills/workflow-commands/issue-scout.md +3 -2
- package/.claude/skills/workflow-commands/kickoff.md +65 -74
- 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 +59 -118
- package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
- package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
- package/.claude/skills/workflow-commands/prerequisite.md +11 -17
- package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
- package/.claude/skills/workflow-commands/prrun.md +3 -5
- package/.claude/skills/workflow-commands/retrospective.md +35 -54
- package/.claude/skills/workflow-commands/split-assessment.md +32 -58
- package/.claude/skills/workflow-commands/target-repository.md +1 -2
- package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
- 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 +7 -6
- package/cspell/index.yaml +7 -0
- package/dist/CLAUDE.md +7 -6
- 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-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
- package/dist/hooks/chunk-7DSBJVFH.js +50 -0
- package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
- package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
- package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
- package/dist/hooks/chunk-GP4IEC4O.js +20 -0
- package/dist/hooks/chunk-JP45SOCJ.js +56 -0
- package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
- package/dist/hooks/chunk-M4HGZV7U.js +83 -0
- package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
- package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
- package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
- package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
- package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
- package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
- package/dist/hooks/chunk-UPUZF3AN.js +279 -0
- package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
- 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 +302 -247
- package/dist/hooks/investigation-guard.js +8 -11
- package/dist/hooks/pretool-guard.js +16 -13
- package/dist/hooks/session-lang.js +372 -26
- package/dist/hooks/stop-guard.js +2593 -189
- package/dist/josh.js +2128 -282
- 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/lefthook/base.yml +5 -3
- package/package.json +14 -12
- package/prompts/coding-standards.md +12 -22
- package/prompts/collaboration-workflow/file-edits.md +25 -45
- package/prompts/collaboration-workflow/gh-rest.md +19 -36
- package/prompts/collaboration-workflow/glossary.md +5 -7
- package/prompts/collaboration-workflow/issue-citation.md +6 -14
- package/prompts/collaboration-workflow/issue-template.md +40 -32
- package/prompts/collaboration-workflow/lane-instructions.md +35 -0
- package/prompts/collaboration-workflow/operating-rules.md +33 -46
- 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 +57 -54
- package/prompts/collaboration-workflow/proposal-request.md +1 -9
- package/prompts/collaboration-workflow/report-format.md +41 -49
- package/prompts/collaboration-workflow/residency.md +15 -11
- package/prompts/collaboration-workflow/rule-delivery.md +7 -15
- package/prompts/collaboration-workflow/shell-body.md +10 -20
- 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 +26 -32
- package/prompts/collaboration-workflow.md +25 -25
- package/prompts/refactoring.md +33 -43
- package/prompts/review-rubric.md +84 -129
- package/prompts/review.md +110 -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-argv.ts +20 -4
- package/scripts/agent/agent-exit-record.ts +23 -1
- 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 +9 -2
- package/scripts/agent/lane-instructions.ts +144 -0
- package/scripts/backlog/backlog-budget-cli.ts +1 -2
- package/scripts/backlog/backlog-budget.ts +4 -4
- 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 +3 -5
- package/scripts/backlog/backlog-drive-restore.ts +28 -2
- package/scripts/backlog/backlog-drive-seats.ts +27 -0
- package/scripts/backlog/backlog-drive.ts +11 -6
- package/scripts/backlog/backlog-occupied.ts +56 -0
- package/scripts/backlog/backlog-ready.ts +59 -15
- 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/duplicate-read-guard.ts +2 -2
- package/scripts/delegation/investigation-guard.ts +15 -65
- package/scripts/delegation/investigation-reads.ts +1 -2
- 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 +28 -27
- package/scripts/document/document-byte-check.ts +1 -2
- package/scripts/document/document-section-cli.ts +17 -5
- package/scripts/document/entry-read-budget.ts +3 -3
- package/scripts/document/read-set-cli.ts +4 -1
- package/scripts/epic/epic-audit-orphans.ts +5 -8
- package/scripts/epic/epic-audit-rationale.ts +8 -2
- package/scripts/epic/epic-audit.ts +52 -1
- package/scripts/epic/epic-bundle-cli.ts +1 -2
- 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/epic/epic-chains.ts +6 -2
- package/scripts/epic/epic-close-comment.ts +1 -2
- 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 +65 -127
- 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 +19 -3
- 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/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/format-edited-file.ts +10 -4
- package/scripts/hooks/pre-commit-lint.ts +91 -0
- package/scripts/hooks/pre-push-unit.ts +15 -4
- package/scripts/hooks/pretool-guard.ts +48 -79
- package/scripts/hooks/reserved-run.ts +1 -0
- package/scripts/hooks/run-hook.sh +10 -12
- package/scripts/hooks/step-zero-notice.ts +3 -2
- package/scripts/hooks/stop-actions.ts +204 -0
- package/scripts/hooks/stop-guard.ts +62 -21
- package/scripts/init/hook-command-rewrite.ts +6 -2
- package/scripts/init/hook-launch.ts +12 -7
- package/scripts/init/init-logic-deploy-vps.ts +4 -2
- package/scripts/issue/behavior-change-lint.ts +12 -6
- 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 +22 -16
- package/scripts/issue/markdown-section.ts +19 -5
- package/scripts/josh/command-usage.ts +110 -0
- package/scripts/josh/file-map-stamp.ts +1 -3
- package/scripts/josh/hook-decision.ts +17 -2
- 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-await.ts +3 -2
- package/scripts/lane/lane-capacity.ts +8 -2
- package/scripts/lane/lane-check-stats.ts +56 -0
- package/scripts/lane/lane-child-invocation.ts +49 -5
- package/scripts/lane/lane-dispatch.ts +3 -3
- package/scripts/lane/lane-guard-policy.ts +5 -5
- package/scripts/lane/lane-handoff.ts +8 -1
- package/scripts/lane/lane-install.ts +12 -4
- package/scripts/lane/lane-ledger.ts +52 -1
- package/scripts/lane/lane-limit-cli.ts +8 -9
- package/scripts/lane/lane-load.ts +9 -2
- package/scripts/lane/lane-measure-cli.ts +4 -0
- package/scripts/lane/lane-parked.ts +18 -0
- package/scripts/lane/lane-preserve.ts +29 -1
- package/scripts/lane/lane-process-load.ts +125 -0
- package/scripts/lane/lane-process-stats.ts +93 -0
- package/scripts/lane/lane-registry.ts +17 -10
- package/scripts/lane/lane-restart.ts +5 -26
- package/scripts/lane/lane-session-action.ts +146 -0
- package/scripts/lane/lane-session-broker.ts +68 -125
- package/scripts/lane/lane-spent.ts +14 -3
- package/scripts/lane/lane-stats.ts +1 -0
- 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/managed-marker/managed-marker-logic.ts +1 -2
- package/scripts/notify/telegram-test.ts +10 -5
- 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-format-reference.ts +2 -0
- package/scripts/report/report-lint.ts +7 -4
- 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/review/review-record-cli.ts +6 -2
- package/scripts/rules/agent-model.ts +59 -0
- package/scripts/rules/delivered-rules.ts +11 -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 +4 -6
- package/scripts/rules/firing-point.ts +6 -5
- package/scripts/rules/git-force.ts +5 -9
- package/scripts/rules/guard-remedy.ts +117 -0
- package/scripts/rules/implementation-cut.ts +37 -35
- package/scripts/rules/index-guard.ts +4 -7
- package/scripts/rules/issue-comments.ts +67 -16
- 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/last-prompt.ts +50 -5
- package/scripts/rules/oracle-consulted.ts +4 -5
- package/scripts/rules/oracle-list-cli.ts +7 -0
- package/scripts/rules/piped-verification.ts +84 -32
- 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/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/run-tail-rule.ts +6 -22
- package/scripts/rules/shell-body-trigger.ts +4 -6
- package/scripts/rules/shell-segments.ts +22 -0
- package/scripts/rules/stop-decision.ts +77 -0
- package/scripts/rules/stop-rules.ts +164 -77
- package/scripts/rules/tail-commands.ts +15 -1
- 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-cli.ts +2 -0
- package/scripts/run/board/run-board-closed.ts +8 -2
- package/scripts/run/board/run-board-github.ts +19 -3
- package/scripts/run/board/run-board-header.ts +4 -1
- package/scripts/run/board/run-board-idle.ts +31 -0
- 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 +25 -12
- package/scripts/run/board/run-board-reason.ts +17 -4
- 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 +9 -0
- package/scripts/run/board/run-board-status.ts +89 -23
- package/scripts/run/board/run-board-thanks.ts +7 -2
- package/scripts/run/board/run-board-tick.ts +37 -8
- 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 +18 -22
- 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 +29 -0
- package/scripts/run/cut/run-cut.ts +10 -1
- 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-cli.ts +14 -3
- package/scripts/run/hold/run-hold-session.ts +46 -0
- package/scripts/run/hold/run-hold.ts +13 -4
- package/scripts/run/launch-priority.ts +32 -0
- package/scripts/run/merge/run-merge-cli.ts +16 -42
- package/scripts/run/merge/run-merge-ship.ts +59 -1
- 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 +22 -9
- package/scripts/run/run-ending.ts +31 -3
- package/scripts/run/run-liveness.ts +1 -1
- package/scripts/run/run-parent-cut-hook.ts +5 -10
- package/scripts/run/run-poll-hook.ts +157 -0
- package/scripts/run/run-preflight.ts +1 -2
- package/scripts/run/run-step-cli.ts +22 -15
- package/scripts/run/run-step-section.ts +85 -0
- package/scripts/run/run-stop-notify.ts +1 -2
- package/scripts/run/run-watcher-guard.ts +6 -3
- 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 +25 -14
- package/scripts/run/ship/run-ship-review.ts +15 -2
- 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/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/test-e2e-guard.ts +30 -5
- package/scripts/test/test-unit-guard.ts +16 -1
- package/scripts/test/unit-worker-share.ts +26 -3
- 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 +39 -4
- package/scripts/time-runtime/time-spans.ts +1 -0
- package/scripts/time-runtime/time-tool-call.ts +7 -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/templates/gitignore +6 -2
- package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
- package/dist/hooks/chunk-UT7YC3N2.js +0 -56
|
@@ -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.
|
|
@@ -128,27 +124,37 @@ function keeps_verdict_intact(command: string): boolean {
|
|
|
128
124
|
// **Only a filter that reads its input to the end qualifies.** One that exits early — `head`, or a
|
|
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.
|
|
127
|
+
// `cut`, `sort`, `uniq` and `wc` read to the end as well, and a `grep … | cut -c1-250` narrowing was the
|
|
128
|
+
// commonest masking still refused after #3875 (joshuafolkken/kit#3920).
|
|
131
129
|
//
|
|
132
|
-
// **
|
|
133
|
-
//
|
|
134
|
-
// `
|
|
130
|
+
// **Each masked check is wrapped on its own, and the rest of the line runs as typed.** Most of the
|
|
131
|
+
// refusals left after #3570 were a check chained beside other work — `vitest …; pnpm josh lint:related
|
|
132
|
+
// | tail -5; git status` — so the check's own pipeline becomes `(set -o pipefail; …)` and nothing else
|
|
133
|
+
// on the line is put under a `pipefail` it was not written for (joshuafolkken/kit#3875). The envelope
|
|
134
|
+
// carries no permission decision (`hook_decision.flow_rewrite_envelope`), so whatever else the line runs
|
|
135
|
+
// meets its own permission prompt exactly as the typed call would have.
|
|
135
136
|
//
|
|
136
|
-
// **The
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
// **The check's own pipeline is read for what the cut cannot see.** `parts_of` does not split on a lone
|
|
138
|
+
// `&`, so `| tail & curl …`, `| tail > ~/.zshrc` and `| tail -n $(cmd)` look like a filter after a check.
|
|
139
|
+
// Any `&`, redirection, backtick or `$(` there beyond the check's own `2>&1` leaves the line to the
|
|
140
|
+
// refusal, as does a heredoc anywhere on it, whose body lines the cut would read as commands. An
|
|
141
|
+
// unquoted `(` or `)` does too: the cut splits inside a group, so `(cd x && pnpm josh lint) | tail` would
|
|
142
|
+
// wrap the inner check and leave the group's own pipe without `pipefail`.
|
|
143
|
+
const PIPEFAIL_OPEN = '(set -o pipefail; '
|
|
144
|
+
const PIPEFAIL_CLOSE = ')'
|
|
145
|
+
const COMMENT_START = /(?:^|\s)#/u
|
|
146
|
+
const FULL_READING_FILTER = /^(?:tail|grep|cut|sort|uniq|wc)\b/u
|
|
142
147
|
const EARLY_EXIT_GREP =
|
|
143
148
|
/^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u
|
|
144
149
|
|
|
145
150
|
const ALLOWED_OPERATORS = /2>&1|&&/gu
|
|
146
151
|
const SIDE_EFFECT_SYNTAX = /[&<>`]|\$\(/u
|
|
152
|
+
const GROUP_SYNTAX = /[()]/u
|
|
147
153
|
|
|
148
154
|
const PIPEFAIL_NOTE =
|
|
149
|
-
'↻ piped verification: the josh check was piped, so
|
|
150
|
-
"
|
|
151
|
-
'alone — `prompts/collaboration-workflow/output-bounds.md
|
|
155
|
+
'↻ piped verification: the josh check was piped, so its pipeline was wrapped in ' +
|
|
156
|
+
"`(set -o pipefail; …)` to carry the check's status, and the rest of the line ran as typed. Read " +
|
|
157
|
+
'the printed verdict, never the exit code alone — `prompts/collaboration-workflow/output-bounds.md`.'
|
|
152
158
|
|
|
153
159
|
function reads_to_the_end(piece: string): boolean {
|
|
154
160
|
return FULL_READING_FILTER.test(piece) && !EARLY_EXIT_GREP.test(piece)
|
|
@@ -164,23 +170,69 @@ function has_side_effect_syntax(command: string): boolean {
|
|
|
164
170
|
return SIDE_EFFECT_SYNTAX.test(command.replaceAll(ALLOWED_OPERATORS, ' '))
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
+
// The side-effect syntax is read on the typed part, since a double-quoted `$(cmd)` still runs; the pipe
|
|
174
|
+
// cut and the grouping are read on the blanked one, since a quoted `|` or `(` is text.
|
|
175
|
+
function is_rewritable_part(raw: string, judged: string): boolean {
|
|
176
|
+
const pipeline = judged.split('|').map((piece) => piece.trim())
|
|
177
|
+
|
|
178
|
+
return !has_side_effect_syntax(raw) && !GROUP_SYNTAX.test(judged) && is_filtered_check(pipeline)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Where the part's code ends: an unquoted `#` opens a comment, which would swallow the closing `)`.
|
|
182
|
+
function code_end(judged: string): number {
|
|
183
|
+
const comment = COMMENT_START.exec(judged)
|
|
184
|
+
|
|
185
|
+
return comment === null ? judged.length : comment.index
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The part with its surrounding whitespace and any trailing comment kept outside the subshell, so the
|
|
189
|
+
// line reads as typed.
|
|
190
|
+
function wrapped(raw: string, judged: string): string {
|
|
191
|
+
const code = raw.slice(0, code_end(judged))
|
|
192
|
+
const core = code.trim()
|
|
193
|
+
const lead = code.slice(0, code.indexOf(core))
|
|
194
|
+
|
|
195
|
+
return `${lead}${PIPEFAIL_OPEN}${core}${PIPEFAIL_CLOSE}${raw.slice(lead.length + core.length)}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// One part of the line as it will run: as typed where it masks nothing, wrapped where it masks a check a
|
|
199
|
+
// wrapper can carry, and `undefined` where it masks one no wrapper can.
|
|
200
|
+
function rewritten_part(raw: string, judged: string): string | undefined {
|
|
201
|
+
if (!is_masked_verification(judged)) return raw
|
|
202
|
+
|
|
203
|
+
return is_rewritable_part(raw, judged) ? wrapped(raw, judged) : undefined
|
|
173
204
|
}
|
|
174
205
|
|
|
175
|
-
// The
|
|
206
|
+
// The command with every masked check wrapped, judged on the quote-blanked line and cut from the typed
|
|
207
|
+
// one at the same offsets — a `|` inside `grep -E "a|b"` is text, and the typed line keeps its quotes.
|
|
208
|
+
function pipefail_command(command: string): string | undefined {
|
|
209
|
+
const judged = time_shell.blanked(command)
|
|
210
|
+
|
|
211
|
+
if (judged === undefined || !is_masked_verification(command)) return undefined
|
|
212
|
+
|
|
213
|
+
const offset = { at: 0 }
|
|
214
|
+
const parts = shell_segments.parts_of(judged).map((part) => {
|
|
215
|
+
const raw = command.slice(offset.at, offset.at + part.length)
|
|
216
|
+
|
|
217
|
+
offset.at += part.length
|
|
218
|
+
|
|
219
|
+
return rewritten_part(raw, part)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const rewritten = parts.includes(undefined) ? undefined : parts.join('')
|
|
223
|
+
|
|
224
|
+
// A line masked as a whole but in no part wraps nothing, so no note may claim a wrap.
|
|
225
|
+
return rewritten === command ? undefined : rewritten
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The call's input with each masked check wrapped, or `undefined` where the call is not a rewritable
|
|
229
|
+
// masking.
|
|
176
230
|
function pipefail_input(call: GuardedCall): Record<string, unknown> | undefined {
|
|
177
231
|
if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return undefined
|
|
178
232
|
|
|
179
|
-
const command = time_shell.bash_command(call.input)
|
|
180
|
-
|
|
181
|
-
if (!is_pipefail_rewritable(command)) return undefined
|
|
233
|
+
const command = pipefail_command(time_shell.bash_command(call.input))
|
|
182
234
|
|
|
183
|
-
return { ...call.input, command
|
|
235
|
+
return command === undefined ? undefined : { ...call.input, command }
|
|
184
236
|
}
|
|
185
237
|
|
|
186
238
|
const piped_verification = {
|
|
@@ -83,25 +83,15 @@ function is_poll_loop(command: string): boolean {
|
|
|
83
83
|
return LOOP_KEYWORD.test(plain) && SLEEP_COMMAND.test(plain)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// The instruction in the shape a refusal can carry:
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// The instruction in the shape a refusal can carry: why the command's exit is the thing to wait on,
|
|
87
|
+
// and the one route that delivers that exit. The measurement behind it is in
|
|
88
|
+
// `docs/maintainers/rule-delivery-rationale.md`.
|
|
89
89
|
const POLL_LOOP_REASON =
|
|
90
90
|
"⛔ output poll loop: this hand-writes a `while`/`until` wait loop over a command's output, a " +
|
|
91
|
-
'marker file or a process
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'(joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do ' +
|
|
96
|
-
'not poll the output file. A command known to take minutes — `pnpm josh gate`, `pnpm josh ' +
|
|
97
|
-
'followup`, `pnpm josh format` — is started detached from the first with `run_in_background` set, ' +
|
|
98
|
-
'and the harness re-invokes you when it exits: its completion notification is what ends the wait, ' +
|
|
99
|
-
'on the task exit rather than a regex or the block limit. So issue the long command in the ' +
|
|
100
|
-
'background and end the turn — do not arm a loop; the wake-up brings you back. To glance at ' +
|
|
101
|
-
'interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, ' +
|
|
102
|
-
'`pnpm josh followup` waits on it for you. The procedure is ' +
|
|
103
|
-
'`.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is ' +
|
|
104
|
-
'issued in the background". **This rule fires on every occurrence, not once per run.**'
|
|
91
|
+
'marker file or a process, and the loop does not know when the command finished. Issue the long ' +
|
|
92
|
+
'command with `run_in_background` set and end the turn — its exit re-invokes you; for CI, ' +
|
|
93
|
+
'`pnpm josh followup` (`.claude/skills/workflow-commands/background-commands.md`). It fires on ' +
|
|
94
|
+
'every occurrence.'
|
|
105
95
|
|
|
106
96
|
// The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
|
|
107
97
|
// occurrence; it declares no `keeps` — waiting on the task exit is the *absence* of a loop, not a
|
|
@@ -133,9 +133,7 @@ function dispatched_issue(state: LaneCutState): string | undefined {
|
|
|
133
133
|
function uncut_lane_issue(state: LaneCutState): string | undefined {
|
|
134
134
|
const issue = lane_paths.lane_issue_of(state.directory)
|
|
135
135
|
|
|
136
|
-
if (issue === undefined) return undefined
|
|
137
|
-
|
|
138
|
-
if (dispatched_issue(state) !== issue) return undefined
|
|
136
|
+
if (issue === undefined || dispatched_issue(state) !== issue) return undefined
|
|
139
137
|
|
|
140
138
|
return state.carried()?.issue === issue ? undefined : issue
|
|
141
139
|
}
|
|
@@ -159,26 +157,14 @@ function is_uncut_gate(command: string, state: LaneCutState = current_state()):
|
|
|
159
157
|
return warrants_the_cut(state.context_verdict())
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
// The instruction in the shape a refusal can carry:
|
|
163
|
-
// the reissue sentence every delivery needs.
|
|
164
|
-
//
|
|
165
|
-
// to go and read which of the other five leave it holding the run — the same reason the WIP cap's
|
|
166
|
-
// delivery names its three interrupt tests instead of naming the file they live in.
|
|
160
|
+
// The instruction in the shape a refusal can carry: the cut, the one verdict that ends the turn, and
|
|
161
|
+
// the reissue sentence every delivery needs. `cut` is the only verdict that ends the turn, so the
|
|
162
|
+
// refusal says so rather than leaving a run to look up which of the others leave it holding the run.
|
|
167
163
|
const PRE_GATE_CUT_REASON =
|
|
168
164
|
'⛔ pre-gate cut: this checkout is a lane dispatched for this issue and the gate is the boundary to ' +
|
|
169
|
-
'cut at
|
|
170
|
-
'
|
|
171
|
-
'
|
|
172
|
-
'176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step ' +
|
|
173
|
-
'itself firing 0 times in 6 lane children while it was carried as prose. Issue ' +
|
|
174
|
-
'`pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** — the ' +
|
|
175
|
-
'fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, ' +
|
|
176
|
-
'`busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next ' +
|
|
177
|
-
'call. Never relaunch a second process after `busy`. This fired because the dispatch mark names ' +
|
|
178
|
-
'this lane; a person working here carries no mark and sees no refusal, so there is no ' +
|
|
179
|
-
'human-or-child judgement left to make. The procedure is ' +
|
|
180
|
-
'`.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue the gate once the cut has answered — ' +
|
|
181
|
-
'this fires once per run, so it cannot repeat on the call in hand.'
|
|
165
|
+
'cut at. Issue `pnpm josh run:cut <N>`: on `cut`, end the turn immediately — no Telegram is owed; ' +
|
|
166
|
+
'on any other verdict (`not-a-lane`, `busy`, …), reissue the gate — this fires once per run ' +
|
|
167
|
+
'(`.claude/skills/workflow-commands/pre-gate-cut.md`).'
|
|
182
168
|
|
|
183
169
|
// **The pre-gate cut is two acts, one call apart**, so its `rule:value`
|
|
184
170
|
// denominator is only the runs that reached the boundary: a run that issues the cut's entry check and,
|
|
@@ -15,20 +15,22 @@ import { tail_commands } from './tail-commands'
|
|
|
15
15
|
// **It reads comments in the josh spelling too.** `pnpm josh issue:read <N>` prints the body and every
|
|
16
16
|
// comment in one call, so a run that opened an Issue with it has read the comments as surely as one
|
|
17
17
|
// that passed `--comments` — and that spelling is the one the workflow itself reaches for, so leaving
|
|
18
|
-
// it out would refuse the reader that obeyed the rule most directly.
|
|
18
|
+
// it out would refuse the reader that obeyed the rule most directly. **`run:entry <N>` and `run:prep <N>`
|
|
19
|
+
// count the same way**: both print the bundled report that carries the body and every comment, and a
|
|
20
|
+
// run that opened with one was refused for a read it had made (joshuafolkken/kit#3875).
|
|
19
21
|
|
|
20
22
|
// The path segment `…/issues/<N>` and `…/issues/<N>/comments` both name the Issue in the same capture.
|
|
21
23
|
const ISSUE_PATH_NUMBER = /issues\/(\d+)/gu
|
|
22
24
|
// The positional after `gh issue view`, past any flags that precede it.
|
|
23
25
|
const ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u
|
|
24
|
-
// The run of numbers
|
|
25
|
-
const ISSUE_READ_ARGUMENTS = /\
|
|
26
|
+
// The run of numbers a josh comment-reading command takes as its positional arguments.
|
|
27
|
+
const ISSUE_READ_ARGUMENTS = /\b(?:issue:read|run:entry|run:prep)\b([\s\d]*)/u
|
|
26
28
|
|
|
27
29
|
// A command that prints an Issue's comments: the comments endpoint, `gh issue view … --comments`, or
|
|
28
|
-
//
|
|
30
|
+
// a josh command that reads body and comments together.
|
|
29
31
|
const COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u
|
|
30
32
|
const VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u
|
|
31
|
-
const ISSUE_READ_COMMAND = /\
|
|
33
|
+
const ISSUE_READ_COMMAND = /\b(?:issue:read|run:entry|run:prep)\b/u
|
|
32
34
|
|
|
33
35
|
function add_issue_read_numbers(command: string, numbers: Set<number>): void {
|
|
34
36
|
const match = ISSUE_READ_ARGUMENTS.exec(command)
|
|
@@ -119,21 +119,17 @@ function is_protected_file_call(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const PROTECTED_FILE_REASON =
|
|
122
|
-
'⛔ protected file: `.env
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'in their own process (the tracked `.env.example` and `.env.test` stay readable). ' +
|
|
126
|
-
'`.claude/settings.json` in a ' +
|
|
127
|
-
'consumer repository is the deny list and the hooks every other rule rests on, so a run must not ' +
|
|
128
|
-
'rewrite it; `josh sync` owns it, and a change belongs upstream in kit (`CLAUDE.md` → "Route ' +
|
|
129
|
-
'distributed-doc / config changes upstream to kit"). Leave these files alone, or ask the user. ' +
|
|
130
|
-
'**This rule fires on every occurrence, not once per run.**'
|
|
122
|
+
'⛔ protected file: `.env`, a `.env.*` variant and `.dev.vars` hold secrets, and a consumer ' +
|
|
123
|
+
'`.claude/settings.json` is owned by `josh sync` (a change belongs upstream in kit). Leave these ' +
|
|
124
|
+
'files alone, or ask the user. It fires on every occurrence.'
|
|
131
125
|
|
|
132
126
|
const ROW = {
|
|
133
127
|
id: 'protected-file',
|
|
134
128
|
is_trigger: is_protected_file_call,
|
|
135
129
|
reason: PROTECTED_FILE_REASON,
|
|
136
130
|
decide: (): boolean => true,
|
|
131
|
+
// The opening the reason carried before it was cut to the action and the next command.
|
|
132
|
+
former_reasons: ['⛔ protected file: `.env` holds the Telegram bot token'],
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
const protected_files = { ROW, is_protected_file_call }
|
|
@@ -35,13 +35,9 @@ function posts_at_path_literally(command: string): boolean {
|
|
|
35
35
|
// it reads as unbelievable — the call succeeds and returns a URL while posting the path as text.
|
|
36
36
|
const RAW_FIELD_BODY_REASON =
|
|
37
37
|
'⛔ literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / ' +
|
|
38
|
-
'`--raw-field`), which
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). ' +
|
|
42
|
-
'Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` — the one ' +
|
|
43
|
-
'spelling, which cannot take the wrong flag. The rule and the safe forms are in ' +
|
|
44
|
-
'`prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.'
|
|
38
|
+
'`--raw-field`), which posts the string `@<path>` rather than the file. Post the comment with ' +
|
|
39
|
+
'`pnpm josh issue:comment <N> --body-file <path>`; this rule fires on every occurrence ' +
|
|
40
|
+
'(`prompts/collaboration-workflow/shell-body.md`).'
|
|
45
41
|
|
|
46
42
|
const ROW = {
|
|
47
43
|
id: 'raw-field-body',
|
|
@@ -131,17 +131,10 @@ function writes_rule_prose(
|
|
|
131
131
|
// skips, the commands that answer them, and the reissue once they have run.
|
|
132
132
|
const RULE_BODY_REASON =
|
|
133
133
|
'⛔ rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill ' +
|
|
134
|
-
'body.
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'`
|
|
138
|
-
'in the run driver, not prose — the ordering question; `pnpm josh run:step` is the state machine a ' +
|
|
139
|
-
'sequencing rule moves into. Only a rule that is neither — not computable, not ordering — stays ' +
|
|
140
|
-
'prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The ' +
|
|
141
|
-
'criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); ' +
|
|
142
|
-
'the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is ' +
|
|
143
|
-
'refused until both commands are on the transcript — run them, confirm the rule can be neither an ' +
|
|
144
|
-
'oracle nor an ordering step, then reissue this edit.'
|
|
134
|
+
'body. Answer question 0 (`pnpm josh oracle:list`) and the ordering question (`pnpm josh ' +
|
|
135
|
+
'run:step`) of `prompts/collaboration-workflow/residency.md`; refused until both commands are on ' +
|
|
136
|
+
'the transcript, then reissue this as a trigger-plus-pointer line ' +
|
|
137
|
+
'(`prompts/collaboration-workflow/rule-delivery.md`).'
|
|
145
138
|
|
|
146
139
|
// The two placement oracles the residency questions name: question 0 is `pnpm josh oracle:list`, the
|
|
147
140
|
// ordering question is `pnpm josh run:step` (its degenerate pre-implementation form `run:next` counts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { delivered_rules } from './delivered-rules'
|
|
4
|
+
import { guard_remedy } from './guard-remedy'
|
|
4
5
|
import { rule_registry, type RegisteredRule } from './rule-registry'
|
|
5
6
|
|
|
6
7
|
// `josh rule:list` — the list of trigger-delivered rules, rendered from the guard rows themselves.
|
|
@@ -12,6 +13,7 @@ import { rule_registry, type RegisteredRule } from './rule-registry'
|
|
|
12
13
|
const HEADER =
|
|
13
14
|
'# Trigger-delivered rules — rendered by `pnpm josh rule:list` from `scripts/rules/delivered-rules.ts`'
|
|
14
15
|
const MISSING = '(no entry in `scripts/rules/rule-registry.ts`)'
|
|
16
|
+
const MISSING_WHY = '(no `why` in `scripts/rules/guard-remedy.ts`)'
|
|
15
17
|
|
|
16
18
|
function item(id: string, rule: RegisteredRule | undefined): string {
|
|
17
19
|
if (rule === undefined) return `- \`${id}\` ${MISSING}`
|
|
@@ -20,6 +22,7 @@ function item(id: string, rule: RegisteredRule | undefined): string {
|
|
|
20
22
|
`- **${rule.title}** (\`${id}\`, ${rule.topic})`,
|
|
21
23
|
` - fires: \`${rule.entry}\` — ${rule.fires}`,
|
|
22
24
|
` - silent: ${rule.quiet}`,
|
|
25
|
+
` - not automated: ${guard_remedy.why_of(id) ?? MISSING_WHY}`,
|
|
23
26
|
].join('\n')
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -46,6 +49,6 @@ function orphans(): ReadonlyArray<string> {
|
|
|
46
49
|
|
|
47
50
|
if (process.argv[1] === fileURLToPath(import.meta.url)) process.stdout.write(`${render()}\n`)
|
|
48
51
|
|
|
49
|
-
const rule_list = { MISSING, orphans, render }
|
|
52
|
+
const rule_list = { MISSING, MISSING_WHY, orphans, render }
|
|
50
53
|
|
|
51
54
|
export { rule_list }
|
|
@@ -157,6 +157,16 @@ const GUARD_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
157
157
|
"a dispatched lane child's `AskUserQuestion`; it hands over the park procedure. Every occurrence",
|
|
158
158
|
quiet: 'not a lane child, or not an interactive tool',
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
id: 'agent-model',
|
|
162
|
+
title: 'Subagent model named',
|
|
163
|
+
topic:
|
|
164
|
+
'`.claude/skills/workflow-commands/delegation.md` → "A `general-purpose` subagent names its model"',
|
|
165
|
+
entry: RULE_GUARD,
|
|
166
|
+
fires:
|
|
167
|
+
'an `Agent` / `Task` call of the `general-purpose` type with no `model`. Every occurrence',
|
|
168
|
+
quiet: 'a named agent type, or a `model` set',
|
|
169
|
+
},
|
|
160
170
|
{
|
|
161
171
|
id: 'lane-carry-conflict',
|
|
162
172
|
title: 'Lane child carry commands',
|
|
@@ -45,14 +45,16 @@ const MEASURED_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
45
45
|
},
|
|
46
46
|
]
|
|
47
47
|
|
|
48
|
-
// The `Stop` hook's rows; `stop:guard` judges every stop
|
|
48
|
+
// The `Stop` hook's rows; `stop:guard` judges every stop. A row with a fixed answer runs it itself and
|
|
49
|
+
// blocks only when that fails; a row that needs judgement blocks the stop while it holds.
|
|
49
50
|
const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
50
51
|
{
|
|
51
52
|
id: 'stop-notify',
|
|
52
53
|
title: 'Stop notification',
|
|
53
54
|
topic: '`CLAUDE.md` → "Mid-workflow stop notification"',
|
|
54
55
|
entry: STOP_GUARD,
|
|
55
|
-
fires:
|
|
56
|
+
fires:
|
|
57
|
+
'a stop with the work tree held and no `confirmation` notify that turn. Sends the notify itself; blocks only when the send fails',
|
|
56
58
|
quiet: 'no hold, a `confirmation` notify on the tail, or a pre-gate cut taken',
|
|
57
59
|
},
|
|
58
60
|
{
|
|
@@ -60,7 +62,8 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
60
62
|
title: 'Hold release',
|
|
61
63
|
topic: '`.claude/skills/workflow-commands/working-tree-hold.md`',
|
|
62
64
|
entry: STOP_GUARD,
|
|
63
|
-
fires:
|
|
65
|
+
fires:
|
|
66
|
+
'a stop on a clean tree with the `run:hold` record still in place. Releases the hold itself; blocks only when that fails',
|
|
64
67
|
quiet:
|
|
65
68
|
'a dirty tree (a `halfrun` stop before the commit, a `needs-human-review` stop) or already released',
|
|
66
69
|
},
|
|
@@ -70,7 +73,7 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
|
|
|
70
73
|
topic: '`prompts/collaboration-workflow/issue-citation.md`',
|
|
71
74
|
entry: STOP_GUARD,
|
|
72
75
|
fires:
|
|
73
|
-
'a bare `#N` in the prose of the last reply.
|
|
76
|
+
'a bare `#N` in the prose of the last reply. Prints the citation lines itself; blocks only when a number cannot be cited',
|
|
74
77
|
quiet: 'link form, inside code or a quote, `#N` right after `PR`, or a GitHub-facing artifact',
|
|
75
78
|
},
|
|
76
79
|
{
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { hook_decision } from '#scripts/josh/hook-decision'
|
|
2
|
+
import { json_value } from '#scripts/lib/json-value'
|
|
3
|
+
import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
|
|
4
|
+
import { time_hook_transcript } from '#scripts/time-runtime/time-hook-transcript'
|
|
5
|
+
import { delivered_rules, type DeliveredRule } from './delivered-rules'
|
|
6
|
+
import { guard_remedy } from './guard-remedy'
|
|
7
|
+
|
|
8
|
+
// The call a `PreToolUse` hook runs in place of a mechanically fixable refusal (joshuafolkken/kit#3920).
|
|
9
|
+
// Each row `guard-remedy.ts` classes as a rewrite gets the call in turn, so one line that both pipes a
|
|
10
|
+
// check and reads a body is made right by both rows. The rewrite is the rule guard's delivery, so the
|
|
11
|
+
// rule guard's switch turns it off too.
|
|
12
|
+
|
|
13
|
+
interface RuleRewrite {
|
|
14
|
+
// The payload carrying the rewritten input, which the guards judge in place of the typed one.
|
|
15
|
+
payload: string
|
|
16
|
+
input: Record<string, unknown>
|
|
17
|
+
// The notes of the rows that rewrote, in row order.
|
|
18
|
+
note: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Folded {
|
|
22
|
+
input: Record<string, unknown>
|
|
23
|
+
notes: ReadonlyArray<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const REWRITING_RULES: ReadonlyArray<DeliveredRule> = delivered_rules.DELIVERED_RULES.filter(
|
|
27
|
+
(rule) => guard_remedy.is_rewrite(rule.id) && rule.rewrite !== undefined,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const NOTE_SEPARATOR = '\n\n'
|
|
31
|
+
|
|
32
|
+
// **A tail that cannot be read is an empty one**, as every guard fails open (`hook-decision.ts`): a
|
|
33
|
+
// throw here would end the hook before any guard judged the call.
|
|
34
|
+
function read_payload_tail(raw_payload: string): string {
|
|
35
|
+
try {
|
|
36
|
+
const payload = hook_decision.parse_hook_payload(raw_payload)
|
|
37
|
+
|
|
38
|
+
return payload
|
|
39
|
+
? time_density_hook.read_tail(
|
|
40
|
+
time_hook_transcript.transcript_of(payload.transcript_path, payload.agent_id),
|
|
41
|
+
)
|
|
42
|
+
: ''
|
|
43
|
+
} catch {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tail_reader(raw_payload: string): () => string {
|
|
49
|
+
const cache: { tail?: string } = {}
|
|
50
|
+
|
|
51
|
+
return function tail(): string {
|
|
52
|
+
cache.tail ??= read_payload_tail(raw_payload)
|
|
53
|
+
|
|
54
|
+
return cache.tail
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fold(name: string, typed: Record<string, unknown>, tail: () => string): Folded {
|
|
59
|
+
const folded: Folded = { input: typed, notes: [] }
|
|
60
|
+
|
|
61
|
+
for (const rule of REWRITING_RULES) {
|
|
62
|
+
const input = rule.rewrite?.({ name, input: folded.input }, tail)
|
|
63
|
+
|
|
64
|
+
if (input === undefined) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
folded.input = input
|
|
69
|
+
folded.notes = [...folded.notes, rule.rewrite_note ?? '']
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return folded
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The typed call, beside the whole payload its rewrite is spread back into.
|
|
76
|
+
interface TypedPayload {
|
|
77
|
+
fields: Record<string, unknown>
|
|
78
|
+
name: string
|
|
79
|
+
input: Record<string, unknown>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function typed_payload(raw_payload: string): TypedPayload | undefined {
|
|
83
|
+
const fields = json_value.parse_or_undefined(raw_payload)
|
|
84
|
+
|
|
85
|
+
if (!json_value.is_record(fields)) return undefined
|
|
86
|
+
|
|
87
|
+
const input = fields['tool_input']
|
|
88
|
+
|
|
89
|
+
return json_value.is_record(input)
|
|
90
|
+
? { fields, name: String(fields['tool_name']), input }
|
|
91
|
+
: undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rewrite(raw_payload: string): RuleRewrite | undefined {
|
|
95
|
+
const typed = delivered_rules.is_enabled() ? typed_payload(raw_payload) : undefined
|
|
96
|
+
|
|
97
|
+
if (typed === undefined) return undefined
|
|
98
|
+
|
|
99
|
+
const { input, notes } = fold(typed.name, typed.input, tail_reader(raw_payload))
|
|
100
|
+
|
|
101
|
+
if (notes.length === 0) return undefined
|
|
102
|
+
|
|
103
|
+
const note = notes.join(NOTE_SEPARATOR)
|
|
104
|
+
|
|
105
|
+
return { payload: JSON.stringify({ ...typed.fields, tool_input: input }), input, note }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const rule_rewrite = {
|
|
109
|
+
NOTE_SEPARATOR,
|
|
110
|
+
rewrite,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type { RuleRewrite }
|
|
114
|
+
export { rule_rewrite }
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '#scripts/time-runtime/time-transcript-line'
|
|
6
6
|
import { delivered_rules, type MeasuredRule } from './delivered-rules'
|
|
7
7
|
import { hook_context_line } from './hook-context-line'
|
|
8
|
+
import { rule_rewrite } from './rule-rewrite'
|
|
8
9
|
|
|
9
10
|
// What a rule is worth on the channel that carries it when its delivery has not fired.
|
|
10
11
|
//
|
|
@@ -287,7 +288,7 @@ function rewritten_id(text: string): string | undefined {
|
|
|
287
288
|
return found?.id
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
function
|
|
291
|
+
function observe_note(states: Map<string, RuleState>, text: string): void {
|
|
291
292
|
const id = rewritten_id(text)
|
|
292
293
|
const state = id === undefined ? undefined : states.get(id)
|
|
293
294
|
|
|
@@ -297,6 +298,11 @@ function observe_rewrite(states: Map<string, RuleState>, text: string): void {
|
|
|
297
298
|
state.is_triggered = true
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
// One line can be rewritten by several rows, whose notes `rule-rewrite.ts` joins into one context.
|
|
302
|
+
function observe_rewrite(states: Map<string, RuleState>, text: string): void {
|
|
303
|
+
for (const note of text.split(rule_rewrite.NOTE_SEPARATOR)) observe_note(states, note)
|
|
304
|
+
}
|
|
305
|
+
|
|
300
306
|
function observe_for_rule(
|
|
301
307
|
rule: MeasuredRule,
|
|
302
308
|
state: RuleState,
|
|
@@ -137,30 +137,14 @@ function decide(): boolean {
|
|
|
137
137
|
return true
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// The instruction in the shape a refusal can carry:
|
|
141
|
-
//
|
|
142
|
-
// is what makes the rule believable — the work was 17 minutes and the run was 45.
|
|
140
|
+
// The instruction in the shape a refusal can carry: the reissue, and the half a run forgets once the
|
|
141
|
+
// push has landed. The measurement behind it is in `docs/maintainers/rule-delivery-rationale.md`.
|
|
143
142
|
const RUN_TAIL_REASON =
|
|
144
143
|
'⛔ run tail: this issues the commit-push-PR step in the foreground, where the harness decides ' +
|
|
145
|
-
'when to stop waiting. Reissue it with `run_in_background` set
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'cap decides, not the number. A backgrounded call re-invokes you when it exits, so the completion ' +
|
|
150
|
-
'notification is what resumes the run. **When it arrives, the turn that reads it goes straight ' +
|
|
151
|
-
'through any branch-2 filing and `pnpm josh epic:bundle` to `pnpm josh followup` — the turn never ' +
|
|
152
|
-
'ends at the push.** CI went green 6m06s before the merge started in that run, and it started ' +
|
|
153
|
-
'only because the person asked; joshuafolkken/kit#1333 had already settled that the merge is ' +
|
|
154
|
-
'issued in the turn the review closes. While the push runs, do the work that writes nothing — ' +
|
|
155
|
-
'drafting the completion body to a file, deciding the branch-2 disposition. `pnpm josh followup` ' +
|
|
156
|
-
'itself stays in the **foreground** — nearly every step after it reads its result. But a tail ' +
|
|
157
|
-
'does follow the merge, measured at 3.0 min and 5.9% of a run (joshuafolkken/kit#1462), so ' +
|
|
158
|
-
'empty it beforehand: the steps that read the merge result stay after `followup`, and the rest ' +
|
|
159
|
-
'are composed in the turn that issues it, bar the one exception background-commands.md names. The ' +
|
|
160
|
-
'procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take ' +
|
|
161
|
-
'minutes is issued in the background". **This rule fires on every foreground push, not once per ' +
|
|
162
|
-
'run**, so ' +
|
|
163
|
-
'reissuing the same call in the foreground will be refused again.'
|
|
144
|
+
'when to stop waiting. Reissue it with `run_in_background` set; when it exits, go straight on to ' +
|
|
145
|
+
'`pnpm josh followup` in the foreground — the turn never ends at the push ' +
|
|
146
|
+
'(`.claude/skills/workflow-commands/background-commands.md`). It fires on every foreground push, ' +
|
|
147
|
+
'not once per run.'
|
|
164
148
|
|
|
165
149
|
const run_tail_rule = {
|
|
166
150
|
RUN_TAIL_REASON,
|