deviatdd 2.5.1__py3-none-any.whl

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.
Files changed (124) hide show
  1. deviatdd-2.5.1.dist-info/METADATA +386 -0
  2. deviatdd-2.5.1.dist-info/RECORD +124 -0
  3. deviatdd-2.5.1.dist-info/WHEEL +4 -0
  4. deviatdd-2.5.1.dist-info/entry_points.txt +2 -0
  5. deviatdd-2.5.1.dist-info/licenses/LICENSE +21 -0
  6. deviate/__init__.py +3 -0
  7. deviate/cli/__init__.py +824 -0
  8. deviate/cli/_common.py +155 -0
  9. deviate/cli/adhoc.py +183 -0
  10. deviate/cli/constitution.py +113 -0
  11. deviate/cli/feature.py +94 -0
  12. deviate/cli/init.py +485 -0
  13. deviate/cli/inspect.py +208 -0
  14. deviate/cli/macro.py +937 -0
  15. deviate/cli/meso.py +1894 -0
  16. deviate/cli/micro.py +3249 -0
  17. deviate/cli/review.py +441 -0
  18. deviate/core/__init__.py +0 -0
  19. deviate/core/_shared.py +20 -0
  20. deviate/core/agent.py +505 -0
  21. deviate/core/cache_discipline.py +65 -0
  22. deviate/core/commands.py +202 -0
  23. deviate/core/commit.py +86 -0
  24. deviate/core/complexity.py +36 -0
  25. deviate/core/constitution.py +85 -0
  26. deviate/core/contract.py +17 -0
  27. deviate/core/convention.py +147 -0
  28. deviate/core/epic.py +73 -0
  29. deviate/core/issues.py +46 -0
  30. deviate/core/prd.py +18 -0
  31. deviate/core/profile.py +33 -0
  32. deviate/core/repo.py +47 -0
  33. deviate/core/run_logger.py +50 -0
  34. deviate/core/tasks_ledger.py +69 -0
  35. deviate/core/treesitter/__init__.py +31 -0
  36. deviate/core/treesitter/analysis.py +457 -0
  37. deviate/core/treesitter/models.py +35 -0
  38. deviate/core/treesitter/parser.py +184 -0
  39. deviate/core/treesitter/queries/bash.scm +7 -0
  40. deviate/core/treesitter/queries/c_sharp.scm +11 -0
  41. deviate/core/treesitter/queries/cpp.scm +9 -0
  42. deviate/core/treesitter/queries/css.scm +4 -0
  43. deviate/core/treesitter/queries/dockerfile.scm +8 -0
  44. deviate/core/treesitter/queries/elixir.scm +6 -0
  45. deviate/core/treesitter/queries/go.scm +9 -0
  46. deviate/core/treesitter/queries/hcl.scm +3 -0
  47. deviate/core/treesitter/queries/html.scm +3 -0
  48. deviate/core/treesitter/queries/javascript.scm +12 -0
  49. deviate/core/treesitter/queries/json.scm +4 -0
  50. deviate/core/treesitter/queries/kotlin.scm +8 -0
  51. deviate/core/treesitter/queries/markdown.scm +4 -0
  52. deviate/core/treesitter/queries/python.scm +10 -0
  53. deviate/core/treesitter/queries/rust.scm +12 -0
  54. deviate/core/treesitter/queries/sql.scm +7 -0
  55. deviate/core/treesitter/queries/swift.scm +10 -0
  56. deviate/core/treesitter/queries/toml.scm +3 -0
  57. deviate/core/treesitter/queries/tsx.scm +14 -0
  58. deviate/core/treesitter/queries/typescript.scm +14 -0
  59. deviate/core/treesitter/queries/yaml.scm +4 -0
  60. deviate/core/validation.py +153 -0
  61. deviate/core/worktree.py +141 -0
  62. deviate/main.py +4 -0
  63. deviate/prompts/__init__.py +0 -0
  64. deviate/prompts/assembly.py +122 -0
  65. deviate/prompts/auto/__init__.py +1 -0
  66. deviate/prompts/auto/execute.md +62 -0
  67. deviate/prompts/auto/explore.md +103 -0
  68. deviate/prompts/auto/green.md +137 -0
  69. deviate/prompts/auto/judge.md +260 -0
  70. deviate/prompts/auto/plan.md +127 -0
  71. deviate/prompts/auto/prd.md +108 -0
  72. deviate/prompts/auto/red.md +156 -0
  73. deviate/prompts/auto/refactor.md +166 -0
  74. deviate/prompts/auto/research.md +111 -0
  75. deviate/prompts/auto/shard.md +90 -0
  76. deviate/prompts/auto/specify.md +77 -0
  77. deviate/prompts/auto/tasks.md +191 -0
  78. deviate/prompts/commands/deviate-adhoc.md +216 -0
  79. deviate/prompts/commands/deviate-architecture.md +147 -0
  80. deviate/prompts/commands/deviate-constitution.md +215 -0
  81. deviate/prompts/commands/deviate-e2e.md +264 -0
  82. deviate/prompts/commands/deviate-execute.md +223 -0
  83. deviate/prompts/commands/deviate-explore.md +253 -0
  84. deviate/prompts/commands/deviate-flows.md +226 -0
  85. deviate/prompts/commands/deviate-green.md +217 -0
  86. deviate/prompts/commands/deviate-hotfix.md +188 -0
  87. deviate/prompts/commands/deviate-init.md +170 -0
  88. deviate/prompts/commands/deviate-judge.md +193 -0
  89. deviate/prompts/commands/deviate-merge.md +221 -0
  90. deviate/prompts/commands/deviate-plan.md +158 -0
  91. deviate/prompts/commands/deviate-pr.md +144 -0
  92. deviate/prompts/commands/deviate-prd.md +216 -0
  93. deviate/prompts/commands/deviate-prune.md +260 -0
  94. deviate/prompts/commands/deviate-red.md +231 -0
  95. deviate/prompts/commands/deviate-refactor.md +211 -0
  96. deviate/prompts/commands/deviate-release.md +123 -0
  97. deviate/prompts/commands/deviate-research.md +359 -0
  98. deviate/prompts/commands/deviate-review.md +289 -0
  99. deviate/prompts/commands/deviate-shard.md +226 -0
  100. deviate/prompts/commands/deviate-tasks.md +281 -0
  101. deviate/prompts/commands/deviate-triage.md +141 -0
  102. deviate/prompts/constitution_seed.md +51 -0
  103. deviate/prompts/core/core.md +21 -0
  104. deviate/prompts/core/macro-auto.md +59 -0
  105. deviate/prompts/core/macro-command.md +54 -0
  106. deviate/prompts/core/macro-skill.md +54 -0
  107. deviate/prompts/core/meso-auto.md +63 -0
  108. deviate/prompts/core/meso-command.md +58 -0
  109. deviate/prompts/core/meso-skill.md +58 -0
  110. deviate/prompts/core/micro-auto.md +76 -0
  111. deviate/prompts/core/micro-command.md +67 -0
  112. deviate/prompts/core/micro-skill.md +67 -0
  113. deviate/prompts/extras/deviate-pr-graphite-routing.md +20 -0
  114. deviate/prompts/governance/__init__.py +0 -0
  115. deviate/prompts/governance/agents_seed.md +1 -0
  116. deviate/prompts/governance/claudemd_seed.md +1 -0
  117. deviate/prompts/governance/graphite_seed.md +18 -0
  118. deviate/prompts/governance/libref_seed.md +3 -0
  119. deviate/state/__init__.py +0 -0
  120. deviate/state/config.py +257 -0
  121. deviate/state/ledger.py +407 -0
  122. deviate/ui/__init__.py +5 -0
  123. deviate/ui/monitor.py +187 -0
  124. deviate/ui/render.py +26 -0
@@ -0,0 +1,76 @@
1
+ <micro_layer_model>
2
+
3
+ NOTE: This differs from ``micro-skill.md`` — the ``-auto`` variants reference the CLI orchestrator,
4
+ while ``-skill`` variants reference the pre/post scripts directly, because skill prompts are invoked
5
+ by the agent directly (agent runs pre/post) while auto prompts are orchestrated by the CLI.
6
+
7
+ This phase operates inside the **MICRO LAYER** — the Red-Green-Refactor cycle for individual tasks.
8
+
9
+ <rgr_cycle>
10
+
11
+ Each task is a Logical Unit (30-90 min) that undergoes ONE complete R-G-R cycle:
12
+
13
+ <item>
14
+ **RED**: Write a failing test — verified to fail due to missing implementation, not syntax errors.
15
+ </item>
16
+
17
+ <item>
18
+ **GREEN**: Write the minimum production code to pass the test.
19
+ </item>
20
+
21
+ <item>
22
+ **REFACTOR**: Behavior-preserving structural cleanup without modifying tests.
23
+ </item>
24
+
25
+ </rgr_cycle>
26
+
27
+ <shared_disciplines>
28
+
29
+ <item>
30
+ <title>Test-First Discipline</title>
31
+ No production code is written before a failing test exists. Tests are the executable specification — the RED phase verifies the test fails before GREEN begins.
32
+ </item>
33
+
34
+ <item>
35
+ <title>Sociable Tests Over Solitary</title>
36
+ Prefer sociable (integration) tests that exercise real component orchestration. Restrict mocking exclusively to non-deterministic external networks, third-party transactional interfaces, or volatile system attributes (system epoch timers, cryptographic entropy paths).
37
+ </item>
38
+
39
+ <item>
40
+ <title>Verification-is-Done</title>
41
+ A task is ONLY finished when its `Verification` command passes. Verification is deterministic and scoped — run the specific test file, not the entire suite.
42
+ </item>
43
+
44
+ <item>
45
+ <title>Git Isolation</title>
46
+ Any test that invokes git operations MUST operate on an isolated temporary directory initialized as a fresh git repo. Tests MUST NOT run git commands within the real project repository. Use `create_temp_dir` → `git init` → copy fixtures → run test in that context.
47
+ </item>
48
+
49
+ <item>
50
+ <title>Orchestrator Lifecycle</title>
51
+ The CLI orchestrator handles all pre/post lifecycle — it injects context, stages files, runs pre-commit hooks, updates the task ledger, and commits. Do NOT run `deviate <phase> pre/post` or use `git add`/`git commit` directly.
52
+ </item>
53
+
54
+ <item>
55
+ <title>YAML Quoting Rule</title>
56
+ ALL string values in the handover manifest YAML MUST be wrapped in double quotes. A value containing a colon (`:`) will BREAK YAML parsing if unquoted.
57
+ </item>
58
+
59
+ <item>
60
+ <title>Flow-Anchored Implementation</title>
61
+ Micro phases are where Product-layer context is at the highest risk of being lost. Every micro phase MUST (a) read the active task's `**Flow References**` field from `tasks.md` before writing any code, (b) restate the user-visible flow(s) the task serves in the handover manifest under `flow_refs`, and (c) verify the test or implementation exercises behavior derivable from those flows — not implementation details detached from user intent. **judge** MUST add a `flow_alignment` rubric dimension alongside Spec Compliance: does the diff preserve or extend the flows named in the task's `**Flow References**`? A change that silently abandons or breaks a named flow MUST fail JUDGE with severity HIGH and a `train_feedback` block instructing the next GREEN attempt to re-anchor to the flow. **red** MUST write tests that describe user-visible behavior derivable from the parent flow's Trigger and Happy Path, not internal function signatures. **green** MUST implement the minimum production code to satisfy those flow-anchored tests, restricting scope to workstation files explicitly tied to the named flow. **refactor**, **yellow**, and **execute** inherit the same flow context and MUST NOT extend scope beyond what the named flow requires. If `tasks.md` carries `**Flow References**: []`, treat the task as enabling/infrastructure (no flow anchor required) but still surface the empty list in the handover manifest.
62
+ </item>
63
+
64
+ </shared_disciplines>
65
+
66
+ </micro_layer_model>
67
+
68
+ <mandate>
69
+ STDOUT OUTPUT MANDATE: Your final stdout response must be EXACTLY the YAML block from the `<handover_manifest>` section. No conversational text, no analysis, no commentary, no markdown formatting, no file content on stdout. Write artifact files to their target paths only (not to stdout). The caller parses your stdout as raw YAML.
70
+ </mandate>
71
+
72
+ <context>
73
+ <user_input>
74
+ $ARGUMENTS
75
+ </user_input>
76
+ </context>
@@ -0,0 +1,67 @@
1
+ <micro_layer_model>
2
+
3
+ This phase operates inside the **MICRO LAYER** — the Red-Green-Refactor cycle for individual tasks.
4
+
5
+ <rgr_cycle>
6
+
7
+ Each task is a Logical Unit (30-90 min) that undergoes ONE complete R-G-R cycle:
8
+
9
+ <item>
10
+ **RED**: Write a failing test — verified to fail due to missing implementation, not syntax errors.
11
+ </item>
12
+
13
+ <item>
14
+ **GREEN**: Write the minimum production code to pass the test.
15
+ </item>
16
+
17
+ <item>
18
+ **REFACTOR**: Behavior-preserving structural cleanup without modifying tests.
19
+ </item>
20
+
21
+ </rgr_cycle>
22
+
23
+ <shared_disciplines>
24
+
25
+ <item>
26
+ <title>Test-First Discipline</title>
27
+ No production code is written before a failing test exists. Tests are the executable specification — the RED phase verifies the test fails before GREEN begins.
28
+ </item>
29
+
30
+ <item>
31
+ <title>Sociable Tests Over Solitary</title>
32
+ Prefer sociable (integration) tests that exercise real component orchestration. Restrict mocking exclusively to non-deterministic external networks, third-party transactional interfaces, or volatile system attributes (system epoch timers, cryptographic entropy paths).
33
+ </item>
34
+
35
+ <item>
36
+ <title>Verification-is-Done</title>
37
+ A task is ONLY finished when its `Verification` command passes and the post-script commits successfully. Verification is deterministic and scoped — run the specific test file, not the entire suite.
38
+ </item>
39
+
40
+ <item>
41
+ <title>Git Isolation</title>
42
+ Any test that invokes git operations MUST operate on an isolated temporary directory initialized as a fresh git repo. Tests MUST NOT run git commands within the real project repository. Use `create_temp_dir` → `git init` → copy fixtures → run test in that context.
43
+ </item>
44
+
45
+ <item>
46
+ <title>Post-Script Protocol</title>
47
+ Every micro phase ends with `deviate <phase> post`. This is MANDATORY — do NOT use `git add` / `git commit` directly. The post-script stages files, runs pre-commit hooks (lint, format-check, tests), updates the task ledger, and commits. Allocate a timeout of at least 180s (3 minutes) for post-script execution.
48
+ </item>
49
+
50
+ <item>
51
+ <title>Handover Manifest YAML</title>
52
+ After post-script success, emit a handover manifest as a YAML code block. ALL string values MUST be wrapped in double quotes. A value containing a colon (`:`) will BREAK YAML parsing if unquoted. Output NOTHING outside the YAML block — no explanations, no commentary.
53
+ </item>
54
+
55
+ <item>
56
+ <title>Offline Documentation Guidance</title>
57
+ When implementing, use `libref query <library> <topic>` to look up library APIs and framework conventions. The `libref` CLI provides offline, version-pinned documentation — prefer it over web fetching. If `libref` is unavailable, fall back to training data or web fetch.
58
+ </item>
59
+
60
+ <item>
61
+ <title>Flow-Anchored Implementation</title>
62
+ Micro phases are where Product-layer context is at the highest risk of being lost. Every micro phase MUST (a) read the active task's `**Flow References**` field from `tasks.md` before writing any code, (b) restate the user-visible flow(s) the task serves in the handover manifest under `flow_refs`, and (c) verify the test or implementation exercises behavior derivable from those flows — not implementation details detached from user intent. **judge** MUST add a `flow_alignment` rubric dimension alongside Spec Compliance: does the diff preserve or extend the flows named in the task's `**Flow References**`? A change that silently abandons or breaks a named flow MUST fail JUDGE with severity HIGH and a `train_feedback` block instructing the next GREEN attempt to re-anchor to the flow. **red** MUST write tests that describe user-visible behavior derivable from the parent flow's Trigger and Happy Path, not internal function signatures. **green** MUST implement the minimum production code to satisfy those flow-anchored tests, restricting scope to workstation files explicitly tied to the named flow. **refactor**, **yellow**, and **execute** inherit the same flow context and MUST NOT extend scope beyond what the named flow requires. If `tasks.md` carries `**Flow References**: []`, treat the task as enabling/infrastructure (no flow anchor required) but still surface the empty list in the handover manifest.
63
+ </item>
64
+
65
+ </shared_disciplines>
66
+
67
+ </micro_layer_model>
@@ -0,0 +1,67 @@
1
+ <micro_layer_model>
2
+
3
+ This phase operates inside the **MICRO LAYER** — the Red-Green-Refactor cycle for individual tasks.
4
+
5
+ <rgr_cycle>
6
+
7
+ Each task is a Logical Unit (30-90 min) that undergoes ONE complete R-G-R cycle:
8
+
9
+ <item>
10
+ **RED**: Write a failing test — verified to fail due to missing implementation, not syntax errors.
11
+ </item>
12
+
13
+ <item>
14
+ **GREEN**: Write the minimum production code to pass the test.
15
+ </item>
16
+
17
+ <item>
18
+ **REFACTOR**: Behavior-preserving structural cleanup without modifying tests.
19
+ </item>
20
+
21
+ </rgr_cycle>
22
+
23
+ <shared_disciplines>
24
+
25
+ <item>
26
+ <title>Test-First Discipline</title>
27
+ No production code is written before a failing test exists. Tests are the executable specification — the RED phase verifies the test fails before GREEN begins.
28
+ </item>
29
+
30
+ <item>
31
+ <title>Sociable Tests Over Solitary</title>
32
+ Prefer sociable (integration) tests that exercise real component orchestration. Restrict mocking exclusively to non-deterministic external networks, third-party transactional interfaces, or volatile system attributes (system epoch timers, cryptographic entropy paths).
33
+ </item>
34
+
35
+ <item>
36
+ <title>Verification-is-Done</title>
37
+ A task is ONLY finished when its `Verification` command passes and the post-script commits successfully. Verification is deterministic and scoped — run the specific test file, not the entire suite.
38
+ </item>
39
+
40
+ <item>
41
+ <title>Git Isolation</title>
42
+ Any test that invokes git operations MUST operate on an isolated temporary directory initialized as a fresh git repo. Tests MUST NOT run git commands within the real project repository. Use `create_temp_dir` → `git init` → copy fixtures → run test in that context.
43
+ </item>
44
+
45
+ <item>
46
+ <title>Post-Script Protocol</title>
47
+ Every micro phase ends with `deviate <phase> post`. This is MANDATORY — do NOT use `git add` / `git commit` directly. The post-script stages files, runs pre-commit hooks (lint, format-check, tests), updates the task ledger, and commits. Allocate a timeout of at least 180s (3 minutes) for post-script execution.
48
+ </item>
49
+
50
+ <item>
51
+ <title>Handover Manifest YAML</title>
52
+ After post-script success, emit a handover manifest as a YAML code block. ALL string values MUST be wrapped in double quotes. A value containing a colon (`:`) will BREAK YAML parsing if unquoted. Output NOTHING outside the YAML block — no explanations, no commentary.
53
+ </item>
54
+
55
+ <item>
56
+ <title>Offline Documentation Guidance</title>
57
+ When implementing, use `libref query <library> <topic>` to look up library APIs and framework conventions. The `libref` CLI provides offline, version-pinned documentation — prefer it over web fetching. If `libref` is unavailable, fall back to training data or web fetch.
58
+ </item>
59
+
60
+ <item>
61
+ <title>Flow-Anchored Implementation</title>
62
+ Micro phases are where Product-layer context is at the highest risk of being lost. Every micro phase MUST (a) read the active task's `**Flow References**` field from `tasks.md` before writing any code, (b) restate the user-visible flow(s) the task serves in the handover manifest under `flow_refs`, and (c) verify the test or implementation exercises behavior derivable from those flows — not implementation details detached from user intent. **judge** MUST add a `flow_alignment` rubric dimension alongside Spec Compliance: does the diff preserve or extend the flows named in the task's `**Flow References**`? A change that silently abandons or breaks a named flow MUST fail JUDGE with severity HIGH and a `train_feedback` block instructing the next GREEN attempt to re-anchor to the flow. **red** MUST write tests that describe user-visible behavior derivable from the parent flow's Trigger and Happy Path, not internal function signatures. **green** MUST implement the minimum production code to satisfy those flow-anchored tests, restricting scope to workstation files explicitly tied to the named flow. **refactor**, **yellow**, and **execute** inherit the same flow context and MUST NOT extend scope beyond what the named flow requires. If `tasks.md` carries `**Flow References**: []`, treat the task as enabling/infrastructure (no flow anchor required) but still surface the empty list in the handover manifest.
63
+ </item>
64
+
65
+ </shared_disciplines>
66
+
67
+ </micro_layer_model>
@@ -0,0 +1,20 @@
1
+ <graphite_routing>
2
+ When Graphite is enabled (`.deviate/config.toml` has `graphite = true`), this skill routes PR operations through the Graphite CLI (`gt`) instead of the GitHub CLI (`gh`).
3
+
4
+ <pr_submission>
5
+ - Use `gt submit --stack --no-edit` to submit the entire stacked branch for review.
6
+ - After `gt submit --stack`, the `deviate pr run` command calls `_update_gt_prs()` in `src/deviate/cli/meso.py:959` which parses the submit output for PR URLs and runs `gh pr edit <number> --title <title> --body-file <path>` on each created PR. This is because `gt submit` has no `--title` or `--body-file` flag — `gh pr edit` post-submit is the only non-interactive fixup path.
7
+ - Do NOT invoke `gh pr create` directly — Graphite owns PR creation from stacked branches.
8
+ - The `--merge` and `--auto-merge` flags are incompatible with `gt submit --stack` and will be ignored at runtime; surface a warning if they were requested.
9
+ </pr_submission>
10
+
11
+ <branch_creation_between_tasks>
12
+ When `graphite = true` and the micro layer runs all tasks via `_run_all()` in `src/deviate/cli/micro.py:1854`, it calls `gt create -m "feat(<next_id>): <description>"` after each successful task (except the last) to create a stacked branch for the next task. This ensures each task lands on its own branch in the Graphite stack.
13
+ </branch_creation_between_tasks>
14
+
15
+ <anti_patterns>
16
+ - Do NOT call `git checkout -b` to create the issue branch — `gt create -am` was already used by the meso layer.
17
+ - Do NOT mix `gh` and `gt` commands in the same PR lifecycle — pick one based on the config flag.
18
+ - Do NOT skip the Graphite routing even if the user requests `gh pr create` manually — defer to the runtime config.
19
+ </anti_patterns>
20
+ </graphite_routing>
File without changes
@@ -0,0 +1 @@
1
+ <!-- Intentionally empty: the DeviaTDD Phase Architecture block was project-internal guidance that did not help consuming projects. Removed 2026-06-28. -->
@@ -0,0 +1 @@
1
+ <!-- Intentionally empty: the DeviaTDD Phase Architecture block was project-internal guidance that did not help consuming projects. Removed 2026-06-28. -->
@@ -0,0 +1,18 @@
1
+ ## Graphite Stacked Changes Workflow
2
+
3
+ When Graphite integration is enabled (`.deviate/config.toml` contains `graphite = true`):
4
+
5
+ ### Branch Creation
6
+ - Use `gt create -am "<message>"` to create a branch with an automatic commit
7
+ - If the working tree is clean, use `gt create -m "<message>"` instead
8
+
9
+ ### PR Submission
10
+ - Use `gt submit --stack` to submit the entire stack for review
11
+
12
+ ### Syncing
13
+ - Use `gt sync` to sync your stack with the remote
14
+
15
+ ### Anti-Patterns
16
+ - Do NOT use `git checkout -b` alongside `gt` — it bypasses Graphite's stack tracking
17
+ - Do NOT use `gh pr create` when Graphite is enabled — use `gt submit --stack`
18
+ - Do NOT run `gt` commands outside a Graphite-tracked repository
@@ -0,0 +1,3 @@
1
+ ## 📚 Offline Documentation (libref)
2
+
3
+ Prefer `libref query <lib> "<topic>"` over web fetching. Workflow: `libref list` → `libref query` → `libref add <git-url>` (register a missing source) → web fetch (last resort).
File without changes
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tomllib
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Literal, Optional
8
+
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+
12
+ class AgentConfig(BaseModel):
13
+ # Agent backend: "opencode", "claude", "droid", "pi", or "omp"
14
+ backend: Literal["opencode", "claude", "droid", "pi", "omp"] = "opencode"
15
+ # Agent invocation timeout in seconds (must be > 0)
16
+ timeout: int = Field(default=600, gt=0)
17
+ # Opt-in RPC mode for Pi — spawns `pi --mode rpc --no-session` instead of `pi -p`
18
+ pi_rpc: bool = Field(
19
+ default=False,
20
+ description="Opt-in RPC mode for Pi (spawns pi --mode rpc --no-session instead of pi -p)",
21
+ )
22
+
23
+ model_config = {"extra": "forbid"}
24
+
25
+
26
+ _VALID_PHASES = frozenset(
27
+ {
28
+ "IDLE",
29
+ "EXPLORE",
30
+ "RESEARCH",
31
+ "PRD",
32
+ "SHARD",
33
+ "SPECIFY",
34
+ "PLAN",
35
+ "TASKS",
36
+ "RED",
37
+ "GREEN",
38
+ "JUDGE",
39
+ "REFACTOR",
40
+ "E2E",
41
+ "EXECUTE",
42
+ "HOTFIX",
43
+ }
44
+ )
45
+
46
+ _PHASE_ARTIFACT_MAP: dict[str, tuple[str, ...]] = {
47
+ "RESEARCH": ("explore.md",),
48
+ "PRD": ("design.md", "data-model.md"),
49
+ "SHARD": ("prd.md",),
50
+ "SPECIFY": ("spec.md",),
51
+ "PLAN": ("plan.md",),
52
+ "TASKS": ("spec.md", "tasks.md"),
53
+ }
54
+
55
+
56
+ class TransitionViolationError(Exception):
57
+ pass
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Module-level utility functions (extracted from SessionState static methods)
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def validate_filesystem_state(
66
+ phase: str,
67
+ epic_slug: str | None,
68
+ repo_path: Path,
69
+ ) -> list[str]:
70
+ expected_artifacts = _PHASE_ARTIFACT_MAP.get(phase, ())
71
+ missing: list[str] = []
72
+ for artifact in expected_artifacts:
73
+ artifact_path = (
74
+ repo_path / "specs" / epic_slug / artifact
75
+ if epic_slug
76
+ else repo_path / artifact
77
+ )
78
+ if not artifact_path.exists():
79
+ missing.append(artifact)
80
+ return missing
81
+
82
+
83
+ def reconstruct_from_worktree(worktree: Path) -> SessionState:
84
+ has_spec = (worktree / "spec.md").exists()
85
+ has_plan = (worktree / "plan.md").exists()
86
+ has_tasks = (worktree / "tasks.md").exists()
87
+ if has_plan and has_tasks:
88
+ phase = "TASKS"
89
+ elif has_plan:
90
+ phase = "PLAN"
91
+ elif has_spec and has_tasks:
92
+ phase = "TASKS"
93
+ elif has_spec:
94
+ phase = "SPECIFY"
95
+ else:
96
+ phase = "IDLE"
97
+ return SessionState(current_phase=phase)
98
+
99
+
100
+ def normalize_task_id(ref: str) -> str:
101
+ return ref.rstrip(":")
102
+
103
+
104
+ class DeviateConfig(BaseModel):
105
+ # Profile name — defines preset config groups (default, full, fast, secure)
106
+ profile: str = "default"
107
+ # CLI inactivity timeout in seconds (must be > 0)
108
+ timeout_seconds: int = Field(default=300, gt=0)
109
+ # Agent export mode: "local" (project .claude/) or "global" (~/.claude/)
110
+ agent_export_mode: Literal["local", "global"] = "local"
111
+ # Agent backend config (opencode, claude, or droid)
112
+ agent: AgentConfig = Field(default_factory=AgentConfig)
113
+ # Per-phase model overrides, e.g. default = "opencode/deepseek-v4-flash"
114
+ models: dict[str, str] = Field(default_factory=dict)
115
+ # Enable the libref CLI for offline documentation lookups
116
+ use_libref: bool = False
117
+ # Enable Graphite CLI integration for stacked changes
118
+ graphite: bool = Field(default=False)
119
+
120
+ model_config = {"extra": "forbid"}
121
+
122
+
123
+ def _load_deviate_config_toml(root: Path) -> dict | None:
124
+ """Load the `.deviate/config.toml` file, returning a dict or None."""
125
+ config_path = root / ".deviate" / "config.toml"
126
+ if not config_path.exists():
127
+ return None
128
+ try:
129
+ with open(config_path, "rb") as f:
130
+ return tomllib.load(f)
131
+ except Exception:
132
+ return None
133
+
134
+
135
+ def resolve_phase_model(phase: str, models: dict[str, str]) -> str | None:
136
+ """Resolve the model ID for *phase* from a `[models]` dict.
137
+
138
+ Resolution order (case-insensitive):
139
+ 1. Phase-specific key (e.g. ``judge``, ``plan``, ``red``)
140
+ 2. ``default`` key
141
+ 3. ``None`` — backend falls back to its native default
142
+ """
143
+ if not models:
144
+ return None
145
+ phase_lower = phase.lower()
146
+ lookup = {k.lower(): val for k, val in models.items() if val}
147
+ if phase_lower in lookup:
148
+ return lookup[phase_lower]
149
+ if "default" in lookup:
150
+ return lookup["default"]
151
+ return None
152
+
153
+
154
+ def resolve_model_for_phase(phase: str, root: Path) -> str | None:
155
+ """Load `[models]` from `.deviate/config.toml` and resolve *phase*.
156
+
157
+ Backed by :func:`resolve_phase_model`. ``opencode`` and ``droid``
158
+ backends accept ``--model <id>``; the ``claude`` backend ignores the
159
+ resolved value silently.
160
+ """
161
+ data = _load_deviate_config_toml(root)
162
+ if data is None:
163
+ return None
164
+ models = data.get("models", {})
165
+ if not isinstance(models, dict):
166
+ return None
167
+ return resolve_phase_model(phase, {k: str(v) for k, v in models.items()})
168
+
169
+
170
+ def resolve_graphite_config(root: Path) -> bool:
171
+ """Check whether the Graphite integration is enabled in `.deviate/config.toml`."""
172
+ data = _load_deviate_config_toml(root)
173
+ if data is None:
174
+ return False
175
+ value = data.get("graphite", False)
176
+ return value if isinstance(value, bool) else False
177
+
178
+
179
+ class PytestReportConfig(BaseModel):
180
+ json_report: bool = False
181
+
182
+ model_config = {"extra": "forbid"}
183
+
184
+
185
+ class ProfileConfig(BaseModel):
186
+ default: Literal["full", "fast", "secure"] = "full"
187
+
188
+ model_config = {"extra": "forbid"}
189
+
190
+ def to_toml_string(self) -> str:
191
+ return 'default = "{}"\n'.format(self.default)
192
+
193
+
194
+ class SessionState(BaseModel):
195
+ current_phase: str = "IDLE"
196
+ active_issue_id: Optional[str] = None
197
+ last_command: str = ""
198
+ train_feedback: str = ""
199
+ judge_rejected: bool = False
200
+ red_commit_sha: str = ""
201
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
202
+
203
+ @field_validator("current_phase")
204
+ @classmethod
205
+ def _validate_phase(cls, v: str) -> str:
206
+ if v not in _VALID_PHASES:
207
+ valid = ", ".join(sorted(_VALID_PHASES))
208
+ raise ValueError(f"Invalid phase '{v}'. Must be one of: {valid}")
209
+ return v
210
+
211
+ def transition_to(self, phase: str) -> SessionState:
212
+ return SessionState(
213
+ current_phase=phase,
214
+ active_issue_id=self.active_issue_id,
215
+ last_command=self.last_command,
216
+ red_commit_sha=self.red_commit_sha,
217
+ timestamp=datetime.now(timezone.utc),
218
+ )
219
+
220
+ def force_transition_to(self, phase: str) -> SessionState:
221
+ return SessionState(
222
+ current_phase=phase,
223
+ active_issue_id=self.active_issue_id,
224
+ last_command=self.last_command,
225
+ red_commit_sha=self.red_commit_sha,
226
+ train_feedback=self.train_feedback,
227
+ judge_rejected=self.judge_rejected,
228
+ timestamp=datetime.now(timezone.utc),
229
+ )
230
+
231
+ def save(self, path: Path) -> None:
232
+ path.parent.mkdir(parents=True, exist_ok=True)
233
+ path.write_text(self.model_dump_json(indent=2), encoding="utf-8")
234
+
235
+ @classmethod
236
+ def load(cls, path: Path) -> SessionState:
237
+ if not path.exists():
238
+ return cls()
239
+ raw = path.read_text(encoding="utf-8")
240
+ data = json.loads(raw)
241
+ return cls.model_validate(data)
242
+
243
+ @staticmethod
244
+ def validate_filesystem_state(
245
+ phase: str,
246
+ epic_slug: str | None,
247
+ repo_path: Path,
248
+ ) -> list[str]:
249
+ return validate_filesystem_state(phase, epic_slug, repo_path)
250
+
251
+ @staticmethod
252
+ def reconstruct_from_worktree(worktree: Path) -> SessionState:
253
+ return reconstruct_from_worktree(worktree)
254
+
255
+ @staticmethod
256
+ def normalize_task_id(ref: str) -> str:
257
+ return normalize_task_id(ref)