@mudssky/trellis 0.6.16-mudssky.5
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/AGENT_INSTALL.md +100 -0
- package/COPYRIGHT +15 -0
- package/INTEGRATION.md +140 -0
- package/LICENSE +235 -0
- package/README.md +125 -0
- package/bin/trellis.js +3 -0
- package/corresponding-source.tar.gz +0 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +304 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/ablate.d.ts +14 -0
- package/dist/commands/ablate.d.ts.map +1 -0
- package/dist/commands/ablate.js +308 -0
- package/dist/commands/ablate.js.map +1 -0
- package/dist/commands/channel/adapters/claude.d.ts +53 -0
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
- package/dist/commands/channel/adapters/claude.js +225 -0
- package/dist/commands/channel/adapters/claude.js.map +1 -0
- package/dist/commands/channel/adapters/codex.d.ts +93 -0
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
- package/dist/commands/channel/adapters/codex.js +573 -0
- package/dist/commands/channel/adapters/codex.js.map +1 -0
- package/dist/commands/channel/adapters/index.d.ts +94 -0
- package/dist/commands/channel/adapters/index.d.ts.map +1 -0
- package/dist/commands/channel/adapters/index.js +116 -0
- package/dist/commands/channel/adapters/index.js.map +1 -0
- package/dist/commands/channel/adapters/types.d.ts +33 -0
- package/dist/commands/channel/adapters/types.d.ts.map +1 -0
- package/dist/commands/channel/adapters/types.js +2 -0
- package/dist/commands/channel/adapters/types.js.map +1 -0
- package/dist/commands/channel/agent-loader.d.ts +32 -0
- package/dist/commands/channel/agent-loader.d.ts.map +1 -0
- package/dist/commands/channel/agent-loader.js +158 -0
- package/dist/commands/channel/agent-loader.js.map +1 -0
- package/dist/commands/channel/context-loader.d.ts +26 -0
- package/dist/commands/channel/context-loader.d.ts.map +1 -0
- package/dist/commands/channel/context-loader.js +311 -0
- package/dist/commands/channel/context-loader.js.map +1 -0
- package/dist/commands/channel/context-trust.d.ts +31 -0
- package/dist/commands/channel/context-trust.d.ts.map +1 -0
- package/dist/commands/channel/context-trust.js +139 -0
- package/dist/commands/channel/context-trust.js.map +1 -0
- package/dist/commands/channel/context.d.ts +16 -0
- package/dist/commands/channel/context.d.ts.map +1 -0
- package/dist/commands/channel/context.js +83 -0
- package/dist/commands/channel/context.js.map +1 -0
- package/dist/commands/channel/create.d.ts +27 -0
- package/dist/commands/channel/create.d.ts.map +1 -0
- package/dist/commands/channel/create.js +39 -0
- package/dist/commands/channel/create.js.map +1 -0
- package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
- package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
- package/dist/commands/channel/dev-parse-trace.js +70 -0
- package/dist/commands/channel/dev-parse-trace.js.map +1 -0
- package/dist/commands/channel/guard.d.ts +150 -0
- package/dist/commands/channel/guard.d.ts.map +1 -0
- package/dist/commands/channel/guard.js +480 -0
- package/dist/commands/channel/guard.js.map +1 -0
- package/dist/commands/channel/index.d.ts +3 -0
- package/dist/commands/channel/index.d.ts.map +1 -0
- package/dist/commands/channel/index.js +535 -0
- package/dist/commands/channel/index.js.map +1 -0
- package/dist/commands/channel/interrupt.d.ts +10 -0
- package/dist/commands/channel/interrupt.d.ts.map +1 -0
- package/dist/commands/channel/interrupt.js +22 -0
- package/dist/commands/channel/interrupt.js.map +1 -0
- package/dist/commands/channel/kill.d.ts +7 -0
- package/dist/commands/channel/kill.d.ts.map +1 -0
- package/dist/commands/channel/kill.js +130 -0
- package/dist/commands/channel/kill.js.map +1 -0
- package/dist/commands/channel/list.d.ts +17 -0
- package/dist/commands/channel/list.d.ts.map +1 -0
- package/dist/commands/channel/list.js +233 -0
- package/dist/commands/channel/list.js.map +1 -0
- package/dist/commands/channel/messages.d.ts +15 -0
- package/dist/commands/channel/messages.d.ts.map +1 -0
- package/dist/commands/channel/messages.js +245 -0
- package/dist/commands/channel/messages.js.map +1 -0
- package/dist/commands/channel/rm.d.ts +27 -0
- package/dist/commands/channel/rm.d.ts.map +1 -0
- package/dist/commands/channel/rm.js +216 -0
- package/dist/commands/channel/rm.js.map +1 -0
- package/dist/commands/channel/run.d.ts +30 -0
- package/dist/commands/channel/run.d.ts.map +1 -0
- package/dist/commands/channel/run.js +130 -0
- package/dist/commands/channel/run.js.map +1 -0
- package/dist/commands/channel/send.d.ts +11 -0
- package/dist/commands/channel/send.d.ts.map +1 -0
- package/dist/commands/channel/send.js +24 -0
- package/dist/commands/channel/send.js.map +1 -0
- package/dist/commands/channel/spawn.d.ts +43 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -0
- package/dist/commands/channel/spawn.js +247 -0
- package/dist/commands/channel/spawn.js.map +1 -0
- package/dist/commands/channel/store/events.d.ts +22 -0
- package/dist/commands/channel/store/events.d.ts.map +1 -0
- package/dist/commands/channel/store/events.js +43 -0
- package/dist/commands/channel/store/events.js.map +1 -0
- package/dist/commands/channel/store/filter.d.ts +3 -0
- package/dist/commands/channel/store/filter.d.ts.map +1 -0
- package/dist/commands/channel/store/filter.js +2 -0
- package/dist/commands/channel/store/filter.js.map +1 -0
- package/dist/commands/channel/store/lock.d.ts +23 -0
- package/dist/commands/channel/store/lock.d.ts.map +1 -0
- package/dist/commands/channel/store/lock.js +99 -0
- package/dist/commands/channel/store/lock.js.map +1 -0
- package/dist/commands/channel/store/paths.d.ts +77 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -0
- package/dist/commands/channel/store/paths.js +272 -0
- package/dist/commands/channel/store/paths.js.map +1 -0
- package/dist/commands/channel/store/schema.d.ts +27 -0
- package/dist/commands/channel/store/schema.d.ts.map +1 -0
- package/dist/commands/channel/store/schema.js +34 -0
- package/dist/commands/channel/store/schema.js.map +1 -0
- package/dist/commands/channel/store/thread-state.d.ts +5 -0
- package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
- package/dist/commands/channel/store/thread-state.js +16 -0
- package/dist/commands/channel/store/thread-state.js.map +1 -0
- package/dist/commands/channel/store/watch.d.ts +19 -0
- package/dist/commands/channel/store/watch.d.ts.map +1 -0
- package/dist/commands/channel/store/watch.js +153 -0
- package/dist/commands/channel/store/watch.js.map +1 -0
- package/dist/commands/channel/supervisor/idle.d.ts +45 -0
- package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/idle.js +70 -0
- package/dist/commands/channel/supervisor/idle.js.map +1 -0
- package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
- package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/inbox.js +160 -0
- package/dist/commands/channel/supervisor/inbox.js.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.js +146 -0
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
- package/dist/commands/channel/supervisor/stdout.d.ts +71 -0
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/stdout.js +229 -0
- package/dist/commands/channel/supervisor/stdout.js.map +1 -0
- package/dist/commands/channel/supervisor/turns.d.ts +31 -0
- package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/turns.js +45 -0
- package/dist/commands/channel/supervisor/turns.js.map +1 -0
- package/dist/commands/channel/supervisor/warning.d.ts +48 -0
- package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/warning.js +77 -0
- package/dist/commands/channel/supervisor/warning.js.map +1 -0
- package/dist/commands/channel/supervisor.d.ts +91 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -0
- package/dist/commands/channel/supervisor.js +463 -0
- package/dist/commands/channel/supervisor.js.map +1 -0
- package/dist/commands/channel/text-body.d.ts +13 -0
- package/dist/commands/channel/text-body.d.ts.map +1 -0
- package/dist/commands/channel/text-body.js +47 -0
- package/dist/commands/channel/text-body.js.map +1 -0
- package/dist/commands/channel/threads.d.ts +39 -0
- package/dist/commands/channel/threads.d.ts.map +1 -0
- package/dist/commands/channel/threads.js +107 -0
- package/dist/commands/channel/threads.js.map +1 -0
- package/dist/commands/channel/title.d.ts +12 -0
- package/dist/commands/channel/title.d.ts.map +1 -0
- package/dist/commands/channel/title.js +24 -0
- package/dist/commands/channel/title.js.map +1 -0
- package/dist/commands/channel/wait.d.ts +17 -0
- package/dist/commands/channel/wait.d.ts.map +1 -0
- package/dist/commands/channel/wait.js +75 -0
- package/dist/commands/channel/wait.js.map +1 -0
- package/dist/commands/init.d.ts +71 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1631 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/integration.d.ts +23 -0
- package/dist/commands/integration.d.ts.map +1 -0
- package/dist/commands/integration.js +373 -0
- package/dist/commands/integration.js.map +1 -0
- package/dist/commands/mem.d.ts +30 -0
- package/dist/commands/mem.d.ts.map +1 -0
- package/dist/commands/mem.js +424 -0
- package/dist/commands/mem.js.map +1 -0
- package/dist/commands/uninstall.d.ts +36 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +205 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +104 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +2184 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/upgrade.d.ts +28 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +86 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/workflow.d.ts +35 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +233 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/configurators/antigravity.d.ts +7 -0
- package/dist/configurators/antigravity.d.ts.map +1 -0
- package/dist/configurators/antigravity.js +11 -0
- package/dist/configurators/antigravity.js.map +1 -0
- package/dist/configurators/claude.d.ts +23 -0
- package/dist/configurators/claude.d.ts.map +1 -0
- package/dist/configurators/claude.js +124 -0
- package/dist/configurators/claude.js.map +1 -0
- package/dist/configurators/codebuddy.d.ts +10 -0
- package/dist/configurators/codebuddy.d.ts.map +1 -0
- package/dist/configurators/codebuddy.js +24 -0
- package/dist/configurators/codebuddy.js.map +1 -0
- package/dist/configurators/codex.d.ts +57 -0
- package/dist/configurators/codex.d.ts.map +1 -0
- package/dist/configurators/codex.js +170 -0
- package/dist/configurators/codex.js.map +1 -0
- package/dist/configurators/copilot.d.ts +14 -0
- package/dist/configurators/copilot.d.ts.map +1 -0
- package/dist/configurators/copilot.js +46 -0
- package/dist/configurators/copilot.js.map +1 -0
- package/dist/configurators/cursor.d.ts +10 -0
- package/dist/configurators/cursor.d.ts.map +1 -0
- package/dist/configurators/cursor.js +23 -0
- package/dist/configurators/cursor.js.map +1 -0
- package/dist/configurators/devin.d.ts +8 -0
- package/dist/configurators/devin.d.ts.map +1 -0
- package/dist/configurators/devin.js +12 -0
- package/dist/configurators/devin.js.map +1 -0
- package/dist/configurators/droid.d.ts +10 -0
- package/dist/configurators/droid.d.ts.map +1 -0
- package/dist/configurators/droid.js +24 -0
- package/dist/configurators/droid.js.map +1 -0
- package/dist/configurators/dsh.d.ts +29 -0
- package/dist/configurators/dsh.d.ts.map +1 -0
- package/dist/configurators/dsh.js +64 -0
- package/dist/configurators/dsh.js.map +1 -0
- package/dist/configurators/gemini.d.ts +18 -0
- package/dist/configurators/gemini.d.ts.map +1 -0
- package/dist/configurators/gemini.js +39 -0
- package/dist/configurators/gemini.js.map +1 -0
- package/dist/configurators/grok.d.ts +17 -0
- package/dist/configurators/grok.d.ts.map +1 -0
- package/dist/configurators/grok.js +38 -0
- package/dist/configurators/grok.js.map +1 -0
- package/dist/configurators/index.d.ts +68 -0
- package/dist/configurators/index.d.ts.map +1 -0
- package/dist/configurators/index.js +191 -0
- package/dist/configurators/index.js.map +1 -0
- package/dist/configurators/kilo.d.ts +7 -0
- package/dist/configurators/kilo.d.ts.map +1 -0
- package/dist/configurators/kilo.js +11 -0
- package/dist/configurators/kilo.js.map +1 -0
- package/dist/configurators/kimi.d.ts +26 -0
- package/dist/configurators/kimi.d.ts.map +1 -0
- package/dist/configurators/kimi.js +74 -0
- package/dist/configurators/kimi.js.map +1 -0
- package/dist/configurators/kiro.d.ts +11 -0
- package/dist/configurators/kiro.d.ts.map +1 -0
- package/dist/configurators/kiro.js +30 -0
- package/dist/configurators/kiro.js.map +1 -0
- package/dist/configurators/omp.d.ts +6 -0
- package/dist/configurators/omp.d.ts.map +1 -0
- package/dist/configurators/omp.js +28 -0
- package/dist/configurators/omp.js.map +1 -0
- package/dist/configurators/opencode.d.ts +5 -0
- package/dist/configurators/opencode.d.ts.map +1 -0
- package/dist/configurators/opencode.js +81 -0
- package/dist/configurators/opencode.js.map +1 -0
- package/dist/configurators/pi.d.ts +5 -0
- package/dist/configurators/pi.d.ts.map +1 -0
- package/dist/configurators/pi.js +39 -0
- package/dist/configurators/pi.js.map +1 -0
- package/dist/configurators/qoder.d.ts +12 -0
- package/dist/configurators/qoder.d.ts.map +1 -0
- package/dist/configurators/qoder.js +30 -0
- package/dist/configurators/qoder.js.map +1 -0
- package/dist/configurators/reasonix.d.ts +17 -0
- package/dist/configurators/reasonix.d.ts.map +1 -0
- package/dist/configurators/reasonix.js +37 -0
- package/dist/configurators/reasonix.js.map +1 -0
- package/dist/configurators/shared.d.ts +187 -0
- package/dist/configurators/shared.d.ts.map +1 -0
- package/dist/configurators/shared.js +589 -0
- package/dist/configurators/shared.js.map +1 -0
- package/dist/configurators/snow.d.ts +25 -0
- package/dist/configurators/snow.d.ts.map +1 -0
- package/dist/configurators/snow.js +69 -0
- package/dist/configurators/snow.js.map +1 -0
- package/dist/configurators/trae.d.ts +16 -0
- package/dist/configurators/trae.d.ts.map +1 -0
- package/dist/configurators/trae.js +34 -0
- package/dist/configurators/trae.js.map +1 -0
- package/dist/configurators/workflow.d.ts +53 -0
- package/dist/configurators/workflow.d.ts.map +1 -0
- package/dist/configurators/workflow.js +212 -0
- package/dist/configurators/workflow.js.map +1 -0
- package/dist/configurators/zcode.d.ts +25 -0
- package/dist/configurators/zcode.d.ts.map +1 -0
- package/dist/configurators/zcode.js +67 -0
- package/dist/configurators/zcode.js.map +1 -0
- package/dist/constants/paths.d.ts +74 -0
- package/dist/constants/paths.d.ts.map +1 -0
- package/dist/constants/paths.js +83 -0
- package/dist/constants/paths.js.map +1 -0
- package/dist/constants/version.d.ts +12 -0
- package/dist/constants/version.d.ts.map +1 -0
- package/dist/constants/version.js +18 -0
- package/dist/constants/version.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +62 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +190 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/manifests/0.1.9.json +30 -0
- package/dist/migrations/manifests/0.2.0.json +49 -0
- package/dist/migrations/manifests/0.2.12.json +9 -0
- package/dist/migrations/manifests/0.2.13.json +9 -0
- package/dist/migrations/manifests/0.2.14.json +175 -0
- package/dist/migrations/manifests/0.2.15.json +33 -0
- package/dist/migrations/manifests/0.3.0-beta.0.json +297 -0
- package/dist/migrations/manifests/0.3.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.6.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.7.json +11 -0
- package/dist/migrations/manifests/0.3.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.9.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.1.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.2.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.3.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.4.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.5.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.6.json +9 -0
- package/dist/migrations/manifests/0.3.0.json +11 -0
- package/dist/migrations/manifests/0.3.1.json +9 -0
- package/dist/migrations/manifests/0.3.10.json +9 -0
- package/dist/migrations/manifests/0.3.2.json +9 -0
- package/dist/migrations/manifests/0.3.3.json +9 -0
- package/dist/migrations/manifests/0.3.4.json +21 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/migrations/manifests/0.3.7.json +9 -0
- package/dist/migrations/manifests/0.3.8.json +9 -0
- package/dist/migrations/manifests/0.3.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
- package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
- package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
- package/dist/migrations/manifests/0.4.0.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.0.json +1646 -0
- package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
- package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.5.json +222 -0
- package/dist/migrations/manifests/0.5.0-beta.6.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
- package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.3.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.4.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.5.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.6.json +9 -0
- package/dist/migrations/manifests/0.5.0-rc.7.json +9 -0
- package/dist/migrations/manifests/0.5.0.json +9 -0
- package/dist/migrations/manifests/0.5.1.json +9 -0
- package/dist/migrations/manifests/0.5.10.json +9 -0
- package/dist/migrations/manifests/0.5.11.json +16 -0
- package/dist/migrations/manifests/0.5.12.json +9 -0
- package/dist/migrations/manifests/0.5.13.json +9 -0
- package/dist/migrations/manifests/0.5.14.json +9 -0
- package/dist/migrations/manifests/0.5.15.json +9 -0
- package/dist/migrations/manifests/0.5.16.json +9 -0
- package/dist/migrations/manifests/0.5.17.json +9 -0
- package/dist/migrations/manifests/0.5.18.json +9 -0
- package/dist/migrations/manifests/0.5.19.json +9 -0
- package/dist/migrations/manifests/0.5.2.json +9 -0
- package/dist/migrations/manifests/0.5.3.json +9 -0
- package/dist/migrations/manifests/0.5.4.json +9 -0
- package/dist/migrations/manifests/0.5.5.json +9 -0
- package/dist/migrations/manifests/0.5.6.json +9 -0
- package/dist/migrations/manifests/0.5.7.json +16 -0
- package/dist/migrations/manifests/0.5.8.json +9 -0
- package/dist/migrations/manifests/0.5.9.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.0.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.21.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.22.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.23.json +88 -0
- package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
- package/dist/migrations/manifests/0.6.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.6.0.json +1 -0
- package/dist/migrations/manifests/0.6.1.json +9 -0
- package/dist/migrations/manifests/0.6.10.json +9 -0
- package/dist/migrations/manifests/0.6.11.json +9 -0
- package/dist/migrations/manifests/0.6.12.json +9 -0
- package/dist/migrations/manifests/0.6.13.json +9 -0
- package/dist/migrations/manifests/0.6.14.json +9 -0
- package/dist/migrations/manifests/0.6.15.json +9 -0
- package/dist/migrations/manifests/0.6.16.json +9 -0
- package/dist/migrations/manifests/0.6.2.json +9 -0
- package/dist/migrations/manifests/0.6.3.json +24 -0
- package/dist/migrations/manifests/0.6.4.json +9 -0
- package/dist/migrations/manifests/0.6.5.json +9 -0
- package/dist/migrations/manifests/0.6.6.json +17 -0
- package/dist/migrations/manifests/0.6.7.json +9 -0
- package/dist/migrations/manifests/0.6.8.json +17 -0
- package/dist/migrations/manifests/0.6.9.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.0.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.3.json +9 -0
- package/dist/templates/claude/agents/trellis-check.md +115 -0
- package/dist/templates/claude/agents/trellis-implement.md +110 -0
- package/dist/templates/claude/agents/trellis-research.md +137 -0
- package/dist/templates/claude/hooks/statusline.py +332 -0
- package/dist/templates/claude/index.d.ts +31 -0
- package/dist/templates/claude/index.d.ts.map +1 -0
- package/dist/templates/claude/index.js +58 -0
- package/dist/templates/claude/index.js.map +1 -0
- package/dist/templates/claude/settings.json +73 -0
- package/dist/templates/codebuddy/agents/trellis-check.md +115 -0
- package/dist/templates/codebuddy/agents/trellis-implement.md +110 -0
- package/dist/templates/codebuddy/agents/trellis-research.md +137 -0
- package/dist/templates/codebuddy/index.d.ts +15 -0
- package/dist/templates/codebuddy/index.d.ts.map +1 -0
- package/dist/templates/codebuddy/index.js +15 -0
- package/dist/templates/codebuddy/index.js.map +1 -0
- package/dist/templates/codebuddy/settings.json +69 -0
- package/dist/templates/codex/agents/trellis-check.toml +56 -0
- package/dist/templates/codex/agents/trellis-implement.toml +37 -0
- package/dist/templates/codex/agents/trellis-research.toml +78 -0
- package/dist/templates/codex/config.toml +39 -0
- package/dist/templates/codex/hooks/session-start.py +552 -0
- package/dist/templates/codex/hooks.json +27 -0
- package/dist/templates/codex/index.d.ts +35 -0
- package/dist/templates/codex/index.d.ts.map +1 -0
- package/dist/templates/codex/index.js +68 -0
- package/dist/templates/codex/index.js.map +1 -0
- package/dist/templates/command-renderer.d.ts +2 -0
- package/dist/templates/command-renderer.d.ts.map +1 -0
- package/dist/templates/command-renderer.js +13 -0
- package/dist/templates/command-renderer.js.map +1 -0
- package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +67 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +480 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +233 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +226 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +276 -0
- package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +128 -0
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +85 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +56 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +84 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +123 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +65 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +159 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +69 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +130 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +83 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +72 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +112 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +86 -0
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +81 -0
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +65 -0
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +93 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/SKILL.md +41 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +90 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/repository-analysis.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +61 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-writing.md +70 -0
- package/dist/templates/common/commands/continue.md +56 -0
- package/dist/templates/common/commands/finish-work.md +66 -0
- package/dist/templates/common/commands/start.md +59 -0
- package/dist/templates/common/index.d.ts +48 -0
- package/dist/templates/common/index.d.ts.map +1 -0
- package/dist/templates/common/index.js +105 -0
- package/dist/templates/common/index.js.map +1 -0
- package/dist/templates/common/skills/before-dev.md +46 -0
- package/dist/templates/common/skills/brainstorm.md +195 -0
- package/dist/templates/common/skills/break-loop.md +184 -0
- package/dist/templates/common/skills/check.md +106 -0
- package/dist/templates/common/skills/update-spec.md +351 -0
- package/dist/templates/copilot/copilot-instructions.md +11 -0
- package/dist/templates/copilot/hooks/session-start.py +548 -0
- package/dist/templates/copilot/hooks.json +19 -0
- package/dist/templates/copilot/index.d.ts +28 -0
- package/dist/templates/copilot/index.d.ts.map +1 -0
- package/dist/templates/copilot/index.js +62 -0
- package/dist/templates/copilot/index.js.map +1 -0
- package/dist/templates/copilot/prompts/before-dev.prompt.md +39 -0
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +156 -0
- package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
- package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +170 -0
- package/dist/templates/copilot/prompts/check.prompt.md +110 -0
- package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
- package/dist/templates/copilot/prompts/finish-work.prompt.md +99 -0
- package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
- package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
- package/dist/templates/copilot/prompts/parallel.prompt.md +204 -0
- package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
- package/dist/templates/copilot/prompts/start.prompt.md +63 -0
- package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
- package/dist/templates/cursor/agents/trellis-check.md +114 -0
- package/dist/templates/cursor/agents/trellis-implement.md +109 -0
- package/dist/templates/cursor/agents/trellis-research.md +136 -0
- package/dist/templates/cursor/hooks.json +24 -0
- package/dist/templates/cursor/index.d.ts +13 -0
- package/dist/templates/cursor/index.d.ts.map +1 -0
- package/dist/templates/cursor/index.js +13 -0
- package/dist/templates/cursor/index.js.map +1 -0
- package/dist/templates/droid/droids/trellis-check.md +107 -0
- package/dist/templates/droid/droids/trellis-implement.md +102 -0
- package/dist/templates/droid/droids/trellis-research.md +137 -0
- package/dist/templates/droid/index.d.ts +15 -0
- package/dist/templates/droid/index.d.ts.map +1 -0
- package/dist/templates/droid/index.js +15 -0
- package/dist/templates/droid/index.js.map +1 -0
- package/dist/templates/droid/settings.json +69 -0
- package/dist/templates/dsh/DSH.md +56 -0
- package/dist/templates/dsh/index.d.ts +21 -0
- package/dist/templates/dsh/index.d.ts.map +1 -0
- package/dist/templates/dsh/index.js +25 -0
- package/dist/templates/dsh/index.js.map +1 -0
- package/dist/templates/extract.d.ts +40 -0
- package/dist/templates/extract.d.ts.map +1 -0
- package/dist/templates/extract.js +113 -0
- package/dist/templates/extract.js.map +1 -0
- package/dist/templates/gemini/agents/trellis-check.md +107 -0
- package/dist/templates/gemini/agents/trellis-implement.md +102 -0
- package/dist/templates/gemini/agents/trellis-research.md +136 -0
- package/dist/templates/gemini/index.d.ts +13 -0
- package/dist/templates/gemini/index.d.ts.map +1 -0
- package/dist/templates/gemini/index.js +13 -0
- package/dist/templates/gemini/index.js.map +1 -0
- package/dist/templates/gemini/settings.json +40 -0
- package/dist/templates/grok/agents/trellis-check.md +105 -0
- package/dist/templates/grok/agents/trellis-implement.md +116 -0
- package/dist/templates/grok/agents/trellis-research.md +146 -0
- package/dist/templates/grok/index.d.ts +16 -0
- package/dist/templates/grok/index.d.ts.map +1 -0
- package/dist/templates/grok/index.js +19 -0
- package/dist/templates/grok/index.js.map +1 -0
- package/dist/templates/kimi/agents/trellis-check.md +103 -0
- package/dist/templates/kimi/agents/trellis-implement.md +115 -0
- package/dist/templates/kimi/agents/trellis-research.md +144 -0
- package/dist/templates/kimi/index.d.ts +20 -0
- package/dist/templates/kimi/index.d.ts.map +1 -0
- package/dist/templates/kimi/index.js +23 -0
- package/dist/templates/kimi/index.js.map +1 -0
- package/dist/templates/kiro/agents/trellis-check.json +26 -0
- package/dist/templates/kiro/agents/trellis-implement.json +26 -0
- package/dist/templates/kiro/agents/trellis-research.json +30 -0
- package/dist/templates/kiro/agents/trellis.json +34 -0
- package/dist/templates/kiro/hooks/trellis-workflow-state.kiro.hook +14 -0
- package/dist/templates/kiro/index.d.ts +32 -0
- package/dist/templates/kiro/index.d.ts.map +1 -0
- package/dist/templates/kiro/index.js +28 -0
- package/dist/templates/kiro/index.js.map +1 -0
- package/dist/templates/markdown/agents.md +21 -0
- package/dist/templates/markdown/gitignore.txt +15 -0
- package/dist/templates/markdown/index.d.ts +27 -0
- package/dist/templates/markdown/index.d.ts.map +1 -0
- package/dist/templates/markdown/index.js +53 -0
- package/dist/templates/markdown/index.js.map +1 -0
- package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
- package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
- package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
- package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +223 -0
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +327 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +633 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +97 -0
- package/dist/templates/markdown/workspace-index.md +125 -0
- package/dist/templates/markdown/worktree.yaml.txt +58 -0
- package/dist/templates/omp/agents/trellis-check.md +41 -0
- package/dist/templates/omp/agents/trellis-implement.md +45 -0
- package/dist/templates/omp/agents/trellis-research.md +30 -0
- package/dist/templates/omp/extensions/trellis/index.ts.txt +1783 -0
- package/dist/templates/omp/index.d.ts +4 -0
- package/dist/templates/omp/index.d.ts.map +1 -0
- package/dist/templates/omp/index.js +9 -0
- package/dist/templates/omp/index.js.map +1 -0
- package/dist/templates/opencode/agents/trellis-check.md +122 -0
- package/dist/templates/opencode/agents/trellis-implement.md +118 -0
- package/dist/templates/opencode/agents/trellis-research.md +145 -0
- package/dist/templates/opencode/lib/context-visibility.js +148 -0
- package/dist/templates/opencode/lib/session-utils.js +527 -0
- package/dist/templates/opencode/lib/trellis-context.js +627 -0
- package/dist/templates/opencode/package.json +5 -0
- package/dist/templates/opencode/plugins/inject-subagent-context.js +569 -0
- package/dist/templates/opencode/plugins/inject-workflow-state.js +240 -0
- package/dist/templates/opencode/plugins/session-start.js +65 -0
- package/dist/templates/pi/agents/trellis-check.md +37 -0
- package/dist/templates/pi/agents/trellis-implement.md +42 -0
- package/dist/templates/pi/agents/trellis-research.md +25 -0
- package/dist/templates/pi/extensions/trellis/index.ts.txt +2149 -0
- package/dist/templates/pi/index.d.ts +5 -0
- package/dist/templates/pi/index.d.ts.map +1 -0
- package/dist/templates/pi/index.js +12 -0
- package/dist/templates/pi/index.js.map +1 -0
- package/dist/templates/pi/settings.json +9 -0
- package/dist/templates/qoder/agents/trellis-check.md +108 -0
- package/dist/templates/qoder/agents/trellis-implement.md +103 -0
- package/dist/templates/qoder/agents/trellis-research.md +137 -0
- package/dist/templates/qoder/index.d.ts +15 -0
- package/dist/templates/qoder/index.d.ts.map +1 -0
- package/dist/templates/qoder/index.js +15 -0
- package/dist/templates/qoder/index.js.map +1 -0
- package/dist/templates/qoder/settings.json +59 -0
- package/dist/templates/reasonix/agents/trellis-check.md +36 -0
- package/dist/templates/reasonix/agents/trellis-implement.md +41 -0
- package/dist/templates/reasonix/index.d.ts +13 -0
- package/dist/templates/reasonix/index.d.ts.map +1 -0
- package/dist/templates/reasonix/index.js +16 -0
- package/dist/templates/reasonix/index.js.map +1 -0
- package/dist/templates/shared-hooks/index.d.ts +77 -0
- package/dist/templates/shared-hooks/index.d.ts.map +1 -0
- package/dist/templates/shared-hooks/index.js +141 -0
- package/dist/templates/shared-hooks/index.js.map +1 -0
- package/dist/templates/shared-hooks/inject-shell-session-context.py +292 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +1212 -0
- package/dist/templates/shared-hooks/inject-workflow-state.py +488 -0
- package/dist/templates/shared-hooks/session-start.py +949 -0
- package/dist/templates/snow/SNOW.md +86 -0
- package/dist/templates/snow/agents/trellis-check.md +135 -0
- package/dist/templates/snow/agents/trellis-implement.md +153 -0
- package/dist/templates/snow/agents/trellis-research.md +114 -0
- package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
- package/dist/templates/snow/hooks/onSessionStart.json +15 -0
- package/dist/templates/snow/hooks/onUserMessage.json +15 -0
- package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
- package/dist/templates/snow/index.d.ts +26 -0
- package/dist/templates/snow/index.d.ts.map +1 -0
- package/dist/templates/snow/index.js +36 -0
- package/dist/templates/snow/index.js.map +1 -0
- package/dist/templates/template-utils.d.ts +26 -0
- package/dist/templates/template-utils.d.ts.map +1 -0
- package/dist/templates/template-utils.js +65 -0
- package/dist/templates/template-utils.js.map +1 -0
- package/dist/templates/trae/agents/trellis-check.md +108 -0
- package/dist/templates/trae/agents/trellis-implement.md +103 -0
- package/dist/templates/trae/agents/trellis-research.md +137 -0
- package/dist/templates/trae/hooks.json +39 -0
- package/dist/templates/trae/index.d.ts +15 -0
- package/dist/templates/trae/index.d.ts.map +1 -0
- package/dist/templates/trae/index.js +15 -0
- package/dist/templates/trae/index.js.map +1 -0
- package/dist/templates/trellis/agents/check.md +70 -0
- package/dist/templates/trellis/agents/implement.md +71 -0
- package/dist/templates/trellis/config.yaml +177 -0
- package/dist/templates/trellis/gitattributes.txt +9 -0
- package/dist/templates/trellis/gitignore.txt +32 -0
- package/dist/templates/trellis/index.d.ts +67 -0
- package/dist/templates/trellis/index.d.ts.map +1 -0
- package/dist/templates/trellis/index.js +134 -0
- package/dist/templates/trellis/index.js.map +1 -0
- package/dist/templates/trellis/scripts/__init__.py +5 -0
- package/dist/templates/trellis/scripts/add_session.py +1545 -0
- package/dist/templates/trellis/scripts/common/__init__.py +94 -0
- package/dist/templates/trellis/scripts/common/active_task.py +857 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +950 -0
- package/dist/templates/trellis/scripts/common/config.py +482 -0
- package/dist/templates/trellis/scripts/common/developer.py +192 -0
- package/dist/templates/trellis/scripts/common/git.py +203 -0
- package/dist/templates/trellis/scripts/common/git_context.py +106 -0
- package/dist/templates/trellis/scripts/common/io.py +160 -0
- package/dist/templates/trellis/scripts/common/log.py +45 -0
- package/dist/templates/trellis/scripts/common/packages_context.py +238 -0
- package/dist/templates/trellis/scripts/common/paths.py +543 -0
- package/dist/templates/trellis/scripts/common/safe_commit.py +322 -0
- package/dist/templates/trellis/scripts/common/session_context.py +892 -0
- package/dist/templates/trellis/scripts/common/task_context.py +470 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +188 -0
- package/dist/templates/trellis/scripts/common/task_store.py +1883 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +498 -0
- package/dist/templates/trellis/scripts/common/tasks.py +122 -0
- package/dist/templates/trellis/scripts/common/trellis_config.py +257 -0
- package/dist/templates/trellis/scripts/common/types.py +110 -0
- package/dist/templates/trellis/scripts/common/workflow_phase.py +242 -0
- package/dist/templates/trellis/scripts/get_context.py +16 -0
- package/dist/templates/trellis/scripts/get_developer.py +26 -0
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
- package/dist/templates/trellis/scripts/init_developer.py +51 -0
- package/dist/templates/trellis/scripts/task.py +811 -0
- package/dist/templates/trellis/tasks/.gitkeep +0 -0
- package/dist/templates/trellis/workflow.md +721 -0
- package/dist/templates/zcode/agents/trellis-check.md +109 -0
- package/dist/templates/zcode/agents/trellis-implement.md +111 -0
- package/dist/templates/zcode/agents/trellis-research.md +133 -0
- package/dist/templates/zcode/config.json +52 -0
- package/dist/templates/zcode/index.d.ts +25 -0
- package/dist/templates/zcode/index.d.ts.map +1 -0
- package/dist/templates/zcode/index.js +29 -0
- package/dist/templates/zcode/index.js.map +1 -0
- package/dist/types/ai-tools.d.ts +105 -0
- package/dist/types/ai-tools.d.ts.map +1 -0
- package/dist/types/ai-tools.js +502 -0
- package/dist/types/ai-tools.js.map +1 -0
- package/dist/types/migration.d.ts +125 -0
- package/dist/types/migration.d.ts.map +1 -0
- package/dist/types/migration.js +8 -0
- package/dist/types/migration.js.map +1 -0
- package/dist/utils/ablation-store.d.ts +123 -0
- package/dist/utils/ablation-store.d.ts.map +1 -0
- package/dist/utils/ablation-store.js +749 -0
- package/dist/utils/ablation-store.js.map +1 -0
- package/dist/utils/agent-refs.d.ts +31 -0
- package/dist/utils/agent-refs.d.ts.map +1 -0
- package/dist/utils/agent-refs.js +63 -0
- package/dist/utils/agent-refs.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +12 -0
- package/dist/utils/atomic-write.d.ts.map +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/utils/atomic-write.js.map +1 -0
- package/dist/utils/compare-versions.d.ts +12 -0
- package/dist/utils/compare-versions.d.ts.map +1 -0
- package/dist/utils/compare-versions.js +86 -0
- package/dist/utils/compare-versions.js.map +1 -0
- package/dist/utils/cwd-guard.d.ts +38 -0
- package/dist/utils/cwd-guard.d.ts.map +1 -0
- package/dist/utils/cwd-guard.js +63 -0
- package/dist/utils/cwd-guard.js.map +1 -0
- package/dist/utils/file-writer.d.ts +36 -0
- package/dist/utils/file-writer.d.ts.map +1 -0
- package/dist/utils/file-writer.js +204 -0
- package/dist/utils/file-writer.js.map +1 -0
- package/dist/utils/integration.d.ts +96 -0
- package/dist/utils/integration.d.ts.map +1 -0
- package/dist/utils/integration.js +1549 -0
- package/dist/utils/integration.js.map +1 -0
- package/dist/utils/managed-paths.d.ts +5 -0
- package/dist/utils/managed-paths.d.ts.map +1 -0
- package/dist/utils/managed-paths.js +51 -0
- package/dist/utils/managed-paths.js.map +1 -0
- package/dist/utils/managed-removal.d.ts +74 -0
- package/dist/utils/managed-removal.d.ts.map +1 -0
- package/dist/utils/managed-removal.js +270 -0
- package/dist/utils/managed-removal.js.map +1 -0
- package/dist/utils/manifest-prune.d.ts +61 -0
- package/dist/utils/manifest-prune.d.ts.map +1 -0
- package/dist/utils/manifest-prune.js +135 -0
- package/dist/utils/manifest-prune.js.map +1 -0
- package/dist/utils/posix.d.ts +13 -0
- package/dist/utils/posix.d.ts.map +1 -0
- package/dist/utils/posix.js +15 -0
- package/dist/utils/posix.js.map +1 -0
- package/dist/utils/project-detector.d.ts +46 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js +666 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/proxy.d.ts +25 -0
- package/dist/utils/proxy.d.ts.map +1 -0
- package/dist/utils/proxy.js +60 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/registry-config.d.ts +7 -0
- package/dist/utils/registry-config.d.ts.map +1 -0
- package/dist/utils/registry-config.js +173 -0
- package/dist/utils/registry-config.js.map +1 -0
- package/dist/utils/task-json.d.ts +13 -0
- package/dist/utils/task-json.d.ts.map +1 -0
- package/dist/utils/task-json.js +12 -0
- package/dist/utils/task-json.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +161 -0
- package/dist/utils/template-fetcher.d.ts.map +1 -0
- package/dist/utils/template-fetcher.js +1020 -0
- package/dist/utils/template-fetcher.js.map +1 -0
- package/dist/utils/template-hash.d.ts +127 -0
- package/dist/utils/template-hash.d.ts.map +1 -0
- package/dist/utils/template-hash.js +335 -0
- package/dist/utils/template-hash.js.map +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts +66 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
- package/dist/utils/uninstall-scrubbers.js +417 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -0
- package/dist/utils/workflow-resolver.d.ts +86 -0
- package/dist/utils/workflow-resolver.d.ts.map +1 -0
- package/dist/utils/workflow-resolver.js +266 -0
- package/dist/utils/workflow-resolver.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1783 @@
|
|
|
1
|
+
// downstream: 010-omp-task-context-append-only
|
|
2
|
+
|
|
3
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
4
|
+
import { closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, statSync, readSync } from "node:fs";
|
|
5
|
+
import { join, dirname, basename, isAbsolute, relative, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Project root detection
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function findProjectRoot(startDir: string): string | null {
|
|
14
|
+
let current = startDir;
|
|
15
|
+
while (true) {
|
|
16
|
+
const marker = join(current, ".trellis");
|
|
17
|
+
if (existsSync(marker) && statSync(marker).isDirectory()) return current;
|
|
18
|
+
const parent = dirname(current);
|
|
19
|
+
if (parent === current) break;
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
type RuntimeIntegrationPolicy = { enabled: boolean; hooks: boolean };
|
|
25
|
+
|
|
26
|
+
function hooksDisabledByEnv(): boolean {
|
|
27
|
+
return process.env.TRELLIS_HOOKS === "0" || process.env.TRELLIS_DISABLE_HOOKS === "1";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseIntegrationBool(raw: string, key: string): boolean {
|
|
31
|
+
const value = stripInlineComment(raw).trim();
|
|
32
|
+
if (value === "true") return true;
|
|
33
|
+
if (value === "false") return false;
|
|
34
|
+
throw new Error(
|
|
35
|
+
`[trellis] Invalid integration boolean for ${key}: ${JSON.stringify(value)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readIntegrationPolicy(
|
|
40
|
+
root: string,
|
|
41
|
+
platform: "pi" | "omp",
|
|
42
|
+
): RuntimeIntegrationPolicy {
|
|
43
|
+
let masterEnabled = true;
|
|
44
|
+
let sharedHooks = true;
|
|
45
|
+
let platformEnabled = true;
|
|
46
|
+
let platformHooks: boolean | undefined;
|
|
47
|
+
const configPath = join(root, ".trellis", "config.yaml");
|
|
48
|
+
if (!existsSync(configPath)) {
|
|
49
|
+
return { enabled: true, hooks: !hooksDisabledByEnv() };
|
|
50
|
+
}
|
|
51
|
+
let content: string;
|
|
52
|
+
try {
|
|
53
|
+
content = readFileSync(configPath, "utf-8");
|
|
54
|
+
} catch {
|
|
55
|
+
return { enabled: true, hooks: !hooksDisabledByEnv() };
|
|
56
|
+
}
|
|
57
|
+
const stack: Array<{ indent: number; key: string }> = [];
|
|
58
|
+
const assign = (parents: string[], key: string, raw: string): void => {
|
|
59
|
+
const path = [...parents, key];
|
|
60
|
+
const parsed = parseIntegrationBool(raw, `integration.${path.slice(1).join(".")}`);
|
|
61
|
+
if (path.length === 2) {
|
|
62
|
+
if (key === "enabled") masterEnabled = parsed;
|
|
63
|
+
else if (key === "hooks") sharedHooks = parsed;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (path.length === 4 && path[1] === "platforms") {
|
|
67
|
+
if (path[2] !== platform) return;
|
|
68
|
+
if (key === "enabled") platformEnabled = parsed;
|
|
69
|
+
else if (key === "hooks") platformHooks = parsed;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
73
|
+
const line = rawLine.replace(/\r$/, "");
|
|
74
|
+
const uncommented = stripInlineComment(line);
|
|
75
|
+
const trimmed = uncommented.trim();
|
|
76
|
+
if (!trimmed) continue;
|
|
77
|
+
const colon = trimmed.indexOf(":");
|
|
78
|
+
if (colon <= 0) continue;
|
|
79
|
+
const key = trimmed.slice(0, colon).trim();
|
|
80
|
+
const rawValue = trimmed.slice(colon + 1).trim();
|
|
81
|
+
const indent = line.length - line.trimStart().length;
|
|
82
|
+
while (stack.length > 0 && stack[stack.length - 1]!.indent >= indent) stack.pop();
|
|
83
|
+
const parents = stack.map((entry) => entry.key);
|
|
84
|
+
if (parents[0] !== "integration") {
|
|
85
|
+
if (key === "integration" && rawValue) {
|
|
86
|
+
throw new Error(`[trellis] Unsupported integration mapping syntax: ${trimmed}`);
|
|
87
|
+
}
|
|
88
|
+
if (!rawValue) stack.push({ indent, key });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const integrationBoolKey =
|
|
92
|
+
(parents.length === 1 &&
|
|
93
|
+
(key === "enabled" || key === "hooks" || key === "skills")) ||
|
|
94
|
+
(parents.length === 3 &&
|
|
95
|
+
parents[1] === "platforms" &&
|
|
96
|
+
(key === "enabled" || key === "hooks" || key === "skills"));
|
|
97
|
+
const integrationMapping =
|
|
98
|
+
integrationBoolKey ||
|
|
99
|
+
(parents.length === 1 && key === "platforms") ||
|
|
100
|
+
(parents.length === 2 && parents[1] === "platforms");
|
|
101
|
+
if (!integrationMapping) {
|
|
102
|
+
throw new Error(`[trellis] Invalid integration entry: ${trimmed}`);
|
|
103
|
+
}
|
|
104
|
+
if (!rawValue) {
|
|
105
|
+
if (integrationBoolKey) assign(parents, key, rawValue);
|
|
106
|
+
stack.push({ indent, key });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (integrationBoolKey) {
|
|
110
|
+
assign(parents, key, rawValue);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (parents.length === 2 && parents[1] === "platforms") {
|
|
114
|
+
throw new Error(`[trellis] Invalid integration platform entry: ${trimmed}`);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`[trellis] Invalid integration entry: ${trimmed}`);
|
|
117
|
+
}
|
|
118
|
+
const enabled = masterEnabled && platformEnabled;
|
|
119
|
+
return {
|
|
120
|
+
enabled,
|
|
121
|
+
hooks: enabled && (platformHooks ?? sharedHooks) && !hooksDisabledByEnv(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function policyForContext(ctx?: { cwd?: string }): RuntimeIntegrationPolicy {
|
|
125
|
+
const root = findProjectRoot(ctx?.cwd ?? process.cwd());
|
|
126
|
+
return root
|
|
127
|
+
? readIntegrationPolicy(root, "omp")
|
|
128
|
+
: { enabled: true, hooks: !hooksDisabledByEnv() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Session identity helpers (mirrors Python _sanitize_key / _hash_value / _context_key)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function sanitizeKey(raw: string): string {
|
|
136
|
+
const safe = raw.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._-]+|[._-]+$/g, "");
|
|
137
|
+
return safe ? safe.slice(0, 160) : "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hashValue(raw: string): string {
|
|
141
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildContextKey(platformName: string, kind: string, value: string): string {
|
|
145
|
+
if (kind === "transcript") {
|
|
146
|
+
return `${platformName}_transcript_${hashValue(value)}`;
|
|
147
|
+
}
|
|
148
|
+
const safeValue = sanitizeKey(value);
|
|
149
|
+
return safeValue ? `${platformName}_${safeValue}` : `${platformName}_${hashValue(value)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function deriveContextKey(ctx?: { sessionManager?: { getSessionId?: () => string | undefined; getSessionFile?: () => string | undefined } }): string | null {
|
|
153
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
154
|
+
if (sessionId) {
|
|
155
|
+
return buildContextKey("omp", "session", sessionId);
|
|
156
|
+
}
|
|
157
|
+
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
158
|
+
if (sessionFile) {
|
|
159
|
+
return buildContextKey("omp", "transcript", sessionFile);
|
|
160
|
+
}
|
|
161
|
+
const override = process.env.TRELLIS_CONTEXT_ID?.trim();
|
|
162
|
+
return override ? sanitizeKey(override) || hashValue(override) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isInsideRoot(root: string, candidate: string): boolean {
|
|
166
|
+
const rel = relative(root, candidate);
|
|
167
|
+
return rel === "" || (rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Trusted context roots (mirrors packages/cli/src/commands/channel/context-trust.ts;
|
|
172
|
+
// standalone copy since templates don't import from the CLI package).
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
const AUTO_TRUST_ENTRIES = ["tasks", "workspace"];
|
|
176
|
+
|
|
177
|
+
function stripTrustValue(s: string): string {
|
|
178
|
+
return s.trim().replace(/\s*#.*$/, "").trim().replace(/^['"]|['"]$/g, "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseChannelTrustSection(content: string): { trustedDirs: string[]; autoTrustSymlinks?: boolean } {
|
|
182
|
+
const lines = content.split("\n");
|
|
183
|
+
const trustedDirs: string[] = [];
|
|
184
|
+
let autoTrustSymlinks: boolean | undefined;
|
|
185
|
+
let inChannel = false;
|
|
186
|
+
let inList = false;
|
|
187
|
+
|
|
188
|
+
for (const raw of lines) {
|
|
189
|
+
const line = raw.replace(/\r$/, "");
|
|
190
|
+
const trimmed = line.trimEnd();
|
|
191
|
+
if (trimmed.trim().startsWith("#")) continue;
|
|
192
|
+
|
|
193
|
+
if (/^channel:\s*$/.test(trimmed)) {
|
|
194
|
+
inChannel = true;
|
|
195
|
+
inList = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!inChannel) continue;
|
|
199
|
+
|
|
200
|
+
if (trimmed.trim() !== "" && /^\S/.test(line)) {
|
|
201
|
+
inChannel = false;
|
|
202
|
+
inList = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (trimmed.trim() === "") continue;
|
|
206
|
+
|
|
207
|
+
if (inList) {
|
|
208
|
+
const item = trimmed.match(/^ {4}-\s*(.+)$/);
|
|
209
|
+
if (item) {
|
|
210
|
+
const val = stripTrustValue(item[1]!);
|
|
211
|
+
if (val) trustedDirs.push(val);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
inList = false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (/^ {2}trusted_context_dirs:\s*$/.test(trimmed)) {
|
|
218
|
+
inList = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const boolMatch = trimmed.match(/^ {2}auto_trust_trellis_symlinks:\s*(.+)$/);
|
|
223
|
+
if (boolMatch) {
|
|
224
|
+
const val = stripTrustValue(boolMatch[1]!).toLowerCase();
|
|
225
|
+
if (val === "false") autoTrustSymlinks = false;
|
|
226
|
+
else if (val === "true") autoTrustSymlinks = true;
|
|
227
|
+
else process.stderr.write(`[channel] channel.auto_trust_trellis_symlinks: invalid value '${val}', ignoring\n`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { trustedDirs, autoTrustSymlinks };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveTrustedRoots(projectRoot: string): string[] {
|
|
236
|
+
const configPath = join(projectRoot, ".trellis", "config.yaml");
|
|
237
|
+
let config: { trustedDirs: string[]; autoTrustSymlinks?: boolean } = { trustedDirs: [] };
|
|
238
|
+
if (existsSync(configPath)) {
|
|
239
|
+
try {
|
|
240
|
+
config = parseChannelTrustSection(readFileSync(configPath, "utf-8"));
|
|
241
|
+
} catch {
|
|
242
|
+
// ignore
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const roots: string[] = [];
|
|
247
|
+
for (const entry of config.trustedDirs) {
|
|
248
|
+
try {
|
|
249
|
+
roots.push(realpathSync(resolve(projectRoot, entry)));
|
|
250
|
+
} catch {
|
|
251
|
+
// entry not found or invalid — skip
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (config.autoTrustSymlinks !== false) {
|
|
256
|
+
for (const entryName of AUTO_TRUST_ENTRIES) {
|
|
257
|
+
const entryPath = join(projectRoot, ".trellis", entryName);
|
|
258
|
+
try {
|
|
259
|
+
if (lstatSync(entryPath).isSymbolicLink()) {
|
|
260
|
+
roots.push(realpathSync(entryPath));
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// missing / broken symlink — nothing to trust
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return [...new Set(roots)];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resolveProjectFile(
|
|
272
|
+
projectRoot: string,
|
|
273
|
+
file: string,
|
|
274
|
+
trustedRoots: string[],
|
|
275
|
+
): string | null {
|
|
276
|
+
try {
|
|
277
|
+
const rootReal = realpathSync(projectRoot);
|
|
278
|
+
const targetReal = realpathSync(resolve(projectRoot, file));
|
|
279
|
+
if (isInsideRoot(rootReal, targetReal)) return targetReal;
|
|
280
|
+
if (trustedRoots.some((root) => isInsideRoot(root, targetReal))) return targetReal;
|
|
281
|
+
return null;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function displayProjectPath(projectRoot: string, filePath: string, taskDir?: string): string {
|
|
288
|
+
const direct = relative(projectRoot, filePath).split("\\").join("/");
|
|
289
|
+
if (direct && !direct.startsWith("../") && !isAbsolute(direct)) return direct;
|
|
290
|
+
if (taskDir) {
|
|
291
|
+
const taskRelative = relative(projectRoot, taskDir).split("\\").join("/");
|
|
292
|
+
const taskLabel = taskRelative && !taskRelative.startsWith("../") && !isAbsolute(taskRelative)
|
|
293
|
+
? taskRelative
|
|
294
|
+
: `.trellis/tasks/${basename(taskDir)}`;
|
|
295
|
+
const withinTask = relative(taskDir, filePath).split("\\").join("/");
|
|
296
|
+
if (withinTask && !withinTask.startsWith("../") && !isAbsolute(withinTask)) {
|
|
297
|
+
return `${taskLabel}/${withinTask}`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
return relative(realpathSync(projectRoot), realpathSync(filePath)).split("\\").join("/");
|
|
302
|
+
} catch {
|
|
303
|
+
return relative(projectRoot, filePath).split("\\").join("/");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Active task resolution
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
function resolveActiveTaskStatus(
|
|
312
|
+
projectRoot: string,
|
|
313
|
+
contextKey: string | null,
|
|
314
|
+
): { status: string; taskDir: string | null; taskTitle: string | null } {
|
|
315
|
+
const sessionsDir = join(projectRoot, ".trellis", ".runtime", "sessions");
|
|
316
|
+
if (!existsSync(sessionsDir)) return { status: "no_task", taskDir: null, taskTitle: null };
|
|
317
|
+
|
|
318
|
+
// --- 通过 context key 解析 session 文件 ---
|
|
319
|
+
let sessionFilePath: string | null = null;
|
|
320
|
+
|
|
321
|
+
if (contextKey) {
|
|
322
|
+
const candidate = join(sessionsDir, `${contextKey}.json`);
|
|
323
|
+
if (existsSync(candidate)) {
|
|
324
|
+
sessionFilePath = candidate;
|
|
325
|
+
} else {
|
|
326
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// No identity: use single-session fallback only when there is exactly one session file.
|
|
330
|
+
let sessionFiles: string[];
|
|
331
|
+
try {
|
|
332
|
+
sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
333
|
+
} catch {
|
|
334
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
335
|
+
}
|
|
336
|
+
if (sessionFiles.length === 1) {
|
|
337
|
+
sessionFilePath = join(sessionsDir, sessionFiles[0]);
|
|
338
|
+
} else {
|
|
339
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- 读取 session 数据 ---
|
|
344
|
+
let sessionData: Record<string, unknown>;
|
|
345
|
+
try {
|
|
346
|
+
sessionData = JSON.parse(readFileSync(sessionFilePath, "utf-8"));
|
|
347
|
+
} catch {
|
|
348
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const currentTask = sessionData.current_task;
|
|
352
|
+
if (typeof currentTask !== "string" || !currentTask)
|
|
353
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
354
|
+
|
|
355
|
+
// Same jail the jsonl-referenced files already go through below. `task.py`
|
|
356
|
+
// now refuses to store a ref that leaves the project, but a session file
|
|
357
|
+
// written before that fix can still hold one, and `trellis update` does not
|
|
358
|
+
// rewrite session files — so a poisoned pointer outlives the upgrade that
|
|
359
|
+
// closed the writer.
|
|
360
|
+
const taskDir = resolveProjectFile(projectRoot, currentTask, resolveTrustedRoots(projectRoot));
|
|
361
|
+
if (!taskDir) return { status: "no_task", taskDir: null, taskTitle: null };
|
|
362
|
+
const taskJsonPath = join(taskDir, "task.json");
|
|
363
|
+
if (!existsSync(taskJsonPath)) return { status: "no_task", taskDir: null, taskTitle: null };
|
|
364
|
+
|
|
365
|
+
let taskData: Record<string, unknown>;
|
|
366
|
+
try {
|
|
367
|
+
taskData = JSON.parse(readFileSync(taskJsonPath, "utf-8"));
|
|
368
|
+
} catch {
|
|
369
|
+
return { status: "no_task", taskDir: null, taskTitle: null };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
status: typeof taskData.status === "string" ? taskData.status : "planning",
|
|
374
|
+
taskDir,
|
|
375
|
+
taskTitle: typeof taskData.title === "string" ? taskData.title : null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Session context — spawns get_context.py default mode (same as Claude hook)
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
const SESSION_CONTEXT_TIMEOUT_MS = 5000;
|
|
384
|
+
|
|
385
|
+
function buildSessionContext(projectRoot: string, contextKey: string | null): string {
|
|
386
|
+
const script = join(projectRoot, ".trellis", "scripts", "get_context.py");
|
|
387
|
+
if (!existsSync(script)) return "";
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const result = spawnSync("python3", [script], {
|
|
391
|
+
cwd: projectRoot,
|
|
392
|
+
encoding: "utf-8",
|
|
393
|
+
env: contextKey
|
|
394
|
+
? { ...process.env, TRELLIS_CONTEXT_ID: contextKey }
|
|
395
|
+
: process.env,
|
|
396
|
+
timeout: SESSION_CONTEXT_TIMEOUT_MS,
|
|
397
|
+
windowsHide: true,
|
|
398
|
+
});
|
|
399
|
+
if (result.status !== 0 || !result.stdout?.trim()) {
|
|
400
|
+
return "";
|
|
401
|
+
}
|
|
402
|
+
return `<session-context>\n${result.stdout.trim()}\n</session-context>`;
|
|
403
|
+
} catch {
|
|
404
|
+
return "";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Task context — prd.md, info.md, and jsonl-referenced spec/research files
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
type AgentType = "trellis-implement" | "trellis-check" | "trellis-research" | null;
|
|
413
|
+
|
|
414
|
+
function taskContextJsonlNames(agentType?: AgentType): string[] {
|
|
415
|
+
if (agentType === "trellis-implement") return ["implement.jsonl"];
|
|
416
|
+
if (agentType === "trellis-check") return ["check.jsonl"];
|
|
417
|
+
if (agentType === "trellis-research") return [];
|
|
418
|
+
return ["implement.jsonl", "check.jsonl"];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function taskContextInputPaths(projectRoot: string, taskDir: string, agentType?: AgentType): string[] {
|
|
422
|
+
const trustedRoots = resolveTrustedRoots(projectRoot);
|
|
423
|
+
const paths = new Set<string>([
|
|
424
|
+
join(projectRoot, ".trellis", "config.yaml"),
|
|
425
|
+
join(taskDir, "prd.md"),
|
|
426
|
+
join(taskDir, "info.md"),
|
|
427
|
+
]);
|
|
428
|
+
for (const jsonlName of taskContextJsonlNames(agentType)) {
|
|
429
|
+
const jsonlPath = join(taskDir, jsonlName);
|
|
430
|
+
paths.add(jsonlPath);
|
|
431
|
+
if (!existsSync(jsonlPath)) continue;
|
|
432
|
+
const displayPath = displayProjectPath(projectRoot, jsonlPath, taskDir);
|
|
433
|
+
const { lines } = readJsonlLines(jsonlPath, displayPath);
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
try {
|
|
436
|
+
const row = JSON.parse(line.trim()) as Record<string, unknown>;
|
|
437
|
+
const file = typeof row.file === "string" ? row.file.trim() : "";
|
|
438
|
+
const candidatePath = file ? resolve(projectRoot, file) : "";
|
|
439
|
+
if (candidatePath && isInsideRoot(resolve(projectRoot), candidatePath)) paths.add(candidatePath);
|
|
440
|
+
const targetPath = file ? resolveProjectFile(projectRoot, file, trustedRoots) : null;
|
|
441
|
+
if (targetPath) paths.add(targetPath);
|
|
442
|
+
} catch {
|
|
443
|
+
// Seed rows and malformed lines do not contribute referenced files.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return [...paths];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function taskContextSignature(projectRoot: string, taskDir: string, agentType?: AgentType): string {
|
|
451
|
+
return taskContextInputPaths(projectRoot, taskDir, agentType).map((filePath) => {
|
|
452
|
+
try {
|
|
453
|
+
const stat = statSync(filePath);
|
|
454
|
+
return `${filePath}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
|
|
455
|
+
} catch {
|
|
456
|
+
return `${filePath}:missing`;
|
|
457
|
+
}
|
|
458
|
+
}).join("\n");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
interface ContextInjectionLimits {
|
|
462
|
+
max_file_bytes: number;
|
|
463
|
+
max_artifact_bytes: number;
|
|
464
|
+
max_total_bytes: number;
|
|
465
|
+
/** Largest unified diff (bytes) appended for one file before falling back to the full block. */
|
|
466
|
+
diff_max_bytes: number;
|
|
467
|
+
/** Consecutive diffs allowed per file within one compaction epoch before a full block resets the baseline. */
|
|
468
|
+
diff_max_stack: number;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const DEFAULT_CONTEXT_INJECTION_LIMITS: ContextInjectionLimits = {
|
|
472
|
+
max_file_bytes: 32768,
|
|
473
|
+
max_artifact_bytes: 65536,
|
|
474
|
+
max_total_bytes: 131072,
|
|
475
|
+
diff_max_bytes: 8192,
|
|
476
|
+
diff_max_stack: 3,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const MAX_JSONL_BYTES = 1024 * 1024;
|
|
480
|
+
|
|
481
|
+
function stripInlineComment(value: string): string {
|
|
482
|
+
let quote: string | null = null;
|
|
483
|
+
for (let i = 0; i < value.length; i++) {
|
|
484
|
+
const char = value[i];
|
|
485
|
+
if (quote) {
|
|
486
|
+
if (char === quote) quote = null;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (char === "'" || char === '"') {
|
|
490
|
+
quote = char;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (char === "#" && (i === 0 || /\s/.test(value[i - 1]!))) {
|
|
494
|
+
return value.slice(0, i);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function unquoteYaml(value: string): string {
|
|
501
|
+
return value.length >= 2 && value[0] === value[value.length - 1] &&
|
|
502
|
+
(value[0] === "'" || value[0] === '"')
|
|
503
|
+
? value.slice(1, -1)
|
|
504
|
+
: value;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function readContextInjectionLimits(projectRoot: string): ContextInjectionLimits {
|
|
508
|
+
const limits = { ...DEFAULT_CONTEXT_INJECTION_LIMITS };
|
|
509
|
+
let config = "";
|
|
510
|
+
try { config = readFileSync(join(projectRoot, ".trellis", "config.yaml"), "utf-8"); } catch { return limits; }
|
|
511
|
+
|
|
512
|
+
let inSection = false;
|
|
513
|
+
let sectionIndent = -1;
|
|
514
|
+
for (const rawLine of config.split(/\r?\n/)) {
|
|
515
|
+
const trimmed = rawLine.trim();
|
|
516
|
+
if (!inSection) {
|
|
517
|
+
if (/^context_injection\s*:\s*(#.*)?$/.test(trimmed)) {
|
|
518
|
+
inSection = true;
|
|
519
|
+
sectionIndent = rawLine.length - rawLine.trimStart().length;
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
524
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
525
|
+
if (indent <= sectionIndent) break;
|
|
526
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/);
|
|
527
|
+
if (!match || !(match[1] in limits)) continue;
|
|
528
|
+
const rawValue = unquoteYaml(stripInlineComment(match[2]!).trim()).trim();
|
|
529
|
+
if (!/^\d+$/.test(rawValue)) continue;
|
|
530
|
+
(limits as unknown as Record<string, number>)[match[1]!] = Number.parseInt(rawValue, 10);
|
|
531
|
+
}
|
|
532
|
+
return limits;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
class ContextBudget {
|
|
536
|
+
private totalBytesUsed: number;
|
|
537
|
+
private terminalSummaryEmitted = false;
|
|
538
|
+
private stopped = false;
|
|
539
|
+
constructor(private readonly maxTotalBytes: number, initialUsed = 0) {
|
|
540
|
+
this.totalBytesUsed = initialUsed;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
get used(): number {
|
|
544
|
+
return this.totalBytesUsed;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
hasRoom(bytes: number): boolean {
|
|
548
|
+
return !this.stopped && (this.maxTotalBytes <= 0 || this.totalBytesUsed + bytes <= this.maxTotalBytes);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
emit(text: string): string | null {
|
|
552
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
553
|
+
if (!this.hasRoom(bytes)) return null;
|
|
554
|
+
this.totalBytesUsed += bytes;
|
|
555
|
+
return text;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
emitCandidate(primary: string | null, fallback: string | null): string | null {
|
|
559
|
+
if (primary !== null) {
|
|
560
|
+
const direct = this.emit(primary);
|
|
561
|
+
if (direct !== null) return direct;
|
|
562
|
+
}
|
|
563
|
+
if (fallback !== null) {
|
|
564
|
+
const notice = this.emit(fallback);
|
|
565
|
+
if (notice !== null) return notice;
|
|
566
|
+
}
|
|
567
|
+
return this.emitTerminalSummary();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private emitTerminalSummary(): string | null {
|
|
571
|
+
if (this.terminalSummaryEmitted) return null;
|
|
572
|
+
this.terminalSummaryEmitted = true;
|
|
573
|
+
this.stopped = true;
|
|
574
|
+
const summary = "[Trellis: context limit reached; omitted entries remain authoritative on disk and require_read paths above.]";
|
|
575
|
+
if (this.maxTotalBytes <= 0) {
|
|
576
|
+
this.totalBytesUsed += Buffer.byteLength(summary, "utf-8");
|
|
577
|
+
return summary;
|
|
578
|
+
}
|
|
579
|
+
const remaining = this.maxTotalBytes - this.totalBytesUsed;
|
|
580
|
+
if (remaining <= 0) return null;
|
|
581
|
+
const bounded = truncateUtf8(Buffer.from(summary, "utf-8"), remaining).toString("utf-8");
|
|
582
|
+
this.totalBytesUsed += Buffer.byteLength(bounded, "utf-8");
|
|
583
|
+
return bounded;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
type Utf8Status = "valid" | "incomplete" | "invalid";
|
|
588
|
+
|
|
589
|
+
function utf8Status(data: Buffer): Utf8Status {
|
|
590
|
+
for (let index = 0; index < data.length; index++) {
|
|
591
|
+
const lead = data[index]!;
|
|
592
|
+
if (lead <= 0x7f) continue;
|
|
593
|
+
|
|
594
|
+
let length: number;
|
|
595
|
+
let secondMin = 0x80;
|
|
596
|
+
let secondMax = 0xbf;
|
|
597
|
+
if (lead >= 0xc2 && lead <= 0xdf) length = 2;
|
|
598
|
+
else if (lead >= 0xe0 && lead <= 0xef) {
|
|
599
|
+
length = 3;
|
|
600
|
+
if (lead === 0xe0) secondMin = 0xa0;
|
|
601
|
+
if (lead === 0xed) secondMax = 0x9f;
|
|
602
|
+
} else if (lead >= 0xf0 && lead <= 0xf4) {
|
|
603
|
+
length = 4;
|
|
604
|
+
if (lead === 0xf0) secondMin = 0x90;
|
|
605
|
+
if (lead === 0xf4) secondMax = 0x8f;
|
|
606
|
+
} else return "invalid";
|
|
607
|
+
|
|
608
|
+
if (index + length > data.length) {
|
|
609
|
+
for (let tail = index + 1; tail < data.length; tail++) {
|
|
610
|
+
const min = tail === index + 1 ? secondMin : 0x80;
|
|
611
|
+
const max = tail === index + 1 ? secondMax : 0xbf;
|
|
612
|
+
if (data[tail]! < min || data[tail]! > max) return "invalid";
|
|
613
|
+
}
|
|
614
|
+
return "incomplete";
|
|
615
|
+
}
|
|
616
|
+
const second = data[index + 1]!;
|
|
617
|
+
if (second < secondMin || second > secondMax) return "invalid";
|
|
618
|
+
for (let continuation = index + 2; continuation < index + length; continuation++) {
|
|
619
|
+
const byte = data[continuation]!;
|
|
620
|
+
if (byte < 0x80 || byte > 0xbf) return "invalid";
|
|
621
|
+
}
|
|
622
|
+
index += length - 1;
|
|
623
|
+
}
|
|
624
|
+
return "valid";
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function truncateUtf8(data: Buffer, cap: number): Buffer {
|
|
628
|
+
if (cap <= 0 || data.length <= cap) return data;
|
|
629
|
+
if (utf8Status(data.subarray(0, cap)) === "valid") return data.subarray(0, cap);
|
|
630
|
+
// A bounded read has already been validated as a complete UTF-8 stream.
|
|
631
|
+
// Only its final, incomplete code point may make the cap prefix invalid.
|
|
632
|
+
for (let end = cap - 1; end >= Math.max(0, cap - 4); end--) {
|
|
633
|
+
const candidate = data.subarray(0, end);
|
|
634
|
+
if (utf8Status(candidate) === "valid") return candidate;
|
|
635
|
+
}
|
|
636
|
+
return Buffer.alloc(0);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function isUtf8(data: Buffer): boolean {
|
|
640
|
+
return utf8Status(data) === "valid";
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function readFilePrefix(filePath: string, maxBytes: number): { data: Buffer; size: number } | null {
|
|
644
|
+
let fd: number | null = null;
|
|
645
|
+
try {
|
|
646
|
+
fd = openSync(filePath, "r");
|
|
647
|
+
if (maxBytes <= 0) {
|
|
648
|
+
const data = readFileSync(fd);
|
|
649
|
+
return { data, size: fstatSync(fd).size };
|
|
650
|
+
}
|
|
651
|
+
// Read a few extra bytes so a UTF-8 sequence crossing the cap can be
|
|
652
|
+
// validated before truncateUtf8 removes its incomplete suffix. The
|
|
653
|
+
// descriptor keeps the read bounded if the path is replaced or grows.
|
|
654
|
+
const data = Buffer.allocUnsafe(maxBytes + 4);
|
|
655
|
+
const bytesRead = readFully(fd, data);
|
|
656
|
+
return { data: data.subarray(0, bytesRead), size: fstatSync(fd).size };
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
} finally {
|
|
660
|
+
if (fd !== null) closeSync(fd);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function readFully(fd: number, buffer: Buffer): number {
|
|
665
|
+
let total = 0;
|
|
666
|
+
while (total < buffer.length) {
|
|
667
|
+
const bytesRead = readSync(fd, buffer, total, buffer.length - total, total);
|
|
668
|
+
if (bytesRead === 0) break;
|
|
669
|
+
total += bytesRead;
|
|
670
|
+
}
|
|
671
|
+
return total;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function omittedNotice(file: string, size: number | null, reason: string): string {
|
|
675
|
+
const sizeText = size === null ? "unknown bytes" : `${size} bytes`;
|
|
676
|
+
return `### ${file} [omitted]\n\n[Trellis: omitted (${reason}) — ${file} (${sizeText}); required_read: ${file}]`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
interface MaterializedFile {
|
|
680
|
+
block: string | null;
|
|
681
|
+
notice: string;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function materialize(
|
|
685
|
+
targetPath: string,
|
|
686
|
+
displayPath: string,
|
|
687
|
+
reason: string,
|
|
688
|
+
maxBytes: number,
|
|
689
|
+
kind: "file" | "artifact",
|
|
690
|
+
): MaterializedFile {
|
|
691
|
+
const file = readFilePrefix(targetPath, maxBytes);
|
|
692
|
+
if (!file) {
|
|
693
|
+
return { block: null, notice: omittedNotice(displayPath, null, `${kind} is missing or unreadable`) };
|
|
694
|
+
}
|
|
695
|
+
const { data, size } = file;
|
|
696
|
+
const encodingStatus = utf8Status(data);
|
|
697
|
+
if (data.includes(0) || encodingStatus === "invalid" || (encodingStatus === "incomplete" && size <= data.length)) {
|
|
698
|
+
return { block: null, notice: omittedNotice(displayPath, size, `binary or non-UTF-8 ${kind}: ${reason}`) };
|
|
699
|
+
}
|
|
700
|
+
const truncated = truncateUtf8(data, maxBytes);
|
|
701
|
+
let content = truncated.toString("utf-8");
|
|
702
|
+
let status: "inline" | "truncated" = "inline";
|
|
703
|
+
if (truncated.length < size) {
|
|
704
|
+
status = "truncated";
|
|
705
|
+
content += `\n[Trellis: truncated at ${maxBytes} bytes — read ${displayPath} for the full content]`;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
block: `### ${displayPath} [${status}]\n\n${content}`,
|
|
709
|
+
notice: omittedNotice(displayPath, size, `total context limit reached: ${reason}`),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function readJsonlLines(jsonlPath: string, displayPath: string): { lines: string[]; omitted: string | null } {
|
|
714
|
+
let fd: number | null = null;
|
|
715
|
+
try {
|
|
716
|
+
fd = openSync(jsonlPath, "r");
|
|
717
|
+
const data = Buffer.allocUnsafe(MAX_JSONL_BYTES + 1);
|
|
718
|
+
const bytesRead = readFully(fd, data);
|
|
719
|
+
const size = fstatSync(fd).size;
|
|
720
|
+
if (bytesRead > MAX_JSONL_BYTES || size > MAX_JSONL_BYTES) {
|
|
721
|
+
return {
|
|
722
|
+
lines: [],
|
|
723
|
+
omitted: omittedNotice(displayPath, size, `manifest exceeds ${MAX_JSONL_BYTES} byte parse limit`),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const content = data.subarray(0, bytesRead);
|
|
727
|
+
if (!isUtf8(content)) {
|
|
728
|
+
return { lines: [], omitted: omittedNotice(displayPath, size, "manifest is not valid UTF-8") };
|
|
729
|
+
}
|
|
730
|
+
return { lines: content.toString("utf-8").split(/\r?\n/), omitted: null };
|
|
731
|
+
} catch {
|
|
732
|
+
return { lines: [], omitted: omittedNotice(displayPath, null, "manifest is missing or unreadable") };
|
|
733
|
+
} finally {
|
|
734
|
+
if (fd !== null) closeSync(fd);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** One ordered unit of task context, before the total-bytes budget is applied. */
|
|
739
|
+
type TaskContextEntry =
|
|
740
|
+
| { kind: "artifact"; displayPath: string; materialized: MaterializedFile }
|
|
741
|
+
| { kind: "manifest-omitted"; section: string; displayPath: string; notice: string }
|
|
742
|
+
| { kind: "file"; section: string; displayPath: string; materialized: MaterializedFile };
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Materializes prd.md, info.md and every manifest-referenced file in the order
|
|
746
|
+
* they are injected. Per-file caps are applied here; the shared total budget is
|
|
747
|
+
* applied by the consumer so the same entries can feed both the system prompt
|
|
748
|
+
* block and the per-file update baselines.
|
|
749
|
+
*/
|
|
750
|
+
function materializeTaskContextFiles(projectRoot: string, taskDir: string, agentType?: AgentType): TaskContextEntry[] {
|
|
751
|
+
const entries: TaskContextEntry[] = [];
|
|
752
|
+
// Resolved once per call (not per referenced file) — avoids re-parsing
|
|
753
|
+
// config.yaml for every jsonl row.
|
|
754
|
+
const trustedRoots = resolveTrustedRoots(projectRoot);
|
|
755
|
+
const limits = readContextInjectionLimits(projectRoot);
|
|
756
|
+
|
|
757
|
+
// prd.md and info.md — always included
|
|
758
|
+
const prdPath = join(taskDir, "prd.md");
|
|
759
|
+
if (existsSync(prdPath)) {
|
|
760
|
+
const displayPath = displayProjectPath(projectRoot, prdPath, taskDir);
|
|
761
|
+
entries.push({
|
|
762
|
+
kind: "artifact",
|
|
763
|
+
displayPath,
|
|
764
|
+
materialized: materialize(
|
|
765
|
+
prdPath,
|
|
766
|
+
displayPath,
|
|
767
|
+
"Requirements document",
|
|
768
|
+
limits.max_artifact_bytes,
|
|
769
|
+
"artifact",
|
|
770
|
+
),
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const infoPath = join(taskDir, "info.md");
|
|
774
|
+
if (existsSync(infoPath)) {
|
|
775
|
+
const displayPath = displayProjectPath(projectRoot, infoPath, taskDir);
|
|
776
|
+
entries.push({
|
|
777
|
+
kind: "artifact",
|
|
778
|
+
displayPath,
|
|
779
|
+
materialized: materialize(infoPath, displayPath, "Task information", limits.max_artifact_bytes, "artifact"),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// A file may be referenced by both manifests. Use the resolved real path so
|
|
784
|
+
// relative aliases and symlinked paths cannot consume the context budget twice.
|
|
785
|
+
const includedPaths = new Set<string>();
|
|
786
|
+
|
|
787
|
+
for (const jsonlName of taskContextJsonlNames(agentType)) {
|
|
788
|
+
const jsonlPath = join(taskDir, jsonlName);
|
|
789
|
+
if (!existsSync(jsonlPath)) continue;
|
|
790
|
+
|
|
791
|
+
const relativeJsonlPath = displayProjectPath(projectRoot, jsonlPath, taskDir);
|
|
792
|
+
const manifest = readJsonlLines(jsonlPath, relativeJsonlPath);
|
|
793
|
+
if (manifest.omitted) {
|
|
794
|
+
entries.push({
|
|
795
|
+
kind: "manifest-omitted",
|
|
796
|
+
section: jsonlName,
|
|
797
|
+
displayPath: relativeJsonlPath,
|
|
798
|
+
notice: manifest.omitted,
|
|
799
|
+
});
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
for (const line of manifest.lines) {
|
|
804
|
+
const trimmed = line.trim();
|
|
805
|
+
if (!trimmed) continue;
|
|
806
|
+
try {
|
|
807
|
+
const row = JSON.parse(trimmed) as Record<string, unknown>;
|
|
808
|
+
const file = typeof row.file === "string" ? row.file.trim() : "";
|
|
809
|
+
if (!file) continue;
|
|
810
|
+
const targetPath = resolveProjectFile(projectRoot, file, trustedRoots);
|
|
811
|
+
if (!targetPath) continue;
|
|
812
|
+
if (includedPaths.has(targetPath)) continue;
|
|
813
|
+
includedPaths.add(targetPath);
|
|
814
|
+
entries.push({
|
|
815
|
+
kind: "file",
|
|
816
|
+
section: jsonlName,
|
|
817
|
+
displayPath: file,
|
|
818
|
+
materialized: materialize(
|
|
819
|
+
targetPath,
|
|
820
|
+
file,
|
|
821
|
+
typeof row.reason === "string" ? row.reason : "-",
|
|
822
|
+
limits.max_file_bytes,
|
|
823
|
+
"file",
|
|
824
|
+
),
|
|
825
|
+
});
|
|
826
|
+
} catch {
|
|
827
|
+
// seed rows and malformed lines are non-fatal
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return entries;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Text the model is meant to see for one entry when the total budget allows it. */
|
|
835
|
+
function entryBlock(entry: TaskContextEntry): string {
|
|
836
|
+
if (entry.kind === "manifest-omitted") return entry.notice;
|
|
837
|
+
return entry.materialized.block ?? entry.materialized.notice;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
interface RenderedTaskContext {
|
|
841
|
+
content: string;
|
|
842
|
+
/** Display paths whose materialized text was replaced by a notice or dropped by the total budget. */
|
|
843
|
+
omittedPaths: Set<string>;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Renders materialized entries into the `<task-context>` block under the
|
|
848
|
+
* shared total budget. `omittedPaths` lets the caller avoid diffing against
|
|
849
|
+
* text the model never received.
|
|
850
|
+
*/
|
|
851
|
+
function renderTaskContext(projectRoot: string, entries: TaskContextEntry[]): RenderedTaskContext {
|
|
852
|
+
const parts: string[] = [];
|
|
853
|
+
const omittedPaths = new Set<string>();
|
|
854
|
+
const limits = readContextInjectionLimits(projectRoot);
|
|
855
|
+
const prefix =
|
|
856
|
+
"<task-context>\nContext is bounded by .trellis/config.yaml. Files marked [truncated] or [omitted] remain authoritative on disk; use their required_read path before relying on missing detail.\n\n";
|
|
857
|
+
const suffix = "\n</task-context>";
|
|
858
|
+
const wrapperBytes = Buffer.byteLength(prefix + suffix, "utf-8");
|
|
859
|
+
if (limits.max_total_bytes > 0 && limits.max_total_bytes < wrapperBytes) {
|
|
860
|
+
for (const entry of entries) omittedPaths.add(entry.displayPath);
|
|
861
|
+
return { content: "", omittedPaths };
|
|
862
|
+
}
|
|
863
|
+
const budget = new ContextBudget(limits.max_total_bytes, wrapperBytes);
|
|
864
|
+
|
|
865
|
+
// Emits `primary` (or `fallback` once the budget is exhausted) and reports
|
|
866
|
+
// whether the text the baseline will remember (`shown`) is what went out.
|
|
867
|
+
const appendCandidate = (primary: string | null, fallback: string | null, shown: string): boolean => {
|
|
868
|
+
const separator = parts.length > 0 ? "\n\n" : "";
|
|
869
|
+
const output = budget.emitCandidate(
|
|
870
|
+
primary === null ? null : separator + primary,
|
|
871
|
+
fallback === null ? null : separator + fallback,
|
|
872
|
+
);
|
|
873
|
+
if (output !== null) parts.push(output);
|
|
874
|
+
return output === separator + shown;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
let sectionName: string | null = null;
|
|
878
|
+
let fileChunks: string[] = [];
|
|
879
|
+
let sectionHeaderEmitted = false;
|
|
880
|
+
const flushSection = (): void => {
|
|
881
|
+
if (fileChunks.length > 0) parts.push(fileChunks.join(""));
|
|
882
|
+
fileChunks = [];
|
|
883
|
+
sectionHeaderEmitted = false;
|
|
884
|
+
sectionName = null;
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
for (const entry of entries) {
|
|
888
|
+
if (entry.kind === "artifact") {
|
|
889
|
+
const shown = entryBlock(entry);
|
|
890
|
+
if (!appendCandidate(entry.materialized.block, entry.materialized.notice, shown)) {
|
|
891
|
+
omittedPaths.add(entry.displayPath);
|
|
892
|
+
}
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (entry.section !== sectionName) {
|
|
896
|
+
flushSection();
|
|
897
|
+
sectionName = entry.section;
|
|
898
|
+
}
|
|
899
|
+
if (entry.kind === "manifest-omitted") {
|
|
900
|
+
const text = `## ${entry.section}\n\n${entry.notice}`;
|
|
901
|
+
if (!appendCandidate(text, null, text)) omittedPaths.add(entry.displayPath);
|
|
902
|
+
sectionName = null;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const sectionPrefix = sectionHeaderEmitted
|
|
906
|
+
? "\n\n---\n\n"
|
|
907
|
+
: `${parts.length > 0 ? "\n\n" : ""}## ${entry.section}\n\n`;
|
|
908
|
+
const primary = entry.materialized.block === null ? null : `${sectionPrefix}${entry.materialized.block}`;
|
|
909
|
+
const output = budget.emitCandidate(primary, `${sectionPrefix}${entry.materialized.notice}`);
|
|
910
|
+
if (output !== null) {
|
|
911
|
+
fileChunks.push(output);
|
|
912
|
+
sectionHeaderEmitted = true;
|
|
913
|
+
}
|
|
914
|
+
if (output !== `${sectionPrefix}${entryBlock(entry)}`) omittedPaths.add(entry.displayPath);
|
|
915
|
+
}
|
|
916
|
+
flushSection();
|
|
917
|
+
|
|
918
|
+
if (parts.length === 0) return { content: "", omittedPaths };
|
|
919
|
+
const context = `${prefix}${parts.join("")}${suffix}`;
|
|
920
|
+
if (limits.max_total_bytes <= 0 || Buffer.byteLength(context, "utf-8") <= limits.max_total_bytes)
|
|
921
|
+
return { content: context, omittedPaths };
|
|
922
|
+
const suffixBytes = Buffer.byteLength(suffix, "utf-8");
|
|
923
|
+
const bodyLimit = Math.max(0, limits.max_total_bytes - suffixBytes);
|
|
924
|
+
const body = truncateUtf8(Buffer.from(`${prefix}${parts.join("")}`, "utf-8"), bodyLimit).toString("utf-8");
|
|
925
|
+
return { content: `${body}${suffix}`, omittedPaths };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function buildTaskContext(projectRoot: string, taskDir: string, agentType?: AgentType): string {
|
|
929
|
+
return renderTaskContext(projectRoot, materializeTaskContextFiles(projectRoot, taskDir, agentType)).content;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ---------------------------------------------------------------------------
|
|
933
|
+
// Prompt injection config (escape hatch)
|
|
934
|
+
// ---------------------------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
// Mirrors DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD in inject-workflow-state.py:
|
|
937
|
+
// the skip keyword defaults to "no-trellis"; an explicit "" disables the
|
|
938
|
+
// escape hatch entirely.
|
|
939
|
+
const DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = "no-trellis";
|
|
940
|
+
|
|
941
|
+
// PyYAML-compatible resolution of scalars that parse as non-strings: null
|
|
942
|
+
// (empty, ~, null variants), bool (YAML 1.1 set), and numbers. Quoted scalars
|
|
943
|
+
// never reach this check and stay strings.
|
|
944
|
+
function isYamlNonStringScalar(raw: string): boolean {
|
|
945
|
+
if (raw === "" || raw === "~" || /^(?:null|Null|NULL)$/.test(raw)) return true;
|
|
946
|
+
if (/^(?:true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/.test(raw)) return true;
|
|
947
|
+
// PyYAML int resolver: binary/octal/decimal/hex; leading-zero decimals are not ints.
|
|
948
|
+
if (/^[-+]?(?:0[bB][01_]+|0[0-7_]+|0[xX][0-9a-fA-F_]+|[1-9][\d_]*|0)$/.test(raw)) return true;
|
|
949
|
+
// PyYAML float resolver: requires a dot and a signed exponent ("1.5e+3",
|
|
950
|
+
// not "1.5e3" — the latter stays a string in PyYAML).
|
|
951
|
+
return /^[-+]?(?:\d[\d_]*\.[\d_]*|\.[\d_]+)(?:[eE][-+]\d+)?$/.test(raw) ||
|
|
952
|
+
/^[-+]?\.(?:inf|Inf|INF)$/.test(raw) ||
|
|
953
|
+
/^\.(?:nan|NaN|NAN)$/.test(raw);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function readPromptInjectionSkipKeyword(projectRoot: string): string {
|
|
957
|
+
let config = "";
|
|
958
|
+
try { config = readFileSync(join(projectRoot, ".trellis", "config.yaml"), "utf-8"); } catch { return DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD; }
|
|
959
|
+
|
|
960
|
+
let inSection = false;
|
|
961
|
+
let sectionIndent = -1;
|
|
962
|
+
for (const rawLine of config.split(/\r?\n/)) {
|
|
963
|
+
const trimmed = rawLine.trim();
|
|
964
|
+
if (!inSection) {
|
|
965
|
+
if (/^prompt_injection\s*:\s*(#.*)?$/.test(trimmed)) {
|
|
966
|
+
inSection = true;
|
|
967
|
+
sectionIndent = rawLine.length - rawLine.trimStart().length;
|
|
968
|
+
}
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
972
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
973
|
+
if (indent <= sectionIndent) break;
|
|
974
|
+
const match = trimmed.match(/^skip_keyword\s*:\s*(.*)$/);
|
|
975
|
+
if (!match) continue;
|
|
976
|
+
const rawValue = stripInlineComment(match[1]!).trim();
|
|
977
|
+
const unquoted = unquoteYaml(rawValue);
|
|
978
|
+
// Preserve YAML scalar typing, mirroring _resolve_skip_keyword's
|
|
979
|
+
// isinstance(raw, str) check: a bare non-string scalar (bool/null/
|
|
980
|
+
// number, including an empty value) falls back to the default, while
|
|
981
|
+
// quoted scalars — including an explicit "" — stay strings.
|
|
982
|
+
if (unquoted === rawValue && isYamlNonStringScalar(rawValue)) {
|
|
983
|
+
return DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD;
|
|
984
|
+
}
|
|
985
|
+
return unquoted.trim();
|
|
986
|
+
}
|
|
987
|
+
return DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Mirrors prompt_has_skip_keyword() in inject-workflow-state.py: hyphen counts
|
|
991
|
+
// as a word char so "no-trellisx" / "xno-trellis" / "foo-no-trellis" don't
|
|
992
|
+
// match, but punctuation/whitespace boundaries do. Empty keyword never matches.
|
|
993
|
+
function shouldSkipWorkflowState(
|
|
994
|
+
userInput: string,
|
|
995
|
+
skipKeyword: string,
|
|
996
|
+
): boolean {
|
|
997
|
+
if (!skipKeyword) return false;
|
|
998
|
+
const escapedKeyword = skipKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
999
|
+
const pattern = new RegExp(`(?<![\\w-])${escapedKeyword}(?![\\w-])`, "i");
|
|
1000
|
+
return pattern.test(userInput);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ---------------------------------------------------------------------------
|
|
1004
|
+
// Per-turn cache — prevents redundant workflow-state resolution within a
|
|
1005
|
+
// single event cascade (input, before_agent_start, and context fire closely)
|
|
1006
|
+
// ---------------------------------------------------------------------------
|
|
1007
|
+
|
|
1008
|
+
const SESSION_OVERVIEW_TEXT =
|
|
1009
|
+
"Trellis workflow system active. Use skills and agents as directed by the workflow state.";
|
|
1010
|
+
|
|
1011
|
+
class TurnContextCache {
|
|
1012
|
+
private key: string | null = null;
|
|
1013
|
+
private timestamp = 0;
|
|
1014
|
+
private workflowMsg = "";
|
|
1015
|
+
private skipThisTurn = false;
|
|
1016
|
+
private static readonly TTL_MS = 1500;
|
|
1017
|
+
|
|
1018
|
+
// Called once per user turn (input event) with the skip decision for that
|
|
1019
|
+
// turn; invalidates the cache so every reader in the cascade
|
|
1020
|
+
// (before_agent_start, context) resolves the same turn state.
|
|
1021
|
+
beginTurn(skipThisTurn: boolean): void {
|
|
1022
|
+
this.skipThisTurn = skipThisTurn;
|
|
1023
|
+
this.key = null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
get(projectRoot: string, contextKey: string | null): { workflowMsg: string } {
|
|
1027
|
+
const now = Date.now();
|
|
1028
|
+
const cacheKey = `${projectRoot}:${contextKey ?? ""}`;
|
|
1029
|
+
if (
|
|
1030
|
+
this.key === cacheKey &&
|
|
1031
|
+
now - this.timestamp < TurnContextCache.TTL_MS
|
|
1032
|
+
) {
|
|
1033
|
+
return { workflowMsg: this.workflowMsg };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const { status } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
1037
|
+
|
|
1038
|
+
const workflowPath = join(projectRoot, ".trellis", "workflow.md");
|
|
1039
|
+
let workflowMd = "";
|
|
1040
|
+
try { workflowMd = readFileSync(workflowPath, "utf-8"); } catch { }
|
|
1041
|
+
|
|
1042
|
+
let workflowBody = "";
|
|
1043
|
+
if (workflowMd) {
|
|
1044
|
+
const blocks = parseWorkflowStateBlocks(workflowMd);
|
|
1045
|
+
const activeBlock = blocks.find((b) => b.status === status);
|
|
1046
|
+
if (activeBlock) {
|
|
1047
|
+
workflowBody = `[workflow-state:${activeBlock.status}]\n${activeBlock.content}\n[/workflow-state:${activeBlock.status}]`;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (!workflowBody) {
|
|
1051
|
+
workflowBody = "Refer to workflow.md for current step.";
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// When skip keyword is present, skip workflow state injection this turn
|
|
1055
|
+
this.workflowMsg = this.skipThisTurn
|
|
1056
|
+
? ""
|
|
1057
|
+
: `<workflow-state>\n${workflowBody}\n</workflow-state>\n\n<session-overview>\n${SESSION_OVERVIEW_TEXT}\n</session-overview>`;
|
|
1058
|
+
|
|
1059
|
+
this.key = cacheKey;
|
|
1060
|
+
this.timestamp = now;
|
|
1061
|
+
return { workflowMsg: this.workflowMsg };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
// Workflow-state tag parsing
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
1068
|
+
|
|
1069
|
+
const WORKFLOW_STATE_RE =
|
|
1070
|
+
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
1071
|
+
|
|
1072
|
+
interface WorkflowStateBlock {
|
|
1073
|
+
status: string;
|
|
1074
|
+
content: string;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function parseWorkflowStateBlocks(markdown: string): WorkflowStateBlock[] {
|
|
1078
|
+
const blocks: WorkflowStateBlock[] = [];
|
|
1079
|
+
for (const match of markdown.matchAll(WORKFLOW_STATE_RE)) {
|
|
1080
|
+
blocks.push({
|
|
1081
|
+
status: match[1],
|
|
1082
|
+
content: match[2].trim(),
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return blocks;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1089
|
+
// Sub-agent detection
|
|
1090
|
+
// ---------------------------------------------------------------------------
|
|
1091
|
+
|
|
1092
|
+
const TRELLIS_AGENTS = new Set(["trellis-implement", "trellis-check", "trellis-research"]);
|
|
1093
|
+
|
|
1094
|
+
function detectAgentType(): AgentType {
|
|
1095
|
+
const blocked = process.env.PI_BLOCKED_AGENT;
|
|
1096
|
+
if (blocked && TRELLIS_AGENTS.has(blocked)) {
|
|
1097
|
+
return blocked as AgentType;
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const SESSION_CONTEXT_TYPE = "trellis-session-context";
|
|
1103
|
+
const TASK_CONTEXT_TYPE = "trellis-task-context";
|
|
1104
|
+
const TASK_CONTEXT_UPDATE_TYPE = "trellis-task-context-update";
|
|
1105
|
+
const WORKFLOW_STATE_TYPE = "trellis-workflow-state";
|
|
1106
|
+
|
|
1107
|
+
// ---------------------------------------------------------------------------
|
|
1108
|
+
// Line diff — self-contained Myers O(ND) with unified output. The provider
|
|
1109
|
+
// prefix cache only survives when history is append-only, so task file
|
|
1110
|
+
// changes are delivered as diffs against the last snapshot the model saw.
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
|
|
1113
|
+
/** Lines per side beyond which a diff is not attempted; the full block is sent instead. */
|
|
1114
|
+
const DIFF_MAX_LINES = 4000;
|
|
1115
|
+
/** Edit-distance ceiling for the Myers search; larger rewrites fall back to the full block. */
|
|
1116
|
+
const DIFF_MAX_EDITS = 1000;
|
|
1117
|
+
const DIFF_CONTEXT_LINES = 3;
|
|
1118
|
+
|
|
1119
|
+
interface DiffEdit {
|
|
1120
|
+
kind: " " | "-" | "+";
|
|
1121
|
+
line: string;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function splitLines(text: string): string[] {
|
|
1125
|
+
if (text === "") return [];
|
|
1126
|
+
const lines = text.split("\n");
|
|
1127
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
1128
|
+
return lines;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function myersEdits(a: string[], b: string[], maxEdits: number): DiffEdit[] | null {
|
|
1132
|
+
const n = a.length;
|
|
1133
|
+
const m = b.length;
|
|
1134
|
+
const max = Math.min(maxEdits, n + m);
|
|
1135
|
+
const offset = max + 1;
|
|
1136
|
+
const width = 2 * max + 3;
|
|
1137
|
+
const trace: Int32Array[] = [];
|
|
1138
|
+
const v = new Int32Array(width);
|
|
1139
|
+
v[offset + 1] = 0;
|
|
1140
|
+
let found = -1;
|
|
1141
|
+
for (let d = 0; d <= max; d++) {
|
|
1142
|
+
trace.push(Int32Array.from(v));
|
|
1143
|
+
for (let k = -d; k <= d; k += 2) {
|
|
1144
|
+
let x: number;
|
|
1145
|
+
if (k === -d || (k !== d && v[offset + k - 1]! < v[offset + k + 1]!)) {
|
|
1146
|
+
x = v[offset + k + 1]!;
|
|
1147
|
+
} else {
|
|
1148
|
+
x = v[offset + k - 1]! + 1;
|
|
1149
|
+
}
|
|
1150
|
+
let y = x - k;
|
|
1151
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
1152
|
+
x++;
|
|
1153
|
+
y++;
|
|
1154
|
+
}
|
|
1155
|
+
v[offset + k] = x;
|
|
1156
|
+
if (x >= n && y >= m) {
|
|
1157
|
+
found = d;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (found >= 0) break;
|
|
1162
|
+
}
|
|
1163
|
+
if (found < 0) return null;
|
|
1164
|
+
|
|
1165
|
+
const edits: DiffEdit[] = [];
|
|
1166
|
+
let x = n;
|
|
1167
|
+
let y = m;
|
|
1168
|
+
for (let d = found; d >= 0; d--) {
|
|
1169
|
+
const vd = trace[d]!;
|
|
1170
|
+
const k = x - y;
|
|
1171
|
+
let prevK: number;
|
|
1172
|
+
if (k === -d || (k !== d && vd[offset + k - 1]! < vd[offset + k + 1]!)) {
|
|
1173
|
+
prevK = k + 1;
|
|
1174
|
+
} else {
|
|
1175
|
+
prevK = k - 1;
|
|
1176
|
+
}
|
|
1177
|
+
const prevX = vd[offset + prevK]!;
|
|
1178
|
+
const prevY = prevX - prevK;
|
|
1179
|
+
while (x > prevX && y > prevY) {
|
|
1180
|
+
edits.push({ kind: " ", line: a[x - 1]! });
|
|
1181
|
+
x--;
|
|
1182
|
+
y--;
|
|
1183
|
+
}
|
|
1184
|
+
if (d > 0) {
|
|
1185
|
+
if (x === prevX) edits.push({ kind: "+", line: b[prevY]! });
|
|
1186
|
+
else edits.push({ kind: "-", line: a[prevX]! });
|
|
1187
|
+
}
|
|
1188
|
+
x = prevX;
|
|
1189
|
+
y = prevY;
|
|
1190
|
+
}
|
|
1191
|
+
edits.reverse();
|
|
1192
|
+
return edits;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Produces a unified diff between two snapshots of one file.
|
|
1197
|
+
*
|
|
1198
|
+
* @returns `''` when the texts are identical, `null` when a diff is not
|
|
1199
|
+
* attempted (too many lines or too many edits), otherwise unified hunks.
|
|
1200
|
+
*/
|
|
1201
|
+
export function unifiedDiff(previous: string, current: string, displayPath: string): string | null {
|
|
1202
|
+
if (previous === current) return "";
|
|
1203
|
+
const a = splitLines(previous);
|
|
1204
|
+
const b = splitLines(current);
|
|
1205
|
+
if (a.length > DIFF_MAX_LINES || b.length > DIFF_MAX_LINES) return null;
|
|
1206
|
+
|
|
1207
|
+
// Trim the shared prefix and suffix so typical localized edits keep the
|
|
1208
|
+
// Myers search tiny regardless of file size.
|
|
1209
|
+
let start = 0;
|
|
1210
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
1211
|
+
let endA = a.length;
|
|
1212
|
+
let endB = b.length;
|
|
1213
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
1214
|
+
endA--;
|
|
1215
|
+
endB--;
|
|
1216
|
+
}
|
|
1217
|
+
const middle = myersEdits(a.slice(start, endA), b.slice(start, endB), DIFF_MAX_EDITS);
|
|
1218
|
+
if (middle === null) return null;
|
|
1219
|
+
const edits: DiffEdit[] = [
|
|
1220
|
+
...a.slice(0, start).map((line): DiffEdit => ({ kind: " ", line })),
|
|
1221
|
+
...middle,
|
|
1222
|
+
...a.slice(endA).map((line): DiffEdit => ({ kind: " ", line })),
|
|
1223
|
+
];
|
|
1224
|
+
|
|
1225
|
+
// Group changes into hunks; merge when the gap fits inside twice the context.
|
|
1226
|
+
const changeIndexes: number[] = [];
|
|
1227
|
+
for (let index = 0; index < edits.length; index++) {
|
|
1228
|
+
if (edits[index]!.kind !== " ") changeIndexes.push(index);
|
|
1229
|
+
}
|
|
1230
|
+
if (changeIndexes.length === 0) return "";
|
|
1231
|
+
const ranges: Array<[number, number]> = [];
|
|
1232
|
+
for (const index of changeIndexes) {
|
|
1233
|
+
const from = Math.max(0, index - DIFF_CONTEXT_LINES);
|
|
1234
|
+
const to = Math.min(edits.length - 1, index + DIFF_CONTEXT_LINES);
|
|
1235
|
+
const last = ranges[ranges.length - 1];
|
|
1236
|
+
if (last && from <= last[1] + 1) last[1] = Math.max(last[1], to);
|
|
1237
|
+
else ranges.push([from, to]);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const out: string[] = [`--- ${displayPath} (previous snapshot)`, `+++ ${displayPath} (current)`];
|
|
1241
|
+
let oldLine = 1;
|
|
1242
|
+
let newLine = 1;
|
|
1243
|
+
let cursor = 0;
|
|
1244
|
+
for (const [from, to] of ranges) {
|
|
1245
|
+
for (; cursor < from; cursor++) {
|
|
1246
|
+
const edit = edits[cursor]!;
|
|
1247
|
+
if (edit.kind !== "+") oldLine++;
|
|
1248
|
+
if (edit.kind !== "-") newLine++;
|
|
1249
|
+
}
|
|
1250
|
+
let oldCount = 0;
|
|
1251
|
+
let newCount = 0;
|
|
1252
|
+
const body: string[] = [];
|
|
1253
|
+
for (let index = from; index <= to; index++) {
|
|
1254
|
+
const edit = edits[index]!;
|
|
1255
|
+
if (edit.kind !== "+") oldCount++;
|
|
1256
|
+
if (edit.kind !== "-") newCount++;
|
|
1257
|
+
body.push(`${edit.kind}${edit.line}`);
|
|
1258
|
+
}
|
|
1259
|
+
// Match git: an empty side reports the line before the hunk.
|
|
1260
|
+
const oldStart = oldCount === 0 ? oldLine - 1 : oldLine;
|
|
1261
|
+
const newStart = newCount === 0 ? newLine - 1 : newLine;
|
|
1262
|
+
out.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`, ...body);
|
|
1263
|
+
oldLine += oldCount;
|
|
1264
|
+
newLine += newCount;
|
|
1265
|
+
cursor = to + 1;
|
|
1266
|
+
}
|
|
1267
|
+
return out.join("\n");
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
// Task context update planning (pure). The system prompt carries the first
|
|
1272
|
+
// snapshot for the life of the process; every later change is appended as a
|
|
1273
|
+
// persisted message so provider-bound history never rewrites earlier bytes.
|
|
1274
|
+
// ---------------------------------------------------------------------------
|
|
1275
|
+
|
|
1276
|
+
/** Last text sent to the model for one task file, plus its diff stack within the current epoch. */
|
|
1277
|
+
export interface TaskFileBaseline {
|
|
1278
|
+
block: string;
|
|
1279
|
+
stackedDiffs: number;
|
|
1280
|
+
stackedDiffBytes: number;
|
|
1281
|
+
/**
|
|
1282
|
+
* The model may no longer hold `block` verbatim (the update carrying it was
|
|
1283
|
+
* omitted by the budget, or a compaction happened since), so the next change
|
|
1284
|
+
* must be sent in full instead of as a diff against it.
|
|
1285
|
+
*/
|
|
1286
|
+
stale: boolean;
|
|
1287
|
+
/**
|
|
1288
|
+
* The model never received `block` at all (omitted by the system prompt or
|
|
1289
|
+
* update budget), so the file is re-sent in full on the next update pass
|
|
1290
|
+
* even when it did not change on disk.
|
|
1291
|
+
*/
|
|
1292
|
+
unseen: boolean;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** Baseline for the bound task; `files` preserves injection order. */
|
|
1296
|
+
export interface TaskContextBaseline {
|
|
1297
|
+
taskDir: string;
|
|
1298
|
+
/** Project-relative label used in update headers. */
|
|
1299
|
+
taskLabel: string;
|
|
1300
|
+
signature: string;
|
|
1301
|
+
files: Map<string, TaskFileBaseline>;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/** Current on-disk snapshot of the bound task. */
|
|
1305
|
+
export interface TaskContextSnapshot {
|
|
1306
|
+
taskDir: string;
|
|
1307
|
+
taskLabel: string;
|
|
1308
|
+
signature: string;
|
|
1309
|
+
files: Array<{ displayPath: string; block: string }>;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export interface TaskContextUpdatePlan {
|
|
1313
|
+
/** Message body to append, or `''` when nothing changed. */
|
|
1314
|
+
content: string;
|
|
1315
|
+
baseline: TaskContextBaseline | null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export interface TaskContextUpdateInput {
|
|
1319
|
+
previous: TaskContextBaseline | null;
|
|
1320
|
+
current: TaskContextSnapshot | null;
|
|
1321
|
+
limits: Pick<ContextInjectionLimits, "max_total_bytes" | "diff_max_bytes" | "diff_max_stack">;
|
|
1322
|
+
/** True once after a compaction: earlier snapshots may be summarized away, so diffs need a fresh base. */
|
|
1323
|
+
epochReset: boolean;
|
|
1324
|
+
/** True when the system prompt holds a task block; only affects the wording of the update header. */
|
|
1325
|
+
promptHasTask: boolean;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const UPDATE_PREFIX = `<${TASK_CONTEXT_UPDATE_TYPE}>\n`;
|
|
1329
|
+
const UPDATE_SUFFIX = `\n</${TASK_CONTEXT_UPDATE_TYPE}>`;
|
|
1330
|
+
|
|
1331
|
+
function snapshotFiles(entries: TaskContextEntry[]): TaskContextSnapshot["files"] {
|
|
1332
|
+
const files: TaskContextSnapshot["files"] = [];
|
|
1333
|
+
const seen = new Set<string>();
|
|
1334
|
+
for (const entry of entries) {
|
|
1335
|
+
if (seen.has(entry.displayPath)) continue;
|
|
1336
|
+
seen.add(entry.displayPath);
|
|
1337
|
+
files.push({ displayPath: entry.displayPath, block: entryBlock(entry) });
|
|
1338
|
+
}
|
|
1339
|
+
return files;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function baselineFrom(snapshot: TaskContextSnapshot, stalePaths: ReadonlySet<string> = new Set()): TaskContextBaseline {
|
|
1343
|
+
const files = new Map<string, TaskFileBaseline>();
|
|
1344
|
+
for (const file of snapshot.files) {
|
|
1345
|
+
files.set(file.displayPath, {
|
|
1346
|
+
block: file.block,
|
|
1347
|
+
stackedDiffs: 0,
|
|
1348
|
+
stackedDiffBytes: 0,
|
|
1349
|
+
stale: stalePaths.has(file.displayPath),
|
|
1350
|
+
unseen: stalePaths.has(file.displayPath),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
taskDir: snapshot.taskDir,
|
|
1355
|
+
taskLabel: snapshot.taskLabel,
|
|
1356
|
+
signature: snapshot.signature,
|
|
1357
|
+
files,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Decides what to append for this turn and what the next baseline is.
|
|
1363
|
+
*
|
|
1364
|
+
* Section shapes degrade in order: unified diff, full block, omitted notice
|
|
1365
|
+
* (when the shared total budget is exhausted). A full block is used whenever
|
|
1366
|
+
* there is no baseline for the file, the baseline is stale (compaction since
|
|
1367
|
+
* it was sent, or its update was omitted), the per-file diff stack is
|
|
1368
|
+
* exhausted, the diffs stacked since the last full send would exceed one full
|
|
1369
|
+
* block, or the diff itself is larger than `diff_max_bytes` or the block.
|
|
1370
|
+
*/
|
|
1371
|
+
export function planTaskContextUpdate(input: TaskContextUpdateInput): TaskContextUpdatePlan {
|
|
1372
|
+
const { previous, current, limits, epochReset, promptHasTask } = input;
|
|
1373
|
+
if (!current) {
|
|
1374
|
+
if (!previous) return { content: "", baseline: null };
|
|
1375
|
+
const label = previous.taskLabel;
|
|
1376
|
+
return {
|
|
1377
|
+
content: `${UPDATE_PREFIX}Task context for ${label} is no longer active. Ignore its files in the system prompt and earlier updates unless a task is bound again.${UPDATE_SUFFIX}`,
|
|
1378
|
+
baseline: null,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const sameTask = previous !== null && previous.taskDir === current.taskDir;
|
|
1383
|
+
const headerLines: string[] = [];
|
|
1384
|
+
if (previous && !sameTask) {
|
|
1385
|
+
headerLines.push(
|
|
1386
|
+
`Task context for ${previous.taskLabel} is no longer active; the sections below describe the newly bound task.`,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
if (!sameTask && !promptHasTask) {
|
|
1390
|
+
headerLines.push(
|
|
1391
|
+
"The system prompt holds no task context for this task; the sections below are the full baseline.",
|
|
1392
|
+
);
|
|
1393
|
+
} else if (!sameTask) {
|
|
1394
|
+
headerLines.push("The sections below are the full baseline for the newly bound task.");
|
|
1395
|
+
} else {
|
|
1396
|
+
headerLines.push(
|
|
1397
|
+
`Task context changed on disk. Each section below supersedes the same file in ${promptHasTask ? "the system prompt and in earlier updates" : "earlier updates"}.`,
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
headerLines.push("Files on disk remain authoritative; use read for anything not shown.");
|
|
1401
|
+
const header = `${headerLines.join(" ")}\n\n`;
|
|
1402
|
+
|
|
1403
|
+
const nextFiles = new Map<string, TaskFileBaseline>();
|
|
1404
|
+
interface Section {
|
|
1405
|
+
primary: string;
|
|
1406
|
+
fallback: string;
|
|
1407
|
+
/** Baseline entry to downgrade to stale when `primary` does not go out; null for removal notices. */
|
|
1408
|
+
file: { displayPath: string; block: string } | null;
|
|
1409
|
+
}
|
|
1410
|
+
const sections: Section[] = [];
|
|
1411
|
+
// A different task has no usable baseline: every file starts without `old`.
|
|
1412
|
+
const previousFiles = sameTask && previous ? previous.files : new Map<string, TaskFileBaseline>();
|
|
1413
|
+
|
|
1414
|
+
for (const file of current.files) {
|
|
1415
|
+
const old = previousFiles.get(file.displayPath);
|
|
1416
|
+
if (old && old.block === file.block && !old.unseen) {
|
|
1417
|
+
// Unchanged. After a compaction the update that carried `old.block` may
|
|
1418
|
+
// have been summarized away, so its next change must be sent in full.
|
|
1419
|
+
nextFiles.set(file.displayPath, epochReset && !old.stale ? { ...old, stale: true } : old);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const blockBytes = Buffer.byteLength(file.block, "utf-8");
|
|
1423
|
+
let diff: string | null = null;
|
|
1424
|
+
if (old && !old.stale && !epochReset && old.stackedDiffs < limits.diff_max_stack) {
|
|
1425
|
+
const candidate = unifiedDiff(old.block, file.block, file.displayPath);
|
|
1426
|
+
const candidateBytes = candidate === null ? 0 : Buffer.byteLength(candidate, "utf-8");
|
|
1427
|
+
// A diff is only worth it when it is cheaper than the block itself and
|
|
1428
|
+
// the diffs stacked since the last full send still cost less than one
|
|
1429
|
+
// full resend; otherwise the full block resets the stack.
|
|
1430
|
+
if (
|
|
1431
|
+
candidate !== null &&
|
|
1432
|
+
candidate !== "" &&
|
|
1433
|
+
candidateBytes <= limits.diff_max_bytes &&
|
|
1434
|
+
candidateBytes < blockBytes &&
|
|
1435
|
+
old.stackedDiffBytes + candidateBytes <= blockBytes
|
|
1436
|
+
) {
|
|
1437
|
+
diff = candidate;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const fallback = `## ${file.displayPath} [omitted]\n\n[Trellis: omitted (update budget reached) — ${file.displayPath}; required_read: ${file.displayPath}]`;
|
|
1441
|
+
if (diff !== null && old) {
|
|
1442
|
+
sections.push({ primary: `## ${file.displayPath} (diff)\n\n${diff}`, fallback, file });
|
|
1443
|
+
nextFiles.set(file.displayPath, {
|
|
1444
|
+
block: file.block,
|
|
1445
|
+
stackedDiffs: old.stackedDiffs + 1,
|
|
1446
|
+
stackedDiffBytes: old.stackedDiffBytes + Buffer.byteLength(diff, "utf-8"),
|
|
1447
|
+
stale: false,
|
|
1448
|
+
unseen: false,
|
|
1449
|
+
});
|
|
1450
|
+
} else {
|
|
1451
|
+
sections.push({ primary: `## ${file.displayPath} (full)\n\n${file.block}`, fallback, file });
|
|
1452
|
+
nextFiles.set(file.displayPath, {
|
|
1453
|
+
block: file.block,
|
|
1454
|
+
stackedDiffs: 0,
|
|
1455
|
+
stackedDiffBytes: 0,
|
|
1456
|
+
stale: false,
|
|
1457
|
+
unseen: false,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
for (const displayPath of previousFiles.keys()) {
|
|
1462
|
+
if (nextFiles.has(displayPath)) continue;
|
|
1463
|
+
const text = `## ${displayPath} (removed)\n\nNo longer part of the task context.`;
|
|
1464
|
+
sections.push({ primary: text, fallback: text, file: null });
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const baseline: TaskContextBaseline = {
|
|
1468
|
+
taskDir: current.taskDir,
|
|
1469
|
+
taskLabel: current.taskLabel,
|
|
1470
|
+
signature: current.signature,
|
|
1471
|
+
files: nextFiles,
|
|
1472
|
+
};
|
|
1473
|
+
if (sections.length === 0) return { content: "", baseline };
|
|
1474
|
+
|
|
1475
|
+
const wrapperBytes = Buffer.byteLength(UPDATE_PREFIX + header + UPDATE_SUFFIX, "utf-8");
|
|
1476
|
+
const budget = new ContextBudget(limits.max_total_bytes, wrapperBytes);
|
|
1477
|
+
const parts: string[] = [];
|
|
1478
|
+
for (const section of sections) {
|
|
1479
|
+
const separator = parts.length > 0 ? "\n\n" : "";
|
|
1480
|
+
const primary = separator + section.primary;
|
|
1481
|
+
const output = budget.emitCandidate(primary, separator + section.fallback);
|
|
1482
|
+
if (output !== null) parts.push(output);
|
|
1483
|
+
if (section.file && output !== primary) {
|
|
1484
|
+
// The model only got a notice (or nothing): remember the current text so
|
|
1485
|
+
// the change is not re-reported, but never diff against it.
|
|
1486
|
+
nextFiles.set(section.file.displayPath, {
|
|
1487
|
+
block: section.file.block,
|
|
1488
|
+
stackedDiffs: 0,
|
|
1489
|
+
stackedDiffBytes: 0,
|
|
1490
|
+
stale: true,
|
|
1491
|
+
unseen: true,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
content: `${UPDATE_PREFIX}${header}${parts.join("")}${UPDATE_SUFFIX}`,
|
|
1497
|
+
baseline,
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
// ---------------------------------------------------------------------------
|
|
1501
|
+
// Extension entry point
|
|
1502
|
+
// ---------------------------------------------------------------------------
|
|
1503
|
+
|
|
1504
|
+
export default function (pi: ExtensionAPI): void {
|
|
1505
|
+
const startupRoot = findProjectRoot(process.cwd());
|
|
1506
|
+
const startupPolicy = startupRoot
|
|
1507
|
+
? readIntegrationPolicy(startupRoot, "omp")
|
|
1508
|
+
: { enabled: true, hooks: !hooksDisabledByEnv() };
|
|
1509
|
+
if (!startupPolicy.enabled || !startupPolicy.hooks) return;
|
|
1510
|
+
let projectRoot: string | null = null;
|
|
1511
|
+
const turnCache = new TurnContextCache();
|
|
1512
|
+
const agentType = detectAgentType();
|
|
1513
|
+
const isSubAgent = agentType !== null;
|
|
1514
|
+
|
|
1515
|
+
// Set when a compaction is announced and cleared once a workflow breadcrumb
|
|
1516
|
+
// has been persisted or re-projected afterwards. A boolean instead of a
|
|
1517
|
+
// timestamp pair: two events inside the same millisecond must not reopen
|
|
1518
|
+
// the safety net.
|
|
1519
|
+
let compactionPending = false;
|
|
1520
|
+
|
|
1521
|
+
// Provider prefix caches invalidate from byte 0 whenever the system prompt
|
|
1522
|
+
// changes, so the task block appended to the system prompt is memoized per
|
|
1523
|
+
// context key + project root and stays byte-identical for the life of the
|
|
1524
|
+
// process. Later on-disk changes travel through persisted update messages.
|
|
1525
|
+
const memoizedTaskBlocks = new Map<string, string>();
|
|
1526
|
+
const taskBaselines = new Map<string, TaskContextBaseline | null>();
|
|
1527
|
+
// Set by session_before_compact and consumed by the next update planning:
|
|
1528
|
+
// earlier snapshots may have been summarized away, so the first change after
|
|
1529
|
+
// a compaction re-sends the full block instead of a diff.
|
|
1530
|
+
let baselineEpochInvalidated = false;
|
|
1531
|
+
|
|
1532
|
+
const rememberContextKey = (ctx?: {
|
|
1533
|
+
sessionManager?: {
|
|
1534
|
+
getSessionId?: () => string | undefined;
|
|
1535
|
+
getSessionFile?: () => string | undefined;
|
|
1536
|
+
};
|
|
1537
|
+
}): string | null => {
|
|
1538
|
+
const key = deriveContextKey(ctx);
|
|
1539
|
+
if (!key) return null;
|
|
1540
|
+
return key;
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
const promptKey = (root: string, contextKey: string | null): string => `${contextKey ?? "default"}::${root}`;
|
|
1544
|
+
|
|
1545
|
+
const snapshotTask = (
|
|
1546
|
+
taskDir: string,
|
|
1547
|
+
root: string,
|
|
1548
|
+
): { snapshot: TaskContextSnapshot; entries: TaskContextEntry[] } => {
|
|
1549
|
+
// Signature before content: a write landing between the two reads then
|
|
1550
|
+
// leaves the stored signature older than the text, so the next turn
|
|
1551
|
+
// re-plans (and stays silent if the content matches) instead of missing it.
|
|
1552
|
+
const signature = taskContextSignature(root, taskDir, agentType);
|
|
1553
|
+
const entries = materializeTaskContextFiles(root, taskDir, agentType);
|
|
1554
|
+
return {
|
|
1555
|
+
entries,
|
|
1556
|
+
snapshot: {
|
|
1557
|
+
taskDir,
|
|
1558
|
+
taskLabel: displayProjectPath(root, taskDir, taskDir),
|
|
1559
|
+
signature,
|
|
1560
|
+
files: snapshotFiles(entries),
|
|
1561
|
+
},
|
|
1562
|
+
};
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1566
|
+
if (!policyForContext(ctx).hooks) return;
|
|
1567
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
1568
|
+
const contextKey = rememberContextKey(ctx);
|
|
1569
|
+
|
|
1570
|
+
if (!projectRoot) return;
|
|
1571
|
+
|
|
1572
|
+
if (isSubAgent) {
|
|
1573
|
+
// Sub-agent: inject precise task context once
|
|
1574
|
+
const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
1575
|
+
if (taskDir) {
|
|
1576
|
+
const taskContext = buildTaskContext(projectRoot, taskDir, agentType);
|
|
1577
|
+
if (taskContext) {
|
|
1578
|
+
await pi.sendMessage({
|
|
1579
|
+
customType: TASK_CONTEXT_TYPE,
|
|
1580
|
+
content: taskContext,
|
|
1581
|
+
display: false,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
} else {
|
|
1586
|
+
// Main session: inject session context (global map). The task context
|
|
1587
|
+
// is carried by the system prompt from the first turn onwards.
|
|
1588
|
+
const sessionContext = buildSessionContext(projectRoot, contextKey);
|
|
1589
|
+
if (sessionContext) {
|
|
1590
|
+
await pi.sendMessage({
|
|
1591
|
+
customType: SESSION_CONTEXT_TYPE,
|
|
1592
|
+
content: sessionContext,
|
|
1593
|
+
display: false,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
ctx.ui.notify("Trellis workflow system available", "info");
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
1602
|
+
if (!policyForContext(ctx).hooks) return;
|
|
1603
|
+
compactionPending = true;
|
|
1604
|
+
baselineEpochInvalidated = true;
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
1608
|
+
if (!policyForContext(ctx).hooks) return;
|
|
1609
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
1610
|
+
if (!projectRoot) return;
|
|
1611
|
+
const contextKey = rememberContextKey(ctx);
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
// Persistent injection: workflow state for this turn
|
|
1615
|
+
const cached = turnCache.get(projectRoot, contextKey);
|
|
1616
|
+
compactionPending = false;
|
|
1617
|
+
|
|
1618
|
+
const message = cached.workflowMsg
|
|
1619
|
+
? {
|
|
1620
|
+
customType: WORKFLOW_STATE_TYPE,
|
|
1621
|
+
content: cached.workflowMsg,
|
|
1622
|
+
display: false,
|
|
1623
|
+
}
|
|
1624
|
+
: undefined;
|
|
1625
|
+
|
|
1626
|
+
if (isSubAgent) return message ? { message } : undefined;
|
|
1627
|
+
|
|
1628
|
+
// Task block: snapshot once per context key, then keep the system prompt
|
|
1629
|
+
// byte-identical. `event.systemPrompt` already carries earlier handlers'
|
|
1630
|
+
// segments; only append, never replace.
|
|
1631
|
+
const key = promptKey(projectRoot, contextKey);
|
|
1632
|
+
let block = memoizedTaskBlocks.get(key);
|
|
1633
|
+
if (block === undefined) {
|
|
1634
|
+
const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
1635
|
+
block = "";
|
|
1636
|
+
let baseline: TaskContextBaseline | null = null;
|
|
1637
|
+
if (taskDir) {
|
|
1638
|
+
// One materialization feeds both the prompt block and the baseline so
|
|
1639
|
+
// later diffs are computed against exactly the text the model saw.
|
|
1640
|
+
const { snapshot, entries } = snapshotTask(taskDir, projectRoot);
|
|
1641
|
+
const rendered = renderTaskContext(projectRoot, entries);
|
|
1642
|
+
block = rendered.content;
|
|
1643
|
+
if (block) baseline = baselineFrom(snapshot, rendered.omittedPaths);
|
|
1644
|
+
}
|
|
1645
|
+
memoizedTaskBlocks.set(key, block);
|
|
1646
|
+
taskBaselines.set(key, baseline);
|
|
1647
|
+
}
|
|
1648
|
+
const basePrompt = (event as { systemPrompt?: unknown }).systemPrompt;
|
|
1649
|
+
const systemPrompt = block && Array.isArray(basePrompt) ? [...basePrompt, block] : undefined;
|
|
1650
|
+
if (!message && !systemPrompt) return;
|
|
1651
|
+
return {
|
|
1652
|
+
...(message ? { message } : {}),
|
|
1653
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
1654
|
+
};
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
// Second handler: append-only task context refresh. Runs after the memoize
|
|
1658
|
+
// handler above (same registration order), returns at most one persisted
|
|
1659
|
+
// message per turn and never touches the system prompt.
|
|
1660
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1661
|
+
if (!policyForContext(ctx).hooks || isSubAgent) return;
|
|
1662
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
1663
|
+
if (!projectRoot) return;
|
|
1664
|
+
const contextKey = rememberContextKey(ctx);
|
|
1665
|
+
const key = promptKey(projectRoot, contextKey);
|
|
1666
|
+
if (!memoizedTaskBlocks.has(key)) return;
|
|
1667
|
+
// Skip turn (escape hatch): inject nothing and leave the baseline alone so
|
|
1668
|
+
// the change is still reported on the next normal turn.
|
|
1669
|
+
if (!turnCache.get(projectRoot, contextKey).workflowMsg) return;
|
|
1670
|
+
|
|
1671
|
+
const previous = taskBaselines.get(key) ?? null;
|
|
1672
|
+
const { taskDir } = resolveActiveTaskStatus(projectRoot, contextKey);
|
|
1673
|
+
const epochReset = baselineEpochInvalidated;
|
|
1674
|
+
if (
|
|
1675
|
+
taskDir &&
|
|
1676
|
+
previous &&
|
|
1677
|
+
previous.taskDir === taskDir &&
|
|
1678
|
+
!epochReset &&
|
|
1679
|
+
previous.signature === taskContextSignature(projectRoot, taskDir, agentType)
|
|
1680
|
+
) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
baselineEpochInvalidated = false;
|
|
1684
|
+
const plan = planTaskContextUpdate({
|
|
1685
|
+
previous,
|
|
1686
|
+
current: taskDir ? snapshotTask(taskDir, projectRoot).snapshot : null,
|
|
1687
|
+
limits: readContextInjectionLimits(projectRoot),
|
|
1688
|
+
epochReset,
|
|
1689
|
+
promptHasTask: (memoizedTaskBlocks.get(key) ?? "") !== "",
|
|
1690
|
+
});
|
|
1691
|
+
taskBaselines.set(key, plan.baseline);
|
|
1692
|
+
if (!plan.content) return;
|
|
1693
|
+
return {
|
|
1694
|
+
message: {
|
|
1695
|
+
customType: TASK_CONTEXT_UPDATE_TYPE,
|
|
1696
|
+
content: plan.content,
|
|
1697
|
+
display: false,
|
|
1698
|
+
},
|
|
1699
|
+
};
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
// context fires before EVERY LLM API call (including tool-use continuations
|
|
1703
|
+
// and post-compaction agent.continue() paths). It only acts as a safety net
|
|
1704
|
+
// for the workflow breadcrumb; task context is never rewritten here because
|
|
1705
|
+
// any change to an earlier message breaks the provider prefix cache.
|
|
1706
|
+
pi.on("context", async (event, ctx) => {
|
|
1707
|
+
if (!policyForContext(ctx).hooks) return;
|
|
1708
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
1709
|
+
if (!projectRoot) return;
|
|
1710
|
+
const contextKey = rememberContextKey(ctx);
|
|
1711
|
+
const messages = event.messages as { role?: string; customType?: string; content?: string }[];
|
|
1712
|
+
|
|
1713
|
+
// Resolve the turn state before the fast path: a skip turn must still
|
|
1714
|
+
// run breadcrumb cleanup even when nothing else changed.
|
|
1715
|
+
const cached = turnCache.get(projectRoot, contextKey);
|
|
1716
|
+
const skipping = !cached.workflowMsg;
|
|
1717
|
+
|
|
1718
|
+
if (!skipping && !compactionPending) return;
|
|
1719
|
+
|
|
1720
|
+
if (skipping) {
|
|
1721
|
+
// Skip turn (escape hatch): drop any persisted breadcrumb from an
|
|
1722
|
+
// earlier turn so the skip actually takes effect.
|
|
1723
|
+
const withoutBreadcrumb = messages.filter(
|
|
1724
|
+
(message) => !(message.role === "custom" && message.customType === WORKFLOW_STATE_TYPE),
|
|
1725
|
+
);
|
|
1726
|
+
if (withoutBreadcrumb.length === messages.length) return;
|
|
1727
|
+
compactionPending = false;
|
|
1728
|
+
return { messages: withoutBreadcrumb };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Post-compaction: reverse-scan to confirm absence before injecting
|
|
1732
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1733
|
+
if (messages[i].role === "custom" && messages[i].customType === WORKFLOW_STATE_TYPE) {
|
|
1734
|
+
compactionPending = false;
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
compactionPending = false;
|
|
1740
|
+
return {
|
|
1741
|
+
messages: [
|
|
1742
|
+
...messages,
|
|
1743
|
+
{
|
|
1744
|
+
role: "custom" as const,
|
|
1745
|
+
customType: WORKFLOW_STATE_TYPE,
|
|
1746
|
+
content: cached.workflowMsg,
|
|
1747
|
+
display: false,
|
|
1748
|
+
timestamp: Date.now(),
|
|
1749
|
+
},
|
|
1750
|
+
],
|
|
1751
|
+
};
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// OMP passes Bash event.input through to the tool execution parameters, so
|
|
1755
|
+
// inject the session key through the shell-agnostic env field. An explicit
|
|
1756
|
+
// per-call value wins over the derived key.
|
|
1757
|
+
pi.on("tool_call", (event, ctx) => {
|
|
1758
|
+
if (!policyForContext(ctx).hooks || event.toolName !== "bash") return;
|
|
1759
|
+
const contextKey = rememberContextKey(ctx);
|
|
1760
|
+
if (!contextKey) return;
|
|
1761
|
+
const input = event.input as { env?: Record<string, string> };
|
|
1762
|
+
input.env = {
|
|
1763
|
+
TRELLIS_CONTEXT_ID: contextKey,
|
|
1764
|
+
...input.env,
|
|
1765
|
+
};
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
pi.on("input", async (event, ctx) => {
|
|
1769
|
+
if (!policyForContext(ctx).hooks) return;
|
|
1770
|
+
projectRoot = findProjectRoot(ctx.cwd);
|
|
1771
|
+
if (!projectRoot) return;
|
|
1772
|
+
const contextKey = rememberContextKey(ctx);
|
|
1773
|
+
|
|
1774
|
+
// Check if this turn should skip workflow state injection
|
|
1775
|
+
const skipKeyword = readPromptInjectionSkipKeyword(projectRoot);
|
|
1776
|
+
const skipThisTurn = shouldSkipWorkflowState(event.text ?? "", skipKeyword);
|
|
1777
|
+
|
|
1778
|
+
// Record the turn's skip decision and pre-warm the cache so
|
|
1779
|
+
// before_agent_start and context can use it
|
|
1780
|
+
turnCache.beginTurn(skipThisTurn);
|
|
1781
|
+
turnCache.get(projectRoot, contextKey);
|
|
1782
|
+
});
|
|
1783
|
+
}
|