@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
|
@@ -1,735 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Create and validate durable artifacts for one bounded Trellis subnode.
|
|
3
|
-
|
|
4
|
-
The coordinator creates an immutable brief before dispatching a subnode. The
|
|
5
|
-
subnode appends its worklog and writes its own final report. This helper never
|
|
6
|
-
dispatches workers, makes the acceptance judgment, or mutates task state.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import argparse
|
|
12
|
-
import json
|
|
13
|
-
import re
|
|
14
|
-
import sys
|
|
15
|
-
from datetime import datetime, timezone
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Any, NoReturn
|
|
18
|
-
|
|
19
|
-
from common.io import write_text_atomic
|
|
20
|
-
from common.paths import get_repo_root, get_tasks_dir
|
|
21
|
-
from common.task_utils import is_within_tasks_dir, resolve_task_dir
|
|
22
|
-
from common.tasks import load_task
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
SCHEMA_VERSION = 2
|
|
26
|
-
MAX_DRAFT_BYTES = 64 * 1024
|
|
27
|
-
MAX_REPORT_BYTES = 128 * 1024
|
|
28
|
-
MAX_WORKLOG_BYTES = 128 * 1024
|
|
29
|
-
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
30
|
-
CHECKPOINT_RE = re.compile(r"^<!-- trellis-checkpoint: (?P<payload>\{.*\}) -->$", re.MULTILINE)
|
|
31
|
-
SECRET_PATTERNS = (
|
|
32
|
-
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
|
33
|
-
re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"),
|
|
34
|
-
re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}\b"),
|
|
35
|
-
)
|
|
36
|
-
TERMINAL_LIFECYCLES = {"done", "error", "killed", "crashed"}
|
|
37
|
-
DISPOSITION_OUTCOMES = {"accepted", "rejected", "deferred"}
|
|
38
|
-
REQUIRED_DISPOSITION_CHECKS = {
|
|
39
|
-
"report_validation",
|
|
40
|
-
"source_recheck",
|
|
41
|
-
"protected_target_check",
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class ArtifactError(Exception):
|
|
46
|
-
"""A user-correctable artifact validation failure."""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _fail(message: str) -> NoReturn:
|
|
50
|
-
raise ArtifactError(message)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _read_json_object(path: Path, max_bytes: int, label: str) -> tuple[dict[str, Any], bytes]:
|
|
54
|
-
"""Read one bounded, non-symlink JSON object."""
|
|
55
|
-
if path.is_symlink():
|
|
56
|
-
_fail(f"refusing symlinked {label}: {path}")
|
|
57
|
-
try:
|
|
58
|
-
size = path.stat().st_size
|
|
59
|
-
except OSError as exc:
|
|
60
|
-
_fail(f"could not stat {label} {path}: {exc}")
|
|
61
|
-
if size > max_bytes:
|
|
62
|
-
_fail(f"{label} exceeds the {max_bytes} byte limit: {path}")
|
|
63
|
-
try:
|
|
64
|
-
raw = path.read_bytes()
|
|
65
|
-
data = json.loads(raw.decode("utf-8"))
|
|
66
|
-
except FileNotFoundError:
|
|
67
|
-
_fail(f"{label} does not exist: {path}")
|
|
68
|
-
except UnicodeDecodeError:
|
|
69
|
-
_fail(f"{label} is not UTF-8: {path}")
|
|
70
|
-
except json.JSONDecodeError as exc:
|
|
71
|
-
_fail(f"{label} is not valid JSON: {path}: {exc.msg}")
|
|
72
|
-
except OSError as exc:
|
|
73
|
-
_fail(f"could not read {label} {path}: {exc}")
|
|
74
|
-
if not isinstance(data, dict):
|
|
75
|
-
_fail(f"{label} must contain a JSON object: {path}")
|
|
76
|
-
return data, raw
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _require_text(value: Any, field: str, *, max_len: int = 4096) -> str:
|
|
80
|
-
if not isinstance(value, str) or not value.strip():
|
|
81
|
-
_fail(f"{field} must be a non-empty string")
|
|
82
|
-
if len(value) > max_len or "\x00" in value:
|
|
83
|
-
_fail(f"{field} is not a bounded text value")
|
|
84
|
-
return value
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _require_text_list(value: Any, field: str, *, allow_empty: bool = False) -> list[str]:
|
|
88
|
-
if not isinstance(value, list) or (not value and not allow_empty):
|
|
89
|
-
_fail(f"{field} must be {'a list' if allow_empty else 'a non-empty list'}")
|
|
90
|
-
result: list[str] = []
|
|
91
|
-
for index, item in enumerate(value):
|
|
92
|
-
result.append(_require_text(item, f"{field}[{index}]"))
|
|
93
|
-
return result
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _require_id(value: Any, field: str) -> str:
|
|
97
|
-
text = _require_text(value, field, max_len=64)
|
|
98
|
-
if not ID_RE.fullmatch(text):
|
|
99
|
-
_fail(f"{field} must match {ID_RE.pattern}")
|
|
100
|
-
return text
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _reject_obvious_secrets(raw: bytes, label: str) -> None:
|
|
104
|
-
text = raw.decode("utf-8", errors="replace")
|
|
105
|
-
for pattern in SECRET_PATTERNS:
|
|
106
|
-
if pattern.search(text):
|
|
107
|
-
_fail(f"{label} appears to contain a credential; remove it before writing the artifact")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _task_id(task_dir: Path) -> str:
|
|
111
|
-
task = load_task(task_dir)
|
|
112
|
-
if task is None:
|
|
113
|
-
_fail(f"task.json is missing or invalid: {task_dir}")
|
|
114
|
-
task_id = task.raw.get("id") or task.dir_name
|
|
115
|
-
if not isinstance(task_id, str) or not task_id.strip():
|
|
116
|
-
_fail(f"task has no usable id: {task_dir}")
|
|
117
|
-
if task.status not in {"planning", "in_progress"}:
|
|
118
|
-
_fail(f"task is not active (planning or in_progress): {task_dir}")
|
|
119
|
-
return task_id
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _task_lexical_from_resolved(task_resolved: Path, repo_root: Path) -> Path:
|
|
123
|
-
tasks_lexical = get_tasks_dir(repo_root)
|
|
124
|
-
try:
|
|
125
|
-
relative = task_resolved.relative_to(tasks_lexical.resolve())
|
|
126
|
-
except ValueError:
|
|
127
|
-
_fail(f"task is outside the Trellis task directory: {task_resolved}")
|
|
128
|
-
return tasks_lexical / relative
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _assert_no_subpath_symlinks(task_dir: Path, target: Path) -> None:
|
|
132
|
-
"""Reject symlinks below a task while permitting a symlinked .trellis root."""
|
|
133
|
-
if task_dir.is_symlink():
|
|
134
|
-
_fail(f"refusing symlinked task directory: {task_dir}")
|
|
135
|
-
try:
|
|
136
|
-
parts = target.relative_to(task_dir).parts
|
|
137
|
-
except ValueError:
|
|
138
|
-
_fail(f"artifact path is outside its task: {target}")
|
|
139
|
-
current = task_dir
|
|
140
|
-
for part in parts:
|
|
141
|
-
current = current / part
|
|
142
|
-
if current.is_symlink():
|
|
143
|
-
_fail(f"refusing symlinked artifact path component: {current}")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _node_dir(task_dir: Path, work_id: str, subnode_id: str) -> Path:
|
|
147
|
-
return task_dir / "subnodes" / work_id / subnode_id
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _relative_to_repo(path: Path, repo_root: Path) -> str:
|
|
151
|
-
try:
|
|
152
|
-
return path.relative_to(repo_root).as_posix()
|
|
153
|
-
except ValueError:
|
|
154
|
-
_fail(f"path is outside repository: {path}")
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _validate_snapshot(value: Any) -> None:
|
|
158
|
-
if not isinstance(value, list) or not value:
|
|
159
|
-
_fail("source_snapshot must be a non-empty list")
|
|
160
|
-
for index, item in enumerate(value):
|
|
161
|
-
if not isinstance(item, dict):
|
|
162
|
-
_fail(f"source_snapshot[{index}] must be an object")
|
|
163
|
-
_require_text(item.get("locator"), f"source_snapshot[{index}].locator")
|
|
164
|
-
_require_text(
|
|
165
|
-
item.get("revision_or_digest"),
|
|
166
|
-
f"source_snapshot[{index}].revision_or_digest",
|
|
167
|
-
)
|
|
168
|
-
_require_text(item.get("observed_at"), f"source_snapshot[{index}].observed_at")
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _validate_channel_ref(value: Any) -> None:
|
|
172
|
-
if not isinstance(value, dict):
|
|
173
|
-
_fail("channel_ref must be an object")
|
|
174
|
-
_require_text(value.get("name"), "channel_ref.name", max_len=128)
|
|
175
|
-
scope = _require_text(value.get("scope"), "channel_ref.scope", max_len=32)
|
|
176
|
-
if scope not in {"project", "global"}:
|
|
177
|
-
_fail("channel_ref.scope must be project or global")
|
|
178
|
-
_require_text(value.get("worker_handle"), "channel_ref.worker_handle", max_len=128)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _validate_retry_target(
|
|
182
|
-
retry_of: Any,
|
|
183
|
-
task_dir: Path,
|
|
184
|
-
work_id: str,
|
|
185
|
-
subnode_id: str,
|
|
186
|
-
) -> str | None:
|
|
187
|
-
"""Validate the coordinator's explicit, already-created retry source."""
|
|
188
|
-
if retry_of is None:
|
|
189
|
-
return None
|
|
190
|
-
retry_id = _require_id(retry_of, "brief.retry_of")
|
|
191
|
-
if retry_id == subnode_id:
|
|
192
|
-
_fail("brief.retry_of must name a different subnode")
|
|
193
|
-
target_dir = _node_dir(task_dir, work_id, retry_id)
|
|
194
|
-
_assert_no_subpath_symlinks(task_dir, target_dir)
|
|
195
|
-
target_brief_path = target_dir / "brief.json"
|
|
196
|
-
if not target_brief_path.exists():
|
|
197
|
-
_fail("brief.retry_of must identify an existing subnode brief in this task and work_id")
|
|
198
|
-
target_brief, _raw = _read_json_object(
|
|
199
|
-
target_brief_path,
|
|
200
|
-
MAX_DRAFT_BYTES,
|
|
201
|
-
"brief.retry_of target brief",
|
|
202
|
-
)
|
|
203
|
-
expected_task_id = _task_id(task_dir)
|
|
204
|
-
if target_brief.get("schema_version") != SCHEMA_VERSION:
|
|
205
|
-
_fail("brief.retry_of target brief has an unsupported schema_version")
|
|
206
|
-
if target_brief.get("task_id") != expected_task_id:
|
|
207
|
-
_fail("brief.retry_of target brief does not belong to this task")
|
|
208
|
-
if target_brief.get("work_id") != work_id:
|
|
209
|
-
_fail("brief.retry_of target brief does not belong to this work_id")
|
|
210
|
-
if target_brief.get("subnode_id") != retry_id:
|
|
211
|
-
_fail("brief.retry_of target brief identity does not match its artifact directory")
|
|
212
|
-
if target_brief.get("role_id") != "subnode":
|
|
213
|
-
_fail("brief.retry_of target brief.role_id must be subnode")
|
|
214
|
-
return retry_id
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _validate_brief_data(
|
|
218
|
-
brief: dict[str, Any],
|
|
219
|
-
task_dir: Path,
|
|
220
|
-
node_dir: Path,
|
|
221
|
-
repo_root: Path,
|
|
222
|
-
) -> None:
|
|
223
|
-
expected_task_id = _task_id(task_dir)
|
|
224
|
-
if brief.get("schema_version") != SCHEMA_VERSION:
|
|
225
|
-
_fail(f"brief.schema_version must be {SCHEMA_VERSION}")
|
|
226
|
-
if brief.get("task_id") != expected_task_id:
|
|
227
|
-
_fail("brief.task_id does not match task.json")
|
|
228
|
-
work_id = _require_id(brief.get("work_id"), "brief.work_id")
|
|
229
|
-
subnode_id = _require_id(brief.get("subnode_id"), "brief.subnode_id")
|
|
230
|
-
if node_dir != _node_dir(task_dir, work_id, subnode_id):
|
|
231
|
-
_fail("brief identity does not match its artifact directory")
|
|
232
|
-
if brief.get("role_id") != "subnode":
|
|
233
|
-
_fail("brief.role_id must be subnode")
|
|
234
|
-
_require_text(brief.get("question"), "brief.question")
|
|
235
|
-
_require_text(brief.get("independence_reason"), "brief.independence_reason")
|
|
236
|
-
_require_text_list(brief.get("scope"), "brief.scope")
|
|
237
|
-
_require_text_list(brief.get("protected_targets"), "brief.protected_targets", allow_empty=True)
|
|
238
|
-
_require_text(brief.get("lens"), "brief.lens")
|
|
239
|
-
_require_text(brief.get("evidence_method"), "brief.evidence_method")
|
|
240
|
-
_validate_snapshot(brief.get("source_snapshot"))
|
|
241
|
-
_require_text_list(brief.get("dependencies"), "brief.dependencies", allow_empty=True)
|
|
242
|
-
_require_text_list(brief.get("stop_conditions"), "brief.stop_conditions")
|
|
243
|
-
_require_text(brief.get("deadline"), "brief.deadline", max_len=128)
|
|
244
|
-
_validate_channel_ref(brief.get("channel_ref"))
|
|
245
|
-
if brief["channel_ref"]["worker_handle"] != subnode_id:
|
|
246
|
-
_fail("channel_ref.worker_handle must match brief.subnode_id")
|
|
247
|
-
_validate_retry_target(
|
|
248
|
-
brief.get("retry_of"),
|
|
249
|
-
task_dir,
|
|
250
|
-
work_id,
|
|
251
|
-
subnode_id,
|
|
252
|
-
)
|
|
253
|
-
counter_of = brief.get("counter_of")
|
|
254
|
-
if counter_of is not None:
|
|
255
|
-
counter_id = _require_id(counter_of, "brief.counter_of")
|
|
256
|
-
if counter_id == subnode_id:
|
|
257
|
-
_fail("brief.counter_of must name a different subnode")
|
|
258
|
-
expected_report = _relative_to_repo(node_dir / "report.json", repo_root)
|
|
259
|
-
if brief.get("report_path") != expected_report:
|
|
260
|
-
_fail("brief.report_path does not match its immutable artifact directory")
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def _resolve_artifact_file(
|
|
264
|
-
value: str,
|
|
265
|
-
expected_name: str,
|
|
266
|
-
repo_root: Path,
|
|
267
|
-
) -> tuple[Path, Path, Path]:
|
|
268
|
-
"""Return lexical artifact, node directory, and task directory after checks."""
|
|
269
|
-
supplied = Path(value)
|
|
270
|
-
lexical = supplied if supplied.is_absolute() else repo_root / supplied
|
|
271
|
-
if lexical.name != expected_name:
|
|
272
|
-
_fail(f"expected a {expected_name} path, received: {value}")
|
|
273
|
-
if lexical.is_symlink():
|
|
274
|
-
_fail(f"refusing symlinked artifact: {lexical}")
|
|
275
|
-
try:
|
|
276
|
-
resolved = lexical.resolve(strict=True)
|
|
277
|
-
except (OSError, RuntimeError) as exc:
|
|
278
|
-
_fail(f"could not resolve artifact {lexical}: {exc}")
|
|
279
|
-
node_resolved = resolved.parent
|
|
280
|
-
work_resolved = node_resolved.parent
|
|
281
|
-
subnodes_resolved = work_resolved.parent
|
|
282
|
-
task_resolved = subnodes_resolved.parent
|
|
283
|
-
if subnodes_resolved.name != "subnodes":
|
|
284
|
-
_fail(f"artifact is not under a task subnodes directory: {lexical}")
|
|
285
|
-
if not is_within_tasks_dir(task_resolved, repo_root):
|
|
286
|
-
_fail(f"artifact task is not a direct active task: {lexical}")
|
|
287
|
-
task_dir = _task_lexical_from_resolved(task_resolved, repo_root)
|
|
288
|
-
expected = _node_dir(task_dir, work_resolved.name, node_resolved.name) / expected_name
|
|
289
|
-
try:
|
|
290
|
-
same_path = lexical.resolve() == expected.resolve()
|
|
291
|
-
except (OSError, RuntimeError) as exc:
|
|
292
|
-
_fail(f"could not verify artifact containment: {exc}")
|
|
293
|
-
if not same_path:
|
|
294
|
-
_fail(f"artifact path does not match its task-owned location: {lexical}")
|
|
295
|
-
_assert_no_subpath_symlinks(task_dir, expected)
|
|
296
|
-
return expected, expected.parent, task_dir
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _validate_brief_file(
|
|
300
|
-
value: str,
|
|
301
|
-
repo_root: Path,
|
|
302
|
-
) -> tuple[dict[str, Any], bytes, Path, Path, Path]:
|
|
303
|
-
brief_path, node_dir, task_dir = _resolve_artifact_file(value, "brief.json", repo_root)
|
|
304
|
-
brief, raw = _read_json_object(brief_path, MAX_DRAFT_BYTES, "brief")
|
|
305
|
-
_reject_obvious_secrets(raw, "brief")
|
|
306
|
-
_validate_brief_data(brief, task_dir, node_dir, repo_root)
|
|
307
|
-
return brief, raw, brief_path, node_dir, task_dir
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def _validate_evidence(value: Any) -> set[str]:
|
|
311
|
-
if not isinstance(value, list):
|
|
312
|
-
_fail("report.evidence must be a list")
|
|
313
|
-
identities: set[tuple[str, str]] = set()
|
|
314
|
-
for index, item in enumerate(value):
|
|
315
|
-
if not isinstance(item, dict):
|
|
316
|
-
_fail(f"report.evidence[{index}] must be an object")
|
|
317
|
-
evidence_id = _require_text(item.get("id"), f"report.evidence[{index}].id", max_len=128)
|
|
318
|
-
locator = _require_text(item.get("locator"), f"report.evidence[{index}].locator")
|
|
319
|
-
_require_text(item.get("summary"), f"report.evidence[{index}].summary")
|
|
320
|
-
identity = (evidence_id, locator)
|
|
321
|
-
if identity in identities:
|
|
322
|
-
_fail("report.evidence contains a duplicate evidence identity")
|
|
323
|
-
identities.add(identity)
|
|
324
|
-
return {evidence_id for evidence_id, _locator in identities}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def _validate_id_list(value: Any, field: str) -> list[str]:
|
|
328
|
-
if not isinstance(value, list):
|
|
329
|
-
_fail(f"{field} must be a list")
|
|
330
|
-
result = []
|
|
331
|
-
for index, item in enumerate(value):
|
|
332
|
-
result.append(_require_text(item, f"{field}[{index}]", max_len=128))
|
|
333
|
-
if len(result) != len(set(result)):
|
|
334
|
-
_fail(f"{field} must not contain duplicates")
|
|
335
|
-
return result
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def _validate_typed_notes(value: Any, field: str) -> None:
|
|
339
|
-
if not isinstance(value, list):
|
|
340
|
-
_fail(f"report.{field} must be a list")
|
|
341
|
-
for index, item in enumerate(value):
|
|
342
|
-
if not isinstance(item, dict):
|
|
343
|
-
_fail(f"report.{field}[{index}] must be an object")
|
|
344
|
-
_require_id(item.get("id"), f"report.{field}[{index}].id")
|
|
345
|
-
_require_text(item.get("type"), f"report.{field}[{index}].type", max_len=64)
|
|
346
|
-
_require_text(item.get("detail"), f"report.{field}[{index}].detail")
|
|
347
|
-
_validate_id_list(item.get("evidence_ids", []), f"report.{field}[{index}].evidence_ids")
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def _validate_checkpoint(node_dir: Path, evidence_ids: set[str], scope: list[str]) -> list[str]:
|
|
351
|
-
worklog_path = node_dir / "worklog.md"
|
|
352
|
-
if worklog_path.is_symlink():
|
|
353
|
-
_fail(f"refusing symlinked worklog: {worklog_path}")
|
|
354
|
-
try:
|
|
355
|
-
raw = worklog_path.read_bytes()
|
|
356
|
-
except FileNotFoundError:
|
|
357
|
-
_fail(f"worklog does not exist: {worklog_path}")
|
|
358
|
-
except OSError as exc:
|
|
359
|
-
_fail(f"could not read worklog {worklog_path}: {exc}")
|
|
360
|
-
if len(raw) > MAX_WORKLOG_BYTES:
|
|
361
|
-
_fail(f"worklog exceeds the {MAX_WORKLOG_BYTES} byte limit: {worklog_path}")
|
|
362
|
-
_reject_obvious_secrets(raw, "worklog")
|
|
363
|
-
try:
|
|
364
|
-
text = raw.decode("utf-8")
|
|
365
|
-
except UnicodeDecodeError:
|
|
366
|
-
_fail(f"worklog is not UTF-8: {worklog_path}")
|
|
367
|
-
matches = list(CHECKPOINT_RE.finditer(text))
|
|
368
|
-
if not matches:
|
|
369
|
-
return ["missing_worklog_checkpoint"]
|
|
370
|
-
concerns: list[str] = []
|
|
371
|
-
checkpoint_ids: set[str] = set()
|
|
372
|
-
for match in matches:
|
|
373
|
-
try:
|
|
374
|
-
checkpoint = json.loads(match.group("payload"))
|
|
375
|
-
except json.JSONDecodeError:
|
|
376
|
-
concerns.append("malformed_worklog_checkpoint")
|
|
377
|
-
continue
|
|
378
|
-
if not isinstance(checkpoint, dict):
|
|
379
|
-
concerns.append("malformed_worklog_checkpoint")
|
|
380
|
-
continue
|
|
381
|
-
try:
|
|
382
|
-
checkpoint_id = _require_id(checkpoint.get("id"), "worklog checkpoint.id")
|
|
383
|
-
if checkpoint_id in checkpoint_ids:
|
|
384
|
-
concerns.append("duplicate_worklog_checkpoint")
|
|
385
|
-
checkpoint_ids.add(checkpoint_id)
|
|
386
|
-
covered_scope = _require_text_list(
|
|
387
|
-
checkpoint.get("covered_scope"),
|
|
388
|
-
"worklog checkpoint.covered_scope",
|
|
389
|
-
allow_empty=True,
|
|
390
|
-
)
|
|
391
|
-
for item in covered_scope:
|
|
392
|
-
if item not in scope:
|
|
393
|
-
concerns.append("checkpoint_scope_outside_brief")
|
|
394
|
-
checkpoint_evidence = _validate_id_list(
|
|
395
|
-
checkpoint.get("evidence_ids"), "worklog checkpoint.evidence_ids"
|
|
396
|
-
)
|
|
397
|
-
if any(item not in evidence_ids for item in checkpoint_evidence):
|
|
398
|
-
concerns.append("checkpoint_evidence_unresolved")
|
|
399
|
-
_require_text(
|
|
400
|
-
checkpoint.get("conclusion_or_blocker"),
|
|
401
|
-
"worklog checkpoint.conclusion_or_blocker",
|
|
402
|
-
)
|
|
403
|
-
_require_text_list(
|
|
404
|
-
checkpoint.get("unknowns"),
|
|
405
|
-
"worklog checkpoint.unknowns",
|
|
406
|
-
allow_empty=True,
|
|
407
|
-
)
|
|
408
|
-
_require_text(checkpoint.get("safe_resume_point"), "worklog checkpoint.safe_resume_point")
|
|
409
|
-
except ArtifactError:
|
|
410
|
-
concerns.append("malformed_worklog_checkpoint")
|
|
411
|
-
return sorted(set(concerns))
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def _validate_report_data(
|
|
415
|
-
report: dict[str, Any],
|
|
416
|
-
brief: dict[str, Any],
|
|
417
|
-
) -> tuple[set[str], list[str]]:
|
|
418
|
-
if report.get("schema_version") != SCHEMA_VERSION:
|
|
419
|
-
_fail(f"report.schema_version must be {SCHEMA_VERSION}")
|
|
420
|
-
for field in ("task_id", "work_id", "subnode_id", "role_id"):
|
|
421
|
-
if report.get(field) != brief.get(field):
|
|
422
|
-
_fail(f"report.{field} does not match brief.{field}")
|
|
423
|
-
if report.get("scope") != brief.get("scope"):
|
|
424
|
-
_fail("report.scope does not match brief.scope")
|
|
425
|
-
if report.get("lens") != brief.get("lens"):
|
|
426
|
-
_fail("report.lens does not match brief.lens")
|
|
427
|
-
status = report.get("status")
|
|
428
|
-
if status not in {"complete", "blocked", "incomplete", "error"}:
|
|
429
|
-
_fail("report.status must be complete, blocked, incomplete, or error")
|
|
430
|
-
evidence = _validate_evidence(report.get("evidence"))
|
|
431
|
-
assessment = report.get("scope_assessment")
|
|
432
|
-
if not isinstance(assessment, list) or len(assessment) != len(brief["scope"]):
|
|
433
|
-
_fail("report.scope_assessment must contain one item for every brief scope item")
|
|
434
|
-
concerns: list[str] = []
|
|
435
|
-
for index, item in enumerate(assessment):
|
|
436
|
-
if not isinstance(item, dict):
|
|
437
|
-
_fail(f"report.scope_assessment[{index}] must be an object")
|
|
438
|
-
if item.get("scope") != brief["scope"][index]:
|
|
439
|
-
_fail(f"report.scope_assessment[{index}].scope does not match brief.scope")
|
|
440
|
-
if item.get("status") not in {"covered", "inconclusive", "not-started"}:
|
|
441
|
-
_fail(f"report.scope_assessment[{index}].status is invalid")
|
|
442
|
-
_require_text(item.get("conclusion"), f"report.scope_assessment[{index}].conclusion")
|
|
443
|
-
assessment_evidence = _validate_id_list(
|
|
444
|
-
item.get("evidence_ids"), f"report.scope_assessment[{index}].evidence_ids"
|
|
445
|
-
)
|
|
446
|
-
if any(value not in evidence for value in assessment_evidence):
|
|
447
|
-
concerns.append("scope_assessment_evidence_unresolved")
|
|
448
|
-
if item["status"] != "covered":
|
|
449
|
-
concerns.append("incomplete_scope_coverage")
|
|
450
|
-
if item["status"] == "covered" and not assessment_evidence:
|
|
451
|
-
concerns.append("covered_scope_without_evidence")
|
|
452
|
-
findings = report.get("findings")
|
|
453
|
-
if not isinstance(findings, list):
|
|
454
|
-
_fail("report.findings must be a list")
|
|
455
|
-
finding_ids: set[str] = set()
|
|
456
|
-
for index, item in enumerate(findings):
|
|
457
|
-
if not isinstance(item, dict):
|
|
458
|
-
_fail(f"report.findings[{index}] must be an object")
|
|
459
|
-
finding_id = _require_id(item.get("id"), f"report.findings[{index}].id")
|
|
460
|
-
if finding_id in finding_ids:
|
|
461
|
-
_fail("report.findings contains duplicate ids")
|
|
462
|
-
finding_ids.add(finding_id)
|
|
463
|
-
_require_text(item.get("conclusion"), f"report.findings[{index}].conclusion")
|
|
464
|
-
finding_evidence = _validate_id_list(
|
|
465
|
-
item.get("evidence_ids"), f"report.findings[{index}].evidence_ids"
|
|
466
|
-
)
|
|
467
|
-
if not finding_evidence:
|
|
468
|
-
concerns.append("finding_without_evidence")
|
|
469
|
-
if any(value not in evidence for value in finding_evidence):
|
|
470
|
-
concerns.append("finding_evidence_unresolved")
|
|
471
|
-
_validate_typed_notes(report.get("uncertainties"), "uncertainties")
|
|
472
|
-
_validate_typed_notes(report.get("corrections"), "corrections")
|
|
473
|
-
if status == "complete" and not evidence:
|
|
474
|
-
_fail("a complete report requires at least one evidence item")
|
|
475
|
-
if status == "complete" and not findings:
|
|
476
|
-
_fail("a complete report requires at least one finding")
|
|
477
|
-
if status != "complete":
|
|
478
|
-
_require_text_list(
|
|
479
|
-
report.get("completed_scope"),
|
|
480
|
-
"report.completed_scope",
|
|
481
|
-
allow_empty=True,
|
|
482
|
-
)
|
|
483
|
-
_require_text(report.get("blocker"), "report.blocker")
|
|
484
|
-
return evidence, sorted(set(concerns))
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def _validate_report_file(
|
|
488
|
-
value: str,
|
|
489
|
-
repo_root: Path,
|
|
490
|
-
) -> tuple[dict[str, Any], set[str], dict[str, Any], Path, list[str]]:
|
|
491
|
-
report_path, node_dir, _task_dir = _resolve_artifact_file(value, "report.json", repo_root)
|
|
492
|
-
brief_path = node_dir / "brief.json"
|
|
493
|
-
brief, _brief_raw, _brief_path, _brief_node, _brief_task = _validate_brief_file(
|
|
494
|
-
str(brief_path), repo_root
|
|
495
|
-
)
|
|
496
|
-
expected_report = _relative_to_repo(report_path, repo_root)
|
|
497
|
-
if brief.get("report_path") != expected_report:
|
|
498
|
-
_fail("brief.report_path does not point to the report being validated")
|
|
499
|
-
report, raw = _read_json_object(report_path, MAX_REPORT_BYTES, "report")
|
|
500
|
-
_reject_obvious_secrets(raw, "report")
|
|
501
|
-
evidence, concerns = _validate_report_data(report, brief)
|
|
502
|
-
concerns.extend(_validate_checkpoint(node_dir, evidence, brief["scope"]))
|
|
503
|
-
return report, evidence, brief, node_dir, sorted(set(concerns))
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def _validate_disposition_data(
|
|
507
|
-
disposition: dict[str, Any],
|
|
508
|
-
report: dict[str, Any],
|
|
509
|
-
brief: dict[str, Any],
|
|
510
|
-
) -> None:
|
|
511
|
-
if disposition.get("schema_version") != SCHEMA_VERSION:
|
|
512
|
-
_fail(f"disposition.schema_version must be {SCHEMA_VERSION}")
|
|
513
|
-
for field in ("task_id", "work_id", "subnode_id", "role_id"):
|
|
514
|
-
if disposition.get(field) != brief.get(field):
|
|
515
|
-
_fail(f"disposition.{field} does not match brief.{field}")
|
|
516
|
-
if disposition.get("report_path") != brief.get("report_path"):
|
|
517
|
-
_fail("disposition.report_path does not match brief.report_path")
|
|
518
|
-
if disposition.get("report_status") != report.get("status"):
|
|
519
|
-
_fail("disposition.report_status does not match report.status")
|
|
520
|
-
if disposition.get("outcome") not in DISPOSITION_OUTCOMES:
|
|
521
|
-
_fail("disposition.outcome must be accepted, rejected, or deferred")
|
|
522
|
-
terminal = disposition.get("terminal")
|
|
523
|
-
if not isinstance(terminal, dict):
|
|
524
|
-
_fail("disposition.terminal must be an object")
|
|
525
|
-
if terminal.get("lifecycle") not in TERMINAL_LIFECYCLES:
|
|
526
|
-
_fail("disposition.terminal.lifecycle must be done, error, killed, or crashed")
|
|
527
|
-
sequence = terminal.get("seq")
|
|
528
|
-
if isinstance(sequence, bool) or not isinstance(sequence, int) or sequence < 1:
|
|
529
|
-
_fail("disposition.terminal.seq must be a positive integer")
|
|
530
|
-
_require_text(terminal.get("observed_at"), "disposition.terminal.observed_at", max_len=128)
|
|
531
|
-
checks = _require_text_list(disposition.get("checks"), "disposition.checks")
|
|
532
|
-
if len(checks) != len(set(checks)):
|
|
533
|
-
_fail("disposition.checks must not contain duplicates")
|
|
534
|
-
missing = REQUIRED_DISPOSITION_CHECKS.difference(checks)
|
|
535
|
-
if missing:
|
|
536
|
-
_fail("disposition.checks is missing: " + ", ".join(sorted(missing)))
|
|
537
|
-
_require_text(disposition.get("reason"), "disposition.reason")
|
|
538
|
-
_require_text(disposition.get("decided_at"), "disposition.decided_at", max_len=128)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
def _init(args: argparse.Namespace) -> None:
|
|
542
|
-
repo_root = get_repo_root()
|
|
543
|
-
task_dir = resolve_task_dir(args.task, repo_root)
|
|
544
|
-
if task_dir is None or not is_within_tasks_dir(task_dir, repo_root):
|
|
545
|
-
_fail("--task must identify a direct active task directory")
|
|
546
|
-
if task_dir.is_symlink():
|
|
547
|
-
_fail(f"refusing symlinked task directory: {task_dir}")
|
|
548
|
-
work_id = _require_id(args.work_id, "--work-id")
|
|
549
|
-
subnode_id = _require_id(args.subnode_id, "--subnode-id")
|
|
550
|
-
node_dir = _node_dir(task_dir, work_id, subnode_id)
|
|
551
|
-
_assert_no_subpath_symlinks(task_dir, node_dir)
|
|
552
|
-
if node_dir.exists() or node_dir.is_symlink():
|
|
553
|
-
_fail(f"subnode artifacts already exist: {node_dir}")
|
|
554
|
-
draft_path = Path(args.draft)
|
|
555
|
-
if not draft_path.is_absolute():
|
|
556
|
-
draft_path = repo_root / draft_path
|
|
557
|
-
draft, raw = _read_json_object(draft_path, MAX_DRAFT_BYTES, "brief draft")
|
|
558
|
-
_reject_obvious_secrets(raw, "brief draft")
|
|
559
|
-
expected_task_id = _task_id(task_dir)
|
|
560
|
-
brief = dict(draft)
|
|
561
|
-
brief["schema_version"] = SCHEMA_VERSION
|
|
562
|
-
brief["task_id"] = expected_task_id
|
|
563
|
-
brief["work_id"] = work_id
|
|
564
|
-
brief["subnode_id"] = subnode_id
|
|
565
|
-
brief["role_id"] = "subnode"
|
|
566
|
-
brief["report_path"] = _relative_to_repo(node_dir / "report.json", repo_root)
|
|
567
|
-
_validate_brief_data(brief, task_dir, node_dir, repo_root)
|
|
568
|
-
for directory in (task_dir / "subnodes", task_dir / "subnodes" / work_id, node_dir):
|
|
569
|
-
if directory.is_symlink():
|
|
570
|
-
_fail(f"refusing symlinked artifact directory: {directory}")
|
|
571
|
-
if directory.exists() and not directory.is_dir():
|
|
572
|
-
_fail(f"artifact path component is not a directory: {directory}")
|
|
573
|
-
try:
|
|
574
|
-
directory.mkdir(exist_ok=True)
|
|
575
|
-
except OSError as exc:
|
|
576
|
-
_fail(f"could not create artifact directory {directory}: {exc}")
|
|
577
|
-
brief_path = node_dir / "brief.json"
|
|
578
|
-
worklog_path = node_dir / "worklog.md"
|
|
579
|
-
brief_text = json.dumps(brief, indent=2, ensure_ascii=False) + "\n"
|
|
580
|
-
worklog_text = (
|
|
581
|
-
"# Subnode Worklog\n\n"
|
|
582
|
-
f"- Task: `{brief['task_id']}`\n"
|
|
583
|
-
f"- Work: `{work_id}`\n"
|
|
584
|
-
f"- Subnode: `{subnode_id}`\n\n"
|
|
585
|
-
"Append substantive observations, corrections, blockers, and completion notes below.\n"
|
|
586
|
-
)
|
|
587
|
-
if not write_text_atomic(brief_path, brief_text):
|
|
588
|
-
_fail(f"could not write immutable brief: {brief_path}")
|
|
589
|
-
if not write_text_atomic(worklog_path, worklog_text):
|
|
590
|
-
_fail(f"could not write subnode worklog: {worklog_path}")
|
|
591
|
-
print(json.dumps({
|
|
592
|
-
"brief": _relative_to_repo(brief_path, repo_root),
|
|
593
|
-
"worklog": _relative_to_repo(worklog_path, repo_root),
|
|
594
|
-
"report": brief["report_path"],
|
|
595
|
-
}))
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
def _validate(args: argparse.Namespace) -> None:
|
|
599
|
-
repo_root = get_repo_root()
|
|
600
|
-
report, _evidence, _brief, _node_dir_value, concerns = _validate_report_file(args.report, repo_root)
|
|
601
|
-
if concerns:
|
|
602
|
-
print(json.dumps({
|
|
603
|
-
"status": "review_concern",
|
|
604
|
-
"subnode_id": report["subnode_id"],
|
|
605
|
-
"concerns": concerns,
|
|
606
|
-
}))
|
|
607
|
-
return
|
|
608
|
-
print(f"Validated pending-review report: {report['subnode_id']}")
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
def _disposition(args: argparse.Namespace) -> None:
|
|
612
|
-
repo_root = get_repo_root()
|
|
613
|
-
report, _evidence, brief, node_dir, _concerns = _validate_report_file(args.report, repo_root)
|
|
614
|
-
disposition_path = node_dir / "disposition.json"
|
|
615
|
-
if disposition_path.exists() or disposition_path.is_symlink():
|
|
616
|
-
_fail(f"disposition already exists and cannot be replaced: {disposition_path}")
|
|
617
|
-
disposition = {
|
|
618
|
-
"schema_version": SCHEMA_VERSION,
|
|
619
|
-
"task_id": brief["task_id"],
|
|
620
|
-
"work_id": brief["work_id"],
|
|
621
|
-
"subnode_id": brief["subnode_id"],
|
|
622
|
-
"role_id": brief["role_id"],
|
|
623
|
-
"report_path": brief["report_path"],
|
|
624
|
-
"report_status": report["status"],
|
|
625
|
-
"outcome": args.outcome,
|
|
626
|
-
"terminal": {
|
|
627
|
-
"lifecycle": args.terminal_lifecycle,
|
|
628
|
-
"seq": args.terminal_seq,
|
|
629
|
-
"observed_at": args.terminal_at,
|
|
630
|
-
},
|
|
631
|
-
"checks": args.check,
|
|
632
|
-
"reason": args.reason,
|
|
633
|
-
"decided_at": datetime.now(timezone.utc).isoformat(),
|
|
634
|
-
}
|
|
635
|
-
_validate_disposition_data(disposition, report, brief)
|
|
636
|
-
try:
|
|
637
|
-
with disposition_path.open("x", encoding="utf-8") as stream:
|
|
638
|
-
json.dump(disposition, stream, indent=2, ensure_ascii=False)
|
|
639
|
-
stream.write("\n")
|
|
640
|
-
except FileExistsError:
|
|
641
|
-
_fail(f"disposition already exists and cannot be replaced: {disposition_path}")
|
|
642
|
-
except OSError as exc:
|
|
643
|
-
_fail(f"could not write disposition {disposition_path}: {exc}")
|
|
644
|
-
print(f"Recorded coordinator disposition: {brief['subnode_id']} -> {args.outcome}")
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def _validate_counter(args: argparse.Namespace) -> None:
|
|
648
|
-
repo_root = get_repo_root()
|
|
649
|
-
primary_dir = Path(args.primary)
|
|
650
|
-
counter_dir = Path(args.counter)
|
|
651
|
-
if not primary_dir.is_absolute():
|
|
652
|
-
primary_dir = repo_root / primary_dir
|
|
653
|
-
if not counter_dir.is_absolute():
|
|
654
|
-
counter_dir = repo_root / counter_dir
|
|
655
|
-
primary_report, primary_evidence, primary_brief, primary_node, _primary_concerns = _validate_report_file(
|
|
656
|
-
str(primary_dir / "report.json"), repo_root
|
|
657
|
-
)
|
|
658
|
-
counter_report, counter_evidence, counter_brief, counter_node, _counter_concerns = _validate_report_file(
|
|
659
|
-
str(counter_dir / "report.json"), repo_root
|
|
660
|
-
)
|
|
661
|
-
if primary_node == counter_node:
|
|
662
|
-
_fail("counter validation requires two different subnode directories")
|
|
663
|
-
if primary_brief["task_id"] != counter_brief["task_id"]:
|
|
664
|
-
_fail("counter subnode must belong to the same task")
|
|
665
|
-
if primary_brief["work_id"] != counter_brief["work_id"]:
|
|
666
|
-
_fail("counter subnode must belong to the same work_id")
|
|
667
|
-
if counter_brief.get("counter_of") != primary_brief["subnode_id"]:
|
|
668
|
-
_fail("counter brief.counter_of must identify the primary subnode")
|
|
669
|
-
if primary_brief["lens"] == counter_brief["lens"]:
|
|
670
|
-
_fail("counter subnode must use a different lens")
|
|
671
|
-
if primary_report["status"] != "complete" or counter_report["status"] != "complete":
|
|
672
|
-
_fail("counter comparison requires two complete reports")
|
|
673
|
-
if primary_evidence & counter_evidence:
|
|
674
|
-
_fail("counter report must use evidence identities independent of the primary report")
|
|
675
|
-
print(
|
|
676
|
-
"Validated independent counter reports: "
|
|
677
|
-
f"{primary_brief['subnode_id']} and {counter_brief['subnode_id']}"
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _parser() -> argparse.ArgumentParser:
|
|
682
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
683
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
684
|
-
|
|
685
|
-
init = subparsers.add_parser("init", help="create immutable brief and empty worklog")
|
|
686
|
-
init.add_argument("--task", required=True, help="direct active Trellis task directory")
|
|
687
|
-
init.add_argument("--work-id", required=True)
|
|
688
|
-
init.add_argument("--subnode-id", required=True)
|
|
689
|
-
init.add_argument("--draft", required=True, help="coordinator-owned brief draft JSON")
|
|
690
|
-
init.set_defaults(handler=_init)
|
|
691
|
-
|
|
692
|
-
validate = subparsers.add_parser("validate", help="validate a pending-review report")
|
|
693
|
-
validate.add_argument("--report", required=True)
|
|
694
|
-
validate.set_defaults(handler=_validate)
|
|
695
|
-
|
|
696
|
-
disposition = subparsers.add_parser(
|
|
697
|
-
"disposition",
|
|
698
|
-
help="record one coordinator disposition for a validated report",
|
|
699
|
-
)
|
|
700
|
-
disposition.add_argument("--report", required=True)
|
|
701
|
-
disposition.add_argument("--outcome", choices=sorted(DISPOSITION_OUTCOMES), required=True)
|
|
702
|
-
disposition.add_argument("--terminal-lifecycle", choices=sorted(TERMINAL_LIFECYCLES), required=True)
|
|
703
|
-
disposition.add_argument("--terminal-seq", type=int, required=True)
|
|
704
|
-
disposition.add_argument("--terminal-at", required=True)
|
|
705
|
-
disposition.add_argument(
|
|
706
|
-
"--check",
|
|
707
|
-
action="append",
|
|
708
|
-
required=True,
|
|
709
|
-
help="independent check name (repeat; requires report_validation, source_recheck, and protected_target_check)",
|
|
710
|
-
)
|
|
711
|
-
disposition.add_argument("--reason", required=True)
|
|
712
|
-
disposition.set_defaults(handler=_disposition)
|
|
713
|
-
|
|
714
|
-
counter = subparsers.add_parser(
|
|
715
|
-
"validate-counter",
|
|
716
|
-
help="validate two completed, independent subnode directories",
|
|
717
|
-
)
|
|
718
|
-
counter.add_argument("--primary", required=True, help="primary subnode artifact directory")
|
|
719
|
-
counter.add_argument("--counter", required=True, help="counter subnode artifact directory")
|
|
720
|
-
counter.set_defaults(handler=_validate_counter)
|
|
721
|
-
return parser
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
def main() -> int:
|
|
725
|
-
args = _parser().parse_args()
|
|
726
|
-
try:
|
|
727
|
-
args.handler(args)
|
|
728
|
-
except ArtifactError as exc:
|
|
729
|
-
print(f"Error: {exc}", file=sys.stderr)
|
|
730
|
-
return 1
|
|
731
|
-
return 0
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if __name__ == "__main__":
|
|
735
|
-
sys.exit(main())
|