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
|
@@ -7,20 +7,43 @@
|
|
|
7
7
|
* cannot overwrite someone else's change.
|
|
8
8
|
* - Files that change out of band are reported in a `<system-reminder>` with the
|
|
9
9
|
* new content around the change, line-numbered.
|
|
10
|
+
* - pi's read, write and edit results read as Claude Code's (`results.ts`).
|
|
11
|
+
* - After a compaction, the files read or written most recently come back as
|
|
12
|
+
* Claude Code brings them back: the contents of a small one, a note to read
|
|
13
|
+
* a large one again (`restore.ts`), and the read state is cleared.
|
|
10
14
|
*
|
|
11
15
|
* Tracking is by content, not by tool call, which is what makes it catch writes
|
|
12
16
|
* that went through bash and never touched an intercepted tool.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
19
|
+
import { CHILD_WROTE_CHANNEL, type ChildWrote } from "../lib/child-writes.ts";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { createReadToolDefinition, type ExtensionAPI, type ExtensionContext, type SessionCompactEvent } from "@earendil-works/pi-coding-agent";
|
|
18
25
|
import { pathArgument } from "../auto-mode/paths.ts";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
26
|
+
import { bashParserReady } from "../lib/bash-parser.ts";
|
|
27
|
+
import { discoverContextFiles, instructionRule } from "../lib/claude-context.ts";
|
|
28
|
+
import { readExternalIncludesApproval } from "../lib/claude-external-includes.ts";
|
|
29
|
+
import { inContextEntries, latestCompaction } from "../lib/compaction-boundary.ts";
|
|
30
|
+
import { autoMemoryEnabled } from "../lib/auto-memory.ts";
|
|
31
|
+
import { projectMemoryDir } from "../lib/memory.ts";
|
|
32
|
+
import { claudeConfigDir, comparablePath, isPathAtOrUnder, tryRealpath } from "../lib/paths.ts";
|
|
33
|
+
import { estimateTextTokens } from "../lib/pi-ai-estimate.ts";
|
|
34
|
+
import { planFileOnBranch } from "../lib/plan-mode-channels.ts";
|
|
35
|
+
import { REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
|
|
36
|
+
import { resolveToolPath } from "../lib/tool-path.ts";
|
|
37
|
+
import { parseRules, ruleMatches } from "../permissions/matcher.ts";
|
|
38
|
+
import { loadPermissionSettings } from "../permissions/settings.ts";
|
|
39
|
+
import { type FileObservation, filesSeenOnBranch, keptReadPaths, keptShellReadPaths, shellCallsOnBranch } from "./replay.ts";
|
|
40
|
+
import { type RestoredRead, restoreBlocks, restoreCandidates } from "./restore.ts";
|
|
41
|
+
import { fileToolResultContent } from "./results.ts";
|
|
42
|
+
import { shellReadPaths, shownInFull } from "./shell-reads.ts";
|
|
43
|
+
import { sessionWorkCwd, WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
21
44
|
import {
|
|
22
45
|
describeChanges,
|
|
23
|
-
EXTERNAL_CHANGE_REMINDER,
|
|
46
|
+
CHILD_CHANGE_REMINDER, EXTERNAL_CHANGE_REMINDER,
|
|
24
47
|
type FileStamp,
|
|
25
48
|
FileTracker,
|
|
26
49
|
STALE_REASON,
|
|
@@ -46,6 +69,15 @@ function readIfPresent(path: string): string | undefined {
|
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
/** Whether `path` is a regular file (symlinks followed): never a device, FIFO or directory. */
|
|
73
|
+
function isRegularFile(path: string): boolean {
|
|
74
|
+
try {
|
|
75
|
+
return statSync(path).isFile();
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
/** The file's disk stamp, or undefined when it is gone (or not a plain file we could stat). */
|
|
50
82
|
function statIfPresent(path: string): FileStamp | undefined {
|
|
51
83
|
try {
|
|
@@ -56,12 +88,95 @@ function statIfPresent(path: string): FileStamp | undefined {
|
|
|
56
88
|
}
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
function commandOf(input: unknown): string {
|
|
92
|
+
const command = (input as { command?: unknown } | undefined)?.command;
|
|
93
|
+
return typeof command === "string" ? command : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function textOf(content: unknown): string {
|
|
97
|
+
if (!Array.isArray(content)) return "";
|
|
98
|
+
return content.map((block) => (block as { type?: string; text?: string })?.type === "text" ? ((block as { text?: string }).text ?? "") : "").join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A file's identity and version, from metadata only: `ctimeMs` changes on any
|
|
103
|
+
* write, rename or metadata change, and cannot be set back by the writer.
|
|
104
|
+
*/
|
|
105
|
+
function versionOf(path: string): string | undefined {
|
|
106
|
+
try {
|
|
107
|
+
const stat = statSync(path);
|
|
108
|
+
return stat.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : undefined;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The files a shell command may print, with each one's version as the command
|
|
116
|
+
* starts (`shell-reads.ts` paths, resolved as the shell resolves them, from
|
|
117
|
+
* the directory a literal `cd` earlier in the command moved to). Taken before
|
|
118
|
+
* the command runs, so a file the command itself rewrites (`cat x; cp y x`)
|
|
119
|
+
* is not certified with its new content (found by a GPT-6 Sol review,
|
|
120
|
+
* 2026-10-04). Metadata only: this runs before the permission gate decides
|
|
121
|
+
* the call, so a file the user then denies is never opened.
|
|
122
|
+
*/
|
|
123
|
+
function shellReadSnapshot(command: string, cwd: string): Map<string, string> {
|
|
124
|
+
const snapshot = new Map<string, string>();
|
|
125
|
+
if (!command) return snapshot;
|
|
126
|
+
for (const path of shellReadPaths(command, cwd, homedir(), (dir) => readdirSync(dir))) {
|
|
127
|
+
// Regular files only: a device or FIFO (`head -c 1 /dev/zero`) reports size 0
|
|
128
|
+
// and a read of it never ends (found by a GPT-6 Astra review).
|
|
129
|
+
if (!isRegularFile(path)) continue;
|
|
130
|
+
const stamp = statIfPresent(path);
|
|
131
|
+
if (!stamp || stamp.size > MAX_SHELL_READ_BYTES) continue;
|
|
132
|
+
const version = versionOf(path);
|
|
133
|
+
if (version !== undefined) snapshot.set(path, version);
|
|
134
|
+
}
|
|
135
|
+
return snapshot;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Count as read each file the command printed in full and did not change:
|
|
140
|
+
* its version before and after the command is the same, and the output holds
|
|
141
|
+
* all of it. Observed, not touched: the post-compaction restore follows the
|
|
142
|
+
* file tools, as Claude Code's does. Returns the files it observed.
|
|
143
|
+
*/
|
|
144
|
+
function observeShellReads(tracker: FileTracker, before: Map<string, string>, output: string): string[] {
|
|
145
|
+
const observed: string[] = [];
|
|
146
|
+
if (!output) return observed;
|
|
147
|
+
for (const [path, version] of before) {
|
|
148
|
+
const stamp = statIfPresent(path);
|
|
149
|
+
if (!stamp || !isRegularFile(path) || versionOf(path) !== version) continue;
|
|
150
|
+
const content = readIfPresent(path);
|
|
151
|
+
if (content === undefined || versionOf(path) !== version) continue;
|
|
152
|
+
if (!shownInFull(output, content)) continue;
|
|
153
|
+
tracker.observe(path, content, Date.now(), stamp);
|
|
154
|
+
observed.push(path);
|
|
155
|
+
}
|
|
156
|
+
return observed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Above this a shell read's output is persisted, not shown, so the file cannot have been seen whole. */
|
|
160
|
+
const MAX_SHELL_READ_BYTES = 256 * 1024;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Observe a file's current content together with the stamp it was read at.
|
|
164
|
+
* Returns that stamp, and what was seen for the result that read it
|
|
165
|
+
* (undefined when the file is gone).
|
|
166
|
+
*/
|
|
167
|
+
function observeFromDisk(tracker: FileTracker, path: string): { stamp?: FileStamp; seen?: FileObservation } {
|
|
61
168
|
const stamp = statIfPresent(path);
|
|
62
169
|
const current = readIfPresent(path);
|
|
63
|
-
if (current === undefined)
|
|
64
|
-
|
|
170
|
+
if (current === undefined) {
|
|
171
|
+
tracker.forget(path);
|
|
172
|
+
return stamp ? { stamp } : {};
|
|
173
|
+
}
|
|
174
|
+
tracker.observe(path, current, Date.now(), stamp);
|
|
175
|
+
return stamp ? { stamp, seen: { path, size: stamp.size, mtimeMs: stamp.mtimeMs, sha256: sha256(current) } } : {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sha256(text: string): string {
|
|
179
|
+
return createHash("sha256").update(text).digest("hex");
|
|
65
180
|
}
|
|
66
181
|
|
|
67
182
|
function pathOf(input: unknown, cwd: string): string | undefined {
|
|
@@ -71,40 +186,191 @@ function pathOf(input: unknown, cwd: string): string | undefined {
|
|
|
71
186
|
// read-before-write guard (code-review).
|
|
72
187
|
const raw = pathArgument(input as Record<string, unknown> | undefined);
|
|
73
188
|
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
74
|
-
|
|
189
|
+
// pi's own resolution (`~`, `@`, `file://`, …): the guard must check the
|
|
190
|
+
// file the tool will actually touch (lib/tool-path.ts).
|
|
191
|
+
return resolveToolPath(raw, cwd);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The files the context still shows after a compaction, as comparable paths:
|
|
196
|
+
* the context stack (the CLAUDE.md family as claude-context discovers it, what
|
|
197
|
+
* they `@`-import, and the auto-memory index), the plan file, the files a
|
|
198
|
+
* `read` in the kept turns returned (`kept`) and the files a kept bash call
|
|
199
|
+
* showed in full (`keptShell`), both absolute as recorded. Claude Code's
|
|
200
|
+
* restore skips them; they stay read.
|
|
201
|
+
*/
|
|
202
|
+
function inContextFiles(ctx: ExtensionContext, branch: readonly { type: string; customType?: string; data?: unknown }[], kept: string[], keptShell: string[]): Set<string> {
|
|
203
|
+
const home = homedir();
|
|
204
|
+
// Use the context loader's import confinement and approval: a literal
|
|
205
|
+
// unapproved @import is not evidence that its file is still in context.
|
|
206
|
+
const stack = discoverContextFiles({ cwd: ctx.cwd, home, homeClaudeDir: claudeConfigDir(), rule: instructionRule(home),
|
|
207
|
+
includeExternal: readExternalIncludesApproval(ctx.cwd, home).approved,
|
|
208
|
+
}).flatMap((file) => [file.path, ...(file.imported ?? [])]);
|
|
209
|
+
return new Set(
|
|
210
|
+
[...stack, autoMemoryEnabled(ctx.cwd) ? join(projectMemoryDir(ctx.cwd, home), "MEMORY.md") : undefined, planFileOnBranch(branch), ...kept, ...keptShell]
|
|
211
|
+
.filter((path): path is string => typeof path === "string")
|
|
212
|
+
.map(comparablePath),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Whether a file's contents may come back without the permission gate that a
|
|
218
|
+
* `read` call passes: only a file that resolves (symlinks followed) inside the
|
|
219
|
+
* working directory and that no read deny or ask rule covers now. Any other
|
|
220
|
+
* file, one read outside the project after a question, say, is not restored;
|
|
221
|
+
* the model reads it again through the gate if it needs it. Nothing passes
|
|
222
|
+
* when the settings cannot be read (the gate reports them).
|
|
223
|
+
*/
|
|
224
|
+
function restoreAllowed(ctx: ExtensionContext): (path: string) => boolean {
|
|
225
|
+
try {
|
|
226
|
+
const settings = loadPermissionSettings(ctx.cwd, homedir());
|
|
227
|
+
const rules = parseRules([...settings.deny, ...settings.ask]);
|
|
228
|
+
const root = tryRealpath(ctx.cwd) ?? ctx.cwd;
|
|
229
|
+
return (path) => {
|
|
230
|
+
const real = tryRealpath(path);
|
|
231
|
+
if (!real || !isPathAtOrUnder(real, root)) return false;
|
|
232
|
+
return !rules.some((rule) => ruleMatches(rule, "read", path, ctx.cwd) || ruleMatches(rule, "read", real, ctx.cwd));
|
|
233
|
+
};
|
|
234
|
+
} catch {
|
|
235
|
+
return () => false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A file read again through pi's own read tool, as the model would see it; undefined when gone, unreadable or an image. */
|
|
240
|
+
async function readAgain(read: ReturnType<typeof createReadToolDefinition>, path: string, ctx: ExtensionContext): Promise<string | undefined> {
|
|
241
|
+
try {
|
|
242
|
+
const result = await read.execute("restore", { path }, undefined, undefined, ctx as never);
|
|
243
|
+
if (result.content.some((block) => block.type !== "text")) return undefined;
|
|
244
|
+
return result.content.map((block) => (block as { text: string }).text).join("\n");
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
75
248
|
}
|
|
76
249
|
|
|
77
250
|
export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
78
251
|
let tracker = new FileTracker();
|
|
252
|
+
/**
|
|
253
|
+
* The files each bash call showed in full, by call id, decided where the
|
|
254
|
+
* call ran: a compaction keeps the reads of the calls it keeps from this,
|
|
255
|
+
* since re-parsing a command later resolves it from today's directory.
|
|
256
|
+
*/
|
|
257
|
+
const shellReadsByCall = new Map<string, string[]>();
|
|
258
|
+
const recordShellReads = (callId: string, paths: string[]) => {
|
|
259
|
+
if (paths.length > 0) shellReadsByCall.set(callId, paths);
|
|
260
|
+
};
|
|
261
|
+
/** The entered worktree, where the shell runs while one is active (lib/worktree-channel.ts). */
|
|
262
|
+
let entered: WorktreeLocation | undefined;
|
|
263
|
+
pi.events.on(WORKTREE_CHANNEL, (data) => {
|
|
264
|
+
const location = data as WorktreeLocation | null | undefined;
|
|
265
|
+
entered = location?.path ? location : undefined;
|
|
266
|
+
});
|
|
79
267
|
let sessionId: string | undefined;
|
|
268
|
+
/**
|
|
269
|
+
* Every file a read or write touched on this branch, with its modification
|
|
270
|
+
* time as stat'ed then (as the result recorded it, for one replayed after a
|
|
271
|
+
* resume): the post-compaction restore's order.
|
|
272
|
+
*/
|
|
273
|
+
let touched = new Map<string, number>();
|
|
274
|
+
/**
|
|
275
|
+
* Files this session's own child agents changed since the parent last saw
|
|
276
|
+
* them (lib/child-writes.ts). Every way the parent sees a file clears its
|
|
277
|
+
* entry: a file tool, a shell read, a rebuild from disk, the change scan.
|
|
278
|
+
*/
|
|
279
|
+
const childWrites = new Map<string, string | undefined>();
|
|
280
|
+
pi.events.on(CHILD_WROTE_CHANNEL, (data) => {
|
|
281
|
+
const wrote = data as ChildWrote | undefined;
|
|
282
|
+
if (wrote?.path) childWrites.set(wrote.path, wrote.agent);
|
|
283
|
+
});
|
|
284
|
+
/** Observe a file the model read or wrote, and record it as touched at its current modification time. */
|
|
285
|
+
const touch = (path: string) => {
|
|
286
|
+
childWrites.delete(path);
|
|
287
|
+
const { stamp, seen } = observeFromDisk(tracker, path);
|
|
288
|
+
if (stamp) touched.set(path, stamp.mtimeMs);
|
|
289
|
+
return seen;
|
|
290
|
+
};
|
|
80
291
|
|
|
81
|
-
// A resumed session (or a session_tree switch) carries
|
|
82
|
-
// earlier in the transcript; rebuild the tracker from
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
292
|
+
// A resumed session (or a /reload, a fork, a session_tree switch) carries
|
|
293
|
+
// reads the model did earlier in the transcript; rebuild the tracker from
|
|
294
|
+
// the part of the branch still in context (since the latest compaction's
|
|
295
|
+
// kept tail) so later edits are not refused as "never read" (replay.ts).
|
|
296
|
+
// Everything comes from the branch, as the results and pending entries
|
|
297
|
+
// recorded it, never from this instance: pi runs the factory again on each
|
|
298
|
+
// of those. A new session id starts from an empty tracker.
|
|
299
|
+
const reconstruct = (ctx: ExtensionContext, fresh = false) => {
|
|
86
300
|
const id = ctx.sessionManager.getSessionId?.() ?? undefined;
|
|
87
|
-
if (id !== sessionId) {
|
|
301
|
+
if (fresh || id !== sessionId) {
|
|
88
302
|
sessionId = id;
|
|
89
303
|
tracker = new FileTracker();
|
|
90
304
|
}
|
|
305
|
+
// Tracked files are re-read from disk below: what a child wrote is now the baseline.
|
|
306
|
+
childWrites.clear();
|
|
91
307
|
let entries: unknown[] = [];
|
|
92
308
|
try {
|
|
93
|
-
entries = ctx.sessionManager.getBranch() as unknown[];
|
|
309
|
+
entries = inContextEntries(ctx.sessionManager.getBranch() as unknown[]);
|
|
94
310
|
} catch {
|
|
95
311
|
return;
|
|
96
312
|
}
|
|
97
|
-
|
|
98
|
-
|
|
313
|
+
// What each result or restore saw, recorded at the time and by the path
|
|
314
|
+
// the tool resolved: a file that changed since is stale, never taken as
|
|
315
|
+
// read in its current form (round-2 review H1), and a gone one untracked.
|
|
316
|
+
const files = filesSeenOnBranch(entries);
|
|
317
|
+
const changed: Pick<FileObservation, "path" | "sha256">[] = [];
|
|
318
|
+
for (const [path, { seen, notified }] of files) {
|
|
319
|
+
const stamp = statIfPresent(path);
|
|
320
|
+
const current = readIfPresent(path);
|
|
321
|
+
if (current === undefined) continue;
|
|
322
|
+
const hash = sha256(current);
|
|
323
|
+
if (hash === seen.sha256) {
|
|
324
|
+
tracker.observe(path, current, Date.now(), stamp);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
tracker.observeChanged(path, Date.now());
|
|
328
|
+
// A version a notice already named is not announced again (round-3 review L9).
|
|
329
|
+
if (!notified.has(hash)) changed.push({ path, sha256: hash });
|
|
330
|
+
}
|
|
331
|
+
if (changed.length > 0) {
|
|
332
|
+
// Unkeyed: a key would let this notice replace an undelivered earlier one
|
|
333
|
+
// whose versions the branch already counts as told.
|
|
334
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
335
|
+
text: `These files changed on disk since you last read them in this conversation: ${changed.map((file) => file.path).join(", ")}. Read them again before editing them.`,
|
|
336
|
+
notified: changed,
|
|
337
|
+
} satisfies ReminderPayload);
|
|
338
|
+
}
|
|
339
|
+
// Shell reads need the bash grammar, which may still be loading this early.
|
|
340
|
+
const target = tracker;
|
|
341
|
+
shellReadsByCall.clear();
|
|
342
|
+
const shellCalls = [...shellCallsOnBranch(entries)];
|
|
343
|
+
if (shellCalls.length > 0) {
|
|
344
|
+
void bashParserReady().then(() => {
|
|
345
|
+
if (target !== tracker) return;
|
|
346
|
+
// A replayed result has no before-state: the files as they are now stand in for it.
|
|
347
|
+
for (const call of shellCalls) recordShellReads(call.id, observeShellReads(target, shellReadSnapshot(call.command, sessionWorkCwd(entered, ctx.cwd)), call.output));
|
|
348
|
+
});
|
|
99
349
|
}
|
|
350
|
+
touched = new Map([...files].map(([path, { seen }]) => [path, seen.mtimeMs]));
|
|
100
351
|
};
|
|
101
352
|
pi.on("session_start", (_event, ctx) => reconstruct(ctx));
|
|
102
|
-
|
|
353
|
+
// A branch switch keeps the session id, but the reads of the branch left
|
|
354
|
+
// behind are not in context any more: start from an empty tracker, as for a
|
|
355
|
+
// new session (found by an adversarial GPT-6 Astra run, 2026-10-04).
|
|
356
|
+
pi.on("session_tree", (_event, ctx) => reconstruct(ctx, true));
|
|
357
|
+
|
|
358
|
+
/** Whether a write's file existed when the call was made, by call id: its result says "created" or "updated". */
|
|
359
|
+
const writeTargetExisted = new Map<string, boolean>();
|
|
103
360
|
|
|
104
|
-
|
|
361
|
+
/** Each running bash call's candidate files as they were when it started (`shellReadSnapshot`). */
|
|
362
|
+
const shellSnapshots = new Map<string, Map<string, string>>();
|
|
363
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
364
|
+
if (event.toolName === "bash") {
|
|
365
|
+
await bashParserReady();
|
|
366
|
+
// Relative paths in the command resolve where the shell runs: the entered worktree, if any.
|
|
367
|
+
shellSnapshots.set(event.toolCallId, shellReadSnapshot(commandOf(event.input), sessionWorkCwd(entered, ctx.cwd)));
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
105
370
|
if (!GUARDED_TOOLS.has(event.toolName)) return undefined;
|
|
106
371
|
const path = pathOf(event.input, ctx.cwd);
|
|
107
372
|
if (!path) return undefined;
|
|
373
|
+
if (event.toolName === "write") writeTargetExisted.set(event.toolCallId, existsSync(path));
|
|
108
374
|
|
|
109
375
|
const current = readIfPresent(path);
|
|
110
376
|
const status = tracker.status(path, current);
|
|
@@ -115,8 +381,19 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
115
381
|
return { block: true, reason: STALE_REASON(path) };
|
|
116
382
|
});
|
|
117
383
|
|
|
118
|
-
pi.on("tool_result", (event, ctx) => {
|
|
119
|
-
|
|
384
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
385
|
+
const existedBefore = writeTargetExisted.get(event.toolCallId);
|
|
386
|
+
writeTargetExisted.delete(event.toolCallId);
|
|
387
|
+
if (event.toolName === "bash") {
|
|
388
|
+
const before = shellSnapshots.get(event.toolCallId);
|
|
389
|
+
shellSnapshots.delete(event.toolCallId);
|
|
390
|
+
if (before) {
|
|
391
|
+
const observed = observeShellReads(tracker, before, textOf(event.content));
|
|
392
|
+
recordShellReads(event.toolCallId, observed);
|
|
393
|
+
for (const path of observed) childWrites.delete(path);
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
120
397
|
if (!READ_TOOLS.has(event.toolName) && !GUARDED_TOOLS.has(event.toolName)) return undefined;
|
|
121
398
|
|
|
122
399
|
const path = pathOf(event.input, ctx.cwd);
|
|
@@ -124,8 +401,96 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
124
401
|
|
|
125
402
|
// After a read we know the file; after our own write we know it again, so a
|
|
126
403
|
// successful edit does not make the file look stale to the next edit.
|
|
127
|
-
|
|
128
|
-
|
|
404
|
+
// What the model saw, on the result: a resume compares it with the disk.
|
|
405
|
+
const observation = event.isError ? undefined : touch(path);
|
|
406
|
+
const content = fileToolResultContent({
|
|
407
|
+
toolName: event.toolName,
|
|
408
|
+
isError: event.isError,
|
|
409
|
+
content: event.content,
|
|
410
|
+
path,
|
|
411
|
+
cwd: ctx.cwd,
|
|
412
|
+
existedBefore,
|
|
413
|
+
isEmptyFile: () => statIfPresent(path)?.size === 0,
|
|
414
|
+
});
|
|
415
|
+
if (!content && !observation) return undefined;
|
|
416
|
+
return {
|
|
417
|
+
...(content ? { content } : {}),
|
|
418
|
+
...(observation ? { details: { ...((event.details as Record<string, unknown> | undefined) ?? {}), oneCodeFile: observation } } : {}),
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Claude Code's post-compaction file restore (`restore.ts`). The
|
|
424
|
+
* blocks open the next user message (`user-prepend`); a request that opens
|
|
425
|
+
* with none (the rest of a run after a mid-turn compaction, an overflow
|
|
426
|
+
* retry) carries them on its tail (`orTail`), so the model sees a restored
|
|
427
|
+
* file before it can edit it. A later compaction drops an undelivered one
|
|
428
|
+
* (`untilCompaction`): it restores for the epoch the one before it opened.
|
|
429
|
+
*
|
|
430
|
+
* Like Claude Code, the compaction also clears what the session has read:
|
|
431
|
+
* the summary no longer holds those files, so an edit needs a fresh read.
|
|
432
|
+
* A file the context still shows stays read (a kept turn's read or whole-file
|
|
433
|
+
* shell read, the context stack, the plan file), and a restored file counts
|
|
434
|
+
* as read and as touched for the next compaction's restore, here and after
|
|
435
|
+
* a reload or a resume (its block records what it showed); a note never
|
|
436
|
+
* marks its file read. Contents come back only for a file the gate would
|
|
437
|
+
* let through without a question (`restoreAllowed`).
|
|
438
|
+
*/
|
|
439
|
+
pi.on("session_compact", async (event: SessionCompactEvent, ctx) => {
|
|
440
|
+
const before = touched;
|
|
441
|
+
try {
|
|
442
|
+
const branch = ctx.sessionManager.getBranch();
|
|
443
|
+
const compaction = latestCompaction(branch);
|
|
444
|
+
const keptTurns = compaction ? branch.slice(compaction.keptStart, compaction.index) : [];
|
|
445
|
+
// The kept shell reads: what each kept bash call certified as it ran.
|
|
446
|
+
const keptShell = keptShellReadPaths(keptTurns, (id) => shellReadsByCall.get(id), (path) => tracker.lastSeen(path));
|
|
447
|
+
// A call the summary took never comes back: only the kept ones need their record.
|
|
448
|
+
const keptCalls = new Set([...shellCallsOnBranch(keptTurns)].map((call) => call.id));
|
|
449
|
+
for (const id of shellReadsByCall.keys()) if (!keptCalls.has(id)) shellReadsByCall.delete(id);
|
|
450
|
+
const inContext = inContextFiles(ctx, branch, keptReadPaths(keptTurns), keptShell);
|
|
451
|
+
// The loader lists imports by real path; a tracked path may be spelled through a symlink.
|
|
452
|
+
const shown = (path: string) => inContext.has(comparablePath(path)) || inContext.has(comparablePath(tryRealpath(path) ?? path));
|
|
453
|
+
// A file the context still shows keeps its read state (what the model
|
|
454
|
+
// saw, not what is on disk now, so the stale-edit guard still holds)
|
|
455
|
+
// and its touch time, for the next compaction's restore.
|
|
456
|
+
for (const path of tracker.tracked) if (!shown(path)) tracker.forget(path);
|
|
457
|
+
touched = new Map([...before].filter(([path]) => shown(path)));
|
|
458
|
+
// Opting out of One Code's restore does not disable pi's compaction:
|
|
459
|
+
// reads discarded by either summarizer still need a fresh read.
|
|
460
|
+
if (process.env.CC_COMPACTION === "0" || before.size === 0) return;
|
|
461
|
+
const allowed = restoreAllowed(ctx);
|
|
462
|
+
const candidates = restoreCandidates(before, (path) => shown(path) || !allowed(path));
|
|
463
|
+
const read = createReadToolDefinition(ctx.cwd);
|
|
464
|
+
const reads: RestoredRead[] = await Promise.all(candidates.map(async (path) => ({ path, text: await readAgain(read, path, ctx) })));
|
|
465
|
+
const shownText = new Map(reads.map(({ path, text }) => [path, text]));
|
|
466
|
+
const { blocks, restored } = restoreBlocks(reads, estimateTextTokens);
|
|
467
|
+
// The block that shows a file records what it showed, so a reload or a
|
|
468
|
+
// resume counts the file read exactly where the branch holds the block
|
|
469
|
+
// (replay.ts filesSeenOnBranch). The file counts as read only when the
|
|
470
|
+
// disk now holds exactly what the block shows: one that changed while
|
|
471
|
+
// it was read back (even back to where it started) fails safe, and a
|
|
472
|
+
// fresh read is asked for instead of passing the stale-edit guard.
|
|
473
|
+
const seen = new Map(
|
|
474
|
+
restored.map((path) => {
|
|
475
|
+
const observed = touch(path);
|
|
476
|
+
const text = shownText.get(path);
|
|
477
|
+
if (observed && (text === undefined || sha256(text) !== observed.sha256)) {
|
|
478
|
+
tracker.forget(path);
|
|
479
|
+
return [path, undefined] as const;
|
|
480
|
+
}
|
|
481
|
+
return [path, observed] as const;
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
for (const { text, restores } of blocks) {
|
|
485
|
+
const observation = restores ? seen.get(restores) : undefined;
|
|
486
|
+
pi.events.emit(REMINDER_CHANNEL, { text, placement: "user-prepend", orTail: true, untilCompaction: true, ...(observation ? { observations: [observation] } : {}) } satisfies ReminderPayload);
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
// The restore is best effort: nothing comes back, and an edit needs a fresh read.
|
|
490
|
+
tracker = new FileTracker();
|
|
491
|
+
touched = new Map();
|
|
492
|
+
shellReadsByCall.clear();
|
|
493
|
+
}
|
|
129
494
|
});
|
|
130
495
|
|
|
131
496
|
/**
|
|
@@ -147,12 +512,10 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
147
512
|
* (one blocked call plus a re-read per edit for anyone with an asynchronous
|
|
148
513
|
* formatter). Claude Code computes its `edited_text_file` attachments on
|
|
149
514
|
* every tool round, so the model reads the change before its next edit
|
|
150
|
-
* (review M5). The one-shot is pending when the next request goes
|
|
151
|
-
* the
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* into the stored result; pinned is the same mechanism tool-search's
|
|
155
|
-
* deferred-tool miss uses. Our own edit/write/read of a file was observed by
|
|
515
|
+
* (review M5). The one-shot is a pending entry when the next request goes
|
|
516
|
+
* out, and the renderer places it on that request's trailing tool result
|
|
517
|
+
* (lib/reminders.ts), the same mechanism tool-search's deferred-tool miss
|
|
518
|
+
* uses; reminders never enter a stored result. Our own edit/write/read of a file was observed by
|
|
156
519
|
* the `tool_result` handler above before this fires, so it is fresh here
|
|
157
520
|
* and never reported as external — but only for a call that ran alone. pi
|
|
158
521
|
* executes a batch of parallel tool calls CONCURRENTLY, so the first edit's
|
|
@@ -163,17 +526,27 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
163
526
|
* runs only once the batch has fully drained.
|
|
164
527
|
*/
|
|
165
528
|
const reportExternalChanges = () => {
|
|
166
|
-
|
|
167
|
-
|
|
529
|
+
// Each notice records the versions it names on its pending entry, so a
|
|
530
|
+
// reload or a resume does not announce them again (replay.ts).
|
|
531
|
+
const detailed: { text: string; notified?: Pick<FileObservation, "path" | "sha256"> }[] = [];
|
|
532
|
+
const overflow: Pick<FileObservation, "path" | "sha256">[] = [];
|
|
168
533
|
for (const path of tracker.tracked) {
|
|
534
|
+
// Undefined for a file tracked without content (oversized); an empty
|
|
535
|
+
// file is compared like any other.
|
|
169
536
|
const previous = tracker.lastSeen(path);
|
|
170
|
-
if (previous === undefined
|
|
537
|
+
if (previous === undefined) continue;
|
|
171
538
|
const stamp = statIfPresent(path);
|
|
172
539
|
if (stamp && tracker.unchangedOnDisk(path, stamp)) continue;
|
|
540
|
+
// This scan accounts for the child's write whatever it finds (same
|
|
541
|
+
// content, already notified, overflow), so a later change by someone
|
|
542
|
+
// else is never credited to that agent.
|
|
543
|
+
const byChild = childWrites.has(path);
|
|
544
|
+
const childAgent = childWrites.get(path);
|
|
545
|
+
childWrites.delete(path);
|
|
173
546
|
const current = stamp ? readIfPresent(path) : undefined;
|
|
174
547
|
if (stamp === undefined || current === undefined) {
|
|
175
548
|
tracker.forget(path);
|
|
176
|
-
detailed.push(`${path} no longer exists; it was deleted or moved after you read it.`);
|
|
549
|
+
detailed.push({ text: `${path} no longer exists; it was deleted or moved after you last read or wrote it.` });
|
|
177
550
|
continue;
|
|
178
551
|
}
|
|
179
552
|
tracker.recordStamp(path, stamp);
|
|
@@ -181,22 +554,24 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
181
554
|
// Don't repeat the same warning every turn…
|
|
182
555
|
if (tracker.alreadyNotified(path, current)) continue;
|
|
183
556
|
|
|
557
|
+
const version = { path, sha256: sha256(current) };
|
|
184
558
|
if (detailed.length < DETAILED_CHANGE_REMINDERS_PER_TURN) {
|
|
185
559
|
const excerpt = describeChanges(previous, current);
|
|
186
|
-
if (excerpt) detailed.push(EXTERNAL_CHANGE_REMINDER(path, excerpt));
|
|
560
|
+
if (excerpt) detailed.push({ text: byChild ? CHILD_CHANGE_REMINDER(path, childAgent, excerpt) : EXTERNAL_CHANGE_REMINDER(path, excerpt), notified: version });
|
|
187
561
|
} else {
|
|
188
|
-
overflow.push(
|
|
562
|
+
overflow.push(version);
|
|
189
563
|
}
|
|
190
564
|
// …but deliberately do NOT record the new content as seen: the file must
|
|
191
565
|
// stay stale so the edit guard still forces a re-read. Marking it read
|
|
192
566
|
// here would announce the change and then permit the clobbering edit.
|
|
193
567
|
tracker.markNotified(path, current);
|
|
194
568
|
}
|
|
195
|
-
for (const text of detailed) pi.events.emit(REMINDER_CHANNEL, { text });
|
|
569
|
+
for (const { text, notified } of detailed) pi.events.emit(REMINDER_CHANNEL, { text, ...(notified ? { notified: [notified] } : {}) } satisfies ReminderPayload);
|
|
196
570
|
if (overflow.length > 0) {
|
|
197
571
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
198
|
-
text: `${overflow.length} more file(s) you read earlier changed on disk since: ${overflow.join(", ")}. Re-read any of them before editing it.`,
|
|
199
|
-
|
|
572
|
+
text: `${overflow.length} more file(s) you read earlier changed on disk since: ${overflow.map((file) => file.path).join(", ")}. Re-read any of them before editing it.`,
|
|
573
|
+
notified: overflow,
|
|
574
|
+
} satisfies ReminderPayload);
|
|
200
575
|
}
|
|
201
576
|
};
|
|
202
577
|
/**
|
|
@@ -212,8 +587,11 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
|
|
|
212
587
|
pi.on("agent_start", () => {
|
|
213
588
|
// A turn aborted mid-batch can leave ids behind; each turn starts from a
|
|
214
589
|
// clean set so the mid-turn scan cannot be wedged off for the rest of the
|
|
215
|
-
// session.
|
|
590
|
+
// session. A write blocked before it ran left its existence entry behind,
|
|
591
|
+
// and a blocked or aborted bash call its snapshot of file versions.
|
|
216
592
|
executing.clear();
|
|
593
|
+
writeTargetExisted.clear();
|
|
594
|
+
shellSnapshots.clear();
|
|
217
595
|
reportExternalChanges();
|
|
218
596
|
return undefined;
|
|
219
597
|
});
|