one-code-extension 0.4.1 → 0.5.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/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
package/extensions/footer/pr.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* a PR whose head lives in the base repo, so it misses the common case of a PR
|
|
9
9
|
* opened from a fork (`isCrossRepository`), even though `list` on the base repo
|
|
10
10
|
* knows about it. `--state open` keeps a merged/closed PR for the same branch
|
|
11
|
-
* name from showing.
|
|
11
|
+
* name from showing. The footer repeats the lookup once a minute while the
|
|
12
|
+
* session is in use (`nextPrPollDelay`), so a PR merged, closed or opened
|
|
13
|
+
* mid-session shows within a minute.
|
|
12
14
|
*
|
|
13
15
|
* The exec lives here but the parse is split out so it unit-tests without a
|
|
14
16
|
* subprocess.
|
|
@@ -35,19 +37,46 @@ export function parsePrNumber(stdout: string): number | undefined {
|
|
|
35
37
|
|
|
36
38
|
const LOOKUP_TIMEOUT_MS = 4000;
|
|
37
39
|
|
|
40
|
+
/** A lookup's outcome: the open PR, if any, and whether `gh` is not installed at all. */
|
|
41
|
+
export interface PrLookup {
|
|
42
|
+
pr: number | undefined;
|
|
43
|
+
ghMissing: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
|
-
* Resolve the open PR number for `branch` in `cwd
|
|
40
|
-
*
|
|
47
|
+
* Resolve the open PR number for `branch` in `cwd`. Never rejects: any failure
|
|
48
|
+
* (no gh, no PR, detached HEAD, timeout) has `pr` undefined, and a `gh` that
|
|
49
|
+
* is not on PATH says so, so the footer stops polling for it.
|
|
41
50
|
*/
|
|
42
|
-
export function fetchPrNumber(cwd: string, branch: string): Promise<
|
|
51
|
+
export function fetchPrNumber(cwd: string, branch: string): Promise<PrLookup> {
|
|
43
52
|
return new Promise((resolve) => {
|
|
44
53
|
execFile(
|
|
45
54
|
"gh",
|
|
46
55
|
["pr", "list", "--head", branch, "--state", "open", "--json", "number"],
|
|
47
56
|
{ cwd, timeout: LOOKUP_TIMEOUT_MS, encoding: "utf8" },
|
|
48
57
|
(error, stdout) => {
|
|
49
|
-
|
|
58
|
+
const ghMissing = (error as NodeJS.ErrnoException | null)?.code === "ENOENT";
|
|
59
|
+
resolve({ pr: error ? undefined : parsePrNumber(stdout), ghMissing });
|
|
50
60
|
},
|
|
51
61
|
);
|
|
52
62
|
});
|
|
53
63
|
}
|
|
64
|
+
|
|
65
|
+
/** Claude Code's PR status cadence (its `usePrStatus`): one lookup a minute while the session is in use. */
|
|
66
|
+
export const PR_POLL_INTERVAL_MS = 60_000;
|
|
67
|
+
/** Polling stops after this long without user input, and resumes on the next input. */
|
|
68
|
+
export const PR_IDLE_STOP_MS = 60 * 60_000;
|
|
69
|
+
/** A lookup slower than this turns polling off for the session (branch changes still look up). */
|
|
70
|
+
export const PR_SLOW_LOOKUP_MS = 4_000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* When the next PR lookup is due, relative to the last one: never sooner than
|
|
74
|
+
* PR_POLL_INTERVAL_MS after it, so a turn boundary asking for a refresh does
|
|
75
|
+
* not spawn `gh` more than once a minute. Undefined when polling should stop:
|
|
76
|
+
* turned off by a slow lookup, or no input for PR_IDLE_STOP_MS.
|
|
77
|
+
*/
|
|
78
|
+
export function nextPrPollDelay(state: { lastLookupAt: number; lastInputAt: number; disabled: boolean }, now: number): number | undefined {
|
|
79
|
+
if (state.disabled) return undefined;
|
|
80
|
+
if (now - state.lastInputAt >= PR_IDLE_STOP_MS) return undefined;
|
|
81
|
+
return Math.max(0, state.lastLookupAt + PR_POLL_INTERVAL_MS - now);
|
|
82
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* pi.exec is deliberately not used: it hardcodes shell:false and gives the
|
|
8
8
|
* child no stdin, while Claude Code hooks are shell command strings that read
|
|
9
9
|
* a JSON payload from stdin. (Ported from pi-code's runHookCommand, MIT — see
|
|
10
|
-
* docs/decisions.md.)
|
|
10
|
+
* working-docs/decisions.md.)
|
|
11
11
|
*
|
|
12
12
|
* The interpreter is Claude Code's: bash (Git Bash on Windows, honouring
|
|
13
13
|
* `CLAUDE_CODE_GIT_BASH_PATH`) unless the hook says `shell: "powershell"`, or
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* otherwise `close` never fires and the promise hangs past the timeout
|
|
23
23
|
* (Windows: `taskkill /T /F` on the leader's pid does the same)
|
|
24
24
|
* - setEncoding("utf8") so multi-byte characters can't be split across chunks
|
|
25
|
-
* -
|
|
25
|
+
* - a hook exceeding 1MB per stream is killed and reported as an output-limit
|
|
26
|
+
* failure, never parsed as a silently truncated JSON verdict
|
|
26
27
|
* - stdin errors ignored (a hook that exits without reading stdin — `exit 2`
|
|
27
28
|
* — would otherwise EPIPE-crash the write)
|
|
28
29
|
* - stdin JSON is newline-terminated: without a trailing "\n" a hook doing
|
|
@@ -41,13 +42,14 @@
|
|
|
41
42
|
* SIGCHLD-driven exit callback; with the process handle unref'd the loop
|
|
42
43
|
* counted itself empty and node exited 0 mid-tool, silently, before the
|
|
43
44
|
* hook's `close` ever fired — measured at ~55% of runs with a trivial
|
|
44
|
-
* `exit 0` hook (docs/one-shot-lsp-event-loop-drain.md has the same
|
|
45
|
+
* `exit 0` hook (working-docs/one-shot-lsp-event-loop-drain.md has the same
|
|
45
46
|
* mechanism for the LSP client). A ref'd child holds the loop until `close`.
|
|
46
47
|
*/
|
|
47
48
|
|
|
48
49
|
import type { ChildProcess } from "node:child_process";
|
|
49
50
|
import { detachedSpawnOptions, killProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
50
51
|
import { bashSpawn, POWERSHELL_UTF8_PREFIX, powerShellSpawn, type ShellSpawn, spawnShellCommand } from "../lib/shell-spawn.ts";
|
|
52
|
+
import type { FinishedRun } from "./protocol.ts";
|
|
51
53
|
import type { HookShell } from "./settings.ts";
|
|
52
54
|
|
|
53
55
|
/**
|
|
@@ -76,29 +78,12 @@ export function hookShellSpawn(shell: HookShell | undefined): { spec?: ShellSpaw
|
|
|
76
78
|
return { spec: resolved.spawn, shell: want };
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* null when the process was killed (timeout) or never spawned. Normalized on
|
|
82
|
-
* the timeout path rather than taken from `waitpid`: a group SIGKILL is not
|
|
83
|
-
* atomic, so the shell can be scheduled after its foreground child is killed
|
|
84
|
-
* and before its own signal lands, reap the child and exit(128+9) itself —
|
|
85
|
-
* `close` then reports a normal exit of 137 instead of death by signal
|
|
86
|
-
* (roughly 1% of timeouts under load, and never when the shell had exec'd
|
|
87
|
-
* away, leaving no shell to reap; findings §10.20). Callers should still
|
|
88
|
-
* prefer `timedOut`, which says what happened rather than what it looked
|
|
89
|
-
* like, but they no longer have to.
|
|
90
|
-
*/
|
|
91
|
-
exitCode: number | null;
|
|
92
|
-
timedOut: boolean;
|
|
93
|
-
/** Set when the child could not be spawned at all. */
|
|
94
|
-
spawnError?: string;
|
|
95
|
-
stdout: string;
|
|
96
|
-
stderr: string;
|
|
97
|
-
durationMs: number;
|
|
98
|
-
}
|
|
81
|
+
/** A finished run (`protocol.ts FinishedRun`, field docs there) and how long it took. */
|
|
82
|
+
export type HookRunResult = FinishedRun & { durationMs: number };
|
|
99
83
|
|
|
100
84
|
export interface HookRunOptions {
|
|
101
85
|
cwd: string;
|
|
86
|
+
signal?: AbortSignal;
|
|
102
87
|
/** Seconds, Claude Code convention. Clamped to [1, MAX_TIMEOUT_S]. */
|
|
103
88
|
timeoutSeconds?: number;
|
|
104
89
|
/** Exposed to the hook as CLAUDE_PROJECT_DIR; defaults to cwd. */
|
|
@@ -129,6 +114,10 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
|
|
|
129
114
|
const started = Date.now();
|
|
130
115
|
|
|
131
116
|
return new Promise((resolve) => {
|
|
117
|
+
if (opts.signal?.aborted) {
|
|
118
|
+
resolve({ exitCode: null, timedOut: false, aborted: true, stdout: "", stderr: "", durationMs: Date.now() - started });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
132
121
|
let child: ChildProcess;
|
|
133
122
|
const failed = (spawnError: string) =>
|
|
134
123
|
resolve({ exitCode: null, timedOut: false, spawnError, stdout: "", stderr: "", durationMs: Date.now() - started });
|
|
@@ -175,14 +164,26 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
|
|
|
175
164
|
let stdout = "";
|
|
176
165
|
let stderr = "";
|
|
177
166
|
let timedOut = false;
|
|
167
|
+
let aborted = false;
|
|
168
|
+
let outputLimitExceeded = false;
|
|
169
|
+
const outputBytes = { stdout: 0, stderr: 0 };
|
|
178
170
|
let settled = false;
|
|
171
|
+
const onAbort = () => {
|
|
172
|
+
aborted = true;
|
|
173
|
+
killProcessTree(child, "SIGKILL");
|
|
174
|
+
};
|
|
175
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
179
176
|
|
|
180
177
|
const capture = (sink: "stdout" | "stderr") => (chunk: string) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
if (outputLimitExceeded) return;
|
|
179
|
+
outputBytes[sink] += Buffer.byteLength(chunk, "utf8");
|
|
180
|
+
if (outputBytes[sink] > MAX_OUTPUT_BYTES) {
|
|
181
|
+
outputLimitExceeded = true;
|
|
182
|
+
killProcessTree(child, "SIGKILL");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (sink === "stdout") stdout += chunk;
|
|
186
|
+
else stderr += chunk;
|
|
186
187
|
};
|
|
187
188
|
child.stdout?.setEncoding("utf8");
|
|
188
189
|
child.stderr?.setEncoding("utf8");
|
|
@@ -200,7 +201,8 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
|
|
|
200
201
|
if (settled) return;
|
|
201
202
|
settled = true;
|
|
202
203
|
clearTimeout(timer);
|
|
203
|
-
|
|
204
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
205
|
+
resolve({ ...result, ...(aborted ? { aborted: true } : {}), ...(outputLimitExceeded ? { outputLimitExceeded: true } : {}), durationMs: Date.now() - started });
|
|
204
206
|
};
|
|
205
207
|
|
|
206
208
|
// Settles on exit plus a short stdio grace, not on `close`: a grandchild
|
|
@@ -211,7 +213,7 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
|
|
|
211
213
|
// keeps "we killed it" from ever looking like an ordinary non-zero exit,
|
|
212
214
|
// which fails OPEN downstream.
|
|
213
215
|
waitForChildExit(child).then(
|
|
214
|
-
({ code }) => finish({ exitCode: timedOut ? null : code, timedOut, stdout, stderr }),
|
|
216
|
+
({ code }) => finish({ exitCode: timedOut || aborted || outputLimitExceeded ? null : code, timedOut, stdout, stderr }),
|
|
215
217
|
(error: Error) => finish({ exitCode: null, timedOut, spawnError: error.message, stdout, stderr }),
|
|
216
218
|
);
|
|
217
219
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* A hook can block anything, loaders included (tool_search/skill/
|
|
17
17
|
* structured_output) — full CC fidelity, the user's config is the user's
|
|
18
|
-
* choice; see docs/findings.md for the foot-gun note. A hook can never
|
|
18
|
+
* choice; see working-docs/findings.md for the foot-gun note. A hook can never
|
|
19
19
|
* pre-approve: "allow" is informational and the permission gate still runs.
|
|
20
20
|
*
|
|
21
21
|
* pi's emitToolCall has no try/catch around handlers, so every hook dispatch
|
|
@@ -38,12 +38,15 @@
|
|
|
38
38
|
* (settings, consent, logging) and a payload naming the child.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
41
42
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
42
43
|
import { RunOutcomeLatch } from "../lib/interrupt.ts";
|
|
44
|
+
import { SESSION_WORK_CHANNEL, type SessionWorkQuery } from "../lib/session-work.ts";
|
|
43
45
|
import { claudeConfigDir } from "../lib/paths.ts";
|
|
44
46
|
import { defaultDiscoverRoots } from "../lib/plugins.ts";
|
|
45
47
|
import { appendHookLog, formatDebugLine, hooksDebugEnabled, hooksLogPath } from "./debug.ts";
|
|
46
48
|
import { runHookCommand } from "./executor.ts";
|
|
49
|
+
import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "./compaction.ts";
|
|
47
50
|
import { changedSince, FORMATTER_NOTICE, type FileSnapshot, fileToolTarget, snapshotFile } from "./formatter-notice.ts";
|
|
48
51
|
import { matcherApplies, ccToolName, toolMatchCandidates, ccToolInput, nativeToolInput } from "./matcher.ts";
|
|
49
52
|
import { loadPluginHooks } from "./plugin-hooks.ts";
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
type CcHookEvent,
|
|
53
56
|
type HookOutcome,
|
|
54
57
|
type HookStdinPayload,
|
|
58
|
+
cancelledOutcome,
|
|
55
59
|
interpretHookResult,
|
|
56
60
|
} from "./protocol.ts";
|
|
57
61
|
import { type HookCommand, type HooksSource, loadHookSettings } from "./settings.ts";
|
|
@@ -59,6 +63,12 @@ import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
|
59
63
|
import { REMINDER_CHANNEL, wrapReminder } from "../lib/reminders.ts";
|
|
60
64
|
import { type ChildHookCall, type ChildHookResult, type HookBridge, SUBAGENT_HOOK_CHANNEL, type SubagentHookPayload } from "./subagent-bridge.ts";
|
|
61
65
|
import { projectHooksApproved } from "./trust.ts";
|
|
66
|
+
import { QueuedDelivery } from "../lib/queued-delivery.ts";
|
|
67
|
+
import { SKILL_INVOCATION_TYPE, type SkillInvocationDetails } from "../skill/invoke.ts";
|
|
68
|
+
import { isNotificationDetails } from "../lib/notifications.ts";
|
|
69
|
+
import { consentDialog } from "../lib/consent-dialogs.ts";
|
|
70
|
+
import { isUserRun } from "../lib/prompt-options.ts";
|
|
71
|
+
import { followWorkCwd } from "../lib/worktree-channel.ts";
|
|
62
72
|
|
|
63
73
|
/** Claude Code's `hook_additional_context` attachment text (utils/messages.ts). */
|
|
64
74
|
export function hookContextText(event: CcHookEvent, text: string): string {
|
|
@@ -95,17 +105,39 @@ interface HookDispatchCtx {
|
|
|
95
105
|
cwd: string;
|
|
96
106
|
hasUI: boolean;
|
|
97
107
|
sessionManager: { getSessionId(): string; getSessionFile(): string | undefined; getSessionDir?(): string | undefined };
|
|
98
|
-
ui: { confirm(title: string, message: string): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
|
|
108
|
+
ui: { confirm(title: string, message: string, options?: { signal?: AbortSignal }): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
|
|
109
|
+
signal?: AbortSignal;
|
|
99
110
|
/** Set on the shutdown snapshot: no consent prompt, no notices — the session is gone. */
|
|
100
111
|
sessionEnded?: true;
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/** Claude Code's default cap on consecutive Stop-hook continuations in one user turn. */
|
|
115
|
+
const MAX_STOP_CONTINUATIONS = 8;
|
|
116
|
+
|
|
117
|
+
/** The customType of the Stop hook's continuation message. */
|
|
118
|
+
const STOP_CONTINUATION_TYPE = "one-code:hook-stop";
|
|
119
|
+
|
|
120
|
+
/** Whether a `before_agent_start` opens the Stop hook's own continuation run (pi 1.2.0 fires it for custom runs). */
|
|
121
|
+
function isStopContinuation(event: unknown): boolean {
|
|
122
|
+
const run = event as { messageType?: string; message?: { customType?: string } };
|
|
123
|
+
return run.messageType === "custom" && run.message?.customType === STOP_CONTINUATION_TYPE;
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
export default function hooksExtension(pi: ExtensionAPI) {
|
|
104
|
-
|
|
127
|
+
const workCwd = followWorkCwd(pi.events);
|
|
128
|
+
let stopHookBlocks = 0;
|
|
105
129
|
/** Context from UserPromptSubmit / SessionStart / PostCompact hooks, delivered with the next prompt. */
|
|
106
130
|
let pendingPromptContext: Array<{ event: CcHookEvent; text: string }> = [];
|
|
131
|
+
/**
|
|
132
|
+
* UserPromptSubmit context for messages queued mid-turn, held until pi
|
|
133
|
+
* delivers the message (lib/queued-delivery.ts): such a message never
|
|
134
|
+
* reaches before_agent_start.
|
|
135
|
+
*/
|
|
136
|
+
const queuedPromptContext = new QueuedDelivery<string>();
|
|
107
137
|
/** The last context seen, for bridged child calls (dispatched parent-side). */
|
|
108
138
|
let lastCtx: ExtensionContext | undefined;
|
|
139
|
+
/** True once a prompt opened a turn in this session (before_agent_start). */
|
|
140
|
+
let prompted = false;
|
|
109
141
|
/**
|
|
110
142
|
* The in-flight SessionStart hook dispatch. It runs OFF the `session_start`
|
|
111
143
|
* path so the editor opens immediately (a slow hook used to hold the prompt
|
|
@@ -116,6 +148,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
116
148
|
let sessionStartPending: Promise<void> | undefined;
|
|
117
149
|
/** Bumped on every session_start; a superseded dispatch checks it before writing context. */
|
|
118
150
|
let sessionGen = 0;
|
|
151
|
+
/** Covers startup hooks and other dispatches that have no active turn signal. */
|
|
152
|
+
let sessionAbort = new AbortController();
|
|
119
153
|
|
|
120
154
|
const notify = (ctx: HookDispatchCtx, message: string) => {
|
|
121
155
|
if (ctx.sessionEnded) return; // session gone; nowhere to show it
|
|
@@ -131,7 +165,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
131
165
|
): Promise<MatchedHook[]> => {
|
|
132
166
|
const claudeDir = claudeConfigDir();
|
|
133
167
|
const loaded = loadHookSettings(claudeDir, ctx.cwd);
|
|
134
|
-
const pluginSources = loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
|
|
168
|
+
const pluginSources = loaded.disabled ? [] : loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
|
|
135
169
|
if (hooksDebugEnabled()) {
|
|
136
170
|
for (const diagnostic of loaded.diagnostics) process.stderr.write(`[hooks] ${diagnostic}\n`);
|
|
137
171
|
}
|
|
@@ -143,7 +177,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
143
177
|
projectAllowed = await projectHooksApproved(ctx.cwd, projectSources, {
|
|
144
178
|
hasUI: ctx.hasUI,
|
|
145
179
|
noPrompt: ctx.sessionEnded,
|
|
146
|
-
|
|
180
|
+
signal: ctx.signal,
|
|
181
|
+
confirm: (title, message) => consentDialog(pi.events, (signal) => ctx.ui.confirm(title, message, { signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })),
|
|
147
182
|
notify: (message) => notify(ctx, message),
|
|
148
183
|
});
|
|
149
184
|
}
|
|
@@ -168,7 +203,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
168
203
|
const basePayload = (ctx: HookDispatchCtx, event: CcHookEvent): HookStdinPayload => ({
|
|
169
204
|
session_id: ctx.sessionManager.getSessionId(),
|
|
170
205
|
transcript_path: ctx.sessionManager.getSessionFile() ?? "",
|
|
171
|
-
cwd: ctx.cwd,
|
|
206
|
+
cwd: workCwd(ctx.cwd),
|
|
172
207
|
hook_event_name: event,
|
|
173
208
|
});
|
|
174
209
|
|
|
@@ -181,20 +216,27 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
181
216
|
ctx: HookDispatchCtx,
|
|
182
217
|
event: CcHookEvent,
|
|
183
218
|
matchValue: { candidates?: string[]; ignoreMatcher?: boolean },
|
|
184
|
-
|
|
219
|
+
payloadOrFactory: HookStdinPayload | (() => Promise<HookStdinPayload | undefined>),
|
|
185
220
|
/** Runs once, only when hooks will actually run — the point where a caller can snapshot state the hooks may change. */
|
|
186
221
|
willRun?: () => void,
|
|
187
222
|
): Promise<HookOutcome> => {
|
|
188
223
|
const merged: HookOutcome = {};
|
|
224
|
+
const signal = ctx.sessionEnded ? ctx.signal : ctx.signal ? AbortSignal.any([ctx.signal, sessionAbort.signal]) : sessionAbort.signal;
|
|
189
225
|
try {
|
|
190
|
-
const hooks = await collectHooks(ctx, event, matchValue);
|
|
191
|
-
|
|
226
|
+
const hooks = await collectHooks({ ...ctx, signal }, event, matchValue);
|
|
227
|
+
const payload = typeof payloadOrFactory === "function" ? await payloadOrFactory() : payloadOrFactory;
|
|
228
|
+
if (!payload || hooks.length === 0) return merged;
|
|
229
|
+
// Cancelled before the hooks ran: the gating events block, as a mid-run cancel does.
|
|
230
|
+
if (signal?.aborted) return cancelledOutcome(event);
|
|
192
231
|
willRun?.();
|
|
193
232
|
const stdin = JSON.stringify(payload);
|
|
194
|
-
const outcomes = await Promise.
|
|
233
|
+
const outcomes = await Promise.allSettled(
|
|
195
234
|
hooks.map(async ({ source, hook }) => {
|
|
196
235
|
const run = await runHookCommand(hook.command, stdin, {
|
|
197
|
-
|
|
236
|
+
// The calling session's worktree or child's directory; settings,
|
|
237
|
+
// consent and CLAUDE_PROJECT_DIR still belong to the parent project.
|
|
238
|
+
cwd: payload.cwd,
|
|
239
|
+
signal,
|
|
198
240
|
timeoutSeconds: hook.timeout,
|
|
199
241
|
shell: hook.shell,
|
|
200
242
|
projectDir: ctx.cwd,
|
|
@@ -221,7 +263,16 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
221
263
|
return outcome;
|
|
222
264
|
}),
|
|
223
265
|
);
|
|
224
|
-
|
|
266
|
+
if (signal?.aborted) return cancelledOutcome(event);
|
|
267
|
+
for (const result of outcomes) {
|
|
268
|
+
// One broken response must not erase another hook's deny or let the
|
|
269
|
+
// tool run while the other hooks are still deciding.
|
|
270
|
+
if (result.status === "rejected") {
|
|
271
|
+
if (hooksDebugEnabled()) process.stderr.write(`[hooks] hook error (${event}): ${String(result.reason)}\n`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const outcome = result.value;
|
|
275
|
+
if (outcome.preventContinuation) merged.preventContinuation = true;
|
|
225
276
|
// A block reason becomes model-facing text (the tool result, or a
|
|
226
277
|
// Stop follow-up), so it is bounded like the other hook texts —
|
|
227
278
|
// stderr on an exit-2 hook is easily a full stack trace (review M2).
|
|
@@ -291,7 +342,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
291
342
|
// to it: this extension awaits the hook and loads before file-tracker, so
|
|
292
343
|
// the tracker records the hook's version as the model's own write
|
|
293
344
|
// (formatter-notice.ts, review M1). Snapshot only when hooks will run.
|
|
294
|
-
const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input,
|
|
345
|
+
const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, payload.cwd);
|
|
295
346
|
let before: FileSnapshot | undefined;
|
|
296
347
|
const outcome = await dispatch(ctx, "PostToolUse", { candidates: toolMatchCandidates(event.toolName) }, payload, () => {
|
|
297
348
|
if (target) before = snapshotFile(target);
|
|
@@ -305,57 +356,119 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
305
356
|
[outcome.additionalContext && wrapReminder(hookContextText("PostToolUse", outcome.additionalContext)), formatterNotice]
|
|
306
357
|
.filter(Boolean)
|
|
307
358
|
.join("\n") || undefined,
|
|
308
|
-
});
|
|
359
|
+
}, event.toolName);
|
|
309
360
|
if (!content) return undefined;
|
|
310
361
|
return { content, isError: event.isError };
|
|
311
362
|
});
|
|
312
363
|
|
|
313
|
-
/**
|
|
364
|
+
/**
|
|
365
|
+
* Await the backgrounded SessionStart dispatch, if one is pending. Every
|
|
366
|
+
* caller awaits the same dispatch, and it is forgotten only once it settles:
|
|
367
|
+
* a notification opening the first turn and a prompt typed meanwhile both
|
|
368
|
+
* wait for it, rather than the second returning at once.
|
|
369
|
+
*/
|
|
314
370
|
const drainSessionStart = async () => {
|
|
315
|
-
if (!sessionStartPending) return;
|
|
316
371
|
const pending = sessionStartPending;
|
|
317
|
-
|
|
372
|
+
if (!pending) return;
|
|
318
373
|
try {
|
|
319
374
|
await pending;
|
|
320
375
|
} catch {
|
|
321
376
|
// dispatch already reports its own failures; never block the turn on one.
|
|
322
377
|
}
|
|
378
|
+
if (sessionStartPending === pending) sessionStartPending = undefined;
|
|
323
379
|
};
|
|
324
380
|
|
|
325
381
|
// ---- UserPromptSubmit ---------------------------------------------------
|
|
326
382
|
pi.on("input", async (event, ctx) => {
|
|
327
383
|
if (event.source === "extension") return undefined;
|
|
328
|
-
|
|
384
|
+
// Set for a message queued mid-turn (pi 0.86+ fires `input` for
|
|
385
|
+
// steer/followUp). The Stop latch is reset in before_agent_start, where a
|
|
386
|
+
// turn really starts: pi re-checks streaming after these handlers, so a
|
|
387
|
+
// queued message whose turn ended during the hook opens a new turn.
|
|
388
|
+
const queued = event.streamingBehavior;
|
|
389
|
+
const gen = sessionGen;
|
|
329
390
|
// Drain the backgrounded SessionStart dispatch before this prompt's own
|
|
330
391
|
// UserPromptSubmit context, so their order in the first turn is unchanged
|
|
331
392
|
// (SessionStart context precedes UserPromptSubmit). before_agent_start is
|
|
332
393
|
// the backstop for turns with no input event (one-shot, programmatic).
|
|
333
394
|
await drainSessionStart();
|
|
395
|
+
if (gen !== sessionGen) return { action: "handled" as const };
|
|
334
396
|
const payload: HookStdinPayload = { ...basePayload(ctx, "UserPromptSubmit"), prompt: event.text };
|
|
335
397
|
const outcome = await dispatch(ctx, "UserPromptSubmit", { ignoreMatcher: true }, payload);
|
|
398
|
+
if (gen !== sessionGen) return { action: "handled" as const };
|
|
336
399
|
if (outcome.block) {
|
|
337
400
|
notify(ctx, `Prompt blocked by UserPromptSubmit hook: ${outcome.block.reason}`);
|
|
338
401
|
return { action: "handled" as const };
|
|
339
402
|
}
|
|
340
|
-
if (outcome.additionalContext)
|
|
403
|
+
if (!outcome.additionalContext) return undefined;
|
|
404
|
+
// A queued message's context waits for pi to deliver that message
|
|
405
|
+
// (message_start below), so it rides the same request.
|
|
406
|
+
if (queued) queuedPromptContext.hold(event.text, hookContextText("UserPromptSubmit", outcome.additionalContext));
|
|
407
|
+
else pendingPromptContext.push({ event: "UserPromptSubmit", text: outcome.additionalContext });
|
|
341
408
|
return undefined;
|
|
342
409
|
});
|
|
343
410
|
|
|
411
|
+
// pi awaits this before the request that carries the delivered message, so a
|
|
412
|
+
// one-shot emitted here is pinned to that message: after the prompt, as the
|
|
413
|
+
// before_agent_start message is for a prompt that opens a turn.
|
|
414
|
+
pi.on("message_start", async (event) => {
|
|
415
|
+
const gen = sessionGen;
|
|
416
|
+
const message = event.message;
|
|
417
|
+
const emit = (text: string) => pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append" });
|
|
418
|
+
if (message.role === "user") {
|
|
419
|
+
if (queuedPromptContext.isEmpty) return;
|
|
420
|
+
const text = queuedPromptContext.release(contentText(message.content, ""));
|
|
421
|
+
if (text) emit(text);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// A `/skill:` command the skill extension took over arrives as its hidden
|
|
425
|
+
// message, carrying the typed text. It opens the turn when the command
|
|
426
|
+
// was idle, through a sendMessage that never reaches before_agent_start,
|
|
427
|
+
// so the prompt context waiting there rides this message too.
|
|
428
|
+
if (message.role !== "custom") return;
|
|
429
|
+
// A background completion that opens the session's first turn
|
|
430
|
+
// (lib/prompt-options.ts) skips before_agent_start as well, so the
|
|
431
|
+
// SessionStart context a typed first prompt would carry rides it.
|
|
432
|
+
const opensFirstTurn = !prompted && isNotificationDetails(message.details);
|
|
433
|
+
if (message.customType !== SKILL_INVOCATION_TYPE && !opensFirstTurn) return;
|
|
434
|
+
if (opensFirstTurn) await drainSessionStart();
|
|
435
|
+
if (gen !== sessionGen) return;
|
|
436
|
+
const input = (message.details as Partial<SkillInvocationDetails> | undefined)?.input;
|
|
437
|
+
const queued = typeof input === "string" ? queuedPromptContext.release(input) : undefined;
|
|
438
|
+
if (queued) emit(queued);
|
|
439
|
+
const waiting = pendingPromptContext;
|
|
440
|
+
pendingPromptContext = [];
|
|
441
|
+
for (const { event: hookEvent, text } of waiting) emit(hookContextText(hookEvent, text));
|
|
442
|
+
});
|
|
443
|
+
|
|
344
444
|
// Prompt/session/compaction hook context rides the turn it belongs to as a
|
|
345
445
|
// custom message pi appends right AFTER the user's prompt (the
|
|
346
446
|
// before_agent_start `message` return). An idle `sendMessage` from here
|
|
347
447
|
// landed it BEFORE the prompt (pi appends first, then adds the user message).
|
|
348
|
-
pi.on("before_agent_start", async (
|
|
448
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
449
|
+
const gen = sessionGen;
|
|
349
450
|
lastCtx = ctx;
|
|
451
|
+
prompted = true;
|
|
350
452
|
// The SessionStart dispatch runs in the background from session_start; the
|
|
351
453
|
// first turn must wait for it so its additionalContext is present (and, on
|
|
352
454
|
// first run, so its approval dialog gates execution before the model runs).
|
|
353
455
|
// The `input` handler usually drained it already; this is the backstop for
|
|
354
456
|
// turns with no input event.
|
|
355
457
|
await drainSessionStart();
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
458
|
+
if (gen !== sessionGen) return;
|
|
459
|
+
// Every run that opens lands here (on pi 1.2.0 also one an extension
|
|
460
|
+
// opened; a queued delivery does not). Only the Stop hook's own
|
|
461
|
+
// continuation keeps the cap counting: a notification or scheduled run
|
|
462
|
+
// starts a fresh turn, as a prompt does.
|
|
463
|
+
if (!isStopContinuation(event)) stopHookBlocks = 0;
|
|
464
|
+
// UserPromptSubmit context belongs to the user's prompt; SessionStart and
|
|
465
|
+
// PostCompact context belong to whichever run opens next, which on pi
|
|
466
|
+
// 1.2.0 can be one a notification or scheduled fire opened. Its
|
|
467
|
+
// message_start can no longer deliver them: `prompted` is set above.
|
|
468
|
+
const userRun = isUserRun(event);
|
|
469
|
+
const texts = pendingPromptContext.filter((entry) => userRun || entry.event !== "UserPromptSubmit");
|
|
470
|
+
if (texts.length === 0) return;
|
|
471
|
+
pendingPromptContext = pendingPromptContext.filter((entry) => !texts.includes(entry));
|
|
359
472
|
return {
|
|
360
473
|
message: {
|
|
361
474
|
customType: "one-code:hook-context",
|
|
@@ -367,8 +480,13 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
367
480
|
|
|
368
481
|
// ---- SessionStart / SessionEnd -----------------------------------------
|
|
369
482
|
const dispatchSessionStart = async (ctx: ExtensionContext, source: string, gen: number) => {
|
|
370
|
-
|
|
371
|
-
|
|
483
|
+
// Reserve project-hook consent synchronously, before MCP and external
|
|
484
|
+
// imports enqueue their dialogs. Only defer the payload: worktree's later
|
|
485
|
+
// synchronous session_start handler must restore cwd before we capture it.
|
|
486
|
+
const outcome = await dispatch(ctx, "SessionStart", { candidates: [source] }, async () => {
|
|
487
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
488
|
+
return gen === sessionGen ? { ...basePayload(ctx, "SessionStart"), source } : undefined;
|
|
489
|
+
});
|
|
372
490
|
// A later session_start (rapid /clear, resume) may have superseded this one
|
|
373
491
|
// while its hook ran; discard the stale result instead of pushing it into
|
|
374
492
|
// the new session's context (`pendingPromptContext` is shared and reset).
|
|
@@ -378,10 +496,15 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
378
496
|
};
|
|
379
497
|
|
|
380
498
|
pi.on("session_start", (event, ctx) => {
|
|
499
|
+
sessionAbort.abort();
|
|
500
|
+
sessionAbort = new AbortController();
|
|
381
501
|
lastCtx = ctx;
|
|
382
502
|
const gen = ++sessionGen;
|
|
383
503
|
pendingPromptContext = [];
|
|
504
|
+
queuedPromptContext.clear();
|
|
384
505
|
sessionStartPending = undefined;
|
|
506
|
+
prompted = false;
|
|
507
|
+
stopHookBlocks = 0;
|
|
385
508
|
// Publish the child hook bridge (subagent-bridge.ts). The closures read
|
|
386
509
|
// live parent state per call, so once per session start is enough.
|
|
387
510
|
pi.events.emit(SUBAGENT_HOOK_CHANNEL, { bridge: childHookBridge } satisfies SubagentHookPayload);
|
|
@@ -410,9 +533,21 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
410
533
|
agent_id: call.sessionId,
|
|
411
534
|
agent_type: call.agentType,
|
|
412
535
|
});
|
|
536
|
+
/**
|
|
537
|
+
* The parent's settings/consent context with the CHILD's run signal. A
|
|
538
|
+
* background child outlives the parent's turn, so Esc in the parent must
|
|
539
|
+
* not skip a child's deny hook; stopping the child must cancel its hooks.
|
|
540
|
+
*/
|
|
541
|
+
const childDispatchCtx = (ctx: ExtensionContext, call: ChildHookCall): HookDispatchCtx => ({
|
|
542
|
+
cwd: ctx.cwd,
|
|
543
|
+
hasUI: ctx.hasUI,
|
|
544
|
+
sessionManager: ctx.sessionManager,
|
|
545
|
+
ui: ctx.ui,
|
|
546
|
+
signal: call.signal,
|
|
547
|
+
});
|
|
413
548
|
const childHookBridge: HookBridge = {
|
|
414
549
|
async preToolUse(call) {
|
|
415
|
-
const ctx = lastCtx;
|
|
550
|
+
const ctx = lastCtx && childDispatchCtx(lastCtx, call);
|
|
416
551
|
if (!ctx) return {};
|
|
417
552
|
const outcome = await dispatch(ctx, "PreToolUse", { candidates: toolMatchCandidates(call.toolName) }, childPayload(call, "PreToolUse"));
|
|
418
553
|
return {
|
|
@@ -422,7 +557,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
422
557
|
};
|
|
423
558
|
},
|
|
424
559
|
async postToolUse(result: ChildHookResult) {
|
|
425
|
-
const ctx = lastCtx;
|
|
560
|
+
const ctx = lastCtx && childDispatchCtx(lastCtx, result);
|
|
426
561
|
if (!ctx) return {};
|
|
427
562
|
const payload: HookStdinPayload = {
|
|
428
563
|
...childPayload(result, "PostToolUse"),
|
|
@@ -449,6 +584,9 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
449
584
|
};
|
|
450
585
|
|
|
451
586
|
pi.on("session_shutdown", (event, ctx) => {
|
|
587
|
+
// Also cancels a queued SessionStart before its deferred dispatch touches a disposed context.
|
|
588
|
+
sessionGen += 1;
|
|
589
|
+
sessionAbort.abort();
|
|
452
590
|
// A reload re-runs the extensions but keeps the conversation (pi's
|
|
453
591
|
// AgentSession.reload): not a session end, Claude Code has no such event.
|
|
454
592
|
if (event.reason === "reload") return;
|
|
@@ -492,19 +630,38 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
492
630
|
});
|
|
493
631
|
|
|
494
632
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
633
|
+
const gen = sessionGen;
|
|
634
|
+
// A queued message pi never delivered as typed (expanded, or taken over by
|
|
635
|
+
// an extension) keeps no context for a later, unrelated prompt.
|
|
636
|
+
queuedPromptContext.clear();
|
|
495
637
|
// CC skips Stop when the turn ended on an API error (a blocking hook would
|
|
496
638
|
// spiral: error → block → retry → error) or on a user interrupt (its query
|
|
497
639
|
// loop returns on the abort signal before the stop-hook step). An empty latch
|
|
498
640
|
// means no run ended in this turn, which is not a response to stop on either.
|
|
499
641
|
if (stopOutcome.take() !== "ok") return;
|
|
500
|
-
|
|
642
|
+
// Claude Code's two lists of what will wake the session later, filled by the
|
|
643
|
+
// background extension (lib/session-work.ts); empty arrays when none.
|
|
644
|
+
const work: SessionWorkQuery = { crons: [], tasks: [] };
|
|
645
|
+
pi.events.emit(SESSION_WORK_CHANNEL, work);
|
|
646
|
+
const payload: HookStdinPayload = {
|
|
647
|
+
...basePayload(ctx, "Stop"),
|
|
648
|
+
stop_hook_active: stopHookBlocks > 0,
|
|
649
|
+
background_tasks: work.tasks,
|
|
650
|
+
session_crons: work.crons,
|
|
651
|
+
};
|
|
501
652
|
const outcome = await dispatch(ctx, "Stop", { ignoreMatcher: true }, payload);
|
|
502
|
-
if (!outcome.block) return;
|
|
503
|
-
|
|
653
|
+
if (gen !== sessionGen || outcome.preventContinuation || !outcome.block) return;
|
|
654
|
+
// Claude Code limits repeated Stop blocks to eight continuations. A hook
|
|
655
|
+
// that ignores stop_hook_active must not run up an unbounded model loop.
|
|
656
|
+
if (stopHookBlocks >= MAX_STOP_CONTINUATIONS) {
|
|
657
|
+
notify(ctx, `Stop hook continuation limit reached (${MAX_STOP_CONTINUATIONS}); ending the turn.`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
stopHookBlocks += 1;
|
|
504
661
|
try {
|
|
505
662
|
pi.sendMessage(
|
|
506
663
|
{
|
|
507
|
-
customType:
|
|
664
|
+
customType: STOP_CONTINUATION_TYPE,
|
|
508
665
|
content: `Stop hook blocked stopping: ${outcome.block.reason}`,
|
|
509
666
|
display: false,
|
|
510
667
|
},
|
|
@@ -533,10 +690,19 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
533
690
|
notify(ctx, `Compaction cancelled by PreCompact hook: ${outcome.block.reason}`);
|
|
534
691
|
return { cancel: true };
|
|
535
692
|
}
|
|
693
|
+
// These instructions belong to the summarizer, not the next user turn.
|
|
694
|
+
// The signal keeps them scoped to this attempt if compaction is cancelled.
|
|
695
|
+
if (outcome.additionalContext) {
|
|
696
|
+
pi.events.emit(PRECOMPACT_INSTRUCTIONS_CHANNEL, {
|
|
697
|
+
signal: event.signal,
|
|
698
|
+
instructions: outcome.additionalContext,
|
|
699
|
+
} satisfies PreCompactInstructions);
|
|
700
|
+
}
|
|
536
701
|
return undefined;
|
|
537
702
|
});
|
|
538
703
|
|
|
539
704
|
pi.on("session_compact", async (event, ctx) => {
|
|
705
|
+
const gen = sessionGen;
|
|
540
706
|
const trigger = compactTrigger(event);
|
|
541
707
|
const payload: HookStdinPayload = {
|
|
542
708
|
...basePayload(ctx, "PostCompact"),
|
|
@@ -544,6 +710,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
544
710
|
compact_summary: event.compactionEntry.summary,
|
|
545
711
|
};
|
|
546
712
|
const outcome = await dispatch(ctx, "PostCompact", { candidates: [trigger] }, payload);
|
|
713
|
+
if (gen !== sessionGen) return;
|
|
547
714
|
if (outcome.additionalContext) pendingPromptContext.push({ event: "PostCompact", text: outcome.additionalContext });
|
|
548
715
|
// CC fires SessionStart(source: "compact") after compaction. Awaited inline
|
|
549
716
|
// (not backgrounded), so the current generation is still in force.
|
|
@@ -38,6 +38,9 @@ const CC_CANONICAL: Record<string, string> = {
|
|
|
38
38
|
task_stop: "TaskStop",
|
|
39
39
|
monitor: "Monitor",
|
|
40
40
|
schedule_wakeup: "ScheduleWakeup",
|
|
41
|
+
cron_create: "CronCreate",
|
|
42
|
+
cron_list: "CronList",
|
|
43
|
+
cron_delete: "CronDelete",
|
|
41
44
|
send_message: "SendMessage",
|
|
42
45
|
enter_worktree: "EnterWorktree",
|
|
43
46
|
exit_worktree: "ExitWorktree",
|