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,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rebuild read-before-write state from a session branch (pure): which files
|
|
3
|
-
* the model has
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the model has seen this conversation, so a `--resume`d session does not
|
|
4
|
+
* block every edit with "has not been read in this conversation", a claim
|
|
5
|
+
* the model can see is false in its own transcript (review T9).
|
|
6
6
|
*
|
|
7
|
-
* The content the model saw is not in the transcript,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The content the model saw is not in the transcript, but each read and write
|
|
8
|
+
* result records what it saw (`details.oneCodeFile`, `FileObservation`) under
|
|
9
|
+
* the absolute path the tool resolved. The replay keys on that path, never on
|
|
10
|
+
* the call's own spelling: a worktree rewrites a relative path only in the
|
|
11
|
+
* call it runs, so the transcript's `a.ts` would resolve to the main
|
|
12
|
+
* checkout's twin (round-3 review M15). A post-compaction restore records the
|
|
13
|
+
* same on its pending entry, and a changed-on-disk notice records the
|
|
14
|
+
* versions it named. The caller compares each observation with the disk: a
|
|
15
|
+
* file that still matches is read, one that changed since is stale. A result
|
|
16
|
+
* that recorded nothing (a session from before the record existed) leaves its
|
|
17
|
+
* file unread: the disk as it is now is never taken as what the model saw
|
|
18
|
+
* (round-2 review H1).
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
import { latestCompaction } from "../lib/compaction-boundary.ts";
|
|
22
|
+
import { type FileVersion, isFileVersion, readRecord } from "../lib/ledger/records.ts";
|
|
23
|
+
import { shownInFull } from "./shell-reads.ts";
|
|
16
24
|
|
|
17
25
|
interface ToolCallBlock {
|
|
18
26
|
type: "toolCall";
|
|
@@ -21,26 +29,131 @@ interface ToolCallBlock {
|
|
|
21
29
|
arguments?: Record<string, unknown>;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
type BranchMessage = { role?: string; content?: unknown; toolCallId?: string; toolName?: string; isError?: boolean; details?: { oneCodeFile?: unknown } };
|
|
33
|
+
|
|
34
|
+
/** What a read or write result saw of its file, recorded in its details as `oneCodeFile`: a version with its size and modification time. */
|
|
35
|
+
export type FileObservation = Required<FileVersion>;
|
|
36
|
+
|
|
37
|
+
const isObservation = (value: unknown): value is FileObservation => isFileVersion(value) && value.size !== undefined && value.mtimeMs !== undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Every successful tool result on the branch that recorded what it saw of a
|
|
41
|
+
* file, in branch order: its position, the tool that ran (from its call, else
|
|
42
|
+
* the result's own `toolName`) and the observation.
|
|
43
|
+
*/
|
|
44
|
+
function* fileResults(entries: readonly unknown[]): Generator<{ index: number; name: string | undefined; seen: FileObservation }> {
|
|
45
|
+
const names = new Map<string, string>();
|
|
46
|
+
for (const [index, entry] of entries.entries()) {
|
|
47
|
+
const e = entry as { type?: string; message?: BranchMessage };
|
|
48
|
+
if (e?.type !== "message" || !e.message) continue;
|
|
49
|
+
const m = e.message;
|
|
50
|
+
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
51
|
+
for (const block of m.content as ToolCallBlock[]) if (block?.type === "toolCall" && block.id) names.set(block.id, block.name);
|
|
52
|
+
} else if (m.role === "toolResult" && !m.isError) {
|
|
53
|
+
const seen = m.details?.oneCodeFile;
|
|
54
|
+
if (isObservation(seen)) yield { index, name: (m.toolCallId ? names.get(m.toolCallId) : undefined) ?? m.toolName, seen };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What the branch shows the model saw of a file, and what it was told about the file since. */
|
|
60
|
+
export interface SeenFile {
|
|
61
|
+
/** The version the model last saw, as the result or restore that showed it recorded it. */
|
|
62
|
+
seen: FileObservation;
|
|
63
|
+
/** Content hashes of the changed versions a notice named after that. */
|
|
64
|
+
notified: Set<string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Each file the model saw in the entries still in context (`inContextEntries`),
|
|
69
|
+
* by absolute path: the last result or post-compaction restore that showed
|
|
70
|
+
* it, and the changed versions a notice named since.
|
|
71
|
+
*
|
|
72
|
+
* In the kept turns before the latest compaction a `read` result counts, as
|
|
73
|
+
* at the compaction itself: a kept edit or write does not show the whole
|
|
74
|
+
* file, so it counts only for a file a kept read showed before it, where it
|
|
75
|
+
* records the version the edit left (the live tracker observes the file
|
|
76
|
+
* again after its own write, and the compaction keeps that). A restore counts only after the latest compaction, and there it
|
|
77
|
+
* counts whether or not a request delivered it yet. A later compaction is
|
|
78
|
+
* the only thing that drops a restore undelivered (lib/ledger/fold.ts), so
|
|
79
|
+
* one recorded since the latest is either delivered or rides the next
|
|
80
|
+
* request, and the model sees the file before it can call a tool on it; the
|
|
81
|
+
* live session counts it read from the compaction on, too. A notice counts
|
|
82
|
+
* wherever it lies after the file's last observation: it is delivered or
|
|
83
|
+
* pending, and announcing the same version again would repeat it.
|
|
84
|
+
*/
|
|
85
|
+
export function filesSeenOnBranch(entries: readonly unknown[]): Map<string, SeenFile> {
|
|
86
|
+
const compaction = latestCompaction(entries)?.index ?? -1;
|
|
87
|
+
const results = new Map([...fileResults(entries)].map((result) => [result.index, result]));
|
|
88
|
+
const files = new Map<string, SeenFile>();
|
|
89
|
+
const see = (seen: FileObservation) => files.set(seen.path, { seen, notified: new Set() });
|
|
90
|
+
entries.forEach((entry, index) => {
|
|
91
|
+
const result = results.get(index);
|
|
92
|
+
if (result) {
|
|
93
|
+
if (index > compaction || result.name === "read" || files.has(result.seen.path)) see(result.seen);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const record = readRecord(entry as never);
|
|
97
|
+
if (record?.kind !== "pending") return;
|
|
98
|
+
if (index > compaction) for (const seen of record.data.observations ?? []) if (isObservation(seen)) see(seen);
|
|
99
|
+
for (const told of record.data.notified ?? []) files.get(told.path)?.notified.add(told.sha256);
|
|
100
|
+
});
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The files (absolute, as their results recorded them) that a successful
|
|
106
|
+
* `read` in the kept turns returned: those files are in context already, so
|
|
107
|
+
* the post-compaction restore skips them.
|
|
108
|
+
*/
|
|
109
|
+
export function keptReadPaths(kept: readonly unknown[]): string[] {
|
|
110
|
+
return [...fileResults(kept)].filter((result) => result.name === "read").map((result) => result.seen.path);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The tracked files that a bash call in the kept turns showed in full: those
|
|
115
|
+
* the call certified when it ran (`readsOf`, by call id: the directory it ran
|
|
116
|
+
* in decided, never today's) whose content as the tracker last saw it (`seen`)
|
|
117
|
+
* is still all in its kept output. Like a kept `read`, they are in context
|
|
118
|
+
* still, so they stay read; a file changed since is then stale, not unread. A
|
|
119
|
+
* call needs its result in the kept turns too, and one with no record
|
|
120
|
+
* certifies nothing (a miss costs one re-read).
|
|
121
|
+
*/
|
|
122
|
+
export function keptShellReadPaths(kept: readonly unknown[], readsOf: (callId: string) => readonly string[] | undefined, seen: (path: string) => string | undefined): string[] {
|
|
123
|
+
const shown = new Set<string>();
|
|
124
|
+
for (const { id, output } of shellCallsOnBranch(kept)) {
|
|
125
|
+
for (const path of readsOf(id) ?? []) {
|
|
126
|
+
if (shown.has(path)) continue;
|
|
127
|
+
const content = seen(path);
|
|
128
|
+
if (content !== undefined && shownInFull(output, content)) shown.add(path);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...shown];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Every bash call on the branch with its command and the text of its result,
|
|
136
|
+
* in result order: a resumed session re-checks which files those outputs
|
|
137
|
+
* showed in full (`shell-reads.ts`), against the files' current content.
|
|
138
|
+
*/
|
|
139
|
+
export function* shellCallsOnBranch(entries: readonly unknown[]): Generator<{ id: string; command: string; output: string }> {
|
|
140
|
+
const commands = new Map<string, string>();
|
|
28
141
|
for (const entry of entries) {
|
|
29
|
-
const e = entry as { type?: string; message?:
|
|
142
|
+
const e = entry as { type?: string; message?: BranchMessage };
|
|
30
143
|
if (e?.type !== "message" || !e.message) continue;
|
|
31
144
|
const m = e.message;
|
|
32
145
|
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
33
146
|
for (const block of m.content as ToolCallBlock[]) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (typeof path === "string" && path.trim()) calls.set(block.id, { name: block.name, path });
|
|
147
|
+
const command = block?.type === "toolCall" && block.name === "bash" ? block.arguments?.command : undefined;
|
|
148
|
+
if (typeof command === "string" && block.id) commands.set(block.id, command);
|
|
37
149
|
}
|
|
38
|
-
} else if (m.role === "toolResult" && m.toolCallId
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
|
|
42
|
-
|
|
150
|
+
} else if (m.role === "toolResult" && m.toolCallId) {
|
|
151
|
+
const command = commands.get(m.toolCallId);
|
|
152
|
+
if (command === undefined || !Array.isArray(m.content)) continue;
|
|
153
|
+
const output = (m.content as { type?: string; text?: string }[])
|
|
154
|
+
.map((block) => (block?.type === "text" ? (block.text ?? "") : ""))
|
|
155
|
+
.join("\n");
|
|
156
|
+
yield { id: m.toolCallId, command, output };
|
|
43
157
|
}
|
|
44
158
|
}
|
|
45
|
-
return seen;
|
|
46
159
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files that come back after a compaction, and how. Pure: no pi imports,
|
|
3
|
+
* no I/O.
|
|
4
|
+
*
|
|
5
|
+
* As in Claude Code: the five files the session read, edited or wrote with
|
|
6
|
+
* the newest modification time (as stat'ed at that read or write), minus the
|
|
7
|
+
* files the context still shows, are read again. A file within 5,000 tokens
|
|
8
|
+
* comes back as a read call and its result, a larger one as a note to read it
|
|
9
|
+
* again, while the total stays within 50,000 tokens (findings §47). Each is
|
|
10
|
+
* its own `<system-reminder>` block at the start of the next user message,
|
|
11
|
+
* worded as Claude Code words them with One Code's tool name and argument
|
|
12
|
+
* (`read`, `path`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const RESTORE_MAX_FILES = 5;
|
|
16
|
+
export const RESTORE_FILE_MAX_TOKENS = 5_000;
|
|
17
|
+
export const RESTORE_TOTAL_MAX_TOKENS = 50_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The files to restore, newest first: the touched files (path to modification
|
|
21
|
+
* time) minus `skip`, at most five. A file that cannot be read again is
|
|
22
|
+
* dropped afterwards, not replaced, as in Claude Code.
|
|
23
|
+
*/
|
|
24
|
+
export function restoreCandidates(touched: ReadonlyMap<string, number>, skip: (path: string) => boolean): string[] {
|
|
25
|
+
const candidates: string[] = [];
|
|
26
|
+
for (const [path] of [...touched].sort((a, b) => b[1] - a[1])) {
|
|
27
|
+
if (candidates.length === RESTORE_MAX_FILES) break;
|
|
28
|
+
if (!skip(path)) candidates.push(path);
|
|
29
|
+
}
|
|
30
|
+
return candidates;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What reading a candidate again gave: its text as the read tool returns it, or nothing (gone, unreadable, an image). */
|
|
34
|
+
export type RestoredRead = { path: string; text: string | undefined };
|
|
35
|
+
|
|
36
|
+
/** One `<system-reminder>` block of a restore; `restores` names the file whose contents it puts back in context. */
|
|
37
|
+
export type RestoreBlock = { text: string; restores?: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The reminder blocks for the reads, in order: two blocks for a file within
|
|
41
|
+
* the per-file cap (the result block `restores` it), one note for a larger
|
|
42
|
+
* one, nothing for a file that could not be read. A file's blocks are kept
|
|
43
|
+
* only while the running total stays within the overall cap. `restored` lists
|
|
44
|
+
* the files whose contents went back into context.
|
|
45
|
+
*/
|
|
46
|
+
export function restoreBlocks(reads: RestoredRead[], estimateTokens: (text: string) => number): { blocks: RestoreBlock[]; restored: string[] } {
|
|
47
|
+
const blocks: RestoreBlock[] = [];
|
|
48
|
+
const restored: string[] = [];
|
|
49
|
+
let total = 0;
|
|
50
|
+
for (const { path, text } of reads) {
|
|
51
|
+
if (text === undefined) continue;
|
|
52
|
+
const fits = estimateTokens(text) <= RESTORE_FILE_MAX_TOKENS;
|
|
53
|
+
const fileBlocks: RestoreBlock[] = fits ? [{ text: readCallBlock(path) }, { text: readResultBlock(text), restores: path }] : [{ text: tooLargeNote(path) }];
|
|
54
|
+
const tokens = fileBlocks.reduce((sum, block) => sum + estimateTokens(block.text), 0);
|
|
55
|
+
if (total + tokens > RESTORE_TOTAL_MAX_TOKENS) continue;
|
|
56
|
+
total += tokens;
|
|
57
|
+
blocks.push(...fileBlocks);
|
|
58
|
+
if (fits) restored.push(path);
|
|
59
|
+
}
|
|
60
|
+
return { blocks, restored };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const readCallBlock = (path: string) => `Called the read tool with the following input: ${JSON.stringify({ path })}`;
|
|
64
|
+
export const readResultBlock = (text: string) => `Result of calling the read tool:\n${text}`;
|
|
65
|
+
export const tooLargeNote = (path: string) =>
|
|
66
|
+
`Note: ${path} was read before the last conversation was summarized, but the contents are too large to include. Use read tool if you need to access it.`;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's result and error texts for pi's built-in read, write and edit
|
|
3
|
+
* (pure). pi's tools keep their call shapes (`path`, `edits[]`) and their own
|
|
4
|
+
* behavior; only what the model reads back changes, by rewriting the first
|
|
5
|
+
* text block of a result whose text is pi's exact wording. Anything else
|
|
6
|
+
* (pi's truncation notes, an image, a block another hook appended) passes
|
|
7
|
+
* through untouched.
|
|
8
|
+
*
|
|
9
|
+
* The success texts say the file state is current, which is true here: the
|
|
10
|
+
* file-tracker observes the file after every successful write or edit, so the
|
|
11
|
+
* next edit is not refused as unread or stale.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type Block = { type: string; text?: string };
|
|
15
|
+
|
|
16
|
+
export const FILE_STATE_CURRENT = " (file state is current in your context — no need to read it back)";
|
|
17
|
+
|
|
18
|
+
/** Claude Code's missing-file error for read and edit. */
|
|
19
|
+
export function fileDoesNotExist(cwd: string): string {
|
|
20
|
+
return `File does not exist. Note: your current working directory is ${cwd}.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Claude Code's empty-file read result. */
|
|
24
|
+
export const EMPTY_FILE_WARNING = "<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>";
|
|
25
|
+
|
|
26
|
+
export interface FileToolResult {
|
|
27
|
+
toolName: string;
|
|
28
|
+
isError: boolean;
|
|
29
|
+
content: Block[];
|
|
30
|
+
/** The absolute path the tool touched. */
|
|
31
|
+
path: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
/** For a write: whether the file existed before the call (Claude Code says "updated" then, "created" otherwise). */
|
|
34
|
+
existedBefore?: boolean;
|
|
35
|
+
/** For a read: whether the file is empty on disk (asked only for an empty result). */
|
|
36
|
+
isEmptyFile?: () => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The first block's replacement text, or undefined to leave the result as pi wrote it. */
|
|
40
|
+
function rewrite(result: FileToolResult, text: string): string | undefined {
|
|
41
|
+
const { toolName, isError, path, cwd } = result;
|
|
42
|
+
if (toolName === "write" && !isError && text.startsWith("Successfully wrote to ")) {
|
|
43
|
+
return result.existedBefore
|
|
44
|
+
? `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`
|
|
45
|
+
: `File created successfully at: ${path}${FILE_STATE_CURRENT}`;
|
|
46
|
+
}
|
|
47
|
+
if (toolName === "edit" && !isError && /^Successfully replaced \d+ block\(s\) in /.test(text)) {
|
|
48
|
+
return `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`;
|
|
49
|
+
}
|
|
50
|
+
if (toolName === "edit" && isError && /^Could not edit file: .*\. Error code: ENOENT\.$/s.test(text)) return fileDoesNotExist(cwd);
|
|
51
|
+
if (toolName === "read" && isError) {
|
|
52
|
+
if (/^ENOENT: no such file or directory, \w+ '/.test(text)) return fileDoesNotExist(cwd);
|
|
53
|
+
if (text === "EISDIR: illegal operation on a directory, read") return `${text} '${path}'`;
|
|
54
|
+
}
|
|
55
|
+
if (toolName === "read" && !isError && text === "" && result.isEmptyFile?.()) return EMPTY_FILE_WARNING;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The result's content with Claude Code's text in place of pi's, or undefined when nothing changes. */
|
|
60
|
+
export function fileToolResultContent<B extends Block>(result: FileToolResult & { content: B[] }): B[] | undefined {
|
|
61
|
+
if (!Array.isArray(result.content)) return undefined;
|
|
62
|
+
const [first, ...rest] = result.content;
|
|
63
|
+
if (first?.type !== "text" || typeof first.text !== "string") return undefined;
|
|
64
|
+
const text = rewrite(result, first.text);
|
|
65
|
+
return text === undefined ? undefined : [{ ...first, text }, ...rest];
|
|
66
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files a shell command showed the model in full (pure apart from the bash
|
|
3
|
+
* grammar, which the caller awaits).
|
|
4
|
+
*
|
|
5
|
+
* The auto-mode note tells the model to read files with cat, head or sed, and
|
|
6
|
+
* GPT-6 models do. Before this, the edit that followed was refused as
|
|
7
|
+
* "not read in this conversation" although the whole file was in the
|
|
8
|
+
* transcript: 16 refused edits in one 72-run battery (2026-10-04). A file now
|
|
9
|
+
* counts as read when a line that runs a reader names it and the output holds
|
|
10
|
+
* the file's entire current content. That one check covers every way the
|
|
11
|
+
* model could have seen less: a pipe into `head`, `cat -n`, a slice, an output
|
|
12
|
+
* persisted or clipped for size, a write later in the same command. The
|
|
13
|
+
* stale-edit guard is unchanged, since the file is observed with that content.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isAbsolute } from "node:path";
|
|
17
|
+
import { toAbsoluteBash } from "../auto-mode/paths.ts";
|
|
18
|
+
import { globComponentRegex, isUnknownTilde, movesDirectory, resolvePayload } from "../auto-mode/shell-analysis.ts";
|
|
19
|
+
import { LOOPS, parseCommand, type ParseResult, scopedTracker, type Segment, type Token } from "../auto-mode/shell-parse.ts";
|
|
20
|
+
|
|
21
|
+
/** Commands whose file arguments may print a whole file. */
|
|
22
|
+
const READERS = new Set(["cat", "head", "tail", "sed"]);
|
|
23
|
+
|
|
24
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
25
|
+
|
|
26
|
+
/** A word a reader may have printed, with the absolute directory the shell resolves it in. */
|
|
27
|
+
export interface ShellReadCandidate {
|
|
28
|
+
word: string;
|
|
29
|
+
dir: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The files `command` may have printed, as written: the plain arguments of
|
|
34
|
+
* its reader commands, plus every path-like word of a line that runs a reader
|
|
35
|
+
* at all. Candidates only, each still to be checked with {@link shownInFull}.
|
|
36
|
+
* Each comes with the directory its reader runs in, starting from `cwd` and
|
|
37
|
+
* following a literal `cd` ({@link segmentDirectories}); a reader whose
|
|
38
|
+
* directory cannot be known offers nothing. Empty when no reader runs or the
|
|
39
|
+
* command does not parse (the grammar not loaded included).
|
|
40
|
+
*/
|
|
41
|
+
export function shellReadCandidates(command: string, cwd: string, home: string): ShellReadCandidate[] {
|
|
42
|
+
const parsed = parseCommand(command);
|
|
43
|
+
if (parsed.parseFailed) return [];
|
|
44
|
+
const readers = parsed.segments.flatMap((segment, index) => {
|
|
45
|
+
const first = segment.tokens.findIndex((token) => !ASSIGNMENT.test(token.value));
|
|
46
|
+
if (first === -1) return [];
|
|
47
|
+
const [head, ...args] = segment.tokens.slice(first);
|
|
48
|
+
return head && !head.dynamic && READERS.has(head.value.split("/").pop() ?? "") ? [{ segment, index, args }] : [];
|
|
49
|
+
});
|
|
50
|
+
// Most commands run no reader: skip the directory walk for them.
|
|
51
|
+
if (readers.length === 0) return [];
|
|
52
|
+
const dirs = segmentDirectories(parsed, cwd, home);
|
|
53
|
+
const candidates = new Map<string, ShellReadCandidate>();
|
|
54
|
+
const add = (word: string, dir: string) => candidates.set(`${dir}\0${word}`, { word, dir });
|
|
55
|
+
const readerDirs = new Set<string>();
|
|
56
|
+
for (const { segment, index, args } of readers) {
|
|
57
|
+
const dir = dirs[index];
|
|
58
|
+
if (dir === undefined) continue;
|
|
59
|
+
readerDirs.add(dir);
|
|
60
|
+
if (segment.substitution !== undefined) continue;
|
|
61
|
+
for (const arg of args) if (isPlainPath(arg)) add(arg.value, dir);
|
|
62
|
+
}
|
|
63
|
+
// A reader fed from a variable (`for f in a.py b.py; do cat "$f"; done`)
|
|
64
|
+
// names its files elsewhere in the line. Every plain path-like word is a
|
|
65
|
+
// candidate then, in each directory a reader ran in: the full-content
|
|
66
|
+
// check, not the parse, decides.
|
|
67
|
+
if (readerDirs.size > 0) {
|
|
68
|
+
const words = new Set<string>();
|
|
69
|
+
for (const word of [...(command.match(PATH_WORD) ?? []), ...(command.match(GLOB_WORD) ?? [])]) if (words.size < MAX_CANDIDATES) words.add(word);
|
|
70
|
+
for (const dir of [...readerDirs].slice(0, MAX_READER_DIRS)) for (const word of words) add(word, dir);
|
|
71
|
+
}
|
|
72
|
+
return [...candidates.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The absolute paths `command` may have printed: its candidates
|
|
77
|
+
* ({@link shellReadCandidates}) resolved as the shell resolves them, from
|
|
78
|
+
* the directory each one's reader runs in, with a glob expanded through
|
|
79
|
+
* `readdir` (given an absolute directory). The shell's own resolution
|
|
80
|
+
* (`toAbsoluteBash`), not the file tools' (`resolveToolPath` strips a leading
|
|
81
|
+
* `@`, which `cat @x` keeps): `~/` and the shell's directory only.
|
|
82
|
+
*/
|
|
83
|
+
export function shellReadPaths(command: string, cwd: string, home: string, readdir: (dir: string) => string[]): string[] {
|
|
84
|
+
const paths = new Set<string>();
|
|
85
|
+
for (const { word, dir } of shellReadCandidates(command, cwd, home)) {
|
|
86
|
+
for (const name of expandCandidate(word, (sub) => readdir(toAbsoluteBash(dir, sub, home)))) paths.add(toAbsoluteBash(dir, name, home));
|
|
87
|
+
}
|
|
88
|
+
return [...paths];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The directory each segment runs in, or undefined where it cannot be known.
|
|
93
|
+
* Only a plain `cd <literal>` (or `cd -- <literal>`) that surely ran moves
|
|
94
|
+
* the commands after it in its own subshell scope (`scopedTracker`). One that
|
|
95
|
+
* may not run moves only the later members of its own `&&` list
|
|
96
|
+
* (`Segment.andList`), which run only once it succeeded (`cd src && cat x`
|
|
97
|
+
* inside the worktree's `cd <wt> && ( … )`). Anything else that may change
|
|
98
|
+
* the directory makes the commands after it unknown, never guessed: a
|
|
99
|
+
* dynamic, glob, `-` or `~user` target, an option, `pushd` and `popd`, a
|
|
100
|
+
* wrapper, assignment or `!` in front, a `cd` that may not run (an `if`
|
|
101
|
+
* branch, after `||`) for everything outside its `&&` list, a `cd` last in a
|
|
102
|
+
* pipeline (`lastpipe` runs it in the shell around), a backgrounded line,
|
|
103
|
+
* `eval` and `source`, and a line that defines a function (which may replace
|
|
104
|
+
* `cd`). A loop body runs more than once, so after a move in any loop every
|
|
105
|
+
* command in a loop is unknown too. A miss costs one re-read; a wrong
|
|
106
|
+
* directory would certify a read that never happened.
|
|
107
|
+
*/
|
|
108
|
+
function segmentDirectories(parsed: ParseResult, cwd: string, home: string): (string | undefined)[] {
|
|
109
|
+
const { segments } = parsed;
|
|
110
|
+
const dirs = scopedTracker<string | undefined>(cwd);
|
|
111
|
+
/** Where a `cd` that may not have run moved the later members of its `&&` list, by list. */
|
|
112
|
+
const listDirs = new Map<number, string | undefined>();
|
|
113
|
+
const inLoop = (segment: Segment) => segment.enclosing.some((construct) => LOOPS.has(construct));
|
|
114
|
+
const moves = segments.map(mayMove);
|
|
115
|
+
const loopMoves = segments.some((segment, index) => moves[index] && inLoop(segment));
|
|
116
|
+
return segments.map((segment, index) => {
|
|
117
|
+
const inList = segment.andList !== undefined && listDirs.has(segment.andList);
|
|
118
|
+
const dir = loopMoves && inLoop(segment) ? undefined : inList ? listDirs.get(segment.andList!) : dirs.get(segment);
|
|
119
|
+
if (!moves[index]) return dir;
|
|
120
|
+
const certain = !parsed.background && !parsed.definesFunction && !segment.pipelineShell && !segment.enclosing.includes("negated_command");
|
|
121
|
+
const target = certain ? literalCdTarget(segment) : undefined;
|
|
122
|
+
// A relative target from an unknown directory stays unknown; an absolute one re-anchors.
|
|
123
|
+
const next = target === undefined ? undefined : dir === undefined && !isAbsoluteTarget(target) ? undefined : toAbsoluteBash(dir ?? cwd, target, home);
|
|
124
|
+
if (segment.pipelineShell) dirs.set({ scopes: segment.pipelineShell }, undefined);
|
|
125
|
+
if (segment.conditional) {
|
|
126
|
+
dirs.set(segment, undefined);
|
|
127
|
+
if (segment.andList !== undefined) listDirs.set(segment.andList, next);
|
|
128
|
+
} else {
|
|
129
|
+
dirs.set(segment, next);
|
|
130
|
+
}
|
|
131
|
+
return dir;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether a segment may change the shell's directory: a `cd`/`pushd`/`popd` in any spelling, or a script run in this shell. */
|
|
136
|
+
function mayMove(segment: Segment): boolean {
|
|
137
|
+
return movesDirectory(segment) || ["eval", "source", "."].includes(resolvePayload(segment.tokens).command);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The target of `cd <word>` or `cd -- <word>` written plainly, or undefined for any other form. */
|
|
141
|
+
function literalCdTarget(segment: Segment): string | undefined {
|
|
142
|
+
const [head, ...args] = segment.tokens;
|
|
143
|
+
if (head?.value !== "cd" || head.dynamic) return undefined;
|
|
144
|
+
const rest = args[0]?.value === "--" ? args.slice(1) : args;
|
|
145
|
+
if (rest.length > 1) return undefined;
|
|
146
|
+
const target = rest[0];
|
|
147
|
+
// A bare `cd` goes home.
|
|
148
|
+
if (!target) return "~";
|
|
149
|
+
if (target.dynamic || target.glob || target.value === "" || target.value.startsWith("-") || isUnknownTilde(target.value)) return undefined;
|
|
150
|
+
return target.value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether a `cd` target lands in the same place from any directory. */
|
|
154
|
+
function isAbsoluteTarget(target: string): boolean {
|
|
155
|
+
return isAbsolute(target) || target === "~" || target.startsWith("~/");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The files a candidate names: itself, or for a glob in its last component
|
|
160
|
+
* (`src/*.py`, a loop's word list), the matching entries of that directory,
|
|
161
|
+
* listed through `readdir` (no shell). Globs in a directory component are not
|
|
162
|
+
* expanded.
|
|
163
|
+
*/
|
|
164
|
+
export function expandCandidate(word: string, readdir: (dir: string) => string[]): string[] {
|
|
165
|
+
if (!/[*?[]/.test(word)) return [word];
|
|
166
|
+
const slash = word.lastIndexOf("/");
|
|
167
|
+
const dir = slash === -1 ? "" : word.slice(0, slash);
|
|
168
|
+
if (/[*?[]/.test(dir)) return [];
|
|
169
|
+
const pattern = globComponentRegex(word.slice(slash + 1));
|
|
170
|
+
if (!pattern) return [];
|
|
171
|
+
let names: string[];
|
|
172
|
+
try {
|
|
173
|
+
names = readdir(dir || ".");
|
|
174
|
+
} catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return names.filter((name) => !name.startsWith(".") && pattern.test(name)).map((name) => (dir ? `${dir}/${name}` : name)).slice(0, MAX_CANDIDATES);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A word that could be a file path as written, absolute or relative: no quoting, expansion or glob characters. */
|
|
181
|
+
const PATH_WORD = /(?<![\w$\-/])\/?[\w.@+~][\w.@+~/,-]*/g;
|
|
182
|
+
/** A word with a glob character, which the shell would expand (`src/*.py`). */
|
|
183
|
+
const GLOB_WORD = /(?<![\w$\-])[\w.@+~/,-]*[*?[][\w.@+~/,*?[\]-]*/g;
|
|
184
|
+
const MAX_CANDIDATES = 50;
|
|
185
|
+
/** The path-like words of a line are tried in at most this many reader directories. */
|
|
186
|
+
const MAX_READER_DIRS = 4;
|
|
187
|
+
|
|
188
|
+
function isPlainPath(token: Token): boolean {
|
|
189
|
+
return !token.dynamic && !token.glob && token.value !== "" && !token.value.startsWith("-");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether `output` shows the whole of `content`. Trailing whitespace is
|
|
194
|
+
* ignored because the shell tool trims it. A tiny one-line file is never
|
|
195
|
+
* counted: its text turns up in outputs by chance (an adversarial GPT-6 Astra
|
|
196
|
+
* run matched `completed` to "(bash completed with no output)", `1` to
|
|
197
|
+
* "Exit code 1", an error line, a persisted-output tag), so the match would
|
|
198
|
+
* not show that the file was printed. What the guard protects still holds for
|
|
199
|
+
* a larger match under another file's name: every byte of the file was in
|
|
200
|
+
* front of the model, so an edit cannot discard content it has not seen.
|
|
201
|
+
*/
|
|
202
|
+
export function shownInFull(output: string, content: string): boolean {
|
|
203
|
+
const body = content.trimEnd();
|
|
204
|
+
if (body.length < MIN_SHOWN_CHARS && !(body.includes("\n") && body.length >= MIN_SHOWN_LINES_CHARS)) return false;
|
|
205
|
+
return output.includes(body);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A one-line file shorter than this is never counted as shown. */
|
|
209
|
+
const MIN_SHOWN_CHARS = 40;
|
|
210
|
+
/** A multi-line file shorter than this is never counted as shown. */
|
|
211
|
+
const MIN_SHOWN_LINES_CHARS = 16;
|
|
@@ -13,9 +13,18 @@
|
|
|
13
13
|
export type FreshnessStatus = "fresh" | "unread" | "stale" | "absent";
|
|
14
14
|
|
|
15
15
|
export interface TrackedFile {
|
|
16
|
-
/** Content as of the last read or write we performed. */
|
|
16
|
+
/** Content as of the last read or write we performed ("" when `oversized`). */
|
|
17
17
|
content: string;
|
|
18
|
+
/**
|
|
19
|
+
* Too large to keep: we saw the file but hold no content to compare. An
|
|
20
|
+
* explicit flag, never the empty string, so a file read while EMPTY
|
|
21
|
+
* compares like any other and cannot be overwritten after someone fills
|
|
22
|
+
* it (TOOLS-REVIEW-2026-09-26 M2).
|
|
23
|
+
*/
|
|
24
|
+
oversized: boolean;
|
|
18
25
|
at: number;
|
|
26
|
+
/** Seen, but changed on disk since (a resumed read whose file moved on): stale until read again. */
|
|
27
|
+
changed?: boolean;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/** What a stat says about a file on disk; equal stamps mean "no need to re-read". */
|
|
@@ -54,9 +63,9 @@ export class FileTracker {
|
|
|
54
63
|
this.notified.delete(path);
|
|
55
64
|
if (content.length > MAX_TRACKED_BYTES) {
|
|
56
65
|
// Too large to diff usefully; remember that we saw it, not its content.
|
|
57
|
-
this.files.set(path, { content: "", at });
|
|
66
|
+
this.files.set(path, { content: "", oversized: true, at });
|
|
58
67
|
} else {
|
|
59
|
-
this.files.set(path, { content, at });
|
|
68
|
+
this.files.set(path, { content, oversized: false, at });
|
|
60
69
|
}
|
|
61
70
|
if (stamp) this.stamps.set(path, stamp);
|
|
62
71
|
else this.stamps.delete(path);
|
|
@@ -66,6 +75,16 @@ export class FileTracker {
|
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Record a file the model saw in a form that is gone: it changed on disk
|
|
80
|
+
* since, and its old content is not kept. It counts as read, but stale.
|
|
81
|
+
*/
|
|
82
|
+
observeChanged(path: string, at: number): void {
|
|
83
|
+
this.notified.delete(path);
|
|
84
|
+
this.stamps.delete(path);
|
|
85
|
+
this.files.set(path, { content: "", oversized: false, at, changed: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
forget(path: string): void {
|
|
70
89
|
this.files.delete(path);
|
|
71
90
|
this.notified.delete(path);
|
|
@@ -103,8 +122,10 @@ export class FileTracker {
|
|
|
103
122
|
return this.files.has(path);
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
/** The content last seen, or undefined when untracked or tracked without content (oversized). */
|
|
106
126
|
lastSeen(path: string): string | undefined {
|
|
107
|
-
|
|
127
|
+
const known = this.files.get(path);
|
|
128
|
+
return known && !known.oversized && !known.changed ? known.content : undefined;
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
get tracked(): string[] {
|
|
@@ -121,9 +142,10 @@ export class FileTracker {
|
|
|
121
142
|
if (currentContent === undefined) return "absent";
|
|
122
143
|
const known = this.files.get(path);
|
|
123
144
|
if (!known) return "unread";
|
|
145
|
+
if (known.changed) return "stale";
|
|
124
146
|
// Oversized files are tracked without content; treat them as fresh rather
|
|
125
147
|
// than blocking edits we cannot reason about.
|
|
126
|
-
if (known.
|
|
148
|
+
if (known.oversized) return "fresh";
|
|
127
149
|
return known.content === currentContent ? "fresh" : "stale";
|
|
128
150
|
}
|
|
129
151
|
}
|
|
@@ -173,20 +195,28 @@ export function describeChanges(
|
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
export const UNREAD_REASON = (path: string, tool: string) =>
|
|
176
|
-
`Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen.`;
|
|
198
|
+
`Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen. A shell read counts only when it printed the whole file as it is on disk (a plain \`cat\`); after a partial one (head, sed -n, a pipe, or output cut off at the size limit) or another version of it (such as \`git show HEAD:<file>\`), use the read tool.`;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The same notice when one of this session's subagents or workflow agents
|
|
202
|
+
* changed the file (lib/child-writes.ts): saying "someone else, a formatter or
|
|
203
|
+
* a command" there sent the model looking for an outside cause.
|
|
204
|
+
*/
|
|
205
|
+
export const CHILD_CHANGE_REMINDER = (path: string, agent: string | undefined, excerpt: ChangeExcerpt) =>
|
|
206
|
+
`Note: ${path} was changed by ${agent ? `the agent "${agent}"` : "an agent"} you started in this session since you last read it. Take it as the current state. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
|
|
177
207
|
|
|
178
208
|
export const STALE_REASON = (path: string) =>
|
|
179
209
|
`${path} has changed on disk since you last read it — someone else, a formatter, or a command may have modified it. Read it again before editing, or your change would overwrite theirs.`;
|
|
180
210
|
|
|
181
211
|
/**
|
|
182
|
-
* Claude Code's `edited_text_file` attachment text,
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
212
|
+
* Claude Code's `edited_text_file` attachment text, plus One Code's closing
|
|
213
|
+
* clause telling the model to re-read before editing: Claude Code marks the
|
|
214
|
+
* file as read when it attaches the snippet, we deliberately do not (the
|
|
215
|
+
* stale-edit guard keeps firing), so without that clause the model's next
|
|
216
|
+
* edit is blocked and costs a round trip. The "take it as the current state
|
|
217
|
+
* rather than reverting it" steer is Claude Code's: without it a model
|
|
218
|
+
* mid-edit tends to restore its own version and narrate linter noise
|
|
219
|
+
* (STEERING-REVIEW-2026-09-05 M5).
|
|
190
220
|
*/
|
|
191
221
|
export const EXTERNAL_CHANGE_REMINDER = (path: string, excerpt: ChangeExcerpt) =>
|
|
192
|
-
`Note: ${path}
|
|
222
|
+
`Note: ${path} changed on disk since you last read it. That's usually deliberate, so take it as the current state rather than reverting it; if the change looks wrong, say so rather than undoing it yourself — otherwise no need to call it out. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
|