prizmkit 1.1.140 → 1.1.142
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/README.md +1 -1
- package/bundled/dev-pipeline/prizmkit_runtime/checkpoint_state.py +165 -125
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +13 -1
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +0 -6
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +5 -3
- package/bundled/dev-pipeline/prizmkit_runtime/test_result.py +160 -0
- package/bundled/dev-pipeline/scripts/check-session-status.py +11 -1
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +23 -806
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +6 -8
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +1 -1
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +6 -8
- package/bundled/dev-pipeline/scripts/prompt_framework.py +68 -56
- package/bundled/dev-pipeline/scripts/update-bug-status.py +21 -2
- package/bundled/dev-pipeline/scripts/update-checkpoint.py +52 -131
- package/bundled/dev-pipeline/scripts/update-feature-status.py +21 -2
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +21 -2
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +8 -19
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -6
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +5 -5
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-commit-report.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-diagnose-plan.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-manual-verification.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-test.md +13 -10
- package/bundled/dev-pipeline/templates/sections/bugfix-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/sections/checkpoint-system.md +18 -9
- package/bundled/dev-pipeline/templates/sections/headless-commit-authorization.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit-full.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-commit.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-base.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-lite-suffix.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-implement-lite.md +1 -1
- package/bundled/dev-pipeline/templates/sections/phase-prizmkit-test.md +14 -26
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +2 -2
- package/bundled/dev-pipeline/templates/sections/phase-specify-plan-full.md +3 -3
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +1 -1
- package/bundled/dev-pipeline/templates/sections/refactor-phase-test.md +13 -10
- package/bundled/dev-pipeline/templates/sections/refactor-reminders.md +1 -1
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/test_checkpoint_state.py +494 -413
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +144 -684
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +20 -41
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +24 -24
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +176 -36
- package/bundled/skills/_metadata.json +6 -6
- package/bundled/skills/prizmkit/SKILL.md +13 -18
- package/bundled/skills/prizmkit/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-code-review/SKILL.md +7 -7
- package/bundled/skills/prizmkit-code-review/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-committer/SKILL.md +8 -7
- package/bundled/skills/prizmkit-committer/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-implement/SKILL.md +3 -2
- package/bundled/skills/prizmkit-implement/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-plan/SKILL.md +2 -1
- package/bundled/skills/prizmkit-plan/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-retrospective/SKILL.md +8 -6
- package/bundled/skills/prizmkit-retrospective/references/workflow-state-protocol.md +73 -80
- package/bundled/skills/prizmkit-test/SKILL.md +217 -189
- package/bundled/skills/prizmkit-test/references/external-contract-mock-guidance.md +119 -0
- package/bundled/skills/prizmkit-test/references/independent-test-review.md +269 -0
- package/bundled/skills/prizmkit-test/references/test-coverage-model.md +193 -0
- package/bundled/skills/prizmkit-test/references/test-report-template.md +164 -79
- package/bundled/skills/prizmkit-workflow/SKILL.md +28 -36
- package/bundled/skills/prizmkit-workflow/references/workflow-state-protocol.md +73 -80
- package/package.json +1 -1
- package/bundled/skills/prizmkit-test/assets/authoritative-records.schema.json +0 -477
- package/bundled/skills/prizmkit-test/assets/behavior-risk-matrix.schema.json +0 -116
- package/bundled/skills/prizmkit-test/assets/evidence-manifest.schema.json +0 -111
- package/bundled/skills/prizmkit-test/assets/evidence-package-template.json +0 -66
- package/bundled/skills/prizmkit-test/references/boundary-coverage-protocol.md +0 -104
- package/bundled/skills/prizmkit-test/references/contract-mock-protocol.md +0 -65
- package/bundled/skills/prizmkit-test/references/evidence-protocol.md +0 -208
- package/bundled/skills/prizmkit-test/references/evidence-request-protocol.md +0 -80
- package/bundled/skills/prizmkit-test/references/examples.md +0 -100
- package/bundled/skills/prizmkit-test/references/service-boundary-test-catalog.md +0 -228
- package/bundled/skills/prizmkit-test/references/test-generation-steps.md +0 -118
- package/bundled/skills/prizmkit-test/references/trusted-evidence-execution.md +0 -107
- package/bundled/skills/prizmkit-test/references/workflow-state-protocol.md +0 -181
- package/bundled/skills/prizmkit-test/scripts/build_test_evidence.py +0 -1930
- package/bundled/skills/prizmkit-test/scripts/validate_test_evidence.py +0 -1847
package/bundled/VERSION.json
CHANGED
|
@@ -359,7 +359,7 @@ python3 scripts/update-feature-status.py \
|
|
|
359
359
|
| `complete` | `--feature-id` | Shortcut for manually marking completed |
|
|
360
360
|
|
|
361
361
|
**Session status values (for `--session-status`):**
|
|
362
|
-
`success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `commit_missing`, `docs_missing`, `merge_conflict`
|
|
362
|
+
`success`, `partial_resumable`, `partial_not_resumable`, `failed`, `crashed`, `timed_out`, `infra_error`, `context_overflow`, `stalled_context_continuation`, `workflow_blocked`, `workflow_skipped`, `commit_missing`, `docs_missing`, `merge_conflict`
|
|
363
363
|
|
|
364
364
|
**Output (JSON to stdout):**
|
|
365
365
|
|
|
@@ -4,20 +4,50 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
7
|
from dataclasses import dataclass, field
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from typing import Any, Mapping
|
|
12
10
|
|
|
11
|
+
from .test_result import valid_test_artifact_pair
|
|
12
|
+
|
|
13
13
|
VALID_CHECKPOINT_STATUSES = {"pending", "in_progress", "completed", "skipped", "failed"}
|
|
14
14
|
TERMINAL_STEP_STATUSES = {"completed", "skipped"}
|
|
15
15
|
STEP_ID_PATTERN = re.compile(r"^S[0-9]+$")
|
|
16
16
|
MAX_OUTER_REPAIR_ROUNDS = 3
|
|
17
17
|
|
|
18
18
|
REVIEW_RESULTS = {"REVIEW_PASS", "REVIEW_NEEDS_FIXES"}
|
|
19
|
-
TEST_RESULTS = {"TEST_PASS", "
|
|
20
|
-
|
|
19
|
+
TEST_RESULTS = {"TEST_PASS", "TEST_NEEDS_FIXES", "TEST_BLOCKED"}
|
|
20
|
+
PLAN_RESULTS = {"PLAN_READY", "PLAN_BLOCKED"}
|
|
21
|
+
IMPLEMENT_RESULTS = {"IMPLEMENTED", "IMPLEMENT_REPAIR", "IMPLEMENT_BLOCKED"}
|
|
22
|
+
RETROSPECTIVE_RESULTS = {"RETRO_COMPLETE", "RETRO_BLOCKED"}
|
|
23
|
+
COMMIT_RESULTS = {"COMMIT_PENDING", "COMMITTED", "COMMIT_BLOCKED"}
|
|
24
|
+
VALID_STAGE_RESULTS = (
|
|
25
|
+
REVIEW_RESULTS
|
|
26
|
+
| TEST_RESULTS
|
|
27
|
+
| PLAN_RESULTS
|
|
28
|
+
| IMPLEMENT_RESULTS
|
|
29
|
+
| RETROSPECTIVE_RESULTS
|
|
30
|
+
| COMMIT_RESULTS
|
|
31
|
+
)
|
|
32
|
+
STAGE_RESULT_OWNERS = {
|
|
33
|
+
"prizmkit-plan": PLAN_RESULTS,
|
|
34
|
+
"prizmkit-implement": IMPLEMENT_RESULTS,
|
|
35
|
+
"prizmkit-code-review": REVIEW_RESULTS,
|
|
36
|
+
"prizmkit-test": TEST_RESULTS,
|
|
37
|
+
"prizmkit-retrospective": RETROSPECTIVE_RESULTS,
|
|
38
|
+
"prizmkit-committer": COMMIT_RESULTS,
|
|
39
|
+
}
|
|
40
|
+
WORKFLOW_STAGE_SKILLS = {
|
|
41
|
+
"plan": "prizmkit-plan",
|
|
42
|
+
"implement": "prizmkit-implement",
|
|
43
|
+
"code-review": "prizmkit-code-review",
|
|
44
|
+
"test": "prizmkit-test",
|
|
45
|
+
"retrospective": "prizmkit-retrospective",
|
|
46
|
+
"committer": "prizmkit-committer",
|
|
47
|
+
}
|
|
48
|
+
SKILL_WORKFLOW_STAGES = {
|
|
49
|
+
skill: stage for stage, skill in WORKFLOW_STAGE_SKILLS.items()
|
|
50
|
+
}
|
|
21
51
|
VALID_TERMINAL_STATUSES = {"WORKFLOW_COMPLETED", "WORKFLOW_BLOCKED"}
|
|
22
52
|
VALID_REPAIR_SCOPES = {
|
|
23
53
|
"production",
|
|
@@ -59,11 +89,6 @@ FAMILY_REQUIRED_STAGES = {
|
|
|
59
89
|
"completion-summary",
|
|
60
90
|
),
|
|
61
91
|
}
|
|
62
|
-
EVIDENCE_FILENAMES = {
|
|
63
|
-
"manifest": "manifest.json",
|
|
64
|
-
"verdict": "verdict.json",
|
|
65
|
-
"validation": "validation.json",
|
|
66
|
-
}
|
|
67
92
|
|
|
68
93
|
|
|
69
94
|
@dataclass(frozen=True)
|
|
@@ -77,7 +102,6 @@ class CheckpointStep:
|
|
|
77
102
|
depends_on: str | None = None
|
|
78
103
|
required_artifacts: tuple[str, ...] = ()
|
|
79
104
|
stage_result: str | None = None
|
|
80
|
-
authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
|
|
81
105
|
index: int = 0
|
|
82
106
|
|
|
83
107
|
@property
|
|
@@ -101,11 +125,10 @@ class SemanticCheckpoint:
|
|
|
101
125
|
stage_result: str | None = None
|
|
102
126
|
repair_scope: str | None = None
|
|
103
127
|
repair_round: int = 0
|
|
104
|
-
authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
|
|
105
128
|
blocked_reason: str | None = None
|
|
106
129
|
terminal_status: str | None = None
|
|
107
130
|
sequence_valid: bool = False
|
|
108
|
-
|
|
131
|
+
test_result_valid: bool = False
|
|
109
132
|
artifacts_valid: bool = False
|
|
110
133
|
completion_ready: bool = False
|
|
111
134
|
complete: bool = False
|
|
@@ -121,7 +144,6 @@ class SemanticCheckpoint:
|
|
|
121
144
|
"stage_result": self.stage_result,
|
|
122
145
|
"repair_scope": self.repair_scope,
|
|
123
146
|
"repair_round": self.repair_round,
|
|
124
|
-
"authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
|
|
125
147
|
"blocked_reason": self.blocked_reason,
|
|
126
148
|
"terminal_status": self.terminal_status,
|
|
127
149
|
}
|
|
@@ -278,11 +300,23 @@ def validate_checkpoint_data(
|
|
|
278
300
|
stage_result = raw_step.get("stage_result")
|
|
279
301
|
if stage_result is not None and stage_result not in VALID_STAGE_RESULTS:
|
|
280
302
|
return _invalid(checkpoint_path, exists, "invalid_stage_result", f"Step {step_id} has invalid stage_result", data)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
if stage_result is not None and not stage_result_allowed_for_skill(skill, stage_result):
|
|
304
|
+
return _invalid(
|
|
305
|
+
checkpoint_path,
|
|
306
|
+
exists,
|
|
307
|
+
"stage_result_owner_mismatch",
|
|
308
|
+
f"Step {step_id} cannot record {stage_result} for {skill}",
|
|
309
|
+
data,
|
|
310
|
+
)
|
|
311
|
+
expected_status = _expected_status_for_result(stage_result)
|
|
312
|
+
if expected_status is not None and status != expected_status:
|
|
313
|
+
return _invalid(
|
|
314
|
+
checkpoint_path,
|
|
315
|
+
exists,
|
|
316
|
+
"stage_result_status_mismatch",
|
|
317
|
+
f"Step {step_id} requires status={expected_status} for {stage_result}",
|
|
318
|
+
data,
|
|
319
|
+
)
|
|
286
320
|
|
|
287
321
|
step = CheckpointStep(
|
|
288
322
|
id=step_id,
|
|
@@ -292,7 +326,6 @@ def validate_checkpoint_data(
|
|
|
292
326
|
depends_on=depends_on,
|
|
293
327
|
required_artifacts=tuple(artifacts),
|
|
294
328
|
stage_result=stage_result,
|
|
295
|
-
authoritative_evidence_paths=dict(evidence),
|
|
296
329
|
index=index,
|
|
297
330
|
)
|
|
298
331
|
steps.append(step)
|
|
@@ -415,6 +448,47 @@ def semantic_state_key(workflow_type: str) -> str:
|
|
|
415
448
|
return SEMANTIC_STATE_KEYS.get(workflow_type, "l4_state")
|
|
416
449
|
|
|
417
450
|
|
|
451
|
+
def expected_status_for_stage_result(stage_result: str | None) -> str | None:
|
|
452
|
+
"""Return the lifecycle status required by one domain stage result."""
|
|
453
|
+
return _expected_status_for_result(stage_result)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def stage_result_allowed_for_skill(skill: str, stage_result: str | None) -> bool:
|
|
457
|
+
"""Return whether one stage owns the supplied domain result."""
|
|
458
|
+
if stage_result is None:
|
|
459
|
+
return True
|
|
460
|
+
allowed_results = STAGE_RESULT_OWNERS.get(skill)
|
|
461
|
+
return allowed_results is not None and stage_result in allowed_results
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _expected_status_for_result(stage_result: str | None) -> str | None:
|
|
465
|
+
if stage_result is None:
|
|
466
|
+
return None
|
|
467
|
+
if stage_result == "COMMIT_PENDING":
|
|
468
|
+
return "in_progress"
|
|
469
|
+
if stage_result in {
|
|
470
|
+
"REVIEW_PASS",
|
|
471
|
+
"TEST_PASS",
|
|
472
|
+
"PLAN_READY",
|
|
473
|
+
"IMPLEMENTED",
|
|
474
|
+
"RETRO_COMPLETE",
|
|
475
|
+
"COMMITTED",
|
|
476
|
+
}:
|
|
477
|
+
return "completed"
|
|
478
|
+
if stage_result in {
|
|
479
|
+
"REVIEW_NEEDS_FIXES",
|
|
480
|
+
"TEST_NEEDS_FIXES",
|
|
481
|
+
"TEST_BLOCKED",
|
|
482
|
+
"PLAN_BLOCKED",
|
|
483
|
+
"IMPLEMENT_REPAIR",
|
|
484
|
+
"IMPLEMENT_BLOCKED",
|
|
485
|
+
"RETRO_BLOCKED",
|
|
486
|
+
"COMMIT_BLOCKED",
|
|
487
|
+
}:
|
|
488
|
+
return "failed"
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
|
|
418
492
|
def _progress_error(steps: list[CheckpointStep]) -> str:
|
|
419
493
|
active_seen = False
|
|
420
494
|
for step in steps:
|
|
@@ -449,6 +523,32 @@ def _derive_semantic_state(
|
|
|
449
523
|
repair_scope = raw_state.get("repair_scope")
|
|
450
524
|
if repair_scope is not None and repair_scope not in VALID_REPAIR_SCOPES:
|
|
451
525
|
return _semantic_error(workflow_type, current_step, "invalid_repair_scope", "repair_scope is not a safe shared scope")
|
|
526
|
+
semantic_status = raw_state.get("status")
|
|
527
|
+
if semantic_status is not None and semantic_status not in VALID_CHECKPOINT_STATUSES:
|
|
528
|
+
return _semantic_error(workflow_type, current_step, "invalid_semantic_status", "semantic status is not a lifecycle status")
|
|
529
|
+
semantic_stage_result = raw_state.get("stage_result")
|
|
530
|
+
if semantic_stage_result is not None and semantic_stage_result not in VALID_STAGE_RESULTS:
|
|
531
|
+
return _semantic_error(workflow_type, current_step, "invalid_semantic_stage_result", "semantic stage_result is not recognized")
|
|
532
|
+
expected_semantic_status = _expected_status_for_result(semantic_stage_result)
|
|
533
|
+
if semantic_status is not None and expected_semantic_status is not None and semantic_status != expected_semantic_status:
|
|
534
|
+
return _semantic_error(
|
|
535
|
+
workflow_type,
|
|
536
|
+
current_step,
|
|
537
|
+
"semantic_stage_result_status_mismatch",
|
|
538
|
+
f"Semantic {semantic_stage_result} requires status={expected_semantic_status}",
|
|
539
|
+
)
|
|
540
|
+
semantic_stage = raw_state.get("stage")
|
|
541
|
+
if semantic_stage is not None and (not isinstance(semantic_stage, str) or not semantic_stage.strip()):
|
|
542
|
+
return _semantic_error(workflow_type, current_step, "invalid_semantic_stage", "semantic stage must be a non-empty string")
|
|
543
|
+
if semantic_stage_result is not None:
|
|
544
|
+
semantic_skill = WORKFLOW_STAGE_SKILLS.get(semantic_stage, semantic_stage)
|
|
545
|
+
if not stage_result_allowed_for_skill(semantic_skill, semantic_stage_result):
|
|
546
|
+
return _semantic_error(
|
|
547
|
+
workflow_type,
|
|
548
|
+
current_step,
|
|
549
|
+
"semantic_stage_result_owner_mismatch",
|
|
550
|
+
f"Semantic stage {semantic_stage} cannot record {semantic_stage_result}",
|
|
551
|
+
)
|
|
452
552
|
terminal_status = raw_state.get("terminal_status")
|
|
453
553
|
if terminal_status is not None and terminal_status not in VALID_TERMINAL_STATUSES:
|
|
454
554
|
return _semantic_error(workflow_type, current_step, "invalid_terminal_status", "terminal_status is not recognized")
|
|
@@ -465,29 +565,23 @@ def _derive_semantic_state(
|
|
|
465
565
|
and review_step.status == "completed"
|
|
466
566
|
and review_step.stage_result == "REVIEW_PASS"
|
|
467
567
|
)
|
|
468
|
-
|
|
469
|
-
dict(test_step.authoritative_evidence_paths)
|
|
470
|
-
if test_step and test_step.authoritative_evidence_paths
|
|
471
|
-
else dict(raw_state.get("authoritative_evidence_paths") or {})
|
|
472
|
-
)
|
|
473
|
-
evidence_valid = bool(
|
|
568
|
+
test_result_valid = bool(
|
|
474
569
|
test_step
|
|
475
570
|
and test_step.status == "completed"
|
|
476
571
|
and test_step.stage_result == "TEST_PASS"
|
|
477
|
-
and _validate_test_evidence(evidence_paths, checkpoint_path, project_root)
|
|
478
572
|
)
|
|
479
573
|
all_required_completed = bool(required) and all(
|
|
480
574
|
(step := _single_step(steps, skill)) is not None and step.status == "completed"
|
|
481
575
|
for skill in required
|
|
482
576
|
)
|
|
483
577
|
artifacts_valid = all(
|
|
484
|
-
|
|
578
|
+
_required_step_artifacts_valid(step, checkpoint_path, project_root)
|
|
485
579
|
for step in steps
|
|
486
580
|
if step.status == "completed"
|
|
487
581
|
)
|
|
488
582
|
completion_ready = (
|
|
489
583
|
sequence_valid and all_required_completed and review_valid
|
|
490
|
-
and
|
|
584
|
+
and test_result_valid and artifacts_valid
|
|
491
585
|
)
|
|
492
586
|
blocked = terminal_status == "WORKFLOW_BLOCKED" and bool(blocked_reason)
|
|
493
587
|
complete = completion_ready and terminal_status == "WORKFLOW_COMPLETED" and not blocked
|
|
@@ -495,9 +589,15 @@ def _derive_semantic_state(
|
|
|
495
589
|
current_stage = current_step.skill if current_step is not None else None
|
|
496
590
|
next_stage = current_stage
|
|
497
591
|
if current_step is not None and current_step.status == "in_progress":
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
592
|
+
next_stage = current_stage
|
|
593
|
+
if (
|
|
594
|
+
current_step is not None
|
|
595
|
+
and current_step.status == "failed"
|
|
596
|
+
and current_step.stage_result == "REVIEW_NEEDS_FIXES"
|
|
597
|
+
and raw_state.get("stage_result") == "REVIEW_NEEDS_FIXES"
|
|
598
|
+
and raw_state.get("next_stage") == "prizmkit-implement"
|
|
599
|
+
):
|
|
600
|
+
next_stage = "prizmkit-implement"
|
|
501
601
|
if terminal_status is not None:
|
|
502
602
|
current_stage = None
|
|
503
603
|
next_stage = None
|
|
@@ -511,8 +611,8 @@ def _derive_semantic_state(
|
|
|
511
611
|
error_code = "incomplete_semantic_evidence"
|
|
512
612
|
if not review_valid:
|
|
513
613
|
error_message = "completed checkpoint lacks REVIEW_PASS on the review stage"
|
|
514
|
-
elif not
|
|
515
|
-
error_message = "completed checkpoint lacks
|
|
614
|
+
elif not test_result_valid:
|
|
615
|
+
error_message = "completed checkpoint lacks TEST_PASS on the test stage"
|
|
516
616
|
else:
|
|
517
617
|
error_message = "completed checkpoint lacks mandatory completed stages"
|
|
518
618
|
elif terminal_status == "WORKFLOW_COMPLETED" and not completion_ready:
|
|
@@ -526,11 +626,10 @@ def _derive_semantic_state(
|
|
|
526
626
|
stage_result=raw_state.get("stage_result"),
|
|
527
627
|
repair_scope=repair_scope,
|
|
528
628
|
repair_round=repair_round,
|
|
529
|
-
authoritative_evidence_paths=evidence_paths,
|
|
530
629
|
blocked_reason=blocked_reason,
|
|
531
630
|
terminal_status=terminal_status,
|
|
532
631
|
sequence_valid=sequence_valid,
|
|
533
|
-
|
|
632
|
+
test_result_valid=test_result_valid,
|
|
534
633
|
artifacts_valid=artifacts_valid,
|
|
535
634
|
completion_ready=completion_ready,
|
|
536
635
|
complete=complete,
|
|
@@ -563,6 +662,30 @@ def _single_step(steps: tuple[CheckpointStep, ...], skill: str) -> CheckpointSte
|
|
|
563
662
|
|
|
564
663
|
|
|
565
664
|
|
|
665
|
+
def _required_step_artifacts_valid(
|
|
666
|
+
step: CheckpointStep,
|
|
667
|
+
checkpoint_path: Path | None,
|
|
668
|
+
project_root: Path | None,
|
|
669
|
+
) -> bool:
|
|
670
|
+
"""Validate declared artifacts, including semantic test terminal output."""
|
|
671
|
+
if step.skill == "prizmkit-test":
|
|
672
|
+
root = (project_root or _infer_project_root(checkpoint_path)).resolve()
|
|
673
|
+
reports = [item for item in step.required_artifacts if item.endswith("/test-report.md")]
|
|
674
|
+
results = [item for item in step.required_artifacts if item.endswith("/test-result.json")]
|
|
675
|
+
if len(reports) != 1 or len(results) != 1:
|
|
676
|
+
return False
|
|
677
|
+
report_path = (root / reports[0]).resolve()
|
|
678
|
+
result_path = (root / results[0]).resolve()
|
|
679
|
+
if not _is_within_root(report_path, root) or not _is_within_root(result_path, root):
|
|
680
|
+
return False
|
|
681
|
+
return valid_test_artifact_pair(
|
|
682
|
+
report_path,
|
|
683
|
+
result_path,
|
|
684
|
+
required_result="TEST_PASS",
|
|
685
|
+
)
|
|
686
|
+
return _required_artifacts_exist(step, checkpoint_path, project_root)
|
|
687
|
+
|
|
688
|
+
|
|
566
689
|
def _required_artifacts_exist(
|
|
567
690
|
step: CheckpointStep,
|
|
568
691
|
checkpoint_path: Path | None,
|
|
@@ -574,7 +697,9 @@ def _required_artifacts_exist(
|
|
|
574
697
|
|
|
575
698
|
root = (project_root or _infer_project_root(checkpoint_path)).resolve()
|
|
576
699
|
for artifact in step.required_artifacts:
|
|
577
|
-
candidate = root / artifact
|
|
700
|
+
candidate = (root / artifact).resolve()
|
|
701
|
+
if not _is_within_root(candidate, root):
|
|
702
|
+
return False
|
|
578
703
|
if glob.has_magic(str(candidate)):
|
|
579
704
|
if not glob.glob(str(candidate)):
|
|
580
705
|
return False
|
|
@@ -582,98 +707,13 @@ def _required_artifacts_exist(
|
|
|
582
707
|
return False
|
|
583
708
|
return True
|
|
584
709
|
|
|
585
|
-
def _valid_evidence_mapping(value: object) -> bool:
|
|
586
|
-
if not isinstance(value, dict):
|
|
587
|
-
return False
|
|
588
|
-
return all(
|
|
589
|
-
key in EVIDENCE_FILENAMES
|
|
590
|
-
and (path is None or isinstance(path, str))
|
|
591
|
-
for key, path in value.items()
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def _validate_test_evidence(
|
|
596
|
-
paths: Mapping[str, str | None],
|
|
597
|
-
checkpoint_path: Path | None,
|
|
598
|
-
project_root: Path | None,
|
|
599
|
-
) -> bool:
|
|
600
|
-
if set(paths) != set(EVIDENCE_FILENAMES):
|
|
601
|
-
return False
|
|
602
|
-
root = (project_root or _infer_project_root(checkpoint_path)).resolve()
|
|
603
|
-
resolved: dict[str, Path] = {}
|
|
604
|
-
for key, filename in EVIDENCE_FILENAMES.items():
|
|
605
|
-
raw_path = paths.get(key)
|
|
606
|
-
if not isinstance(raw_path, str) or not raw_path.strip():
|
|
607
|
-
return False
|
|
608
|
-
candidate = Path(raw_path)
|
|
609
|
-
if not candidate.is_absolute():
|
|
610
|
-
candidate = root / candidate
|
|
611
|
-
try:
|
|
612
|
-
candidate = candidate.resolve()
|
|
613
|
-
candidate.relative_to(root)
|
|
614
|
-
except (OSError, ValueError):
|
|
615
|
-
return False
|
|
616
|
-
if candidate.name != filename or not candidate.is_file():
|
|
617
|
-
return False
|
|
618
|
-
resolved[key] = candidate
|
|
619
|
-
parents = {path.parent for path in resolved.values()}
|
|
620
|
-
if len(parents) != 1:
|
|
621
|
-
return False
|
|
622
|
-
try:
|
|
623
|
-
manifest = json.loads(resolved["manifest"].read_text(encoding="utf-8"))
|
|
624
|
-
verdict = json.loads(resolved["verdict"].read_text(encoding="utf-8"))
|
|
625
|
-
validation = json.loads(resolved["validation"].read_text(encoding="utf-8"))
|
|
626
|
-
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
627
|
-
return False
|
|
628
|
-
if not all(isinstance(record, dict) for record in (manifest, verdict, validation)):
|
|
629
|
-
return False
|
|
630
|
-
evidence_id = manifest.get("evidence_id")
|
|
631
|
-
if isinstance(evidence_id, str) and evidence_id and resolved["manifest"].parent.name != evidence_id:
|
|
632
|
-
return False
|
|
633
|
-
records_agree = (
|
|
634
|
-
manifest.get("final_verdict") == "TEST_PASS"
|
|
635
|
-
and verdict.get("verdict") == "TEST_PASS"
|
|
636
|
-
and validation.get("verdict") == "TEST_PASS"
|
|
637
|
-
and validation.get("result") == "passed"
|
|
638
|
-
)
|
|
639
|
-
if not records_agree:
|
|
640
|
-
return False
|
|
641
|
-
validator = _resolve_evidence_validator(root)
|
|
642
|
-
if validator is None:
|
|
643
|
-
return False
|
|
644
|
-
return _evidence_validator_passes(root, resolved["manifest"].parent, validator)
|
|
645
|
-
|
|
646
710
|
|
|
647
|
-
def
|
|
648
|
-
project_root: Path,
|
|
649
|
-
evidence_dir: Path,
|
|
650
|
-
validator: Path,
|
|
651
|
-
) -> bool:
|
|
711
|
+
def _is_within_root(path: Path, root: Path) -> bool:
|
|
652
712
|
try:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
sys.executable, str(validator),
|
|
656
|
-
"--evidence-dir", str(evidence_dir),
|
|
657
|
-
"--project-root", str(project_root),
|
|
658
|
-
],
|
|
659
|
-
stdout=subprocess.DEVNULL,
|
|
660
|
-
stderr=subprocess.DEVNULL,
|
|
661
|
-
timeout=120,
|
|
662
|
-
check=False,
|
|
663
|
-
)
|
|
664
|
-
except (OSError, subprocess.SubprocessError):
|
|
713
|
+
path.relative_to(root)
|
|
714
|
+
except ValueError:
|
|
665
715
|
return False
|
|
666
|
-
return
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def _resolve_evidence_validator(project_root: Path) -> Path | None:
|
|
670
|
-
candidates = (
|
|
671
|
-
project_root / "core" / "skills" / "prizmkit-skill" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
|
|
672
|
-
project_root / ".claude" / "command-assets" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
|
|
673
|
-
project_root / ".agents" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
|
|
674
|
-
project_root / ".codebuddy" / "skills" / "prizmkit-test" / "scripts" / "validate_test_evidence.py",
|
|
675
|
-
)
|
|
676
|
-
return next((path for path in candidates if path.is_file()), None)
|
|
716
|
+
return True
|
|
677
717
|
|
|
678
718
|
|
|
679
719
|
def _infer_project_root(checkpoint_path: Path | None) -> Path:
|
|
@@ -13,6 +13,7 @@ from .sessions import AISessionResult
|
|
|
13
13
|
|
|
14
14
|
CONTEXT_OVERFLOW = "context_overflow"
|
|
15
15
|
WORKFLOW_BLOCKED = "workflow_blocked"
|
|
16
|
+
WORKFLOW_SKIPPED = "workflow_skipped"
|
|
16
17
|
STALL_REASON = "context_overflow_without_git_checkpoint_or_artifact_progress"
|
|
17
18
|
CONTEXT_MARKERS = (
|
|
18
19
|
"context_overflow",
|
|
@@ -50,8 +51,9 @@ def classify_session(
|
|
|
50
51
|
fingerprint = progress_fingerprint(project_root, prompt)
|
|
51
52
|
checkpoint = _workflow_checkpoint(prompt, project_root)
|
|
52
53
|
if checkpoint is not None and not checkpoint.valid:
|
|
54
|
+
session_status = WORKFLOW_SKIPPED if _is_terminal_success(result) else "crashed"
|
|
53
55
|
return SessionClassification(
|
|
54
|
-
|
|
56
|
+
session_status,
|
|
55
57
|
reason=f"invalid_workflow_checkpoint:{checkpoint.error_code}",
|
|
56
58
|
progress_fingerprint=fingerprint,
|
|
57
59
|
checkpoint_state=checkpoint.semantic_snapshot,
|
|
@@ -79,6 +81,16 @@ def classify_session(
|
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
semantic = checkpoint.semantic_snapshot if checkpoint is not None else {}
|
|
84
|
+
if _is_terminal_success(result):
|
|
85
|
+
reason = "no_semantic_completion"
|
|
86
|
+
if checkpoint is not None and checkpoint.semantic.error_code:
|
|
87
|
+
reason = f"{reason}:{checkpoint.semantic.error_code}"
|
|
88
|
+
return SessionClassification(
|
|
89
|
+
WORKFLOW_SKIPPED,
|
|
90
|
+
reason=reason,
|
|
91
|
+
progress_fingerprint=fingerprint,
|
|
92
|
+
checkpoint_state=semantic,
|
|
93
|
+
)
|
|
82
94
|
if _is_context_overflow(result):
|
|
83
95
|
no_progress_count = _next_no_progress_count(previous_fingerprint, fingerprint, previous_no_progress_count)
|
|
84
96
|
if no_progress_count >= 2:
|
|
@@ -137,23 +137,18 @@ class SemanticCheckpointState:
|
|
|
137
137
|
stage_result: str | None = None
|
|
138
138
|
repair_scope: str | None = None
|
|
139
139
|
repair_round: int = 0
|
|
140
|
-
authoritative_evidence_paths: Mapping[str, str | None] = field(default_factory=dict)
|
|
141
140
|
blocked_reason: str | None = None
|
|
142
141
|
terminal_status: str | None = None
|
|
143
142
|
|
|
144
143
|
@classmethod
|
|
145
144
|
def from_mapping(cls, value: Mapping[str, object] | None) -> "SemanticCheckpointState":
|
|
146
145
|
data = value or {}
|
|
147
|
-
evidence = data.get("authoritative_evidence_paths")
|
|
148
146
|
return cls(
|
|
149
147
|
current_stage=_optional_text(data.get("current_stage")),
|
|
150
148
|
next_stage=_optional_text(data.get("next_stage")),
|
|
151
149
|
stage_result=_optional_text(data.get("stage_result")),
|
|
152
150
|
repair_scope=_optional_text(data.get("repair_scope")),
|
|
153
151
|
repair_round=max(0, _safe_int(data.get("repair_round"), 0)),
|
|
154
|
-
authoritative_evidence_paths=(
|
|
155
|
-
dict(evidence) if isinstance(evidence, Mapping) else {}
|
|
156
|
-
),
|
|
157
152
|
blocked_reason=_optional_text(data.get("blocked_reason")),
|
|
158
153
|
terminal_status=_optional_text(data.get("terminal_status")),
|
|
159
154
|
)
|
|
@@ -165,7 +160,6 @@ class SemanticCheckpointState:
|
|
|
165
160
|
"stage_result": self.stage_result,
|
|
166
161
|
"repair_scope": self.repair_scope,
|
|
167
162
|
"repair_round": self.repair_round,
|
|
168
|
-
"authoritative_evidence_paths": dict(self.authoritative_evidence_paths),
|
|
169
163
|
"blocked_reason": self.blocked_reason,
|
|
170
164
|
"terminal_status": self.terminal_status,
|
|
171
165
|
}
|
|
@@ -650,8 +650,7 @@ def _process_item_locked(
|
|
|
650
650
|
key: metadata.get(key)
|
|
651
651
|
for key in (
|
|
652
652
|
"current_stage", "next_stage", "stage_result",
|
|
653
|
-
"repair_scope", "repair_round",
|
|
654
|
-
"authoritative_evidence_paths", "blocked_reason",
|
|
653
|
+
"repair_scope", "repair_round", "blocked_reason",
|
|
655
654
|
"terminal_status",
|
|
656
655
|
)
|
|
657
656
|
},
|
|
@@ -863,7 +862,10 @@ def _emit_interruption_cleanup_failure(result, message: str, recovery: str) -> N
|
|
|
863
862
|
|
|
864
863
|
def _is_retryable_ai_outcome(session_status: str, new_status: str) -> bool:
|
|
865
864
|
"""Return whether a persisted post-launch outcome should continue in-process."""
|
|
866
|
-
if session_status in {
|
|
865
|
+
if session_status in {
|
|
866
|
+
"success", "stalled_context_continuation", "workflow_blocked",
|
|
867
|
+
"workflow_skipped",
|
|
868
|
+
}:
|
|
867
869
|
return False
|
|
868
870
|
return new_status in {"in_progress", "pending"} and session_status in {
|
|
869
871
|
"context_overflow",
|