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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The classifier call itself — the one file here that talks to a provider.
|
|
3
3
|
*
|
|
4
|
-
* Claude Code's two-stage formula, reproduced (docs/decisions/auto-mode.md, P4):
|
|
4
|
+
* Claude Code's two-stage formula, reproduced (working-docs/decisions/auto-mode.md, P4):
|
|
5
5
|
* Stage 1 grades HARM ONLY (maxTokens 64). severity < 50 → allow, no stage 2.
|
|
6
|
-
* Stage 2 applies intent + ALLOW (maxTokens
|
|
6
|
+
* Stage 2 applies intent + ALLOW (maxTokens 4096, <thinking> CoT) → severity +
|
|
7
7
|
* <category> (+ our verified <intent>). Both stages share one system prompt and
|
|
8
|
-
* transcript byte-for-byte.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* transcript byte-for-byte. Where the request carries explicit breakpoints
|
|
9
|
+
* (Anthropic Messages, OpenRouter's Anthropic-format Chat Completions,
|
|
10
|
+
* Bedrock Converse), onPayload splits the user text at stable history-entry
|
|
11
|
+
* boundaries and moves the user cache marker before the pending action and
|
|
12
|
+
* stage instruction (cache.ts). Both stages and later gated calls can read
|
|
13
|
+
* that prefix; the system markers stay intact. Other APIs keep the single
|
|
14
|
+
* string and their provider's prefix caching.
|
|
14
15
|
*
|
|
15
16
|
* A one-shot `completeSimple` per stage rather than an agent session: no tools,
|
|
16
17
|
* no history beyond the transcript it is handed, nothing to be talked into. Every
|
|
@@ -19,11 +20,15 @@
|
|
|
19
20
|
* has approved nothing.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
import
|
|
23
|
+
import { appendFileSync } from "node:fs";
|
|
24
|
+
import { cleanupSessionResources, isContextOverflow, type Model, type Api, type AssistantMessage, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
23
25
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
24
26
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import {
|
|
27
|
+
import { costRatioText, isNotablyCostlier } from "../lib/model-choice-warnings.ts";
|
|
28
|
+
import { modeCycleKey } from "../lib/keys.ts";
|
|
29
|
+
import { classifierCostRatio, forcedReasoningLevel, isReasoningMandatoryError, modelSpec as spec, reasoningRetryLevel } from "../lib/model-policy.ts";
|
|
26
30
|
import type { AutoModeConfig } from "./config.ts";
|
|
31
|
+
import { CLASSIFIER_CACHE_APIS, cacheClassifierHistory } from "./cache.ts";
|
|
27
32
|
import {
|
|
28
33
|
buildPayload,
|
|
29
34
|
type ClassifyRequest,
|
|
@@ -37,6 +42,7 @@ import {
|
|
|
37
42
|
} from "./prompt.ts";
|
|
38
43
|
import {
|
|
39
44
|
type Candidate,
|
|
45
|
+
type ClassifierFallback,
|
|
40
46
|
type ClassifierNotice,
|
|
41
47
|
classifierCandidates,
|
|
42
48
|
describeCandidate,
|
|
@@ -44,10 +50,25 @@ import {
|
|
|
44
50
|
replyText,
|
|
45
51
|
withAuthBaseUrl,
|
|
46
52
|
} from "./model-select.ts";
|
|
53
|
+
import { actionLength, MAX_ACTION_CHARS } from "./transcript.ts";
|
|
47
54
|
|
|
48
55
|
/** A slow classifier stalls every tool call, so the wait is capped per stage. */
|
|
49
56
|
export const CLASSIFIER_TIMEOUT_MS = 30_000;
|
|
50
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Claude Code's default retry count for a rate limit or server error (its
|
|
60
|
+
* classifier's side query uses it). pi-ai's default is none, so one 429 left
|
|
61
|
+
* the call unjudged; the stage timeout above still bounds the retries.
|
|
62
|
+
*/
|
|
63
|
+
export const CLASSIFIER_MAX_RETRIES = 10;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Output headroom for a model that cannot turn thinking off, added to both
|
|
67
|
+
* stages' caps as Claude Code pads its always-on-thinking models: the
|
|
68
|
+
* reasoning otherwise spends the budget before the verdict is written.
|
|
69
|
+
*/
|
|
70
|
+
export const FORCED_THINKING_PADDING = 2048;
|
|
71
|
+
|
|
51
72
|
/**
|
|
52
73
|
* A pinned model that times out this many calls in a row is unpinned, so a model
|
|
53
74
|
* that was fast when first chosen but has since fallen behind demand does not
|
|
@@ -55,6 +76,19 @@ export const CLASSIFIER_TIMEOUT_MS = 30_000;
|
|
|
55
76
|
*/
|
|
56
77
|
export const ROTATE_AFTER_TIMEOUTS = 2;
|
|
57
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The provider refused the classifier's prompt as a possible usage-policy
|
|
81
|
+
* violation ("Invalid prompt: your prompt was flagged as potentially violating
|
|
82
|
+
* our usage policy"). GPT-6 Luna on Codex did this to about one classifier
|
|
83
|
+
* prompt in ten, at random, and passed the same prompt on a retry (findings
|
|
84
|
+
* §59). A refused call is retried once on the same model; a second refusal
|
|
85
|
+
* drops the model for the rest of the session (decisions/auto-mode.md,
|
|
86
|
+
* "A classifier that refuses its prompt").
|
|
87
|
+
*/
|
|
88
|
+
export function isPromptPolicyRefusal(message: string): boolean {
|
|
89
|
+
return /invalid prompt\b[^.\n]{0,80}\bflagged\b[^.\n]{0,80}usage policy/i.test(message);
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
/**
|
|
59
93
|
* Whether a failure is our own classifier timeout (or an equivalent abort/stall)
|
|
60
94
|
* rather than a substantive provider error. A timeout is transient — worth a
|
|
@@ -66,6 +100,42 @@ export function isClassifierTimeout(message: string): boolean {
|
|
|
66
100
|
return /\babort|\btimed?\s*out\b|\btimeout\b|\betimedout\b|\bdeadline\b/i.test(message);
|
|
67
101
|
}
|
|
68
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Whether a failure happened below HTTP: the request never got an answer
|
|
105
|
+
* (`fetch failed`, a reset or refused connection, a DNS miss). Like a timeout it
|
|
106
|
+
* says nothing about the call or the model, so it takes the timeout path: one
|
|
107
|
+
* retry, then a "not judged" block. A provider's own error reply (a 500, a 429)
|
|
108
|
+
* is not one; it stays a substantive error. Observed 2026-10-04: a few minutes
|
|
109
|
+
* of `fetch failed` on openai-codex blocked read-only calls as "denied".
|
|
110
|
+
*/
|
|
111
|
+
export function isTransportFailure(message: string): boolean {
|
|
112
|
+
return /\bfetch failed\b|\beconn(reset|refused|aborted)\b|\benotfound\b|\beai_again\b|\bepipe\b|socket hang up|other side closed|\bund_err_socket\b|network (error|is unreachable)|\bwebsocket (closed|error)\b/i.test(
|
|
113
|
+
message,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A provider's error without its advice to the user ("Please try again with a
|
|
119
|
+
* different prompt", "try rephrasing your request", a link): handed to the
|
|
120
|
+
* model in a block, that advice invites rephrasing the call past the gate.
|
|
121
|
+
*/
|
|
122
|
+
export function providerErrorSummary(message: string): string {
|
|
123
|
+
// Advice follows a sentence end, a colon or semicolon, or a line break.
|
|
124
|
+
const cut = message.search(/(?:[.!:;]\s+|\n\s*)(please\b|if this seems|try\b|you can\b|learn more|see\b)/i);
|
|
125
|
+
const kept = cut >= 0 ? message.slice(0, cut + 1) : message;
|
|
126
|
+
return kept.replace(/:?\s*https?:\/\/\S+/g, "").trim().replace(/[:;]$/, "");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A provider rejected the input window, not the action's safety or output limit. */
|
|
130
|
+
export function isClassifierTranscriptTooLong(message: string): boolean {
|
|
131
|
+
// Only pi-ai's error-pattern branch: no catalog window means this never
|
|
132
|
+
// infers overflow from an estimate or a model's reported usage.
|
|
133
|
+
return isContextOverflow({ stopReason: "error", errorMessage: message } as AssistantMessage);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Pause before retrying after a transport failure, so a brief network drop can clear. */
|
|
137
|
+
export const TRANSPORT_RETRY_DELAY_MS = 1500;
|
|
138
|
+
|
|
69
139
|
/**
|
|
70
140
|
* Which model the classifier settled on, and which candidates turned out to be
|
|
71
141
|
* unusable. Owned by the caller so this module stays stateless, and so the
|
|
@@ -76,6 +146,12 @@ export interface ClassifierState {
|
|
|
76
146
|
pinned?: Model<Api>;
|
|
77
147
|
/** `provider/id` of candidates that failed as unusable. */
|
|
78
148
|
rejected: Set<string>;
|
|
149
|
+
/**
|
|
150
|
+
* The subset of `rejected` that refused the classifier's prompt twice. Unlike
|
|
151
|
+
* another rejection, such a model is not retried when every candidate is
|
|
152
|
+
* gone: it would refuse again, so the gate stops and says how to go on.
|
|
153
|
+
*/
|
|
154
|
+
refused: Set<string>;
|
|
79
155
|
/** Notices already delivered, so a per-call warning is not repeated per call. */
|
|
80
156
|
notified: Set<string>;
|
|
81
157
|
/**
|
|
@@ -84,15 +160,15 @@ export interface ClassifierState {
|
|
|
84
160
|
*/
|
|
85
161
|
timeoutStreak: number;
|
|
86
162
|
/**
|
|
87
|
-
* The candidate chain cached for the current
|
|
88
|
-
*
|
|
89
|
-
* Invalidated
|
|
90
|
-
*
|
|
163
|
+
* The candidate chain cached for the current session-model/window signature,
|
|
164
|
+
* so the O(catalog) ranking is not rebuilt on every gated tool call.
|
|
165
|
+
* Invalidated when the session model or its catalog context window changes —
|
|
166
|
+
* catalog churn is otherwise deliberately ignored, matching the pin, which
|
|
91
167
|
* already commits the session to one classifier. The live `rejected` filter is
|
|
92
168
|
* still applied to the cached chain per call, so a model that dies mid-session
|
|
93
169
|
* is still stepped over without a rebuild.
|
|
94
170
|
*/
|
|
95
|
-
chainCache?: { signature: string; candidates: Candidate[]; notices: ClassifierNotice[] };
|
|
171
|
+
chainCache?: { signature: string; candidates: Candidate[]; notices: ClassifierNotice[]; fallback?: ClassifierFallback };
|
|
96
172
|
/**
|
|
97
173
|
* `provider/id` -> the thinking level a model turned out to REQUIRE (its
|
|
98
174
|
* provider rejected the thinking-off request with "reasoning is mandatory").
|
|
@@ -102,10 +178,28 @@ export interface ClassifierState {
|
|
|
102
178
|
* proactively, without an error).
|
|
103
179
|
*/
|
|
104
180
|
forcedReasoning: Map<string, ThinkingLevel>;
|
|
181
|
+
/** Previous history boundary on a breakpoint API, a cache lookup hint only (cache.ts). */
|
|
182
|
+
cacheHistoryEnd?: number;
|
|
105
183
|
}
|
|
106
184
|
|
|
107
185
|
export function createClassifierState(): ClassifierState {
|
|
108
|
-
return { rejected: new Set(), notified: new Set(), timeoutStreak: 0, forcedReasoning: new Map() };
|
|
186
|
+
return { rejected: new Set(), refused: new Set(), notified: new Set(), timeoutStreak: 0, forcedReasoning: new Map() };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The `cacheKey` a session's classifier calls pass (`ClassifierDeps.cacheKey`). */
|
|
190
|
+
export function classifierCacheKey(sessionId: string): string {
|
|
191
|
+
return `${sessionId}:auto-mode`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Close what pi-ai holds open under the session's classifier key. The key
|
|
196
|
+
* doubles as pi-ai's `sessionId`, and on openai-codex that keeps one WebSocket
|
|
197
|
+
* per id with a 5-minute idle timer holding the process; pi's own dispose
|
|
198
|
+
* closes only the session's id, so without this a `pi -p` run waits out the
|
|
199
|
+
* timer after its answer (findings §59).
|
|
200
|
+
*/
|
|
201
|
+
export function releaseClassifierResources(sessionId: string): void {
|
|
202
|
+
cleanupSessionResources(classifierCacheKey(sessionId));
|
|
109
203
|
}
|
|
110
204
|
|
|
111
205
|
export interface ClassifierDeps {
|
|
@@ -114,8 +208,15 @@ export interface ClassifierDeps {
|
|
|
114
208
|
config: AutoModeConfig;
|
|
115
209
|
signal?: AbortSignal;
|
|
116
210
|
state: ClassifierState;
|
|
211
|
+
/**
|
|
212
|
+
* A stable key for this session's classifier calls. pi-ai sends it as
|
|
213
|
+
* OpenAI's prompt_cache_key (Codex and the OpenAI APIs), which routes the
|
|
214
|
+
* calls to the cache that holds the previous call's prefix; without it
|
|
215
|
+
* consecutive calls landed on different cache machines and missed.
|
|
216
|
+
*/
|
|
217
|
+
cacheKey?: string;
|
|
117
218
|
/** Tell the user something once — which model is in use, or that theirs is dead. */
|
|
118
|
-
onNotice?: (message: string, level: "info" | "warning") => void;
|
|
219
|
+
onNotice?: (message: string, level: "info" | "warning", notice?: ClassifierNotice) => void;
|
|
119
220
|
/** Report each classifier reply's usage, for the all-in footer cost. Observer
|
|
120
221
|
* only — it never affects the payload sent or the verdict parsed. */
|
|
121
222
|
onUsage?: (usage: unknown) => void;
|
|
@@ -135,7 +236,7 @@ export interface ClassifierDeps {
|
|
|
135
236
|
}
|
|
136
237
|
|
|
137
238
|
/** How a single candidate's attempt failed, so the outer loop can react. */
|
|
138
|
-
type StepKind = "timeout" | "cancelled" | "unavailable" | "error" | "truncated";
|
|
239
|
+
type StepKind = "timeout" | "cancelled" | "unavailable" | "refused" | "error" | "truncated" | "transcript-too-long";
|
|
139
240
|
class StepError extends Error {
|
|
140
241
|
constructor(
|
|
141
242
|
readonly kind: StepKind,
|
|
@@ -146,42 +247,80 @@ class StepError extends Error {
|
|
|
146
247
|
}
|
|
147
248
|
|
|
148
249
|
/** Signature that must stay equal for the cached chain to be reused (see ClassifierState.chainCache). */
|
|
149
|
-
function selectionSignature(
|
|
150
|
-
const
|
|
151
|
-
return `${
|
|
250
|
+
function selectionSignature(sessionModel: Model<Api> | undefined, config: Pick<AutoModeConfig, "classifierModel" | "classifierModelSetFor">): string {
|
|
251
|
+
const configured = `${config.classifierModel ?? ""}|${config.classifierModelSetFor ?? ""}`;
|
|
252
|
+
return sessionModel ? `${sessionModel.provider}/${sessionModel.id}|${sessionModel.contextWindow ?? "unknown"}|${configured}` : `(none)|${configured}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The selection chain for this session model and setting, built once per
|
|
257
|
+
* signature and shared by every reader: the gate, the badge and banner that
|
|
258
|
+
* name the next classifier, and the announcement. The newer-model warnings
|
|
259
|
+
* are always in it; a reader that honours `suggestNewerModels` drops them.
|
|
260
|
+
*/
|
|
261
|
+
export function classifierChain(
|
|
262
|
+
state: ClassifierState,
|
|
263
|
+
input: { available: () => Model<Api>[]; sessionModel: Model<Api> | undefined; config: Pick<AutoModeConfig, "classifierModel" | "classifierModelSetFor"> },
|
|
264
|
+
): NonNullable<ClassifierState["chainCache"]> {
|
|
265
|
+
const signature = selectionSignature(input.sessionModel, input.config);
|
|
266
|
+
const cached = state.chainCache;
|
|
267
|
+
if (cached && cached.signature === signature) return cached;
|
|
268
|
+
const built = classifierCandidates({
|
|
269
|
+
available: input.available(),
|
|
270
|
+
sessionModel: input.sessionModel,
|
|
271
|
+
configured: input.config.classifierModel,
|
|
272
|
+
configuredSetForContainment: input.config.classifierModelSetFor,
|
|
273
|
+
});
|
|
274
|
+
const chain = { signature, candidates: built.candidates, notices: built.notices, ...(built.fallback ? { fallback: built.fallback } : {}) };
|
|
275
|
+
// Don't poison the cache with an empty chain (e.g. a not-yet-populated
|
|
276
|
+
// registry) — that would permanently block the gate; recompute next call.
|
|
277
|
+
if (built.candidates.length > 0) state.chainCache = chain;
|
|
278
|
+
return chain;
|
|
152
279
|
}
|
|
153
280
|
|
|
154
281
|
/** The candidate chain (minus anything already unusable this session) and its notices. */
|
|
155
282
|
function remainingCandidates(deps: ClassifierDeps): { candidates: Candidate[]; notices: ClassifierNotice[] } {
|
|
156
|
-
const
|
|
157
|
-
let cached = deps.state.chainCache;
|
|
158
|
-
if (!cached || cached.signature !== signature) {
|
|
159
|
-
const built = classifierCandidates({
|
|
160
|
-
available: deps.registry.getAvailable(),
|
|
161
|
-
sessionModel: deps.sessionModel,
|
|
162
|
-
configured: deps.config.classifierModel,
|
|
163
|
-
configuredSetForContainment: deps.config.classifierModelSetFor,
|
|
164
|
-
});
|
|
165
|
-
cached = { signature, candidates: built.candidates, notices: built.notices };
|
|
166
|
-
// Don't poison the cache with an empty chain (e.g. a not-yet-populated
|
|
167
|
-
// registry) — that would permanently block the gate; recompute next call.
|
|
168
|
-
if (built.candidates.length > 0) deps.state.chainCache = cached;
|
|
169
|
-
}
|
|
170
|
-
const { candidates: all, notices } = cached;
|
|
283
|
+
const { candidates: all, notices } = classifierChain(deps.state, { available: () => deps.registry.getAvailable(), sessionModel: deps.sessionModel, config: deps.config });
|
|
171
284
|
const usable = all.filter((entry) => !deps.state.rejected.has(`${entry.model.provider}/${entry.model.id}`));
|
|
172
285
|
if (usable.length > 0) return { candidates: usable, notices };
|
|
173
286
|
// If everything has been rejected, the session model is still worth one more
|
|
174
287
|
// attempt: a gate that stops asking has stopped gating. It has to be the
|
|
175
288
|
// session model specifically — the *last* chain entry is the cost-ranked pick,
|
|
176
|
-
// which nothing price-chosen should lead, let alone be the sole retry.
|
|
289
|
+
// which nothing price-chosen should lead, let alone be the sole retry. A model
|
|
290
|
+
// that refused the prompt is not retried: it would refuse again
|
|
291
|
+
// (`refusalStop`).
|
|
177
292
|
const session = all.filter((entry) => entry.source === "session");
|
|
178
|
-
|
|
293
|
+
const retry = session.length > 0 ? session : all.slice(-1);
|
|
294
|
+
return { candidates: retry.filter((entry) => !deps.state.refused.has(`${entry.model.provider}/${entry.model.id}`)), notices };
|
|
179
295
|
}
|
|
180
296
|
|
|
181
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Every candidate is gone and one refused the classifier's prompt: the call is
|
|
299
|
+
* not judged, and the user is told once how to go on.
|
|
300
|
+
*/
|
|
301
|
+
function refusalStop(deps: ClassifierDeps, unusable: readonly string[] = []): ClassifyVerdict {
|
|
302
|
+
const refused = [...deps.state.refused].join(", ");
|
|
303
|
+
// A model that failed this call for another cause has its own fix: name it too.
|
|
304
|
+
const others = unusable.length > 0 ? ` ${unusable.join("; ")}.` : "";
|
|
305
|
+
notifyOnce(
|
|
306
|
+
deps,
|
|
307
|
+
"refusal-stop",
|
|
308
|
+
`Auto mode has no classifier left: ${refused} refused the classifier's prompt as a possible usage-policy violation, and no other eligible model can screen calls.${others} ` +
|
|
309
|
+
`Calls are not judged and do not run. Switch to manual approval (${modeCycleKey()} cycles permission modes), or choose another classifier with /auto-mode model <provider/id>.`,
|
|
310
|
+
"warning",
|
|
311
|
+
);
|
|
312
|
+
return {
|
|
313
|
+
decision: "block",
|
|
314
|
+
reason: `Auto mode has no classifier: ${refused} refused the classifier's prompt as a possible usage-policy violation.${others}`,
|
|
315
|
+
tier: "unmatched",
|
|
316
|
+
noVerdict: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function notifyOnce(deps: ClassifierDeps, key: string, message: string, level: "info" | "warning", notice?: ClassifierNotice) {
|
|
182
321
|
if (deps.state.notified.has(key)) return;
|
|
183
322
|
deps.state.notified.add(key);
|
|
184
|
-
deps.onNotice?.(message, level);
|
|
323
|
+
deps.onNotice?.(message, level, notice);
|
|
185
324
|
}
|
|
186
325
|
|
|
187
326
|
/**
|
|
@@ -191,17 +330,31 @@ function notifyOnce(deps: ClassifierDeps, key: string, message: string, level: "
|
|
|
191
330
|
export async function classify(request: ClassifyRequest, deps: ClassifierDeps): Promise<ClassifyVerdict> {
|
|
192
331
|
const { candidates, notices } = remainingCandidates(deps);
|
|
193
332
|
if (candidates.length === 0) {
|
|
194
|
-
|
|
333
|
+
if (deps.state.refused.size > 0) return refusalStop(deps);
|
|
334
|
+
return { decision: "block", reason: "No model is available to run the auto-mode classifier.", tier: "unmatched", noVerdict: true };
|
|
195
335
|
}
|
|
196
336
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
for (const notice of notices) notifyOnce(deps, notice.text, notice.text, notice.level);
|
|
337
|
+
// Automatic-selection fallback explanations are surfaced once per distinct message.
|
|
338
|
+
// A newer-model hint about a chosen classifier is the session-start
|
|
339
|
+
// announcement's to make (it honours suggestNewerModels), not a gated call's.
|
|
340
|
+
for (const notice of notices) if (notice.choiceWarning !== "newer") notifyOnce(deps, notice.text, notice.text, notice.level, notice);
|
|
341
|
+
|
|
342
|
+
// The action is sent whole (transcript.ts MAX_ACTION_CHARS); one too large for
|
|
343
|
+
// that is refused with its size, never judged from a prefix.
|
|
344
|
+
const actionChars = deps.reviewOnly ? 0 : actionLength(request.transcript);
|
|
345
|
+
if (actionChars > MAX_ACTION_CHARS) {
|
|
346
|
+
return {
|
|
347
|
+
decision: "block",
|
|
348
|
+
reason: `This call is ${actionChars} characters, more than the ${MAX_ACTION_CHARS} the auto-mode classifier reviews in full. Split it into smaller calls.`,
|
|
349
|
+
tier: "unmatched",
|
|
350
|
+
noVerdict: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
201
353
|
|
|
202
354
|
// buildPayload builds the ~110KB ruleset once and returns the grounding index
|
|
203
355
|
// derived from it, so the ruleset is not rebuilt/re-parsed a second time here.
|
|
204
|
-
const { system, userPrefix, index } = buildPayload(request);
|
|
356
|
+
const { system, userPrefix, history, tail, index } = buildPayload(request);
|
|
357
|
+
const previousHistoryEnd = deps.state.cacheHistoryEnd;
|
|
205
358
|
const stage1Text = stage1User(userPrefix);
|
|
206
359
|
// A permission rule refused something this turn, so stage 2 gets the extra
|
|
207
360
|
// instruction that the user's intent does not excuse an equivalent-effect
|
|
@@ -209,6 +362,10 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
209
362
|
// left byte-identical to CC's.
|
|
210
363
|
const afterRuleDenial = request.transcript.some((entry) => entry.kind === "denied");
|
|
211
364
|
const debug = process.env.CC_AUTO_MODE_DEBUG;
|
|
365
|
+
// Probe hook (test/e2e/cache-probe.sh): one JSON line per classifier reply with
|
|
366
|
+
// its text and pi-ai's normalized usage, whatever the transport (Codex runs
|
|
367
|
+
// over WebSocket, where a fetch wrapper sees nothing). Opt-in, never read back.
|
|
368
|
+
const usageLog = process.env.CC_AUTO_MODE_LOG;
|
|
212
369
|
|
|
213
370
|
// A model already pinned this session is tried first, so the classifier does
|
|
214
371
|
// not change under the session while the pin is healthy. The rest of the chain
|
|
@@ -234,7 +391,11 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
234
391
|
|
|
235
392
|
let lastError = "";
|
|
236
393
|
let sawTimeout = false;
|
|
394
|
+
/** Candidates rejected this call as unusable, with their errors. */
|
|
395
|
+
const unusable: string[] = [];
|
|
237
396
|
let timedOutKey: string | undefined;
|
|
397
|
+
/** The timed-out candidate's own error: a later candidate's failure must not relabel it. */
|
|
398
|
+
let timedOutError = "";
|
|
238
399
|
// Walks `attempts` in order; a candidate that turns out to require thinking is
|
|
239
400
|
// re-queued at the front for exactly one retry (see the reasoning-mandatory
|
|
240
401
|
// branch below). The `!== undefined` guard narrows `candidate`, so no cast.
|
|
@@ -257,7 +418,7 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
257
418
|
// One provider call. `reasoning`/`temperature` are deliberately omitted: pi
|
|
258
419
|
// turns thinking off entirely when `reasoning` is absent (what a classifier
|
|
259
420
|
// wants), and `temperature` is deprecated/unsupported on several models and
|
|
260
|
-
// fails closed. See docs/decisions/auto-mode.md. The one exception: a model
|
|
421
|
+
// fails closed. See working-docs/decisions/auto-mode.md. The one exception: a model
|
|
261
422
|
// that CANNOT disable thinking (catalog-marked, or learned via the
|
|
262
423
|
// reasoning-mandatory retry below) gets its lowest supported level — the
|
|
263
424
|
// off-request would 400 and the gate would fail closed on every call.
|
|
@@ -279,7 +440,15 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
279
440
|
env: auth.env,
|
|
280
441
|
signal: deps.signal ? AbortSignal.any([deps.signal, timeout]) : timeout,
|
|
281
442
|
maxTokens,
|
|
443
|
+
maxRetries: CLASSIFIER_MAX_RETRIES,
|
|
282
444
|
cacheRetention: "long",
|
|
445
|
+
...(deps.cacheKey ? { sessionId: deps.cacheKey } : {}),
|
|
446
|
+
...(CLASSIFIER_CACHE_APIS.has(model.api) ? {
|
|
447
|
+
onPayload: (payload: unknown) => {
|
|
448
|
+
cacheClassifierHistory(payload, [...history, tail, userText.slice(userPrefix.length)], history.length - 1, previousHistoryEnd);
|
|
449
|
+
deps.state.cacheHistoryEnd = history.length - 1;
|
|
450
|
+
},
|
|
451
|
+
} : {}),
|
|
283
452
|
...(reasoning ? { reasoning } : {}),
|
|
284
453
|
},
|
|
285
454
|
);
|
|
@@ -288,13 +457,25 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
288
457
|
// Turn one reply into text, or throw a StepError the loop knows how to route.
|
|
289
458
|
// `big` is the retry cap for a length-truncated reply; a still-truncated reply
|
|
290
459
|
// at `big` is its own outcome (not a candidate to step past).
|
|
460
|
+
const padding = reasoning ? FORCED_THINKING_PADDING : 0;
|
|
291
461
|
const call = async (userText: string, base: number, big: number, stage: number): Promise<string> => {
|
|
462
|
+
base += padding;
|
|
463
|
+
big += padding;
|
|
292
464
|
const inspect = (reply: AssistantMessage): string | "length" => {
|
|
293
465
|
deps.onUsage?.(reply.usage);
|
|
466
|
+
if (usageLog) {
|
|
467
|
+
try {
|
|
468
|
+
appendFileSync(usageLog, `${JSON.stringify({ model: key, api: model.api, stage, system, user: userText, usage: reply.usage, stopReason: reply.stopReason })}\n`);
|
|
469
|
+
} catch {
|
|
470
|
+
// A probe log that cannot be written must not fail the gate.
|
|
471
|
+
}
|
|
472
|
+
}
|
|
294
473
|
if (deps.signal?.aborted) throw new StepError("cancelled", "cancelled");
|
|
295
474
|
if (reply.stopReason === "error" || reply.stopReason === "aborted") {
|
|
296
475
|
const msg = reply.errorMessage ?? reply.stopReason ?? "provider error";
|
|
297
|
-
if (
|
|
476
|
+
if (isContextOverflow(reply)) throw new StepError("transcript-too-long", msg);
|
|
477
|
+
if (isClassifierTimeout(msg) || isTransportFailure(msg)) throw new StepError("timeout", msg);
|
|
478
|
+
if (isPromptPolicyRefusal(msg)) throw new StepError("refused", msg);
|
|
298
479
|
throw new StepError(isModelUnavailableError(msg) ? "unavailable" : "error", msg);
|
|
299
480
|
}
|
|
300
481
|
if (reply.stopReason === "length") return "length";
|
|
@@ -306,10 +487,13 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
306
487
|
try {
|
|
307
488
|
seen = inspect(reply);
|
|
308
489
|
} catch (error) {
|
|
309
|
-
// One retry on a timeout, before giving up on
|
|
310
|
-
// load usually clears on a second try
|
|
311
|
-
//
|
|
312
|
-
|
|
490
|
+
// One retry on a timeout or a usage-policy refusal, before giving up on
|
|
491
|
+
// this model: a stall under load usually clears on a second try, and a
|
|
492
|
+
// refused prompt usually passes. The identical request still reads the
|
|
493
|
+
// session's cached prefix. A user cancel or a substantive error is not
|
|
494
|
+
// retried.
|
|
495
|
+
if (error instanceof StepError && (error.kind === "timeout" || error.kind === "refused")) {
|
|
496
|
+
if (error.kind === "timeout" && isTransportFailure(error.message)) await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
|
|
313
497
|
reply = await runCall(userText, base, stage);
|
|
314
498
|
seen = inspect(reply);
|
|
315
499
|
} else {
|
|
@@ -331,12 +515,13 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
331
515
|
if (deps.reviewOnly) {
|
|
332
516
|
// A finished run has no harm floor to short-circuit — go straight to the
|
|
333
517
|
// full stage-2 evaluation, reframed to judge the whole sequence.
|
|
334
|
-
const text = await call(reviewUser(userPrefix),
|
|
518
|
+
const text = await call(reviewUser(userPrefix), 4096, 8192, 2);
|
|
335
519
|
verdict = parseStage2(text, index, request.userMessages);
|
|
336
520
|
stageInfo = `review sev=${parseSeverity(text) ?? "?"}`;
|
|
337
521
|
} else {
|
|
338
522
|
// STAGE 1 — harm only. maxTokens 64 like CC; if the reply overruns 64
|
|
339
523
|
// without a readable severity, one retry with headroom recovers it.
|
|
524
|
+
// Stage 2's 4096 is CC's too, with one retry at 8192.
|
|
340
525
|
const s1 = await call(stage1Text, 64, 1024, 1);
|
|
341
526
|
const sev1 = parseSeverity(s1);
|
|
342
527
|
if (sev1 !== null && sev1 < SEVERITY_THRESHOLD) {
|
|
@@ -346,7 +531,7 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
346
531
|
// sev1 >= 50, or unparseable → fall to the fuller stage-2 evaluation.
|
|
347
532
|
// stage2User is built here (not eagerly) so the common allow case
|
|
348
533
|
// does not concatenate the transcript prefix a second time.
|
|
349
|
-
const s2 = await call(stage2User(userPrefix, afterRuleDenial),
|
|
534
|
+
const s2 = await call(stage2User(userPrefix, afterRuleDenial), 4096, 8192, 2);
|
|
350
535
|
verdict = parseStage2(s2, index, request.userMessages);
|
|
351
536
|
stageInfo = `s1=${sev1 ?? "?"} s2=${parseSeverity(s2) ?? "?"}`;
|
|
352
537
|
// Stage 1 flagged the call and stage 2 cleared it without quoting the
|
|
@@ -385,15 +570,27 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
385
570
|
lastError = error instanceof Error ? error.message : String(error);
|
|
386
571
|
|
|
387
572
|
if (kind === "cancelled") {
|
|
388
|
-
return { decision: "block", reason: "Auto-mode classification was cancelled.", tier: "unmatched" };
|
|
573
|
+
return { decision: "block", reason: "Auto-mode classification was cancelled.", tier: "unmatched", noVerdict: true };
|
|
574
|
+
}
|
|
575
|
+
// The provider is authoritative about its token window. Do not clip the
|
|
576
|
+
// history or retry another model: the gate must offer manual approval
|
|
577
|
+
// interactively, or stop a headless run, rather than invent a verdict.
|
|
578
|
+
if (kind === "transcript-too-long" || (kind === "error" && isClassifierTranscriptTooLong(lastError))) {
|
|
579
|
+
return {
|
|
580
|
+
decision: "block",
|
|
581
|
+
reason: `Classifier transcript exceeded context window (${key}). The action was not judged. Compact the session (/compact) and continue.`,
|
|
582
|
+
tier: "unmatched",
|
|
583
|
+
noVerdict: true,
|
|
584
|
+
transcriptTooLong: true,
|
|
585
|
+
};
|
|
389
586
|
}
|
|
390
587
|
if (kind === "truncated") {
|
|
391
588
|
if (debug) process.stderr.write(`[auto-mode] ${key} ${request.toolName} → verdict truncated at maxTokens\n`);
|
|
392
589
|
return {
|
|
393
590
|
decision: "block",
|
|
394
|
-
reason:
|
|
395
|
-
"The approval classifier's reply was cut off by its output limit before the verdict completed. If this keeps happening, pin a stronger classifier model with /auto-mode model.",
|
|
591
|
+
reason: "The approval classifier's reply was cut off by its output limit before the verdict completed.",
|
|
396
592
|
tier: "unmatched",
|
|
593
|
+
noVerdict: true,
|
|
397
594
|
};
|
|
398
595
|
}
|
|
399
596
|
if (kind === "error") {
|
|
@@ -414,29 +611,53 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
414
611
|
}
|
|
415
612
|
// A substantive, non-transient failure that is not "model unusable here":
|
|
416
613
|
// surface it rather than papering over something about to clear.
|
|
417
|
-
return { decision: "block", reason: `
|
|
614
|
+
return { decision: "block", reason: `The auto-mode classifier returned no verdict (${providerErrorSummary(lastError)}).`, tier: "unmatched", noVerdict: true };
|
|
418
615
|
}
|
|
419
616
|
if (kind === "timeout") {
|
|
420
617
|
// Transient — do NOT reject the model, it may be fine next call. Step to
|
|
421
618
|
// the next candidate; a pinned model that keeps timing out is unpinned.
|
|
422
619
|
sawTimeout = true;
|
|
423
620
|
timedOutKey = key;
|
|
621
|
+
timedOutError = lastError;
|
|
424
622
|
if (deps.state.pinned && `${deps.state.pinned.provider}/${deps.state.pinned.id}` === key) {
|
|
425
623
|
deps.state.timeoutStreak += 1;
|
|
426
624
|
if (deps.state.timeoutStreak >= ROTATE_AFTER_TIMEOUTS) deps.state.pinned = undefined;
|
|
427
625
|
}
|
|
428
626
|
continue;
|
|
429
627
|
}
|
|
628
|
+
if (kind === "refused") {
|
|
629
|
+
// Refused twice in a row: drop the model for the session and pin the next
|
|
630
|
+
// one, rather than pay a fallback's uncached prefix on every refusal. The
|
|
631
|
+
// refusal is about the classifier's prompt, not the account, so the
|
|
632
|
+
// subagent selector is not told.
|
|
633
|
+
reject(key);
|
|
634
|
+
deps.state.refused.add(key);
|
|
635
|
+
const next = queue[0]?.model;
|
|
636
|
+
// A candidate that timed out on this call is still eligible: end the
|
|
637
|
+
// chain as a timeout (after the loop), not as "no classifier left".
|
|
638
|
+
if (!next) {
|
|
639
|
+
if (sawTimeout) continue;
|
|
640
|
+
return refusalStop(deps, unusable);
|
|
641
|
+
}
|
|
642
|
+
const ratio = classifierCostRatio(next, model);
|
|
643
|
+
notifyOnce(
|
|
644
|
+
deps,
|
|
645
|
+
`refused:${key}`,
|
|
646
|
+
`${key} refused the auto-mode classifier's prompt twice as a possible usage-policy violation, so auto mode screens calls with ` +
|
|
647
|
+
`${spec(next)} for the rest of this session${isNotablyCostlier(ratio) ? `, at about ${costRatioText(ratio)} the cost per call` : ""}. ` +
|
|
648
|
+
"Choose another classifier with /auto-mode model <provider/id>.",
|
|
649
|
+
"warning",
|
|
650
|
+
);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
430
653
|
// unavailable: this model is not usable here — step over it and record it.
|
|
431
654
|
reject(key);
|
|
432
655
|
deps.onModelUnusable?.(key, lastError);
|
|
433
|
-
|
|
656
|
+
unusable.push(`${key} cannot be used (${lastError})`);
|
|
434
657
|
notifyOnce(
|
|
435
658
|
deps,
|
|
436
659
|
`rejected:${key}`,
|
|
437
|
-
|
|
438
|
-
? `Auto mode cannot use ${key} from autoMode.classifierModel (${lastError}). Set a different model in ~/.onecode/settings.json.`
|
|
439
|
-
: `Auto mode cannot use ${key} as its classifier (${lastError}); trying another model. Set autoMode.classifierModel in ~/.onecode/settings.json to choose one.`,
|
|
660
|
+
`Auto mode cannot use ${key} as its classifier (${lastError}); trying another eligible model.`,
|
|
440
661
|
"warning",
|
|
441
662
|
);
|
|
442
663
|
}
|
|
@@ -448,19 +669,23 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
|
|
|
448
669
|
// where it can, and only blocks outright when running non-interactively.
|
|
449
670
|
if (sawTimeout) {
|
|
450
671
|
const model = timedOutKey ? timedOutKey.split("/").pop() : undefined;
|
|
672
|
+
const network = isTransportFailure(timedOutError);
|
|
451
673
|
return {
|
|
452
674
|
decision: "block",
|
|
453
675
|
tier: "timeout",
|
|
676
|
+
noVerdict: true,
|
|
454
677
|
reason:
|
|
455
|
-
`Auto mode could not screen this ${request.toolName} call in time — the approval classifier${model ? ` (${model})` : ""} ` +
|
|
456
|
-
|
|
678
|
+
`Auto mode could not screen this ${request.toolName} call${network ? "" : " in time"} — the approval classifier${model ? ` (${model})` : ""} ` +
|
|
679
|
+
`is temporarily unavailable (${network ? `network error: ${timedOutError}` : "timed out"}), so the call was not judged either way.`,
|
|
457
680
|
};
|
|
458
681
|
}
|
|
459
682
|
|
|
683
|
+
if (deps.state.refused.size > 0) return refusalStop(deps, unusable);
|
|
460
684
|
// Every candidate was unusable — say which knob fixes it.
|
|
461
685
|
return {
|
|
462
686
|
decision: "block",
|
|
463
|
-
reason: `No usable auto-mode classifier model (last error: ${lastError})
|
|
687
|
+
reason: `No usable auto-mode classifier model (last error: ${lastError}).`,
|
|
464
688
|
tier: "unmatched",
|
|
689
|
+
noVerdict: true,
|
|
465
690
|
};
|
|
466
691
|
}
|