@pennixrv/trellis 0.7.0-beta.8 → 0.7.1
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.0-beta.9.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 -191
- package/dist/templates/copilot/hooks/session-start.py +30 -26
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +7 -150
- 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 +563 -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 +114 -377
- package/dist/templates/trellis/workflow.md +111 -699
- 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 -193
- 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 -69
- 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 -597
- 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
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Current-task continuity snapshots and explicit session ownership transfer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import contextlib
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .active_task import (
|
|
14
|
+
compare_and_set_active_task,
|
|
15
|
+
clear_active_task_if_matches,
|
|
16
|
+
resolve_active_task,
|
|
17
|
+
)
|
|
18
|
+
from .git import run_git
|
|
19
|
+
from .io import JSON_READ_MISSING, read_json_checked, write_json
|
|
20
|
+
from .paths import FILE_TASK_JSON, get_repo_root
|
|
21
|
+
from .task_utils import is_within_tasks_dir, resolve_task_dir
|
|
22
|
+
from .task_workflow import TaskWorkflowError, _artifact_digest, _state
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
PACKAGE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
26
|
+
PACKAGE_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ContinuityError(Exception):
|
|
30
|
+
def __init__(self, code: str, message: str):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.code = code
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _key(task_path: str) -> str:
|
|
36
|
+
return hashlib.sha256(task_path.encode("utf-8")).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _continuity_path(root: Path, task_path: str) -> Path:
|
|
40
|
+
return root / ".trellis" / ".runtime" / "continuity" / f"{_key(task_path)}.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ownership_path(root: Path, task_path: str) -> Path:
|
|
44
|
+
return root / ".trellis" / ".runtime" / "task-ownership" / f"{_key(task_path)}.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextlib.contextmanager
|
|
48
|
+
def _record_lock(path: Path):
|
|
49
|
+
lock_path = path.with_suffix(".lock")
|
|
50
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with lock_path.open("a+b") as handle:
|
|
52
|
+
if os.name == "nt":
|
|
53
|
+
import msvcrt
|
|
54
|
+
|
|
55
|
+
handle.seek(0)
|
|
56
|
+
if handle.read(1) == b"":
|
|
57
|
+
handle.write(b"\0")
|
|
58
|
+
handle.flush()
|
|
59
|
+
handle.seek(0)
|
|
60
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
61
|
+
try:
|
|
62
|
+
yield
|
|
63
|
+
finally:
|
|
64
|
+
handle.seek(0)
|
|
65
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
66
|
+
else:
|
|
67
|
+
import fcntl
|
|
68
|
+
|
|
69
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
70
|
+
try:
|
|
71
|
+
yield
|
|
72
|
+
finally:
|
|
73
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _task(root: Path, task_path: str | None = None) -> tuple[Path, dict, str, str]:
|
|
77
|
+
active = resolve_active_task(root)
|
|
78
|
+
if active.stale or active.source_type == "session-fallback":
|
|
79
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "current task is stale or not directly bound to this session")
|
|
80
|
+
if task_path is None:
|
|
81
|
+
if not active.task_path:
|
|
82
|
+
raise ContinuityError("TASK_CONTINUITY_ABSENT", "this direct session has no current task")
|
|
83
|
+
task_path = active.task_path
|
|
84
|
+
elif active.task_path != task_path:
|
|
85
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "requested task is not the direct current task")
|
|
86
|
+
directory = resolve_task_dir(task_path, root)
|
|
87
|
+
if directory is None or not is_within_tasks_dir(directory, root):
|
|
88
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task path is unavailable or unsafe")
|
|
89
|
+
data, reason = read_json_checked(directory / FILE_TASK_JSON)
|
|
90
|
+
if data is None:
|
|
91
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"task record cannot be read: {reason}")
|
|
92
|
+
task_id = data.get("id") or data.get("name")
|
|
93
|
+
if not isinstance(task_id, str) or not task_id.strip():
|
|
94
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task identity is missing")
|
|
95
|
+
if not active.context_key:
|
|
96
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "direct session identity is unavailable")
|
|
97
|
+
return directory, data, task_id, active.context_key
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _projection(root: Path, directory: Path, data: dict, task_id: str) -> dict:
|
|
101
|
+
meta = data.get("meta")
|
|
102
|
+
if not isinstance(meta, dict):
|
|
103
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task metadata is invalid")
|
|
104
|
+
workflow = meta.get("task_workflow")
|
|
105
|
+
phases: list[dict] = []
|
|
106
|
+
evidence: list[str] = []
|
|
107
|
+
route_revision = input_revision = current_phase = None
|
|
108
|
+
if workflow is not None:
|
|
109
|
+
try:
|
|
110
|
+
state = _state(meta)
|
|
111
|
+
except TaskWorkflowError as error:
|
|
112
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", str(error)) from error
|
|
113
|
+
route_revision = state["task_route_revision"]
|
|
114
|
+
input_revision = state["input_revision"]
|
|
115
|
+
current_phase = state["current_phase_id"]
|
|
116
|
+
for phase_id in state["phase_ids"]:
|
|
117
|
+
record = state["phases"][phase_id]
|
|
118
|
+
refs = record.get("artifact_refs", [])
|
|
119
|
+
if not isinstance(refs, list) or not all(isinstance(ref, str) for ref in refs):
|
|
120
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"phase artifact references are invalid: {phase_id}")
|
|
121
|
+
try:
|
|
122
|
+
artifact_digest = _artifact_digest(directory, refs)
|
|
123
|
+
except TaskWorkflowError as error:
|
|
124
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", str(error)) from error
|
|
125
|
+
evidence_refs = record.get("evidence_refs", [])
|
|
126
|
+
if not isinstance(evidence_refs, list) or not all(isinstance(ref, str) for ref in evidence_refs):
|
|
127
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"phase evidence references are invalid: {phase_id}")
|
|
128
|
+
evidence.extend(evidence_refs)
|
|
129
|
+
acceptance = record.get("acceptance")
|
|
130
|
+
phases.append({
|
|
131
|
+
"id": phase_id,
|
|
132
|
+
"status": record.get("status"),
|
|
133
|
+
"input_revision": record.get("input_revision"),
|
|
134
|
+
"artifact_revision": record.get("artifact_revision"),
|
|
135
|
+
"artifact_digest": artifact_digest,
|
|
136
|
+
"result_revision": record.get("result_revision"),
|
|
137
|
+
"result_ref": record.get("result_ref"),
|
|
138
|
+
"outcome_kind": record.get("outcome_kind"),
|
|
139
|
+
"evidence_refs": record.get("evidence_refs", []),
|
|
140
|
+
"acceptance": acceptance,
|
|
141
|
+
})
|
|
142
|
+
rc, head, _ = run_git(["rev-parse", "HEAD"], cwd=root, timeout=5)
|
|
143
|
+
if rc:
|
|
144
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git HEAD is unavailable")
|
|
145
|
+
rc, branch, _ = run_git(["branch", "--show-current"], cwd=root, timeout=5)
|
|
146
|
+
if rc:
|
|
147
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git branch is unavailable")
|
|
148
|
+
rc, status, _ = run_git(["status", "--porcelain=v1", "--untracked-files=all"], cwd=root, timeout=10)
|
|
149
|
+
if rc:
|
|
150
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git working-tree status is unavailable")
|
|
151
|
+
status = "".join(
|
|
152
|
+
line for line in status.splitlines(keepends=True)
|
|
153
|
+
if ".trellis/session-handoffs/" not in line[3:]
|
|
154
|
+
)
|
|
155
|
+
rc, diff, _ = run_git(["diff", "--binary", "HEAD", "--", "."], cwd=root, timeout=20)
|
|
156
|
+
if rc:
|
|
157
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git working-tree diff is unavailable")
|
|
158
|
+
rc, untracked, _ = run_git(["ls-files", "--others", "--exclude-standard", "-z"], cwd=root, timeout=10)
|
|
159
|
+
if rc:
|
|
160
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git untracked-file list is unavailable")
|
|
161
|
+
worktree = hashlib.sha256(diff.encode("utf-8", errors="surrogateescape"))
|
|
162
|
+
for raw_path in sorted(os.fsencode(path) for path in untracked.split("\0") if path):
|
|
163
|
+
relative = Path(os.fsdecode(raw_path))
|
|
164
|
+
if relative.parts[:2] == (".trellis", "session-handoffs"):
|
|
165
|
+
continue
|
|
166
|
+
candidate = root / relative
|
|
167
|
+
try:
|
|
168
|
+
candidate.resolve(strict=True).relative_to(root.resolve())
|
|
169
|
+
if candidate.is_symlink():
|
|
170
|
+
content = os.fsencode(os.readlink(candidate))
|
|
171
|
+
elif candidate.is_file():
|
|
172
|
+
content = candidate.read_bytes()
|
|
173
|
+
else:
|
|
174
|
+
raise OSError("not a regular file or symbolic link")
|
|
175
|
+
except (OSError, ValueError) as error:
|
|
176
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"untracked path is unavailable or unsafe: {relative}") from error
|
|
177
|
+
worktree.update(raw_path)
|
|
178
|
+
worktree.update(b"\0")
|
|
179
|
+
worktree.update(content)
|
|
180
|
+
worktree.update(b"\0")
|
|
181
|
+
evidence_digest = hashlib.sha256(
|
|
182
|
+
json.dumps(sorted(evidence), ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
183
|
+
).hexdigest()
|
|
184
|
+
return {
|
|
185
|
+
"task": {"id": task_id, "path": directory.relative_to(root).as_posix(), "status": data.get("status")},
|
|
186
|
+
"phase": {"current_phase_id": current_phase, "task_route_revision": route_revision, "input_revision": input_revision, "phases": phases},
|
|
187
|
+
"evidence_digest": evidence_digest,
|
|
188
|
+
"git": {
|
|
189
|
+
"head": head.strip(),
|
|
190
|
+
"branch": branch.strip() or None,
|
|
191
|
+
"clean": not status,
|
|
192
|
+
"status_digest": hashlib.sha256(status.encode("utf-8")).hexdigest(),
|
|
193
|
+
"content_digest": worktree.hexdigest(),
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _digest(value: dict) -> str:
|
|
199
|
+
raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
200
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _read_record(path: Path) -> tuple[dict | None, str | None]:
|
|
204
|
+
value, reason = read_json_checked(path)
|
|
205
|
+
if value is None and reason == JSON_READ_MISSING:
|
|
206
|
+
return None, None
|
|
207
|
+
if value is None:
|
|
208
|
+
raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"runtime record cannot be read: {reason}")
|
|
209
|
+
return value, None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def continuity_show(args: argparse.Namespace) -> int:
|
|
213
|
+
root = get_repo_root()
|
|
214
|
+
try:
|
|
215
|
+
directory, data, task_id, _ = _task(root)
|
|
216
|
+
current = _projection(root, directory, data, task_id)
|
|
217
|
+
source_digest = _digest(current)
|
|
218
|
+
task_path = directory.relative_to(root).as_posix()
|
|
219
|
+
record, _ = _read_record(_continuity_path(root, task_path))
|
|
220
|
+
if record is None:
|
|
221
|
+
result = {"status": "absent", "source_digest": source_digest, "current": current}
|
|
222
|
+
elif record.get("schema_version") != 1 or not isinstance(record.get("generation"), int):
|
|
223
|
+
result = {"status": "withheld", "reason": "continuity record has an invalid shape"}
|
|
224
|
+
else:
|
|
225
|
+
result = {
|
|
226
|
+
"status": "ready" if record.get("source_digest") == source_digest else "stale",
|
|
227
|
+
"generation": record["generation"],
|
|
228
|
+
"source_digest": source_digest,
|
|
229
|
+
"captured_source_digest": record.get("source_digest"),
|
|
230
|
+
"current": current,
|
|
231
|
+
}
|
|
232
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
233
|
+
return 0
|
|
234
|
+
except ContinuityError as error:
|
|
235
|
+
status = "absent" if error.code == "TASK_CONTINUITY_ABSENT" else "withheld"
|
|
236
|
+
print(json.dumps({"status": status, "code": error.code, "reason": str(error)}, ensure_ascii=False))
|
|
237
|
+
return 0 if status == "absent" else 1
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def continuity_capture(args: argparse.Namespace) -> int:
|
|
241
|
+
root = get_repo_root()
|
|
242
|
+
try:
|
|
243
|
+
directory, data, task_id, _ = _task(root)
|
|
244
|
+
current = _projection(root, directory, data, task_id)
|
|
245
|
+
source_digest = _digest(current)
|
|
246
|
+
task_path = directory.relative_to(root).as_posix()
|
|
247
|
+
path = _continuity_path(root, task_path)
|
|
248
|
+
with _record_lock(path):
|
|
249
|
+
record, _ = _read_record(path)
|
|
250
|
+
actual = record.get("source_digest") if record else "absent"
|
|
251
|
+
if actual != args.expected_source_digest:
|
|
252
|
+
raise ContinuityError("TASK_CONTINUITY_STALE", f"expected source digest {args.expected_source_digest}, current record is {actual}")
|
|
253
|
+
latest = _projection(root, directory, data, task_id)
|
|
254
|
+
if _digest(latest) != source_digest:
|
|
255
|
+
raise ContinuityError("TASK_CONTINUITY_STALE", "task or Git sources changed while capturing continuity")
|
|
256
|
+
generation = (record.get("generation", 0) + 1) if record else 1
|
|
257
|
+
saved = {"schema_version": 1, "generation": generation, "source_digest": source_digest, "current": current}
|
|
258
|
+
if not write_json(path, saved):
|
|
259
|
+
raise ContinuityError("TASK_CONTINUITY_WRITE_FAILED", "could not atomically write continuity record")
|
|
260
|
+
print(json.dumps({"status": "ready", "generation": generation, "source_digest": source_digest, "current": current}, ensure_ascii=False, sort_keys=True))
|
|
261
|
+
return 0
|
|
262
|
+
except ContinuityError as error:
|
|
263
|
+
print(json.dumps({"status": "stale" if error.code == "TASK_CONTINUITY_STALE" else "withheld", "code": error.code, "reason": str(error)}, ensure_ascii=False))
|
|
264
|
+
return 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _route_snapshot(data: dict) -> dict:
|
|
268
|
+
meta = data.get("meta")
|
|
269
|
+
if not isinstance(meta, dict):
|
|
270
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "task metadata is invalid")
|
|
271
|
+
workflow = meta.get("task_workflow")
|
|
272
|
+
if workflow is None:
|
|
273
|
+
return {"current_phase_id": None, "task_route_revision": None, "input_revision": None, "implementation_authorization": None}
|
|
274
|
+
try:
|
|
275
|
+
state = _state(meta)
|
|
276
|
+
except TaskWorkflowError as error:
|
|
277
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", str(error)) from error
|
|
278
|
+
authorization = state.get("implementation_authorization")
|
|
279
|
+
return {
|
|
280
|
+
"current_phase_id": state["current_phase_id"],
|
|
281
|
+
"task_route_revision": state["task_route_revision"],
|
|
282
|
+
"input_revision": state["input_revision"],
|
|
283
|
+
"implementation_authorization": authorization,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _direct_pointer(root: Path, task_path: str | None = None) -> tuple[str, str | None]:
|
|
288
|
+
active = resolve_active_task(root)
|
|
289
|
+
if not active.context_key or active.source_type == "session-fallback" or active.stale:
|
|
290
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "a direct, unambiguous session identity is required")
|
|
291
|
+
if task_path is not None and active.task_path != task_path:
|
|
292
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "task is not current in this direct session")
|
|
293
|
+
return active.context_key, active.task_path
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _load_owner(path: Path) -> dict | None:
|
|
297
|
+
record, _ = _read_record(path)
|
|
298
|
+
if record is None:
|
|
299
|
+
return None
|
|
300
|
+
required = {"schema_version", "task_id", "task_path", "package_id", "package_digest", "generation", "status", "source_session_id", "target_session_id", "continuity_digest", "route"}
|
|
301
|
+
if set(record) != required or record.get("schema_version") != 1 or not isinstance(record.get("generation"), int):
|
|
302
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "ownership record has an invalid shape")
|
|
303
|
+
return record
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _current_projection_digest(root: Path, task_dir: Path, data: dict, task_id: str) -> str:
|
|
307
|
+
return _digest(_projection(root, task_dir, data, task_id))
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def transfer_task_ownership(args: argparse.Namespace) -> int:
|
|
311
|
+
root = get_repo_root()
|
|
312
|
+
operation = args.transfer_action
|
|
313
|
+
try:
|
|
314
|
+
task_dir = resolve_task_dir(args.task_path, root)
|
|
315
|
+
if task_dir is None or not is_within_tasks_dir(task_dir, root):
|
|
316
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "task path is unavailable or unsafe")
|
|
317
|
+
task_path = task_dir.relative_to(root).as_posix()
|
|
318
|
+
data, reason = read_json_checked(task_dir / FILE_TASK_JSON)
|
|
319
|
+
if data is None:
|
|
320
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", f"task record cannot be read: {reason}")
|
|
321
|
+
task_id = data.get("id") or data.get("name")
|
|
322
|
+
if not isinstance(task_id, str) or task_id != args.task_id:
|
|
323
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "task identity does not match the requested task")
|
|
324
|
+
if operation != "status" and data.get("status") in {"completed", "cancelled"}:
|
|
325
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "terminal tasks cannot change ownership")
|
|
326
|
+
path = _ownership_path(root, task_path)
|
|
327
|
+
with _record_lock(path):
|
|
328
|
+
record = _load_owner(path)
|
|
329
|
+
if operation == "status":
|
|
330
|
+
if record and (record["package_id"] != args.package_id or record["package_digest"] != args.package_digest):
|
|
331
|
+
raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
|
|
332
|
+
result = record or {"status": "absent", "generation": 0, "task_id": task_id}
|
|
333
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
334
|
+
return 0
|
|
335
|
+
if not args.explicit_user_request:
|
|
336
|
+
raise ContinuityError("TASK_TRANSFER_NOT_AUTHORIZED", "ownership changes require an explicit user request")
|
|
337
|
+
if args.expected_generation is None or args.expected_generation < 0:
|
|
338
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "a non-negative expected generation is required")
|
|
339
|
+
if operation == "quiesce":
|
|
340
|
+
if not PACKAGE_ID_RE.fullmatch(args.package_id) or not PACKAGE_DIGEST_RE.fullmatch(args.package_digest):
|
|
341
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "session package identity or SHA-256 digest is invalid")
|
|
342
|
+
context_key, current_path = _direct_pointer(root, task_path)
|
|
343
|
+
if current_path is None:
|
|
344
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session has no current task")
|
|
345
|
+
continuity_digest = _current_projection_digest(root, task_dir, data, task_id)
|
|
346
|
+
if args.expected_continuity_digest is not None:
|
|
347
|
+
if not PACKAGE_DIGEST_RE.fullmatch(args.expected_continuity_digest):
|
|
348
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "expected continuity digest must be a SHA-256 digest")
|
|
349
|
+
if continuity_digest != args.expected_continuity_digest:
|
|
350
|
+
raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "current task projection differs from the package snapshot")
|
|
351
|
+
if record is not None:
|
|
352
|
+
if record["status"] == "quiescing" and record["generation"] == args.expected_generation + 1 and record["source_session_id"] == context_key and record["package_id"] == args.package_id and record["package_digest"] == args.package_digest:
|
|
353
|
+
if record["continuity_digest"] != continuity_digest:
|
|
354
|
+
raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
|
|
355
|
+
result = record
|
|
356
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
357
|
+
return 0
|
|
358
|
+
raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "task already has an ownership record")
|
|
359
|
+
if args.expected_generation != 0:
|
|
360
|
+
raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "initial ownership generation must be 0")
|
|
361
|
+
record = {
|
|
362
|
+
"schema_version": 1, "task_id": task_id, "task_path": task_path,
|
|
363
|
+
"package_id": args.package_id, "package_digest": args.package_digest,
|
|
364
|
+
"generation": 1, "status": "quiescing", "source_session_id": context_key,
|
|
365
|
+
"target_session_id": None, "continuity_digest": continuity_digest,
|
|
366
|
+
"route": _route_snapshot(data),
|
|
367
|
+
}
|
|
368
|
+
else:
|
|
369
|
+
if record is None:
|
|
370
|
+
raise ContinuityError("TASK_TRANSFER_ABSENT", "ownership transfer has not started")
|
|
371
|
+
if args.package_id != record["package_id"] or args.package_digest != record["package_digest"]:
|
|
372
|
+
raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
|
|
373
|
+
if args.expected_generation != record["generation"]:
|
|
374
|
+
raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", f"expected generation {args.expected_generation}, current generation is {record['generation']}")
|
|
375
|
+
if operation != "release" and _current_projection_digest(root, task_dir, data, task_id) != record["continuity_digest"]:
|
|
376
|
+
raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
|
|
377
|
+
if operation != "release" and _route_snapshot(data) != record["route"]:
|
|
378
|
+
raise ContinuityError("TASK_TRANSFER_STALE_ROUTE", "task phase, route, input, or implementation authorization changed")
|
|
379
|
+
context_key, current_path = _direct_pointer(root)
|
|
380
|
+
if operation in {"seal", "retire"}:
|
|
381
|
+
if context_key != record["source_session_id"] or current_path not in {record["task_path"], None}:
|
|
382
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session no longer owns the current task")
|
|
383
|
+
elif operation in {"claim", "consume"}:
|
|
384
|
+
if context_key == record["source_session_id"]:
|
|
385
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "target must be a different direct session")
|
|
386
|
+
if operation == "release":
|
|
387
|
+
if record["status"] not in {"quiescing", "sealed", "recovery_required"} or record["target_session_id"] is not None:
|
|
388
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "ownership can be released only before target claim")
|
|
389
|
+
if context_key != record["source_session_id"] or current_path not in {None, record["task_path"]}:
|
|
390
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the source session can release this transfer")
|
|
391
|
+
if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
|
|
392
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer could not be restored")
|
|
393
|
+
active = resolve_active_task(root)
|
|
394
|
+
if active.task_path != record["task_path"] or active.context_key != context_key:
|
|
395
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer restoration could not be verified")
|
|
396
|
+
path.unlink(missing_ok=True)
|
|
397
|
+
print(json.dumps({"status": "released", "generation": record["generation"], "task_id": task_id}, ensure_ascii=False, sort_keys=True))
|
|
398
|
+
return 0
|
|
399
|
+
if operation == "seal" and record["status"] == "quiescing":
|
|
400
|
+
record.update({"status": "sealed", "generation": record["generation"] + 1})
|
|
401
|
+
elif operation == "retire" and record["status"] in {"sealed", "retiring", "recovery_required"}:
|
|
402
|
+
if record["status"] == "sealed":
|
|
403
|
+
record.update({"status": "retiring", "generation": record["generation"] + 1})
|
|
404
|
+
if not write_json(path, record):
|
|
405
|
+
raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record source retirement")
|
|
406
|
+
if current_path is not None and not compare_and_set_active_task(root, context_key, record["task_path"], None):
|
|
407
|
+
record["status"] = "recovery_required"
|
|
408
|
+
write_json(path, record)
|
|
409
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer changed during retirement")
|
|
410
|
+
if resolve_active_task(root).task_path is not None:
|
|
411
|
+
record["status"] = "recovery_required"
|
|
412
|
+
write_json(path, record)
|
|
413
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer remains after retirement")
|
|
414
|
+
record["status"] = "ready"
|
|
415
|
+
elif operation == "claim" and record["status"] in {"ready", "claiming", "recovery_required", "claimed"}:
|
|
416
|
+
if record["status"] == "recovery_required" and record["target_session_id"] != context_key:
|
|
417
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the recorded target session can recover a claim")
|
|
418
|
+
if current_path not in {None, record["task_path"]}:
|
|
419
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "target session already has a current task")
|
|
420
|
+
if record["status"] == "ready":
|
|
421
|
+
record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
|
|
422
|
+
if not write_json(path, record):
|
|
423
|
+
raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target claim")
|
|
424
|
+
elif record["status"] == "claimed":
|
|
425
|
+
previous_target = record["target_session_id"]
|
|
426
|
+
if previous_target == context_key:
|
|
427
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "the current target must consume ownership instead of claiming it again")
|
|
428
|
+
if previous_target is not None and not clear_active_task_if_matches(root, previous_target, record["task_path"]):
|
|
429
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "previous target pointer could not be cleared")
|
|
430
|
+
record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
|
|
431
|
+
if not write_json(path, record):
|
|
432
|
+
raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target takeover")
|
|
433
|
+
if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
|
|
434
|
+
record["status"] = "recovery_required"
|
|
435
|
+
write_json(path, record)
|
|
436
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer could not be claimed")
|
|
437
|
+
active = resolve_active_task(root)
|
|
438
|
+
if active.task_path != record["task_path"] or active.context_key != context_key:
|
|
439
|
+
record["status"] = "recovery_required"
|
|
440
|
+
write_json(path, record)
|
|
441
|
+
raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer verification failed")
|
|
442
|
+
record["status"] = "claimed"
|
|
443
|
+
elif operation == "consume" and record["status"] == "claimed":
|
|
444
|
+
if context_key != record["target_session_id"] or current_path != record["task_path"]:
|
|
445
|
+
raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the direct target session can consume the task")
|
|
446
|
+
record.update({"status": "consumed", "generation": record["generation"] + 1})
|
|
447
|
+
else:
|
|
448
|
+
raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", f"cannot apply {operation} while ownership is {record['status']}")
|
|
449
|
+
if not write_json(path, record):
|
|
450
|
+
raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not atomically update ownership record")
|
|
451
|
+
print(json.dumps(record, ensure_ascii=False, sort_keys=True))
|
|
452
|
+
return 0
|
|
453
|
+
except ContinuityError as error:
|
|
454
|
+
print(json.dumps({"status": "withheld", "code": error.code, "reason": str(error)}, ensure_ascii=False))
|
|
455
|
+
return 1
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def add_continuity_commands(subparsers) -> None:
|
|
459
|
+
continuity = subparsers.add_parser("continuity", help="Read or capture current task continuity")
|
|
460
|
+
continuity_sub = continuity.add_subparsers(dest="continuity_action", required=True)
|
|
461
|
+
continuity_sub.add_parser("show", help="Recompute current continuity status")
|
|
462
|
+
capture = continuity_sub.add_parser("capture", help="Capture the current task projection with compare-and-set")
|
|
463
|
+
capture.add_argument("--expected-source-digest", required=True, help="Current captured digest, or absent")
|
|
464
|
+
|
|
465
|
+
transfer = subparsers.add_parser("transfer", help="Transfer task ownership between direct sessions")
|
|
466
|
+
transfer.add_argument("transfer_action", choices=("status", "quiesce", "seal", "retire", "claim", "consume", "release"))
|
|
467
|
+
transfer.add_argument("--task-id", required=True)
|
|
468
|
+
transfer.add_argument("--task-path", required=True)
|
|
469
|
+
transfer.add_argument("--package-id", required=True)
|
|
470
|
+
transfer.add_argument("--package-digest", required=True)
|
|
471
|
+
transfer.add_argument("--expected-continuity-digest")
|
|
472
|
+
transfer.add_argument("--expected-generation", type=int)
|
|
473
|
+
transfer.add_argument("--explicit-user-request", action="store_true")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def clear_task_runtime_records(repo_root: Path, task_path: str) -> bool:
|
|
477
|
+
"""Remove only this task's current continuity and ownership projections."""
|
|
478
|
+
for path in (_continuity_path(repo_root, task_path), _ownership_path(repo_root, task_path)):
|
|
479
|
+
if not path.exists():
|
|
480
|
+
continue
|
|
481
|
+
with _record_lock(path):
|
|
482
|
+
try:
|
|
483
|
+
path.unlink(missing_ok=True)
|
|
484
|
+
except OSError:
|
|
485
|
+
return False
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@contextlib.contextmanager
|
|
490
|
+
def active_task_change_guard(repo_root: Path, context_key: str, task_path: str, *, clearing: bool):
|
|
491
|
+
"""Serialize pointer writes with ownership changes for the same task."""
|
|
492
|
+
directory = resolve_task_dir(task_path, repo_root)
|
|
493
|
+
if directory is None:
|
|
494
|
+
yield False
|
|
495
|
+
return
|
|
496
|
+
task_path = directory.relative_to(repo_root).as_posix()
|
|
497
|
+
path = _ownership_path(repo_root, task_path)
|
|
498
|
+
with _record_lock(path):
|
|
499
|
+
try:
|
|
500
|
+
record = _load_owner(path)
|
|
501
|
+
except ContinuityError:
|
|
502
|
+
yield False
|
|
503
|
+
return
|
|
504
|
+
if record is None:
|
|
505
|
+
yield True
|
|
506
|
+
return
|
|
507
|
+
data, _ = read_json_checked(directory / FILE_TASK_JSON)
|
|
508
|
+
task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
|
|
509
|
+
if task_id != record["task_id"] or record["task_path"] != task_path:
|
|
510
|
+
yield False
|
|
511
|
+
return
|
|
512
|
+
if clearing:
|
|
513
|
+
allowed = record["status"] == "consumed" and record["target_session_id"] == context_key
|
|
514
|
+
elif record["status"] == "ready":
|
|
515
|
+
allowed = context_key != record["source_session_id"] and task_path == record["task_path"]
|
|
516
|
+
elif record["status"] == "consumed":
|
|
517
|
+
allowed = context_key == record["target_session_id"] and task_path == record["task_path"]
|
|
518
|
+
else:
|
|
519
|
+
allowed = record["status"] in {"claiming", "claimed"} and context_key == record["target_session_id"] and task_path == record["task_path"]
|
|
520
|
+
yield allowed
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def assert_task_mutation_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
|
|
524
|
+
"""Reject task writes until ownership is consumed or the source releases it."""
|
|
525
|
+
with task_mutation_guard(repo_root, task_dir, data) as allowed:
|
|
526
|
+
if not allowed:
|
|
527
|
+
raise TaskWorkflowError("TASK_TRANSFER_ACTIVE", "task changes are fenced until the target session consumes ownership or the source releases the transfer")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def assert_task_identity_change_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
|
|
531
|
+
"""Keep an active ownership record addressable while the task ID is fixed."""
|
|
532
|
+
task_path = task_dir.relative_to(repo_root).as_posix()
|
|
533
|
+
path = _ownership_path(repo_root, task_path)
|
|
534
|
+
with _record_lock(path):
|
|
535
|
+
try:
|
|
536
|
+
record = _load_owner(path)
|
|
537
|
+
except ContinuityError as error:
|
|
538
|
+
raise TaskWorkflowError(error.code, str(error)) from error
|
|
539
|
+
if record is not None:
|
|
540
|
+
raise TaskWorkflowError("TASK_TRANSFER_TASK_ID_LOCKED", "task ID cannot change while its ownership record is active")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@contextlib.contextmanager
|
|
544
|
+
def task_mutation_guard(repo_root: Path, task_dir: Path, data: dict):
|
|
545
|
+
"""Hold the ownership lock across a single task mutation."""
|
|
546
|
+
task_path = task_dir.relative_to(repo_root).as_posix()
|
|
547
|
+
path = _ownership_path(repo_root, task_path)
|
|
548
|
+
with _record_lock(path):
|
|
549
|
+
try:
|
|
550
|
+
record = _load_owner(path)
|
|
551
|
+
except ContinuityError:
|
|
552
|
+
yield False
|
|
553
|
+
return
|
|
554
|
+
if record is None:
|
|
555
|
+
yield True
|
|
556
|
+
return
|
|
557
|
+
task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
|
|
558
|
+
if task_id != record["task_id"] or record["task_path"] != task_path:
|
|
559
|
+
yield False
|
|
560
|
+
return
|
|
561
|
+
active = resolve_active_task(repo_root)
|
|
562
|
+
allowed = record["status"] == "consumed" and active.context_key == record["target_session_id"] and active.task_path == task_path
|
|
563
|
+
yield allowed
|