@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
|
@@ -187,12 +187,10 @@ function carries_a_body(command: string): boolean {
|
|
|
187
187
|
// (`git-force.ts`, `lane-carry-conflict.ts`).
|
|
188
188
|
const SHELL_BODY_REASON =
|
|
189
189
|
'⛔ shell-evaluated body: this command carries a body inline in double quotes, and that body ' +
|
|
190
|
-
'contains a backtick or a `$`, so the shell
|
|
191
|
-
'
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"trigger's blind spots are in `prompts/collaboration-workflow/shell-body.md`. Reissue this call " +
|
|
195
|
-
'once the body is in a file — it fires once per run and cannot repeat on the call in hand.'
|
|
190
|
+
'contains a backtick or a `$`, so the shell executes it. Reissue this call once the body is in a ' +
|
|
191
|
+
'file, passed by path (`pnpm josh issue:comment <N> --body-file <path>`, ' +
|
|
192
|
+
"`--notify-message-file <path>`), or use `$'…'` quoting " +
|
|
193
|
+
'(`prompts/collaboration-workflow/shell-body.md`).'
|
|
196
194
|
|
|
197
195
|
const shell_body_trigger = {
|
|
198
196
|
SHELL_BODY_REASON,
|
|
@@ -14,12 +14,32 @@ import { time_shell } from '#scripts/time-runtime/time-shell'
|
|
|
14
14
|
// because `sleep 1200 &` is an armed timer sent to the background and has to be seen as one. That is
|
|
15
15
|
// a different question about a different character, not a second copy of this answer.
|
|
16
16
|
const SEGMENT_SEPARATOR = /&&|\|\||;|\n/u
|
|
17
|
+
const SEGMENT_PARTS = new RegExp(`(${SEGMENT_SEPARATOR.source})`, 'u')
|
|
17
18
|
|
|
18
19
|
/** The line's commands, trimmed. Empty segments are kept: a caller anchoring at `^` rejects them. */
|
|
19
20
|
function segments_of(command: string): Array<string> {
|
|
20
21
|
return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim())
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** The same cut, untrimmed and with each separator kept between its commands, so the parts join back to the line. */
|
|
25
|
+
function parts_of(command: string): Array<string> {
|
|
26
|
+
return command.split(SEGMENT_PARTS)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The line's first command as the shell runs it — cut where the quote-blanked line puts the first
|
|
31
|
+
* separator, so one inside a quoted string does not cut it — or `undefined` for a heredoc line.
|
|
32
|
+
*/
|
|
33
|
+
function first_of(command: string): string | undefined {
|
|
34
|
+
const plain = time_shell.blanked(command)
|
|
35
|
+
|
|
36
|
+
if (plain === undefined) return undefined
|
|
37
|
+
|
|
38
|
+
const cut = plain.search(SEGMENT_SEPARATOR)
|
|
39
|
+
|
|
40
|
+
return (cut === -1 ? command : command.slice(0, cut)).trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
// **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
|
|
24
44
|
// in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
|
|
25
45
|
// matches it as it is. Widening as well would be a second copy of one rule, and the caller that
|
|
@@ -35,7 +55,9 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
|
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
const shell_segments = {
|
|
58
|
+
first_of,
|
|
38
59
|
is_josh_command,
|
|
60
|
+
parts_of,
|
|
39
61
|
segments_of,
|
|
40
62
|
}
|
|
41
63
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// What a held stop is waiting on, read from records rather than guessed from prose
|
|
2
|
+
// (joshuafolkken/kit#3961): the decision lines the reply states, and whether the person's own prompt
|
|
3
|
+
// this turn was a question.
|
|
4
|
+
//
|
|
5
|
+
// **The recommended choice is the reply's, never a fixed placeholder.** A `confirmation` reaches a
|
|
6
|
+
// person away from the session, and "read the reply" tells them nothing about what they are asked to
|
|
7
|
+
// decide. The reply names it on a line of its own — `Recommended: <choice>`, with an optional
|
|
8
|
+
// `Alternative: <choice>` — the same two fields `josh notify` takes, so the hook copies them rather
|
|
9
|
+
// than inferring them.
|
|
10
|
+
//
|
|
11
|
+
// **A turn the person opened with a question is answered in the session they are at.** Its stop is
|
|
12
|
+
// the answer, not a pause, so a Telegram saying "confirmation required" only buries the ones that are.
|
|
13
|
+
|
|
14
|
+
interface StopDecision {
|
|
15
|
+
recommended: string
|
|
16
|
+
alternative: string | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A label may close its emphasis before or after the colon: `**Recommended:**`, `**Recommended**:`.
|
|
20
|
+
const RECOMMENDED_LABEL = /^recommended[*_]*:/iu
|
|
21
|
+
const ALTERNATIVE_LABEL = /^alternative[*_]*:/iu
|
|
22
|
+
// The list bullet and the Markdown emphasis a decision line may be written with. Emphasis is
|
|
23
|
+
// stripped only at the edges of the label and the choice: an underscore inside a choice belongs to
|
|
24
|
+
// a `snake_case` name the person is asked to approve.
|
|
25
|
+
const LEADING_EMPHASIS = /^[*_]+/u
|
|
26
|
+
const TRAILING_EMPHASIS = /(?<![*_])[*_]+$/u
|
|
27
|
+
const BULLET = /^[-*]\s+/u
|
|
28
|
+
|
|
29
|
+
// A prompt that ends asking: a question mark, or a Japanese sentence-final question particle, before
|
|
30
|
+
// any trailing full stop or space.
|
|
31
|
+
const QUESTION_ENDING = /(?:[??]|か|かな|かね)[\s。.]*$/u
|
|
32
|
+
|
|
33
|
+
function unwrapped(text: string): string {
|
|
34
|
+
return text.trim().replace(LEADING_EMPHASIS, '').replace(TRAILING_EMPHASIS, '').trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function bare_line(line: string): string {
|
|
38
|
+
return unwrapped(line.trim().replace(BULLET, ''))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The choice on the reply's first line labelled `label`, or `undefined` when none carries one.
|
|
42
|
+
function choice_of(message: string, label: RegExp): string | undefined {
|
|
43
|
+
const line = message
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((one) => bare_line(one))
|
|
46
|
+
.find((one) => label.test(one))
|
|
47
|
+
const choice = line === undefined ? undefined : unwrapped(line.replace(label, ''))
|
|
48
|
+
|
|
49
|
+
return choice === '' ? undefined : choice
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The decision the reply asks for, or `undefined` when it names no recommended choice.
|
|
53
|
+
function decision_of(message: string): StopDecision | undefined {
|
|
54
|
+
const recommended = choice_of(message, RECOMMENDED_LABEL)
|
|
55
|
+
if (recommended === undefined) return undefined
|
|
56
|
+
|
|
57
|
+
return { recommended, alternative: choice_of(message, ALTERNATIVE_LABEL) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function is_question(prompt: string): boolean {
|
|
61
|
+
return QUESTION_ENDING.test(prompt.trim())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// **A held stop with no decision line is sent back once rather than announced.** Either it waits on a
|
|
65
|
+
// person and has to say on what, or it waits on nobody and the notify would be the noise this rule
|
|
66
|
+
// exists to end; only the run can tell which, so the reason hands it both answers.
|
|
67
|
+
const DECISION_REASON =
|
|
68
|
+
'⛔ stop decision: this working tree is held by your run and your reply names no decision. If the ' +
|
|
69
|
+
'run waits on a person, end the reply with a `Recommended: <choice>` line and, where there is ' +
|
|
70
|
+
'one, an `Alternative: <choice>` line — the hook sends the `confirmation` Telegram from them. If ' +
|
|
71
|
+
'it waits on nobody, continue the run, or say in one line that nothing is awaited. Do not repeat ' +
|
|
72
|
+
'your previous reply.'
|
|
73
|
+
|
|
74
|
+
const stop_decision = { DECISION_REASON, decision_of, is_question }
|
|
75
|
+
|
|
76
|
+
export type { StopDecision }
|
|
77
|
+
export { stop_decision }
|
|
@@ -4,6 +4,7 @@ import { filing_offer } from './filing-offer'
|
|
|
4
4
|
import { issue_citation } from './issue-citation'
|
|
5
5
|
import { lane_background } from './lane-background'
|
|
6
6
|
import { reply_language } from './reply-language'
|
|
7
|
+
import { stop_decision, type StopDecision } from './stop-decision'
|
|
7
8
|
|
|
8
9
|
// The stop-time rules, delivered on the `Stop` hook.
|
|
9
10
|
//
|
|
@@ -19,11 +20,14 @@ import { reply_language } from './reply-language'
|
|
|
19
20
|
// permission envelope. The stop guard is a second *entry* on the one foundation, exactly as
|
|
20
21
|
// `pretool-guard` is one — not a second copy of the plumbing.
|
|
21
22
|
//
|
|
22
|
-
// **
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
23
|
+
// **A rule whose answer is fixed acts; only a rule that needs judgement refuses** (joshuafolkken/kit#3919).
|
|
24
|
+
// The stop notification, the hold release and the citation lines are each one mechanical operation the
|
|
25
|
+
// hook can run itself, so they come back as `actions` and the stop goes through: a refusal is a whole
|
|
26
|
+
// model turn spent re-reading the context to run a command the hook already knew. `stop-actions.ts`
|
|
27
|
+
// runs them, and only an action that fails falls back to its refusal. The rest — the reply's language,
|
|
28
|
+
// an unattended filing offer, a lane's index request or running task, a headless parent's wait — need
|
|
29
|
+
// the model, so they still block with `{"decision":"block"}`, the one channel a `Stop` hook has to it;
|
|
30
|
+
// `stop_hook_active` caps a false positive at a single wasted turn.
|
|
27
31
|
|
|
28
32
|
const SWITCH_ENV_KEY = 'JOSH_STOP_GUARD'
|
|
29
33
|
const BLOCK_DECISION = 'block'
|
|
@@ -40,14 +44,22 @@ type StopPayload = z.infer<typeof stop_payload_schema>
|
|
|
40
44
|
|
|
41
45
|
// What one stop looks like to the rules, computed once by the CLI from the payload and the world.
|
|
42
46
|
interface StopContext {
|
|
43
|
-
// The `run:hold` record for this working tree
|
|
44
|
-
|
|
47
|
+
// The Issue the `run:hold` record for this working tree names (`held` or `stale`) — `new` for an
|
|
48
|
+
// unnumbered entry — or `undefined` when no record is in place.
|
|
49
|
+
hold_issue: string | undefined
|
|
50
|
+
// That record is this session's own run's, or its owner cannot be told (`run-hold-session.ts`). A
|
|
51
|
+
// hold another run took is that run's pause to announce and release, never this session's.
|
|
52
|
+
hold_owned: boolean
|
|
53
|
+
// The person opened this turn with a question: the reply answers someone at the session, not a
|
|
54
|
+
// pause waiting on them (`stop-decision.ts` → `is_question`).
|
|
55
|
+
answers_question: boolean
|
|
45
56
|
// `git status --porcelain` came back empty.
|
|
46
57
|
tree_clean: boolean
|
|
47
58
|
// That record carries a `prrun` stop mark: the run stopped at a green pull
|
|
48
59
|
// request and keeps the hold over a clean tree on purpose, so a person can look before it merges.
|
|
49
60
|
prrun_stopped: boolean
|
|
50
|
-
// A `confirmation` notify is on this run's transcript tail
|
|
61
|
+
// A `confirmation` notify is on this run's transcript tail, or the hook's own send is recorded on the
|
|
62
|
+
// hold since the pause began (`run-hold-session.ts` → `is_notified_since`).
|
|
51
63
|
notified: boolean
|
|
52
64
|
// The turn's session-facing reply text.
|
|
53
65
|
message: string
|
|
@@ -56,7 +68,8 @@ interface StopContext {
|
|
|
56
68
|
// This stop already forced a continuation earlier in the prompt — the loop-breaker Claude Code
|
|
57
69
|
// documents, so a run that will not comply is not blocked forever.
|
|
58
70
|
stop_hook_active: boolean
|
|
59
|
-
//
|
|
71
|
+
// This turn ended at a cut — its record is in place, or this turn relaunched the successor that may
|
|
72
|
+
// already have cleared it (`stop-guard.ts` → `is_cut_stop`). A dispatched lane child ends its turn at
|
|
60
73
|
// the cut holding the tree by design and hands off to a fresh process (`pre-gate-cut.md`), so that
|
|
61
74
|
// turn-end is an automatic continuation, not the person-waiting pause the block rules are for.
|
|
62
75
|
cut_pending: boolean
|
|
@@ -90,38 +103,44 @@ interface StopContext {
|
|
|
90
103
|
backlog_parent: boolean
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
// The operations the hook runs itself instead of refusing — each carries what it acts on.
|
|
107
|
+
type StopAction =
|
|
108
|
+
| { kind: 'notify'; issue: string; message: string; decision: StopDecision }
|
|
109
|
+
| { kind: 'release'; issue: string }
|
|
110
|
+
| { kind: 'cite'; references: ReadonlyArray<string> }
|
|
111
|
+
|
|
112
|
+
// `reason` refuses the stop; otherwise `actions` run, and `blocks_on_failure` says whether a failed one
|
|
113
|
+
// falls back to its refusal — never past the loop-breaker.
|
|
93
114
|
interface StopOutcome {
|
|
94
115
|
reason: string | undefined
|
|
116
|
+
actions: ReadonlyArray<StopAction>
|
|
117
|
+
blocks_on_failure: boolean
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
const NO_OUTCOME: StopOutcome = { reason: undefined }
|
|
120
|
+
const NO_OUTCOME: StopOutcome = { reason: undefined, actions: [], blocks_on_failure: false }
|
|
98
121
|
|
|
99
122
|
// **A mid-workflow stop is announced off-screen before it happens.** `CLAUDE.md` → "Mid-workflow stop
|
|
100
123
|
// notification" requires the `confirmation` Telegram before any pause; the hold is what says a run is
|
|
101
|
-
// mid-workflow, and the transcript is what says the notify was sent.
|
|
124
|
+
// mid-workflow, and the transcript is what says the notify was sent. The hook sends it; this reason is
|
|
125
|
+
// only the fallback for a send that failed.
|
|
102
126
|
const STOP_NOTIFY_REASON =
|
|
103
127
|
'⛔ mid-workflow stop notification: this working tree is still held by a run and no `confirmation` ' +
|
|
104
|
-
'Telegram was sent this turn
|
|
105
|
-
'paused. `CLAUDE.md` → "Mid-workflow stop notification" requires ' +
|
|
128
|
+
'Telegram was sent this turn. Send ' +
|
|
106
129
|
'`pnpm josh notify --task-type confirmation --issue-url "<url>" --body=$\'<reason>\' ' +
|
|
107
|
-
'--recommended "<choice>" --alternative "<choice>"`
|
|
108
|
-
'
|
|
109
|
-
'your previous reply. This fires only while the hold is held and no notify is on the transcript, and ' +
|
|
110
|
-
'`stop_hook_active` lets a second stop through so a run is never wedged.'
|
|
130
|
+
'--recommended "<choice>" --alternative "<choice>"` (`CLAUDE.md` → "Mid-workflow stop ' +
|
|
131
|
+
'notification"), then confirm in one line that it was sent — do not repeat your previous reply.'
|
|
111
132
|
|
|
112
133
|
// **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
|
|
113
134
|
// leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
|
|
114
135
|
// keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
|
|
136
|
+
// The hook releases it; this reason is only the fallback for a release that failed.
|
|
115
137
|
const HOLD_RELEASE_REASON =
|
|
116
138
|
'⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
|
|
117
|
-
'place
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
'not repeat your previous reply.'
|
|
123
|
-
|
|
124
|
-
// **A refusal that corrects rather than advises**. The bare `#N` is already
|
|
139
|
+
'place. Release it with `pnpm josh run:release <N>` (bare for a `new` entry; ' +
|
|
140
|
+
'`working-tree-hold.md`), then confirm in one line that it was released — do not repeat your ' +
|
|
141
|
+
'previous reply.'
|
|
142
|
+
|
|
143
|
+
// **The fallback when the hook could not print the citation lines itself**. The bare `#N` is already
|
|
125
144
|
// on screen and the hook cannot unsay it, so the reason does the one thing that helps the *next* reply:
|
|
126
145
|
// it names the numbers it detected and hands over the exact `issue:cite` call that prints the
|
|
127
146
|
// paste-ready lines, then asks for the corrected citation lines alone — not the whole reply reissued.
|
|
@@ -149,13 +168,10 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
|
|
|
149
168
|
// first**: there the offer is the correct reply, so `needs_filing` is silent.
|
|
150
169
|
const FILING_OFFER_REASON =
|
|
151
170
|
'⛔ filing offer: your reply offers to file an Issue instead of filing it, and this run is ' +
|
|
152
|
-
'unattended — nobody is there to answer.
|
|
153
|
-
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
|
|
157
|
-
'your previous reply. If it is not worth filing ' +
|
|
158
|
-
'after all, say so in one line instead.'
|
|
171
|
+
'unattended — nobody is there to answer. Run ' +
|
|
172
|
+
'`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` (Tier A, ' +
|
|
173
|
+
'`observation-filing.md`), then end with the one-line citation of what was filed; do not repeat ' +
|
|
174
|
+
'your previous reply. If it is not worth filing after all, say so in one line instead.'
|
|
159
175
|
|
|
160
176
|
// **A headless parent's turn-end is its process's end**. Under `claude -p` the
|
|
161
177
|
// background waits die with the turn, so the lanes lose their parent and nothing re-invokes it. The
|
|
@@ -176,12 +192,10 @@ const HEADLESS_REFUSAL_CAP = 3
|
|
|
176
192
|
const HEADLESS_WAIT_CALLS = ['"command":"pnpm josh lane:await']
|
|
177
193
|
|
|
178
194
|
const HEADLESS_WAIT_BODY =
|
|
179
|
-
' this is a `claude -p` `backlogrun` parent
|
|
180
|
-
'
|
|
181
|
-
"lanes are left without a parent. Do not watch them yourself: the `run:wake` supervisor's driver " +
|
|
182
|
-
'watches the lanes and wakes a session only for a branch that needs judging. Hand the loop back with ' +
|
|
195
|
+
' this is a `claude -p` `backlogrun` parent with lanes in flight, and ending this turn kills the ' +
|
|
196
|
+
'process and its background waits. Hand the loop back with ' +
|
|
183
197
|
'`pnpm josh run:carry --cut --owner "$PPID"` (or `--end` when the run is over), then end the turn ' +
|
|
184
|
-
'(`backlogrun-progress.md`
|
|
198
|
+
'(`backlogrun-progress.md`).'
|
|
185
199
|
|
|
186
200
|
const HEADLESS_WAIT_REASON = `${HEADLESS_WAIT_MARKER}${HEADLESS_WAIT_BODY}`
|
|
187
201
|
|
|
@@ -195,11 +209,9 @@ const PERMISSION_PATTERN = /許可|承認|permission|approve|approval|authorizat
|
|
|
195
209
|
|
|
196
210
|
const LANE_INDEX_REASON =
|
|
197
211
|
'⛔ lane index permission: this is a dispatched lane child (`JOSH_LANE_CHILD`) and your reply asks ' +
|
|
198
|
-
|
|
199
|
-
'
|
|
200
|
-
'
|
|
201
|
-
'This is Tier A (`.claude/skills/workflow-commands/chain-rule.md` → "origin/main is merged in before ' +
|
|
202
|
-
'the gate"). Do it and continue the run — do not repeat your previous reply.'
|
|
212
|
+
'a person for permission to change the git index. That is Tier A here (`chain-rule.md`): remove any ' +
|
|
213
|
+
'conflict markers, run `pnpm josh git -y` (it stages and commits, a conflicted merge included) and ' +
|
|
214
|
+
'continue the run — do not repeat your previous reply.'
|
|
203
215
|
|
|
204
216
|
function asks_index_permission(message: string): boolean {
|
|
205
217
|
return INDEX_COMMAND_PATTERN.test(message) && PERMISSION_PATTERN.test(message)
|
|
@@ -259,42 +271,52 @@ function needs_filing(context: StopContext): boolean {
|
|
|
259
271
|
//
|
|
260
272
|
// **A lane handed to the detached ship stands them down too**: the hold now
|
|
261
273
|
// belongs to the supervisor driving the merge, and the person hears from that supervisor's own notify.
|
|
274
|
+
//
|
|
275
|
+
// **Another run's hold stands them down as well**: the pause and the claim are that run's.
|
|
262
276
|
function is_held_idle(context: StopContext): boolean {
|
|
263
|
-
|
|
277
|
+
if (context.hold_issue === undefined || !context.hold_owned) return false
|
|
278
|
+
|
|
279
|
+
return !context.agent_pending && !context.handed_off
|
|
264
280
|
}
|
|
265
281
|
|
|
282
|
+
// **A stop that answers the person's question is no pause** (joshuafolkken/kit#3961): they are reading
|
|
283
|
+
// the reply where it was written, whatever the tree's hold says.
|
|
266
284
|
function needs_notify(context: StopContext): boolean {
|
|
267
|
-
return is_held_idle(context) && !context.notified
|
|
285
|
+
return is_held_idle(context) && !context.notified && !context.answers_question
|
|
268
286
|
}
|
|
269
287
|
|
|
270
|
-
|
|
271
|
-
|
|
288
|
+
// The notify a stop owes but cannot send: its reply names no decision to announce.
|
|
289
|
+
function needs_decision(context: StopContext): boolean {
|
|
290
|
+
return needs_notify(context) && stop_decision.decision_of(context.message) === undefined
|
|
272
291
|
}
|
|
273
292
|
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
if (references.length === 0) return undefined
|
|
277
|
-
|
|
278
|
-
return build_citation_reason(references)
|
|
293
|
+
function needs_release(context: StopContext): boolean {
|
|
294
|
+
return is_held_idle(context) && context.tree_clean && !context.prrun_stopped
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
// The
|
|
282
|
-
// its text lands in a stream-json log, so correcting its citations or its
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
function
|
|
286
|
-
if (context.headless_agent) return
|
|
297
|
+
// The bare references the citation lines are printed for. **A headless agent's reply has no reader**:
|
|
298
|
+
// its text lands in a stream-json log, so correcting its citations or its language costs a turn and
|
|
299
|
+
// reaches nobody. The artifacts a person does read — a Telegram, an Issue, a pull request — are held
|
|
300
|
+
// to the same rules by their own guards, not here.
|
|
301
|
+
function bare_references(context: StopContext): ReadonlyArray<string> {
|
|
302
|
+
if (context.headless_agent) return []
|
|
287
303
|
|
|
288
|
-
return
|
|
304
|
+
return issue_citation.unquoted_references(context.message, context.prompt)
|
|
289
305
|
}
|
|
290
306
|
|
|
291
|
-
// The
|
|
307
|
+
// The two rules that read the reply and need the model, the filing offer first. A drifted reply
|
|
292
308
|
// refused for another reason is not rewritten, but the drift still ends: `block_envelope` appends the
|
|
293
309
|
// session-language note to every refusal, so the turn it continues is written in the session language.
|
|
294
310
|
function reply_reason(context: StopContext): string | undefined {
|
|
295
311
|
if (needs_filing(context)) return FILING_OFFER_REASON
|
|
312
|
+
if (context.headless_agent) return undefined
|
|
313
|
+
|
|
314
|
+
return language_reason(context)
|
|
315
|
+
}
|
|
296
316
|
|
|
297
|
-
|
|
317
|
+
// Sent back once; past the loop-breaker the run has said nothing is awaited, and nothing is sent.
|
|
318
|
+
function decision_reason(context: StopContext): string | undefined {
|
|
319
|
+
return needs_decision(context) ? stop_decision.DECISION_REASON : undefined
|
|
298
320
|
}
|
|
299
321
|
|
|
300
322
|
// The headless wait holds past the loop-breaker until the spin bound is reached.
|
|
@@ -319,24 +341,56 @@ function needs_background_wait(context: StopContext): boolean {
|
|
|
319
341
|
return context.lane_child && context.background_pending
|
|
320
342
|
}
|
|
321
343
|
|
|
322
|
-
// The two
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
function hold_reason(context: StopContext): string | undefined {
|
|
344
|
+
// The two lane rules that mean the run is not pausing at all — ahead of every action, since the notify
|
|
345
|
+
// would announce a pause the run has no reason to take.
|
|
346
|
+
function lane_reason(context: StopContext): string | undefined {
|
|
326
347
|
if (needs_index_route(context)) return LANE_INDEX_REASON
|
|
327
348
|
if (needs_background_wait(context)) return lane_background.LANE_BACKGROUND_STOP_REASON
|
|
328
|
-
if (needs_notify(context)) return STOP_NOTIFY_REASON
|
|
329
|
-
if (needs_release(context)) return HOLD_RELEASE_REASON
|
|
330
349
|
|
|
331
350
|
return undefined
|
|
332
351
|
}
|
|
333
352
|
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
353
|
+
// The refusals that need the model: the lane rules alone, else the reply's own and the decision
|
|
354
|
+
// together. **Joined, since the loop-breaker grants one continuation**: a decision refusal queued behind
|
|
355
|
+
// a language or filing refusal would never be sent, and its stop would pass with no notify.
|
|
356
|
+
function model_reason(context: StopContext): string | undefined {
|
|
357
|
+
const lane = lane_reason(context)
|
|
358
|
+
if (lane !== undefined) return lane
|
|
359
|
+
|
|
360
|
+
const reasons = [reply_reason(context), decision_reason(context)].filter(
|
|
361
|
+
(reason) => reason !== undefined,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return reasons.length === 0 ? undefined : reasons.join('\n\n')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The notify carries the reply's own decision; a reply with none sends nothing (`needs_decision`).
|
|
368
|
+
function notify_actions(context: StopContext, issue: string): Array<StopAction> {
|
|
369
|
+
const decision = stop_decision.decision_of(context.message)
|
|
370
|
+
if (decision === undefined || !needs_notify(context)) return []
|
|
371
|
+
|
|
372
|
+
return [{ kind: 'notify', issue, message: context.message, decision }]
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function hold_actions(context: StopContext): Array<StopAction> {
|
|
376
|
+
const issue = context.hold_issue
|
|
377
|
+
if (issue === undefined) return []
|
|
378
|
+
|
|
379
|
+
const notify = notify_actions(context, issue)
|
|
380
|
+
|
|
381
|
+
return needs_release(context) ? [...notify, { kind: 'release', issue }] : notify
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The actions in the order they run: the notify ahead of the release that ends the hold it reports.
|
|
385
|
+
function stop_actions(context: StopContext): ReadonlyArray<StopAction> {
|
|
386
|
+
const references = bare_references(context)
|
|
387
|
+
const cite: Array<StopAction> = references.length > 0 ? [{ kind: 'cite', references }] : []
|
|
388
|
+
|
|
389
|
+
return [...hold_actions(context), ...cite]
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// First-wins across the refusals. `stop_hook_active` is the loop-breaker, so a run that already got one
|
|
393
|
+
// continuation this prompt may stop; the lane rules go first, then the filing offer, then the language.
|
|
340
394
|
//
|
|
341
395
|
// **The headless wait comes first and outlasts `stop_hook_active` up to its spin bound**.
|
|
342
396
|
// The loop-breaker exists so a person is not wedged behind a rule the run will not satisfy; a headless
|
|
@@ -347,18 +401,49 @@ function block_reason(context: StopContext): string | undefined {
|
|
|
347
401
|
if (is_headless_held(context)) return HEADLESS_WAIT_REASON
|
|
348
402
|
if (context.stop_hook_active || context.cut_pending) return undefined
|
|
349
403
|
|
|
350
|
-
return
|
|
404
|
+
return model_reason(context)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// **The actions run only on the stop that goes through**, so a refused stop never notifies a pause its
|
|
408
|
+
// continuation then cancels, while the continuation's own stop, past the loop-breaker, still runs them
|
|
409
|
+
// and a failure there no longer refuses. A lane child still waiting on its own work runs none, and a
|
|
410
|
+
// pre-gate cut in flight stands everything but the headless wait down: it is a lane child's automatic
|
|
411
|
+
// turn-end, not a person-waiting pause.
|
|
412
|
+
function runs_actions(context: StopContext, reason: string | undefined): boolean {
|
|
413
|
+
return reason === undefined && !context.cut_pending && lane_reason(context) === undefined
|
|
351
414
|
}
|
|
352
415
|
|
|
353
416
|
function stop_outcome(context: StopContext): StopOutcome {
|
|
354
|
-
|
|
417
|
+
const reason = block_reason(context)
|
|
418
|
+
if (!runs_actions(context, reason)) return { ...NO_OUTCOME, reason }
|
|
419
|
+
|
|
420
|
+
const actions = stop_actions(context)
|
|
421
|
+
if (actions.length === 0) return NO_OUTCOME
|
|
422
|
+
|
|
423
|
+
return { reason, actions, blocks_on_failure: !context.stop_hook_active }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// The refusal an action falls back to when the hook could not run it.
|
|
427
|
+
function fallback_reason(action: StopAction): string {
|
|
428
|
+
if (action.kind === 'notify') return STOP_NOTIFY_REASON
|
|
429
|
+
if (action.kind === 'release') return HOLD_RELEASE_REASON
|
|
430
|
+
|
|
431
|
+
return build_citation_reason(action.references)
|
|
355
432
|
}
|
|
356
433
|
|
|
357
434
|
// The documented shape a `Stop` hook blocks with: `reason` is fed back to Claude, which then continues
|
|
358
435
|
// instead of stopping. Plain stdout is not it — only this envelope holds the stop. Every reason leaves
|
|
359
436
|
// with the session-language note, so no rule can hand the model an English-only turn.
|
|
360
|
-
|
|
361
|
-
|
|
437
|
+
// `notice` is what the hook already did on this stop, shown to the person alongside.
|
|
438
|
+
function block_envelope(reason: string, lang: string, notice?: string): string {
|
|
439
|
+
const refusal = { decision: BLOCK_DECISION, reason: `${reason} ${language_note(lang)}` }
|
|
440
|
+
|
|
441
|
+
return JSON.stringify(notice === undefined ? refusal : { ...refusal, systemMessage: notice })
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The shape a `Stop` hook shows the person a line in without holding the stop: `systemMessage` alone.
|
|
445
|
+
function notice_envelope(notice: string): string {
|
|
446
|
+
return JSON.stringify({ systemMessage: notice })
|
|
362
447
|
}
|
|
363
448
|
|
|
364
449
|
function parse_stop_payload(raw_payload: string): StopPayload | undefined {
|
|
@@ -384,10 +469,12 @@ const stop_rules = {
|
|
|
384
469
|
block_envelope,
|
|
385
470
|
build_language_reason,
|
|
386
471
|
count_headless_refusals,
|
|
472
|
+
fallback_reason,
|
|
387
473
|
is_enabled,
|
|
474
|
+
notice_envelope,
|
|
388
475
|
parse_stop_payload,
|
|
389
476
|
stop_outcome,
|
|
390
477
|
}
|
|
391
478
|
|
|
392
|
-
export type { StopContext, StopOutcome }
|
|
479
|
+
export type { StopAction, StopContext, StopOutcome }
|
|
393
480
|
export { stop_rules }
|
|
@@ -30,6 +30,20 @@ function prior_bash_commands(tail: string): ReadonlyArray<string> {
|
|
|
30
30
|
return tail.split('\n').flatMap((line) => bash_commands_of(line))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// **Whether a call `is_call` names answered with the `token` line** — the call and its own result
|
|
34
|
+
// paired off parsed blocks, so a transcript that merely quotes the command or the token does not
|
|
35
|
+
// count. An unreadable text reads as no answer.
|
|
36
|
+
function answered(text: string, is_call: (command: string) => boolean, token: string): boolean {
|
|
37
|
+
const blocks = time_transcript_line.parse_text(text).flatMap((line) => line.blocks)
|
|
38
|
+
const call_ids = new Set(
|
|
39
|
+
blocks
|
|
40
|
+
.filter((block) => is_call(time_shell.bash_command(block.input)))
|
|
41
|
+
.map((block) => block.id),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return blocks.some((block) => call_ids.has(block.result_id) && block.token_lines.includes(token))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tail_commands = { answered, is_bash_use, prior_bash_commands }
|
|
34
48
|
|
|
35
49
|
export { tail_commands }
|
|
@@ -30,16 +30,10 @@ function is_untested_commit(call: GuardedCall): boolean {
|
|
|
30
30
|
|
|
31
31
|
const REASON =
|
|
32
32
|
'⛔ test declared: this commits a runtime code change with no test beside it — `pnpm josh ' +
|
|
33
|
-
'test:declared` reads
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'E2E. Add the declared test and reissue this commit. **If the change genuinely touches no ' +
|
|
38
|
-
'executable runtime path** it is the pre-approved non-runtime exception — declare it in your Step ' +
|
|
39
|
-
'0 work summary with the reason it is exempt (non-executable config, a cosmetic asset swap) and ' +
|
|
40
|
-
'reissue; the exception is a presentation, not a stop. Run `pnpm josh test:declared` to see the ' +
|
|
41
|
-
'files it names. The procedure is `prompts/collaboration-workflow/rule-delivery.md` → ' +
|
|
42
|
-
'"配送されている規則". This fires once per run, so reissuing will not repeat on the call in hand.'
|
|
33
|
+
'test:declared` reads it `required` and names the files. Tests are required for ALL code changes: ' +
|
|
34
|
+
'add the test and reissue, or, for a change that touches no runtime path, declare the non-runtime ' +
|
|
35
|
+
'exception in your Step 0 work summary and reissue. It fires once per run (`CLAUDE.md` → Code ' +
|
|
36
|
+
'Change Rules).'
|
|
43
37
|
|
|
44
38
|
// The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
|
|
45
39
|
// trigger and reason it already single-sources here. Once per run (the default delivery) for the
|
|
@@ -103,16 +103,11 @@ function writes_third_party(
|
|
|
103
103
|
// to do instead. The command that computes the party is handed over, because the whole point of this
|
|
104
104
|
// Issue is that the test is computed rather than judged.
|
|
105
105
|
const THIRD_PARTY_WRITE_REASON =
|
|
106
|
-
'⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'`prompts/collaboration-workflow/upstream-interrupt.md`).
|
|
111
|
-
'`pnpm josh repo:party <owner/repo>` — owner equality, computed not judged. A read (`-X GET`, or no ' +
|
|
112
|
-
'field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or ' +
|
|
113
|
-
'`-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact ' +
|
|
114
|
-
'for a person to post — do not publish it yourself. **This rule fires on every occurrence, not once ' +
|
|
115
|
-
'per run.**'
|
|
106
|
+
'⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session, ' +
|
|
107
|
+
'which is Tier C and needs explicit current-turn user instruction. Check with ' +
|
|
108
|
+
'`pnpm josh repo:party <owner/repo>`, then record the finding on our own Issue and draft the ' +
|
|
109
|
+
'artifact for a person to post. This rule fires on every occurrence ' +
|
|
110
|
+
'(`prompts/collaboration-workflow/upstream-interrupt.md`).'
|
|
116
111
|
|
|
117
112
|
// `decide` returns true so it refuses every occurrence; it declares no `keeps` — not writing to
|
|
118
113
|
// another owner's tracker is the absence of a call, not a call, so the row is reported unmeasured
|
|
@@ -124,18 +124,11 @@ function is_unauthorized_worktree_change(command: string): boolean {
|
|
|
124
124
|
// and — for a stash — the message form that is allowed. `pnpm josh git` and `pnpm josh stash:pop` are
|
|
125
125
|
// invisible to this trigger, so a run that uses them is never refused.
|
|
126
126
|
const WORKTREE_MUTATION_REASON =
|
|
127
|
-
'⛔ unauthorized working-tree change: `git checkout
|
|
128
|
-
'`git
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'divert. A stash push is authorized only inside a documented flow and only with a findable message ' +
|
|
133
|
-
'— `git stash push -u -m "<message>"` — which this row allows; a bare `git stash`, a message-less ' +
|
|
134
|
-
'push, or `pop` / `apply` / `drop` is refused. A plan that restores a handed-over stash with `git ' +
|
|
135
|
-
'stash apply` / `git stash pop` is read as `pnpm josh stash:pop "<message>"`: apply it that way, ' +
|
|
136
|
-
'continue, and record the substitution on the Issue — Tier A, never a `needs-decision` park ' +
|
|
137
|
-
'unless the plan says why the owner keeps the stash: then stop and confirm (`operating-rules.md` ' +
|
|
138
|
-
'→ "no-self-staging"). **This rule fires on every occurrence, not once per run.**'
|
|
127
|
+
'⛔ unauthorized working-tree change: `git checkout --`, `git restore`, `git clean -f`, ' +
|
|
128
|
+
'`git stash`. A documented flow stashes with `git stash push -u -m "<message>"`; a handed-over ' +
|
|
129
|
+
'stash is `pnpm josh stash:pop "<message>"`; record the substitution on the Issue (Tier A) unless ' +
|
|
130
|
+
'the plan says why the owner keeps the stash: then stop and confirm. It fires on every occurrence ' +
|
|
131
|
+
'(`operating-rules.md`).'
|
|
139
132
|
|
|
140
133
|
const ROW = {
|
|
141
134
|
id: 'worktree-mutation',
|