one-code-extension 0.4.1 → 0.6.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 +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -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 +124 -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 +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -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 +56 -0
- package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- 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 +141 -25
- 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 +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -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 +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -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 +26 -12
- 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/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -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 +52 -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 +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -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/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- 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 +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -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 +357 -73
- 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 +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -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/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -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 +24 -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 +112 -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 +129 -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 +159 -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 +76 -12
- 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 +90 -43
- 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 +220 -121
- 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 +164 -73
- 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/model-default/index.ts +86 -19
- 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 +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -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 +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- 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 +64 -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 +14 -7
- package/extensions/recap/index.ts +73 -71
- 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/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- 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 +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -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/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -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 +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- 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 +204 -70
- 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/index.ts +366 -144
- 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 +108 -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 +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -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 +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -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/tool-search/announce.ts +0 -42
|
@@ -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
|
|
@@ -26,11 +26,17 @@
|
|
|
26
26
|
* never as a message of its own: PreToolUse context is queued as a one-shot on
|
|
27
27
|
* the reminder channel, so it lands INSIDE the tool result it belongs to,
|
|
28
28
|
* persisted, at no extra turn; PostToolUse context is appended to the result
|
|
29
|
-
* the same way; prompt
|
|
30
|
-
*
|
|
29
|
+
* the same way; prompt, session and compaction context is a one-shot too,
|
|
30
|
+
* persisted as a `one-code:pending` entry when the hook answers, so it rides
|
|
31
|
+
* the next request whatever opens it (a prompt, a notification, the
|
|
32
|
+
* continuation after a mid-turn compaction) and counts as delivered only when
|
|
33
|
+
* that request's reply stays on the branch (lib/ledger/fold.ts). Until
|
|
31
34
|
* 2026-09-05 each was a custom steer (one extra LLM turn per hook per tool,
|
|
32
35
|
* pi draining one steer per call) and prompt context landed BEFORE the prompt
|
|
33
|
-
* (STEERING-REVIEW-2026-09-05 M6)
|
|
36
|
+
* (STEERING-REVIEW-2026-09-05 M6); until 2026-10-21 prompt, session and
|
|
37
|
+
* compaction context was a `before_agent_start` message, taken off its queue
|
|
38
|
+
* before the request went out and held until a run started (round 3,
|
|
39
|
+
* R3-M14).
|
|
34
40
|
*
|
|
35
41
|
* Subagents: the tool hooks also run for a child's tool calls, through the
|
|
36
42
|
* bridge published on `SUBAGENT_HOOK_CHANNEL` (subagent-bridge.ts): the child
|
|
@@ -38,12 +44,16 @@
|
|
|
38
44
|
* (settings, consent, logging) and a payload naming the child.
|
|
39
45
|
*/
|
|
40
46
|
|
|
47
|
+
import { randomUUID } from "node:crypto";
|
|
48
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
41
49
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
42
50
|
import { RunOutcomeLatch } from "../lib/interrupt.ts";
|
|
51
|
+
import { SESSION_WORK_CHANNEL, type SessionWorkQuery } from "../lib/session-work.ts";
|
|
43
52
|
import { claudeConfigDir } from "../lib/paths.ts";
|
|
44
53
|
import { defaultDiscoverRoots } from "../lib/plugins.ts";
|
|
45
54
|
import { appendHookLog, formatDebugLine, hooksDebugEnabled, hooksLogPath } from "./debug.ts";
|
|
46
55
|
import { runHookCommand } from "./executor.ts";
|
|
56
|
+
import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "./compaction.ts";
|
|
47
57
|
import { changedSince, FORMATTER_NOTICE, type FileSnapshot, fileToolTarget, snapshotFile } from "./formatter-notice.ts";
|
|
48
58
|
import { matcherApplies, ccToolName, toolMatchCandidates, ccToolInput, nativeToolInput } from "./matcher.ts";
|
|
49
59
|
import { loadPluginHooks } from "./plugin-hooks.ts";
|
|
@@ -52,13 +62,18 @@ import {
|
|
|
52
62
|
type CcHookEvent,
|
|
53
63
|
type HookOutcome,
|
|
54
64
|
type HookStdinPayload,
|
|
65
|
+
cancelledOutcome,
|
|
55
66
|
interpretHookResult,
|
|
56
67
|
} from "./protocol.ts";
|
|
57
68
|
import { type HookCommand, type HooksSource, loadHookSettings } from "./settings.ts";
|
|
58
69
|
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
59
|
-
import { REMINDER_CHANNEL, wrapReminder } from "../lib/reminders.ts";
|
|
70
|
+
import { REMINDER_CHANNEL, type ReminderPayload, wrapReminder } from "../lib/reminders.ts";
|
|
60
71
|
import { type ChildHookCall, type ChildHookResult, type HookBridge, SUBAGENT_HOOK_CHANNEL, type SubagentHookPayload } from "./subagent-bridge.ts";
|
|
61
72
|
import { projectHooksApproved } from "./trust.ts";
|
|
73
|
+
import { PromptContextRouter } from "./prompt-context.ts";
|
|
74
|
+
import { SKILL_INVOCATION_TYPE, type SkillInvocationDetails } from "../skill/invoke.ts";
|
|
75
|
+
import { consentDialog } from "../lib/consent-dialogs.ts";
|
|
76
|
+
import { followWorkCwd } from "../lib/worktree-channel.ts";
|
|
62
77
|
|
|
63
78
|
/** Claude Code's `hook_additional_context` attachment text (utils/messages.ts). */
|
|
64
79
|
export function hookContextText(event: CcHookEvent, text: string): string {
|
|
@@ -74,11 +89,13 @@ export function hookContextText(event: CcHookEvent, text: string): string {
|
|
|
74
89
|
* convention) and the model gets a preview plus the path — nothing is cut away.
|
|
75
90
|
*/
|
|
76
91
|
const HOOK_MODEL_TEXT_CAP = 20_000;
|
|
77
|
-
// A
|
|
78
|
-
//
|
|
79
|
-
|
|
92
|
+
// A random id, never a counter: the results directory is shared by every
|
|
93
|
+
// session of the project, and a counter restarts with each process and each
|
|
94
|
+
// /reload, so a later hook overwrote a file an earlier transcript points to
|
|
95
|
+
// (design.md section 14). Two hooks finishing in the same millisecond still get
|
|
96
|
+
// two files (review M2).
|
|
80
97
|
function capHookText(text: string, ctx: HookDispatchCtx, label: string): string {
|
|
81
|
-
return persistIfLarge(text, { dir: sessionResultsDir(ctx), id: `hook-${label}-${
|
|
98
|
+
return persistIfLarge(text, { dir: sessionResultsDir(ctx), id: `hook-${label}-${randomUUID()}`, maxBytes: HOOK_MODEL_TEXT_CAP });
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
interface MatchedHook {
|
|
@@ -95,15 +112,31 @@ interface HookDispatchCtx {
|
|
|
95
112
|
cwd: string;
|
|
96
113
|
hasUI: boolean;
|
|
97
114
|
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 };
|
|
115
|
+
ui: { confirm(title: string, message: string, options?: { signal?: AbortSignal }): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
|
|
116
|
+
signal?: AbortSignal;
|
|
99
117
|
/** Set on the shutdown snapshot: no consent prompt, no notices — the session is gone. */
|
|
100
118
|
sessionEnded?: true;
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/** Claude Code's default cap on consecutive Stop-hook continuations in one user turn. */
|
|
122
|
+
const MAX_STOP_CONTINUATIONS = 8;
|
|
123
|
+
|
|
124
|
+
/** The customType of the Stop hook's continuation message. */
|
|
125
|
+
const STOP_CONTINUATION_TYPE = "one-code:hook-stop";
|
|
126
|
+
|
|
127
|
+
/** Whether a `before_agent_start` opens the Stop hook's own continuation run (pi 1.2.0 fires it for custom runs). */
|
|
128
|
+
function isStopContinuation(event: unknown): boolean {
|
|
129
|
+
const run = event as { messageType?: string; message?: { customType?: string } };
|
|
130
|
+
return run.messageType === "custom" && run.message?.customType === STOP_CONTINUATION_TYPE;
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
export default function hooksExtension(pi: ExtensionAPI) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
const workCwd = followWorkCwd(pi.events);
|
|
135
|
+
let stopHookBlocks = 0;
|
|
136
|
+
/** Prompt, session and compaction hook context, as a one-shot the next request carries (lib/reminders.ts). */
|
|
137
|
+
const deliverContext = (event: CcHookEvent, text: string) => pi.events.emit(REMINDER_CHANNEL, { text: hookContextText(event, text), placement: "last-append" });
|
|
138
|
+
/** UserPromptSubmit context, held until pi delivers its prompt (prompt-context.ts). */
|
|
139
|
+
const promptContext = new PromptContextRouter();
|
|
107
140
|
/** The last context seen, for bridged child calls (dispatched parent-side). */
|
|
108
141
|
let lastCtx: ExtensionContext | undefined;
|
|
109
142
|
/**
|
|
@@ -116,6 +149,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
116
149
|
let sessionStartPending: Promise<void> | undefined;
|
|
117
150
|
/** Bumped on every session_start; a superseded dispatch checks it before writing context. */
|
|
118
151
|
let sessionGen = 0;
|
|
152
|
+
/** Covers startup hooks and other dispatches that have no active turn signal. */
|
|
153
|
+
let sessionAbort = new AbortController();
|
|
119
154
|
|
|
120
155
|
const notify = (ctx: HookDispatchCtx, message: string) => {
|
|
121
156
|
if (ctx.sessionEnded) return; // session gone; nowhere to show it
|
|
@@ -131,7 +166,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
131
166
|
): Promise<MatchedHook[]> => {
|
|
132
167
|
const claudeDir = claudeConfigDir();
|
|
133
168
|
const loaded = loadHookSettings(claudeDir, ctx.cwd);
|
|
134
|
-
const pluginSources = loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
|
|
169
|
+
const pluginSources = loaded.disabled ? [] : loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
|
|
135
170
|
if (hooksDebugEnabled()) {
|
|
136
171
|
for (const diagnostic of loaded.diagnostics) process.stderr.write(`[hooks] ${diagnostic}\n`);
|
|
137
172
|
}
|
|
@@ -143,7 +178,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
143
178
|
projectAllowed = await projectHooksApproved(ctx.cwd, projectSources, {
|
|
144
179
|
hasUI: ctx.hasUI,
|
|
145
180
|
noPrompt: ctx.sessionEnded,
|
|
146
|
-
|
|
181
|
+
signal: ctx.signal,
|
|
182
|
+
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
183
|
notify: (message) => notify(ctx, message),
|
|
148
184
|
});
|
|
149
185
|
}
|
|
@@ -168,7 +204,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
168
204
|
const basePayload = (ctx: HookDispatchCtx, event: CcHookEvent): HookStdinPayload => ({
|
|
169
205
|
session_id: ctx.sessionManager.getSessionId(),
|
|
170
206
|
transcript_path: ctx.sessionManager.getSessionFile() ?? "",
|
|
171
|
-
cwd: ctx.cwd,
|
|
207
|
+
cwd: workCwd(ctx.cwd),
|
|
172
208
|
hook_event_name: event,
|
|
173
209
|
});
|
|
174
210
|
|
|
@@ -181,20 +217,27 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
181
217
|
ctx: HookDispatchCtx,
|
|
182
218
|
event: CcHookEvent,
|
|
183
219
|
matchValue: { candidates?: string[]; ignoreMatcher?: boolean },
|
|
184
|
-
|
|
220
|
+
payloadOrFactory: HookStdinPayload | (() => Promise<HookStdinPayload | undefined>),
|
|
185
221
|
/** Runs once, only when hooks will actually run — the point where a caller can snapshot state the hooks may change. */
|
|
186
222
|
willRun?: () => void,
|
|
187
223
|
): Promise<HookOutcome> => {
|
|
188
224
|
const merged: HookOutcome = {};
|
|
225
|
+
const signal = ctx.sessionEnded ? ctx.signal : ctx.signal ? AbortSignal.any([ctx.signal, sessionAbort.signal]) : sessionAbort.signal;
|
|
189
226
|
try {
|
|
190
|
-
const hooks = await collectHooks(ctx, event, matchValue);
|
|
191
|
-
|
|
227
|
+
const hooks = await collectHooks({ ...ctx, signal }, event, matchValue);
|
|
228
|
+
const payload = typeof payloadOrFactory === "function" ? await payloadOrFactory() : payloadOrFactory;
|
|
229
|
+
if (!payload || hooks.length === 0) return merged;
|
|
230
|
+
// Cancelled before the hooks ran: the gating events block, as a mid-run cancel does.
|
|
231
|
+
if (signal?.aborted) return cancelledOutcome(event);
|
|
192
232
|
willRun?.();
|
|
193
233
|
const stdin = JSON.stringify(payload);
|
|
194
|
-
const outcomes = await Promise.
|
|
234
|
+
const outcomes = await Promise.allSettled(
|
|
195
235
|
hooks.map(async ({ source, hook }) => {
|
|
196
236
|
const run = await runHookCommand(hook.command, stdin, {
|
|
197
|
-
|
|
237
|
+
// The calling session's worktree or child's directory; settings,
|
|
238
|
+
// consent and CLAUDE_PROJECT_DIR still belong to the parent project.
|
|
239
|
+
cwd: payload.cwd,
|
|
240
|
+
signal,
|
|
198
241
|
timeoutSeconds: hook.timeout,
|
|
199
242
|
shell: hook.shell,
|
|
200
243
|
projectDir: ctx.cwd,
|
|
@@ -221,7 +264,16 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
221
264
|
return outcome;
|
|
222
265
|
}),
|
|
223
266
|
);
|
|
224
|
-
|
|
267
|
+
if (signal?.aborted) return cancelledOutcome(event);
|
|
268
|
+
for (const result of outcomes) {
|
|
269
|
+
// One broken response must not erase another hook's deny or let the
|
|
270
|
+
// tool run while the other hooks are still deciding.
|
|
271
|
+
if (result.status === "rejected") {
|
|
272
|
+
if (hooksDebugEnabled()) process.stderr.write(`[hooks] hook error (${event}): ${String(result.reason)}\n`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const outcome = result.value;
|
|
276
|
+
if (outcome.preventContinuation) merged.preventContinuation = true;
|
|
225
277
|
// A block reason becomes model-facing text (the tool result, or a
|
|
226
278
|
// Stop follow-up), so it is bounded like the other hook texts —
|
|
227
279
|
// stderr on an exit-2 hook is easily a full stack trace (review M2).
|
|
@@ -291,7 +343,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
291
343
|
// to it: this extension awaits the hook and loads before file-tracker, so
|
|
292
344
|
// the tracker records the hook's version as the model's own write
|
|
293
345
|
// (formatter-notice.ts, review M1). Snapshot only when hooks will run.
|
|
294
|
-
const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input,
|
|
346
|
+
const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, payload.cwd);
|
|
295
347
|
let before: FileSnapshot | undefined;
|
|
296
348
|
const outcome = await dispatch(ctx, "PostToolUse", { candidates: toolMatchCandidates(event.toolName) }, payload, () => {
|
|
297
349
|
if (target) before = snapshotFile(target);
|
|
@@ -305,47 +357,98 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
305
357
|
[outcome.additionalContext && wrapReminder(hookContextText("PostToolUse", outcome.additionalContext)), formatterNotice]
|
|
306
358
|
.filter(Boolean)
|
|
307
359
|
.join("\n") || undefined,
|
|
308
|
-
});
|
|
360
|
+
}, event.toolName);
|
|
309
361
|
if (!content) return undefined;
|
|
310
362
|
return { content, isError: event.isError };
|
|
311
363
|
});
|
|
312
364
|
|
|
313
|
-
/**
|
|
365
|
+
/**
|
|
366
|
+
* Await the backgrounded SessionStart dispatch, if one is pending. Every
|
|
367
|
+
* caller awaits the same dispatch, and it is forgotten only once it settles:
|
|
368
|
+
* a notification opening the first turn and a prompt typed meanwhile both
|
|
369
|
+
* wait for it, rather than the second returning at once.
|
|
370
|
+
*/
|
|
314
371
|
const drainSessionStart = async () => {
|
|
315
|
-
if (!sessionStartPending) return;
|
|
316
372
|
const pending = sessionStartPending;
|
|
317
|
-
|
|
373
|
+
if (!pending) return;
|
|
318
374
|
try {
|
|
319
375
|
await pending;
|
|
320
376
|
} catch {
|
|
321
377
|
// dispatch already reports its own failures; never block the turn on one.
|
|
322
378
|
}
|
|
379
|
+
if (sessionStartPending === pending) sessionStartPending = undefined;
|
|
323
380
|
};
|
|
324
381
|
|
|
325
382
|
// ---- UserPromptSubmit ---------------------------------------------------
|
|
326
383
|
pi.on("input", async (event, ctx) => {
|
|
384
|
+
// A new idle prompt means an earlier idle one was delivered or never will
|
|
385
|
+
// be (an extension after this one handled it): its context is stale. An
|
|
386
|
+
// extension's prompt (a command's sendUserMessage) counts too, or its
|
|
387
|
+
// message would take that context as its own.
|
|
388
|
+
if (!event.streamingBehavior) promptContext.dropIdle();
|
|
327
389
|
if (event.source === "extension") return undefined;
|
|
328
|
-
|
|
390
|
+
// Set for a message queued mid-turn (pi 0.86+ fires `input` for
|
|
391
|
+
// steer/followUp). The Stop latch is reset in before_agent_start, where a
|
|
392
|
+
// turn really starts: pi re-checks streaming after these handlers, so a
|
|
393
|
+
// queued message whose turn ended during the hook opens a new turn.
|
|
394
|
+
const gen = sessionGen;
|
|
329
395
|
// Drain the backgrounded SessionStart dispatch before this prompt's own
|
|
330
396
|
// UserPromptSubmit context, so their order in the first turn is unchanged
|
|
331
397
|
// (SessionStart context precedes UserPromptSubmit). before_agent_start is
|
|
332
398
|
// the backstop for turns with no input event (one-shot, programmatic).
|
|
333
399
|
await drainSessionStart();
|
|
400
|
+
if (gen !== sessionGen) return { action: "handled" as const };
|
|
334
401
|
const payload: HookStdinPayload = { ...basePayload(ctx, "UserPromptSubmit"), prompt: event.text };
|
|
335
402
|
const outcome = await dispatch(ctx, "UserPromptSubmit", { ignoreMatcher: true }, payload);
|
|
403
|
+
if (gen !== sessionGen) return { action: "handled" as const };
|
|
336
404
|
if (outcome.block) {
|
|
337
405
|
notify(ctx, `Prompt blocked by UserPromptSubmit hook: ${outcome.block.reason}`);
|
|
338
406
|
return { action: "handled" as const };
|
|
339
407
|
}
|
|
340
|
-
|
|
408
|
+
// The context waits for pi to deliver this prompt (message_start below),
|
|
409
|
+
// queued mid-turn or not, so it rides the prompt's own request and never a
|
|
410
|
+
// run a notification opens first. A queued message is held without
|
|
411
|
+
// context too, so its delivery never takes an idle prompt's.
|
|
412
|
+
const text = outcome.additionalContext ? hookContextText("UserPromptSubmit", outcome.additionalContext) : undefined;
|
|
413
|
+
if (event.streamingBehavior) promptContext.holdQueued(event.text, text);
|
|
414
|
+
else if (text) promptContext.holdIdle(event.text, text);
|
|
341
415
|
return undefined;
|
|
342
416
|
});
|
|
343
417
|
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// before_agent_start
|
|
347
|
-
|
|
348
|
-
|
|
418
|
+
// pi awaits this before the request that carries the delivered message, so a
|
|
419
|
+
// one-shot emitted here is pinned to that message: after the prompt, as the
|
|
420
|
+
// before_agent_start message is for a prompt that opens a turn.
|
|
421
|
+
pi.on("message_start", async (event) => {
|
|
422
|
+
const gen = sessionGen;
|
|
423
|
+
const message = event.message;
|
|
424
|
+
// Pinned to its prompt: when that prompt's request fails, the next prompt's
|
|
425
|
+
// request resends it with its own context, not with this one's.
|
|
426
|
+
const emit = (text: string) => pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append", promptTimestamp: message.timestamp } satisfies ReminderPayload);
|
|
427
|
+
if (message.role === "user") {
|
|
428
|
+
if (promptContext.isEmpty) return;
|
|
429
|
+
// A queued message delivers as typed, so an exact match is its own; the
|
|
430
|
+
// idle prompt's message may carry its expansion instead.
|
|
431
|
+
const text = promptContext.release(contentText(message.content, ""));
|
|
432
|
+
if (text) emit(text);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (message.role !== "custom") return;
|
|
436
|
+
// A background completion that opens the first turn skips
|
|
437
|
+
// before_agent_start: the SessionStart context must be pending before its
|
|
438
|
+
// request is rendered.
|
|
439
|
+
await drainSessionStart();
|
|
440
|
+
// A `/skill:` command the skill extension took over arrives as its hidden
|
|
441
|
+
// message, carrying the typed text: that prompt's context rides it.
|
|
442
|
+
if (message.customType !== SKILL_INVOCATION_TYPE || promptContext.isEmpty) return;
|
|
443
|
+
const input = (message.details as Partial<SkillInvocationDetails> | undefined)?.input;
|
|
444
|
+
const queued = typeof input === "string" ? promptContext.release(input, true) : undefined;
|
|
445
|
+
if (gen === sessionGen && queued) emit(queued);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// The first turn waits for the backgrounded SessionStart dispatch, so its
|
|
449
|
+
// context is pending before the request is rendered.
|
|
450
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
451
|
+
const gen = sessionGen;
|
|
349
452
|
lastCtx = ctx;
|
|
350
453
|
// The SessionStart dispatch runs in the background from session_start; the
|
|
351
454
|
// first turn must wait for it so its additionalContext is present (and, on
|
|
@@ -353,35 +456,37 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
353
456
|
// The `input` handler usually drained it already; this is the backstop for
|
|
354
457
|
// turns with no input event.
|
|
355
458
|
await drainSessionStart();
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
content: texts.map(({ event, text }) => wrapReminder(hookContextText(event, text))).join("\n"),
|
|
363
|
-
display: false,
|
|
364
|
-
},
|
|
365
|
-
};
|
|
459
|
+
if (gen !== sessionGen) return;
|
|
460
|
+
// Every run that opens lands here (on pi 1.2.0 also one an extension
|
|
461
|
+
// opened; a queued delivery does not). Only the Stop hook's own
|
|
462
|
+
// continuation keeps the cap counting: a notification or scheduled run
|
|
463
|
+
// starts a fresh turn, as a prompt does.
|
|
464
|
+
if (!isStopContinuation(event)) stopHookBlocks = 0;
|
|
366
465
|
});
|
|
367
466
|
|
|
368
467
|
// ---- SessionStart / SessionEnd -----------------------------------------
|
|
369
468
|
const dispatchSessionStart = async (ctx: ExtensionContext, source: string, gen: number) => {
|
|
370
|
-
|
|
371
|
-
|
|
469
|
+
// Reserve project-hook consent synchronously, before MCP and external
|
|
470
|
+
// imports enqueue their dialogs. Only defer the payload: worktree's later
|
|
471
|
+
// synchronous session_start handler must restore cwd before we capture it.
|
|
472
|
+
const outcome = await dispatch(ctx, "SessionStart", { candidates: [source] }, async () => {
|
|
473
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
474
|
+
return gen === sessionGen ? { ...basePayload(ctx, "SessionStart"), source } : undefined;
|
|
475
|
+
});
|
|
372
476
|
// A later session_start (rapid /clear, resume) may have superseded this one
|
|
373
477
|
// while its hook ran; discard the stale result instead of pushing it into
|
|
374
|
-
// the new session's context
|
|
375
|
-
if (gen === sessionGen && outcome.additionalContext)
|
|
376
|
-
pendingPromptContext.push({ event: "SessionStart", text: outcome.additionalContext });
|
|
377
|
-
}
|
|
478
|
+
// the new session's context.
|
|
479
|
+
if (gen === sessionGen && outcome.additionalContext) deliverContext("SessionStart", outcome.additionalContext);
|
|
378
480
|
};
|
|
379
481
|
|
|
380
482
|
pi.on("session_start", (event, ctx) => {
|
|
483
|
+
sessionAbort.abort();
|
|
484
|
+
sessionAbort = new AbortController();
|
|
381
485
|
lastCtx = ctx;
|
|
382
486
|
const gen = ++sessionGen;
|
|
383
|
-
|
|
487
|
+
promptContext.clear();
|
|
384
488
|
sessionStartPending = undefined;
|
|
489
|
+
stopHookBlocks = 0;
|
|
385
490
|
// Publish the child hook bridge (subagent-bridge.ts). The closures read
|
|
386
491
|
// live parent state per call, so once per session start is enough.
|
|
387
492
|
pi.events.emit(SUBAGENT_HOOK_CHANNEL, { bridge: childHookBridge } satisfies SubagentHookPayload);
|
|
@@ -410,9 +515,21 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
410
515
|
agent_id: call.sessionId,
|
|
411
516
|
agent_type: call.agentType,
|
|
412
517
|
});
|
|
518
|
+
/**
|
|
519
|
+
* The parent's settings/consent context with the CHILD's run signal. A
|
|
520
|
+
* background child outlives the parent's turn, so Esc in the parent must
|
|
521
|
+
* not skip a child's deny hook; stopping the child must cancel its hooks.
|
|
522
|
+
*/
|
|
523
|
+
const childDispatchCtx = (ctx: ExtensionContext, call: ChildHookCall): HookDispatchCtx => ({
|
|
524
|
+
cwd: ctx.cwd,
|
|
525
|
+
hasUI: ctx.hasUI,
|
|
526
|
+
sessionManager: ctx.sessionManager,
|
|
527
|
+
ui: ctx.ui,
|
|
528
|
+
signal: call.signal,
|
|
529
|
+
});
|
|
413
530
|
const childHookBridge: HookBridge = {
|
|
414
531
|
async preToolUse(call) {
|
|
415
|
-
const ctx = lastCtx;
|
|
532
|
+
const ctx = lastCtx && childDispatchCtx(lastCtx, call);
|
|
416
533
|
if (!ctx) return {};
|
|
417
534
|
const outcome = await dispatch(ctx, "PreToolUse", { candidates: toolMatchCandidates(call.toolName) }, childPayload(call, "PreToolUse"));
|
|
418
535
|
return {
|
|
@@ -422,7 +539,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
422
539
|
};
|
|
423
540
|
},
|
|
424
541
|
async postToolUse(result: ChildHookResult) {
|
|
425
|
-
const ctx = lastCtx;
|
|
542
|
+
const ctx = lastCtx && childDispatchCtx(lastCtx, result);
|
|
426
543
|
if (!ctx) return {};
|
|
427
544
|
const payload: HookStdinPayload = {
|
|
428
545
|
...childPayload(result, "PostToolUse"),
|
|
@@ -449,6 +566,9 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
449
566
|
};
|
|
450
567
|
|
|
451
568
|
pi.on("session_shutdown", (event, ctx) => {
|
|
569
|
+
// Also cancels a queued SessionStart before its deferred dispatch touches a disposed context.
|
|
570
|
+
sessionGen += 1;
|
|
571
|
+
sessionAbort.abort();
|
|
452
572
|
// A reload re-runs the extensions but keeps the conversation (pi's
|
|
453
573
|
// AgentSession.reload): not a session end, Claude Code has no such event.
|
|
454
574
|
if (event.reason === "reload") return;
|
|
@@ -492,19 +612,40 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
492
612
|
});
|
|
493
613
|
|
|
494
614
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
615
|
+
const gen = sessionGen;
|
|
616
|
+
// A queued message pi never delivered as typed (expanded, or taken over by
|
|
617
|
+
// an extension) keeps no context for a later, unrelated prompt; an idle
|
|
618
|
+
// prompt that opened no run of its own (one handled after this extension)
|
|
619
|
+
// keeps none either.
|
|
620
|
+
promptContext.clear();
|
|
495
621
|
// CC skips Stop when the turn ended on an API error (a blocking hook would
|
|
496
622
|
// spiral: error → block → retry → error) or on a user interrupt (its query
|
|
497
623
|
// loop returns on the abort signal before the stop-hook step). An empty latch
|
|
498
624
|
// means no run ended in this turn, which is not a response to stop on either.
|
|
499
625
|
if (stopOutcome.take() !== "ok") return;
|
|
500
|
-
|
|
626
|
+
// Claude Code's two lists of what will wake the session later, filled by the
|
|
627
|
+
// background extension (lib/session-work.ts); empty arrays when none.
|
|
628
|
+
const work: SessionWorkQuery = { crons: [], tasks: [] };
|
|
629
|
+
pi.events.emit(SESSION_WORK_CHANNEL, work);
|
|
630
|
+
const payload: HookStdinPayload = {
|
|
631
|
+
...basePayload(ctx, "Stop"),
|
|
632
|
+
stop_hook_active: stopHookBlocks > 0,
|
|
633
|
+
background_tasks: work.tasks,
|
|
634
|
+
session_crons: work.crons,
|
|
635
|
+
};
|
|
501
636
|
const outcome = await dispatch(ctx, "Stop", { ignoreMatcher: true }, payload);
|
|
502
|
-
if (!outcome.block) return;
|
|
503
|
-
|
|
637
|
+
if (gen !== sessionGen || outcome.preventContinuation || !outcome.block) return;
|
|
638
|
+
// Claude Code limits repeated Stop blocks to eight continuations. A hook
|
|
639
|
+
// that ignores stop_hook_active must not run up an unbounded model loop.
|
|
640
|
+
if (stopHookBlocks >= MAX_STOP_CONTINUATIONS) {
|
|
641
|
+
notify(ctx, `Stop hook continuation limit reached (${MAX_STOP_CONTINUATIONS}); ending the turn.`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
stopHookBlocks += 1;
|
|
504
645
|
try {
|
|
505
646
|
pi.sendMessage(
|
|
506
647
|
{
|
|
507
|
-
customType:
|
|
648
|
+
customType: STOP_CONTINUATION_TYPE,
|
|
508
649
|
content: `Stop hook blocked stopping: ${outcome.block.reason}`,
|
|
509
650
|
display: false,
|
|
510
651
|
},
|
|
@@ -533,10 +674,19 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
533
674
|
notify(ctx, `Compaction cancelled by PreCompact hook: ${outcome.block.reason}`);
|
|
534
675
|
return { cancel: true };
|
|
535
676
|
}
|
|
677
|
+
// These instructions belong to the summarizer, not the next user turn.
|
|
678
|
+
// The signal keeps them scoped to this attempt if compaction is cancelled.
|
|
679
|
+
if (outcome.additionalContext) {
|
|
680
|
+
pi.events.emit(PRECOMPACT_INSTRUCTIONS_CHANNEL, {
|
|
681
|
+
signal: event.signal,
|
|
682
|
+
instructions: outcome.additionalContext,
|
|
683
|
+
} satisfies PreCompactInstructions);
|
|
684
|
+
}
|
|
536
685
|
return undefined;
|
|
537
686
|
});
|
|
538
687
|
|
|
539
688
|
pi.on("session_compact", async (event, ctx) => {
|
|
689
|
+
const gen = sessionGen;
|
|
540
690
|
const trigger = compactTrigger(event);
|
|
541
691
|
const payload: HookStdinPayload = {
|
|
542
692
|
...basePayload(ctx, "PostCompact"),
|
|
@@ -544,7 +694,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
544
694
|
compact_summary: event.compactionEntry.summary,
|
|
545
695
|
};
|
|
546
696
|
const outcome = await dispatch(ctx, "PostCompact", { candidates: [trigger] }, payload);
|
|
547
|
-
if (
|
|
697
|
+
if (gen !== sessionGen) return;
|
|
698
|
+
if (outcome.additionalContext) deliverContext("PostCompact", outcome.additionalContext);
|
|
548
699
|
// CC fires SessionStart(source: "compact") after compaction. Awaited inline
|
|
549
700
|
// (not backgrounded), so the current generation is still in force.
|
|
550
701
|
await dispatchSessionStart(ctx, "compact", sessionGen);
|
|
@@ -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",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which delivered message takes a UserPromptSubmit hook's context. Pure (no pi
|
|
3
|
+
* imports).
|
|
4
|
+
*
|
|
5
|
+
* A message queued mid-turn delivers as typed, so the user message with its
|
|
6
|
+
* text is its own (lib/queued-delivery.ts). A prompt typed while idle is
|
|
7
|
+
* expanded by pi after the input handlers (a prompt template, a `/skill:` no
|
|
8
|
+
* extension took), and a later handler may transform it, so its message
|
|
9
|
+
* rarely carries the typed text. It is the next prompt pi delivers, though:
|
|
10
|
+
* the first user message no queued message claims takes it.
|
|
11
|
+
*
|
|
12
|
+
* A queued message is held even when its hook added no context, so its
|
|
13
|
+
* delivery claims nothing instead of taking an idle prompt's context.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { QueuedDelivery } from "../lib/queued-delivery.ts";
|
|
17
|
+
|
|
18
|
+
export class PromptContextRouter {
|
|
19
|
+
/** Context per queued message; null marks one whose hook added none. */
|
|
20
|
+
private readonly queued = new QueuedDelivery<string | null>();
|
|
21
|
+
private idle: Array<{ text: string; payload: string }> = [];
|
|
22
|
+
|
|
23
|
+
/** A message queued mid-turn, with its hook's context or none. */
|
|
24
|
+
holdQueued(text: string, payload: string | undefined): void {
|
|
25
|
+
this.queued.hold(text, payload ?? null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A prompt typed while idle and its hook's context. */
|
|
29
|
+
holdIdle(text: string, payload: string): void {
|
|
30
|
+
this.idle.push({ text, payload });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A new idle prompt: an earlier idle one was delivered or never will be, so its context is stale. */
|
|
34
|
+
dropIdle(): void {
|
|
35
|
+
this.idle = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isEmpty(): boolean {
|
|
39
|
+
return this.queued.isEmpty && this.idle.length === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The context for a delivered user message with this text: a queued message
|
|
44
|
+
* typed so claims its own (none, when its hook added none); else the idle
|
|
45
|
+
* prompt typed so; else, unless `exact`, the oldest idle prompt. Released once.
|
|
46
|
+
*/
|
|
47
|
+
release(text: string, exact = false): string | undefined {
|
|
48
|
+
const queued = this.queued.release(text);
|
|
49
|
+
if (queued !== undefined) return queued ?? undefined;
|
|
50
|
+
const at = this.idle.findIndex((entry) => entry.text === text);
|
|
51
|
+
const index = at === -1 && !exact && this.idle.length > 0 ? 0 : at;
|
|
52
|
+
return index === -1 ? undefined : this.idle.splice(index, 1)[0].payload;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear(): void {
|
|
56
|
+
this.queued.clear();
|
|
57
|
+
this.idle = [];
|
|
58
|
+
}
|
|
59
|
+
}
|