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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +0 -1
- package/bundled/dev-pipeline/README.md +4 -6
- package/bundled/dev-pipeline/SCHEMA_ANALYSIS.md +40 -518
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +27 -99
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +283 -0
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +3 -13
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +2 -0
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +0 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +14 -2
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +25 -31
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -8
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +0 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +13 -7
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +13 -1
- package/bundled/dev-pipeline/scripts/continuation.py +8 -40
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +76 -261
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +16 -35
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -34
- package/bundled/dev-pipeline/scripts/init-bugfix-pipeline.py +6 -0
- package/bundled/dev-pipeline/scripts/init-pipeline.py +6 -0
- package/bundled/dev-pipeline/scripts/init-refactor-pipeline.py +6 -0
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +1 -1
- package/bundled/dev-pipeline/scripts/prompt_framework.py +114 -63
- package/bundled/dev-pipeline/scripts/update-bug-status.py +3 -7
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +54 -22
- package/bundled/dev-pipeline/scripts/update-feature-status.py +3 -7
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +3 -7
- package/bundled/dev-pipeline/scripts/utils.py +6 -37
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +9 -91
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +9 -66
- package/bundled/dev-pipeline/templates/bug-fix-list-schema.json +10 -16
- package/bundled/dev-pipeline/templates/feature-list-schema.json +32 -17
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +0 -16
- package/bundled/dev-pipeline/templates/refactor-list-schema.json +10 -16
- package/bundled/dev-pipeline/templates/sections/bugfix-session-context.md +1 -1
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +1 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-agent.md +0 -1
- package/bundled/dev-pipeline/templates/sections/critical-paths-full.md +0 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-session-context.md +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +164 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +48 -76
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +25 -3
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +25 -5
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +329 -16
- package/bundled/dev-pipeline/tests/test_unified_cli.py +40 -9
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bug-planner/SKILL.md +5 -4
- package/bundled/skills/bug-planner/references/verification.md +29 -0
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +8 -0
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +2 -2
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +1 -6
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +6 -12
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +0 -1
- package/bundled/skills/feature-planner/SKILL.md +13 -12
- package/bundled/skills/feature-planner/assets/planning-guide.md +2 -2
- package/bundled/skills/feature-planner/scripts/validate-and-generate.py +8 -11
- package/bundled/skills/prizmkit-plan/SKILL.md +15 -1
- package/bundled/skills/prizmkit-plan/references/review-plan-spec-loop.md +101 -0
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +5 -5
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +2 -7
- package/bundled/skills/refactor-planner/SKILL.md +10 -8
- package/bundled/skills/refactor-planner/scripts/validate-and-generate-refactor.py +9 -0
- package/bundled/team/prizm-dev-team.json +2 -9
- package/package.json +1 -1
- package/bundled/agents/prizm-dev-team-critic.md +0 -179
- package/bundled/dev-pipeline/templates/agent-prompts/critic-plan-challenge.md +0 -8
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-critic.md +0 -22
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan-full.md +0 -61
- package/bundled/dev-pipeline/templates/sections/phase-critic-plan.md +0 -38
- package/bundled/dev-pipeline/templates/sections/refactor-phase-critic.md +0 -22
- 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,
|
|
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/
|
|
14
|
-
├── templates/
|
|
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
|
-
├──
|
|
19
|
-
├──
|
|
20
|
-
├──
|
|
21
|
-
├──
|
|
22
|
-
├──
|
|
23
|
-
|
|
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
|
|
34
|
-
| Reviewer | `.claude/agents/prizm-dev-team-reviewer.md` (or
|
|
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
|
-
|
|
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-
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
51
|
-
| `update-
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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"
|