prizmkit 1.1.114 → 1.1.116

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 (77) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/dev-pipeline/.env.example +0 -1
  3. package/bundled/dev-pipeline/README.md +4 -6
  4. package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +40 -518
  5. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +27 -99
  6. package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +283 -0
  7. package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -13
  8. package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -0
  9. package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +0 -1
  10. package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +14 -2
  11. package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +25 -31
  12. package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
  13. package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +0 -3
  14. package/bundled/dev-pipeline/prizmkit_runtime/runners.py +13 -7
  15. package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +13 -1
  16. package/bundled/dev-pipeline/scripts/continuation.py +8 -40
  17. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +76 -261
  18. package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +16 -35
  19. package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -34
  20. package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +6 -0
  21. package/bundled/dev-pipeline/scripts/init-pipeline.py +6 -0
  22. package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +6 -0
  23. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
  24. package/bundled/dev-pipeline/scripts/prompt_framework.py +114 -63
  25. package/bundled/dev-pipeline/scripts/update-bug-status.py +3 -7
  26. package/bundled/dev-pipeline/scripts/update-checkpoint.py +54 -22
  27. package/bundled/dev-pipeline/scripts/update-feature-status.py +3 -7
  28. package/bundled/dev-pipeline/scripts/update-refactor-status.py +3 -7
  29. package/bundled/dev-pipeline/scripts/utils.py +6 -37
  30. package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
  31. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +1 -1
  32. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +9 -91
  33. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -66
  34. package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +10 -16
  35. package/bundled/dev-pipeline/templates/feature-list-schema.json +32 -17
  36. package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +0 -16
  37. package/bundled/dev-pipeline/templates/refactor-list-schema.json +10 -16
  38. package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +1 -1
  39. package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
  40. package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
  41. package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
  42. package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
  43. package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
  44. package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +1 -1
  45. package/bundled/dev-pipeline/tests/conftest.py +1 -0
  46. package/bundled/dev-pipeline/tests/test_checkpoint_state.py +164 -0
  47. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +48 -76
  48. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +25 -3
  49. package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +25 -5
  50. package/bundled/dev-pipeline/tests/test_python_runner_parity.py +329 -16
  51. package/bundled/dev-pipeline/tests/test_unified_cli.py +40 -9
  52. package/bundled/skills/_metadata.json +1 -1
  53. package/bundled/skills/bug-planner/SKILL.md +5 -4
  54. package/bundled/skills/bug-planner/references/verification.md +29 -0
  55. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +8 -0
  56. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +2 -2
  57. package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +1 -6
  58. package/bundled/skills/feature-pipeline-launcher/SKILL.md +6 -12
  59. package/bundled/skills/feature-pipeline-launcher/references/configuration.md +0 -1
  60. package/bundled/skills/feature-planner/SKILL.md +13 -12
  61. package/bundled/skills/feature-planner/assets/planning-guide.md +2 -2
  62. package/bundled/skills/feature-planner/scripts/validate-and-generate.py +8 -11
  63. package/bundled/skills/prizmkit-plan/SKILL.md +15 -1
  64. package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +101 -0
  65. package/bundled/skills/refactor-pipeline-launcher/SKILL.md +5 -5
  66. package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +2 -7
  67. package/bundled/skills/refactor-planner/SKILL.md +10 -8
  68. package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +9 -0
  69. package/bundled/team/prizm-dev-team.json +2 -9
  70. package/package.json +1 -1
  71. package/bundled/agents/prizm-dev-team-critic.md +0 -179
  72. package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +0 -8
  73. package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +0 -22
  74. package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +0 -61
  75. package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +0 -38
  76. package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +0 -22
  77. package/bundled/skills/bug-planner/references/critic-and-verification.md +0 -44
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- dev-pipeline drives feature sessions through the unified Python outer loop. Each iteration spawns a new AI CLI session with a bootstrap prompt that makes the main session orchestrator responsible for implementation, while Critic and Reviewer agents remain available for challenge/review phases.
5
+ dev-pipeline drives feature sessions through the unified Python outer loop. Each iteration spawns a new AI CLI session with a bootstrap prompt that makes the main session orchestrator responsible for planning, implementation, review coordination, testing, retrospective, and commit.
6
6
 
7
7
  ## Architecture
8
8
 
@@ -10,31 +10,27 @@ dev-pipeline drives feature sessions through the unified Python outer loop. Each
10
10
  dev-pipeline (outer loop)
11
11
 
12
12
  ├── cli.py feature Python runtime CLI — picks next feature, spawns CLI
13
- ├── scripts/ Python state management scripts
14
- ├── templates/bootstrap-prompt.md Session prompt template
13
+ ├── scripts/ Python state and prompt management scripts
14
+ ├── templates/ Session prompt templates and sections
15
15
 
16
16
  └── [per session] AI CLI
17
17
 
18
- ├── Phase 0: Init (Orchestrator)
19
- ├── Phase 1-2: Context snapshot + Specify + Plan (Orchestrator)
20
- ├── Phase 3: Analyze (Reviewer agent) [tier2] / Phase 4: Analyze [tier3]
21
- ├── Phase 3.5: Plan Challenge (Critic agent(s), 1 or 3 parallel) [tier2+]
22
- ├── Phase 4: Implement (main orchestrator runs /prizmkit-implement directly)
23
- ├── Phase 5: Review (Reviewer agent)
24
- └── Phase 6: Retrospective & Commit (Orchestrator)
25
-
26
- └── Write session-status.json → exit
18
+ ├── Context snapshot + specify + plan (Orchestrator)
19
+ ├── Local plan/spec review loop inside prizmkit-plan
20
+ ├── Implement (main orchestrator runs /prizmkit-implement directly)
21
+ ├── Code review (/prizmkit-code-review via Reviewer)
22
+ ├── Scoped test gate and browser attempt
23
+ └── Retrospective & commit (Orchestrator)
27
24
  ```
28
25
 
29
26
  ## Agent Definitions (Source of Truth)
30
27
 
31
28
  | Agent | Definition Path | Type |
32
29
  |-------|----------------|------|
33
- | Dev | `.claude/agents/prizm-dev-team-dev.md` (or `.codebuddy/agents/`) | prizm-dev-team-dev; reserved for targeted fix/specialized handoffs, not top-level feature implementation |
34
- | Reviewer | `.claude/agents/prizm-dev-team-reviewer.md` (or `.codebuddy/agents/`) | prizm-dev-team-reviewer |
35
- | Critic | `.claude/agents/prizm-dev-team-critic.md` (or `.codebuddy/agents/`) | prizm-dev-team-critic |
30
+ | Dev | `.claude/agents/prizm-dev-team-dev.md` (or platform equivalent) | reserved for targeted specialized handoffs, not top-level feature implementation |
31
+ | Reviewer | `.claude/agents/prizm-dev-team-reviewer.md` (or platform equivalent) | code review quality gate |
36
32
 
37
- Note: The Orchestrator role is handled by the main agent (session orchestrator) directly — no separate agent definition needed.
33
+ The Orchestrator role is handled by the main agent directly — no separate agent definition needed.
38
34
 
39
35
  ## Pipeline Scripts
40
36
 
@@ -42,96 +38,28 @@ Located at `.prizmkit/dev-pipeline/scripts/`:
42
38
 
43
39
  | Script | Purpose |
44
40
  |--------|---------|
45
- | `init-dev-team.py` | Initialize `.dev-team/` + `.prizmkit/` directories |
46
- | `init-pipeline.py` | Initialize pipeline state directories and config |
41
+ | `init-pipeline.py` | Initialize feature pipeline state |
47
42
  | `init-bugfix-pipeline.py` | Initialize bugfix pipeline state |
48
- | `generate-bootstrap-prompt.py` | Render tier-specific bootstrap prompt with feature context |
43
+ | `init-refactor-pipeline.py` | Initialize refactor pipeline state |
44
+ | `generate-bootstrap-prompt.py` | Render feature bootstrap prompt with feature context |
49
45
  | `generate-bugfix-prompt.py` | Render bugfix bootstrap prompt with bug context |
50
- | `update-feature-status.py` | Update feature status in feature-list.json after session |
51
- | `update-bug-status.py` | Update bug status in bug-fix-list.json after session |
52
- | `check-session-status.py` | Read and validate session-status.json output |
53
- | `detect-stuck.py` | Detect stuck/hung pipeline sessions via heartbeat |
46
+ | `generate-refactor-prompt.py` | Render refactor bootstrap prompt with refactor context |
47
+ | `update-feature-status.py` | Update feature status after session |
48
+ | `update-bug-status.py` | Update bug status after session |
49
+ | `update-refactor-status.py` | Update refactor status after session |
50
+ | `check-session-status.py` | Read and validate session-status output |
51
+ | `detect-stuck.py` | Detect stuck or stale pipeline sessions |
54
52
  | `parse-stream-progress.py` | Parse AI CLI output stream for progress tracking |
55
53
  | `utils.py` | Shared utility functions for pipeline scripts |
56
54
 
57
- ## Artifact Mapping
58
-
59
- ### PrizmKit Artifacts (.prizmkit/)
60
-
61
- | Phase | File | Content |
62
- |-------|------|---------|
63
- | 1 | `specs/spec.md` | Feature specification (WHAT/WHY) |
64
- | 2 | `plans/plan.md` | Technical plan (architecture, API, tests) |
65
- | 3 | `tasks/tasks.md` | Executable task list with `[ ]` / `[x]` (legacy — now part of plan.md Tasks section) |
66
- | 4 | `analysis/analyze-report.md` | Consistency analysis |
67
-
68
- ### Dev-Team Artifacts (.dev-team/)
69
-
70
- | Phase | File | Content |
71
- |-------|------|---------|
72
- | 1 | `specs/requirements.md` | Requirements with REQ-NNN IDs |
73
- | 2 | `contracts/*.contract.json` | Interface contracts |
74
- | 2 | `contracts/data-models.json` | Data entity definitions |
75
- | 3 | `tasks/task-manifest.json` | Structured tasks with T-NNN IDs |
76
- | 3 | `tasks/dependency-graph.json` | Task DAG |
77
- | 6 | `reports/dev/self-test-*.md` | Dev self-test reports |
78
- | 7 | `reports/qa/integration-test.md` | QA integration test report |
79
- | 7 | `reports/review/code-review.md` | Review report |
80
-
81
55
  ## Session Lifecycle
82
56
 
83
- ### 1. Session Start
84
-
85
- The bootstrap prompt instructs the agent to:
86
- - Execute phases directly as the session orchestrator
87
- - Spawn Dev and Reviewer agents as subagents for implementation and review phases
88
- - The orchestrator handles context building, planning, retrospective, and commit phases directly
89
-
90
- ### 2. Pipeline Execution
91
-
92
- The Orchestrator drives the pipeline phases with checkpoints (CP-0 through CP-3). Each checkpoint validates artifacts.
93
-
94
- ### 3. Session End
95
-
96
- The agent MUST write `session-status.json` before exiting:
97
-
98
- ```json
99
- {
100
- "session_id": "F-001-20260304100000",
101
- "feature_id": "F-001",
102
- "status": "success",
103
- "completed_phases": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
104
- "current_phase": 9,
105
- "checkpoint_reached": "CP-7",
106
- "tasks_completed": 12,
107
- "tasks_total": 12,
108
- "errors": [],
109
- "can_resume": false,
110
- "resume_from_phase": null,
111
- "artifacts": {
112
- "spec_path": ".prizmkit/specs/spec.md",
113
- "plan_path": ".prizmkit/plans/plan.md"
114
- },
115
- "timestamp": "2026-03-04T11:30:00Z"
116
- }
117
- ```
57
+ 1. Runtime selects the next eligible item from the plan list.
58
+ 2. Prompt generator renders a session prompt and checkpoint file.
59
+ 3. AI CLI session executes the generated prompt in the active project checkout or runner-owned worktree.
60
+ 4. Session writes status and completion artifacts.
61
+ 5. Runtime updates plan-list status and propagates completion notes.
118
62
 
119
63
  ## Failure Recovery
120
64
 
121
- | Session Outcome | Pipeline Action |
122
- |-----------------|-----------------|
123
- | `status: "success"` | Mark feature completed, pick next |
124
- | `status: "partial"`, `can_resume: true` | Resume from `resume_from_phase` |
125
- | `status: "partial"`, `can_resume: false` | Retry from scratch |
126
- | `status: "failed"` | Retry (up to MAX_RETRIES) |
127
- | No status file (crash) | Treat as failed, retry |
128
-
129
- ## Team Naming Convention
130
-
131
- Each feature gets its own team instance: `prizm-dev-team-{FEATURE_ID}`
132
-
133
- Examples:
134
- - `prizm-dev-team-F-001` — User Authentication
135
- - `prizm-dev-team-F-002` — Dashboard
136
-
137
- This ensures clean isolation between features and allows multiple features to have separate artifact histories.
65
+ The generated checkpoint file is the recovery truth. Continuation prompts resume from the first pending or in-progress checkpoint step and prefer synthesized artifacts over full log rereads.
@@ -0,0 +1,283 @@
1
+ """Shared workflow-checkpoint.json reader and validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Mapping
10
+
11
+ VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
12
+ TERMINAL_STEP_STATUSES = {"completed", "skipped"}
13
+ STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class CheckpointStep:
18
+ """Validated workflow checkpoint step."""
19
+
20
+ id: str
21
+ skill: str
22
+ name: str
23
+ status: str
24
+ depends_on: str | None = None
25
+ required_artifacts: tuple[str, ...] = ()
26
+ index: int = 0
27
+
28
+ @property
29
+ def number(self) -> int:
30
+ """Return the numeric cursor component from an SNN step ID."""
31
+ return int(self.id[1:])
32
+
33
+ @property
34
+ def label(self) -> str:
35
+ """Return a compact human label for cursor summaries."""
36
+ return f"{self.id} ({self.skill})"
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class CheckpointState:
41
+ """Result of loading and validating one workflow checkpoint."""
42
+
43
+ path: Path | None
44
+ exists: bool
45
+ valid: bool
46
+ error_code: str = ""
47
+ error_message: str = ""
48
+ steps: tuple[CheckpointStep, ...] = ()
49
+ current_step: CheckpointStep | None = None
50
+ all_completed: bool = False
51
+ duplicate_skills: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
52
+ raw_data: Mapping[str, Any] | None = None
53
+
54
+ @property
55
+ def current_step_name(self) -> str:
56
+ """Return the user-facing phase label, or an empty string when unavailable."""
57
+ return self.current_step.name if self.valid and self.current_step is not None else ""
58
+
59
+ @property
60
+ def cursor(self) -> int | None:
61
+ """Return the current numeric cursor for valid checkpoints."""
62
+ if not self.valid:
63
+ return None
64
+ if self.current_step is not None:
65
+ return self.current_step.number
66
+ if self.steps:
67
+ return self.steps[-1].number
68
+ return None
69
+
70
+ @property
71
+ def cursor_signature(self) -> str:
72
+ """Return a stable progress fingerprint cursor string."""
73
+ if not self.valid:
74
+ return f"invalid:{self.error_code}" if self.exists else ""
75
+ if self.all_completed:
76
+ return "complete"
77
+ if self.current_step is None:
78
+ return ""
79
+ return f"{self.current_step.id}:{self.current_step.skill}:{self.current_step.status}"
80
+
81
+
82
+ def load_checkpoint_state(path: str | Path | None) -> CheckpointState:
83
+ """Load and validate one workflow-checkpoint.json file."""
84
+ checkpoint_path = Path(path) if path is not None else None
85
+ if checkpoint_path is None or not checkpoint_path.is_file():
86
+ return _invalid(checkpoint_path, False, "missing", "Checkpoint file not found")
87
+ try:
88
+ data = json.loads(checkpoint_path.read_text(encoding="utf-8"))
89
+ except json.JSONDecodeError as exc:
90
+ return _invalid(checkpoint_path, True, "malformed_json", f"Malformed checkpoint JSON: {exc}")
91
+ except OSError as exc:
92
+ return _invalid(checkpoint_path, True, "read_error", f"Cannot read checkpoint: {exc}")
93
+ return validate_checkpoint_data(data, path=checkpoint_path, exists=True)
94
+
95
+
96
+ def validate_checkpoint_data(
97
+ data: Any,
98
+ *,
99
+ path: str | Path | None = None,
100
+ exists: bool = True,
101
+ ) -> CheckpointState:
102
+ """Validate already-loaded checkpoint data."""
103
+ checkpoint_path = Path(path) if path is not None else None
104
+ if not isinstance(data, dict):
105
+ return _invalid(checkpoint_path, exists, "invalid_root", "Checkpoint root must be an object")
106
+
107
+ raw_steps = data.get("steps")
108
+ if raw_steps is None:
109
+ return _invalid(checkpoint_path, exists, "missing_steps", "Checkpoint is missing a steps array", data)
110
+ if not isinstance(raw_steps, list):
111
+ return _invalid(checkpoint_path, exists, "invalid_steps", "Checkpoint steps must be an array", data)
112
+ if not raw_steps:
113
+ return _invalid(checkpoint_path, exists, "empty_steps", "Checkpoint steps array must not be empty", data)
114
+
115
+ seen_ids: dict[str, int] = {}
116
+ steps: list[CheckpointStep] = []
117
+ skill_to_ids: dict[str, list[str]] = {}
118
+ pending_depends_on: list[tuple[str, str]] = []
119
+
120
+ for index, raw_step in enumerate(raw_steps):
121
+ if not isinstance(raw_step, dict):
122
+ return _invalid(checkpoint_path, exists, "invalid_step", f"Step at index {index} must be an object", data)
123
+
124
+ step_id = raw_step.get("id")
125
+ if not isinstance(step_id, str) or not STEP_ID_PATTERN.match(step_id):
126
+ return _invalid(checkpoint_path, exists, "malformed_step_id", f"Step at index {index} has malformed id", data)
127
+ if step_id in seen_ids:
128
+ return _invalid(checkpoint_path, exists, "duplicate_step_id", f"Duplicate step id: {step_id}", data)
129
+ seen_ids[step_id] = index
130
+
131
+ skill = raw_step.get("skill")
132
+ if not isinstance(skill, str) or not skill.strip():
133
+ return _invalid(checkpoint_path, exists, "invalid_skill", f"Step {step_id} has invalid skill", data)
134
+ skill = skill.strip()
135
+
136
+ name = raw_step.get("name")
137
+ if not isinstance(name, str) or not name.strip():
138
+ return _invalid(checkpoint_path, exists, "invalid_step_name", f"Step {step_id} has invalid name", data)
139
+ name = name.strip()
140
+
141
+ status = raw_step.get("status")
142
+ if status not in VALID_CHECKPOINT_STATUSES:
143
+ return _invalid(checkpoint_path, exists, "invalid_status", f"Step {step_id} has invalid status", data)
144
+
145
+ depends_on = raw_step.get("depends_on")
146
+ if depends_on is not None:
147
+ if not isinstance(depends_on, str) or not STEP_ID_PATTERN.match(depends_on):
148
+ return _invalid(checkpoint_path, exists, "invalid_depends_on", f"Step {step_id} has invalid depends_on", data)
149
+ if depends_on == step_id:
150
+ return _invalid(checkpoint_path, exists, "invalid_depends_on", f"Step {step_id} depends on itself", data)
151
+ pending_depends_on.append((step_id, depends_on))
152
+
153
+ artifacts = raw_step.get("required_artifacts", [])
154
+ if artifacts is None:
155
+ artifacts = []
156
+ if not isinstance(artifacts, list) or any(not isinstance(item, str) for item in artifacts):
157
+ return _invalid(checkpoint_path, exists, "invalid_required_artifacts", f"Step {step_id} has invalid required_artifacts", data)
158
+
159
+ step = CheckpointStep(
160
+ id=step_id,
161
+ skill=skill,
162
+ name=name,
163
+ status=str(status),
164
+ depends_on=depends_on,
165
+ required_artifacts=tuple(artifacts),
166
+ index=index,
167
+ )
168
+ steps.append(step)
169
+ skill_to_ids.setdefault(skill, []).append(step_id)
170
+
171
+ for step_id, dependency_id in pending_depends_on:
172
+ if dependency_id not in seen_ids:
173
+ return _invalid(
174
+ checkpoint_path,
175
+ exists,
176
+ "invalid_depends_on",
177
+ f"Step {step_id} depends on missing step {dependency_id}",
178
+ data,
179
+ )
180
+ if seen_ids[dependency_id] >= seen_ids[step_id]:
181
+ return _invalid(
182
+ checkpoint_path,
183
+ exists,
184
+ "invalid_depends_on",
185
+ f"Step {step_id} depends on non-prior step {dependency_id}",
186
+ data,
187
+ )
188
+
189
+ duplicate_skills = {
190
+ skill: tuple(ids)
191
+ for skill, ids in skill_to_ids.items()
192
+ if len(ids) > 1
193
+ }
194
+ current_step = next((step for step in steps if step.status not in TERMINAL_STEP_STATUSES), None)
195
+ return CheckpointState(
196
+ path=checkpoint_path,
197
+ exists=exists,
198
+ valid=True,
199
+ steps=tuple(steps),
200
+ current_step=current_step,
201
+ all_completed=current_step is None,
202
+ duplicate_skills=duplicate_skills,
203
+ raw_data=data,
204
+ )
205
+
206
+
207
+ def read_checkpoint_cursor(path: str | Path | None) -> int | None:
208
+ """Read the numeric cursor from a valid workflow checkpoint."""
209
+ return load_checkpoint_state(path).cursor
210
+
211
+
212
+ def checkpoint_cursor(checkpoint: Mapping[str, Any] | None) -> dict[str, object]:
213
+ """Summarize checkpoint step groups and the first active cursor."""
214
+ state = validate_checkpoint_data(checkpoint or {}, exists=checkpoint is not None)
215
+ if not state.valid:
216
+ return {
217
+ "valid": False,
218
+ "error_code": state.error_code,
219
+ "error_message": state.error_message,
220
+ "completed": [],
221
+ "skipped": [],
222
+ "in_progress": [],
223
+ "pending": [],
224
+ "failed": [],
225
+ "first_cursor": f"(invalid checkpoint: {state.error_code})",
226
+ "current_step_name": "",
227
+ }
228
+
229
+ grouped: dict[str, list[str]] = {
230
+ "completed": [],
231
+ "skipped": [],
232
+ "in_progress": [],
233
+ "pending": [],
234
+ "failed": [],
235
+ }
236
+ first_cursor = "(all checkpoint steps completed or skipped)"
237
+ for step in state.steps:
238
+ grouped[step.status].append(step.label)
239
+ if step is state.current_step:
240
+ first_cursor = step.label
241
+
242
+ return {
243
+ "valid": True,
244
+ "error_code": "",
245
+ "error_message": "",
246
+ "completed": grouped["completed"],
247
+ "skipped": grouped["skipped"],
248
+ "in_progress": grouped["in_progress"],
249
+ "pending": grouped["pending"],
250
+ "failed": grouped["failed"],
251
+ "first_cursor": first_cursor,
252
+ "current_step_name": state.current_step_name,
253
+ }
254
+
255
+
256
+ def completed_workflow_state(path: str | Path | None) -> CheckpointState:
257
+ """Return checkpoint state for completed-workflow evidence checks."""
258
+ return load_checkpoint_state(path)
259
+
260
+
261
+ def invalid_existing_checkpoint(path: str | Path | None) -> CheckpointState | None:
262
+ """Return invalid state only when a checkpoint file exists but is invalid."""
263
+ state = load_checkpoint_state(path)
264
+ if state.exists and not state.valid:
265
+ return state
266
+ return None
267
+
268
+
269
+ def _invalid(
270
+ path: Path | None,
271
+ exists: bool,
272
+ code: str,
273
+ message: str,
274
+ raw_data: Mapping[str, Any] | None = None,
275
+ ) -> CheckpointState:
276
+ return CheckpointState(
277
+ path=path,
278
+ exists=exists,
279
+ valid=False,
280
+ error_code=code,
281
+ error_message=message,
282
+ raw_data=raw_data,
283
+ )
@@ -39,7 +39,6 @@ class DaemonStartOptions:
39
39
  env_overrides: str = ""
40
40
  mode: str = ""
41
41
  features_filter: str = ""
42
- critic: bool | None = None
43
42
  help_requested: bool = False
44
43
 
45
44
 
@@ -247,7 +246,6 @@ def _parse_start_args(family: RunnerFamily, legacy_args: tuple[str, ...]) -> Dae
247
246
  env_overrides = ""
248
247
  mode = ""
249
248
  features_filter = ""
250
- critic: bool | None = None
251
249
  help_requested = False
252
250
  positional: list[str] = []
253
251
  index = 0
@@ -266,16 +264,14 @@ def _parse_start_args(family: RunnerFamily, legacy_args: tuple[str, ...]) -> Dae
266
264
  elif arg == "--features" and index + 1 < len(legacy_args):
267
265
  index += 1
268
266
  features_filter = legacy_args[index]
269
- elif arg == "--critic":
270
- critic = True
271
- elif arg == "--no-critic":
272
- critic = False
267
+ elif arg.startswith("-"):
268
+ raise ValueError("Unsupported option: {}".format(arg))
273
269
  else:
274
270
  positional.append(arg)
275
271
  index += 1
276
272
  if positional:
277
273
  list_path = Path(positional[0]).expanduser()
278
- return DaemonStartOptions(list_path=list_path, env_overrides=env_overrides, mode=mode, features_filter=features_filter, critic=critic, help_requested=help_requested)
274
+ return DaemonStartOptions(list_path=list_path, env_overrides=env_overrides, mode=mode, features_filter=features_filter, help_requested=help_requested)
279
275
 
280
276
 
281
277
  def _parse_log_args(legacy_args: tuple[str, ...]) -> DaemonLogOptions:
@@ -312,10 +308,6 @@ def _payload_command(family: RunnerFamily, paths, list_path: Path, options: Daem
312
308
  command.extend(["--features", options.features_filter])
313
309
  if options.mode:
314
310
  command.extend(["--mode", options.mode])
315
- if options.critic is True:
316
- command.append("--critic")
317
- elif options.critic is False:
318
- command.append("--no-critic")
319
311
  return command
320
312
 
321
313
 
@@ -328,8 +320,6 @@ def _daemon_env(options: DaemonStartOptions) -> dict[str, str]:
328
320
  env[key] = value
329
321
  if options.mode:
330
322
  env["PIPELINE_MODE"] = options.mode
331
- if options.critic is not None:
332
- env["ENABLE_CRITIC"] = "true" if options.critic else "false"
333
323
  return env
334
324
 
335
325
 
@@ -31,6 +31,8 @@ HIDDEN_TOOL_WORKTREE_EXCLUDES = (
31
31
  ":(top,exclude,glob).prizmkit/prizm-docs/**",
32
32
  ":(top,exclude).prizmkit/config.json",
33
33
  ":(top,exclude).prizmkit/manifest.json",
34
+ ":(top,exclude).prizmkit/state",
35
+ ":(top,exclude,glob).prizmkit/state/**",
34
36
  ":(top,exclude).prizmkit/dev-pipeline/scripts",
35
37
  ":(top,exclude,glob).prizmkit/dev-pipeline/scripts/**",
36
38
  ":(top,exclude).prizmkit/dev-pipeline/prizmkit_runtime",
@@ -69,7 +69,6 @@ def _progress_signature(path: Path | None) -> tuple[str, str]:
69
69
  child_signature = str(data.get("child_activity_signature") or "")
70
70
  signature = json.dumps(
71
71
  [
72
- data.get("current_phase") or "",
73
72
  data.get("current_tool") or "",
74
73
  int(data.get("message_count") or 0),
75
74
  int(data.get("total_tool_calls") or 0),
@@ -42,6 +42,10 @@ def _repo_path(project_root: Path, path: Path) -> str:
42
42
  return path.as_posix()
43
43
 
44
44
 
45
+ def _visible_status_result(project_root: Path) -> GitCommandResult:
46
+ return run_git_command(project_root, ("status", "--porcelain", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
47
+
48
+
45
49
  def _changed_paths_in(project_root: Path, paths: Sequence[str]) -> tuple[str, ...]:
46
50
  result = run_git_command(project_root, ("status", "--porcelain", "--", *paths))
47
51
  if result.return_code != 0:
@@ -73,7 +77,10 @@ def commit_bookkeeping_changes(
73
77
 
74
78
  def commit_preflight_ready_changes(project_root: Path, item_id: str) -> BookkeepingResult:
75
79
  """Preserve visible pre-existing changes before task branch setup."""
76
- if not git_status_safe(project_root).strip():
80
+ visible_status = _visible_status_result(project_root)
81
+ if visible_status.return_code != 0:
82
+ return BookkeepingResult(attempted=True, committed=False, result=visible_status)
83
+ if not visible_status.stdout.strip():
77
84
  return BookkeepingResult(attempted=False, committed=False)
78
85
  add_result = run_git_command(project_root, ("add", "-A", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES))
79
86
  if add_result.return_code != 0:
@@ -82,7 +89,12 @@ def commit_preflight_ready_changes(project_root: Path, item_id: str) -> Bookkeep
82
89
  project_root,
83
90
  ("commit", "--no-verify", "-m", f"chore: ready for {item_id}", "--", ".", *HIDDEN_TOOL_WORKTREE_EXCLUDES),
84
91
  )
85
- return BookkeepingResult(attempted=True, committed=result.return_code == 0, result=result)
92
+ if result.return_code != 0:
93
+ return BookkeepingResult(attempted=True, committed=False, result=result)
94
+ clean_result = _visible_status_result(project_root)
95
+ if clean_result.return_code != 0 or clean_result.stdout.strip():
96
+ return BookkeepingResult(attempted=True, committed=False, result=clean_result)
97
+ return BookkeepingResult(attempted=True, committed=True, result=result)
86
98
 
87
99
 
88
100
  def commit_task_branch_changes(
@@ -3,10 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import hashlib
6
- import json
7
6
  import subprocess
8
7
  from pathlib import Path
9
8
 
9
+ from .checkpoint_state import load_checkpoint_state
10
10
  from .gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES, git_status_safe, run_git_command
11
11
  from .runner_models import PromptGenerationResult, SessionClassification
12
12
  from .sessions import AISessionResult
@@ -47,6 +47,13 @@ def classify_session(
47
47
  ) -> SessionClassification:
48
48
  """Classify raw AI session signals with durable success precedence."""
49
49
  fingerprint = progress_fingerprint(project_root, prompt)
50
+ invalid_checkpoint = _invalid_existing_workflow_checkpoint(prompt)
51
+ if invalid_checkpoint is not None:
52
+ return SessionClassification(
53
+ "crashed",
54
+ reason=f"invalid_workflow_checkpoint:{invalid_checkpoint.error_code}",
55
+ progress_fingerprint=fingerprint,
56
+ )
50
57
  has_commit_since_base = _has_commit_since(project_root, base_head)
51
58
 
52
59
  if _is_terminal_success(result) and _has_completed_workflow_artifact(prompt):
@@ -108,11 +115,7 @@ def progress_fingerprint(project_root: Path, prompt: PromptGenerationResult | No
108
115
  if prompt and prompt.artifact_path and prompt.artifact_path.exists():
109
116
  artifact_hash = _hash_tree(prompt.artifact_path)
110
117
  if prompt and prompt.checkpoint_path and prompt.checkpoint_path.is_file():
111
- try:
112
- data = json.loads(prompt.checkpoint_path.read_text(encoding="utf-8"))
113
- checkpoint_cursor = _checkpoint_cursor(data)
114
- except (OSError, json.JSONDecodeError):
115
- checkpoint_cursor = "unreadable"
118
+ checkpoint_cursor = load_checkpoint_state(prompt.checkpoint_path).cursor_signature
116
119
  return {
117
120
  "git_diff_fingerprint": hashlib.sha256(status_text.encode()).hexdigest(),
118
121
  "checkpoint_cursor": checkpoint_cursor,
@@ -146,12 +149,8 @@ def _has_completed_workflow_artifact(prompt: PromptGenerationResult | None) -> b
146
149
  if prompt is None:
147
150
  return False
148
151
  if prompt.checkpoint_path and prompt.checkpoint_path.is_file():
149
- try:
150
- data = json.loads(prompt.checkpoint_path.read_text(encoding="utf-8"))
151
- except (OSError, json.JSONDecodeError):
152
- data = {}
153
- if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
154
- return True
152
+ state = load_checkpoint_state(prompt.checkpoint_path)
153
+ return state.valid and state.all_completed
155
154
  if prompt.artifact_path and has_completed_artifact_path(prompt.artifact_path):
156
155
  return True
157
156
  return False
@@ -169,15 +168,23 @@ def has_completed_artifact_path(artifact_path: Path | None) -> bool:
169
168
  return False
170
169
  checkpoint = artifact_path / "workflow-checkpoint.json"
171
170
  if checkpoint.is_file():
172
- try:
173
- data = json.loads(checkpoint.read_text(encoding="utf-8"))
174
- except (OSError, json.JSONDecodeError):
175
- data = {}
176
- if isinstance(data, dict) and _checkpoint_cursor(data) == "complete":
177
- return True
171
+ state = load_checkpoint_state(checkpoint)
172
+ return state.valid and state.all_completed
178
173
  return (artifact_path / "completion-summary.json").is_file()
179
174
 
180
175
 
176
+ def _invalid_existing_workflow_checkpoint(prompt: PromptGenerationResult | None):
177
+ if prompt is None:
178
+ return None
179
+ checkpoint = prompt.checkpoint_path
180
+ if checkpoint is None and prompt.artifact_path is not None:
181
+ checkpoint = prompt.artifact_path / "workflow-checkpoint.json"
182
+ if checkpoint is None or not checkpoint.is_file():
183
+ return None
184
+ state = load_checkpoint_state(checkpoint)
185
+ return state if not state.valid else None
186
+
187
+
181
188
  def _next_no_progress_count(previous: object | None, current: object, count: int) -> int:
182
189
  if previous and previous == current:
183
190
  return max(0, count) + 1
@@ -251,16 +258,3 @@ def _hash_tree(path: Path) -> str:
251
258
  except OSError:
252
259
  continue
253
260
  return digest.hexdigest()
254
-
255
-
256
- def _checkpoint_cursor(data: dict[str, object]) -> str:
257
- steps = data.get("steps")
258
- if not isinstance(steps, list):
259
- return ""
260
- for step in steps:
261
- if not isinstance(step, dict):
262
- continue
263
- status = step.get("status", "pending")
264
- if status not in {"completed", "skipped"}:
265
- return f"{step.get('id', '?')}:{step.get('skill', 'unknown')}:{status}"
266
- return "complete"