@pennixrv/trellis 0.6.17
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/LICENSE +235 -0
- package/README.md +168 -0
- package/bin/trellis.js +3 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +299 -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 +307 -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 +479 -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 +134 -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 +106 -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 +81 -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 +1603 -0
- package/dist/commands/init.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 +423 -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 +2163 -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 +84 -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 +232 -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 +123 -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 +80 -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 +51 -0
- package/dist/configurators/workflow.d.ts.map +1 -0
- package/dist/configurators/workflow.js +210 -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 +9 -0
- package/dist/constants/version.d.ts.map +1 -0
- package/dist/constants/version.js +15 -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 +187 -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.17.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 +57 -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 +67 -0
- package/dist/templates/codex/index.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 +104 -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 +61 -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 +106 -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 +52 -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 +1105 -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 +2027 -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 +140 -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 +64 -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 +158 -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 +121 -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 +1874 -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 +239 -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 +801 -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 +62 -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/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 +172 -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 +994 -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 +265 -0
- package/dist/utils/workflow-resolver.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Add a new session to journal file and update index.md.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python3 add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli]
|
|
8
|
+
python3 add_session.py --title "Title" --branch "feat/my-branch"
|
|
9
|
+
|
|
10
|
+
# Pipe detailed content via stdin (use --stdin to opt in):
|
|
11
|
+
cat << 'EOF' | python3 add_session.py --stdin --title "Title" --summary "Summary"
|
|
12
|
+
<session content here>
|
|
13
|
+
EOF
|
|
14
|
+
|
|
15
|
+
# Structured content (repeatable; a section with no bullets is omitted):
|
|
16
|
+
python3 add_session.py --title "Title" --change "Did X" --test "Ran Y" --next-step "Do Z"
|
|
17
|
+
|
|
18
|
+
# Commit evidence that cannot be resolved from the local object database
|
|
19
|
+
# (amended, not yet fetched) needs an explicit subject, one per OID:
|
|
20
|
+
python3 add_session.py --title "Title" --commit "abc1234" \
|
|
21
|
+
--commit-subject "abc1234=fix(cli): stop truncating the manifest"
|
|
22
|
+
|
|
23
|
+
Commit evidence:
|
|
24
|
+
Every --commit token is a bounded hex OID (7-40 chars). Each one is
|
|
25
|
+
resolved to its real subject from the local object database BEFORE
|
|
26
|
+
anything is written. An OID that cannot be resolved fails the command
|
|
27
|
+
before the journal or index is touched — no placeholder prose is ever
|
|
28
|
+
recorded. Pass "-" (the default) for a planning session with no commits.
|
|
29
|
+
|
|
30
|
+
Retry convergence:
|
|
31
|
+
The journal append, the index row and the optional auto-commit are one
|
|
32
|
+
resumable operation. Every entry carries a fingerprint marker derived from
|
|
33
|
+
its own inputs, so re-running an interrupted command repairs the pending
|
|
34
|
+
record instead of appending a second one, and a failed auto-commit exits
|
|
35
|
+
non-zero with the checkpoint to resume from. A record that is already
|
|
36
|
+
committed is never reused: an identical later request is a new session
|
|
37
|
+
unless an explicit --idempotency-key says otherwise.
|
|
38
|
+
|
|
39
|
+
Branch resolution order:
|
|
40
|
+
1. --branch CLI arg (explicit)
|
|
41
|
+
2. task.json branch field (from active task, if still exists)
|
|
42
|
+
3. git branch --show-current (auto-detect)
|
|
43
|
+
4. None (omitted gracefully)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import hashlib
|
|
50
|
+
import json
|
|
51
|
+
import re
|
|
52
|
+
import sys
|
|
53
|
+
from datetime import datetime
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
from common.paths import (
|
|
57
|
+
DIR_TASKS,
|
|
58
|
+
DIR_WORKFLOW,
|
|
59
|
+
FILE_JOURNAL_PREFIX,
|
|
60
|
+
get_repo_root,
|
|
61
|
+
get_current_task,
|
|
62
|
+
get_developer,
|
|
63
|
+
get_workspace_dir,
|
|
64
|
+
)
|
|
65
|
+
from common.developer import ensure_developer
|
|
66
|
+
from common.git import branch_exists_locally, run_git
|
|
67
|
+
from common.io import write_text_atomic
|
|
68
|
+
from common.log import Colors, colored
|
|
69
|
+
from common.safe_commit import (
|
|
70
|
+
print_gitignore_warning,
|
|
71
|
+
safe_git_add,
|
|
72
|
+
safe_trellis_paths_to_add,
|
|
73
|
+
)
|
|
74
|
+
from common.tasks import load_task
|
|
75
|
+
from common.types import TaskInfo
|
|
76
|
+
from common.config import (
|
|
77
|
+
get_packages,
|
|
78
|
+
get_session_auto_commit,
|
|
79
|
+
get_session_commit_message,
|
|
80
|
+
get_max_journal_lines,
|
|
81
|
+
is_monorepo,
|
|
82
|
+
resolve_package,
|
|
83
|
+
validate_package,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Bounded, argv-safe input shapes. Anything outside them is rejected during
|
|
88
|
+
# preflight, before a single byte is written.
|
|
89
|
+
COMMIT_TOKEN_RE = re.compile(r"^[0-9a-fA-F]{7,40}$")
|
|
90
|
+
IDEMPOTENCY_KEY_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
|
91
|
+
MAX_SUBJECT_LEN = 500
|
|
92
|
+
|
|
93
|
+
# Machine-readable record identity. An HTML comment renders as nothing, so the
|
|
94
|
+
# human evidence in the entry is unchanged by its presence.
|
|
95
|
+
MARKER_PREFIX = "<!-- trellis-session:"
|
|
96
|
+
# Bumped when the fingerprint inputs change. Untagged markers are v1, whose
|
|
97
|
+
# fingerprint mixed in the calendar date; see compute_record_fingerprint.
|
|
98
|
+
MARKER_VERSION = 2
|
|
99
|
+
LEGACY_MARKER_RE = re.compile(r"^<!-- trellis-session: fp=([0-9a-f]{16}) -->$")
|
|
100
|
+
ENTRY_DATE_RE = re.compile(r"^\*\*Date\*\*: (\d{4}-\d{2}-\d{2})\s*$")
|
|
101
|
+
SESSION_HEADING_RE = re.compile(r"^## Session (\d+):", re.MULTILINE)
|
|
102
|
+
|
|
103
|
+
# Recording states, in the order the operation walks them.
|
|
104
|
+
STATE_ABSENT = "absent"
|
|
105
|
+
STATE_JOURNAL_RECORDED = "journal-recorded"
|
|
106
|
+
STATE_INDEX_RECORDED = "index-recorded"
|
|
107
|
+
STATE_COMMITTED = "committed"
|
|
108
|
+
|
|
109
|
+
# Auto-commit outcomes.
|
|
110
|
+
COMMIT_DONE = "committed"
|
|
111
|
+
COMMIT_SKIPPED = "skipped"
|
|
112
|
+
COMMIT_BLOCKED = "blocked"
|
|
113
|
+
COMMIT_FAILED = "failed"
|
|
114
|
+
|
|
115
|
+
# Git probes against other refs are best-effort; never let one hang a session.
|
|
116
|
+
GIT_PROBE_TIMEOUT = 15.0
|
|
117
|
+
# Upper bound on the refs folded into session numbering. A repo with hundreds
|
|
118
|
+
# of stale branches must not turn session recording into a tree walk.
|
|
119
|
+
MAX_CONVERGENCE_REFS = 100
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# =============================================================================
|
|
123
|
+
# Helper Functions
|
|
124
|
+
# =============================================================================
|
|
125
|
+
|
|
126
|
+
def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
|
|
127
|
+
"""Get latest journal file info.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Tuple of (file_path, file_number, line_count).
|
|
131
|
+
"""
|
|
132
|
+
latest_file: Path | None = None
|
|
133
|
+
latest_num = -1
|
|
134
|
+
|
|
135
|
+
for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
|
|
136
|
+
if not f.is_file():
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
match = re.search(r"(\d+)$", f.stem)
|
|
140
|
+
if match:
|
|
141
|
+
num = int(match.group(1))
|
|
142
|
+
if num > latest_num:
|
|
143
|
+
latest_num = num
|
|
144
|
+
latest_file = f
|
|
145
|
+
|
|
146
|
+
if latest_file:
|
|
147
|
+
lines = len(latest_file.read_text(encoding="utf-8").splitlines())
|
|
148
|
+
return latest_file, latest_num, lines
|
|
149
|
+
|
|
150
|
+
return None, 0, 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_current_session(index_file: Path) -> int:
|
|
154
|
+
"""Get current session number from index.md."""
|
|
155
|
+
if not index_file.is_file():
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
content = index_file.read_text(encoding="utf-8")
|
|
159
|
+
return _max_session_in_index(content)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _max_session_in_index(content: str) -> int:
|
|
163
|
+
"""Read the `Total Sessions` counter out of an index.md body."""
|
|
164
|
+
for line in content.splitlines():
|
|
165
|
+
if "Total Sessions" in line:
|
|
166
|
+
match = re.search(r":\s*(\d+)", line)
|
|
167
|
+
if match:
|
|
168
|
+
return int(match.group(1))
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _max_session_in_journal(content: str) -> int:
|
|
173
|
+
"""Highest `## Session N:` heading in a journal body (0 when none)."""
|
|
174
|
+
numbers = [int(m.group(1)) for m in SESSION_HEADING_RE.finditer(content)]
|
|
175
|
+
return max(numbers) if numbers else 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _extract_journal_num(filename: str) -> int:
|
|
179
|
+
"""Extract journal number from filename for sorting."""
|
|
180
|
+
match = re.search(r"(\d+)", filename)
|
|
181
|
+
return int(match.group(1)) if match else 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def count_journal_files(dev_dir: Path, active_num: int) -> str:
|
|
185
|
+
"""Count journal files and return table rows."""
|
|
186
|
+
active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md"
|
|
187
|
+
result_lines = []
|
|
188
|
+
|
|
189
|
+
files = sorted(
|
|
190
|
+
[f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()],
|
|
191
|
+
key=lambda f: _extract_journal_num(f.stem),
|
|
192
|
+
reverse=True
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for f in files:
|
|
196
|
+
filename = f.name
|
|
197
|
+
lines = len(f.read_text(encoding="utf-8").splitlines())
|
|
198
|
+
status = "Active" if filename == active_file else "Archived"
|
|
199
|
+
result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
|
|
200
|
+
|
|
201
|
+
return "\n".join(result_lines)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_current_git_branch(repo_root: Path) -> str | None:
|
|
205
|
+
"""Return the current checkout branch, or None for detached/non-git states."""
|
|
206
|
+
rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
207
|
+
if rc != 0:
|
|
208
|
+
return None
|
|
209
|
+
detected = branch_out.strip()
|
|
210
|
+
return detected or None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def branch_ref_exists(repo_root: Path, branch: str) -> bool:
|
|
214
|
+
"""Return True when branch exists locally or as the local origin ref."""
|
|
215
|
+
for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"):
|
|
216
|
+
rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root)
|
|
217
|
+
if rc == 0:
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def resolve_session_branch(
|
|
223
|
+
repo_root: Path,
|
|
224
|
+
cli_branch: str | None,
|
|
225
|
+
task_data: TaskInfo | None,
|
|
226
|
+
) -> str | None:
|
|
227
|
+
"""Resolve journal branch without trusting stale task.json branch fields."""
|
|
228
|
+
if cli_branch:
|
|
229
|
+
return cli_branch
|
|
230
|
+
|
|
231
|
+
current_branch = get_current_git_branch(repo_root)
|
|
232
|
+
raw_task_branch = task_data.raw.get("branch") if task_data else None
|
|
233
|
+
task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else ""
|
|
234
|
+
if not task_branch:
|
|
235
|
+
return current_branch
|
|
236
|
+
|
|
237
|
+
if branch_ref_exists(repo_root, task_branch):
|
|
238
|
+
return task_branch
|
|
239
|
+
|
|
240
|
+
if current_branch:
|
|
241
|
+
print(
|
|
242
|
+
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; using current branch '{current_branch}'.",
|
|
243
|
+
file=sys.stderr,
|
|
244
|
+
)
|
|
245
|
+
return current_branch
|
|
246
|
+
|
|
247
|
+
print(
|
|
248
|
+
f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; omitting branch.",
|
|
249
|
+
file=sys.stderr,
|
|
250
|
+
)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def is_git_worktree(repo_root: Path) -> bool:
|
|
255
|
+
"""Return True when repo_root is a linked worktree (not the main working tree).
|
|
256
|
+
|
|
257
|
+
Standard test: `git rev-parse --git-dir` (per-worktree) differs from
|
|
258
|
+
`git rev-parse --git-common-dir` (shared across all worktrees) once both
|
|
259
|
+
are resolved to absolute paths. In the main working tree these are the
|
|
260
|
+
same directory.
|
|
261
|
+
"""
|
|
262
|
+
rc_dir, git_dir, _ = run_git(["rev-parse", "--git-dir"], cwd=repo_root)
|
|
263
|
+
rc_common, git_common_dir, _ = run_git(
|
|
264
|
+
["rev-parse", "--git-common-dir"], cwd=repo_root
|
|
265
|
+
)
|
|
266
|
+
if rc_dir != 0 or rc_common != 0:
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
git_dir_path = (repo_root / git_dir.strip()).resolve()
|
|
270
|
+
git_common_dir_path = (repo_root / git_common_dir.strip()).resolve()
|
|
271
|
+
return git_dir_path != git_common_dir_path
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def warn_if_parallel_worktree(repo_root: Path) -> None:
|
|
275
|
+
"""Non-blocking note: index.md conflicts across parallel worktrees/branches
|
|
276
|
+
are expected and safe. Only fires when running in a linked git worktree
|
|
277
|
+
(not the main tree) with `session_auto_commit` enabled (#415 quick-fix tier).
|
|
278
|
+
"""
|
|
279
|
+
if not get_session_auto_commit(repo_root):
|
|
280
|
+
return
|
|
281
|
+
if not is_git_worktree(repo_root):
|
|
282
|
+
return
|
|
283
|
+
print(
|
|
284
|
+
colored(
|
|
285
|
+
"[NOTE] Running in a git worktree with session_auto_commit enabled: "
|
|
286
|
+
"journal-*.md files auto-merge via .gitattributes, but index.md "
|
|
287
|
+
"conflicts across parallel worktrees/branches are expected and safe "
|
|
288
|
+
"to resolve by picking either side (task state lives in task.json, "
|
|
289
|
+
"not index.md). See .trellis/spec/cli/backend/directory-structure.md "
|
|
290
|
+
'("Workspace Journal Merge Behavior").',
|
|
291
|
+
Colors.YELLOW,
|
|
292
|
+
),
|
|
293
|
+
file=sys.stderr,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def create_new_journal_file(
|
|
298
|
+
dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
|
|
299
|
+
) -> Path | None:
|
|
300
|
+
"""Create a new journal file. Returns None when the write fails."""
|
|
301
|
+
prev_num = num - 1
|
|
302
|
+
new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
|
|
303
|
+
|
|
304
|
+
content = f"""# Journal - {developer} (Part {num})
|
|
305
|
+
|
|
306
|
+
> Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines)
|
|
307
|
+
> Started: {today}
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
if not write_text_atomic(new_file, content):
|
|
313
|
+
return None
|
|
314
|
+
return new_file
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# =============================================================================
|
|
318
|
+
# Commit evidence (R1) — accurate subjects or nothing
|
|
319
|
+
# =============================================================================
|
|
320
|
+
|
|
321
|
+
def parse_commit_tokens(commit: str) -> tuple[list[str], str | None]:
|
|
322
|
+
"""Split --commit into bounded hex OIDs. Returns (tokens, error)."""
|
|
323
|
+
raw = (commit or "").strip()
|
|
324
|
+
if not raw or raw == "-":
|
|
325
|
+
return [], None
|
|
326
|
+
|
|
327
|
+
tokens: list[str] = []
|
|
328
|
+
for part in raw.split(","):
|
|
329
|
+
token = part.strip()
|
|
330
|
+
if not token:
|
|
331
|
+
continue
|
|
332
|
+
if not COMMIT_TOKEN_RE.match(token):
|
|
333
|
+
return [], (
|
|
334
|
+
f"invalid --commit token '{token}': expected a 7-40 character hex "
|
|
335
|
+
"commit OID, or '-' for a planning session with no commits"
|
|
336
|
+
)
|
|
337
|
+
token = token.lower()
|
|
338
|
+
if token not in tokens:
|
|
339
|
+
tokens.append(token)
|
|
340
|
+
return tokens, None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def parse_subject_overrides(values: list[str] | None) -> tuple[dict[str, str], str | None]:
|
|
344
|
+
"""Parse `--commit-subject <oid>=<subject>` into a one-to-one mapping."""
|
|
345
|
+
overrides: dict[str, str] = {}
|
|
346
|
+
for raw in values or []:
|
|
347
|
+
if "=" not in raw:
|
|
348
|
+
return {}, (
|
|
349
|
+
f"invalid --commit-subject '{raw}': expected <oid>=<subject>"
|
|
350
|
+
)
|
|
351
|
+
oid_part, subject = raw.split("=", 1)
|
|
352
|
+
oid = oid_part.strip().lower()
|
|
353
|
+
subject = subject.strip()
|
|
354
|
+
if not COMMIT_TOKEN_RE.match(oid):
|
|
355
|
+
return {}, (
|
|
356
|
+
f"invalid --commit-subject key '{oid_part.strip()}': expected a "
|
|
357
|
+
"7-40 character hex commit OID"
|
|
358
|
+
)
|
|
359
|
+
if not subject:
|
|
360
|
+
return {}, f"invalid --commit-subject for '{oid}': subject is empty"
|
|
361
|
+
if len(subject) > MAX_SUBJECT_LEN:
|
|
362
|
+
return {}, (
|
|
363
|
+
f"invalid --commit-subject for '{oid}': subject exceeds "
|
|
364
|
+
f"{MAX_SUBJECT_LEN} characters"
|
|
365
|
+
)
|
|
366
|
+
if oid in overrides:
|
|
367
|
+
return {}, f"duplicate --commit-subject mapping for '{oid}'"
|
|
368
|
+
overrides[oid] = subject
|
|
369
|
+
return overrides, None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def resolve_commit_subject(repo_root: Path, oid: str) -> str | None:
|
|
373
|
+
"""Resolve one OID to its subject from the local object database.
|
|
374
|
+
|
|
375
|
+
Passed as argv (never interpolated into a shell) and peeled with
|
|
376
|
+
``^{commit}`` so a hex-looking ref name cannot resolve to a tree or tag.
|
|
377
|
+
Returns None when the object is missing, ambiguous, or has no subject.
|
|
378
|
+
"""
|
|
379
|
+
rc, out, _ = run_git(
|
|
380
|
+
["show", "-s", "--format=%s", f"{oid}^{{commit}}", "--"], cwd=repo_root
|
|
381
|
+
)
|
|
382
|
+
if rc != 0:
|
|
383
|
+
return None
|
|
384
|
+
for line in out.splitlines():
|
|
385
|
+
subject = line.strip()
|
|
386
|
+
if subject:
|
|
387
|
+
return subject[:MAX_SUBJECT_LEN]
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def build_commit_evidence(
|
|
392
|
+
repo_root: Path,
|
|
393
|
+
tokens: list[str],
|
|
394
|
+
overrides: dict[str, str],
|
|
395
|
+
) -> tuple[list[tuple[str, str]], str | None]:
|
|
396
|
+
"""Pair every commit OID with an accurate subject, or fail.
|
|
397
|
+
|
|
398
|
+
There is no placeholder path: an OID that neither resolves locally nor
|
|
399
|
+
carries an explicit mapping stops the command before any mutation.
|
|
400
|
+
"""
|
|
401
|
+
evidence: list[tuple[str, str]] = []
|
|
402
|
+
unresolved: list[str] = []
|
|
403
|
+
|
|
404
|
+
for oid in tokens:
|
|
405
|
+
if oid in overrides:
|
|
406
|
+
evidence.append((oid, overrides[oid]))
|
|
407
|
+
continue
|
|
408
|
+
subject = resolve_commit_subject(repo_root, oid)
|
|
409
|
+
if subject is None:
|
|
410
|
+
unresolved.append(oid)
|
|
411
|
+
continue
|
|
412
|
+
evidence.append((oid, subject))
|
|
413
|
+
|
|
414
|
+
if unresolved:
|
|
415
|
+
return [], (
|
|
416
|
+
"cannot resolve commit evidence for: "
|
|
417
|
+
+ ", ".join(unresolved)
|
|
418
|
+
+ ". Fetch the objects, correct the OID, or pass the exact subject "
|
|
419
|
+
"with --commit-subject <oid>=<subject>. Nothing was written."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
unused = sorted(oid for oid in overrides if oid not in tokens)
|
|
423
|
+
if unused:
|
|
424
|
+
return [], (
|
|
425
|
+
"--commit-subject supplied for OIDs that are not in --commit: "
|
|
426
|
+
+ ", ".join(unused)
|
|
427
|
+
+ ". The mapping must be one-to-one. Nothing was written."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return evidence, None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def escape_markdown_cell(text: str) -> str:
|
|
434
|
+
"""Make a commit subject safe inside a Markdown table cell."""
|
|
435
|
+
collapsed = " ".join(text.split())
|
|
436
|
+
return collapsed.replace("\\", "\\\\").replace("|", "\\|")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# =============================================================================
|
|
440
|
+
# Record identity (R2)
|
|
441
|
+
# =============================================================================
|
|
442
|
+
|
|
443
|
+
def _normalize_text(value: str | None) -> str:
|
|
444
|
+
return " ".join((value or "").split())
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _fingerprint_payload(
|
|
448
|
+
developer: str,
|
|
449
|
+
title: str,
|
|
450
|
+
summary: str,
|
|
451
|
+
package: str | None,
|
|
452
|
+
branch: str | None,
|
|
453
|
+
evidence: list[tuple[str, str]],
|
|
454
|
+
changes: list[str] | None,
|
|
455
|
+
extra_content: str | None,
|
|
456
|
+
tests: list[str] | None,
|
|
457
|
+
next_steps: list[str] | None,
|
|
458
|
+
idempotency_key: str | None,
|
|
459
|
+
) -> dict:
|
|
460
|
+
"""Normalized semantic inputs of one record, shared by both schemes."""
|
|
461
|
+
return {
|
|
462
|
+
"developer": developer,
|
|
463
|
+
"title": _normalize_text(title),
|
|
464
|
+
"summary": _normalize_text(summary),
|
|
465
|
+
"package": package or "",
|
|
466
|
+
"branch": branch or "",
|
|
467
|
+
"commits": [[oid, _normalize_text(subject)] for oid, subject in evidence],
|
|
468
|
+
"changes": [_normalize_text(c) for c in changes or []],
|
|
469
|
+
"extra": _normalize_text(extra_content),
|
|
470
|
+
"tests": [_normalize_text(t) for t in tests or []],
|
|
471
|
+
"next_steps": [_normalize_text(n) for n in next_steps or []],
|
|
472
|
+
"idempotency_key": idempotency_key or "",
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _hash_payload(payload: dict) -> str:
|
|
477
|
+
encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
|
478
|
+
return hashlib.sha256(encoded).hexdigest()[:16]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def compute_record_fingerprint(payload: dict) -> str:
|
|
482
|
+
"""Bounded fingerprint over the normalized semantic inputs of one record.
|
|
483
|
+
|
|
484
|
+
This is the retry key while the record is still pending in the worktree —
|
|
485
|
+
not a global dedupe key. Two genuinely separate sessions with identical
|
|
486
|
+
prose differ only if the caller says so, which is why an already-committed
|
|
487
|
+
match is never adopted (see classify_record).
|
|
488
|
+
|
|
489
|
+
Deliberately date-free (v2). v1 mixed in the calendar date, which made a
|
|
490
|
+
record unfindable by its own retry across a midnight rollover: the journal
|
|
491
|
+
and index were already written, the commit had failed, and the recomputed
|
|
492
|
+
fingerprint no longer matched the marker sitting in the journal, so the
|
|
493
|
+
retry appended a second entry for one session.
|
|
494
|
+
|
|
495
|
+
Dropping the date cannot over-collapse two same-day sessions, because the
|
|
496
|
+
date was equal for both of them anyway. Across days it only ever separated
|
|
497
|
+
records with byte-identical prose, commits, branch and package — and a
|
|
498
|
+
*committed* record of that shape is already refused for reuse by
|
|
499
|
+
cmd_add_session, which steps to the next `generation` of the payload
|
|
500
|
+
rather than reusing the committed marker. What is left
|
|
501
|
+
is exactly the question this key should answer: is there an uncommitted
|
|
502
|
+
record in this worktree matching these inputs?
|
|
503
|
+
"""
|
|
504
|
+
return _hash_payload(payload)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def compute_legacy_fingerprint(payload: dict, date: str) -> str:
|
|
508
|
+
"""The v1 fingerprint, for resolving markers written before the change.
|
|
509
|
+
|
|
510
|
+
Lookup only — nothing writes this scheme any more.
|
|
511
|
+
"""
|
|
512
|
+
return _hash_payload({**payload, "date": date})
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def render_marker(fingerprint: str) -> str:
|
|
516
|
+
"""Machine-readable record marker; renders as nothing in Markdown."""
|
|
517
|
+
return f"{MARKER_PREFIX} v={MARKER_VERSION} fp={fingerprint} -->"
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def render_legacy_marker(fingerprint: str) -> str:
|
|
521
|
+
"""The v1 marker form: no version tag. Read, never written."""
|
|
522
|
+
return f"{MARKER_PREFIX} fp={fingerprint} -->"
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# Session numbering (collision-proof across concurrent branches)
|
|
527
|
+
# =============================================================================
|
|
528
|
+
|
|
529
|
+
def _repo_relative(repo_root: Path, path: Path) -> str | None:
|
|
530
|
+
try:
|
|
531
|
+
return path.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
532
|
+
except (OSError, ValueError):
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _default_branch_local(repo_root: Path) -> str | None:
|
|
537
|
+
"""Resolve the default branch without ever touching the network.
|
|
538
|
+
|
|
539
|
+
`resolve_default_branch()` falls back to `git remote show origin`, which
|
|
540
|
+
can block on a fetch. Session recording is a hot path, so it stops at the
|
|
541
|
+
local symbolic ref and the two conventional names.
|
|
542
|
+
"""
|
|
543
|
+
rc, out, _ = run_git(
|
|
544
|
+
["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"],
|
|
545
|
+
cwd=repo_root,
|
|
546
|
+
timeout=GIT_PROBE_TIMEOUT,
|
|
547
|
+
)
|
|
548
|
+
if rc == 0 and out.strip():
|
|
549
|
+
return out.strip().rsplit("/", 1)[-1]
|
|
550
|
+
for candidate in ("main", "master"):
|
|
551
|
+
if branch_exists_locally(candidate, repo_root):
|
|
552
|
+
return candidate
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _convergence_refs(repo_root: Path) -> list[str]:
|
|
557
|
+
"""Refs whose recorded workspace state must be folded into the counter.
|
|
558
|
+
|
|
559
|
+
Ordered by how much they matter, then capped: HEAD and the default branch
|
|
560
|
+
first (the merge target every branch eventually meets), then every other
|
|
561
|
+
local head — a parallel worktree's branch lives there and is the case that
|
|
562
|
+
actually collided — then remote-tracking refs.
|
|
563
|
+
"""
|
|
564
|
+
refs: list[str] = []
|
|
565
|
+
|
|
566
|
+
def add(ref: str) -> None:
|
|
567
|
+
if ref not in refs:
|
|
568
|
+
refs.append(ref)
|
|
569
|
+
|
|
570
|
+
rc, _, _ = run_git(["rev-parse", "--verify", "--quiet", "HEAD"], cwd=repo_root)
|
|
571
|
+
if rc == 0:
|
|
572
|
+
add("HEAD")
|
|
573
|
+
|
|
574
|
+
default = _default_branch_local(repo_root)
|
|
575
|
+
if default:
|
|
576
|
+
for ref in (f"refs/heads/{default}", f"refs/remotes/origin/{default}"):
|
|
577
|
+
rc, _, _ = run_git(
|
|
578
|
+
["show-ref", "--verify", "--quiet", ref], cwd=repo_root
|
|
579
|
+
)
|
|
580
|
+
if rc == 0:
|
|
581
|
+
add(ref)
|
|
582
|
+
|
|
583
|
+
for pattern in ("refs/heads/", "refs/remotes/"):
|
|
584
|
+
rc, out, _ = run_git(
|
|
585
|
+
["for-each-ref", "--format=%(refname)", pattern],
|
|
586
|
+
cwd=repo_root,
|
|
587
|
+
timeout=GIT_PROBE_TIMEOUT,
|
|
588
|
+
)
|
|
589
|
+
if rc != 0:
|
|
590
|
+
continue
|
|
591
|
+
for line in out.splitlines():
|
|
592
|
+
ref = line.strip()
|
|
593
|
+
# A symbolic ref like refs/remotes/origin/HEAD is a duplicate of
|
|
594
|
+
# the branch it points at and git grep rejects some of them.
|
|
595
|
+
if ref and not ref.endswith("/HEAD"):
|
|
596
|
+
add(ref)
|
|
597
|
+
|
|
598
|
+
return refs[:MAX_CONVERGENCE_REFS]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _max_session_across_refs(repo_root: Path, refs: list[str], dev_rel: str) -> int:
|
|
602
|
+
"""Highest session number recorded under `dev_rel` in any of `refs`.
|
|
603
|
+
|
|
604
|
+
One `git grep` over every ref rather than an ls-tree + show per ref: the
|
|
605
|
+
number of branches in a real repo is unbounded, the per-ref cost is not.
|
|
606
|
+
Exit status 1 (no match) is a normal outcome, not an error.
|
|
607
|
+
"""
|
|
608
|
+
if not refs:
|
|
609
|
+
return 0
|
|
610
|
+
|
|
611
|
+
rc, out, _ = run_git(
|
|
612
|
+
[
|
|
613
|
+
"grep",
|
|
614
|
+
"--no-color",
|
|
615
|
+
"-h",
|
|
616
|
+
"-E",
|
|
617
|
+
"-e",
|
|
618
|
+
r"^## Session [0-9]+:",
|
|
619
|
+
"-e",
|
|
620
|
+
r"Total Sessions",
|
|
621
|
+
]
|
|
622
|
+
+ refs
|
|
623
|
+
+ ["--", dev_rel],
|
|
624
|
+
cwd=repo_root,
|
|
625
|
+
timeout=GIT_PROBE_TIMEOUT,
|
|
626
|
+
)
|
|
627
|
+
if rc > 1 or not out:
|
|
628
|
+
return 0
|
|
629
|
+
|
|
630
|
+
# `git grep` prefixes each hit with `<rev>:<path>:`, so the numbers are
|
|
631
|
+
# matched anywhere in the line rather than anchored.
|
|
632
|
+
numbers = [int(m.group(1)) for m in re.finditer(r"## Session (\d+):", out)]
|
|
633
|
+
numbers += [
|
|
634
|
+
int(m.group(1)) for m in re.finditer(r"Total Sessions[^\d]{0,8}(\d+)", out)
|
|
635
|
+
]
|
|
636
|
+
return max(numbers) if numbers else 0
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def max_local_session(dev_dir: Path, index_file: Path) -> int:
|
|
640
|
+
"""Highest session number visible in the working tree."""
|
|
641
|
+
highest = get_current_session(index_file)
|
|
642
|
+
for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
|
|
643
|
+
if not f.is_file():
|
|
644
|
+
continue
|
|
645
|
+
try:
|
|
646
|
+
content = f.read_text(encoding="utf-8")
|
|
647
|
+
except OSError:
|
|
648
|
+
continue
|
|
649
|
+
highest = max(highest, _max_session_in_journal(content))
|
|
650
|
+
return highest
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def resolve_next_session(repo_root: Path, dev_dir: Path, index_file: Path) -> int:
|
|
654
|
+
"""Next session number, converged across the local tree and other branches.
|
|
655
|
+
|
|
656
|
+
Deriving the number from the working tree alone lets two branches that
|
|
657
|
+
each record before merging claim the same number (observed twice on
|
|
658
|
+
2026-08-06). Taking the union with every recorded ref — the default
|
|
659
|
+
branch, and the other local heads a parallel worktree records on — makes
|
|
660
|
+
the number monotonic across concurrent branches; `journal-*.md`
|
|
661
|
+
merge=union then keeps both entries when the branches meet.
|
|
662
|
+
"""
|
|
663
|
+
highest = max_local_session(dev_dir, index_file)
|
|
664
|
+
|
|
665
|
+
dev_rel = _repo_relative(repo_root, dev_dir)
|
|
666
|
+
if dev_rel:
|
|
667
|
+
highest = max(
|
|
668
|
+
highest,
|
|
669
|
+
_max_session_across_refs(
|
|
670
|
+
repo_root, _convergence_refs(repo_root), dev_rel
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
return highest + 1
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# =============================================================================
|
|
678
|
+
# Pending-record classifier (R3/R4)
|
|
679
|
+
# =============================================================================
|
|
680
|
+
|
|
681
|
+
def find_marker_entries(dev_dir: Path, marker: str) -> list[tuple[Path, int | None]]:
|
|
682
|
+
"""Locate every journal entry carrying `marker`.
|
|
683
|
+
|
|
684
|
+
Returns (journal_file, session_number); the number is None when the marker
|
|
685
|
+
is not directly under a `## Session N:` heading, which is malformed
|
|
686
|
+
pending evidence and must never be guessed into completion.
|
|
687
|
+
"""
|
|
688
|
+
hits: list[tuple[Path, int | None]] = []
|
|
689
|
+
for f in sorted(dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
|
|
690
|
+
if not f.is_file():
|
|
691
|
+
continue
|
|
692
|
+
try:
|
|
693
|
+
lines = f.read_text(encoding="utf-8").splitlines()
|
|
694
|
+
except OSError:
|
|
695
|
+
continue
|
|
696
|
+
for i, line in enumerate(lines):
|
|
697
|
+
if line.strip() != marker:
|
|
698
|
+
continue
|
|
699
|
+
session_num: int | None = None
|
|
700
|
+
for j in range(i - 1, max(-1, i - 4), -1):
|
|
701
|
+
match = SESSION_HEADING_RE.match(lines[j])
|
|
702
|
+
if match:
|
|
703
|
+
session_num = int(match.group(1))
|
|
704
|
+
break
|
|
705
|
+
hits.append((f, session_num))
|
|
706
|
+
return hits
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def content_at_head(repo_root: Path, path: Path) -> str | None:
|
|
710
|
+
"""File content as committed at HEAD, or None when it isn't there.
|
|
711
|
+
|
|
712
|
+
The `./` prefix makes the path cwd-relative. `HEAD:<path>` alone is
|
|
713
|
+
relative to the *git* toplevel, which is not always `repo_root` — the
|
|
714
|
+
Trellis root is the nearest directory holding `.trellis/`, which can sit
|
|
715
|
+
below the git root. Without it, every lookup in that layout fails and a
|
|
716
|
+
committed record looks pending.
|
|
717
|
+
"""
|
|
718
|
+
rel = _repo_relative(repo_root, path)
|
|
719
|
+
if not rel:
|
|
720
|
+
return None
|
|
721
|
+
rc, out, _ = run_git(["show", f"HEAD:./{rel}"], cwd=repo_root)
|
|
722
|
+
return out if rc == 0 else None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def index_has_session_row(index_file: Path, session_num: int) -> bool:
|
|
726
|
+
"""Whether the session-history block already holds this session's row."""
|
|
727
|
+
if not index_file.is_file():
|
|
728
|
+
return False
|
|
729
|
+
try:
|
|
730
|
+
lines = index_file.read_text(encoding="utf-8").splitlines()
|
|
731
|
+
except OSError:
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
row_re = re.compile(r"^\|\s*%d\s*\|" % session_num)
|
|
735
|
+
in_history = False
|
|
736
|
+
for line in lines:
|
|
737
|
+
if "@@@auto:session-history" in line:
|
|
738
|
+
in_history = True
|
|
739
|
+
continue
|
|
740
|
+
if "@@@/auto:session-history" in line:
|
|
741
|
+
in_history = False
|
|
742
|
+
continue
|
|
743
|
+
if in_history and row_re.match(line):
|
|
744
|
+
return True
|
|
745
|
+
return False
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _entry_date_at(lines: list[str], marker_index: int) -> str | None:
|
|
749
|
+
"""The `**Date**:` value belonging to the entry whose marker is at `marker_index`.
|
|
750
|
+
|
|
751
|
+
`generate_session_content` renders it two lines below the marker. Scanning
|
|
752
|
+
a short window rather than a fixed offset keeps this working if blank-line
|
|
753
|
+
spacing around the header ever changes, and stops well before it could
|
|
754
|
+
reach the next entry.
|
|
755
|
+
"""
|
|
756
|
+
for line in lines[marker_index + 1:marker_index + 6]:
|
|
757
|
+
match = ENTRY_DATE_RE.match(line.strip())
|
|
758
|
+
if match:
|
|
759
|
+
return match.group(1)
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def resolve_effective_marker(
|
|
764
|
+
dev_dir: Path,
|
|
765
|
+
payload: dict,
|
|
766
|
+
marker: str,
|
|
767
|
+
) -> tuple[str, str | None]:
|
|
768
|
+
"""The marker this record actually carries, across both schemes.
|
|
769
|
+
|
|
770
|
+
Returns (marker, error). The v2 marker wins whenever an entry carries it,
|
|
771
|
+
which is every record written since the change and costs one scan.
|
|
772
|
+
|
|
773
|
+
Otherwise this looks for a pending record written under v1. Those markers
|
|
774
|
+
are only a hash, but the entry renders its own `**Date**:` line right
|
|
775
|
+
below, so the date that produced the v1 fingerprint is recoverable from
|
|
776
|
+
the entry itself: recompute with that date and compare against what is
|
|
777
|
+
actually written there. That is exact, and unlike a +/-1 day window it does
|
|
778
|
+
not quietly fail a retry resumed after a weekend.
|
|
779
|
+
|
|
780
|
+
Returning the v1 marker leaves the entry untouched — it is an in-flight
|
|
781
|
+
record about to be committed, and rewriting its marker mid-repair would
|
|
782
|
+
move it to a state the machine does not model.
|
|
783
|
+
"""
|
|
784
|
+
if find_marker_entries(dev_dir, marker):
|
|
785
|
+
return marker, None
|
|
786
|
+
|
|
787
|
+
matches: set[str] = set()
|
|
788
|
+
for journal in sorted(dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
|
|
789
|
+
if not journal.is_file():
|
|
790
|
+
continue
|
|
791
|
+
try:
|
|
792
|
+
lines = journal.read_text(encoding="utf-8").splitlines()
|
|
793
|
+
except OSError:
|
|
794
|
+
continue
|
|
795
|
+
for i, line in enumerate(lines):
|
|
796
|
+
legacy = LEGACY_MARKER_RE.match(line.strip())
|
|
797
|
+
if not legacy:
|
|
798
|
+
continue
|
|
799
|
+
date = _entry_date_at(lines, i)
|
|
800
|
+
if date is None:
|
|
801
|
+
continue
|
|
802
|
+
if compute_legacy_fingerprint(payload, date) == legacy.group(1):
|
|
803
|
+
matches.add(line.strip())
|
|
804
|
+
|
|
805
|
+
if len(matches) > 1:
|
|
806
|
+
return "", (
|
|
807
|
+
f"found {len(matches)} pending journal entries matching this record "
|
|
808
|
+
"under the pre-versioning marker scheme. Refusing to guess which one "
|
|
809
|
+
"to resume — remove the duplicate entry or pass --idempotency-key to "
|
|
810
|
+
"record a new session."
|
|
811
|
+
)
|
|
812
|
+
if matches:
|
|
813
|
+
return matches.pop(), None
|
|
814
|
+
return marker, None
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def classify_record(
|
|
818
|
+
repo_root: Path,
|
|
819
|
+
dev_dir: Path,
|
|
820
|
+
index_file: Path,
|
|
821
|
+
marker: str,
|
|
822
|
+
) -> tuple[str, Path | None, int | None, str | None]:
|
|
823
|
+
"""Classify the current state of this exact record.
|
|
824
|
+
|
|
825
|
+
Returns (state, journal_file, session_num, error). Only a unique, exact,
|
|
826
|
+
still-uncommitted match is ever adopted; anything ambiguous, malformed or
|
|
827
|
+
partially written comes back as an error so the caller fails safely.
|
|
828
|
+
"""
|
|
829
|
+
hits = find_marker_entries(dev_dir, marker)
|
|
830
|
+
|
|
831
|
+
if not hits:
|
|
832
|
+
return STATE_ABSENT, None, None, None
|
|
833
|
+
|
|
834
|
+
if len(hits) > 1:
|
|
835
|
+
files = ", ".join(sorted({p.name for p, _ in hits}))
|
|
836
|
+
return "", None, None, (
|
|
837
|
+
f"found {len(hits)} journal entries carrying this record's marker "
|
|
838
|
+
f"({files}). Refusing to guess which one to resume — remove the "
|
|
839
|
+
"duplicate entry or pass --idempotency-key to record a new session."
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
journal_file, session_num = hits[0]
|
|
843
|
+
if session_num is None:
|
|
844
|
+
return "", None, None, (
|
|
845
|
+
f"{journal_file.name} carries this record's marker without a "
|
|
846
|
+
"'## Session N:' heading above it. The pending record is malformed; "
|
|
847
|
+
"repair it by hand before retrying."
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
head_content = content_at_head(repo_root, journal_file)
|
|
851
|
+
if head_content is not None and marker in head_content:
|
|
852
|
+
return STATE_COMMITTED, journal_file, session_num, None
|
|
853
|
+
|
|
854
|
+
if index_has_session_row(index_file, session_num):
|
|
855
|
+
return STATE_INDEX_RECORDED, journal_file, session_num, None
|
|
856
|
+
|
|
857
|
+
recorded_total = get_current_session(index_file)
|
|
858
|
+
if recorded_total > session_num:
|
|
859
|
+
return "", None, None, (
|
|
860
|
+
f"journal entry for session {session_num} has no index row, but "
|
|
861
|
+
f"index.md already counts {recorded_total} sessions. Repairing the "
|
|
862
|
+
"row would rewrite a later record — resolve index.md by hand."
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
return STATE_JOURNAL_RECORDED, journal_file, session_num, None
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# =============================================================================
|
|
869
|
+
# Rendering
|
|
870
|
+
# =============================================================================
|
|
871
|
+
|
|
872
|
+
def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "- ") -> str:
|
|
873
|
+
"""Render a Markdown section as bullets, or "" when there is no content.
|
|
874
|
+
|
|
875
|
+
A section with zero provided values is omitted entirely from the
|
|
876
|
+
rendered entry rather than falling back to a placeholder string.
|
|
877
|
+
"""
|
|
878
|
+
if not items:
|
|
879
|
+
return ""
|
|
880
|
+
bullets = "\n".join(f"{bullet_prefix}{item}" for item in items)
|
|
881
|
+
return f"\n\n### {header}\n\n{bullets}"
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _render_main_changes(changes: list[str], extra_content: str | None) -> str:
|
|
885
|
+
"""Render the Main Changes section from --change bullets or freeform content."""
|
|
886
|
+
if changes:
|
|
887
|
+
return _render_bullet_section("Main Changes", changes)
|
|
888
|
+
if extra_content:
|
|
889
|
+
return f"\n\n### Main Changes\n\n{extra_content}"
|
|
890
|
+
return ""
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def generate_session_content(
|
|
894
|
+
session_num: int,
|
|
895
|
+
title: str,
|
|
896
|
+
evidence: list[tuple[str, str]],
|
|
897
|
+
summary: str,
|
|
898
|
+
today: str,
|
|
899
|
+
marker: str,
|
|
900
|
+
package: str | None = None,
|
|
901
|
+
branch: str | None = None,
|
|
902
|
+
changes: list[str] | None = None,
|
|
903
|
+
extra_content: str | None = None,
|
|
904
|
+
tests: list[str] | None = None,
|
|
905
|
+
next_steps: list[str] | None = None,
|
|
906
|
+
) -> str:
|
|
907
|
+
"""Generate session content."""
|
|
908
|
+
if evidence:
|
|
909
|
+
commit_table = """| Hash | Message |
|
|
910
|
+
|------|---------|"""
|
|
911
|
+
for oid, subject in evidence:
|
|
912
|
+
commit_table += f"\n| `{oid}` | {escape_markdown_cell(subject)} |"
|
|
913
|
+
else:
|
|
914
|
+
commit_table = "(No commits - planning session)"
|
|
915
|
+
|
|
916
|
+
package_line = f"\n**Package**: {package}" if package else ""
|
|
917
|
+
branch_line = f"\n**Branch**: `{branch}`" if branch else ""
|
|
918
|
+
|
|
919
|
+
main_changes_section = _render_main_changes(changes or [], extra_content)
|
|
920
|
+
testing_section = _render_bullet_section("Testing", tests or [], bullet_prefix="- [OK] ")
|
|
921
|
+
next_steps_section = _render_bullet_section("Next Steps", next_steps or [])
|
|
922
|
+
|
|
923
|
+
return f"""
|
|
924
|
+
|
|
925
|
+
## Session {session_num}: {title}
|
|
926
|
+
{marker}
|
|
927
|
+
|
|
928
|
+
**Date**: {today}
|
|
929
|
+
**Task**: {title}{package_line}{branch_line}
|
|
930
|
+
|
|
931
|
+
### Summary
|
|
932
|
+
|
|
933
|
+
{summary}{main_changes_section}
|
|
934
|
+
|
|
935
|
+
### Git Commits
|
|
936
|
+
|
|
937
|
+
{commit_table}{testing_section}
|
|
938
|
+
|
|
939
|
+
### Status
|
|
940
|
+
|
|
941
|
+
[OK] **Completed**{next_steps_section}
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def format_commit_display(evidence: list[tuple[str, str]]) -> str:
|
|
946
|
+
"""Index-table rendering of the commit OIDs."""
|
|
947
|
+
if not evidence:
|
|
948
|
+
return "-"
|
|
949
|
+
return ", ".join(f"`{oid}`" for oid, _ in evidence)
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def update_index(
|
|
953
|
+
index_file: Path,
|
|
954
|
+
dev_dir: Path,
|
|
955
|
+
title: str,
|
|
956
|
+
evidence: list[tuple[str, str]],
|
|
957
|
+
new_session: int,
|
|
958
|
+
active_file: str,
|
|
959
|
+
today: str,
|
|
960
|
+
branch: str | None = None,
|
|
961
|
+
) -> bool:
|
|
962
|
+
"""Update index.md with new session info."""
|
|
963
|
+
commit_display = format_commit_display(evidence)
|
|
964
|
+
|
|
965
|
+
# Get file number from active_file name
|
|
966
|
+
match = re.search(r"(\d+)", active_file)
|
|
967
|
+
active_num = int(match.group(1)) if match else 0
|
|
968
|
+
files_table = count_journal_files(dev_dir, active_num)
|
|
969
|
+
|
|
970
|
+
print(f"Updating index.md for session {new_session}...")
|
|
971
|
+
print(f" Title: {title}")
|
|
972
|
+
print(f" Commit: {commit_display}")
|
|
973
|
+
print(f" Active File: {active_file}")
|
|
974
|
+
print()
|
|
975
|
+
|
|
976
|
+
content = index_file.read_text(encoding="utf-8")
|
|
977
|
+
|
|
978
|
+
if "@@@auto:current-status" not in content:
|
|
979
|
+
print("Error: Markers not found in index.md. Please ensure markers exist.", file=sys.stderr)
|
|
980
|
+
return False
|
|
981
|
+
|
|
982
|
+
# Process sections
|
|
983
|
+
lines = content.splitlines()
|
|
984
|
+
new_lines = []
|
|
985
|
+
|
|
986
|
+
in_current_status = False
|
|
987
|
+
in_active_documents = False
|
|
988
|
+
in_session_history = False
|
|
989
|
+
header_written = False
|
|
990
|
+
|
|
991
|
+
for line in lines:
|
|
992
|
+
if "@@@auto:current-status" in line:
|
|
993
|
+
new_lines.append(line)
|
|
994
|
+
in_current_status = True
|
|
995
|
+
new_lines.append(f"- **Active File**: `{active_file}`")
|
|
996
|
+
new_lines.append(f"- **Total Sessions**: {new_session}")
|
|
997
|
+
new_lines.append(f"- **Last Active**: {today}")
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
if "@@@/auto:current-status" in line:
|
|
1001
|
+
in_current_status = False
|
|
1002
|
+
new_lines.append(line)
|
|
1003
|
+
continue
|
|
1004
|
+
|
|
1005
|
+
if "@@@auto:active-documents" in line:
|
|
1006
|
+
new_lines.append(line)
|
|
1007
|
+
in_active_documents = True
|
|
1008
|
+
new_lines.append("| File | Lines | Status |")
|
|
1009
|
+
new_lines.append("|------|-------|--------|")
|
|
1010
|
+
new_lines.append(files_table)
|
|
1011
|
+
continue
|
|
1012
|
+
|
|
1013
|
+
if "@@@/auto:active-documents" in line:
|
|
1014
|
+
in_active_documents = False
|
|
1015
|
+
new_lines.append(line)
|
|
1016
|
+
continue
|
|
1017
|
+
|
|
1018
|
+
if "@@@auto:session-history" in line:
|
|
1019
|
+
new_lines.append(line)
|
|
1020
|
+
in_session_history = True
|
|
1021
|
+
header_written = False
|
|
1022
|
+
continue
|
|
1023
|
+
|
|
1024
|
+
if "@@@/auto:session-history" in line:
|
|
1025
|
+
in_session_history = False
|
|
1026
|
+
new_lines.append(line)
|
|
1027
|
+
continue
|
|
1028
|
+
|
|
1029
|
+
if in_current_status:
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1032
|
+
if in_active_documents:
|
|
1033
|
+
continue
|
|
1034
|
+
|
|
1035
|
+
if in_session_history:
|
|
1036
|
+
# Migrate old 4/6-column headers to 5-column Branch-only history.
|
|
1037
|
+
if re.match(
|
|
1038
|
+
r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$",
|
|
1039
|
+
line,
|
|
1040
|
+
):
|
|
1041
|
+
new_lines.append("| # | Date | Title | Commits | Branch |")
|
|
1042
|
+
continue
|
|
1043
|
+
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line):
|
|
1044
|
+
new_lines.append("| # | Date | Title | Commits | Branch |")
|
|
1045
|
+
continue
|
|
1046
|
+
if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line):
|
|
1047
|
+
new_lines.append("| # | Date | Title | Commits | Branch |")
|
|
1048
|
+
continue
|
|
1049
|
+
if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written:
|
|
1050
|
+
new_lines.append("|---|------|-------|---------|--------|")
|
|
1051
|
+
new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |")
|
|
1052
|
+
header_written = True
|
|
1053
|
+
continue
|
|
1054
|
+
new_lines.append(line)
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
new_lines.append(line)
|
|
1058
|
+
|
|
1059
|
+
if not write_text_atomic(index_file, "\n".join(new_lines)):
|
|
1060
|
+
print(f"Error: failed to write {index_file}", file=sys.stderr)
|
|
1061
|
+
return False
|
|
1062
|
+
print("[OK] Updated index.md successfully!")
|
|
1063
|
+
return True
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
# =============================================================================
|
|
1067
|
+
# Main Function
|
|
1068
|
+
# =============================================================================
|
|
1069
|
+
|
|
1070
|
+
def _auto_commit_workspace(repo_root: Path) -> str:
|
|
1071
|
+
"""Stage Trellis-owned workspace + current-task paths and commit.
|
|
1072
|
+
|
|
1073
|
+
Path scope is restricted to specific products: the current developer's
|
|
1074
|
+
journal files + index.md, and ONLY the current task directory (resolved
|
|
1075
|
+
via ``get_current_task``). We never `git add` the whole `.trellis/` tree
|
|
1076
|
+
or iterate over all active task dirs (#303: parallel-window dirty task
|
|
1077
|
+
dirs must not be bundled into the session auto-commit). If `.gitignore`
|
|
1078
|
+
blocks the specific paths we warn + skip — never retry with ``-f``.
|
|
1079
|
+
|
|
1080
|
+
Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to
|
|
1081
|
+
``false``, this function returns immediately without touching git
|
|
1082
|
+
(journal/index files are still written to disk by the caller).
|
|
1083
|
+
|
|
1084
|
+
Returns one of ``COMMIT_DONE`` / ``COMMIT_SKIPPED`` / ``COMMIT_BLOCKED`` /
|
|
1085
|
+
``COMMIT_FAILED``. ``COMMIT_BLOCKED`` is the gitignored-`.trellis/` case:
|
|
1086
|
+
the user has told git to stay out of this tree, so it is a configured skip
|
|
1087
|
+
like ``session_auto_commit: false``, not a failure to retry.
|
|
1088
|
+
"""
|
|
1089
|
+
if not get_session_auto_commit(repo_root):
|
|
1090
|
+
print(
|
|
1091
|
+
"[OK] session_auto_commit: false — skipping git stage/commit.",
|
|
1092
|
+
file=sys.stderr,
|
|
1093
|
+
)
|
|
1094
|
+
return COMMIT_SKIPPED
|
|
1095
|
+
|
|
1096
|
+
# Not a git repository at all: like the gitignored case, this is an
|
|
1097
|
+
# environment the user configured, not a transient git error — a retry
|
|
1098
|
+
# can never succeed, so it must not be COMMIT_FAILED's exit-1 loop.
|
|
1099
|
+
rc, _, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=repo_root)
|
|
1100
|
+
if rc != 0:
|
|
1101
|
+
print(
|
|
1102
|
+
"[WARN] Not a git repository — journal/index written, "
|
|
1103
|
+
"skipping auto-commit.",
|
|
1104
|
+
file=sys.stderr,
|
|
1105
|
+
)
|
|
1106
|
+
return COMMIT_BLOCKED
|
|
1107
|
+
|
|
1108
|
+
commit_msg = get_session_commit_message(repo_root)
|
|
1109
|
+
# Resolve the current task so staging is scoped to its dir only. The ref
|
|
1110
|
+
# is ``.trellis/tasks/<name>`` (or under archive/) — pass the bare name.
|
|
1111
|
+
current = get_current_task(repo_root)
|
|
1112
|
+
if current:
|
|
1113
|
+
task_name = Path(current).name
|
|
1114
|
+
paths = safe_trellis_paths_to_add(repo_root, task_name=task_name)
|
|
1115
|
+
else:
|
|
1116
|
+
# Current task unknown (0 or >=2 parallel sessions — exactly the
|
|
1117
|
+
# parallel-window case #303 is about). Do NOT fall back to the wide
|
|
1118
|
+
# `tasks_dir.iterdir()` scan; that would re-leak other tasks' dirty
|
|
1119
|
+
# dirs into the session commit. Stage only the developer's journal/
|
|
1120
|
+
# index and skip every task dir.
|
|
1121
|
+
paths = [
|
|
1122
|
+
p
|
|
1123
|
+
for p in safe_trellis_paths_to_add(repo_root, task_name=None)
|
|
1124
|
+
if not p.startswith(f"{DIR_WORKFLOW}/{DIR_TASKS}/")
|
|
1125
|
+
]
|
|
1126
|
+
if not paths:
|
|
1127
|
+
print("[OK] No workspace changes to commit.", file=sys.stderr)
|
|
1128
|
+
return COMMIT_SKIPPED
|
|
1129
|
+
|
|
1130
|
+
success, _, err = safe_git_add(paths, repo_root)
|
|
1131
|
+
if not success:
|
|
1132
|
+
if err and "ignored by" in err.lower():
|
|
1133
|
+
print_gitignore_warning(paths)
|
|
1134
|
+
return COMMIT_BLOCKED
|
|
1135
|
+
print(
|
|
1136
|
+
f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
|
|
1137
|
+
file=sys.stderr,
|
|
1138
|
+
)
|
|
1139
|
+
return COMMIT_FAILED
|
|
1140
|
+
|
|
1141
|
+
# Check if there are staged changes for the paths we just staged.
|
|
1142
|
+
rc, _, _ = run_git(
|
|
1143
|
+
["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
|
|
1144
|
+
)
|
|
1145
|
+
if rc == 0:
|
|
1146
|
+
print("[OK] No workspace changes to commit.", file=sys.stderr)
|
|
1147
|
+
return COMMIT_SKIPPED
|
|
1148
|
+
|
|
1149
|
+
# Commit with an explicit pathspec: a bare `git commit` would sweep any
|
|
1150
|
+
# unrelated entries the developer had staged before this script ran into
|
|
1151
|
+
# the chore commit (#579). The pathspec keeps their staged work untouched.
|
|
1152
|
+
rc, _, commit_err = run_git(
|
|
1153
|
+
["commit", "-m", commit_msg, "--", *paths], cwd=repo_root
|
|
1154
|
+
)
|
|
1155
|
+
if rc == 0:
|
|
1156
|
+
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
1157
|
+
return COMMIT_DONE
|
|
1158
|
+
|
|
1159
|
+
print(
|
|
1160
|
+
f"[WARN] Auto-commit failed: {commit_err.strip()}",
|
|
1161
|
+
file=sys.stderr,
|
|
1162
|
+
)
|
|
1163
|
+
return COMMIT_FAILED
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _print_commit_checkpoint(session_num: int, journal_name: str) -> None:
|
|
1167
|
+
"""Actionable checkpoint for a failed auto-commit (R5)."""
|
|
1168
|
+
print("", file=sys.stderr)
|
|
1169
|
+
print(
|
|
1170
|
+
f"[BLOCKED] Checkpoint: session {session_num} is recorded in "
|
|
1171
|
+
f"{journal_name} and index.md, but the auto-commit did not complete.",
|
|
1172
|
+
file=sys.stderr,
|
|
1173
|
+
)
|
|
1174
|
+
print(
|
|
1175
|
+
"[BLOCKED] The pending record was left exactly as written — nothing was "
|
|
1176
|
+
"rolled back.",
|
|
1177
|
+
file=sys.stderr,
|
|
1178
|
+
)
|
|
1179
|
+
print(
|
|
1180
|
+
"[BLOCKED] Fix the git error above, then re-run the identical "
|
|
1181
|
+
"add_session.py command: it resumes at the commit step and will not add "
|
|
1182
|
+
"a second session.",
|
|
1183
|
+
file=sys.stderr,
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def add_session(
|
|
1188
|
+
title: str,
|
|
1189
|
+
commit: str = "-",
|
|
1190
|
+
summary: str = "Session summary was not supplied.",
|
|
1191
|
+
changes: list[str] | None = None,
|
|
1192
|
+
extra_content: str | None = None,
|
|
1193
|
+
tests: list[str] | None = None,
|
|
1194
|
+
next_steps: list[str] | None = None,
|
|
1195
|
+
auto_commit: bool = True,
|
|
1196
|
+
package: str | None = None,
|
|
1197
|
+
branch: str | None = None,
|
|
1198
|
+
commit_subjects: list[str] | None = None,
|
|
1199
|
+
idempotency_key: str | None = None,
|
|
1200
|
+
) -> int:
|
|
1201
|
+
"""Add a new session, resuming an interrupted one instead of duplicating it."""
|
|
1202
|
+
repo_root = get_repo_root()
|
|
1203
|
+
warn_if_parallel_worktree(repo_root)
|
|
1204
|
+
ensure_developer(repo_root)
|
|
1205
|
+
|
|
1206
|
+
developer = get_developer(repo_root)
|
|
1207
|
+
if not developer:
|
|
1208
|
+
print("Error: Developer not initialized", file=sys.stderr)
|
|
1209
|
+
return 1
|
|
1210
|
+
|
|
1211
|
+
dev_dir = get_workspace_dir(repo_root)
|
|
1212
|
+
if not dev_dir:
|
|
1213
|
+
print("Error: Workspace directory not found", file=sys.stderr)
|
|
1214
|
+
return 1
|
|
1215
|
+
|
|
1216
|
+
# -------------------------------------------------------------------
|
|
1217
|
+
# Preflight — no writes happen until every one of these succeeds.
|
|
1218
|
+
# -------------------------------------------------------------------
|
|
1219
|
+
if idempotency_key is not None and not IDEMPOTENCY_KEY_RE.match(idempotency_key):
|
|
1220
|
+
print(
|
|
1221
|
+
f"Error: invalid --idempotency-key '{idempotency_key}': expected 1-64 "
|
|
1222
|
+
"characters from [A-Za-z0-9._-]",
|
|
1223
|
+
file=sys.stderr,
|
|
1224
|
+
)
|
|
1225
|
+
return 1
|
|
1226
|
+
|
|
1227
|
+
tokens, token_error = parse_commit_tokens(commit)
|
|
1228
|
+
if token_error:
|
|
1229
|
+
print(f"Error: {token_error}", file=sys.stderr)
|
|
1230
|
+
return 1
|
|
1231
|
+
|
|
1232
|
+
overrides, override_error = parse_subject_overrides(commit_subjects)
|
|
1233
|
+
if override_error:
|
|
1234
|
+
print(f"Error: {override_error}", file=sys.stderr)
|
|
1235
|
+
return 1
|
|
1236
|
+
|
|
1237
|
+
evidence, evidence_error = build_commit_evidence(repo_root, tokens, overrides)
|
|
1238
|
+
if evidence_error:
|
|
1239
|
+
print(f"Error: {evidence_error}", file=sys.stderr)
|
|
1240
|
+
return 1
|
|
1241
|
+
|
|
1242
|
+
max_lines = get_max_journal_lines(repo_root)
|
|
1243
|
+
index_file = dev_dir / "index.md"
|
|
1244
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
1245
|
+
|
|
1246
|
+
payload = _fingerprint_payload(
|
|
1247
|
+
developer, title, summary, package, branch, evidence,
|
|
1248
|
+
changes, extra_content, tests, next_steps, idempotency_key,
|
|
1249
|
+
)
|
|
1250
|
+
marker = render_marker(compute_record_fingerprint(payload))
|
|
1251
|
+
|
|
1252
|
+
# `today` is no longer a fingerprint input; a record has to be findable by
|
|
1253
|
+
# its own retry after a date rollover. It still dates the rendered entry.
|
|
1254
|
+
marker, resolve_error = resolve_effective_marker(dev_dir, payload, marker)
|
|
1255
|
+
if resolve_error:
|
|
1256
|
+
print(f"Error: {resolve_error}", file=sys.stderr)
|
|
1257
|
+
return 1
|
|
1258
|
+
|
|
1259
|
+
state, matched_file, matched_num, classify_error = classify_record(
|
|
1260
|
+
repo_root, dev_dir, index_file, marker
|
|
1261
|
+
)
|
|
1262
|
+
if classify_error:
|
|
1263
|
+
print(f"Error: {classify_error}", file=sys.stderr)
|
|
1264
|
+
return 1
|
|
1265
|
+
|
|
1266
|
+
if state == STATE_COMMITTED and idempotency_key:
|
|
1267
|
+
print(
|
|
1268
|
+
f"[OK] Session {matched_num} with idempotency key "
|
|
1269
|
+
f"'{idempotency_key}' is already recorded and committed in "
|
|
1270
|
+
f"{matched_file.name if matched_file else 'the journal'}; "
|
|
1271
|
+
"nothing to do.",
|
|
1272
|
+
file=sys.stderr,
|
|
1273
|
+
)
|
|
1274
|
+
return 0
|
|
1275
|
+
|
|
1276
|
+
# A committed record is finished, so an identical later request is a
|
|
1277
|
+
# legitimately new session. It must not reuse the committed marker: two
|
|
1278
|
+
# entries carrying one marker make every later run ambiguous, and
|
|
1279
|
+
# classify_record refuses to guess between them. Step to the next
|
|
1280
|
+
# generation of this record instead. The marker stays a pure function of
|
|
1281
|
+
# (payload, generation), so a retry of the new entry recomputes the same
|
|
1282
|
+
# marker and still resumes; generation 0 omits the field entirely, leaving
|
|
1283
|
+
# first-time markers byte-identical to what this scheme already writes.
|
|
1284
|
+
generation = 0
|
|
1285
|
+
while state == STATE_COMMITTED:
|
|
1286
|
+
generation += 1
|
|
1287
|
+
marker = render_marker(
|
|
1288
|
+
compute_record_fingerprint({**payload, "generation": generation})
|
|
1289
|
+
)
|
|
1290
|
+
state, matched_file, matched_num, classify_error = classify_record(
|
|
1291
|
+
repo_root, dev_dir, index_file, marker
|
|
1292
|
+
)
|
|
1293
|
+
if classify_error:
|
|
1294
|
+
print(f"Error: {classify_error}", file=sys.stderr)
|
|
1295
|
+
return 1
|
|
1296
|
+
|
|
1297
|
+
print("========================================", file=sys.stderr)
|
|
1298
|
+
print("ADD SESSION", file=sys.stderr)
|
|
1299
|
+
print("========================================", file=sys.stderr)
|
|
1300
|
+
print("", file=sys.stderr)
|
|
1301
|
+
|
|
1302
|
+
# -------------------------------------------------------------------
|
|
1303
|
+
# Absent → journal-recorded
|
|
1304
|
+
# -------------------------------------------------------------------
|
|
1305
|
+
target_file: Path | None
|
|
1306
|
+
target_num: int
|
|
1307
|
+
new_session: int
|
|
1308
|
+
|
|
1309
|
+
if state == STATE_ABSENT:
|
|
1310
|
+
journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
|
|
1311
|
+
new_session = resolve_next_session(repo_root, dev_dir, index_file)
|
|
1312
|
+
|
|
1313
|
+
session_content = generate_session_content(
|
|
1314
|
+
new_session, title, evidence, summary, today, marker, package, branch,
|
|
1315
|
+
changes=changes, extra_content=extra_content, tests=tests,
|
|
1316
|
+
next_steps=next_steps,
|
|
1317
|
+
)
|
|
1318
|
+
content_lines = len(session_content.splitlines())
|
|
1319
|
+
|
|
1320
|
+
print(f"Session: {new_session}", file=sys.stderr)
|
|
1321
|
+
print(f"Title: {title}", file=sys.stderr)
|
|
1322
|
+
print(f"Commit: {format_commit_display(evidence)}", file=sys.stderr)
|
|
1323
|
+
print("", file=sys.stderr)
|
|
1324
|
+
print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
|
|
1325
|
+
print(f"Current lines: {current_lines}", file=sys.stderr)
|
|
1326
|
+
print(f"New content lines: {content_lines}", file=sys.stderr)
|
|
1327
|
+
print(f"Total after append: {current_lines + content_lines}", file=sys.stderr)
|
|
1328
|
+
print("", file=sys.stderr)
|
|
1329
|
+
|
|
1330
|
+
target_file = journal_file
|
|
1331
|
+
target_num = current_num
|
|
1332
|
+
|
|
1333
|
+
if current_lines + content_lines > max_lines:
|
|
1334
|
+
target_num = current_num + 1
|
|
1335
|
+
print(f"[!] Exceeds {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
|
|
1336
|
+
target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
|
|
1337
|
+
if target_file is None:
|
|
1338
|
+
print(
|
|
1339
|
+
f"Error: failed to create {FILE_JOURNAL_PREFIX}{target_num}.md; "
|
|
1340
|
+
"nothing was recorded.",
|
|
1341
|
+
file=sys.stderr,
|
|
1342
|
+
)
|
|
1343
|
+
return 1
|
|
1344
|
+
print(f"Created: {target_file}", file=sys.stderr)
|
|
1345
|
+
|
|
1346
|
+
if target_file is None:
|
|
1347
|
+
print(
|
|
1348
|
+
"Error: no journal file to append to. Run init_developer.py first.",
|
|
1349
|
+
file=sys.stderr,
|
|
1350
|
+
)
|
|
1351
|
+
return 1
|
|
1352
|
+
|
|
1353
|
+
try:
|
|
1354
|
+
existing = target_file.read_text(encoding="utf-8") if target_file.is_file() else ""
|
|
1355
|
+
except OSError as exc:
|
|
1356
|
+
print(f"Error: cannot read {target_file}: {exc}", file=sys.stderr)
|
|
1357
|
+
return 1
|
|
1358
|
+
|
|
1359
|
+
if not write_text_atomic(target_file, existing + session_content):
|
|
1360
|
+
print(
|
|
1361
|
+
f"Error: failed to append the session to {target_file.name}; "
|
|
1362
|
+
"the previous journal content is intact and nothing was recorded.",
|
|
1363
|
+
file=sys.stderr,
|
|
1364
|
+
)
|
|
1365
|
+
return 1
|
|
1366
|
+
print(f"[OK] Appended session to {target_file.name}", file=sys.stderr)
|
|
1367
|
+
state = STATE_JOURNAL_RECORDED
|
|
1368
|
+
else:
|
|
1369
|
+
if matched_file is None or matched_num is None:
|
|
1370
|
+
print(
|
|
1371
|
+
f"Error: resumed state '{state}' without a matching journal entry.",
|
|
1372
|
+
file=sys.stderr,
|
|
1373
|
+
)
|
|
1374
|
+
return 1
|
|
1375
|
+
target_file = matched_file
|
|
1376
|
+
new_session = matched_num
|
|
1377
|
+
target_num = _extract_journal_num(target_file.stem)
|
|
1378
|
+
print(
|
|
1379
|
+
f"[RESUME] Session {new_session} is already in {target_file.name} and "
|
|
1380
|
+
f"uncommitted; continuing from '{state}' instead of appending again.",
|
|
1381
|
+
file=sys.stderr,
|
|
1382
|
+
)
|
|
1383
|
+
print("", file=sys.stderr)
|
|
1384
|
+
|
|
1385
|
+
print("", file=sys.stderr)
|
|
1386
|
+
|
|
1387
|
+
# -------------------------------------------------------------------
|
|
1388
|
+
# Journal-recorded → index-recorded
|
|
1389
|
+
# -------------------------------------------------------------------
|
|
1390
|
+
active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
|
|
1391
|
+
if state == STATE_JOURNAL_RECORDED:
|
|
1392
|
+
if not update_index(
|
|
1393
|
+
index_file,
|
|
1394
|
+
dev_dir,
|
|
1395
|
+
title,
|
|
1396
|
+
evidence,
|
|
1397
|
+
new_session,
|
|
1398
|
+
active_file,
|
|
1399
|
+
today,
|
|
1400
|
+
branch,
|
|
1401
|
+
):
|
|
1402
|
+
print(
|
|
1403
|
+
f"[BLOCKED] Checkpoint: session {new_session} is in "
|
|
1404
|
+
f"{active_file} but index.md was not updated. Fix index.md, then "
|
|
1405
|
+
"re-run the identical command to repair the row without adding a "
|
|
1406
|
+
"second session.",
|
|
1407
|
+
file=sys.stderr,
|
|
1408
|
+
)
|
|
1409
|
+
return 1
|
|
1410
|
+
state = STATE_INDEX_RECORDED
|
|
1411
|
+
else:
|
|
1412
|
+
print(f"[OK] index.md already records session {new_session}.", file=sys.stderr)
|
|
1413
|
+
|
|
1414
|
+
print("", file=sys.stderr)
|
|
1415
|
+
print("========================================", file=sys.stderr)
|
|
1416
|
+
print(f"[OK] Session {new_session} added successfully!", file=sys.stderr)
|
|
1417
|
+
print("========================================", file=sys.stderr)
|
|
1418
|
+
print("", file=sys.stderr)
|
|
1419
|
+
print("Files updated:", file=sys.stderr)
|
|
1420
|
+
print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
|
|
1421
|
+
print(" - index.md", file=sys.stderr)
|
|
1422
|
+
|
|
1423
|
+
# -------------------------------------------------------------------
|
|
1424
|
+
# Index-recorded → committed
|
|
1425
|
+
# -------------------------------------------------------------------
|
|
1426
|
+
if not auto_commit:
|
|
1427
|
+
return 0
|
|
1428
|
+
|
|
1429
|
+
print("", file=sys.stderr)
|
|
1430
|
+
outcome = _auto_commit_workspace(repo_root)
|
|
1431
|
+
|
|
1432
|
+
if outcome == COMMIT_FAILED:
|
|
1433
|
+
_print_commit_checkpoint(
|
|
1434
|
+
new_session, target_file.name if target_file else "the journal"
|
|
1435
|
+
)
|
|
1436
|
+
return 1
|
|
1437
|
+
|
|
1438
|
+
if outcome == COMMIT_DONE and target_file is not None:
|
|
1439
|
+
committed = content_at_head(repo_root, target_file)
|
|
1440
|
+
if committed is None or marker not in committed:
|
|
1441
|
+
print(
|
|
1442
|
+
f"[WARN] Auto-commit reported success but {target_file.name} at "
|
|
1443
|
+
"HEAD does not contain this session's record.",
|
|
1444
|
+
file=sys.stderr,
|
|
1445
|
+
)
|
|
1446
|
+
_print_commit_checkpoint(new_session, target_file.name)
|
|
1447
|
+
return 1
|
|
1448
|
+
|
|
1449
|
+
return 0
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
# =============================================================================
|
|
1453
|
+
# Main Entry
|
|
1454
|
+
# =============================================================================
|
|
1455
|
+
|
|
1456
|
+
def main() -> int:
|
|
1457
|
+
"""CLI entry point."""
|
|
1458
|
+
parser = argparse.ArgumentParser(
|
|
1459
|
+
description="Add a new session to journal file and update index.md"
|
|
1460
|
+
)
|
|
1461
|
+
parser.add_argument("--title", required=True, help="Session title")
|
|
1462
|
+
parser.add_argument(
|
|
1463
|
+
"--commit",
|
|
1464
|
+
default="-",
|
|
1465
|
+
help=(
|
|
1466
|
+
"Comma-separated commit OIDs (7-40 hex chars each). Each one is "
|
|
1467
|
+
"resolved to its real subject before anything is written; an "
|
|
1468
|
+
"unresolvable OID fails the command. Use '-' for a planning session."
|
|
1469
|
+
),
|
|
1470
|
+
)
|
|
1471
|
+
parser.add_argument(
|
|
1472
|
+
"--commit-subject",
|
|
1473
|
+
action="append",
|
|
1474
|
+
metavar="OID=SUBJECT",
|
|
1475
|
+
help=(
|
|
1476
|
+
"Explicit subject for a commit that cannot be resolved locally "
|
|
1477
|
+
"(repeatable). The mapping must be one-to-one with --commit."
|
|
1478
|
+
),
|
|
1479
|
+
)
|
|
1480
|
+
parser.add_argument("--summary", default="Session summary was not supplied.", help="Brief summary")
|
|
1481
|
+
parser.add_argument("--content-file", help="Path to file with detailed content")
|
|
1482
|
+
parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)")
|
|
1483
|
+
parser.add_argument("--branch", help="Branch name (auto-detected if omitted)")
|
|
1484
|
+
parser.add_argument("--change", action="append", help="Main Changes bullet (repeatable)")
|
|
1485
|
+
parser.add_argument("--test", action="append", help="Testing bullet (repeatable)")
|
|
1486
|
+
parser.add_argument("--next-step", action="append", help="Next Steps bullet (repeatable)")
|
|
1487
|
+
parser.add_argument(
|
|
1488
|
+
"--idempotency-key",
|
|
1489
|
+
help=(
|
|
1490
|
+
"Caller-supplied retry key ([A-Za-z0-9._-], 1-64 chars). Makes an "
|
|
1491
|
+
"already-committed identical record a no-op instead of a new session."
|
|
1492
|
+
),
|
|
1493
|
+
)
|
|
1494
|
+
parser.add_argument("--no-commit", action="store_true",
|
|
1495
|
+
help="Skip auto-commit of workspace changes")
|
|
1496
|
+
parser.add_argument("--stdin", action="store_true",
|
|
1497
|
+
help="Read extra content from stdin (explicit opt-in)")
|
|
1498
|
+
|
|
1499
|
+
args = parser.parse_args()
|
|
1500
|
+
|
|
1501
|
+
extra_content: str | None = None
|
|
1502
|
+
if args.content_file:
|
|
1503
|
+
content_path = Path(args.content_file)
|
|
1504
|
+
if content_path.is_file():
|
|
1505
|
+
extra_content = content_path.read_text(encoding="utf-8")
|
|
1506
|
+
elif args.stdin:
|
|
1507
|
+
extra_content = sys.stdin.read()
|
|
1508
|
+
|
|
1509
|
+
# Load active task once — shared by package and branch resolution
|
|
1510
|
+
repo_root = get_repo_root()
|
|
1511
|
+
current = get_current_task(repo_root)
|
|
1512
|
+
task_data = load_task(repo_root / current) if current else None
|
|
1513
|
+
|
|
1514
|
+
package = args.package
|
|
1515
|
+
if package:
|
|
1516
|
+
# CLI source: fail-fast in monorepo, ignore in single-repo
|
|
1517
|
+
if not is_monorepo(repo_root):
|
|
1518
|
+
print("Warning: --package ignored in single-repo project", file=sys.stderr)
|
|
1519
|
+
package = None
|
|
1520
|
+
elif not validate_package(package, repo_root):
|
|
1521
|
+
packages = get_packages(repo_root)
|
|
1522
|
+
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
|
|
1523
|
+
print(f"Error: unknown package '{package}'. Available: {available}", file=sys.stderr)
|
|
1524
|
+
return 1
|
|
1525
|
+
else:
|
|
1526
|
+
# Inferred: active task's task.json.package → default_package → None
|
|
1527
|
+
task_package = task_data.package if task_data else None
|
|
1528
|
+
package = resolve_package(task_package, repo_root)
|
|
1529
|
+
|
|
1530
|
+
branch = resolve_session_branch(repo_root, args.branch, task_data)
|
|
1531
|
+
|
|
1532
|
+
return add_session(
|
|
1533
|
+
args.title, args.commit, args.summary,
|
|
1534
|
+
changes=args.change, extra_content=extra_content, tests=args.test,
|
|
1535
|
+
next_steps=args.next_step,
|
|
1536
|
+
auto_commit=not args.no_commit,
|
|
1537
|
+
package=package,
|
|
1538
|
+
branch=branch,
|
|
1539
|
+
commit_subjects=args.commit_subject,
|
|
1540
|
+
idempotency_key=args.idempotency_key,
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
if __name__ == "__main__":
|
|
1545
|
+
sys.exit(main())
|