@pennixrv/trellis 0.7.0-beta.9 → 0.7.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/ablate.js +1 -1
- package/dist/commands/ablate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -56
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/task-progress.d.ts +2 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +37 -30
- package/dist/commands/task-progress.js.map +1 -1
- package/dist/commands/{channel → teamwork}/adapters/claude.d.ts +3 -3
- package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/claude.js +3 -3
- package/dist/commands/teamwork/adapters/claude.js.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/codex.d.ts +1 -15
- package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/codex.js +5 -42
- package/dist/commands/teamwork/adapters/codex.js.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/index.d.ts +6 -6
- package/dist/commands/teamwork/adapters/index.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/index.js +16 -52
- package/dist/commands/teamwork/adapters/index.js.map +1 -0
- package/dist/commands/{channel → teamwork}/adapters/types.d.ts +2 -2
- package/dist/commands/teamwork/adapters/types.d.ts.map +1 -0
- package/dist/commands/teamwork/adapters/types.js.map +1 -0
- package/dist/commands/{channel → teamwork}/agent-loader.d.ts +2 -4
- package/dist/commands/teamwork/agent-loader.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/agent-loader.js +4 -35
- package/dist/commands/teamwork/agent-loader.js.map +1 -0
- package/dist/commands/teamwork/artifact.d.ts +177 -0
- package/dist/commands/teamwork/artifact.d.ts.map +1 -0
- package/dist/commands/teamwork/artifact.js +908 -0
- package/dist/commands/teamwork/artifact.js.map +1 -0
- package/dist/commands/{channel → teamwork}/context-loader.d.ts +1 -1
- package/dist/commands/teamwork/context-loader.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/context-loader.js +12 -12
- package/dist/commands/teamwork/context-loader.js.map +1 -0
- package/dist/commands/{channel → teamwork}/context-trust.d.ts +6 -6
- package/dist/commands/teamwork/context-trust.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/context-trust.js +16 -16
- package/dist/commands/teamwork/context-trust.js.map +1 -0
- package/dist/commands/teamwork/context.d.ts +16 -0
- package/dist/commands/teamwork/context.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/context.js +17 -17
- package/dist/commands/teamwork/context.js.map +1 -0
- package/dist/commands/teamwork/coordinator.d.ts +89 -0
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -0
- package/dist/commands/teamwork/coordinator.js +1198 -0
- package/dist/commands/teamwork/coordinator.js.map +1 -0
- package/dist/commands/teamwork/create.d.ts +23 -0
- package/dist/commands/teamwork/create.d.ts.map +1 -0
- package/dist/commands/teamwork/create.js +39 -0
- package/dist/commands/teamwork/create.js.map +1 -0
- package/dist/commands/{channel → teamwork}/dev-parse-trace.d.ts +1 -1
- package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/dev-parse-trace.js +1 -1
- package/dist/commands/teamwork/dev-parse-trace.js.map +1 -0
- package/dist/commands/{channel → teamwork}/guard.d.ts +27 -46
- package/dist/commands/teamwork/guard.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/guard.js +54 -156
- package/dist/commands/teamwork/guard.js.map +1 -0
- package/dist/commands/teamwork/index.d.ts +3 -0
- package/dist/commands/teamwork/index.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/index.js +135 -165
- package/dist/commands/teamwork/index.js.map +1 -0
- package/dist/commands/{channel → teamwork}/interrupt.d.ts +1 -1
- package/dist/commands/teamwork/interrupt.d.ts.map +1 -0
- package/dist/commands/teamwork/interrupt.js +22 -0
- package/dist/commands/teamwork/interrupt.js.map +1 -0
- package/dist/commands/teamwork/kill.d.ts +7 -0
- package/dist/commands/teamwork/kill.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/kill.js +21 -21
- package/dist/commands/teamwork/kill.js.map +1 -0
- package/dist/commands/teamwork/list.d.ts +17 -0
- package/dist/commands/teamwork/list.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/list.js +18 -27
- package/dist/commands/teamwork/list.js.map +1 -0
- package/dist/commands/{channel → teamwork}/messages.d.ts +1 -1
- package/dist/commands/teamwork/messages.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/messages.js +18 -18
- package/dist/commands/teamwork/messages.js.map +1 -0
- package/dist/commands/teamwork/profiles.d.ts +25 -0
- package/dist/commands/teamwork/profiles.d.ts.map +1 -0
- package/dist/commands/teamwork/profiles.js +125 -0
- package/dist/commands/teamwork/profiles.js.map +1 -0
- package/dist/commands/teamwork/rm.d.ts +27 -0
- package/dist/commands/teamwork/rm.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/rm.js +21 -22
- package/dist/commands/teamwork/rm.js.map +1 -0
- package/dist/commands/teamwork/run.d.ts +29 -0
- package/dist/commands/{channel → teamwork}/run.d.ts.map +1 -1
- package/dist/commands/teamwork/run.js +129 -0
- package/dist/commands/teamwork/run.js.map +1 -0
- package/dist/commands/{channel → teamwork}/send.d.ts +1 -1
- package/dist/commands/teamwork/send.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/send.js +7 -7
- package/dist/commands/teamwork/send.js.map +1 -0
- package/dist/commands/{channel → teamwork}/spawn.d.ts +8 -12
- package/dist/commands/teamwork/spawn.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/spawn.js +51 -112
- package/dist/commands/teamwork/spawn.js.map +1 -0
- package/dist/commands/teamwork/store/events.d.ts +22 -0
- package/dist/commands/teamwork/store/events.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/store/events.js +8 -8
- package/dist/commands/teamwork/store/events.js.map +1 -0
- package/dist/commands/teamwork/store/filter.d.ts +3 -0
- package/dist/commands/teamwork/store/filter.d.ts.map +1 -0
- package/dist/commands/teamwork/store/filter.js +2 -0
- package/dist/commands/teamwork/store/filter.js.map +1 -0
- package/dist/commands/{channel → teamwork}/store/lock.d.ts +1 -1
- package/dist/commands/teamwork/store/lock.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/store/lock.js +2 -2
- package/dist/commands/teamwork/store/lock.js.map +1 -0
- package/dist/commands/{channel → teamwork}/store/paths.d.ts +18 -32
- package/dist/commands/teamwork/store/paths.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/store/paths.js +42 -109
- package/dist/commands/teamwork/store/paths.js.map +1 -0
- package/dist/commands/teamwork/store/schema.d.ts +18 -0
- package/dist/commands/teamwork/store/schema.d.ts.map +1 -0
- package/dist/commands/teamwork/store/schema.js +23 -0
- package/dist/commands/teamwork/store/schema.js.map +1 -0
- package/dist/commands/{channel → teamwork}/store/thread-state.d.ts +3 -3
- package/dist/commands/teamwork/store/thread-state.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/store/thread-state.js +1 -1
- package/dist/commands/teamwork/store/thread-state.js.map +1 -0
- package/dist/commands/teamwork/store/watch.d.ts +19 -0
- package/dist/commands/teamwork/store/watch.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/store/watch.js +8 -8
- package/dist/commands/teamwork/store/watch.js.map +1 -0
- package/dist/commands/teamwork/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/teamwork/supervisor/idle.js.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/inbox.d.ts +2 -2
- package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/inbox.js +14 -14
- package/dist/commands/teamwork/supervisor/inbox.js.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/shutdown.d.ts +3 -6
- package/dist/commands/teamwork/supervisor/shutdown.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/shutdown.js +7 -17
- package/dist/commands/teamwork/supervisor/shutdown.js.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/stdout.d.ts +3 -5
- package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/stdout.js +12 -23
- package/dist/commands/teamwork/supervisor/stdout.js.map +1 -0
- package/dist/commands/teamwork/supervisor/turns.d.ts.map +1 -0
- package/dist/commands/teamwork/supervisor/turns.js.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/warning.d.ts +5 -5
- package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor/warning.js +6 -6
- package/dist/commands/teamwork/supervisor/warning.js.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor.d.ts +21 -21
- package/dist/commands/teamwork/supervisor.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/supervisor.js +53 -63
- package/dist/commands/teamwork/supervisor.js.map +1 -0
- package/dist/commands/teamwork/teammate-adapters.d.ts +31 -0
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -0
- package/dist/commands/teamwork/teammate-adapters.js +96 -0
- package/dist/commands/teamwork/teammate-adapters.js.map +1 -0
- package/dist/commands/teamwork/teammate-config.d.ts +19 -0
- package/dist/commands/teamwork/teammate-config.d.ts.map +1 -0
- package/dist/commands/teamwork/teammate-config.js +97 -0
- package/dist/commands/teamwork/teammate-config.js.map +1 -0
- package/dist/commands/teamwork/teammate-launch.d.ts +58 -0
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -0
- package/dist/commands/teamwork/teammate-launch.js +125 -0
- package/dist/commands/teamwork/teammate-launch.js.map +1 -0
- package/dist/commands/teamwork/text-body.d.ts +13 -0
- package/dist/commands/teamwork/text-body.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/text-body.js +3 -3
- package/dist/commands/teamwork/text-body.js.map +1 -0
- package/dist/commands/teamwork/threads.d.ts +35 -0
- package/dist/commands/teamwork/threads.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/threads.js +19 -19
- package/dist/commands/teamwork/threads.js.map +1 -0
- package/dist/commands/teamwork/title.d.ts +12 -0
- package/dist/commands/teamwork/title.d.ts.map +1 -0
- package/dist/commands/teamwork/title.js +24 -0
- package/dist/commands/teamwork/title.js.map +1 -0
- package/dist/commands/{channel → teamwork}/wait.d.ts +1 -5
- package/dist/commands/teamwork/wait.d.ts.map +1 -0
- package/dist/commands/{channel → teamwork}/wait.js +8 -21
- package/dist/commands/teamwork/wait.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +48 -89
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +9 -10
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/claude.d.ts +0 -17
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +2 -61
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +2 -2
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/snow.js +1 -1
- package/dist/configurators/snow.js.map +1 -1
- package/dist/configurators/workflow.d.ts +1 -1
- package/dist/configurators/workflow.js +5 -5
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +1 -1
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +1 -1
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.18.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.20.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.21.json +17 -0
- package/dist/migrations/manifests/0.7.0-beta.22.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.23.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.24.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.25.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.26.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.27.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.28.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.29.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.30.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.31.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.32.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.35.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.36.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.37.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.38.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.39.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.40.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.41.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.42.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.43.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.44.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.45.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.46.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.47.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.48.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.49.json +13 -0
- package/dist/migrations/manifests/0.7.0-beta.50.json +12 -0
- package/dist/migrations/manifests/0.7.0-beta.51.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.52.json +9 -0
- package/dist/migrations/manifests/0.7.0-beta.53.json +27 -0
- package/dist/migrations/manifests/0.7.0-beta.54.json +8 -0
- package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
- package/dist/migrations/manifests/0.7.1.json +9 -0
- package/dist/templates/claude/hooks/statusline.py +0 -7
- package/dist/templates/codex/hooks/session-start.py +31 -38
- package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +40 -0
- package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/workflow.md +55 -0
- package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +33 -0
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +25 -25
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +13 -19
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +27 -75
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +17 -56
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +14 -11
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -33
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +32 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +25 -111
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +19 -60
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +36 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/SKILL.md +10 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +29 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +73 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +29 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +31 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +37 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +78 -0
- package/dist/templates/common/commands/continue.md +9 -49
- package/dist/templates/common/commands/finish-work.md +10 -58
- package/dist/templates/common/commands/start.md +19 -49
- package/dist/templates/common/skills/brainstorm.md +11 -199
- package/dist/templates/copilot/hooks/session-start.py +30 -26
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +7 -160
- package/dist/templates/copilot/prompts/finish-work.prompt.md +11 -88
- package/dist/templates/copilot/prompts/record-session.prompt.md +4 -12
- package/dist/templates/copilot/prompts/start.prompt.md +12 -10
- package/dist/templates/dsh/DSH.md +2 -2
- package/dist/templates/kiro/agents/trellis.json +2 -2
- package/dist/templates/markdown/agents.md +14 -3
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +9 -8
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +1 -1
- package/dist/templates/markdown/workspace-index.md +0 -1
- package/dist/templates/omp/extensions/trellis/index.ts.txt +28 -17
- package/dist/templates/opencode/lib/session-utils.js +20 -25
- package/dist/templates/opencode/lib/trellis-context.js +4 -76
- package/dist/templates/opencode/plugins/inject-subagent-context.js +5 -4
- package/dist/templates/opencode/plugins/inject-workflow-state.js +14 -17
- package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
- package/dist/templates/shared-hooks/inject-subagent-context.py +445 -67
- package/dist/templates/shared-hooks/inject-workflow-state.py +44 -36
- package/dist/templates/shared-hooks/session-start.py +21 -33
- package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +18 -0
- package/dist/templates/trellis/agents/teammate-profiles.json +8 -0
- package/dist/templates/trellis/agents/teammate.md +63 -0
- package/dist/templates/trellis/config.yaml +29 -38
- package/dist/templates/trellis/gitignore.txt +1 -0
- package/dist/templates/trellis/index.d.ts +8 -12
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +14 -25
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/__init__.py +0 -1
- package/dist/templates/trellis/scripts/common/active_task.py +136 -100
- package/dist/templates/trellis/scripts/common/config.py +7 -9
- package/dist/templates/trellis/scripts/common/git_context.py +1 -1
- package/dist/templates/trellis/scripts/common/io.py +19 -0
- package/dist/templates/trellis/scripts/common/paths.py +0 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +11 -90
- package/dist/templates/trellis/scripts/common/session_context.py +1 -8
- package/dist/templates/trellis/scripts/common/spec_match.py +1 -1
- package/dist/templates/trellis/scripts/common/task_context.py +152 -59
- package/dist/templates/trellis/scripts/common/task_continuity.py +598 -0
- package/dist/templates/trellis/scripts/common/task_store.py +70 -548
- package/dist/templates/trellis/scripts/common/task_utils.py +4 -98
- package/dist/templates/trellis/scripts/common/task_workflow.py +1096 -0
- package/dist/templates/trellis/scripts/common/tasks.py +5 -8
- package/dist/templates/trellis/scripts/common/workflow_phase.py +8 -7
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +3 -3
- package/dist/templates/trellis/scripts/task.py +119 -377
- package/dist/templates/trellis/workflow.md +111 -706
- package/dist/utils/agent-refs.d.ts +5 -5
- package/dist/utils/agent-refs.js +6 -6
- package/dist/utils/file-writer.d.ts +3 -2
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +3 -2
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/managed-paths.d.ts +0 -7
- package/dist/utils/managed-paths.d.ts.map +1 -1
- package/dist/utils/managed-paths.js +0 -40
- package/dist/utils/managed-paths.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +2 -11
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +3 -3
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/dist/utils/workflow-resolver.d.ts +10 -16
- package/dist/utils/workflow-resolver.d.ts.map +1 -1
- package/dist/utils/workflow-resolver.js +25 -36
- package/dist/utils/workflow-resolver.js.map +1 -1
- package/package.json +2 -2
- package/dist/commands/channel/adapters/claude.d.ts.map +0 -1
- package/dist/commands/channel/adapters/claude.js.map +0 -1
- package/dist/commands/channel/adapters/codex.d.ts.map +0 -1
- package/dist/commands/channel/adapters/codex.js.map +0 -1
- package/dist/commands/channel/adapters/index.d.ts.map +0 -1
- package/dist/commands/channel/adapters/index.js.map +0 -1
- package/dist/commands/channel/adapters/types.d.ts.map +0 -1
- package/dist/commands/channel/adapters/types.js.map +0 -1
- package/dist/commands/channel/agent-loader.d.ts.map +0 -1
- package/dist/commands/channel/agent-loader.js.map +0 -1
- package/dist/commands/channel/context-loader.d.ts.map +0 -1
- package/dist/commands/channel/context-loader.js.map +0 -1
- package/dist/commands/channel/context-trust.d.ts.map +0 -1
- package/dist/commands/channel/context-trust.js.map +0 -1
- package/dist/commands/channel/context.d.ts +0 -16
- package/dist/commands/channel/context.d.ts.map +0 -1
- package/dist/commands/channel/context.js.map +0 -1
- package/dist/commands/channel/create.d.ts +0 -29
- package/dist/commands/channel/create.d.ts.map +0 -1
- package/dist/commands/channel/create.js +0 -47
- package/dist/commands/channel/create.js.map +0 -1
- package/dist/commands/channel/dev-parse-trace.d.ts.map +0 -1
- package/dist/commands/channel/dev-parse-trace.js.map +0 -1
- package/dist/commands/channel/guard.d.ts.map +0 -1
- package/dist/commands/channel/guard.js.map +0 -1
- package/dist/commands/channel/index.d.ts +0 -3
- package/dist/commands/channel/index.d.ts.map +0 -1
- package/dist/commands/channel/index.js.map +0 -1
- package/dist/commands/channel/interrupt.d.ts.map +0 -1
- package/dist/commands/channel/interrupt.js +0 -22
- package/dist/commands/channel/interrupt.js.map +0 -1
- package/dist/commands/channel/kill.d.ts +0 -7
- package/dist/commands/channel/kill.d.ts.map +0 -1
- package/dist/commands/channel/kill.js.map +0 -1
- package/dist/commands/channel/list.d.ts +0 -18
- package/dist/commands/channel/list.d.ts.map +0 -1
- package/dist/commands/channel/list.js.map +0 -1
- package/dist/commands/channel/messages.d.ts.map +0 -1
- package/dist/commands/channel/messages.js.map +0 -1
- package/dist/commands/channel/rm.d.ts +0 -27
- package/dist/commands/channel/rm.d.ts.map +0 -1
- package/dist/commands/channel/rm.js.map +0 -1
- package/dist/commands/channel/run.d.ts +0 -32
- package/dist/commands/channel/run.js +0 -141
- package/dist/commands/channel/run.js.map +0 -1
- package/dist/commands/channel/send.d.ts.map +0 -1
- package/dist/commands/channel/send.js.map +0 -1
- package/dist/commands/channel/spawn.d.ts.map +0 -1
- package/dist/commands/channel/spawn.js.map +0 -1
- package/dist/commands/channel/store/events.d.ts +0 -22
- package/dist/commands/channel/store/events.d.ts.map +0 -1
- package/dist/commands/channel/store/events.js.map +0 -1
- package/dist/commands/channel/store/filter.d.ts +0 -3
- package/dist/commands/channel/store/filter.d.ts.map +0 -1
- package/dist/commands/channel/store/filter.js +0 -2
- package/dist/commands/channel/store/filter.js.map +0 -1
- package/dist/commands/channel/store/lock.d.ts.map +0 -1
- package/dist/commands/channel/store/lock.js.map +0 -1
- package/dist/commands/channel/store/paths.d.ts.map +0 -1
- package/dist/commands/channel/store/paths.js.map +0 -1
- package/dist/commands/channel/store/schema.d.ts +0 -27
- package/dist/commands/channel/store/schema.d.ts.map +0 -1
- package/dist/commands/channel/store/schema.js +0 -34
- package/dist/commands/channel/store/schema.js.map +0 -1
- package/dist/commands/channel/store/thread-state.d.ts.map +0 -1
- package/dist/commands/channel/store/thread-state.js.map +0 -1
- package/dist/commands/channel/store/watch.d.ts +0 -19
- package/dist/commands/channel/store/watch.d.ts.map +0 -1
- package/dist/commands/channel/store/watch.js.map +0 -1
- package/dist/commands/channel/supervisor/idle.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/idle.js.map +0 -1
- package/dist/commands/channel/supervisor/inbox.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/inbox.js.map +0 -1
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/shutdown.js.map +0 -1
- package/dist/commands/channel/supervisor/stdout.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/stdout.js.map +0 -1
- package/dist/commands/channel/supervisor/turns.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/turns.js.map +0 -1
- package/dist/commands/channel/supervisor/warning.d.ts.map +0 -1
- package/dist/commands/channel/supervisor/warning.js.map +0 -1
- package/dist/commands/channel/supervisor.d.ts.map +0 -1
- package/dist/commands/channel/supervisor.js.map +0 -1
- package/dist/commands/channel/text-body.d.ts +0 -13
- package/dist/commands/channel/text-body.d.ts.map +0 -1
- package/dist/commands/channel/text-body.js.map +0 -1
- package/dist/commands/channel/threads.d.ts +0 -39
- package/dist/commands/channel/threads.d.ts.map +0 -1
- package/dist/commands/channel/threads.js.map +0 -1
- package/dist/commands/channel/title.d.ts +0 -12
- package/dist/commands/channel/title.d.ts.map +0 -1
- package/dist/commands/channel/title.js +0 -24
- package/dist/commands/channel/title.js.map +0 -1
- package/dist/commands/channel/wait.d.ts.map +0 -1
- package/dist/commands/channel/wait.js.map +0 -1
- package/dist/commands/channel/workers.d.ts +0 -9
- package/dist/commands/channel/workers.d.ts.map +0 -1
- package/dist/commands/channel/workers.js +0 -31
- package/dist/commands/channel/workers.js.map +0 -1
- package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +0 -69
- package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +0 -549
- package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +0 -233
- package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +0 -237
- package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +0 -222
- package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +0 -295
- package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +0 -131
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +0 -70
- package/dist/templates/common/bundled-skills/trellis-research-record/SKILL.md +0 -14
- package/dist/templates/copilot/prompts/parallel.prompt.md +0 -204
- package/dist/templates/trellis/agents/check.md +0 -70
- package/dist/templates/trellis/agents/implement.md +0 -71
- package/dist/templates/trellis/agents/subnode.env +0 -4
- package/dist/templates/trellis/agents/subnode.md +0 -76
- package/dist/templates/trellis/scripts/common/context_projection.py +0 -508
- package/dist/templates/trellis/scripts/common/continuation_record.py +0 -450
- package/dist/templates/trellis/scripts/common/ownership_record.py +0 -439
- package/dist/templates/trellis/scripts/subnode_artifact.py +0 -735
- package/dist/templates/trellis/scripts/workspace_note.py +0 -126
- /package/dist/commands/{channel → teamwork}/adapters/types.js +0 -0
- /package/dist/commands/{channel → teamwork}/supervisor/idle.d.ts +0 -0
- /package/dist/commands/{channel → teamwork}/supervisor/idle.js +0 -0
- /package/dist/commands/{channel → teamwork}/supervisor/turns.d.ts +0 -0
- /package/dist/commands/{channel → teamwork}/supervisor/turns.js +0 -0
|
@@ -6,7 +6,6 @@ Provides:
|
|
|
6
6
|
ensure_tasks_dir - Ensure tasks directory exists
|
|
7
7
|
cmd_create - Create a new task
|
|
8
8
|
cmd_rename - Rename a task and every reference to it
|
|
9
|
-
cmd_archive - Archive completed task
|
|
10
9
|
cmd_set_branch - Set git branch for task
|
|
11
10
|
cmd_set_base_branch - Set PR target branch
|
|
12
11
|
cmd_set_scope - Set scope for PR title
|
|
@@ -27,26 +26,18 @@ from pathlib import Path
|
|
|
27
26
|
from .config import (
|
|
28
27
|
get_codex_dispatch_mode,
|
|
29
28
|
get_packages,
|
|
30
|
-
get_session_auto_commit,
|
|
31
29
|
is_monorepo,
|
|
32
30
|
resolve_package,
|
|
33
31
|
validate_package,
|
|
34
32
|
)
|
|
35
33
|
from .git import (
|
|
36
|
-
INDEX_LOCK_RETRY_ATTEMPTS,
|
|
37
|
-
branch_exists_locally,
|
|
38
|
-
has_git_remote,
|
|
39
|
-
index_lock_path,
|
|
40
34
|
resolve_default_branch,
|
|
41
35
|
run_git,
|
|
42
|
-
run_git_retry_index_lock,
|
|
43
|
-
stderr_indicates_index_lock,
|
|
44
36
|
)
|
|
45
37
|
from .io import describe_json_read_failure, read_json_checked, write_json
|
|
46
38
|
from .log import Colors, colored
|
|
47
39
|
from .paths import (
|
|
48
40
|
DEVELOPER_HINT,
|
|
49
|
-
DIR_ARCHIVE,
|
|
50
41
|
DIR_TASKS,
|
|
51
42
|
DIR_WORKFLOW,
|
|
52
43
|
FILE_TASK_JSON,
|
|
@@ -55,14 +46,7 @@ from .paths import (
|
|
|
55
46
|
get_repo_root,
|
|
56
47
|
get_tasks_dir,
|
|
57
48
|
)
|
|
58
|
-
from .safe_commit import (
|
|
59
|
-
print_gitignore_warning,
|
|
60
|
-
safe_archive_paths_to_add,
|
|
61
|
-
safe_git_add,
|
|
62
|
-
)
|
|
63
49
|
from .task_utils import (
|
|
64
|
-
archive_destination_for,
|
|
65
|
-
archive_task_complete,
|
|
66
50
|
find_task_by_name,
|
|
67
51
|
is_within_tasks_dir,
|
|
68
52
|
resolve_task_dir,
|
|
@@ -81,7 +65,7 @@ from .workflow_selection import DIR_WORKFLOWS, WORKFLOW_ID_RE
|
|
|
81
65
|
# U+0085 (NEL) and U+001C-U+001F, which JS keeps; JS strips U+FEFF (BOM),
|
|
82
66
|
# which Python keeps. Stripping the union means anything create accepts is
|
|
83
67
|
# still non-empty after either side trims it, so a record cannot pass here and
|
|
84
|
-
# then
|
|
68
|
+
# then fail equivalent validation in a JavaScript consumer.
|
|
85
69
|
BLANK_CHARS = (
|
|
86
70
|
"\t\n\v\f\r" # U+0009-U+000D
|
|
87
71
|
"\x1c\x1d\x1e\x1f" # U+001C-U+001F separators (Python only)
|
|
@@ -114,34 +98,14 @@ def _slugify(title: str) -> str:
|
|
|
114
98
|
def ensure_tasks_dir(repo_root: Path) -> Path:
|
|
115
99
|
"""Ensure tasks directory exists."""
|
|
116
100
|
tasks_dir = get_tasks_dir(repo_root)
|
|
117
|
-
archive_dir = tasks_dir / "archive"
|
|
118
101
|
|
|
119
102
|
if not tasks_dir.exists():
|
|
120
103
|
tasks_dir.mkdir(parents=True)
|
|
121
104
|
print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
|
|
122
105
|
|
|
123
|
-
if not archive_dir.exists():
|
|
124
|
-
archive_dir.mkdir(parents=True)
|
|
125
|
-
|
|
126
106
|
return tasks_dir
|
|
127
107
|
|
|
128
108
|
|
|
129
|
-
def _find_archived_task_by_dir_name(tasks_dir: Path, dir_name: str) -> Path | None:
|
|
130
|
-
"""Find an archived task directory with the exact active-task dir name."""
|
|
131
|
-
archive_dir = tasks_dir / DIR_ARCHIVE
|
|
132
|
-
if not archive_dir.is_dir():
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
for month_dir in sorted(archive_dir.iterdir()):
|
|
136
|
-
if not month_dir.is_dir():
|
|
137
|
-
continue
|
|
138
|
-
candidate = month_dir / dir_name
|
|
139
|
-
if candidate.is_dir():
|
|
140
|
-
return candidate
|
|
141
|
-
|
|
142
|
-
return None
|
|
143
|
-
|
|
144
|
-
|
|
145
109
|
def _repo_relative_path(path: Path, repo_root: Path) -> str:
|
|
146
110
|
"""Format a path relative to the repo root when possible."""
|
|
147
111
|
try:
|
|
@@ -197,41 +161,30 @@ def _report_write_failure(path: Path) -> None:
|
|
|
197
161
|
)
|
|
198
162
|
|
|
199
163
|
|
|
200
|
-
def
|
|
201
|
-
|
|
164
|
+
def _task_mutation_allowed(repo_root: Path, task_dir: Path, data: dict) -> bool:
|
|
165
|
+
from .task_continuity import assert_task_mutation_allowed
|
|
166
|
+
from .task_workflow import TaskWorkflowError
|
|
202
167
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
168
|
+
try:
|
|
169
|
+
assert_task_mutation_allowed(repo_root, task_dir, data)
|
|
170
|
+
except TaskWorkflowError as error:
|
|
171
|
+
print(colored(f"{error.code}: {error}", Colors.RED), file=sys.stderr)
|
|
172
|
+
return False
|
|
173
|
+
return True
|
|
209
174
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
broken.append(child_json.parent.name)
|
|
222
|
-
if broken:
|
|
223
|
-
print(
|
|
224
|
-
colored(
|
|
225
|
-
f"Warning: could not restore the parent link on: {', '.join(broken)}. "
|
|
226
|
-
"Re-link each one with `python3 .trellis/scripts/task.py "
|
|
227
|
-
"add-subtask <parent> <child>`.",
|
|
228
|
-
Colors.RED,
|
|
229
|
-
),
|
|
230
|
-
file=sys.stderr,
|
|
231
|
-
)
|
|
175
|
+
|
|
176
|
+
def _task_identity_change_allowed(repo_root: Path, task_dir: Path, data: dict) -> bool:
|
|
177
|
+
from .task_continuity import assert_task_identity_change_allowed
|
|
178
|
+
from .task_workflow import TaskWorkflowError
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
assert_task_identity_change_allowed(repo_root, task_dir, data)
|
|
182
|
+
except TaskWorkflowError as error:
|
|
183
|
+
print(colored(f"{error.code}: {error}", Colors.RED), file=sys.stderr)
|
|
184
|
+
return False
|
|
185
|
+
return True
|
|
232
186
|
|
|
233
187
|
|
|
234
|
-
# =============================================================================
|
|
235
188
|
# Sub-agent platform detection + JSONL context files
|
|
236
189
|
# =============================================================================
|
|
237
190
|
|
|
@@ -266,9 +219,9 @@ def _has_subagent_platform(repo_root: Path) -> bool:
|
|
|
266
219
|
"""Return True if any sub-agent-capable platform is configured.
|
|
267
220
|
|
|
268
221
|
Detected by probing well-known config directories at the repo root. Codex
|
|
269
|
-
counts
|
|
270
|
-
|
|
271
|
-
|
|
222
|
+
counts by default through ``codex.dispatch_mode: auto`` (including the
|
|
223
|
+
legacy ``sub-agent`` alias); explicit inline mode loads context through
|
|
224
|
+
skills, not JSONL.
|
|
272
225
|
"""
|
|
273
226
|
for config_dir in _SUBAGENT_CONFIG_DIRS:
|
|
274
227
|
if (repo_root / config_dir).is_dir():
|
|
@@ -334,13 +287,13 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
334
287
|
|
|
335
288
|
# Title and description are checked first, before ensure_tasks_dir and any
|
|
336
289
|
# other write, so a rejected create leaves the tree untouched. Both are
|
|
337
|
-
# required non-empty
|
|
338
|
-
#
|
|
290
|
+
# required non-empty; catching that here costs one retyped command instead
|
|
291
|
+
# of discovering the invalid task later.
|
|
339
292
|
if not strip_blank(args.title):
|
|
340
293
|
print(colored("Error: title is required", Colors.RED), file=sys.stderr)
|
|
341
294
|
print(
|
|
342
295
|
'Pass a non-empty <title> (whitespace only does not count): a task '
|
|
343
|
-
"with an empty title
|
|
296
|
+
"with an empty title cannot be completed.",
|
|
344
297
|
file=sys.stderr,
|
|
345
298
|
)
|
|
346
299
|
return 1
|
|
@@ -350,7 +303,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
350
303
|
print(colored("Error: --description is required", Colors.RED), file=sys.stderr)
|
|
351
304
|
print(
|
|
352
305
|
'Pass --description "<what this task delivers>" (whitespace only does '
|
|
353
|
-
"not count): a task with an empty description
|
|
306
|
+
"not count): a task with an empty description cannot be completed.",
|
|
354
307
|
file=sys.stderr,
|
|
355
308
|
)
|
|
356
309
|
return 1
|
|
@@ -486,22 +439,17 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
486
439
|
_report_read_failure(parent_json_path, parent_reason)
|
|
487
440
|
print("No task was created. Fix the parent task.json, then retry.", file=sys.stderr)
|
|
488
441
|
return 1
|
|
442
|
+
if not _task_mutation_allowed(repo_root, parent_dir, parent_data):
|
|
443
|
+
return 1
|
|
489
444
|
|
|
490
445
|
dir_name = f"{date_prefix}-{slug}"
|
|
491
446
|
task_dir = tasks_dir / dir_name
|
|
492
447
|
task_json_path = task_dir / FILE_TASK_JSON
|
|
493
448
|
|
|
494
|
-
archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, dir_name)
|
|
495
|
-
if archived_task_dir:
|
|
496
|
-
print(colored(f"Error: Task already archived: {dir_name}", Colors.RED), file=sys.stderr)
|
|
497
|
-
print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
|
|
498
|
-
print("Use a new slug if you intend to create a new task.", file=sys.stderr)
|
|
499
|
-
return 1
|
|
500
|
-
|
|
501
449
|
# Reusing a slug on the same day is an ordinary accident, and continuing
|
|
502
450
|
# would rewrite the existing task.json below — resetting status, children,
|
|
503
451
|
# parent, branch and meta, which nothing else can reconstruct. Fail like
|
|
504
|
-
#
|
|
452
|
+
# unless the caller asks for it.
|
|
505
453
|
if task_dir.exists():
|
|
506
454
|
if not getattr(args, "force", False):
|
|
507
455
|
print(colored(f"Error: Task already exists: {dir_name}", Colors.RED), file=sys.stderr)
|
|
@@ -518,6 +466,13 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
518
466
|
),
|
|
519
467
|
file=sys.stderr,
|
|
520
468
|
)
|
|
469
|
+
if task_json_path.is_file():
|
|
470
|
+
existing_data, existing_reason = read_json_checked(task_json_path)
|
|
471
|
+
if existing_data is None:
|
|
472
|
+
_report_read_failure(task_json_path, existing_reason)
|
|
473
|
+
return 1
|
|
474
|
+
if not _task_mutation_allowed(repo_root, task_dir, existing_data):
|
|
475
|
+
return 1
|
|
521
476
|
created_dir = False
|
|
522
477
|
else:
|
|
523
478
|
task_dir.mkdir(parents=True)
|
|
@@ -887,7 +842,7 @@ def _plan_backrefs(
|
|
|
887
842
|
unreadable: list[Path] = []
|
|
888
843
|
|
|
889
844
|
for candidate in sorted(tasks_dir.iterdir()):
|
|
890
|
-
if not candidate.is_dir()
|
|
845
|
+
if not candidate.is_dir():
|
|
891
846
|
continue
|
|
892
847
|
if candidate == task_dir:
|
|
893
848
|
continue
|
|
@@ -1017,6 +972,10 @@ def _apply_rename(plan: _RenamePlan, repo_root: Path) -> int:
|
|
|
1017
972
|
finishes; a move-first ordering would instead leave the old name
|
|
1018
973
|
unresolvable and the remaining edits to be made by hand.
|
|
1019
974
|
"""
|
|
975
|
+
if not _task_mutation_allowed(repo_root, plan.task_dir, plan.task_data):
|
|
976
|
+
return 1
|
|
977
|
+
if plan.identity and not _task_identity_change_allowed(repo_root, plan.task_dir, plan.task_data):
|
|
978
|
+
return 1
|
|
1020
979
|
if plan.identity:
|
|
1021
980
|
data = dict(plan.task_data)
|
|
1022
981
|
for field_name, _old_value, new_value in plan.identity:
|
|
@@ -1056,8 +1015,7 @@ def _apply_rename(plan: _RenamePlan, repo_root: Path) -> int:
|
|
|
1056
1015
|
# Last, and after the move: a session pointing at the old path would now
|
|
1057
1016
|
# resolve to a missing directory, so `current` would report the task stale
|
|
1058
1017
|
# and the context hook would inject nothing until the user ran `start`
|
|
1059
|
-
# again.
|
|
1060
|
-
# moves them, because the task is still the one being worked on.
|
|
1018
|
+
# again. Rename moves the pointers because the task is still being worked on.
|
|
1061
1019
|
from .active_task import repoint_task_in_sessions
|
|
1062
1020
|
|
|
1063
1021
|
repoint_task_in_sessions(str(plan.task_dir), str(plan.new_dir), repo_root)
|
|
@@ -1076,13 +1034,12 @@ def cmd_rename(args: argparse.Namespace) -> int:
|
|
|
1076
1034
|
print(colored(f"Error: Task not found: {args.name}", Colors.RED), file=sys.stderr)
|
|
1077
1035
|
return 1
|
|
1078
1036
|
|
|
1079
|
-
#
|
|
1080
|
-
# the active set, so its back-references are no longer maintained here.
|
|
1037
|
+
# Restrict renames to task records directly under the tasks directory.
|
|
1081
1038
|
if not is_within_tasks_dir(task_dir, repo_root):
|
|
1082
1039
|
print(
|
|
1083
1040
|
colored(
|
|
1084
1041
|
f"Error: refusing to rename '{args.name}': "
|
|
1085
|
-
f"{task_dir} is not
|
|
1042
|
+
f"{task_dir} is not a task directly under {tasks_dir}",
|
|
1086
1043
|
Colors.RED,
|
|
1087
1044
|
),
|
|
1088
1045
|
file=sys.stderr,
|
|
@@ -1114,16 +1071,6 @@ def cmd_rename(args: argparse.Namespace) -> int:
|
|
|
1114
1071
|
print("Pick a different slug.", file=sys.stderr)
|
|
1115
1072
|
return 1
|
|
1116
1073
|
|
|
1117
|
-
archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, new_name)
|
|
1118
|
-
if archived_task_dir:
|
|
1119
|
-
print(
|
|
1120
|
-
colored(f"Error: Task already archived: {new_name}", Colors.RED),
|
|
1121
|
-
file=sys.stderr,
|
|
1122
|
-
)
|
|
1123
|
-
print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
|
|
1124
|
-
print("Pick a different slug.", file=sys.stderr)
|
|
1125
|
-
return 1
|
|
1126
|
-
|
|
1127
1074
|
task_json_path = task_dir / FILE_TASK_JSON
|
|
1128
1075
|
if not task_json_path.is_file():
|
|
1129
1076
|
print(
|
|
@@ -1194,455 +1141,6 @@ def cmd_rename(args: argparse.Namespace) -> int:
|
|
|
1194
1141
|
)
|
|
1195
1142
|
return 0
|
|
1196
1143
|
|
|
1197
|
-
|
|
1198
|
-
# =============================================================================
|
|
1199
|
-
# Command: archive
|
|
1200
|
-
# =============================================================================
|
|
1201
|
-
|
|
1202
|
-
def _task_branch_field(data: dict, key: str) -> str:
|
|
1203
|
-
"""Read a branch field as a trimmed string ("" when unset or not a string)."""
|
|
1204
|
-
value = data.get(key)
|
|
1205
|
-
return strip_blank(value) if isinstance(value, str) else ""
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
def _validate_branch_metadata(
|
|
1209
|
-
data: dict,
|
|
1210
|
-
task_name: str,
|
|
1211
|
-
repo_root: Path,
|
|
1212
|
-
skip: bool,
|
|
1213
|
-
) -> bool:
|
|
1214
|
-
"""Check branch metadata before the task leaves the active tree.
|
|
1215
|
-
|
|
1216
|
-
Returns False when archiving must stop. Archive is the last gate that sees
|
|
1217
|
-
a task, so metadata nobody can reconstruct afterwards is refused here
|
|
1218
|
-
rather than repaired by hand later (#399 follow-up).
|
|
1219
|
-
|
|
1220
|
-
"PR-backed" is deliberately pragmatic: a task carrying a base_branch in a
|
|
1221
|
-
repo that has a remote was created expecting a PR, so a missing `branch`
|
|
1222
|
-
means the metadata was never recorded — not that the work had no branch.
|
|
1223
|
-
Local-only repos and tasks without a base_branch are left alone.
|
|
1224
|
-
|
|
1225
|
-
A recorded branch that no longer exists locally stays a warning: after a
|
|
1226
|
-
merge the feature branch is normally deleted, and refusing to archive then
|
|
1227
|
-
would be backwards.
|
|
1228
|
-
"""
|
|
1229
|
-
branch = _task_branch_field(data, "branch")
|
|
1230
|
-
base_branch = _task_branch_field(data, "base_branch")
|
|
1231
|
-
task_py = f"python3 {DIR_WORKFLOW}/scripts/task.py"
|
|
1232
|
-
|
|
1233
|
-
if branch and not branch_exists_locally(branch, repo_root):
|
|
1234
|
-
print(
|
|
1235
|
-
colored(
|
|
1236
|
-
f"Warning: recorded branch '{branch}' no longer exists locally "
|
|
1237
|
-
"(likely merged and deleted).",
|
|
1238
|
-
Colors.YELLOW,
|
|
1239
|
-
),
|
|
1240
|
-
file=sys.stderr,
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
if skip:
|
|
1244
|
-
return True
|
|
1245
|
-
|
|
1246
|
-
if branch and base_branch and branch == base_branch:
|
|
1247
|
-
print(
|
|
1248
|
-
colored(
|
|
1249
|
-
f"Error: refusing to archive '{task_name}': branch and base_branch "
|
|
1250
|
-
f"are both '{branch}'. A PR cannot target its own branch, so this "
|
|
1251
|
-
"metadata cannot describe the work that was merged.",
|
|
1252
|
-
Colors.RED,
|
|
1253
|
-
),
|
|
1254
|
-
file=sys.stderr,
|
|
1255
|
-
)
|
|
1256
|
-
print("Repair whichever field is wrong:", file=sys.stderr)
|
|
1257
|
-
print(f" {task_py} set-branch {task_name} <feature-branch>", file=sys.stderr)
|
|
1258
|
-
print(f" {task_py} set-base-branch {task_name} <target-branch>", file=sys.stderr)
|
|
1259
|
-
print(
|
|
1260
|
-
f" {task_py} archive {task_name} --skip-branch-validation"
|
|
1261
|
-
" # only if this task was never PR-backed",
|
|
1262
|
-
file=sys.stderr,
|
|
1263
|
-
)
|
|
1264
|
-
return False
|
|
1265
|
-
|
|
1266
|
-
if not branch and base_branch and has_git_remote(repo_root):
|
|
1267
|
-
print(
|
|
1268
|
-
colored(
|
|
1269
|
-
f"Error: refusing to archive '{task_name}': no branch is recorded, "
|
|
1270
|
-
f"but the task targets base_branch '{base_branch}' in a repo with a "
|
|
1271
|
-
"remote — the branch it was built on was never written down.",
|
|
1272
|
-
Colors.RED,
|
|
1273
|
-
),
|
|
1274
|
-
file=sys.stderr,
|
|
1275
|
-
)
|
|
1276
|
-
print("Repair with:", file=sys.stderr)
|
|
1277
|
-
print(f" {task_py} set-branch {task_name} <branch>", file=sys.stderr)
|
|
1278
|
-
print(
|
|
1279
|
-
f" {task_py} archive {task_name} --skip-branch-validation"
|
|
1280
|
-
" # only if the work landed without a branch of its own",
|
|
1281
|
-
file=sys.stderr,
|
|
1282
|
-
)
|
|
1283
|
-
return False
|
|
1284
|
-
|
|
1285
|
-
return True
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
def cmd_archive(args: argparse.Namespace) -> int:
|
|
1289
|
-
"""Archive completed task."""
|
|
1290
|
-
repo_root = get_repo_root()
|
|
1291
|
-
task_name = args.name
|
|
1292
|
-
|
|
1293
|
-
if not task_name:
|
|
1294
|
-
print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
|
|
1295
|
-
return 1
|
|
1296
|
-
|
|
1297
|
-
tasks_dir = get_tasks_dir(repo_root)
|
|
1298
|
-
|
|
1299
|
-
# Resolve task directory (supports task name, relative path, or absolute path)
|
|
1300
|
-
task_dir = resolve_task_dir(task_name, repo_root)
|
|
1301
|
-
|
|
1302
|
-
if task_dir is None or not task_dir.is_dir():
|
|
1303
|
-
if task_dir is None:
|
|
1304
|
-
# resolve_task_dir already reported why; keep the archive-specific
|
|
1305
|
-
# refusal so it reads the same as the is_within_tasks_dir guard.
|
|
1306
|
-
print(colored(
|
|
1307
|
-
f"Error: refusing to archive '{task_name}': "
|
|
1308
|
-
f"it does not resolve to a task under {tasks_dir}",
|
|
1309
|
-
Colors.RED), file=sys.stderr)
|
|
1310
|
-
else:
|
|
1311
|
-
print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
|
|
1312
|
-
print("Active tasks:", file=sys.stderr)
|
|
1313
|
-
# Import lazily to avoid circular dependency
|
|
1314
|
-
from .tasks import iter_active_tasks
|
|
1315
|
-
for t in iter_active_tasks(tasks_dir):
|
|
1316
|
-
print(f" - {t.dir_name}/", file=sys.stderr)
|
|
1317
|
-
return 1
|
|
1318
|
-
|
|
1319
|
-
# Refuse to archive anything that isn't a real task directly under
|
|
1320
|
-
# .trellis/tasks/. resolve_task_dir keeps the target inside the tasks dir;
|
|
1321
|
-
# this narrows it further to a direct child, so an already-archived task
|
|
1322
|
-
# (.trellis/tasks/archive/<month>/<task>) is not archived a second time.
|
|
1323
|
-
if not is_within_tasks_dir(task_dir, repo_root):
|
|
1324
|
-
print(colored(
|
|
1325
|
-
f"Error: refusing to archive '{task_name}': "
|
|
1326
|
-
f"{task_dir} is not a task under {tasks_dir}",
|
|
1327
|
-
Colors.RED), file=sys.stderr)
|
|
1328
|
-
return 1
|
|
1329
|
-
|
|
1330
|
-
# Check the destination before anything below mutates task state. The
|
|
1331
|
-
# mover refuses a collision too, but by then this command has already
|
|
1332
|
-
# marked the task completed, re-parented its children and cleared the
|
|
1333
|
-
# sessions pointing at it — all of which would have to be undone by hand.
|
|
1334
|
-
archive_dest_check = archive_destination_for(task_dir)
|
|
1335
|
-
if archive_dest_check.exists():
|
|
1336
|
-
print(colored(
|
|
1337
|
-
f"Error: refusing to archive '{task_name}': "
|
|
1338
|
-
f"archive destination already exists: "
|
|
1339
|
-
f"{_repo_relative_path(archive_dest_check, repo_root)}",
|
|
1340
|
-
Colors.RED), file=sys.stderr)
|
|
1341
|
-
print(f"Task remains at: {_repo_relative_path(task_dir, repo_root)}", file=sys.stderr)
|
|
1342
|
-
print("Move or rename the existing archived task, then retry.", file=sys.stderr)
|
|
1343
|
-
return 1
|
|
1344
|
-
|
|
1345
|
-
dir_name = task_dir.name
|
|
1346
|
-
task_json_path = task_dir / FILE_TASK_JSON
|
|
1347
|
-
|
|
1348
|
-
# Update status before archiving
|
|
1349
|
-
today = datetime.now().strftime("%Y-%m-%d")
|
|
1350
|
-
# Names of child task dirs whose task.json gets modified below; passed
|
|
1351
|
-
# into safe_archive_paths_to_add so they're staged in this commit.
|
|
1352
|
-
modified_children: list[str] = []
|
|
1353
|
-
if task_json_path.is_file():
|
|
1354
|
-
data, read_reason = read_json_checked(task_json_path)
|
|
1355
|
-
if data is None:
|
|
1356
|
-
# Archiving is still the right outcome for a task whose task.json
|
|
1357
|
-
# is broken — but say so, or the missing "completed" status looks
|
|
1358
|
-
# like the archive silently did half its job.
|
|
1359
|
-
problem, _ = describe_json_read_failure(task_json_path, read_reason)
|
|
1360
|
-
print(
|
|
1361
|
-
colored(
|
|
1362
|
-
f"Warning: {problem}; archiving without updating status/children.",
|
|
1363
|
-
Colors.YELLOW,
|
|
1364
|
-
),
|
|
1365
|
-
file=sys.stderr,
|
|
1366
|
-
)
|
|
1367
|
-
else:
|
|
1368
|
-
# Before any mutation: branch metadata is unrecoverable once the
|
|
1369
|
-
# task leaves the active tree. Stale branches only warn.
|
|
1370
|
-
if not _validate_branch_metadata(
|
|
1371
|
-
data,
|
|
1372
|
-
task_name,
|
|
1373
|
-
repo_root,
|
|
1374
|
-
getattr(args, "skip_branch_validation", False),
|
|
1375
|
-
):
|
|
1376
|
-
print(
|
|
1377
|
-
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged.",
|
|
1378
|
-
file=sys.stderr,
|
|
1379
|
-
)
|
|
1380
|
-
return 1
|
|
1381
|
-
|
|
1382
|
-
data["status"] = "completed"
|
|
1383
|
-
data["completedAt"] = today
|
|
1384
|
-
if not write_json(task_json_path, data):
|
|
1385
|
-
_report_write_failure(task_json_path)
|
|
1386
|
-
print(
|
|
1387
|
-
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged. "
|
|
1388
|
-
"Archiving a task still marked in progress would hide it from `list` "
|
|
1389
|
-
"with the wrong status.",
|
|
1390
|
-
file=sys.stderr,
|
|
1391
|
-
)
|
|
1392
|
-
return 1
|
|
1393
|
-
|
|
1394
|
-
# Handle subtask relationships on archive.
|
|
1395
|
-
# Keep this task in its parent's children list so progress
|
|
1396
|
-
# counters (children_progress) stay consistent — children
|
|
1397
|
-
# missing from the active set are treated as completed.
|
|
1398
|
-
task_children = data.get("children", [])
|
|
1399
|
-
|
|
1400
|
-
# If this is a parent, clear parent field in all children.
|
|
1401
|
-
# Remember each link removed so a later failure in this loop can
|
|
1402
|
-
# put it back (see _restore_child_links). Keyed by the child's
|
|
1403
|
-
# task.json: a `children` list that names the same child twice
|
|
1404
|
-
# would otherwise record a second, already-cleared snapshot and
|
|
1405
|
-
# the restore would write that `null` back over the real parent.
|
|
1406
|
-
unlinked_children: dict[Path, str | None] = {}
|
|
1407
|
-
if task_children:
|
|
1408
|
-
for child_name in task_children:
|
|
1409
|
-
child_dir_path = find_task_by_name(child_name, tasks_dir)
|
|
1410
|
-
if child_dir_path:
|
|
1411
|
-
child_json = child_dir_path / FILE_TASK_JSON
|
|
1412
|
-
if child_json.is_file():
|
|
1413
|
-
child_data, child_reason = read_json_checked(child_json)
|
|
1414
|
-
if child_data is None:
|
|
1415
|
-
problem, _ = describe_json_read_failure(child_json, child_reason)
|
|
1416
|
-
print(
|
|
1417
|
-
colored(
|
|
1418
|
-
f"Warning: {problem}; child '{child_dir_path.name}' "
|
|
1419
|
-
"keeps its parent reference.",
|
|
1420
|
-
Colors.YELLOW,
|
|
1421
|
-
),
|
|
1422
|
-
file=sys.stderr,
|
|
1423
|
-
)
|
|
1424
|
-
continue
|
|
1425
|
-
# Only the first visit to a child records its original
|
|
1426
|
-
# parent: a `children` list naming the same child twice
|
|
1427
|
-
# would otherwise snapshot the already-cleared value and
|
|
1428
|
-
# the restore would write that `null` back over the link.
|
|
1429
|
-
first_visit = child_json not in unlinked_children
|
|
1430
|
-
original_parent = child_data.get("parent")
|
|
1431
|
-
if first_visit:
|
|
1432
|
-
unlinked_children[child_json] = original_parent
|
|
1433
|
-
child_data["parent"] = None
|
|
1434
|
-
if not write_json(child_json, child_data):
|
|
1435
|
-
# Stop before the move: a child pointing at a
|
|
1436
|
-
# parent that has left .trellis/tasks/ is a
|
|
1437
|
-
# dangling reference nothing repairs later.
|
|
1438
|
-
# Put back the children already unlinked above —
|
|
1439
|
-
# their parent is staying, so losing the link the
|
|
1440
|
-
# other way round is just as unrecoverable.
|
|
1441
|
-
# Retrying is safe — every step so far is
|
|
1442
|
-
# idempotent.
|
|
1443
|
-
if first_visit:
|
|
1444
|
-
# The link never came off, so there is nothing
|
|
1445
|
-
# to put back for this child.
|
|
1446
|
-
del unlinked_children[child_json]
|
|
1447
|
-
_restore_child_links(unlinked_children)
|
|
1448
|
-
_report_write_failure(child_json)
|
|
1449
|
-
print(
|
|
1450
|
-
f"Not archived: {_repo_relative_path(task_dir, repo_root)} is "
|
|
1451
|
-
f"marked completed but stays in place because child "
|
|
1452
|
-
f"'{child_dir_path.name}' could not be unlinked. "
|
|
1453
|
-
"Fix the child, then run archive again.",
|
|
1454
|
-
file=sys.stderr,
|
|
1455
|
-
)
|
|
1456
|
-
return 1
|
|
1457
|
-
modified_children.append(child_dir_path.name)
|
|
1458
|
-
|
|
1459
|
-
# Clear any session that still points at this task before the path moves.
|
|
1460
|
-
from .active_task import clear_task_from_sessions
|
|
1461
|
-
clear_task_from_sessions(str(task_dir), repo_root)
|
|
1462
|
-
|
|
1463
|
-
# Archive
|
|
1464
|
-
result = archive_task_complete(task_dir, repo_root)
|
|
1465
|
-
if "archived_to" in result:
|
|
1466
|
-
archive_dest = Path(result["archived_to"])
|
|
1467
|
-
year_month = archive_dest.parent.name
|
|
1468
|
-
print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
|
|
1469
|
-
|
|
1470
|
-
# Auto-commit unless --no-commit
|
|
1471
|
-
if not getattr(args, "no_commit", False):
|
|
1472
|
-
if not _auto_commit_archive(dir_name, repo_root, modified_children):
|
|
1473
|
-
print(
|
|
1474
|
-
colored(
|
|
1475
|
-
"Archive moved on disk, but git auto-commit did not complete. "
|
|
1476
|
-
"Resolve `git status` before continuing.",
|
|
1477
|
-
Colors.RED,
|
|
1478
|
-
),
|
|
1479
|
-
file=sys.stderr,
|
|
1480
|
-
)
|
|
1481
|
-
return 1
|
|
1482
|
-
|
|
1483
|
-
# Return the archive path
|
|
1484
|
-
print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
|
|
1485
|
-
|
|
1486
|
-
# Run hooks with the archived path
|
|
1487
|
-
archived_json = archive_dest / FILE_TASK_JSON
|
|
1488
|
-
run_task_hooks("after_archive", archived_json, repo_root)
|
|
1489
|
-
return 0
|
|
1490
|
-
|
|
1491
|
-
return 1
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
def _auto_commit_archive(
|
|
1495
|
-
task_name: str,
|
|
1496
|
-
repo_root: Path,
|
|
1497
|
-
modified_children: list[str] | None = None,
|
|
1498
|
-
) -> bool:
|
|
1499
|
-
"""Stage Trellis-owned task paths and commit after archive.
|
|
1500
|
-
|
|
1501
|
-
Scoped narrowly to the archived task's source + destination paths
|
|
1502
|
-
plus any child task dirs whose ``task.json`` was edited (parent →
|
|
1503
|
-
children relationship update). Dirty changes in OTHER active task
|
|
1504
|
-
dirs are NOT bundled into the archive commit.
|
|
1505
|
-
|
|
1506
|
-
If ``.gitignore`` blocks the paths, we warn + skip — we do NOT
|
|
1507
|
-
retry with ``git add -f``. The warning explicitly forbids
|
|
1508
|
-
``git add -f .trellis/`` (which would fan out to caches/backups)
|
|
1509
|
-
and points users at ``session_auto_commit: false``.
|
|
1510
|
-
|
|
1511
|
-
Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when
|
|
1512
|
-
set to ``false``, this function returns immediately without
|
|
1513
|
-
touching git (the archive directory move on disk is unaffected).
|
|
1514
|
-
"""
|
|
1515
|
-
if not get_session_auto_commit(repo_root):
|
|
1516
|
-
print(
|
|
1517
|
-
"[OK] session_auto_commit: false — skipping git stage/commit.",
|
|
1518
|
-
file=sys.stderr,
|
|
1519
|
-
)
|
|
1520
|
-
return True
|
|
1521
|
-
|
|
1522
|
-
source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
|
|
1523
|
-
rc, tracked_out, _ = run_git(
|
|
1524
|
-
["ls-files", "--", source_rel],
|
|
1525
|
-
cwd=repo_root,
|
|
1526
|
-
)
|
|
1527
|
-
source_was_tracked = rc == 0 and bool(tracked_out.strip())
|
|
1528
|
-
|
|
1529
|
-
paths = safe_archive_paths_to_add(
|
|
1530
|
-
repo_root, task_name=task_name, modified_children=modified_children
|
|
1531
|
-
)
|
|
1532
|
-
if not paths:
|
|
1533
|
-
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
1534
|
-
return True
|
|
1535
|
-
|
|
1536
|
-
success, _, err = safe_git_add(paths, repo_root, retry_on_index_lock=True)
|
|
1537
|
-
if not success:
|
|
1538
|
-
if err and "ignored by" in err.lower():
|
|
1539
|
-
print_gitignore_warning(paths)
|
|
1540
|
-
elif stderr_indicates_index_lock(err):
|
|
1541
|
-
_print_index_lock_warning(
|
|
1542
|
-
"git add", task_name, repo_root, [*paths, source_rel]
|
|
1543
|
-
)
|
|
1544
|
-
else:
|
|
1545
|
-
print(
|
|
1546
|
-
f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
|
|
1547
|
-
file=sys.stderr,
|
|
1548
|
-
)
|
|
1549
|
-
return not source_was_tracked
|
|
1550
|
-
|
|
1551
|
-
# Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses
|
|
1552
|
-
# `git add` (no -A) which only stages additions/modifications. The
|
|
1553
|
-
# source task directory was moved away by `shutil.move`, so its files
|
|
1554
|
-
# need an explicit `git rm --cached` to stage the deletions in this
|
|
1555
|
-
# same commit — otherwise they sit as uncommitted "phantom deletes"
|
|
1556
|
-
# against HEAD until something later picks them up.
|
|
1557
|
-
#
|
|
1558
|
-
# `--ignore-unmatch` makes this a no-op when the task was never tracked
|
|
1559
|
-
# (e.g. archiving a task that lived only in working tree).
|
|
1560
|
-
rc, _, err = run_git_retry_index_lock(
|
|
1561
|
-
["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel],
|
|
1562
|
-
cwd=repo_root,
|
|
1563
|
-
)
|
|
1564
|
-
if rc != 0 and stderr_indicates_index_lock(err):
|
|
1565
|
-
# Committing now would record the archived copy without the
|
|
1566
|
-
# source-side deletes — a half-archived tree in history.
|
|
1567
|
-
_print_index_lock_warning(
|
|
1568
|
-
"git rm --cached", task_name, repo_root, [*paths, source_rel]
|
|
1569
|
-
)
|
|
1570
|
-
return not source_was_tracked
|
|
1571
|
-
|
|
1572
|
-
commit_paths = [*paths, *([source_rel] if source_was_tracked else [])]
|
|
1573
|
-
rc, _, _ = run_git(
|
|
1574
|
-
["diff", "--cached", "--quiet", "--", *commit_paths],
|
|
1575
|
-
cwd=repo_root,
|
|
1576
|
-
)
|
|
1577
|
-
if rc == 0:
|
|
1578
|
-
print("[OK] No task changes to commit.", file=sys.stderr)
|
|
1579
|
-
return True
|
|
1580
|
-
|
|
1581
|
-
commit_msg = f"chore(task): archive {task_name}"
|
|
1582
|
-
# Commit with an explicit pathspec: a bare `git commit` would sweep any
|
|
1583
|
-
# unrelated entries the developer had staged before archiving into the
|
|
1584
|
-
# chore commit (#579). Include `source_rel` only when it was tracked, so
|
|
1585
|
-
# the source-side deletions land in that commit without passing an absent,
|
|
1586
|
-
# never-tracked path to git.
|
|
1587
|
-
rc, _, err = run_git_retry_index_lock(
|
|
1588
|
-
["commit", "-m", commit_msg, "--", *commit_paths], cwd=repo_root
|
|
1589
|
-
)
|
|
1590
|
-
if rc == 0:
|
|
1591
|
-
print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
|
|
1592
|
-
return True
|
|
1593
|
-
elif stderr_indicates_index_lock(err):
|
|
1594
|
-
_print_index_lock_warning(
|
|
1595
|
-
"git commit", task_name, repo_root, [*paths, source_rel]
|
|
1596
|
-
)
|
|
1597
|
-
return not source_was_tracked
|
|
1598
|
-
else:
|
|
1599
|
-
print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
|
|
1600
|
-
return not source_was_tracked
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
def _print_index_lock_warning(
|
|
1604
|
-
action: str, task_name: str, repo_root: Path, paths: list[str]
|
|
1605
|
-
) -> None:
|
|
1606
|
-
"""Report an archive auto-commit that gave up on a held index.lock.
|
|
1607
|
-
|
|
1608
|
-
The move itself already succeeded, so the state is consistent — the task
|
|
1609
|
-
lives in archive/ and its changes are staged-or-not but never half of
|
|
1610
|
-
both in a commit. Only the commit is outstanding, which the user (or an
|
|
1611
|
-
agent reading the log) has to finish by hand.
|
|
1612
|
-
|
|
1613
|
-
``paths`` are the paths the auto-commit would have staged, so the manual
|
|
1614
|
-
command keeps the same narrow scope — a blanket ``git add -A -- .trellis/``
|
|
1615
|
-
would sweep dirty changes from other active tasks into the archive commit.
|
|
1616
|
-
"""
|
|
1617
|
-
lock = index_lock_path(repo_root)
|
|
1618
|
-
print(
|
|
1619
|
-
f"[WARN] {action} gave up after {INDEX_LOCK_RETRY_ATTEMPTS} attempts: "
|
|
1620
|
-
f"another process is holding {lock}",
|
|
1621
|
-
file=sys.stderr,
|
|
1622
|
-
)
|
|
1623
|
-
print(
|
|
1624
|
-
"[WARN] The task was moved into archive/ on disk; only the commit is pending.",
|
|
1625
|
-
file=sys.stderr,
|
|
1626
|
-
)
|
|
1627
|
-
print(
|
|
1628
|
-
"[WARN] Close whatever holds the lock (an IDE git integration, a status",
|
|
1629
|
-
file=sys.stderr,
|
|
1630
|
-
)
|
|
1631
|
-
print(
|
|
1632
|
-
f"[WARN] daemon, another session), or delete {lock} if it is stale, then",
|
|
1633
|
-
file=sys.stderr,
|
|
1634
|
-
)
|
|
1635
|
-
print(
|
|
1636
|
-
f'[WARN] commit manually: git add -A -- {" ".join(paths)} && '
|
|
1637
|
-
f'git commit -m "chore(task): archive {task_name}"',
|
|
1638
|
-
file=sys.stderr,
|
|
1639
|
-
)
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
# =============================================================================
|
|
1643
|
-
# Command: add-subtask
|
|
1644
|
-
# =============================================================================
|
|
1645
|
-
|
|
1646
1144
|
def cmd_add_subtask(args: argparse.Namespace) -> int:
|
|
1647
1145
|
"""Link a child task to a parent task."""
|
|
1648
1146
|
repo_root = get_repo_root()
|
|
@@ -1679,6 +1177,8 @@ def cmd_add_subtask(args: argparse.Namespace) -> int:
|
|
|
1679
1177
|
if child_data is None:
|
|
1680
1178
|
_report_read_failure(child_json_path, child_reason)
|
|
1681
1179
|
return 1
|
|
1180
|
+
if not _task_mutation_allowed(repo_root, parent_dir, parent_data) or not _task_mutation_allowed(repo_root, child_dir, child_data):
|
|
1181
|
+
return 1
|
|
1682
1182
|
|
|
1683
1183
|
# Check if child already has a parent
|
|
1684
1184
|
existing_parent = child_data.get("parent")
|
|
@@ -1756,6 +1256,20 @@ def cmd_remove_subtask(args: argparse.Namespace) -> int:
|
|
|
1756
1256
|
if child_data is None:
|
|
1757
1257
|
_report_read_failure(child_json_path, child_reason)
|
|
1758
1258
|
return 1
|
|
1259
|
+
if not _task_mutation_allowed(repo_root, parent_dir, parent_data) or not _task_mutation_allowed(repo_root, child_dir, child_data):
|
|
1260
|
+
return 1
|
|
1261
|
+
if parent_data.get("status") == "completed":
|
|
1262
|
+
print("Error: A completed parent cannot unlink a child after closeout acceptance.", file=sys.stderr)
|
|
1263
|
+
return 1
|
|
1264
|
+
if parent_data.get("status") == "cancelled":
|
|
1265
|
+
workflow = parent_data.get("meta", {}).get("task_workflow", {})
|
|
1266
|
+
phases = workflow.get("phases", {}) if isinstance(workflow, dict) else {}
|
|
1267
|
+
closeout = phases.get("delivery_review_and_closeout", {}) if isinstance(phases, dict) else {}
|
|
1268
|
+
acceptance = closeout.get("acceptance", {}) if isinstance(closeout, dict) else {}
|
|
1269
|
+
choices = acceptance.get("child_dispositions", {}) if isinstance(acceptance, dict) else {}
|
|
1270
|
+
if child_data.get("parent") != parent_dir.name or not isinstance(choices, dict) or choices.get(child_dir.name) != "independent":
|
|
1271
|
+
print("Error: A cancelled parent can unlink only a child recorded as independently continuing in its accepted closeout.", file=sys.stderr)
|
|
1272
|
+
return 1
|
|
1759
1273
|
|
|
1760
1274
|
# Remove child from parent's children list
|
|
1761
1275
|
parent_children = _ensure_children_list(parent_data)
|
|
@@ -1818,6 +1332,8 @@ def cmd_set_branch(args: argparse.Namespace) -> int:
|
|
|
1818
1332
|
if data is None:
|
|
1819
1333
|
_report_read_failure(task_json, reason)
|
|
1820
1334
|
return 1
|
|
1335
|
+
if not _task_mutation_allowed(repo_root, target_dir, data):
|
|
1336
|
+
return 1
|
|
1821
1337
|
|
|
1822
1338
|
data["branch"] = branch
|
|
1823
1339
|
if not write_json(task_json, data):
|
|
@@ -1863,6 +1379,8 @@ def cmd_set_base_branch(args: argparse.Namespace) -> int:
|
|
|
1863
1379
|
if data is None:
|
|
1864
1380
|
_report_read_failure(task_json, reason)
|
|
1865
1381
|
return 1
|
|
1382
|
+
if not _task_mutation_allowed(repo_root, target_dir, data):
|
|
1383
|
+
return 1
|
|
1866
1384
|
|
|
1867
1385
|
data["base_branch"] = base_branch
|
|
1868
1386
|
if not write_json(task_json, data):
|
|
@@ -1906,6 +1424,8 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
|
|
|
1906
1424
|
if data is None:
|
|
1907
1425
|
_report_read_failure(task_json, reason)
|
|
1908
1426
|
return 1
|
|
1427
|
+
if not _task_mutation_allowed(repo_root, target_dir, data):
|
|
1428
|
+
return 1
|
|
1909
1429
|
|
|
1910
1430
|
data["scope"] = scope
|
|
1911
1431
|
if not write_json(task_json, data):
|
|
@@ -1949,6 +1469,8 @@ def cmd_set_meta(args: argparse.Namespace) -> int:
|
|
|
1949
1469
|
if data is None:
|
|
1950
1470
|
_report_read_failure(task_json, reason)
|
|
1951
1471
|
return 1
|
|
1472
|
+
if not _task_mutation_allowed(repo_root, target_dir, data):
|
|
1473
|
+
return 1
|
|
1952
1474
|
|
|
1953
1475
|
meta = data.get("meta")
|
|
1954
1476
|
if not isinstance(meta, dict):
|