@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,1874 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Task CRUD operations.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
ensure_tasks_dir - Ensure tasks directory exists
|
|
7
|
+
cmd_create - Create a new task
|
|
8
|
+
cmd_rename - Rename a task and every reference to it
|
|
9
|
+
cmd_archive - Archive completed task
|
|
10
|
+
cmd_set_branch - Set git branch for task
|
|
11
|
+
cmd_set_base_branch - Set PR target branch
|
|
12
|
+
cmd_set_scope - Set scope for PR title
|
|
13
|
+
cmd_set_meta - Set/overwrite a task metadata key
|
|
14
|
+
cmd_add_subtask - Link child task to parent
|
|
15
|
+
cmd_remove_subtask - Unlink child task from parent
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from .config import (
|
|
28
|
+
get_codex_dispatch_mode,
|
|
29
|
+
get_packages,
|
|
30
|
+
get_session_auto_commit,
|
|
31
|
+
is_monorepo,
|
|
32
|
+
resolve_package,
|
|
33
|
+
validate_package,
|
|
34
|
+
)
|
|
35
|
+
from .git import (
|
|
36
|
+
INDEX_LOCK_RETRY_ATTEMPTS,
|
|
37
|
+
branch_exists_locally,
|
|
38
|
+
has_git_remote,
|
|
39
|
+
index_lock_path,
|
|
40
|
+
resolve_default_branch,
|
|
41
|
+
run_git,
|
|
42
|
+
run_git_retry_index_lock,
|
|
43
|
+
stderr_indicates_index_lock,
|
|
44
|
+
)
|
|
45
|
+
from .io import describe_json_read_failure, read_json_checked, write_json
|
|
46
|
+
from .log import Colors, colored
|
|
47
|
+
from .paths import (
|
|
48
|
+
DEVELOPER_HINT,
|
|
49
|
+
DIR_ARCHIVE,
|
|
50
|
+
DIR_TASKS,
|
|
51
|
+
DIR_WORKFLOW,
|
|
52
|
+
FILE_TASK_JSON,
|
|
53
|
+
generate_task_date_prefix,
|
|
54
|
+
get_developer,
|
|
55
|
+
get_repo_root,
|
|
56
|
+
get_tasks_dir,
|
|
57
|
+
)
|
|
58
|
+
from .safe_commit import (
|
|
59
|
+
print_gitignore_warning,
|
|
60
|
+
safe_archive_paths_to_add,
|
|
61
|
+
safe_git_add,
|
|
62
|
+
)
|
|
63
|
+
from .task_utils import (
|
|
64
|
+
archive_destination_for,
|
|
65
|
+
archive_task_complete,
|
|
66
|
+
find_task_by_name,
|
|
67
|
+
is_within_tasks_dir,
|
|
68
|
+
resolve_task_dir,
|
|
69
|
+
run_task_hooks,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Helper Functions
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
# Characters stripped before deciding whether a title / description is empty.
|
|
78
|
+
# This is deliberately the *union* of Python's str.strip() default set and
|
|
79
|
+
# ECMAScript's String.trim() set, because the two disagree: Python strips
|
|
80
|
+
# U+0085 (NEL) and U+001C-U+001F, which JS keeps; JS strips U+FEFF (BOM),
|
|
81
|
+
# which Python keeps. Stripping the union means anything create accepts is
|
|
82
|
+
# still non-empty after either side trims it, so a record cannot pass here and
|
|
83
|
+
# then be refused as empty by a JS pre-archive validator.
|
|
84
|
+
BLANK_CHARS = (
|
|
85
|
+
"\t\n\v\f\r" # U+0009-U+000D
|
|
86
|
+
"\x1c\x1d\x1e\x1f" # U+001C-U+001F separators (Python only)
|
|
87
|
+
" " # U+0020 space
|
|
88
|
+
"\x85" # U+0085 next line (Python only)
|
|
89
|
+
"\xa0" # U+00A0 no-break space
|
|
90
|
+
"\u1680" # ogham space mark
|
|
91
|
+
"\u2000\u2001\u2002\u2003\u2004\u2005" # en/em quad, en/em/three/four-per-em
|
|
92
|
+
"\u2006\u2007\u2008\u2009\u200a" # six-per-em .. hair space
|
|
93
|
+
"\u2028\u2029" # line / paragraph separator
|
|
94
|
+
"\u202f\u205f\u3000" # narrow nbsp, math space, ideographic space
|
|
95
|
+
"\ufeff" # zero-width no-break space / BOM (JS only)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def strip_blank(value: str | None) -> str:
|
|
100
|
+
"""Return ``value`` with :data:`BLANK_CHARS` trimmed from both ends."""
|
|
101
|
+
return (value or "").strip(BLANK_CHARS)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _slugify(title: str) -> str:
|
|
105
|
+
"""Convert title to slug (only works with ASCII)."""
|
|
106
|
+
result = title.lower()
|
|
107
|
+
result = re.sub(r"[^a-z0-9]", "-", result)
|
|
108
|
+
result = re.sub(r"-+", "-", result)
|
|
109
|
+
result = result.strip("-")
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ensure_tasks_dir(repo_root: Path) -> Path:
|
|
114
|
+
"""Ensure tasks directory exists."""
|
|
115
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
116
|
+
archive_dir = tasks_dir / "archive"
|
|
117
|
+
|
|
118
|
+
if not tasks_dir.exists():
|
|
119
|
+
tasks_dir.mkdir(parents=True)
|
|
120
|
+
print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
|
|
121
|
+
|
|
122
|
+
if not archive_dir.exists():
|
|
123
|
+
archive_dir.mkdir(parents=True)
|
|
124
|
+
|
|
125
|
+
return tasks_dir
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _find_archived_task_by_dir_name(tasks_dir: Path, dir_name: str) -> Path | None:
|
|
129
|
+
"""Find an archived task directory with the exact active-task dir name."""
|
|
130
|
+
archive_dir = tasks_dir / DIR_ARCHIVE
|
|
131
|
+
if not archive_dir.is_dir():
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
for month_dir in sorted(archive_dir.iterdir()):
|
|
135
|
+
if not month_dir.is_dir():
|
|
136
|
+
continue
|
|
137
|
+
candidate = month_dir / dir_name
|
|
138
|
+
if candidate.is_dir():
|
|
139
|
+
return candidate
|
|
140
|
+
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _repo_relative_path(path: Path, repo_root: Path) -> str:
|
|
145
|
+
"""Format a path relative to the repo root when possible."""
|
|
146
|
+
try:
|
|
147
|
+
return path.relative_to(repo_root).as_posix()
|
|
148
|
+
except ValueError:
|
|
149
|
+
return str(path)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _report_read_failure(path: Path, reason: str | None) -> None:
|
|
153
|
+
"""Print why a JSON file could not be loaded, and what to do about it.
|
|
154
|
+
|
|
155
|
+
These commands are about to rewrite the file they just failed to read, so
|
|
156
|
+
this message is the last warning the user gets. Exiting non-zero with no
|
|
157
|
+
output at all leaves a parse error, a permissions error and an empty file
|
|
158
|
+
indistinguishable.
|
|
159
|
+
"""
|
|
160
|
+
problem, hint = describe_json_read_failure(path, reason)
|
|
161
|
+
print(colored(f"Error: {problem}", Colors.RED), file=sys.stderr)
|
|
162
|
+
print(hint, file=sys.stderr)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _ensure_children_list(data: dict) -> list:
|
|
166
|
+
"""The task's `children` as a list, repaired in place if it is not one.
|
|
167
|
+
|
|
168
|
+
`.get(key, default)` returns the default only when the key is *absent*. A
|
|
169
|
+
task.json carrying `"children": null` — older format, or hand-edited —
|
|
170
|
+
yields None, and every caller below goes on to use the result as a list.
|
|
171
|
+
In `cmd_create` that raise lands *after* the new task.json is written,
|
|
172
|
+
leaving a task on disk its parent does not reference.
|
|
173
|
+
|
|
174
|
+
The repair is written into `data` rather than only returned, because the
|
|
175
|
+
unlink path removes a name it may not find and would otherwise persist the
|
|
176
|
+
malformed value untouched — no crash, but the next caller inherits it.
|
|
177
|
+
|
|
178
|
+
Anything that is not a list is discarded rather than coerced: a string
|
|
179
|
+
would iterate per character and a dict per key, each producing a plausible
|
|
180
|
+
child set that is not one.
|
|
181
|
+
"""
|
|
182
|
+
children = data.get("children")
|
|
183
|
+
if not isinstance(children, list):
|
|
184
|
+
children = []
|
|
185
|
+
data["children"] = children
|
|
186
|
+
return children
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _report_write_failure(path: Path) -> None:
|
|
190
|
+
"""Print that a JSON write failed. Writes are atomic, so nothing changed."""
|
|
191
|
+
print(colored(f"Error: Failed to write {path}", Colors.RED), file=sys.stderr)
|
|
192
|
+
print(
|
|
193
|
+
"The existing file is unchanged (writes are atomic). "
|
|
194
|
+
"Check permissions and free disk space, then retry.",
|
|
195
|
+
file=sys.stderr,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# Sub-agent platform detection + JSONL context files
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
# Config directories of platforms that consume implement.jsonl / check.jsonl.
|
|
204
|
+
# Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
|
|
205
|
+
# platforms listed in workflow.md's "agent-capable" Skill Routing block.
|
|
206
|
+
# Codex is checked separately because explicit inline mode does not consume
|
|
207
|
+
# JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
|
|
208
|
+
# specs through skills instead of JSONL.
|
|
209
|
+
_SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
|
|
210
|
+
".claude",
|
|
211
|
+
".cursor",
|
|
212
|
+
".kiro",
|
|
213
|
+
".gemini",
|
|
214
|
+
".opencode",
|
|
215
|
+
".qoder",
|
|
216
|
+
".codebuddy",
|
|
217
|
+
".factory", # Factory Droid
|
|
218
|
+
".github/copilot",
|
|
219
|
+
".pi", # Pi Agent
|
|
220
|
+
".trae", # Trae IDE
|
|
221
|
+
".omp", # Oh My Pi
|
|
222
|
+
".zcode", # ZCode
|
|
223
|
+
".grok", # Grok Build
|
|
224
|
+
".kimi-code", # Kimi Code
|
|
225
|
+
)
|
|
226
|
+
_CODEX_CONFIG_DIR = ".codex"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _has_subagent_platform(repo_root: Path) -> bool:
|
|
230
|
+
"""Return True if any sub-agent-capable platform is configured.
|
|
231
|
+
|
|
232
|
+
Detected by probing well-known config directories at the repo root. Codex
|
|
233
|
+
counts by default through ``codex.dispatch_mode: auto`` (including the
|
|
234
|
+
legacy ``sub-agent`` alias); explicit inline mode loads context through
|
|
235
|
+
skills, not JSONL.
|
|
236
|
+
"""
|
|
237
|
+
for config_dir in _SUBAGENT_CONFIG_DIRS:
|
|
238
|
+
if (repo_root / config_dir).is_dir():
|
|
239
|
+
return True
|
|
240
|
+
if (repo_root / _CODEX_CONFIG_DIR).is_dir():
|
|
241
|
+
return get_codex_dispatch_mode(repo_root) == "auto"
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
|
|
246
|
+
"""Parse repeatable ``--meta key=value`` pairs into a dict.
|
|
247
|
+
|
|
248
|
+
Returns ``None`` (after printing an error naming the bad value) on the
|
|
249
|
+
first malformed pair: missing ``=`` or an empty key. Values are stored
|
|
250
|
+
as-is (strings, no nesting, no type coercion).
|
|
251
|
+
"""
|
|
252
|
+
meta: dict[str, str] = {}
|
|
253
|
+
for pair in pairs or []:
|
|
254
|
+
key, sep, value = pair.partition("=")
|
|
255
|
+
if not sep or not key:
|
|
256
|
+
print(
|
|
257
|
+
colored(f"Error: malformed --meta value '{pair}' (expected key=value)", Colors.RED),
|
|
258
|
+
file=sys.stderr,
|
|
259
|
+
)
|
|
260
|
+
return None
|
|
261
|
+
meta[key] = value
|
|
262
|
+
return meta
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _default_prd_content(title: str, description: str | None = None) -> str:
|
|
266
|
+
"""Return the default PRD skeleton created with every task."""
|
|
267
|
+
goal = (description or "").strip() or "TBD."
|
|
268
|
+
heading = title.strip() or "Untitled task"
|
|
269
|
+
return f"""# {heading}
|
|
270
|
+
|
|
271
|
+
## Goal
|
|
272
|
+
|
|
273
|
+
{goal}
|
|
274
|
+
|
|
275
|
+
## Requirements
|
|
276
|
+
|
|
277
|
+
- TBD
|
|
278
|
+
|
|
279
|
+
## Acceptance Criteria
|
|
280
|
+
|
|
281
|
+
- [ ] TBD
|
|
282
|
+
|
|
283
|
+
## Notes
|
|
284
|
+
|
|
285
|
+
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
|
286
|
+
- Lightweight tasks can remain PRD-only.
|
|
287
|
+
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# =============================================================================
|
|
292
|
+
# Command: create
|
|
293
|
+
# =============================================================================
|
|
294
|
+
|
|
295
|
+
def cmd_create(args: argparse.Namespace) -> int:
|
|
296
|
+
"""Create a new task."""
|
|
297
|
+
repo_root = get_repo_root()
|
|
298
|
+
|
|
299
|
+
# Title and description are checked first, before ensure_tasks_dir and any
|
|
300
|
+
# other write, so a rejected create leaves the tree untouched. Both are
|
|
301
|
+
# required non-empty by pre-archive validation; catching that here costs
|
|
302
|
+
# one retyped command instead of blocking a PR hours later.
|
|
303
|
+
if not strip_blank(args.title):
|
|
304
|
+
print(colored("Error: title is required", Colors.RED), file=sys.stderr)
|
|
305
|
+
print(
|
|
306
|
+
'Pass a non-empty <title> (whitespace only does not count): a task '
|
|
307
|
+
"with an empty title is refused at archive.",
|
|
308
|
+
file=sys.stderr,
|
|
309
|
+
)
|
|
310
|
+
return 1
|
|
311
|
+
|
|
312
|
+
description = strip_blank(getattr(args, "description", None))
|
|
313
|
+
if not description:
|
|
314
|
+
print(colored("Error: --description is required", Colors.RED), file=sys.stderr)
|
|
315
|
+
print(
|
|
316
|
+
'Pass --description "<what this task delivers>" (whitespace only does '
|
|
317
|
+
"not count): a task with an empty description is refused at archive.",
|
|
318
|
+
file=sys.stderr,
|
|
319
|
+
)
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
# Validate --meta (CLI source: fail-fast, before any directory is created)
|
|
323
|
+
meta = _parse_meta_pairs(getattr(args, "meta", None))
|
|
324
|
+
if meta is None:
|
|
325
|
+
return 1
|
|
326
|
+
|
|
327
|
+
# Validate --package (CLI source: fail-fast)
|
|
328
|
+
package: str | None = getattr(args, "package", None)
|
|
329
|
+
if not is_monorepo(repo_root):
|
|
330
|
+
# Single-repo: ignore --package, no package prefix
|
|
331
|
+
if package:
|
|
332
|
+
print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
|
|
333
|
+
package = None
|
|
334
|
+
elif package:
|
|
335
|
+
if not validate_package(package, repo_root):
|
|
336
|
+
packages = get_packages(repo_root)
|
|
337
|
+
available = ", ".join(sorted(packages.keys())) if packages else "(none)"
|
|
338
|
+
print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr)
|
|
339
|
+
return 1
|
|
340
|
+
else:
|
|
341
|
+
# Inferred: default_package → None (no task.json yet for create)
|
|
342
|
+
package = resolve_package(repo_root=repo_root)
|
|
343
|
+
|
|
344
|
+
# Default assignee to current developer
|
|
345
|
+
assignee = args.assignee
|
|
346
|
+
if not assignee:
|
|
347
|
+
assignee = get_developer(repo_root)
|
|
348
|
+
if not assignee:
|
|
349
|
+
print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
|
|
350
|
+
print(DEVELOPER_HINT, file=sys.stderr)
|
|
351
|
+
return 1
|
|
352
|
+
|
|
353
|
+
ensure_tasks_dir(repo_root)
|
|
354
|
+
|
|
355
|
+
# Get current developer as creator
|
|
356
|
+
creator = get_developer(repo_root) or assignee
|
|
357
|
+
|
|
358
|
+
# Generate slug if not provided. A title-derived slug is sanitized by
|
|
359
|
+
# _slugify; an explicit --slug is not, so reject the characters that would
|
|
360
|
+
# let it escape the tasks directory once joined into the dir name.
|
|
361
|
+
slug = args.slug or _slugify(args.title)
|
|
362
|
+
if not slug:
|
|
363
|
+
print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
|
|
364
|
+
return 1
|
|
365
|
+
|
|
366
|
+
if args.slug and ("/" in slug or "\\" in slug or ".." in slug):
|
|
367
|
+
print(
|
|
368
|
+
colored(
|
|
369
|
+
f"Error: --slug must be a plain name without path separators or '..': {slug}",
|
|
370
|
+
Colors.RED,
|
|
371
|
+
),
|
|
372
|
+
file=sys.stderr,
|
|
373
|
+
)
|
|
374
|
+
return 1
|
|
375
|
+
|
|
376
|
+
# Create task directory with MM-DD-slug format
|
|
377
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
378
|
+
date_prefix = generate_task_date_prefix()
|
|
379
|
+
|
|
380
|
+
# Guard against date-prefixed --slug (e.g. a full task dir name pasted in),
|
|
381
|
+
# which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an
|
|
382
|
+
# explicit --slug is guarded; title-derived slugs are left untouched.
|
|
383
|
+
if args.slug:
|
|
384
|
+
m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug)
|
|
385
|
+
if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
|
|
386
|
+
slug_prefix = f"{m.group(1)}-{m.group(2)}"
|
|
387
|
+
if slug_prefix == date_prefix:
|
|
388
|
+
slug = m.group(3)
|
|
389
|
+
print(
|
|
390
|
+
colored(
|
|
391
|
+
f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"',
|
|
392
|
+
Colors.YELLOW,
|
|
393
|
+
),
|
|
394
|
+
file=sys.stderr,
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
print(
|
|
398
|
+
colored(
|
|
399
|
+
f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).",
|
|
400
|
+
Colors.RED,
|
|
401
|
+
),
|
|
402
|
+
file=sys.stderr,
|
|
403
|
+
)
|
|
404
|
+
print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr)
|
|
405
|
+
return 1
|
|
406
|
+
|
|
407
|
+
# Resolve --parent before anything is created. An explicit --parent that
|
|
408
|
+
# cannot be linked is a failed request, not a warning: creating the task
|
|
409
|
+
# anyway leaves a half-specified relationship that reads as success.
|
|
410
|
+
parent_dir: Path | None = None
|
|
411
|
+
parent_data: dict | None = None
|
|
412
|
+
if args.parent:
|
|
413
|
+
parent_dir = resolve_task_dir(args.parent, repo_root)
|
|
414
|
+
if parent_dir is None:
|
|
415
|
+
print(colored(f"Error: Parent task not resolved: {args.parent}", Colors.RED), file=sys.stderr)
|
|
416
|
+
print("No task was created. Pass an existing task directory to --parent.", file=sys.stderr)
|
|
417
|
+
return 1
|
|
418
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
419
|
+
if not parent_json_path.is_file():
|
|
420
|
+
print(colored(f"Error: Parent task.json not found: {args.parent}", Colors.RED), file=sys.stderr)
|
|
421
|
+
print("No task was created. Pass an existing task directory to --parent.", file=sys.stderr)
|
|
422
|
+
return 1
|
|
423
|
+
parent_data, parent_reason = read_json_checked(parent_json_path)
|
|
424
|
+
if parent_data is None:
|
|
425
|
+
_report_read_failure(parent_json_path, parent_reason)
|
|
426
|
+
print("No task was created. Fix the parent task.json, then retry.", file=sys.stderr)
|
|
427
|
+
return 1
|
|
428
|
+
|
|
429
|
+
dir_name = f"{date_prefix}-{slug}"
|
|
430
|
+
task_dir = tasks_dir / dir_name
|
|
431
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
432
|
+
|
|
433
|
+
archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, dir_name)
|
|
434
|
+
if archived_task_dir:
|
|
435
|
+
print(colored(f"Error: Task already archived: {dir_name}", Colors.RED), file=sys.stderr)
|
|
436
|
+
print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
|
|
437
|
+
print("Use a new slug if you intend to create a new task.", file=sys.stderr)
|
|
438
|
+
return 1
|
|
439
|
+
|
|
440
|
+
# Reusing a slug on the same day is an ordinary accident, and continuing
|
|
441
|
+
# would rewrite the existing task.json below — resetting status, children,
|
|
442
|
+
# parent, branch and meta, which nothing else can reconstruct. Fail like
|
|
443
|
+
# the archived-name collision above unless the caller asks for it.
|
|
444
|
+
if task_dir.exists():
|
|
445
|
+
if not getattr(args, "force", False):
|
|
446
|
+
print(colored(f"Error: Task already exists: {dir_name}", Colors.RED), file=sys.stderr)
|
|
447
|
+
print(f"Existing task at: {_repo_relative_path(task_dir, repo_root)}", file=sys.stderr)
|
|
448
|
+
print(
|
|
449
|
+
"Use a different --slug, or pass --force to overwrite its task.json.",
|
|
450
|
+
file=sys.stderr,
|
|
451
|
+
)
|
|
452
|
+
return 1
|
|
453
|
+
print(
|
|
454
|
+
colored(
|
|
455
|
+
f"Warning: --force: overwriting task.json in existing task: {dir_name}",
|
|
456
|
+
Colors.YELLOW,
|
|
457
|
+
),
|
|
458
|
+
file=sys.stderr,
|
|
459
|
+
)
|
|
460
|
+
created_dir = False
|
|
461
|
+
else:
|
|
462
|
+
task_dir.mkdir(parents=True)
|
|
463
|
+
created_dir = True
|
|
464
|
+
|
|
465
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
466
|
+
|
|
467
|
+
# Record the PR target branch. Prefer the repo's actual default branch
|
|
468
|
+
# (origin/HEAD) so creating a task from a feature branch doesn't
|
|
469
|
+
# mis-stamp that feature branch as the PR target (#399 item 1). Falls
|
|
470
|
+
# back to the checked-out branch when the default can't be resolved
|
|
471
|
+
# (no remote configured, offline, etc.) — the pre-existing behavior.
|
|
472
|
+
# --base-branch lets the caller override both when neither is correct.
|
|
473
|
+
_, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
|
|
474
|
+
current_branch = branch_out.strip() or "main"
|
|
475
|
+
explicit_base_branch: str | None = getattr(args, "base_branch", None)
|
|
476
|
+
if explicit_base_branch:
|
|
477
|
+
base_branch = explicit_base_branch
|
|
478
|
+
else:
|
|
479
|
+
resolved_base_branch = resolve_default_branch(repo_root)
|
|
480
|
+
if resolved_base_branch:
|
|
481
|
+
base_branch = resolved_base_branch
|
|
482
|
+
else:
|
|
483
|
+
base_branch = current_branch
|
|
484
|
+
print(
|
|
485
|
+
colored(
|
|
486
|
+
f"warning: could not resolve the repository's default branch "
|
|
487
|
+
f"(no remote configured, offline, etc.); stamping base_branch as "
|
|
488
|
+
f"the checked-out branch '{base_branch}'. Pass --base-branch to override.",
|
|
489
|
+
Colors.YELLOW,
|
|
490
|
+
),
|
|
491
|
+
file=sys.stderr,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
task_data = {
|
|
495
|
+
"id": slug,
|
|
496
|
+
"name": slug,
|
|
497
|
+
"title": args.title,
|
|
498
|
+
"description": description,
|
|
499
|
+
"status": "planning",
|
|
500
|
+
"dev_type": None,
|
|
501
|
+
"scope": None,
|
|
502
|
+
"package": package,
|
|
503
|
+
"priority": args.priority,
|
|
504
|
+
"creator": creator,
|
|
505
|
+
"assignee": assignee,
|
|
506
|
+
"createdAt": today,
|
|
507
|
+
"completedAt": None,
|
|
508
|
+
"branch": None,
|
|
509
|
+
"base_branch": base_branch,
|
|
510
|
+
"worktree_path": None,
|
|
511
|
+
"commit": None,
|
|
512
|
+
"pr_url": None,
|
|
513
|
+
"subtasks": [],
|
|
514
|
+
"children": [],
|
|
515
|
+
"parent": None,
|
|
516
|
+
"relatedFiles": [],
|
|
517
|
+
"notes": "",
|
|
518
|
+
"meta": meta,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# A directory without task.json is not a task: `list` hides it and every
|
|
522
|
+
# lifecycle command refuses it. Fail here rather than printing "Created
|
|
523
|
+
# task" and emitting the path for script chaining.
|
|
524
|
+
if not write_json(task_json_path, task_data):
|
|
525
|
+
_report_write_failure(task_json_path)
|
|
526
|
+
print(colored(f"No task was created: {dir_name}", Colors.RED), file=sys.stderr)
|
|
527
|
+
if created_dir:
|
|
528
|
+
try:
|
|
529
|
+
task_dir.rmdir()
|
|
530
|
+
except OSError:
|
|
531
|
+
print(
|
|
532
|
+
f"Leftover empty directory: {_repo_relative_path(task_dir, repo_root)}",
|
|
533
|
+
file=sys.stderr,
|
|
534
|
+
)
|
|
535
|
+
return 1
|
|
536
|
+
|
|
537
|
+
prd_path = task_dir / "prd.md"
|
|
538
|
+
if not prd_path.exists():
|
|
539
|
+
prd_path.write_text(
|
|
540
|
+
_default_prd_content(args.title, description),
|
|
541
|
+
encoding="utf-8",
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Create empty implement.jsonl / check.jsonl for sub-agent-capable
|
|
545
|
+
# platforms. They stay empty until the agent curates real entries during
|
|
546
|
+
# planning — a placeholder row would read as unresolved scaffolding to
|
|
547
|
+
# `task.py validate` and to PR preflight, so the curation instructions go
|
|
548
|
+
# to the console below instead of into the files. Agent-less platforms
|
|
549
|
+
# (Kilo / Antigravity / Devin) skip this — they load specs via the
|
|
550
|
+
# trellis-before-dev skill instead of JSONL.
|
|
551
|
+
created_jsonl = False
|
|
552
|
+
if _has_subagent_platform(repo_root):
|
|
553
|
+
for jsonl_name in ("implement.jsonl", "check.jsonl"):
|
|
554
|
+
jsonl_path = task_dir / jsonl_name
|
|
555
|
+
if not jsonl_path.exists():
|
|
556
|
+
jsonl_path.write_text("", encoding="utf-8")
|
|
557
|
+
created_jsonl = True
|
|
558
|
+
|
|
559
|
+
# Establish the bidirectional link. Both sides were validated above, so a
|
|
560
|
+
# failure here is a write failure, not a bad argument.
|
|
561
|
+
if parent_dir is not None and parent_data is not None:
|
|
562
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
563
|
+
|
|
564
|
+
# Add child to parent's children list
|
|
565
|
+
parent_children = _ensure_children_list(parent_data)
|
|
566
|
+
if dir_name not in parent_children:
|
|
567
|
+
parent_children.append(dir_name)
|
|
568
|
+
parent_data["children"] = parent_children
|
|
569
|
+
if not write_json(parent_json_path, parent_data):
|
|
570
|
+
_report_write_failure(parent_json_path)
|
|
571
|
+
print(
|
|
572
|
+
f"The task exists at {_repo_relative_path(task_dir, repo_root)} but is NOT "
|
|
573
|
+
f"linked to {parent_dir.name}. Re-link with: task.py add-subtask "
|
|
574
|
+
f"{parent_dir.name} {dir_name}",
|
|
575
|
+
file=sys.stderr,
|
|
576
|
+
)
|
|
577
|
+
return 1
|
|
578
|
+
|
|
579
|
+
# Set parent in child's task.json
|
|
580
|
+
task_data["parent"] = parent_dir.name
|
|
581
|
+
if not write_json(task_json_path, task_data):
|
|
582
|
+
_report_write_failure(task_json_path)
|
|
583
|
+
print(
|
|
584
|
+
f"Link is half-written: {parent_dir.name} now lists '{dir_name}' as a child, "
|
|
585
|
+
f"but the new task does not record its parent.",
|
|
586
|
+
file=sys.stderr,
|
|
587
|
+
)
|
|
588
|
+
return 1
|
|
589
|
+
|
|
590
|
+
print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
591
|
+
|
|
592
|
+
# Auto-activate the new task so the per-turn breadcrumb fires planning
|
|
593
|
+
# state. Best-effort: gracefully degrade if no session identity (CLI run
|
|
594
|
+
# outside an AI session) — the task is still created, the user can run
|
|
595
|
+
# task.py start later. Pointer is session-scoped so this never affects
|
|
596
|
+
# other AI sessions.
|
|
597
|
+
if getattr(args, "no_start", False):
|
|
598
|
+
print(
|
|
599
|
+
colored(
|
|
600
|
+
"Skipped session activation (--no-start); run task.py start when ready.",
|
|
601
|
+
Colors.YELLOW,
|
|
602
|
+
),
|
|
603
|
+
file=sys.stderr,
|
|
604
|
+
)
|
|
605
|
+
else:
|
|
606
|
+
try:
|
|
607
|
+
from .active_task import resolve_context_key, set_active_task
|
|
608
|
+
except Exception as exc:
|
|
609
|
+
print(
|
|
610
|
+
colored(f"Warning: session activation unavailable (import failed: {exc})", Colors.YELLOW),
|
|
611
|
+
file=sys.stderr,
|
|
612
|
+
)
|
|
613
|
+
else:
|
|
614
|
+
try:
|
|
615
|
+
context_key = resolve_context_key()
|
|
616
|
+
except Exception as exc:
|
|
617
|
+
print(
|
|
618
|
+
colored(f"Warning: session activation failed (context resolution: {exc})", Colors.YELLOW),
|
|
619
|
+
file=sys.stderr,
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
# No session identity is the normal CLI-outside-an-AI-session
|
|
623
|
+
# case (see comment above) — stay silent, not a failure.
|
|
624
|
+
if context_key:
|
|
625
|
+
try:
|
|
626
|
+
rel_dir = task_dir.relative_to(repo_root).as_posix()
|
|
627
|
+
except ValueError:
|
|
628
|
+
rel_dir = str(task_dir)
|
|
629
|
+
try:
|
|
630
|
+
active = set_active_task(rel_dir, repo_root)
|
|
631
|
+
except Exception as exc:
|
|
632
|
+
print(
|
|
633
|
+
colored(f"Warning: session activation failed (pointer persistence: {exc})", Colors.YELLOW),
|
|
634
|
+
file=sys.stderr,
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
if active:
|
|
638
|
+
print(
|
|
639
|
+
colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
|
|
640
|
+
file=sys.stderr,
|
|
641
|
+
)
|
|
642
|
+
print(f"Source: {active.source}", file=sys.stderr)
|
|
643
|
+
else:
|
|
644
|
+
print(
|
|
645
|
+
colored("Warning: session activation failed (no pointer returned)", Colors.YELLOW),
|
|
646
|
+
file=sys.stderr,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
650
|
+
print("", file=sys.stderr)
|
|
651
|
+
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
|
|
652
|
+
print(" - Fill prd.md with requirements and acceptance criteria", file=sys.stderr)
|
|
653
|
+
print(" - Lightweight task: PRD-only is valid", file=sys.stderr)
|
|
654
|
+
print(" - Complex task: add design.md and implement.md before task.py start", file=sys.stderr)
|
|
655
|
+
if created_jsonl:
|
|
656
|
+
print(
|
|
657
|
+
" - Curate implement.jsonl / check.jsonl (created empty) as spec/research "
|
|
658
|
+
"manifests before task.py start when sub-agents need context:",
|
|
659
|
+
file=sys.stderr,
|
|
660
|
+
)
|
|
661
|
+
print(
|
|
662
|
+
' one JSON object per line — {"file": "<path>", "reason": "<why>"}; '
|
|
663
|
+
"spec/research docs only, no code paths",
|
|
664
|
+
file=sys.stderr,
|
|
665
|
+
)
|
|
666
|
+
print(
|
|
667
|
+
" list available specs: python3 .trellis/scripts/get_context.py --mode packages",
|
|
668
|
+
file=sys.stderr,
|
|
669
|
+
)
|
|
670
|
+
print(" - Use /trellis:continue or phase context to decide the next step", file=sys.stderr)
|
|
671
|
+
print("", file=sys.stderr)
|
|
672
|
+
|
|
673
|
+
# Output relative path for script chaining
|
|
674
|
+
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
|
|
675
|
+
|
|
676
|
+
run_task_hooks("after_create", task_json_path, repo_root)
|
|
677
|
+
return 0
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# =============================================================================
|
|
681
|
+
# Command: rename
|
|
682
|
+
# =============================================================================
|
|
683
|
+
|
|
684
|
+
# task.json fields that carry the task's own slug. The directory name carries
|
|
685
|
+
# it too, prefixed with the creation date; everything else that names the task
|
|
686
|
+
# (parent / children / subtasks in *other* tasks, jsonl paths) stores the full
|
|
687
|
+
# directory name instead.
|
|
688
|
+
RENAME_IDENTITY_FIELDS: tuple[str, ...] = ("id", "name")
|
|
689
|
+
|
|
690
|
+
_JSONL_NAMES: tuple[str, ...] = ("implement.jsonl", "check.jsonl")
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
@dataclass
|
|
694
|
+
class _RenamePlan:
|
|
695
|
+
"""Every change a rename would make, computed before anything is written.
|
|
696
|
+
|
|
697
|
+
``--dry-run`` and the real run render their output from this one structure,
|
|
698
|
+
so the printed change set cannot drift from the applied one.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
task_dir: Path
|
|
702
|
+
new_dir: Path
|
|
703
|
+
old_name: str
|
|
704
|
+
new_name: str
|
|
705
|
+
old_rel: str
|
|
706
|
+
new_rel: str
|
|
707
|
+
task_json_path: Path
|
|
708
|
+
task_data: dict
|
|
709
|
+
# (field, current value, new value) for each identity field that changes.
|
|
710
|
+
identity: list[tuple[str, object, str]] = field(default_factory=list)
|
|
711
|
+
# (jsonl path, changed line numbers, full new file text)
|
|
712
|
+
jsonl: list[tuple[Path, list[int], str]] = field(default_factory=list)
|
|
713
|
+
# (other task's task.json, rewritten data, labels of the changed refs)
|
|
714
|
+
backrefs: list[tuple[Path, dict, list[str]]] = field(default_factory=list)
|
|
715
|
+
# (file, line number) of old-name mentions elsewhere under .trellis/
|
|
716
|
+
reported: list[tuple[Path, int]] = field(default_factory=list)
|
|
717
|
+
# Task dirs whose task.json could not be read, so back-refs in them are
|
|
718
|
+
# neither rewritten nor ruled out.
|
|
719
|
+
unreadable: list[Path] = field(default_factory=list)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _split_date_prefix(dir_name: str) -> tuple[str, str]:
|
|
723
|
+
"""Split ``MM-DD-slug`` into ``("MM-DD", "slug")``.
|
|
724
|
+
|
|
725
|
+
Returns ``("", dir_name)`` when there is no plausible date prefix, so a
|
|
726
|
+
hand-made directory name without one is renamed to the bare slug.
|
|
727
|
+
"""
|
|
728
|
+
m = re.match(r"^(\d{2})-(\d{2})-(.+)$", dir_name)
|
|
729
|
+
if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
|
|
730
|
+
return f"{m.group(1)}-{m.group(2)}", m.group(3)
|
|
731
|
+
return "", dir_name
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _validate_rename_slug(slug: str, date_prefix: str) -> str | None:
|
|
735
|
+
"""Return the slug body to rename to, or None after reporting the refusal.
|
|
736
|
+
|
|
737
|
+
Mirrors ``create``'s ``--slug`` handling: no path separators or ``..`` (the
|
|
738
|
+
slug is joined into a directory name), and a pasted-in date prefix is
|
|
739
|
+
normalized away rather than doubled — but a rename keeps the task's
|
|
740
|
+
*original* creation date, not today's.
|
|
741
|
+
"""
|
|
742
|
+
if not slug:
|
|
743
|
+
print(colored("Error: new slug is required", Colors.RED), file=sys.stderr)
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
if "/" in slug or "\\" in slug or ".." in slug:
|
|
747
|
+
print(
|
|
748
|
+
colored(
|
|
749
|
+
f"Error: <new-slug> must be a plain name without path separators or '..': {slug}",
|
|
750
|
+
Colors.RED,
|
|
751
|
+
),
|
|
752
|
+
file=sys.stderr,
|
|
753
|
+
)
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
slug_prefix, body = _split_date_prefix(slug)
|
|
757
|
+
if not slug_prefix:
|
|
758
|
+
return slug
|
|
759
|
+
if slug_prefix == date_prefix:
|
|
760
|
+
print(
|
|
761
|
+
colored(
|
|
762
|
+
f'warning: <new-slug> should not include the MM-DD prefix; normalized to "{body}"',
|
|
763
|
+
Colors.YELLOW,
|
|
764
|
+
),
|
|
765
|
+
file=sys.stderr,
|
|
766
|
+
)
|
|
767
|
+
return body
|
|
768
|
+
print(
|
|
769
|
+
colored(
|
|
770
|
+
f"Error: <new-slug> starts with a date prefix ({slug_prefix}-), but rename keeps "
|
|
771
|
+
f"the task's own creation date ({date_prefix}).",
|
|
772
|
+
Colors.RED,
|
|
773
|
+
),
|
|
774
|
+
file=sys.stderr,
|
|
775
|
+
)
|
|
776
|
+
print(f"Pass only the slug body, e.g. {body}", file=sys.stderr)
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _plan_jsonl_rewrites(
|
|
781
|
+
task_dir: Path, old_rel: str, new_rel: str
|
|
782
|
+
) -> list[tuple[Path, list[int], str]]:
|
|
783
|
+
"""Plan rewrites of context entries pointing under the old task directory.
|
|
784
|
+
|
|
785
|
+
Matching on ``<old path>/`` rather than the bare path keeps a sibling task
|
|
786
|
+
whose name merely starts with this one's out of the rewrite.
|
|
787
|
+
"""
|
|
788
|
+
rewrites: list[tuple[Path, list[int], str]] = []
|
|
789
|
+
needle = f"{old_rel}/"
|
|
790
|
+
replacement = f"{new_rel}/"
|
|
791
|
+
|
|
792
|
+
for jsonl_name in _JSONL_NAMES:
|
|
793
|
+
jsonl_path = task_dir / jsonl_name
|
|
794
|
+
if not jsonl_path.is_file():
|
|
795
|
+
continue
|
|
796
|
+
try:
|
|
797
|
+
lines = jsonl_path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
798
|
+
except (OSError, UnicodeDecodeError):
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
changed: list[int] = []
|
|
802
|
+
for index, line in enumerate(lines):
|
|
803
|
+
if needle in line:
|
|
804
|
+
lines[index] = line.replace(needle, replacement)
|
|
805
|
+
changed.append(index + 1)
|
|
806
|
+
if changed:
|
|
807
|
+
rewrites.append((jsonl_path, changed, "".join(lines)))
|
|
808
|
+
|
|
809
|
+
return rewrites
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _plan_backrefs(
|
|
813
|
+
tasks_dir: Path, task_dir: Path, old_name: str, new_name: str
|
|
814
|
+
) -> tuple[list[tuple[Path, dict, list[str]]], list[Path]]:
|
|
815
|
+
"""Plan back-reference rewrites in the other active tasks.
|
|
816
|
+
|
|
817
|
+
Returns ``(changes, unreadable)``. ``subtasks`` is the legacy spelling of
|
|
818
|
+
``children`` and is still carried in older task.json files, so both lists
|
|
819
|
+
are rewritten.
|
|
820
|
+
"""
|
|
821
|
+
changes: list[tuple[Path, dict, list[str]]] = []
|
|
822
|
+
unreadable: list[Path] = []
|
|
823
|
+
|
|
824
|
+
for candidate in sorted(tasks_dir.iterdir()):
|
|
825
|
+
if not candidate.is_dir() or candidate.name == DIR_ARCHIVE:
|
|
826
|
+
continue
|
|
827
|
+
if candidate == task_dir:
|
|
828
|
+
continue
|
|
829
|
+
json_path = candidate / FILE_TASK_JSON
|
|
830
|
+
if not json_path.is_file():
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
data, _reason = read_json_checked(json_path)
|
|
834
|
+
if data is None:
|
|
835
|
+
unreadable.append(json_path)
|
|
836
|
+
continue
|
|
837
|
+
|
|
838
|
+
labels: list[str] = []
|
|
839
|
+
if data.get("parent") == old_name:
|
|
840
|
+
data["parent"] = new_name
|
|
841
|
+
labels.append("parent")
|
|
842
|
+
for list_field in ("children", "subtasks"):
|
|
843
|
+
values = data.get(list_field)
|
|
844
|
+
if not isinstance(values, list):
|
|
845
|
+
continue
|
|
846
|
+
for index, value in enumerate(values):
|
|
847
|
+
if value == old_name:
|
|
848
|
+
values[index] = new_name
|
|
849
|
+
labels.append(f"{list_field}[{index}]")
|
|
850
|
+
|
|
851
|
+
if labels:
|
|
852
|
+
changes.append((json_path, data, labels))
|
|
853
|
+
|
|
854
|
+
return changes, unreadable
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _plan_reported_refs(
|
|
858
|
+
repo_root: Path, task_dir: Path, rewritten: set[Path], old_name: str
|
|
859
|
+
) -> list[tuple[Path, int]]:
|
|
860
|
+
"""Find remaining mentions of the old task name elsewhere under .trellis/.
|
|
861
|
+
|
|
862
|
+
These are reported, never rewritten: journal entries and workflow prose
|
|
863
|
+
cite tasks in free text, and a blind substitution there is how a rename
|
|
864
|
+
turns into a diff nobody asked for. The boundary-anchored pattern keeps a
|
|
865
|
+
longer task name that merely contains this one out.
|
|
866
|
+
|
|
867
|
+
Runtime session pointers are excluded because they are not prose and they
|
|
868
|
+
*are* rewritten — see ``repoint_task_in_sessions``. Listing them here as
|
|
869
|
+
"not rewritten" would be a false statement about the one file whose
|
|
870
|
+
staleness actually breaks the next command.
|
|
871
|
+
"""
|
|
872
|
+
from .active_task import _runtime_sessions_dir
|
|
873
|
+
|
|
874
|
+
pattern = re.compile(
|
|
875
|
+
r"(?<![0-9A-Za-z_-])" + re.escape(old_name) + r"(?![0-9A-Za-z_-])"
|
|
876
|
+
)
|
|
877
|
+
hits: list[tuple[Path, int]] = []
|
|
878
|
+
trellis_dir = repo_root / DIR_WORKFLOW
|
|
879
|
+
sessions_dir = _runtime_sessions_dir(repo_root)
|
|
880
|
+
if not trellis_dir.is_dir():
|
|
881
|
+
return hits
|
|
882
|
+
|
|
883
|
+
for path in sorted(trellis_dir.rglob("*")):
|
|
884
|
+
if not path.is_file() or path in rewritten:
|
|
885
|
+
continue
|
|
886
|
+
if path == task_dir or task_dir in path.parents:
|
|
887
|
+
continue
|
|
888
|
+
if path.parent == sessions_dir:
|
|
889
|
+
continue
|
|
890
|
+
if any(
|
|
891
|
+
part == "__pycache__" or part.startswith(".backup-")
|
|
892
|
+
for part in path.relative_to(trellis_dir).parts[:-1]
|
|
893
|
+
):
|
|
894
|
+
continue
|
|
895
|
+
try:
|
|
896
|
+
text = path.read_text(encoding="utf-8")
|
|
897
|
+
except (OSError, UnicodeDecodeError):
|
|
898
|
+
continue
|
|
899
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
900
|
+
if pattern.search(line):
|
|
901
|
+
hits.append((path, lineno))
|
|
902
|
+
|
|
903
|
+
return hits
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _render_rename_plan(plan: _RenamePlan, repo_root: Path) -> list[str]:
|
|
907
|
+
"""Render the change set. Identical for --dry-run and the real run."""
|
|
908
|
+
lines = [
|
|
909
|
+
f"rename: {plan.old_name} -> {plan.new_name}",
|
|
910
|
+
f" dir: {plan.old_rel} -> {plan.new_rel}",
|
|
911
|
+
]
|
|
912
|
+
for field_name, old_value, new_value in plan.identity:
|
|
913
|
+
lines.append(f" task.json: {field_name}: {old_value} -> {new_value}")
|
|
914
|
+
for json_path, _data, labels in plan.backrefs:
|
|
915
|
+
rel = _repo_relative_path(json_path, repo_root)
|
|
916
|
+
for label in labels:
|
|
917
|
+
lines.append(
|
|
918
|
+
f" backref: {rel}: {label}: {plan.old_name} -> {plan.new_name}"
|
|
919
|
+
)
|
|
920
|
+
for jsonl_path, linenos, _text in plan.jsonl:
|
|
921
|
+
rel = _repo_relative_path(jsonl_path, repo_root)
|
|
922
|
+
for lineno in linenos:
|
|
923
|
+
lines.append(
|
|
924
|
+
f" jsonl: {rel}:{lineno}: {plan.old_rel}/ -> {plan.new_rel}/"
|
|
925
|
+
)
|
|
926
|
+
for path, lineno in plan.reported:
|
|
927
|
+
lines.append(
|
|
928
|
+
f" reported (not rewritten): {_repo_relative_path(path, repo_root)}:{lineno}"
|
|
929
|
+
)
|
|
930
|
+
lines.append(
|
|
931
|
+
f" sessions: any active-task pointer at {plan.old_rel} is repointed to {plan.new_rel}"
|
|
932
|
+
)
|
|
933
|
+
return lines
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _rename_interrupted(plan: _RenamePlan) -> None:
|
|
937
|
+
"""Explain how to finish a rename that stopped part-way through."""
|
|
938
|
+
print(
|
|
939
|
+
f"The task is still at {plan.old_rel} and was NOT moved. Every step up to "
|
|
940
|
+
f"this one is idempotent: fix the write failure, then run the same rename "
|
|
941
|
+
f"again to finish it.",
|
|
942
|
+
file=sys.stderr,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _apply_rename(plan: _RenamePlan, repo_root: Path) -> int:
|
|
947
|
+
"""Write the planned change set.
|
|
948
|
+
|
|
949
|
+
The directory move goes last on purpose. Everything before it is an
|
|
950
|
+
in-place edit that re-running the identical command recomputes as
|
|
951
|
+
already-done, so an interruption leaves a tree that the same command
|
|
952
|
+
finishes; a move-first ordering would instead leave the old name
|
|
953
|
+
unresolvable and the remaining edits to be made by hand.
|
|
954
|
+
"""
|
|
955
|
+
if plan.identity:
|
|
956
|
+
data = dict(plan.task_data)
|
|
957
|
+
for field_name, _old_value, new_value in plan.identity:
|
|
958
|
+
data[field_name] = new_value
|
|
959
|
+
if not write_json(plan.task_json_path, data):
|
|
960
|
+
_report_write_failure(plan.task_json_path)
|
|
961
|
+
print("Nothing was renamed.", file=sys.stderr)
|
|
962
|
+
return 1
|
|
963
|
+
|
|
964
|
+
for jsonl_path, _linenos, text in plan.jsonl:
|
|
965
|
+
try:
|
|
966
|
+
jsonl_path.write_text(text, encoding="utf-8")
|
|
967
|
+
except OSError as exc:
|
|
968
|
+
print(
|
|
969
|
+
colored(f"Error: Failed to write {jsonl_path}: {exc}", Colors.RED),
|
|
970
|
+
file=sys.stderr,
|
|
971
|
+
)
|
|
972
|
+
_rename_interrupted(plan)
|
|
973
|
+
return 1
|
|
974
|
+
|
|
975
|
+
for json_path, data, _labels in plan.backrefs:
|
|
976
|
+
if not write_json(json_path, data):
|
|
977
|
+
_report_write_failure(json_path)
|
|
978
|
+
_rename_interrupted(plan)
|
|
979
|
+
return 1
|
|
980
|
+
|
|
981
|
+
try:
|
|
982
|
+
plan.task_dir.rename(plan.new_dir)
|
|
983
|
+
except OSError as exc:
|
|
984
|
+
print(
|
|
985
|
+
colored(f"Error: Failed to move {plan.old_rel} -> {plan.new_rel}: {exc}", Colors.RED),
|
|
986
|
+
file=sys.stderr,
|
|
987
|
+
)
|
|
988
|
+
_rename_interrupted(plan)
|
|
989
|
+
return 1
|
|
990
|
+
|
|
991
|
+
# Last, and after the move: a session pointing at the old path would now
|
|
992
|
+
# resolve to a missing directory, so `current` would report the task stale
|
|
993
|
+
# and the context hook would inject nothing until the user ran `start`
|
|
994
|
+
# again. Archive clears these pointers because the task is leaving; rename
|
|
995
|
+
# moves them, because the task is still the one being worked on.
|
|
996
|
+
from .active_task import repoint_task_in_sessions
|
|
997
|
+
|
|
998
|
+
repoint_task_in_sessions(str(plan.task_dir), str(plan.new_dir), repo_root)
|
|
999
|
+
|
|
1000
|
+
return 0
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def cmd_rename(args: argparse.Namespace) -> int:
|
|
1004
|
+
"""Rename a task and every reference to it."""
|
|
1005
|
+
repo_root = get_repo_root()
|
|
1006
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
1007
|
+
|
|
1008
|
+
task_dir = resolve_task_dir(args.name, repo_root)
|
|
1009
|
+
if task_dir is None or not task_dir.is_dir():
|
|
1010
|
+
if task_dir is not None:
|
|
1011
|
+
print(colored(f"Error: Task not found: {args.name}", Colors.RED), file=sys.stderr)
|
|
1012
|
+
return 1
|
|
1013
|
+
|
|
1014
|
+
# Narrower than resolve_task_dir's containment: an archived task has left
|
|
1015
|
+
# the active set, so its back-references are no longer maintained here.
|
|
1016
|
+
if not is_within_tasks_dir(task_dir, repo_root):
|
|
1017
|
+
print(
|
|
1018
|
+
colored(
|
|
1019
|
+
f"Error: refusing to rename '{args.name}': "
|
|
1020
|
+
f"{task_dir} is not an active task under {tasks_dir}",
|
|
1021
|
+
Colors.RED,
|
|
1022
|
+
),
|
|
1023
|
+
file=sys.stderr,
|
|
1024
|
+
)
|
|
1025
|
+
return 1
|
|
1026
|
+
|
|
1027
|
+
old_name = task_dir.name
|
|
1028
|
+
date_prefix, _old_slug = _split_date_prefix(old_name)
|
|
1029
|
+
|
|
1030
|
+
slug = _validate_rename_slug(args.new_slug, date_prefix)
|
|
1031
|
+
if slug is None:
|
|
1032
|
+
return 1
|
|
1033
|
+
|
|
1034
|
+
new_name = f"{date_prefix}-{slug}" if date_prefix else slug
|
|
1035
|
+
if new_name == old_name:
|
|
1036
|
+
print(
|
|
1037
|
+
colored(f"Error: '{new_name}' is the task's current name", Colors.RED),
|
|
1038
|
+
file=sys.stderr,
|
|
1039
|
+
)
|
|
1040
|
+
return 1
|
|
1041
|
+
|
|
1042
|
+
new_dir = task_dir.parent / new_name
|
|
1043
|
+
if new_dir.exists():
|
|
1044
|
+
print(
|
|
1045
|
+
colored(f"Error: Task already exists: {new_name}", Colors.RED),
|
|
1046
|
+
file=sys.stderr,
|
|
1047
|
+
)
|
|
1048
|
+
print(f"Existing task at: {_repo_relative_path(new_dir, repo_root)}", file=sys.stderr)
|
|
1049
|
+
print("Pick a different slug.", file=sys.stderr)
|
|
1050
|
+
return 1
|
|
1051
|
+
|
|
1052
|
+
archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, new_name)
|
|
1053
|
+
if archived_task_dir:
|
|
1054
|
+
print(
|
|
1055
|
+
colored(f"Error: Task already archived: {new_name}", Colors.RED),
|
|
1056
|
+
file=sys.stderr,
|
|
1057
|
+
)
|
|
1058
|
+
print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
|
|
1059
|
+
print("Pick a different slug.", file=sys.stderr)
|
|
1060
|
+
return 1
|
|
1061
|
+
|
|
1062
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
1063
|
+
if not task_json_path.is_file():
|
|
1064
|
+
print(
|
|
1065
|
+
colored(f"Error: task.json not found at {task_dir}", Colors.RED),
|
|
1066
|
+
file=sys.stderr,
|
|
1067
|
+
)
|
|
1068
|
+
return 1
|
|
1069
|
+
|
|
1070
|
+
task_data, reason = read_json_checked(task_json_path)
|
|
1071
|
+
if task_data is None:
|
|
1072
|
+
_report_read_failure(task_json_path, reason)
|
|
1073
|
+
print("Nothing was renamed.", file=sys.stderr)
|
|
1074
|
+
return 1
|
|
1075
|
+
|
|
1076
|
+
plan = _RenamePlan(
|
|
1077
|
+
task_dir=task_dir,
|
|
1078
|
+
new_dir=new_dir,
|
|
1079
|
+
old_name=old_name,
|
|
1080
|
+
new_name=new_name,
|
|
1081
|
+
old_rel=_repo_relative_path(task_dir, repo_root),
|
|
1082
|
+
new_rel=_repo_relative_path(new_dir, repo_root),
|
|
1083
|
+
task_json_path=task_json_path,
|
|
1084
|
+
task_data=task_data,
|
|
1085
|
+
)
|
|
1086
|
+
plan.identity = [
|
|
1087
|
+
(name, task_data.get(name), slug)
|
|
1088
|
+
for name in RENAME_IDENTITY_FIELDS
|
|
1089
|
+
if task_data.get(name) != slug
|
|
1090
|
+
]
|
|
1091
|
+
plan.jsonl = _plan_jsonl_rewrites(task_dir, plan.old_rel, plan.new_rel)
|
|
1092
|
+
plan.backrefs, plan.unreadable = _plan_backrefs(
|
|
1093
|
+
tasks_dir, task_dir, old_name, new_name
|
|
1094
|
+
)
|
|
1095
|
+
rewritten = {path for path, _linenos, _text in plan.jsonl}
|
|
1096
|
+
rewritten.update(path for path, _data, _labels in plan.backrefs)
|
|
1097
|
+
plan.reported = _plan_reported_refs(repo_root, task_dir, rewritten, old_name)
|
|
1098
|
+
|
|
1099
|
+
for line in _render_rename_plan(plan, repo_root):
|
|
1100
|
+
print(line)
|
|
1101
|
+
|
|
1102
|
+
for json_path in plan.unreadable:
|
|
1103
|
+
print(
|
|
1104
|
+
colored(
|
|
1105
|
+
f"Warning: {_repo_relative_path(json_path, repo_root)} could not be read; "
|
|
1106
|
+
"any back-reference in it is left as-is.",
|
|
1107
|
+
Colors.YELLOW,
|
|
1108
|
+
),
|
|
1109
|
+
file=sys.stderr,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
if getattr(args, "dry_run", False):
|
|
1113
|
+
print(colored("Dry run: nothing was written.", Colors.YELLOW), file=sys.stderr)
|
|
1114
|
+
return 0
|
|
1115
|
+
|
|
1116
|
+
rc = _apply_rename(plan, repo_root)
|
|
1117
|
+
if rc != 0:
|
|
1118
|
+
return rc
|
|
1119
|
+
|
|
1120
|
+
print(colored(f"Renamed: {old_name} -> {new_name}", Colors.GREEN), file=sys.stderr)
|
|
1121
|
+
if plan.reported:
|
|
1122
|
+
print(
|
|
1123
|
+
colored(
|
|
1124
|
+
f"{len(plan.reported)} reference(s) elsewhere under {DIR_WORKFLOW}/ "
|
|
1125
|
+
"still name the old task; they are listed above and were not rewritten.",
|
|
1126
|
+
Colors.YELLOW,
|
|
1127
|
+
),
|
|
1128
|
+
file=sys.stderr,
|
|
1129
|
+
)
|
|
1130
|
+
return 0
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
# =============================================================================
|
|
1134
|
+
# Command: archive
|
|
1135
|
+
# =============================================================================
|
|
1136
|
+
|
|
1137
|
+
def _task_branch_field(data: dict, key: str) -> str:
|
|
1138
|
+
"""Read a branch field as a trimmed string ("" when unset or not a string)."""
|
|
1139
|
+
value = data.get(key)
|
|
1140
|
+
return strip_blank(value) if isinstance(value, str) else ""
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _validate_branch_metadata(
|
|
1144
|
+
data: dict,
|
|
1145
|
+
task_name: str,
|
|
1146
|
+
repo_root: Path,
|
|
1147
|
+
skip: bool,
|
|
1148
|
+
) -> bool:
|
|
1149
|
+
"""Check branch metadata before the task leaves the active tree.
|
|
1150
|
+
|
|
1151
|
+
Returns False when archiving must stop. Archive is the last gate that sees
|
|
1152
|
+
a task, so metadata nobody can reconstruct afterwards is refused here
|
|
1153
|
+
rather than repaired by hand later (#399 follow-up).
|
|
1154
|
+
|
|
1155
|
+
"PR-backed" is deliberately pragmatic: a task carrying a base_branch in a
|
|
1156
|
+
repo that has a remote was created expecting a PR, so a missing `branch`
|
|
1157
|
+
means the metadata was never recorded — not that the work had no branch.
|
|
1158
|
+
Local-only repos and tasks without a base_branch are left alone.
|
|
1159
|
+
|
|
1160
|
+
A recorded branch that no longer exists locally stays a warning: after a
|
|
1161
|
+
merge the feature branch is normally deleted, and refusing to archive then
|
|
1162
|
+
would be backwards.
|
|
1163
|
+
"""
|
|
1164
|
+
branch = _task_branch_field(data, "branch")
|
|
1165
|
+
base_branch = _task_branch_field(data, "base_branch")
|
|
1166
|
+
task_py = f"python3 {DIR_WORKFLOW}/scripts/task.py"
|
|
1167
|
+
|
|
1168
|
+
if branch and not branch_exists_locally(branch, repo_root):
|
|
1169
|
+
print(
|
|
1170
|
+
colored(
|
|
1171
|
+
f"Warning: recorded branch '{branch}' no longer exists locally "
|
|
1172
|
+
"(likely merged and deleted).",
|
|
1173
|
+
Colors.YELLOW,
|
|
1174
|
+
),
|
|
1175
|
+
file=sys.stderr,
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
if skip:
|
|
1179
|
+
return True
|
|
1180
|
+
|
|
1181
|
+
if branch and base_branch and branch == base_branch:
|
|
1182
|
+
print(
|
|
1183
|
+
colored(
|
|
1184
|
+
f"Error: refusing to archive '{task_name}': branch and base_branch "
|
|
1185
|
+
f"are both '{branch}'. A PR cannot target its own branch, so this "
|
|
1186
|
+
"metadata cannot describe the work that was merged.",
|
|
1187
|
+
Colors.RED,
|
|
1188
|
+
),
|
|
1189
|
+
file=sys.stderr,
|
|
1190
|
+
)
|
|
1191
|
+
print("Repair whichever field is wrong:", file=sys.stderr)
|
|
1192
|
+
print(f" {task_py} set-branch {task_name} <feature-branch>", file=sys.stderr)
|
|
1193
|
+
print(f" {task_py} set-base-branch {task_name} <target-branch>", file=sys.stderr)
|
|
1194
|
+
print(
|
|
1195
|
+
f" {task_py} archive {task_name} --skip-branch-validation"
|
|
1196
|
+
" # only if this task was never PR-backed",
|
|
1197
|
+
file=sys.stderr,
|
|
1198
|
+
)
|
|
1199
|
+
return False
|
|
1200
|
+
|
|
1201
|
+
if not branch and base_branch and has_git_remote(repo_root):
|
|
1202
|
+
print(
|
|
1203
|
+
colored(
|
|
1204
|
+
f"Error: refusing to archive '{task_name}': no branch is recorded, "
|
|
1205
|
+
f"but the task targets base_branch '{base_branch}' in a repo with a "
|
|
1206
|
+
"remote — the branch it was built on was never written down.",
|
|
1207
|
+
Colors.RED,
|
|
1208
|
+
),
|
|
1209
|
+
file=sys.stderr,
|
|
1210
|
+
)
|
|
1211
|
+
print("Repair with:", file=sys.stderr)
|
|
1212
|
+
print(f" {task_py} set-branch {task_name} <branch>", file=sys.stderr)
|
|
1213
|
+
print(
|
|
1214
|
+
f" {task_py} archive {task_name} --skip-branch-validation"
|
|
1215
|
+
" # only if the work landed without a branch of its own",
|
|
1216
|
+
file=sys.stderr,
|
|
1217
|
+
)
|
|
1218
|
+
return False
|
|
1219
|
+
|
|
1220
|
+
return True
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def cmd_archive(args: argparse.Namespace) -> int:
|
|
1224
|
+
"""Archive completed task."""
|
|
1225
|
+
repo_root = get_repo_root()
|
|
1226
|
+
task_name = args.name
|
|
1227
|
+
|
|
1228
|
+
if not task_name:
|
|
1229
|
+
print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
|
|
1230
|
+
return 1
|
|
1231
|
+
|
|
1232
|
+
tasks_dir = get_tasks_dir(repo_root)
|
|
1233
|
+
|
|
1234
|
+
# Resolve task directory (supports task name, relative path, or absolute path)
|
|
1235
|
+
task_dir = resolve_task_dir(task_name, repo_root)
|
|
1236
|
+
|
|
1237
|
+
if task_dir is None or not task_dir.is_dir():
|
|
1238
|
+
if task_dir is None:
|
|
1239
|
+
# resolve_task_dir already reported why; keep the archive-specific
|
|
1240
|
+
# refusal so it reads the same as the is_within_tasks_dir guard.
|
|
1241
|
+
print(colored(
|
|
1242
|
+
f"Error: refusing to archive '{task_name}': "
|
|
1243
|
+
f"it does not resolve to a task under {tasks_dir}",
|
|
1244
|
+
Colors.RED), file=sys.stderr)
|
|
1245
|
+
else:
|
|
1246
|
+
print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
|
|
1247
|
+
print("Active tasks:", file=sys.stderr)
|
|
1248
|
+
# Import lazily to avoid circular dependency
|
|
1249
|
+
from .tasks import iter_active_tasks
|
|
1250
|
+
for t in iter_active_tasks(tasks_dir):
|
|
1251
|
+
print(f" - {t.dir_name}/", file=sys.stderr)
|
|
1252
|
+
return 1
|
|
1253
|
+
|
|
1254
|
+
# Refuse to archive anything that isn't a real task directly under
|
|
1255
|
+
# .trellis/tasks/. resolve_task_dir keeps the target inside the tasks dir;
|
|
1256
|
+
# this narrows it further to a direct child, so an already-archived task
|
|
1257
|
+
# (.trellis/tasks/archive/<month>/<task>) is not archived a second time.
|
|
1258
|
+
if not is_within_tasks_dir(task_dir, repo_root):
|
|
1259
|
+
print(colored(
|
|
1260
|
+
f"Error: refusing to archive '{task_name}': "
|
|
1261
|
+
f"{task_dir} is not a task under {tasks_dir}",
|
|
1262
|
+
Colors.RED), file=sys.stderr)
|
|
1263
|
+
return 1
|
|
1264
|
+
|
|
1265
|
+
# Check the destination before anything below mutates task state. The
|
|
1266
|
+
# mover refuses a collision too, but by then this command has already
|
|
1267
|
+
# marked the task completed, re-parented its children and cleared the
|
|
1268
|
+
# sessions pointing at it — all of which would have to be undone by hand.
|
|
1269
|
+
archive_dest_check = archive_destination_for(task_dir)
|
|
1270
|
+
if archive_dest_check.exists():
|
|
1271
|
+
print(colored(
|
|
1272
|
+
f"Error: refusing to archive '{task_name}': "
|
|
1273
|
+
f"archive destination already exists: "
|
|
1274
|
+
f"{_repo_relative_path(archive_dest_check, repo_root)}",
|
|
1275
|
+
Colors.RED), file=sys.stderr)
|
|
1276
|
+
print(f"Task remains at: {_repo_relative_path(task_dir, repo_root)}", file=sys.stderr)
|
|
1277
|
+
print("Move or rename the existing archived task, then retry.", file=sys.stderr)
|
|
1278
|
+
return 1
|
|
1279
|
+
|
|
1280
|
+
dir_name = task_dir.name
|
|
1281
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
1282
|
+
|
|
1283
|
+
# Update status before archiving
|
|
1284
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
1285
|
+
# Names of child task dirs whose task.json gets modified below; passed
|
|
1286
|
+
# into safe_archive_paths_to_add so they're staged in this commit.
|
|
1287
|
+
modified_children: list[str] = []
|
|
1288
|
+
if task_json_path.is_file():
|
|
1289
|
+
data, read_reason = read_json_checked(task_json_path)
|
|
1290
|
+
if data is None:
|
|
1291
|
+
# Archiving is still the right outcome for a task whose task.json
|
|
1292
|
+
# is broken — but say so, or the missing "completed" status looks
|
|
1293
|
+
# like the archive silently did half its job.
|
|
1294
|
+
problem, _ = describe_json_read_failure(task_json_path, read_reason)
|
|
1295
|
+
print(
|
|
1296
|
+
colored(
|
|
1297
|
+
f"Warning: {problem}; archiving without updating status/children.",
|
|
1298
|
+
Colors.YELLOW,
|
|
1299
|
+
),
|
|
1300
|
+
file=sys.stderr,
|
|
1301
|
+
)
|
|
1302
|
+
else:
|
|
1303
|
+
# Before any mutation: branch metadata is unrecoverable once the
|
|
1304
|
+
# task leaves the active tree. Stale branches only warn.
|
|
1305
|
+
if not _validate_branch_metadata(
|
|
1306
|
+
data,
|
|
1307
|
+
task_name,
|
|
1308
|
+
repo_root,
|
|
1309
|
+
getattr(args, "skip_branch_validation", False),
|
|
1310
|
+
):
|
|
1311
|
+
print(
|
|
1312
|
+
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged.",
|
|
1313
|
+
file=sys.stderr,
|
|
1314
|
+
)
|
|
1315
|
+
return 1
|
|
1316
|
+
|
|
1317
|
+
data["status"] = "completed"
|
|
1318
|
+
data["completedAt"] = today
|
|
1319
|
+
if not write_json(task_json_path, data):
|
|
1320
|
+
_report_write_failure(task_json_path)
|
|
1321
|
+
print(
|
|
1322
|
+
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged. "
|
|
1323
|
+
"Archiving a task still marked in progress would hide it from `list` "
|
|
1324
|
+
"with the wrong status.",
|
|
1325
|
+
file=sys.stderr,
|
|
1326
|
+
)
|
|
1327
|
+
return 1
|
|
1328
|
+
|
|
1329
|
+
# Handle subtask relationships on archive.
|
|
1330
|
+
# Keep this task in its parent's children list so progress
|
|
1331
|
+
# counters (children_progress) stay consistent — children
|
|
1332
|
+
# missing from the active set are treated as completed.
|
|
1333
|
+
task_children = data.get("children", [])
|
|
1334
|
+
|
|
1335
|
+
# If this is a parent, clear parent field in all children
|
|
1336
|
+
if task_children:
|
|
1337
|
+
for child_name in task_children:
|
|
1338
|
+
child_dir_path = find_task_by_name(child_name, tasks_dir)
|
|
1339
|
+
if child_dir_path:
|
|
1340
|
+
child_json = child_dir_path / FILE_TASK_JSON
|
|
1341
|
+
if child_json.is_file():
|
|
1342
|
+
child_data, child_reason = read_json_checked(child_json)
|
|
1343
|
+
if child_data is None:
|
|
1344
|
+
problem, _ = describe_json_read_failure(child_json, child_reason)
|
|
1345
|
+
print(
|
|
1346
|
+
colored(
|
|
1347
|
+
f"Warning: {problem}; child '{child_dir_path.name}' "
|
|
1348
|
+
"keeps its parent reference.",
|
|
1349
|
+
Colors.YELLOW,
|
|
1350
|
+
),
|
|
1351
|
+
file=sys.stderr,
|
|
1352
|
+
)
|
|
1353
|
+
continue
|
|
1354
|
+
child_data["parent"] = None
|
|
1355
|
+
if not write_json(child_json, child_data):
|
|
1356
|
+
# Stop before the move: a child pointing at a
|
|
1357
|
+
# parent that has left .trellis/tasks/ is a
|
|
1358
|
+
# dangling reference nothing repairs later.
|
|
1359
|
+
# Retrying is safe — every step so far is
|
|
1360
|
+
# idempotent.
|
|
1361
|
+
_report_write_failure(child_json)
|
|
1362
|
+
print(
|
|
1363
|
+
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is "
|
|
1364
|
+
f"marked completed but stays in place because child "
|
|
1365
|
+
f"'{child_dir_path.name}' could not be unlinked. "
|
|
1366
|
+
"Fix the child, then run archive again.",
|
|
1367
|
+
file=sys.stderr,
|
|
1368
|
+
)
|
|
1369
|
+
return 1
|
|
1370
|
+
modified_children.append(child_dir_path.name)
|
|
1371
|
+
|
|
1372
|
+
# Clear any session that still points at this task before the path moves.
|
|
1373
|
+
from .active_task import clear_task_from_sessions
|
|
1374
|
+
clear_task_from_sessions(str(task_dir), repo_root)
|
|
1375
|
+
|
|
1376
|
+
# Archive
|
|
1377
|
+
result = archive_task_complete(task_dir, repo_root)
|
|
1378
|
+
if "archived_to" in result:
|
|
1379
|
+
archive_dest = Path(result["archived_to"])
|
|
1380
|
+
year_month = archive_dest.parent.name
|
|
1381
|
+
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
|
|
1382
|
+
|
|
1383
|
+
# Auto-commit unless --no-commit
|
|
1384
|
+
if not getattr(args, "no_commit", False):
|
|
1385
|
+
if not _auto_commit_archive(dir_name, repo_root, modified_children):
|
|
1386
|
+
print(
|
|
1387
|
+
colored(
|
|
1388
|
+
"Archive moved on disk, but git auto-commit did not complete. "
|
|
1389
|
+
"Resolve `git status` before continuing.",
|
|
1390
|
+
Colors.RED,
|
|
1391
|
+
),
|
|
1392
|
+
file=sys.stderr,
|
|
1393
|
+
)
|
|
1394
|
+
return 1
|
|
1395
|
+
|
|
1396
|
+
# Return the archive path
|
|
1397
|
+
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
|
|
1398
|
+
|
|
1399
|
+
# Run hooks with the archived path
|
|
1400
|
+
archived_json = archive_dest / FILE_TASK_JSON
|
|
1401
|
+
run_task_hooks("after_archive", archived_json, repo_root)
|
|
1402
|
+
return 0
|
|
1403
|
+
|
|
1404
|
+
return 1
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _auto_commit_archive(
|
|
1408
|
+
task_name: str,
|
|
1409
|
+
repo_root: Path,
|
|
1410
|
+
modified_children: list[str] | None = None,
|
|
1411
|
+
) -> bool:
|
|
1412
|
+
"""Stage Trellis-owned task paths and commit after archive.
|
|
1413
|
+
|
|
1414
|
+
Scoped narrowly to the archived task's source + destination paths
|
|
1415
|
+
plus any child task dirs whose ``task.json`` was edited (parent →
|
|
1416
|
+
children relationship update). Dirty changes in OTHER active task
|
|
1417
|
+
dirs are NOT bundled into the archive commit.
|
|
1418
|
+
|
|
1419
|
+
If ``.gitignore`` blocks the paths, we warn + skip — we do NOT
|
|
1420
|
+
retry with ``git add -f``. The warning explicitly forbids
|
|
1421
|
+
``git add -f .trellis/`` (which would fan out to caches/backups)
|
|
1422
|
+
and points users at ``session_auto_commit: false``.
|
|
1423
|
+
|
|
1424
|
+
Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when
|
|
1425
|
+
set to ``false``, this function returns immediately without
|
|
1426
|
+
touching git (the archive directory move on disk is unaffected).
|
|
1427
|
+
"""
|
|
1428
|
+
if not get_session_auto_commit(repo_root):
|
|
1429
|
+
print(
|
|
1430
|
+
"[OK] session_auto_commit: false — skipping git stage/commit.",
|
|
1431
|
+
file=sys.stderr,
|
|
1432
|
+
)
|
|
1433
|
+
return True
|
|
1434
|
+
|
|
1435
|
+
source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
|
|
1436
|
+
rc, tracked_out, _ = run_git(
|
|
1437
|
+
["ls-files", "--", source_rel],
|
|
1438
|
+
cwd=repo_root,
|
|
1439
|
+
)
|
|
1440
|
+
source_was_tracked = rc == 0 and bool(tracked_out.strip())
|
|
1441
|
+
|
|
1442
|
+
paths = safe_archive_paths_to_add(
|
|
1443
|
+
repo_root, task_name=task_name, modified_children=modified_children
|
|
1444
|
+
)
|
|
1445
|
+
if not paths:
|
|
1446
|
+
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
1447
|
+
return True
|
|
1448
|
+
|
|
1449
|
+
success, _, err = safe_git_add(paths, repo_root, retry_on_index_lock=True)
|
|
1450
|
+
if not success:
|
|
1451
|
+
if err and "ignored by" in err.lower():
|
|
1452
|
+
print_gitignore_warning(paths)
|
|
1453
|
+
elif stderr_indicates_index_lock(err):
|
|
1454
|
+
_print_index_lock_warning(
|
|
1455
|
+
"git add", task_name, repo_root, [*paths, source_rel]
|
|
1456
|
+
)
|
|
1457
|
+
else:
|
|
1458
|
+
print(
|
|
1459
|
+
f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
|
|
1460
|
+
file=sys.stderr,
|
|
1461
|
+
)
|
|
1462
|
+
return not source_was_tracked
|
|
1463
|
+
|
|
1464
|
+
# Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses
|
|
1465
|
+
# `git add` (no -A) which only stages additions/modifications. The
|
|
1466
|
+
# source task directory was moved away by `shutil.move`, so its files
|
|
1467
|
+
# need an explicit `git rm --cached` to stage the deletions in this
|
|
1468
|
+
# same commit — otherwise they sit as uncommitted "phantom deletes"
|
|
1469
|
+
# against HEAD until something later picks them up.
|
|
1470
|
+
#
|
|
1471
|
+
# `--ignore-unmatch` makes this a no-op when the task was never tracked
|
|
1472
|
+
# (e.g. archiving a task that lived only in working tree).
|
|
1473
|
+
rc, _, err = run_git_retry_index_lock(
|
|
1474
|
+
["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel],
|
|
1475
|
+
cwd=repo_root,
|
|
1476
|
+
)
|
|
1477
|
+
if rc != 0 and stderr_indicates_index_lock(err):
|
|
1478
|
+
# Committing now would record the archived copy without the
|
|
1479
|
+
# source-side deletes — a half-archived tree in history.
|
|
1480
|
+
_print_index_lock_warning(
|
|
1481
|
+
"git rm --cached", task_name, repo_root, [*paths, source_rel]
|
|
1482
|
+
)
|
|
1483
|
+
return not source_was_tracked
|
|
1484
|
+
|
|
1485
|
+
rc, _, _ = run_git(
|
|
1486
|
+
["diff", "--cached", "--quiet", "--", *paths, source_rel],
|
|
1487
|
+
cwd=repo_root,
|
|
1488
|
+
)
|
|
1489
|
+
if rc == 0:
|
|
1490
|
+
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
1491
|
+
return True
|
|
1492
|
+
|
|
1493
|
+
commit_msg = f"chore(task): archive {task_name}"
|
|
1494
|
+
# Commit with an explicit pathspec: a bare `git commit` would sweep any
|
|
1495
|
+
# unrelated entries the developer had staged before archiving into the
|
|
1496
|
+
# chore commit (#579). `source_rel` is included so the source-side
|
|
1497
|
+
# deletions staged above land in the same commit.
|
|
1498
|
+
rc, _, err = run_git_retry_index_lock(
|
|
1499
|
+
["commit", "-m", commit_msg, "--", *paths, source_rel], cwd=repo_root
|
|
1500
|
+
)
|
|
1501
|
+
if rc == 0:
|
|
1502
|
+
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
1503
|
+
return True
|
|
1504
|
+
elif stderr_indicates_index_lock(err):
|
|
1505
|
+
_print_index_lock_warning(
|
|
1506
|
+
"git commit", task_name, repo_root, [*paths, source_rel]
|
|
1507
|
+
)
|
|
1508
|
+
return not source_was_tracked
|
|
1509
|
+
else:
|
|
1510
|
+
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
|
|
1511
|
+
return not source_was_tracked
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
def _print_index_lock_warning(
|
|
1515
|
+
action: str, task_name: str, repo_root: Path, paths: list[str]
|
|
1516
|
+
) -> None:
|
|
1517
|
+
"""Report an archive auto-commit that gave up on a held index.lock.
|
|
1518
|
+
|
|
1519
|
+
The move itself already succeeded, so the state is consistent — the task
|
|
1520
|
+
lives in archive/ and its changes are staged-or-not but never half of
|
|
1521
|
+
both in a commit. Only the commit is outstanding, which the user (or an
|
|
1522
|
+
agent reading the log) has to finish by hand.
|
|
1523
|
+
|
|
1524
|
+
``paths`` are the paths the auto-commit would have staged, so the manual
|
|
1525
|
+
command keeps the same narrow scope — a blanket ``git add -A -- .trellis/``
|
|
1526
|
+
would sweep dirty changes from other active tasks into the archive commit.
|
|
1527
|
+
"""
|
|
1528
|
+
lock = index_lock_path(repo_root)
|
|
1529
|
+
print(
|
|
1530
|
+
f"[WARN] {action} gave up after {INDEX_LOCK_RETRY_ATTEMPTS} attempts: "
|
|
1531
|
+
f"another process is holding {lock}",
|
|
1532
|
+
file=sys.stderr,
|
|
1533
|
+
)
|
|
1534
|
+
print(
|
|
1535
|
+
"[WARN] The task was moved into archive/ on disk; only the commit is pending.",
|
|
1536
|
+
file=sys.stderr,
|
|
1537
|
+
)
|
|
1538
|
+
print(
|
|
1539
|
+
"[WARN] Close whatever holds the lock (an IDE git integration, a status",
|
|
1540
|
+
file=sys.stderr,
|
|
1541
|
+
)
|
|
1542
|
+
print(
|
|
1543
|
+
f"[WARN] daemon, another session), or delete {lock} if it is stale, then",
|
|
1544
|
+
file=sys.stderr,
|
|
1545
|
+
)
|
|
1546
|
+
print(
|
|
1547
|
+
f'[WARN] commit manually: git add -A -- {" ".join(paths)} && '
|
|
1548
|
+
f'git commit -m "chore(task): archive {task_name}"',
|
|
1549
|
+
file=sys.stderr,
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
# =============================================================================
|
|
1554
|
+
# Command: add-subtask
|
|
1555
|
+
# =============================================================================
|
|
1556
|
+
|
|
1557
|
+
def cmd_add_subtask(args: argparse.Namespace) -> int:
|
|
1558
|
+
"""Link a child task to a parent task."""
|
|
1559
|
+
repo_root = get_repo_root()
|
|
1560
|
+
|
|
1561
|
+
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
1562
|
+
child_dir = resolve_task_dir(args.child_dir, repo_root)
|
|
1563
|
+
if parent_dir is None or child_dir is None:
|
|
1564
|
+
return 1
|
|
1565
|
+
|
|
1566
|
+
if not parent_dir:
|
|
1567
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
1568
|
+
return 1
|
|
1569
|
+
|
|
1570
|
+
if not child_dir:
|
|
1571
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
1572
|
+
return 1
|
|
1573
|
+
|
|
1574
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
1575
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
1576
|
+
|
|
1577
|
+
if not parent_json_path.is_file():
|
|
1578
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
1579
|
+
return 1
|
|
1580
|
+
|
|
1581
|
+
if not child_json_path.is_file():
|
|
1582
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
1583
|
+
return 1
|
|
1584
|
+
|
|
1585
|
+
parent_data, parent_reason = read_json_checked(parent_json_path)
|
|
1586
|
+
if parent_data is None:
|
|
1587
|
+
_report_read_failure(parent_json_path, parent_reason)
|
|
1588
|
+
return 1
|
|
1589
|
+
child_data, child_reason = read_json_checked(child_json_path)
|
|
1590
|
+
if child_data is None:
|
|
1591
|
+
_report_read_failure(child_json_path, child_reason)
|
|
1592
|
+
return 1
|
|
1593
|
+
|
|
1594
|
+
# Check if child already has a parent
|
|
1595
|
+
existing_parent = child_data.get("parent")
|
|
1596
|
+
if existing_parent:
|
|
1597
|
+
print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
|
|
1598
|
+
return 1
|
|
1599
|
+
|
|
1600
|
+
# Add child to parent's children list
|
|
1601
|
+
parent_children = _ensure_children_list(parent_data)
|
|
1602
|
+
child_dir_name = child_dir.name
|
|
1603
|
+
if child_dir_name not in parent_children:
|
|
1604
|
+
parent_children.append(child_dir_name)
|
|
1605
|
+
parent_data["children"] = parent_children
|
|
1606
|
+
|
|
1607
|
+
# Set parent in child's task.json
|
|
1608
|
+
child_data["parent"] = parent_dir.name
|
|
1609
|
+
|
|
1610
|
+
# Write both. A link is two files; if the second write fails silently the
|
|
1611
|
+
# two sides disagree with no error, so check each and name the side that
|
|
1612
|
+
# did land so the user knows what to undo.
|
|
1613
|
+
if not write_json(parent_json_path, parent_data):
|
|
1614
|
+
print(colored(f"Error: Failed to write parent task.json: {parent_json_path}", Colors.RED), file=sys.stderr)
|
|
1615
|
+
print("No link was written.", file=sys.stderr)
|
|
1616
|
+
return 1
|
|
1617
|
+
if not write_json(child_json_path, child_data):
|
|
1618
|
+
print(colored(f"Error: Failed to write child task.json: {child_json_path}", Colors.RED), file=sys.stderr)
|
|
1619
|
+
print(
|
|
1620
|
+
f"Link is half-written: {parent_json_path} now lists "
|
|
1621
|
+
f"'{child_dir.name}' as a child, but the child does not record the parent.",
|
|
1622
|
+
file=sys.stderr,
|
|
1623
|
+
)
|
|
1624
|
+
return 1
|
|
1625
|
+
|
|
1626
|
+
print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
1627
|
+
return 0
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
# =============================================================================
|
|
1631
|
+
# Command: remove-subtask
|
|
1632
|
+
# =============================================================================
|
|
1633
|
+
|
|
1634
|
+
def cmd_remove_subtask(args: argparse.Namespace) -> int:
|
|
1635
|
+
"""Unlink a child task from a parent task."""
|
|
1636
|
+
repo_root = get_repo_root()
|
|
1637
|
+
|
|
1638
|
+
parent_dir = resolve_task_dir(args.parent_dir, repo_root)
|
|
1639
|
+
child_dir = resolve_task_dir(args.child_dir, repo_root)
|
|
1640
|
+
if parent_dir is None or child_dir is None:
|
|
1641
|
+
return 1
|
|
1642
|
+
|
|
1643
|
+
if not parent_dir:
|
|
1644
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
1645
|
+
return 1
|
|
1646
|
+
|
|
1647
|
+
if not child_dir:
|
|
1648
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
1649
|
+
return 1
|
|
1650
|
+
|
|
1651
|
+
parent_json_path = parent_dir / FILE_TASK_JSON
|
|
1652
|
+
child_json_path = child_dir / FILE_TASK_JSON
|
|
1653
|
+
|
|
1654
|
+
if not parent_json_path.is_file():
|
|
1655
|
+
print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
|
|
1656
|
+
return 1
|
|
1657
|
+
|
|
1658
|
+
if not child_json_path.is_file():
|
|
1659
|
+
print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
|
|
1660
|
+
return 1
|
|
1661
|
+
|
|
1662
|
+
parent_data, parent_reason = read_json_checked(parent_json_path)
|
|
1663
|
+
if parent_data is None:
|
|
1664
|
+
_report_read_failure(parent_json_path, parent_reason)
|
|
1665
|
+
return 1
|
|
1666
|
+
child_data, child_reason = read_json_checked(child_json_path)
|
|
1667
|
+
if child_data is None:
|
|
1668
|
+
_report_read_failure(child_json_path, child_reason)
|
|
1669
|
+
return 1
|
|
1670
|
+
|
|
1671
|
+
# Remove child from parent's children list
|
|
1672
|
+
parent_children = _ensure_children_list(parent_data)
|
|
1673
|
+
child_dir_name = child_dir.name
|
|
1674
|
+
if child_dir_name in parent_children:
|
|
1675
|
+
parent_children.remove(child_dir_name)
|
|
1676
|
+
parent_data["children"] = parent_children
|
|
1677
|
+
|
|
1678
|
+
# Clear parent in child's task.json
|
|
1679
|
+
child_data["parent"] = None
|
|
1680
|
+
|
|
1681
|
+
# Write both — see cmd_add_subtask: an unchecked second write leaves the
|
|
1682
|
+
# two sides disagreeing with no error.
|
|
1683
|
+
if not write_json(parent_json_path, parent_data):
|
|
1684
|
+
print(colored(f"Error: Failed to write parent task.json: {parent_json_path}", Colors.RED), file=sys.stderr)
|
|
1685
|
+
print("Nothing was unlinked.", file=sys.stderr)
|
|
1686
|
+
return 1
|
|
1687
|
+
if not write_json(child_json_path, child_data):
|
|
1688
|
+
print(colored(f"Error: Failed to write child task.json: {child_json_path}", Colors.RED), file=sys.stderr)
|
|
1689
|
+
print(
|
|
1690
|
+
f"Unlink is half-written: {parent_json_path} no longer lists "
|
|
1691
|
+
f"'{child_dir.name}', but the child still records the parent.",
|
|
1692
|
+
file=sys.stderr,
|
|
1693
|
+
)
|
|
1694
|
+
return 1
|
|
1695
|
+
|
|
1696
|
+
print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
1697
|
+
return 0
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
# =============================================================================
|
|
1701
|
+
# Command: set-branch
|
|
1702
|
+
# =============================================================================
|
|
1703
|
+
|
|
1704
|
+
def cmd_set_branch(args: argparse.Namespace) -> int:
|
|
1705
|
+
"""Set git branch for task."""
|
|
1706
|
+
repo_root = get_repo_root()
|
|
1707
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1708
|
+
if target_dir is None:
|
|
1709
|
+
return 1
|
|
1710
|
+
branch = args.branch
|
|
1711
|
+
|
|
1712
|
+
if not branch:
|
|
1713
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
1714
|
+
print("Usage: python3 task.py set-branch <task-dir> <branch-name>")
|
|
1715
|
+
return 1
|
|
1716
|
+
|
|
1717
|
+
if not target_dir:
|
|
1718
|
+
# target_dir is None here, so it must not appear in the message. This
|
|
1719
|
+
# is also the branch a ref pointing outside the repo lands in.
|
|
1720
|
+
print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
|
|
1721
|
+
return 1
|
|
1722
|
+
|
|
1723
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1724
|
+
if not task_json.is_file():
|
|
1725
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
1726
|
+
return 1
|
|
1727
|
+
|
|
1728
|
+
data, reason = read_json_checked(task_json)
|
|
1729
|
+
if data is None:
|
|
1730
|
+
_report_read_failure(task_json, reason)
|
|
1731
|
+
return 1
|
|
1732
|
+
|
|
1733
|
+
data["branch"] = branch
|
|
1734
|
+
if not write_json(task_json, data):
|
|
1735
|
+
_report_write_failure(task_json)
|
|
1736
|
+
return 1
|
|
1737
|
+
|
|
1738
|
+
print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
|
|
1739
|
+
return 0
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
# =============================================================================
|
|
1743
|
+
# Command: set-base-branch
|
|
1744
|
+
# =============================================================================
|
|
1745
|
+
|
|
1746
|
+
def cmd_set_base_branch(args: argparse.Namespace) -> int:
|
|
1747
|
+
"""Set the base branch (PR target) for task."""
|
|
1748
|
+
repo_root = get_repo_root()
|
|
1749
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1750
|
+
if target_dir is None:
|
|
1751
|
+
return 1
|
|
1752
|
+
base_branch = args.base_branch
|
|
1753
|
+
|
|
1754
|
+
if not base_branch:
|
|
1755
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
1756
|
+
print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>")
|
|
1757
|
+
print("Example: python3 task.py set-base-branch <dir> develop")
|
|
1758
|
+
print()
|
|
1759
|
+
print("This sets the target branch for PR (the branch your feature will merge into).")
|
|
1760
|
+
return 1
|
|
1761
|
+
|
|
1762
|
+
if not target_dir:
|
|
1763
|
+
# target_dir is None here, so it must not appear in the message. This
|
|
1764
|
+
# is also the branch a ref pointing outside the repo lands in.
|
|
1765
|
+
print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
|
|
1766
|
+
return 1
|
|
1767
|
+
|
|
1768
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1769
|
+
if not task_json.is_file():
|
|
1770
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
1771
|
+
return 1
|
|
1772
|
+
|
|
1773
|
+
data, reason = read_json_checked(task_json)
|
|
1774
|
+
if data is None:
|
|
1775
|
+
_report_read_failure(task_json, reason)
|
|
1776
|
+
return 1
|
|
1777
|
+
|
|
1778
|
+
data["base_branch"] = base_branch
|
|
1779
|
+
if not write_json(task_json, data):
|
|
1780
|
+
_report_write_failure(task_json)
|
|
1781
|
+
return 1
|
|
1782
|
+
|
|
1783
|
+
print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
|
|
1784
|
+
print(f" PR will target: {base_branch}")
|
|
1785
|
+
return 0
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
# =============================================================================
|
|
1789
|
+
# Command: set-scope
|
|
1790
|
+
# =============================================================================
|
|
1791
|
+
|
|
1792
|
+
def cmd_set_scope(args: argparse.Namespace) -> int:
|
|
1793
|
+
"""Set scope for PR title."""
|
|
1794
|
+
repo_root = get_repo_root()
|
|
1795
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1796
|
+
if target_dir is None:
|
|
1797
|
+
return 1
|
|
1798
|
+
scope = args.scope
|
|
1799
|
+
|
|
1800
|
+
if not scope:
|
|
1801
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
1802
|
+
print("Usage: python3 task.py set-scope <task-dir> <scope>")
|
|
1803
|
+
return 1
|
|
1804
|
+
|
|
1805
|
+
if not target_dir:
|
|
1806
|
+
# target_dir is None here, so it must not appear in the message. This
|
|
1807
|
+
# is also the branch a ref pointing outside the repo lands in.
|
|
1808
|
+
print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
|
|
1809
|
+
return 1
|
|
1810
|
+
|
|
1811
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1812
|
+
if not task_json.is_file():
|
|
1813
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
1814
|
+
return 1
|
|
1815
|
+
|
|
1816
|
+
data, reason = read_json_checked(task_json)
|
|
1817
|
+
if data is None:
|
|
1818
|
+
_report_read_failure(task_json, reason)
|
|
1819
|
+
return 1
|
|
1820
|
+
|
|
1821
|
+
data["scope"] = scope
|
|
1822
|
+
if not write_json(task_json, data):
|
|
1823
|
+
_report_write_failure(task_json)
|
|
1824
|
+
return 1
|
|
1825
|
+
|
|
1826
|
+
print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
|
|
1827
|
+
return 0
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
# =============================================================================
|
|
1831
|
+
# Command: set-meta
|
|
1832
|
+
# =============================================================================
|
|
1833
|
+
|
|
1834
|
+
def cmd_set_meta(args: argparse.Namespace) -> int:
|
|
1835
|
+
"""Set/overwrite one metadata key on an existing task."""
|
|
1836
|
+
repo_root = get_repo_root()
|
|
1837
|
+
target_dir = resolve_task_dir(args.dir, repo_root)
|
|
1838
|
+
if target_dir is None:
|
|
1839
|
+
return 1
|
|
1840
|
+
key = args.key
|
|
1841
|
+
value = args.value
|
|
1842
|
+
|
|
1843
|
+
if not key:
|
|
1844
|
+
print(colored("Error: Missing arguments", Colors.RED))
|
|
1845
|
+
print("Usage: python3 task.py set-meta <task-dir> <key> <value>")
|
|
1846
|
+
return 1
|
|
1847
|
+
|
|
1848
|
+
if not target_dir:
|
|
1849
|
+
# target_dir is None here, so it must not appear in the message. This
|
|
1850
|
+
# is also the branch a ref pointing outside the repo lands in.
|
|
1851
|
+
print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
|
|
1852
|
+
return 1
|
|
1853
|
+
|
|
1854
|
+
task_json = target_dir / FILE_TASK_JSON
|
|
1855
|
+
if not task_json.is_file():
|
|
1856
|
+
print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
|
|
1857
|
+
return 1
|
|
1858
|
+
|
|
1859
|
+
data, reason = read_json_checked(task_json)
|
|
1860
|
+
if data is None:
|
|
1861
|
+
_report_read_failure(task_json, reason)
|
|
1862
|
+
return 1
|
|
1863
|
+
|
|
1864
|
+
meta = data.get("meta")
|
|
1865
|
+
if not isinstance(meta, dict):
|
|
1866
|
+
meta = {}
|
|
1867
|
+
meta[key] = value
|
|
1868
|
+
data["meta"] = meta
|
|
1869
|
+
if not write_json(task_json, data):
|
|
1870
|
+
_report_write_failure(task_json)
|
|
1871
|
+
return 1
|
|
1872
|
+
|
|
1873
|
+
print(colored(f"✓ Meta set: {key} = {value}", Colors.GREEN))
|
|
1874
|
+
return 0
|