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,24 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* background extension — Claude Code's background-task surface:
|
|
3
|
-
* monitor, task_output, task_stop, schedule_wakeup
|
|
3
|
+
* monitor, task_output, task_stop, schedule_wakeup, cron_create, cron_list,
|
|
4
|
+
* cron_delete, and /loop.
|
|
4
5
|
*
|
|
5
6
|
* Owns the BackgroundRegistry. Other extensions (subagents) register their
|
|
6
7
|
* long-running work over TASK_REGISTER_CHANNEL at runtime, so task_output and
|
|
7
8
|
* task_stop address every background task in the session regardless of which
|
|
8
9
|
* extension started it. Events and completions are delivered as steered
|
|
9
10
|
* task notifications (lib/notifications.ts), never as user input; a fired
|
|
10
|
-
* wakeup or
|
|
11
|
+
* wakeup or cron job re-invokes the session with its prompt verbatim.
|
|
12
|
+
*
|
|
13
|
+
* Every scheduled prompt lives in one CronStore (cron.ts): cron_create jobs
|
|
14
|
+
* and schedule_wakeup's one-shot wakeups (wakeup.ts). One timer is armed to
|
|
15
|
+
* the store's next fire; jobs fire only while the agent is idle, so a job
|
|
16
|
+
* that came due during a turn fires once at agent_settled. `/loop` is
|
|
17
|
+
* Claude Code's skill (skills/loop, its body built by loop-skill.ts): the
|
|
18
|
+
* model schedules the loop itself.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
22
|
import { Type } from "typebox";
|
|
15
23
|
import { whenAborted } from "../lib/abort.ts";
|
|
16
24
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
17
|
-
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
18
|
-
import { detachedSpawnOptions, KILL_GRACE_MS, stopProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
25
|
+
import { persistedFilePreview, persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
26
|
+
import { detachedSpawnOptions, KILL_GRACE_MS, rememberProcessGroup, stopProcessTree, waitForChildExit } from "../lib/process-tree.ts";
|
|
19
27
|
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
20
28
|
import { bashSpawn, spawnShellCommand } from "../lib/shell-spawn.ts";
|
|
21
|
-
import { ccToolRenderers, customMessageText, liveUiCtx, notificationComponent, scheduledTaskComponent } from "../lib/tui-render.ts";
|
|
29
|
+
import { ccToolRenderers, customMessageText, formatFireTime, liveUiCtx, notificationComponent, notificationUserMarkdown, scheduledTaskComponent } from "../lib/tui-render.ts";
|
|
22
30
|
import {
|
|
23
31
|
type BackgroundTask,
|
|
24
32
|
BackgroundRegistry,
|
|
@@ -26,16 +34,47 @@ import {
|
|
|
26
34
|
generateTaskId,
|
|
27
35
|
TASK_REGISTER_CHANNEL,
|
|
28
36
|
} from "./registry.ts";
|
|
37
|
+
import { SWITCH_CANCEL, SWITCH_STOP, switchWarning, workWidgetLine } from "./switch-guard.ts";
|
|
29
38
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
parseLoopArgs,
|
|
39
|
+
AGED_OUT_RESULT,
|
|
40
|
+
DynamicLoop,
|
|
41
|
+
formatScheduled,
|
|
42
|
+
formatStopped,
|
|
43
|
+
SCHEDULE_WAKEUP_DESCRIPTION,
|
|
44
|
+
SCHEDULE_WAKEUP_PARAMS,
|
|
45
|
+
WAKEUP_ERRORS,
|
|
38
46
|
} from "./wakeup.ts";
|
|
47
|
+
import {
|
|
48
|
+
deliveryFromState,
|
|
49
|
+
isAutonomousSentinel,
|
|
50
|
+
isLoopFileSentinel,
|
|
51
|
+
LOOP_DELIVERY_DETAILS_KEY,
|
|
52
|
+
LOOP_DELIVERY_STATE,
|
|
53
|
+
type LoopDeliveryState,
|
|
54
|
+
readLoopFile,
|
|
55
|
+
resolveLoopFire,
|
|
56
|
+
} from "./loop-fire.ts";
|
|
57
|
+
import { autonomousPreamble, loopSkillPrompt } from "./loop-skill.ts";
|
|
58
|
+
import {
|
|
59
|
+
CRON_CREATE_DESCRIPTION,
|
|
60
|
+
CRON_CREATE_PARAMETERS,
|
|
61
|
+
CRON_DELETE_DESCRIPTION,
|
|
62
|
+
CRON_DELETE_PARAMETERS,
|
|
63
|
+
CRON_LIST_DESCRIPTION,
|
|
64
|
+
CRON_LIST_PARAMETERS,
|
|
65
|
+
CronStore,
|
|
66
|
+
type CronFire,
|
|
67
|
+
type CronJob,
|
|
68
|
+
describeCadence,
|
|
69
|
+
formatCannotFire,
|
|
70
|
+
formatCreateResult,
|
|
71
|
+
formatDeleteResult,
|
|
72
|
+
formatJobList,
|
|
73
|
+
formatUnknownJob,
|
|
74
|
+
JOB_FIRED_ENTRY,
|
|
75
|
+
scheduledJobsOnBranch,
|
|
76
|
+
WAKEUP_ARMED_ENTRY,
|
|
77
|
+
} from "./cron.ts";
|
|
39
78
|
import {
|
|
40
79
|
createTaskNotifier,
|
|
41
80
|
monitorEndedSummary,
|
|
@@ -55,29 +94,105 @@ import {
|
|
|
55
94
|
MONITOR_BATCH_MAX_CHARS,
|
|
56
95
|
MONITOR_BATCH_IDLE_MS,
|
|
57
96
|
type MonitorBatch,
|
|
97
|
+
MonitorLineSplitter,
|
|
58
98
|
pushEvent,
|
|
59
99
|
} from "./monitor-batch.ts";
|
|
60
100
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
101
|
+
import { applyNoopFolds, decideFold, type FoldDetails, type FoldEntry, foldSuffix } from "./noop-fold.ts";
|
|
102
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
103
|
+
import { AGENT_CRON_CHANNEL, AGENT_CRON_FIRE_CHANNEL, type AgentCronFire, type AgentCronRequest, type FireConversation, formatNotOwner } from "../lib/agent-cron.ts";
|
|
104
|
+
import { SKILL_BODY_CHANNEL, type SkillBodyQuery, SLASH_EXPAND_CHANNEL, type SlashExpandQuery } from "../lib/skill-body.ts";
|
|
105
|
+
import { BUNDLED_SKILLS_DIR } from "../lib/skill-scan.ts";
|
|
106
|
+
import { createWriteStream, mkdirSync, type WriteStream } from "node:fs";
|
|
107
|
+
import { join, resolve } from "node:path";
|
|
108
|
+
import { StringDecoder } from "node:string_decoder";
|
|
109
|
+
import { SESSION_WORK_CHANNEL, sessionBackgroundTask, sessionCron, type SessionWorkQuery } from "../lib/session-work.ts";
|
|
110
|
+
import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
111
|
+
import { queueNotice } from "../lib/notices.ts";
|
|
112
|
+
import { makeRecord, RECORD } from "../lib/ledger/records.ts";
|
|
113
|
+
import { recordState, stateSinceCompaction } from "../lib/ledger/state.ts";
|
|
114
|
+
import { processState } from "../lib/process-state.ts";
|
|
115
|
+
import { extensionVersion } from "../lib/package-version.ts";
|
|
61
116
|
|
|
62
|
-
|
|
117
|
+
/** Claude Code's `task_output` limit (`TASK_MAX_OUTPUT_DEFAULT`). */
|
|
118
|
+
const TASK_OUTPUT_MAX_CHARS = 32_000;
|
|
63
119
|
const STORED_OUTPUT_CAP = 200_000;
|
|
64
120
|
const DEFAULT_MONITOR_TIMEOUT_MS = 300_000;
|
|
65
121
|
const MAX_MONITOR_TIMEOUT_MS = 3_600_000;
|
|
66
122
|
const MAX_BLOCK_TIMEOUT_MS = 600_000;
|
|
123
|
+
/** A finished monitor that saw nothing says so, never a blank a model reads as "still running" (bash's EMPTY_OUTPUT_MARKER). */
|
|
124
|
+
const MONITOR_EMPTY_OUTPUT = "(no output — the monitor received no events and nothing on stderr)";
|
|
125
|
+
/** Heads a monitor's output when the start is gone: past the in-memory cap with no spool file to hold it. */
|
|
126
|
+
const SPOOL_LOST_NOTE = `[The monitor's spool file could not be written, so only the last ${STORED_OUTPUT_CAP.toLocaleString("en-US")} characters of its output were kept.]\n`;
|
|
127
|
+
/** setTimeout's longest delay; a later fire re-arms when this one wakes. */
|
|
128
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
67
129
|
function tail(text: string, cap: number): string {
|
|
68
130
|
return text.length <= cap ? text : `… (earlier output truncated)\n${text.slice(-cap)}`;
|
|
69
131
|
}
|
|
70
132
|
|
|
71
133
|
/**
|
|
72
|
-
* `/clear`, `/new` and `/
|
|
73
|
-
* they belong to (the notifier that would report them is inert by
|
|
74
|
-
* the new
|
|
75
|
-
* the old session's UI is torn down right after `session_shutdown`,
|
|
76
|
-
* notice printed there is lost
|
|
77
|
-
*
|
|
78
|
-
*
|
|
134
|
+
* `/clear`, `/new`, `/resume` and `/reload` stop every background task with
|
|
135
|
+
* the session they belong to (the notifier that would report them is inert by
|
|
136
|
+
* then, and the new instance has no handle on them). Say so, once, in the NEW
|
|
137
|
+
* session: the old session's UI is torn down right after `session_shutdown`,
|
|
138
|
+
* so a notice printed there is lost (LIFECYCLE-REVIEW-2026-09-06 L3). The
|
|
139
|
+
* notice is process state (lib/process-state.ts): module scope would lose it
|
|
140
|
+
* wherever pi evaluates this module again, at `/reload` and at a `/resume`
|
|
141
|
+
* into another directory (lifecycle grid R3-L13).
|
|
142
|
+
*/
|
|
143
|
+
const shutdownNotice = () => processState<{ text?: string }>("background:shutdown-notice", () => ({}));
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `<sessionDir>/monitor/<taskId>/output.log`, beside background shells'
|
|
147
|
+
* `bash/<taskId>/`. A session-less run spools under the session's private
|
|
148
|
+
* results dir (`sessionResultsDir`), so its output is never kept only as the
|
|
149
|
+
* in-memory tail. Undefined when the folder cannot be made.
|
|
150
|
+
*/
|
|
151
|
+
function monitorLogPath(ctx: ExtensionContext, taskId: string): string | undefined {
|
|
152
|
+
try {
|
|
153
|
+
const dir = join(sessionResultsDir(ctx), "monitor", taskId);
|
|
154
|
+
mkdirSync(dir, { recursive: true });
|
|
155
|
+
return join(dir, "output.log");
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `task_output`'s body. Past `TASK_OUTPUT_MAX_CHARS` a task with a spool file
|
|
163
|
+
* gets Claude Code's clip, `[Truncated. Full output: <path>]` and the tail
|
|
164
|
+
* that fits, since the file holds the full text; a task without one (a
|
|
165
|
+
* subagent's report) is persisted whole to the session's tool-results
|
|
166
|
+
* folder, never cut (decisions/tools.md).
|
|
79
167
|
*/
|
|
80
|
-
|
|
168
|
+
function taskOutputBody(task: BackgroundTask, ctx: ExtensionContext): string {
|
|
169
|
+
const output = task.output();
|
|
170
|
+
if (output.length <= TASK_OUTPUT_MAX_CHARS) return output;
|
|
171
|
+
if (task.logPath) {
|
|
172
|
+
const header = `[Truncated. Full output: ${task.logPath}]\n\n`;
|
|
173
|
+
return `${header}${output.slice(-(TASK_OUTPUT_MAX_CHARS - header.length))}`;
|
|
174
|
+
}
|
|
175
|
+
return persistIfLarge(output, { dir: sessionResultsDir(ctx), id: `task-output-${task.id}`, maxBytes: TASK_OUTPUT_MAX_CHARS });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A session entry as the no-op fold reads it (noop-fold.ts). */
|
|
179
|
+
function foldEntry(entry: { type: string; timestamp?: string; customType?: string; details?: unknown; message?: unknown }): FoldEntry {
|
|
180
|
+
if (entry.type === "compaction") return { kind: "compaction" };
|
|
181
|
+
if (entry.type === "custom_message") {
|
|
182
|
+
if (entry.customType === "wakeup") return { kind: "wakeup", timestamp: Date.parse(entry.timestamp ?? "") || 0, details: entry.details as FoldDetails | undefined };
|
|
183
|
+
return entry.customType === "cron" ? { kind: "fire" } : { kind: "other" };
|
|
184
|
+
}
|
|
185
|
+
if (entry.type !== "message") return { kind: "other" };
|
|
186
|
+
const message = entry.message as { role?: string; content?: unknown; stopReason?: string; toolCallId?: string };
|
|
187
|
+
if (message.role === "user") return { kind: "user" };
|
|
188
|
+
if (message.role === "toolResult") return { kind: "toolResult", toolCallId: message.toolCallId ?? "" };
|
|
189
|
+
if (message.role === "assistant") {
|
|
190
|
+
const blocks = Array.isArray(message.content) ? (message.content as Array<{ type?: string; id?: string; name?: string; arguments?: { noop?: unknown } }>) : [];
|
|
191
|
+
const toolCalls = blocks.filter((b) => b.type === "toolCall").map((b) => ({ id: b.id ?? "", name: b.name ?? "", noop: b.arguments?.noop }));
|
|
192
|
+
return { kind: "assistant", toolCalls, aborted: message.stopReason === "aborted" };
|
|
193
|
+
}
|
|
194
|
+
return { kind: "other" };
|
|
195
|
+
}
|
|
81
196
|
|
|
82
197
|
export default function backgroundExtension(pi: ExtensionAPI) {
|
|
83
198
|
// After session_shutdown the captured ctx throws on every access and the
|
|
@@ -88,32 +203,68 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
88
203
|
const alive = sessionAlive(pi);
|
|
89
204
|
const registry = new BackgroundRegistry();
|
|
90
205
|
let lastCtx: ExtensionContext | undefined;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
206
|
+
const cron = new CronStore();
|
|
207
|
+
const dynamicLoop = new DynamicLoop(cron);
|
|
208
|
+
let sessionCwd = process.cwd();
|
|
209
|
+
// The main session's worktree while it is in one (enter_worktree), where its fires resolve.
|
|
210
|
+
let worktreeCwd: string | undefined;
|
|
211
|
+
pi.events.on(WORKTREE_CHANNEL, (data) => {
|
|
212
|
+
worktreeCwd = (data as WorktreeLocation | null)?.path;
|
|
213
|
+
});
|
|
214
|
+
let cronTimer: NodeJS.Timeout | undefined;
|
|
215
|
+
// True between agent_start and agent_settled, so a due cron job waits for the
|
|
216
|
+
// turn to end instead of injecting one mid-run. agent_settled, not agent_end:
|
|
217
|
+
// agent_end can precede an internal retry / auto-compaction / queued
|
|
218
|
+
// continuation, and a fire landing in that gap would inject a turn while the
|
|
219
|
+
// prior one is still about to resume.
|
|
100
220
|
let agentBusy = false;
|
|
101
|
-
// Set when a dynamic /loop force-activated schedule_wakeup, so /loop stop can
|
|
102
|
-
// restore the lean tool surface it changed.
|
|
103
|
-
let activatedWakeupTool = false;
|
|
104
221
|
|
|
105
|
-
pi.events.on(TASK_REGISTER_CHANNEL, (
|
|
222
|
+
pi.events.on(TASK_REGISTER_CHANNEL, (data) => {
|
|
223
|
+
const task = data as BackgroundTask;
|
|
224
|
+
registry.register(task);
|
|
225
|
+
// Shells and subagent runs count toward the widget's warning line too.
|
|
226
|
+
updateWidget();
|
|
227
|
+
task.finished?.then(updateWidget, updateWidget);
|
|
228
|
+
});
|
|
229
|
+
// A subagent's cron tools (lib/agent-cron.ts): its jobs, its view, its deletes.
|
|
230
|
+
pi.events.on(AGENT_CRON_CHANNEL, (data) => {
|
|
231
|
+
const request = data as AgentCronRequest;
|
|
232
|
+
if (request.op === "create") {
|
|
233
|
+
const created = cron.create({ cron: request.cron, prompt: request.prompt, recurring: request.recurring, agentId: request.agentId, cwd: request.cwd }, Date.now());
|
|
234
|
+
request.result = created.ok
|
|
235
|
+
? { text: formatCreateResult(created.job), details: { jobId: created.job.id } }
|
|
236
|
+
: { text: created.error, isError: true };
|
|
237
|
+
if (created.ok) runCron();
|
|
238
|
+
} else if (request.op === "list") {
|
|
239
|
+
const jobs = cron.list().filter((job) => job.agentId === request.agentId);
|
|
240
|
+
request.result = { text: formatJobList(jobs), details: { jobCount: jobs.length } };
|
|
241
|
+
} else {
|
|
242
|
+
const job = cron.get(request.id);
|
|
243
|
+
if (!job) request.result = { text: formatUnknownJob(request.id), isError: true };
|
|
244
|
+
else if (job.agentId !== request.agentId) request.result = { text: formatNotOwner(request.id), isError: true };
|
|
245
|
+
else {
|
|
246
|
+
cron.delete(request.id);
|
|
247
|
+
runCron();
|
|
248
|
+
request.result = { text: formatDeleteResult(request.id) };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
// The Stop hook's session_crons / background_tasks (lib/session-work.ts).
|
|
253
|
+
pi.events.on(SESSION_WORK_CHANNEL, (data) => {
|
|
254
|
+
const query = data as SessionWorkQuery;
|
|
255
|
+
query.crons.push(...cron.list().map(sessionCron));
|
|
256
|
+
query.tasks.push(...registry.running().map(sessionBackgroundTask));
|
|
257
|
+
});
|
|
106
258
|
|
|
107
259
|
const updateWidget = () => {
|
|
108
260
|
// liveUiCtx: a stale ctx (session replaced) reads as "no UI", never a throw;
|
|
109
261
|
// session_shutdown also drops lastCtx, so a late repaint is a no-op.
|
|
110
262
|
const live = liveUiCtx(lastCtx);
|
|
111
263
|
if (!live) return;
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
live.ui.setWidget("cc-background", running > 0 ? [` background tasks: ${running} running`] : undefined);
|
|
264
|
+
// Every running task and scheduled job, panel-owned ones included: the
|
|
265
|
+
// line is the warning of what /clear, /reload or quitting stops (switch-guard.ts).
|
|
266
|
+
const line = workWidgetLine(registry.running().length, cron.list().length);
|
|
267
|
+
live.ui.setWidget("cc-background", line ? [line] : undefined);
|
|
117
268
|
};
|
|
118
269
|
|
|
119
270
|
// The wire frames are model-facing; the transcript shows a compact headline
|
|
@@ -123,19 +274,146 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
123
274
|
pi.registerMessageRenderer("task-notification", (message, { expanded }, theme) =>
|
|
124
275
|
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
125
276
|
);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
pi.registerMessageRenderer("loop-start", (message, { expanded }, theme) =>
|
|
133
|
-
notificationComponent(theme, customMessageText(message.content), expanded),
|
|
277
|
+
// A notification that opens the session's first turn is a user message
|
|
278
|
+
// (lib/notifications.ts, "First turn of a session"); pi draws those as
|
|
279
|
+
// markdown, so the same headline replaces the frames here. Every notifier's
|
|
280
|
+
// messages pass through this one transformer.
|
|
281
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
282
|
+
context.messageType === "user" ? (notificationUserMarkdown(markdown) ?? markdown) : markdown,
|
|
134
283
|
);
|
|
284
|
+
for (const customType of ["wakeup", "cron"]) {
|
|
285
|
+
// Claude Code: "Claude resuming /loop wakeup (…)" for a wakeup; the model here may not be Claude.
|
|
286
|
+
const label = customType === "wakeup" ? "Resuming /loop wakeup" : undefined;
|
|
287
|
+
pi.registerMessageRenderer(customType, (message, { expanded }, theme) => {
|
|
288
|
+
const fold = message.details as FoldDetails | undefined;
|
|
289
|
+
const suffix = fold?.noOpStreak ? foldSuffix(fold.noOpStreak, formatFireTime(fold.streakStartedAt ?? Date.now())) : "";
|
|
290
|
+
return scheduledTaskComponent(theme, customMessageText(message.content), (message as { timestamp?: number }).timestamp ?? Date.now(), expanded, label, suffix);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
135
293
|
|
|
136
294
|
// A monitor's end the model already read through task_output is withdrawn
|
|
137
295
|
// like a shell's (lib/notifications.ts).
|
|
138
|
-
const notify = createTaskNotifier(pi, {
|
|
296
|
+
const notify = createTaskNotifier(pi, {
|
|
297
|
+
withdrawOnDelivery: true,
|
|
298
|
+
// A fire's message reached the conversation: what it told the model is on the branch from here.
|
|
299
|
+
onDelivered: (customType, details) => {
|
|
300
|
+
if (customType !== "cron" && customType !== "wakeup") return;
|
|
301
|
+
const stored = details[LOOP_DELIVERY_DETAILS_KEY];
|
|
302
|
+
if (stored !== undefined) recordDelivery(pi, deliveryFromState(stored));
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A no-prompt /loop sentinel's text (loop-fire.ts), against what the
|
|
308
|
+
* conversation it lands in was already sent. That record is branch state
|
|
309
|
+
* of that conversation: the main session's, or a subagent's own session,
|
|
310
|
+
* since each has its own history and compacts on its own. Read since the
|
|
311
|
+
* last compaction, so the first fire after one resends the instructions,
|
|
312
|
+
* as Claude Code does. `delivery` is the record this text makes true, set
|
|
313
|
+
* only when it changes (lifecycle grid R3-L12); the caller writes it when
|
|
314
|
+
* the text reaches the conversation.
|
|
315
|
+
*/
|
|
316
|
+
const expandSentinel = (prompt: string, cwd: string, conversation: FireConversation | undefined): { text: string; delivery?: LoopDeliveryState } => {
|
|
317
|
+
let branch: readonly unknown[] = [];
|
|
318
|
+
try {
|
|
319
|
+
branch = conversation?.getBranch() ?? [];
|
|
320
|
+
} catch {
|
|
321
|
+
// A torn-down session reads as one that was sent nothing.
|
|
322
|
+
}
|
|
323
|
+
const before = deliveryFromState(stateSinceCompaction(branch, LOOP_DELIVERY_STATE));
|
|
324
|
+
const state = { ...before };
|
|
325
|
+
const text = resolveLoopFire(state, prompt, cwd, autonomousPreamble());
|
|
326
|
+
const changed = state.autonomousPreambleDelivered !== before.autonomousPreambleDelivered || state.lastLoopFileDelivered !== before.lastLoopFileDelivered;
|
|
327
|
+
return changed ? { text, delivery: state } : { text };
|
|
328
|
+
};
|
|
329
|
+
/** Record what a delivered fire told its conversation; a torn-down one has nothing to record into. */
|
|
330
|
+
const recordDelivery = (conversation: Pick<FireConversation, "appendEntry">, delivery: LoopDeliveryState) => {
|
|
331
|
+
try {
|
|
332
|
+
recordState(conversation, LOOP_DELIVERY_STATE, delivery);
|
|
333
|
+
} catch {
|
|
334
|
+
// Same: nothing to record into.
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
/** The main session as a fire's conversation, while it is live. */
|
|
338
|
+
const mainConversation = (): FireConversation | undefined => {
|
|
339
|
+
const ctx = lastCtx;
|
|
340
|
+
return ctx ? { getBranch: () => ctx.sessionManager.getBranch(), appendEntry: (customType, data) => pi.appendEntry(customType, data) } : undefined;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The text a fired prompt delivers, as Claude Code's queue would run it: a
|
|
345
|
+
* no-prompt /loop sentinel expands (loop-fire.ts), and a slash command that
|
|
346
|
+
* names a skill runs that skill (lib/skill-body.ts); anything else is the
|
|
347
|
+
* prompt verbatim. Both resolve in the job owner's directory: a subagent's
|
|
348
|
+
* own, or the main session's worktree while it is in one.
|
|
349
|
+
*/
|
|
350
|
+
const fireText = (job: CronJob, conversation: FireConversation | undefined): { text: string; delivery?: LoopDeliveryState } => {
|
|
351
|
+
const { prompt } = job;
|
|
352
|
+
const cwd = job.cwd ?? worktreeCwd ?? sessionCwd;
|
|
353
|
+
if (isAutonomousSentinel(prompt) || isLoopFileSentinel(prompt)) return expandSentinel(prompt, cwd, conversation);
|
|
354
|
+
if (!prompt.startsWith("/")) return { text: prompt };
|
|
355
|
+
const query: SlashExpandQuery = { text: prompt, cwd };
|
|
356
|
+
pi.events.emit(SLASH_EXPAND_CHANNEL, query);
|
|
357
|
+
return { text: query.expanded ?? prompt };
|
|
358
|
+
};
|
|
359
|
+
/** Claude Code's no-op fold for the wakeup firing now (noop-fold.ts), read off the session branch. */
|
|
360
|
+
const foldDetails = (): FoldDetails => {
|
|
361
|
+
let entries: FoldEntry[];
|
|
362
|
+
try {
|
|
363
|
+
entries = (lastCtx?.sessionManager.getBranch() ?? []).map(foldEntry);
|
|
364
|
+
} catch {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
const decision = decideFold(entries);
|
|
368
|
+
return decision.kind === "fold" ? { noOpStreak: decision.priorStreak + 1, streakStartedAt: decision.since } : {};
|
|
369
|
+
};
|
|
370
|
+
const fireCron = (fire: CronFire) => {
|
|
371
|
+
const { job, final } = fire;
|
|
372
|
+
if (job.agentId !== undefined) {
|
|
373
|
+
// A subagent's job goes to that agent, or is dropped once it has ended.
|
|
374
|
+
// The agent's resident sends the text as soon as it asks for it, so the record goes with the text.
|
|
375
|
+
const text = (conversation: FireConversation | undefined) => {
|
|
376
|
+
const fired = fireText(job, conversation);
|
|
377
|
+
if (fired.delivery && conversation) recordDelivery(conversation, fired.delivery);
|
|
378
|
+
return fired.text;
|
|
379
|
+
};
|
|
380
|
+
const delivery: AgentCronFire = { agentId: job.agentId, jobId: job.id, text };
|
|
381
|
+
pi.events.emit(AGENT_CRON_FIRE_CHANNEL, delivery);
|
|
382
|
+
// The agent has ended: its job goes with it.
|
|
383
|
+
if (!delivery.delivered) cron.delete(job.id);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const fold = job.source === "wakeup" ? foldDetails() : {};
|
|
387
|
+
// A fold removes the tick's messages from the model's context: above the
|
|
388
|
+
// append point, so the next request opens an epoch (extensions/ledger)
|
|
389
|
+
// and renders the folded context whole, once.
|
|
390
|
+
if (fold.noOpStreak !== undefined) pi.appendEntry(RECORD.epoch, makeRecord("epoch", extensionVersion(), { cause: "fold" }));
|
|
391
|
+
if (job.source === "wakeup") dynamicLoop.inFlight = job.prompt;
|
|
392
|
+
// Recorded whatever way the notification goes out, so a /reload knows the fire happened.
|
|
393
|
+
pi.appendEntry(JOB_FIRED_ENTRY, { jobId: job.id, final, source: job.source });
|
|
394
|
+
// The notifier may hold the message (a coalescing window, an interrupt) and
|
|
395
|
+
// a /reload drops what it holds, so the loop's record rides the message and
|
|
396
|
+
// is written when it is delivered (the notifier's onDelivered above).
|
|
397
|
+
const { text, delivery } = fireText(job, mainConversation());
|
|
398
|
+
const details = { jobId: job.id, cron: job.cron, source: job.source, final, ...fold, ...(delivery ? { [LOOP_DELIVERY_DETAILS_KEY]: delivery } : {}) };
|
|
399
|
+
notify(job.source === "wakeup" ? "wakeup" : "cron", text, details);
|
|
400
|
+
};
|
|
401
|
+
/** Fire every due job (only while idle), then arm the one timer to the next fire. */
|
|
402
|
+
const runCron = () => {
|
|
403
|
+
if (cronTimer) clearTimeout(cronTimer);
|
|
404
|
+
cronTimer = undefined;
|
|
405
|
+
// A busy agent re-runs this at agent_settled; a dead session never does.
|
|
406
|
+
if (!alive() || agentBusy) {
|
|
407
|
+
updateWidget();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
for (const fire of cron.takeDue(Date.now())) fireCron(fire);
|
|
411
|
+
updateWidget();
|
|
412
|
+
const next = cron.nextFireAt();
|
|
413
|
+
if (next === undefined) return;
|
|
414
|
+
cronTimer = setTimeout(runCron, Math.min(Math.max(0, next - Date.now()), MAX_TIMER_MS));
|
|
415
|
+
cronTimer.unref?.();
|
|
416
|
+
};
|
|
139
417
|
|
|
140
418
|
pi.registerTool({
|
|
141
419
|
name: "monitor",
|
|
@@ -180,6 +458,32 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
180
458
|
const id = generateTaskId();
|
|
181
459
|
let status: BackgroundTask["status"] = "running";
|
|
182
460
|
let stored = "";
|
|
461
|
+
// The whole stream, spooled like a background shell's, so the batches'
|
|
462
|
+
// "task_output has the full stream" and task_output's clip header point
|
|
463
|
+
// at something true, and a one-shot result past the in-memory cap can
|
|
464
|
+
// still persist every line. Opened on the first output.
|
|
465
|
+
let log: WriteStream | undefined;
|
|
466
|
+
let spoolFailed = false;
|
|
467
|
+
let overflowed = false;
|
|
468
|
+
const record = (text: string) => {
|
|
469
|
+
if (stored.length + text.length > STORED_OUTPUT_CAP) overflowed = true;
|
|
470
|
+
stored = tail(`${stored}${text}`, STORED_OUTPUT_CAP);
|
|
471
|
+
if (!text || spoolFailed) return;
|
|
472
|
+
if (!log) {
|
|
473
|
+
const path = monitorLogPath(ctx, id);
|
|
474
|
+
if (!path) return;
|
|
475
|
+
log = createWriteStream(path, { flags: "a" });
|
|
476
|
+
log.on("error", () => {
|
|
477
|
+
// Best-effort, as bash's spool: the in-memory tail stays
|
|
478
|
+
// authoritative, and nothing names a missing or partial file as
|
|
479
|
+
// the full output any more.
|
|
480
|
+
spoolFailed = true;
|
|
481
|
+
task.logPath = undefined;
|
|
482
|
+
});
|
|
483
|
+
task.logPath = path;
|
|
484
|
+
}
|
|
485
|
+
log.write(text);
|
|
486
|
+
};
|
|
183
487
|
let eventCount = 0;
|
|
184
488
|
let pending: MonitorBatch = emptyBatch();
|
|
185
489
|
let flushTimer: NodeJS.Timeout | undefined;
|
|
@@ -188,9 +492,16 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
188
492
|
finish = resolve;
|
|
189
493
|
});
|
|
190
494
|
|
|
191
|
-
const flush = () => {
|
|
495
|
+
const flush = (force = false) => {
|
|
192
496
|
flushTimer = undefined;
|
|
193
497
|
if (batchSize(pending) === 0) return;
|
|
498
|
+
if (!force && notify.holding()) {
|
|
499
|
+
// Held after an interrupt: one bounded batch accumulates (its
|
|
500
|
+
// overflow counted) instead of one held message per flush, and
|
|
501
|
+
// goes out once the next turn starts.
|
|
502
|
+
flushTimer = setTimeout(() => flush(), MONITOR_BATCH_IDLE_MS);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
194
505
|
const batch = pending;
|
|
195
506
|
pending = emptyBatch();
|
|
196
507
|
// CC's mid-run monitor batch: no status, the lines in `<event>`.
|
|
@@ -201,48 +512,71 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
201
512
|
);
|
|
202
513
|
};
|
|
203
514
|
|
|
515
|
+
// Set by stop(): what the command prints while it is torn down (a shell
|
|
516
|
+
// running the rest of `sleep 30; echo done` before the SIGKILL grace ends)
|
|
517
|
+
// is not an event, so the count says what happened before the stop.
|
|
518
|
+
let stopRequested = false;
|
|
204
519
|
const onEvent = (line: string) => {
|
|
520
|
+
if (stopRequested) return;
|
|
205
521
|
eventCount++;
|
|
206
|
-
|
|
522
|
+
record(`${line}\n`);
|
|
207
523
|
if (oneShot) return; // collected into the tool result instead
|
|
208
524
|
pushEvent(pending, line);
|
|
209
525
|
// Bounded batches, and a wider window mid-turn so a chatty stream
|
|
210
526
|
// coalesces instead of steering one notification per second
|
|
211
527
|
// (monitor-batch.ts).
|
|
212
|
-
flushTimer ??= setTimeout(flush, agentBusy ? MONITOR_BATCH_BUSY_MS : MONITOR_BATCH_IDLE_MS);
|
|
528
|
+
flushTimer ??= setTimeout(() => flush(), agentBusy ? MONITOR_BATCH_BUSY_MS : MONITOR_BATCH_IDLE_MS);
|
|
213
529
|
};
|
|
214
530
|
|
|
215
531
|
const end = (finalStatus: BackgroundTask["status"], note?: string) => {
|
|
216
532
|
if (status !== "running") return;
|
|
217
533
|
status = finalStatus;
|
|
218
|
-
task.status = finalStatus;
|
|
219
|
-
task.finishedAt = Date.now();
|
|
220
534
|
if (flushTimer) clearTimeout(flushTimer);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
535
|
+
const complete = () => {
|
|
536
|
+
if (task.status !== "running") return;
|
|
537
|
+
// Terminal status and its notification promise readable final output.
|
|
538
|
+
task.status = finalStatus;
|
|
539
|
+
task.finishedAt = Date.now();
|
|
540
|
+
finish();
|
|
541
|
+
// After shutdown or inside a one-shot run, report through `finished` alone.
|
|
542
|
+
if (!alive() || oneShot) return;
|
|
543
|
+
flush(true);
|
|
544
|
+
updateWidget();
|
|
545
|
+
// CC's monitor end: the recent tail rides <event> under the ended summary.
|
|
546
|
+
const ccStatus = taskStatusOf(finalStatus);
|
|
547
|
+
const recent = tail(stored, MONITOR_BATCH_MAX_CHARS).trim();
|
|
548
|
+
notify(
|
|
549
|
+
"task-notification",
|
|
550
|
+
taskNotification({
|
|
551
|
+
kind: "monitor",
|
|
552
|
+
taskId: id,
|
|
553
|
+
toolUseId: toolCallId,
|
|
554
|
+
status: ccStatus,
|
|
555
|
+
summary: monitorEndedSummary(params.description, ccStatus, eventCount > 0, note),
|
|
556
|
+
result: recent || undefined,
|
|
557
|
+
}),
|
|
558
|
+
{ taskId: id, status: finalStatus },
|
|
559
|
+
);
|
|
560
|
+
};
|
|
561
|
+
if (log && !log.closed) {
|
|
562
|
+
// `close` follows a clean end and an error alike. A stuck stream
|
|
563
|
+
// must not hold a blocking tool forever or advertise a partial file.
|
|
564
|
+
const fallback = setTimeout(() => {
|
|
565
|
+
spoolFailed = true;
|
|
566
|
+
task.logPath = undefined;
|
|
567
|
+
log?.destroy();
|
|
568
|
+
complete();
|
|
569
|
+
}, 1_000);
|
|
570
|
+
log.once("close", () => {
|
|
571
|
+
clearTimeout(fallback);
|
|
572
|
+
complete();
|
|
573
|
+
});
|
|
574
|
+
log.end();
|
|
575
|
+
} else {
|
|
576
|
+
complete();
|
|
577
|
+
}
|
|
243
578
|
};
|
|
244
579
|
|
|
245
|
-
let stopRequested = false;
|
|
246
580
|
let stop: () => void;
|
|
247
581
|
if (params.command) {
|
|
248
582
|
// Own process group: stop() must end the COMMAND, not just the `$SHELL -c`
|
|
@@ -263,27 +597,32 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
263
597
|
...detachedSpawnOptions(),
|
|
264
598
|
stdio: ["ignore", "pipe", "pipe"],
|
|
265
599
|
});
|
|
266
|
-
|
|
600
|
+
rememberProcessGroup(child);
|
|
601
|
+
const lines = new MonitorLineSplitter();
|
|
602
|
+
// Streaming decoders: a UTF-8 character split across reads stays whole.
|
|
603
|
+
const stdoutText = new StringDecoder("utf8");
|
|
604
|
+
const stderrText = new StringDecoder("utf8");
|
|
267
605
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
268
|
-
|
|
269
|
-
let idx: number;
|
|
270
|
-
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
271
|
-
const line = buffer.slice(0, idx).trimEnd();
|
|
272
|
-
buffer = buffer.slice(idx + 1);
|
|
273
|
-
if (line) onEvent(line);
|
|
274
|
-
}
|
|
606
|
+
for (const line of lines.push(stdoutText.write(chunk))) onEvent(line);
|
|
275
607
|
});
|
|
276
608
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
277
|
-
|
|
609
|
+
record(stderrText.write(chunk));
|
|
278
610
|
});
|
|
279
611
|
// Exit plus a short stdio grace, not `close` (lib/process-tree.ts).
|
|
612
|
+
// The last line may have no newline; it is an event all the same.
|
|
613
|
+
const drain = () => {
|
|
614
|
+
for (const line of [...lines.push(stdoutText.end()), ...lines.end()]) onEvent(line);
|
|
615
|
+
record(stderrText.end());
|
|
616
|
+
};
|
|
280
617
|
waitForChildExit(child).then(
|
|
281
|
-
({ code }) =>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
618
|
+
({ code }) => {
|
|
619
|
+
drain();
|
|
620
|
+
end(stopRequested ? "stopped" : code === 0 ? "completed" : "failed", code !== null && code !== 0 ? `exit code ${code}` : undefined);
|
|
621
|
+
},
|
|
622
|
+
(error: Error) => {
|
|
623
|
+
drain();
|
|
624
|
+
end("failed", error.message);
|
|
625
|
+
},
|
|
287
626
|
);
|
|
288
627
|
stop = () => {
|
|
289
628
|
stopRequested = true;
|
|
@@ -322,7 +661,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
322
661
|
description: params.description,
|
|
323
662
|
status,
|
|
324
663
|
startedAt: Date.now(),
|
|
325
|
-
output: () => stored,
|
|
664
|
+
output: () => (overflowed && !task.logPath ? `${SPOOL_LOST_NOTE}${stored}` : stored || (task.status === "running" ? "" : MONITOR_EMPTY_OUTPUT)),
|
|
326
665
|
stop,
|
|
327
666
|
finished,
|
|
328
667
|
};
|
|
@@ -349,7 +688,11 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
349
688
|
} finally {
|
|
350
689
|
unhook();
|
|
351
690
|
}
|
|
352
|
-
|
|
691
|
+
// Past the in-memory cap the spool holds every line: point at it rather
|
|
692
|
+
// than persist the tail. Without one, say what was lost.
|
|
693
|
+
const spooled = overflowed && task.logPath ? persistedFilePreview(task.logPath) : undefined;
|
|
694
|
+
const kept = overflowed && !spooled ? `${SPOOL_LOST_NOTE}${stored.trim()}` : stored.trim() || "(no events)";
|
|
695
|
+
const output = spooled ?? persistIfLarge(kept, { dir: sessionResultsDir(ctx), id: `monitor-${id}` });
|
|
353
696
|
const finalStatus = task.status;
|
|
354
697
|
let how: string = finalStatus;
|
|
355
698
|
if (finalStatus === "stopped") {
|
|
@@ -407,22 +750,26 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
407
750
|
|
|
408
751
|
if (params.block !== false && task.status === "running") {
|
|
409
752
|
const timeoutMs = Math.min(params.timeout ?? 30_000, MAX_BLOCK_TIMEOUT_MS);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
resolve
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
753
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
754
|
+
let unhook = () => {};
|
|
755
|
+
try {
|
|
756
|
+
await Promise.race([
|
|
757
|
+
task.finished,
|
|
758
|
+
new Promise<void>((resolve) => {
|
|
759
|
+
// The tool awaits this deadline: keep it referenced in one-shot runs.
|
|
760
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
761
|
+
unhook = whenAborted(signal, resolve);
|
|
762
|
+
}),
|
|
763
|
+
]);
|
|
764
|
+
} finally {
|
|
765
|
+
clearTimeout(timer);
|
|
766
|
+
unhook();
|
|
767
|
+
}
|
|
421
768
|
}
|
|
422
769
|
|
|
423
770
|
updateWidget();
|
|
424
771
|
const header = formatTaskLine(task);
|
|
425
|
-
const body =
|
|
772
|
+
const body = taskOutputBody(task, ctx) || "(no output yet)";
|
|
426
773
|
// The model now holds a finished task's output: a completion notification
|
|
427
774
|
// still waiting to go out for it is redundant (lib/notifications.ts).
|
|
428
775
|
if (task.status !== "running") pi.events.emit(TASK_OUTPUT_DELIVERED_CHANNEL, { taskId: task.id } satisfies TaskOutputDelivered);
|
|
@@ -437,9 +784,12 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
437
784
|
name: "task_stop",
|
|
438
785
|
label: "Stop Task",
|
|
439
786
|
...ccToolRenderers("Stop Task"),
|
|
440
|
-
|
|
787
|
+
// Claude Code's TaskStop text without its agent-team and agent-name clauses:
|
|
788
|
+
// a task is stopped by its id here.
|
|
789
|
+
description:
|
|
790
|
+
"\n- Stops a running background task by its ID\n- Takes a task_id parameter identifying the task to stop\n- Returns a success or failure status\n- Use this tool when you need to terminate a long-running task\n",
|
|
441
791
|
parameters: Type.Object({
|
|
442
|
-
task_id: Type.String({ description: "The
|
|
792
|
+
task_id: Type.String({ description: "The ID of the background task to stop." }),
|
|
443
793
|
}),
|
|
444
794
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
445
795
|
lastCtx = ctx;
|
|
@@ -463,7 +813,7 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
463
813
|
{
|
|
464
814
|
type: "text",
|
|
465
815
|
text: wasResident
|
|
466
|
-
? `Terminated the resident agent behind ${task.id} (${task.description}); it can no longer be messaged live (
|
|
816
|
+
? `Terminated the resident agent behind ${task.id} (${task.description}); it can no longer be messaged live (SendMessage will resume it from its session file).`
|
|
467
817
|
: `Stop requested for ${task.id} (${task.description}).`,
|
|
468
818
|
},
|
|
469
819
|
],
|
|
@@ -478,61 +828,91 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
478
828
|
...ccToolRenderers<{ delaySeconds?: number; stop?: boolean }>("Schedule Wakeup", {
|
|
479
829
|
title: (a) => (a?.stop ? "stop" : a?.delaySeconds !== undefined ? `${a.delaySeconds}s` : undefined),
|
|
480
830
|
}),
|
|
481
|
-
description:
|
|
482
|
-
"Schedule when to resume work on a self-paced recurring task — the dynamic mode of the /loop command. After `delaySeconds` (clamped to [60, 3600]) the harness re-invokes you with the given prompt, verbatim, as the next turn's input. One wakeup is pending at a time — scheduling again replaces it; {stop: true} ends the loop and cancels any pending wakeup.\n\nPass the same task back via `prompt` each turn so the next firing repeats it. Set `noop: true` when nothing changed this tick (you checked and there's nothing to report); `noop: false` when something happened worth keeping. Pick `delaySeconds` from what you're actually waiting for: poll external state (a CI run, a deploy) at the rate it changes; for a quiet idle heartbeat prefer a long delay (1200s+). Do NOT schedule a short wakeup just to poll background work you started here — its completion already notifies you.",
|
|
831
|
+
description: SCHEDULE_WAKEUP_DESCRIPTION,
|
|
483
832
|
parameters: Type.Object({
|
|
484
|
-
delaySeconds: Type.Optional(Type.Number({ description:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
833
|
+
delaySeconds: Type.Optional(Type.Number({ description: SCHEDULE_WAKEUP_PARAMS.delaySeconds })),
|
|
834
|
+
reason: Type.Optional(Type.String({ description: SCHEDULE_WAKEUP_PARAMS.reason })),
|
|
835
|
+
prompt: Type.Optional(Type.String({ description: SCHEDULE_WAKEUP_PARAMS.prompt })),
|
|
836
|
+
stop: Type.Optional(Type.Boolean({ description: SCHEDULE_WAKEUP_PARAMS.stop })),
|
|
837
|
+
noop: Type.Optional(Type.Boolean({ description: SCHEDULE_WAKEUP_PARAMS.noop })),
|
|
489
838
|
}),
|
|
490
839
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
491
840
|
lastCtx = ctx;
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
if (params.stop) {
|
|
499
|
-
clearPending();
|
|
500
|
-
return { content: [{ type: "text", text: "Wakeup loop stopped; no further wakeups will fire." }], details: {} };
|
|
501
|
-
}
|
|
502
|
-
// Validate BEFORE touching the pending wakeup: clearing it up front meant
|
|
503
|
-
// a malformed reschedule silently killed a running /loop with no signal.
|
|
504
|
-
if (params.delaySeconds === undefined || !params.prompt || !params.reason) {
|
|
505
|
-
return {
|
|
506
|
-
content: [
|
|
507
|
-
{
|
|
508
|
-
type: "text",
|
|
509
|
-
text: `delaySeconds, prompt, and reason are all required unless stop is true.${wakeup ? " Your previous wakeup is still pending — this call was rejected and left it untouched." : ""}`,
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
details: {},
|
|
513
|
-
isError: true,
|
|
514
|
-
};
|
|
841
|
+
const fail = (text: string) => ({ content: [{ type: "text" as const, text }], details: {}, isError: true });
|
|
842
|
+
if (params.stop === true) {
|
|
843
|
+
const cancelled = dynamicLoop.stop();
|
|
844
|
+
runCron();
|
|
845
|
+
return { content: [{ type: "text", text: formatStopped(cancelled) }], details: { stopped: true, cancelledWakeups: cancelled } };
|
|
515
846
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
847
|
+
if (params.delaySeconds === undefined || params.reason === undefined) return fail(WAKEUP_ERRORS.delayAndReason);
|
|
848
|
+
if (params.prompt === undefined) return fail(WAKEUP_ERRORS.prompt);
|
|
849
|
+
if (params.noop === undefined) return fail(WAKEUP_ERRORS.noop);
|
|
850
|
+
const now = Date.now();
|
|
851
|
+
const scheduled = dynamicLoop.schedule(params.delaySeconds, params.prompt, now);
|
|
852
|
+
runCron();
|
|
853
|
+
if (!scheduled) return { content: [{ type: "text", text: AGED_OUT_RESULT }], details: { scheduledFor: 0 } };
|
|
854
|
+
const text = formatScheduled(scheduled, now);
|
|
855
|
+
return {
|
|
856
|
+
content: [{ type: "text", text: sessionOutlivesTurn(ctx.mode) ? text : `${text} ${formatCannotFire()}` }],
|
|
857
|
+
details: { ...scheduled, reason: params.reason, noop: params.noop },
|
|
858
|
+
};
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
pi.registerTool({
|
|
863
|
+
name: "cron_create",
|
|
864
|
+
label: "Cron Create",
|
|
865
|
+
...ccToolRenderers<{ cron?: string }>("Cron Create", { title: (a) => a?.cron }),
|
|
866
|
+
description: CRON_CREATE_DESCRIPTION,
|
|
867
|
+
parameters: CRON_CREATE_PARAMETERS,
|
|
868
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
869
|
+
lastCtx = ctx;
|
|
870
|
+
const created = cron.create({ cron: params.cron, prompt: params.prompt, recurring: params.recurring }, Date.now());
|
|
871
|
+
if (!created.ok) return { content: [{ type: "text", text: created.error }], details: {}, isError: true };
|
|
872
|
+
runCron();
|
|
873
|
+
const text = formatCreateResult(created.job);
|
|
874
|
+
return {
|
|
875
|
+
content: [{ type: "text", text: sessionOutlivesTurn(ctx.mode) ? text : `${text} ${formatCannotFire()}` }],
|
|
876
|
+
details: { jobId: created.job.id, cron: created.job.cron, recurring: created.job.recurring, nextFireAt: created.job.nextFireAt },
|
|
877
|
+
};
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
pi.registerTool({
|
|
882
|
+
name: "cron_list",
|
|
883
|
+
label: "Cron List",
|
|
884
|
+
...ccToolRenderers("Cron List"),
|
|
885
|
+
description: CRON_LIST_DESCRIPTION,
|
|
886
|
+
parameters: CRON_LIST_PARAMETERS,
|
|
887
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
888
|
+
lastCtx = ctx;
|
|
889
|
+
const jobs = cron.list();
|
|
890
|
+
return { content: [{ type: "text", text: formatJobList(jobs) }], details: { count: jobs.length } };
|
|
528
891
|
},
|
|
529
892
|
});
|
|
530
893
|
|
|
894
|
+
pi.registerTool({
|
|
895
|
+
name: "cron_delete",
|
|
896
|
+
label: "Cron Delete",
|
|
897
|
+
...ccToolRenderers<{ id?: string }>("Cron Delete", { title: (a) => a?.id }),
|
|
898
|
+
description: CRON_DELETE_DESCRIPTION,
|
|
899
|
+
parameters: CRON_DELETE_PARAMETERS,
|
|
900
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
901
|
+
lastCtx = ctx;
|
|
902
|
+
if (!cron.delete(params.id)) return { content: [{ type: "text", text: formatUnknownJob(params.id) }], details: {}, isError: true };
|
|
903
|
+
runCron();
|
|
904
|
+
return { content: [{ type: "text", text: formatDeleteResult(params.id) }], details: { jobId: params.id } };
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// schedule_wakeup stays eager, as Claude Code's ScheduleWakeup is.
|
|
531
909
|
for (const [name, keywords] of Object.entries({
|
|
532
910
|
monitor: ["monitor", "watch", "background", "tail", "events", "stream", "websocket"],
|
|
533
911
|
task_output: ["task", "background", "output", "status", "wait"],
|
|
534
912
|
task_stop: ["task", "background", "stop", "kill", "cancel"],
|
|
535
|
-
|
|
913
|
+
cron_create: ["cron", "schedule", "recurring", "remind", "timer", "every", "later"],
|
|
914
|
+
cron_list: ["cron", "schedule", "list", "jobs", "scheduled"],
|
|
915
|
+
cron_delete: ["cron", "schedule", "cancel", "delete", "stop"],
|
|
536
916
|
})) {
|
|
537
917
|
pi.events.emit(DEFER_CHANNEL, { name, keywords });
|
|
538
918
|
}
|
|
@@ -545,100 +925,45 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
545
925
|
},
|
|
546
926
|
});
|
|
547
927
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
928
|
+
// `/loop` is a bundled skill whose body depends on its arguments; build it
|
|
929
|
+
// for the skill extension (lib/skill-body.ts). Only our own SKILL.md: a
|
|
930
|
+
// project skill named "loop" keeps its file's body.
|
|
931
|
+
const loopSkillPath = join(BUNDLED_SKILLS_DIR, "loop", "SKILL.md");
|
|
932
|
+
pi.events.on(SKILL_BODY_CHANNEL, (data) => {
|
|
933
|
+
const query = data as SkillBodyQuery;
|
|
934
|
+
if (query.skill !== "loop" || resolve(query.path) !== loopSkillPath) return;
|
|
935
|
+
query.body = loopSkillPrompt(query.args, readLoopFile(query.cwd));
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
/** Arm the branch's scheduled jobs, with each wakeup's loop age and keepalive spend. */
|
|
939
|
+
const armFromBranch = (ctx: ExtensionContext) => {
|
|
940
|
+
const now = Date.now();
|
|
941
|
+
for (const job of scheduledJobsOnBranch(ctx.sessionManager.getBranch() as never)) {
|
|
942
|
+
if (!cron.restore(job, now) || job.source !== "wakeup") continue;
|
|
943
|
+
dynamicLoop.restoreChain(job.prompt, job.chainStartedAt ?? job.createdAt, job.fireAt ?? now, job.keepalive === true);
|
|
558
944
|
}
|
|
945
|
+
runCron();
|
|
559
946
|
};
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (keyword === "stop") {
|
|
569
|
-
const had = Boolean(loop || wakeup);
|
|
570
|
-
clearLoop();
|
|
571
|
-
clearWakeup();
|
|
572
|
-
if (activatedWakeupTool) {
|
|
573
|
-
const active = pi.getActiveTools();
|
|
574
|
-
if (active.includes("schedule_wakeup")) pi.setActiveTools(active.filter((t) => t !== "schedule_wakeup"));
|
|
575
|
-
activatedWakeupTool = false;
|
|
576
|
-
}
|
|
577
|
-
ctx.ui.notify(had ? "Loop stopped; no further iterations will fire." : "No loop was running.", "info");
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (raw === "" || keyword === "status") {
|
|
581
|
-
const parts: string[] = [];
|
|
582
|
-
if (loop) parts.push(`fixed interval every ${loop.intervalSeconds}s — ${loop.task}`);
|
|
583
|
-
if (wakeup) parts.push(`self-paced wakeup pending — ${wakeup.reason}`);
|
|
584
|
-
ctx.ui.notify(
|
|
585
|
-
parts.length
|
|
586
|
-
? `Running:\n- ${parts.join("\n- ")}\nStop with /loop stop.`
|
|
587
|
-
: "No loop running. Start with `/loop [interval] <task>`, e.g. `/loop 5m check the build`.",
|
|
588
|
-
"info",
|
|
589
|
-
);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const parsed = parseLoopArgs(raw);
|
|
594
|
-
if (!parsed.task) {
|
|
595
|
-
ctx.ui.notify("Give a task to loop, e.g. `/loop 10m check for new PRs` or `/loop watch the deploy`.", "info");
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// One loop at a time — replace any existing interval loop or pending wakeup.
|
|
600
|
-
clearLoop();
|
|
601
|
-
clearWakeup();
|
|
602
|
-
|
|
603
|
-
if (parsed.intervalSeconds !== undefined) {
|
|
604
|
-
// Fixed interval, harness-driven: re-arm automatically and fire the first tick now.
|
|
605
|
-
const seconds = clampDelaySeconds(parsed.intervalSeconds);
|
|
606
|
-
const adjusted =
|
|
607
|
-
seconds !== parsed.intervalSeconds
|
|
608
|
-
? ` (adjusted from ${parsed.intervalSeconds}s; allowed range ${MIN_DELAY_SECONDS}-${MAX_DELAY_SECONDS}s)`
|
|
609
|
-
: "";
|
|
610
|
-
// Skip (don't queue) a tick while the previous tick's turn is still
|
|
611
|
-
// running, so a slow task can't build a backlog of stale iterations.
|
|
612
|
-
const tick = () => {
|
|
613
|
-
if (agentBusy) return;
|
|
614
|
-
notify("loop", buildLoopMessage(parsed.task), { intervalSeconds: seconds });
|
|
615
|
-
};
|
|
616
|
-
const timer = setInterval(tick, seconds * 1000);
|
|
617
|
-
timer.unref?.();
|
|
618
|
-
loop = { timer, intervalSeconds: seconds, task: parsed.task };
|
|
619
|
-
ctx.ui.notify(`Looping every ${seconds}s${adjusted}: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
620
|
-
tick(); // fire the first iteration now
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Dynamic (self-paced): the model drives schedule_wakeup, so make sure it is
|
|
625
|
-
// active — remembering we did, so /loop stop can restore the lean surface.
|
|
626
|
-
const active = pi.getActiveTools();
|
|
627
|
-
if (!active.includes("schedule_wakeup")) {
|
|
628
|
-
pi.setActiveTools([...active, "schedule_wakeup"]);
|
|
629
|
-
activatedWakeupTool = true;
|
|
630
|
-
}
|
|
631
|
-
ctx.ui.notify(`Self-paced loop started: ${parsed.task}. Stop with /loop stop.`, "info");
|
|
632
|
-
notify("loop-start", buildDynamicLoopPrompt(parsed.task), {});
|
|
633
|
-
},
|
|
947
|
+
// The jobs follow the branch: a /tree switch ends the abandoned branch's and
|
|
948
|
+
// arms the selected one's (a subagent's own jobs stay with the agent).
|
|
949
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
950
|
+
lastCtx = ctx;
|
|
951
|
+
cron.deleteWhere((job) => job.agentId === undefined);
|
|
952
|
+
dynamicLoop.clear();
|
|
953
|
+
armFromBranch(ctx);
|
|
634
954
|
});
|
|
635
|
-
|
|
636
|
-
pi.on("session_start", (_event, ctx) => {
|
|
955
|
+
pi.on("session_start", (event, ctx) => {
|
|
637
956
|
lastCtx = ctx;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
957
|
+
sessionCwd = ctx.cwd;
|
|
958
|
+
// A /reload keeps the process and the conversation, so the session's
|
|
959
|
+
// scheduled jobs come back from the branch (design.md section 13); a new
|
|
960
|
+
// process ends them, as Claude Code's session-only jobs end.
|
|
961
|
+
if (event.reason === "reload") armFromBranch(ctx);
|
|
962
|
+
const pending = shutdownNotice();
|
|
963
|
+
if (pending.text) {
|
|
964
|
+
const notice = pending.text;
|
|
965
|
+
pending.text = undefined;
|
|
966
|
+
if (ctx.hasUI) queueNotice(pi.events, ctx, "info", notice);
|
|
642
967
|
}
|
|
643
968
|
});
|
|
644
969
|
pi.on("agent_start", () => {
|
|
@@ -646,22 +971,63 @@ export default function backgroundExtension(pi: ExtensionAPI) {
|
|
|
646
971
|
});
|
|
647
972
|
pi.on("agent_settled", () => {
|
|
648
973
|
agentBusy = false;
|
|
974
|
+
// Claude Code's keepalive: a wakeup's turn that scheduled no next one gets
|
|
975
|
+
// one fallback, recorded so a /reload re-arms it (no tool result does).
|
|
976
|
+
if (dynamicLoop.settle(Date.now()) === "armed") {
|
|
977
|
+
const armed = cron.list().find((job) => job.source === "wakeup");
|
|
978
|
+
if (armed) pi.appendEntry(WAKEUP_ARMED_ENTRY, { id: armed.id, prompt: armed.prompt, fireAt: armed.nextFireAt, startedAt: dynamicLoop.chainStart(armed.prompt) });
|
|
979
|
+
}
|
|
980
|
+
runCron();
|
|
981
|
+
});
|
|
982
|
+
// Folded no-op ticks leave the model's context (noop-fold.ts); the note stands in for them.
|
|
983
|
+
pi.on("context", (event) => {
|
|
984
|
+
const messages = applyNoopFolds(event.messages as Array<AgentMessage & { role: string }>, (text, timestamp) => ({ role: "user", content: [{ type: "text", text }], timestamp }) as AgentMessage & { role: string });
|
|
985
|
+
return messages ? { messages } : undefined;
|
|
649
986
|
});
|
|
987
|
+
// A switch stops everything below, so ask first. pi can cancel a switch but
|
|
988
|
+
// not a quit; the widget line covers quitting (switch-guard.ts).
|
|
989
|
+
const confirmSwitch = async (ctx: ExtensionContext, action: string): Promise<{ cancel: true } | undefined> => {
|
|
990
|
+
if (!ctx.hasUI) return undefined;
|
|
991
|
+
const title = switchWarning(
|
|
992
|
+
registry.running().map((t) => ({ id: t.id, label: t.description })),
|
|
993
|
+
cron.list().map((j) => ({ id: j.id, label: describeCadence(j.cron) })),
|
|
994
|
+
action,
|
|
995
|
+
);
|
|
996
|
+
if (!title) return undefined;
|
|
997
|
+
const choice = await ctx.ui.select(title, [SWITCH_CANCEL, SWITCH_STOP]);
|
|
998
|
+
return choice === SWITCH_STOP ? undefined : { cancel: true };
|
|
999
|
+
};
|
|
1000
|
+
pi.on("session_before_switch", (event, ctx) =>
|
|
1001
|
+
confirmSwitch(ctx, event.reason === "resume" ? "Resuming another session" : "Starting a new session"),
|
|
1002
|
+
);
|
|
1003
|
+
pi.on("session_before_fork", (_event, ctx) => confirmSwitch(ctx, "Forking the session"));
|
|
650
1004
|
|
|
651
1005
|
pi.on("session_shutdown", (event) => {
|
|
652
1006
|
// Fires on /clear, /new, /resume and /reload too (findings §8). Everything
|
|
653
1007
|
// running dies with this instance — the replacement instance has no handle
|
|
654
|
-
// on it — and the replacement is told (see
|
|
1008
|
+
// on it — and the replacement is told (see shutdownNotice); a quit
|
|
655
1009
|
// needs no note. A reload keeps the conversation, so its note says so.
|
|
656
1010
|
const running = registry.running();
|
|
657
|
-
|
|
658
|
-
|
|
1011
|
+
// A reload re-arms the jobs from the branch: only the tasks stop.
|
|
1012
|
+
const jobs = event.reason === "reload" ? [] : cron.list();
|
|
1013
|
+
if ((running.length > 0 || jobs.length > 0) && event.reason !== "quit") {
|
|
659
1014
|
const when = event.reason === "reload" ? "on reload" : "with the previous session";
|
|
660
|
-
|
|
1015
|
+
const notices: string[] = [];
|
|
1016
|
+
if (running.length > 0) {
|
|
1017
|
+
const names = running.map((t) => `${t.id} (${t.description})`).join(", ");
|
|
1018
|
+
notices.push(`Stopped ${running.length} background task${running.length === 1 ? "" : "s"} ${when}: ${names}.`);
|
|
1019
|
+
}
|
|
1020
|
+
if (jobs.length > 0) {
|
|
1021
|
+
const names = jobs.map((j) => `${j.id} (${describeCadence(j.cron)})`).join(", ");
|
|
1022
|
+
notices.push(`Cancelled ${jobs.length} scheduled job${jobs.length === 1 ? "" : "s"} ${when}: ${names}.`);
|
|
1023
|
+
}
|
|
1024
|
+
shutdownNotice().text = notices.join(" ");
|
|
661
1025
|
}
|
|
662
1026
|
registry.stopAll();
|
|
663
|
-
|
|
664
|
-
|
|
1027
|
+
cron.clear();
|
|
1028
|
+
dynamicLoop.clear();
|
|
1029
|
+
if (cronTimer) clearTimeout(cronTimer);
|
|
1030
|
+
cronTimer = undefined;
|
|
665
1031
|
lastCtx = undefined;
|
|
666
1032
|
});
|
|
667
1033
|
}
|