project-loop-harness 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcl/__init__.py +1 -0
- pcl/__main__.py +4 -0
- pcl/agents.py +501 -0
- pcl/checkpoints.py +201 -0
- pcl/cli.py +1404 -0
- pcl/commands.py +1006 -0
- pcl/db/migrations/001_initial.sql +180 -0
- pcl/db/schema.sql +180 -0
- pcl/db.py +49 -0
- pcl/decisions.py +275 -0
- pcl/errors.py +83 -0
- pcl/escalations.py +302 -0
- pcl/events.py +41 -0
- pcl/evidence.py +25 -0
- pcl/exporters.py +77 -0
- pcl/guards.py +14 -0
- pcl/ids.py +15 -0
- pcl/init_project.py +112 -0
- pcl/lifecycle.py +1073 -0
- pcl/links.py +108 -0
- pcl/mcp_server.py +328 -0
- pcl/migrations.py +220 -0
- pcl/paths.py +65 -0
- pcl/renderer.py +823 -0
- pcl/reports.py +766 -0
- pcl/resources.py +26 -0
- pcl/stories.py +762 -0
- pcl/templates/dashboard/dashboard.html +165 -0
- pcl/templates/project/AGENTS.block.md +16 -0
- pcl/templates/project/CLAUDE.block.md +13 -0
- pcl/templates/project/gitignore.fragment +13 -0
- pcl/templates/project/pcl.yaml +60 -0
- pcl/templates/skills/project-control-loop/SKILL.md +120 -0
- pcl/templates/workflows/defect_repair.yaml +61 -0
- pcl/templates/workflows/executor_smoke.yaml +32 -0
- pcl/templates/workflows/feature_coverage.yaml +52 -0
- pcl/templates/workflows/regression_loop.yaml +51 -0
- pcl/timeutil.py +7 -0
- pcl/validators.py +788 -0
- pcl/workflow_executor.py +911 -0
- pcl/workflow_proposal_validation.py +50 -0
- pcl/workflow_proposals.py +442 -0
- pcl/workflow_sandbox.py +683 -0
- pcl/workflow_verifier.py +333 -0
- pcl/workflow_yaml.py +190 -0
- pcl/workflows.py +569 -0
- project_loop_harness-0.1.2.dist-info/METADATA +361 -0
- project_loop_harness-0.1.2.dist-info/RECORD +52 -0
- project_loop_harness-0.1.2.dist-info/WHEEL +5 -0
- project_loop_harness-0.1.2.dist-info/entry_points.txt +3 -0
- project_loop_harness-0.1.2.dist-info/licenses/LICENSE +21 -0
- project_loop_harness-0.1.2.dist-info/top_level.txt +1 -0
pcl/checkpoints.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .db import connect
|
|
8
|
+
from .evidence import record_inline_evidence
|
|
9
|
+
from .events import append_event
|
|
10
|
+
from .errors import InvalidInputError
|
|
11
|
+
from .guards import require_initialized
|
|
12
|
+
from .paths import ProjectPaths
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CHECKPOINT_FEATURE_THRESHOLD = 5
|
|
16
|
+
CHECKPOINT_REVIEW_TYPES = {"integration", "commit", "ux", "release", "package"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def checkpoint_status(paths: ProjectPaths) -> dict[str, Any]:
|
|
20
|
+
require_initialized(paths)
|
|
21
|
+
|
|
22
|
+
conn = connect(paths.db_path)
|
|
23
|
+
try:
|
|
24
|
+
latest = conn.execute(
|
|
25
|
+
"""
|
|
26
|
+
SELECT id, payload_json, created_at, rowid
|
|
27
|
+
FROM events
|
|
28
|
+
WHERE event_type = 'checkpoint_recorded'
|
|
29
|
+
ORDER BY rowid DESC
|
|
30
|
+
LIMIT 1
|
|
31
|
+
"""
|
|
32
|
+
).fetchone()
|
|
33
|
+
latest_rowid = int(latest["rowid"]) if latest is not None else 0
|
|
34
|
+
feature_events_since_checkpoint = conn.execute(
|
|
35
|
+
"""
|
|
36
|
+
SELECT entity_id, payload_json
|
|
37
|
+
FROM events
|
|
38
|
+
WHERE event_type = 'feature_status_updated'
|
|
39
|
+
AND entity_type = 'feature'
|
|
40
|
+
AND rowid > ?
|
|
41
|
+
AND entity_id IS NOT NULL
|
|
42
|
+
ORDER BY rowid
|
|
43
|
+
""",
|
|
44
|
+
(latest_rowid,),
|
|
45
|
+
).fetchall()
|
|
46
|
+
status_counts = {
|
|
47
|
+
str(row["status"]): int(row["count"])
|
|
48
|
+
for row in conn.execute(
|
|
49
|
+
"""
|
|
50
|
+
SELECT status, COUNT(*) AS count
|
|
51
|
+
FROM features
|
|
52
|
+
GROUP BY status
|
|
53
|
+
ORDER BY status
|
|
54
|
+
"""
|
|
55
|
+
).fetchall()
|
|
56
|
+
}
|
|
57
|
+
passed_runs_since = int(
|
|
58
|
+
conn.execute(
|
|
59
|
+
"""
|
|
60
|
+
SELECT COUNT(*) AS count
|
|
61
|
+
FROM events
|
|
62
|
+
WHERE event_type = 'workflow_run_completed'
|
|
63
|
+
AND rowid > ?
|
|
64
|
+
""",
|
|
65
|
+
(latest_rowid,),
|
|
66
|
+
).fetchone()["count"]
|
|
67
|
+
)
|
|
68
|
+
open_goal_count = int(
|
|
69
|
+
conn.execute(
|
|
70
|
+
"""
|
|
71
|
+
SELECT COUNT(*) AS count
|
|
72
|
+
FROM goals
|
|
73
|
+
WHERE status NOT IN ('closed', 'cancelled')
|
|
74
|
+
"""
|
|
75
|
+
).fetchone()["count"]
|
|
76
|
+
)
|
|
77
|
+
done_feature_ids = _done_feature_ids(feature_events_since_checkpoint)
|
|
78
|
+
git_state = _git_status(paths)
|
|
79
|
+
recommended = len(done_feature_ids) >= CHECKPOINT_FEATURE_THRESHOLD
|
|
80
|
+
return {
|
|
81
|
+
"ok": True,
|
|
82
|
+
"checkpoint_recommended": recommended,
|
|
83
|
+
"threshold": CHECKPOINT_FEATURE_THRESHOLD,
|
|
84
|
+
"completed_features_since_checkpoint": len(done_feature_ids),
|
|
85
|
+
"completed_feature_ids_since_checkpoint": done_feature_ids,
|
|
86
|
+
"passed_workflow_runs_since_checkpoint": passed_runs_since,
|
|
87
|
+
"feature_status_counts": status_counts,
|
|
88
|
+
"open_goal_count": open_goal_count,
|
|
89
|
+
"latest_checkpoint": None if latest is None else dict(latest),
|
|
90
|
+
"git": git_state,
|
|
91
|
+
}
|
|
92
|
+
finally:
|
|
93
|
+
conn.close()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def record_checkpoint(
|
|
97
|
+
paths: ProjectPaths,
|
|
98
|
+
*,
|
|
99
|
+
summary: str,
|
|
100
|
+
evidence: str,
|
|
101
|
+
review_type: str = "integration",
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
require_initialized(paths)
|
|
104
|
+
summary = summary.strip()
|
|
105
|
+
evidence = evidence.strip()
|
|
106
|
+
review_type = review_type.strip()
|
|
107
|
+
if not summary:
|
|
108
|
+
raise InvalidInputError("--summary is required to record a checkpoint.", details={"field": "summary"})
|
|
109
|
+
if not evidence:
|
|
110
|
+
raise InvalidInputError("--evidence is required to record a checkpoint.", details={"field": "evidence"})
|
|
111
|
+
if review_type not in CHECKPOINT_REVIEW_TYPES:
|
|
112
|
+
raise InvalidInputError(
|
|
113
|
+
f"Invalid checkpoint review type: {review_type}",
|
|
114
|
+
details={"review_type": review_type, "allowed": sorted(CHECKPOINT_REVIEW_TYPES)},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
before = checkpoint_status(paths)
|
|
118
|
+
conn = connect(paths.db_path)
|
|
119
|
+
try:
|
|
120
|
+
evidence_id = record_inline_evidence(
|
|
121
|
+
conn,
|
|
122
|
+
evidence_type="checkpoint_review",
|
|
123
|
+
summary=evidence,
|
|
124
|
+
context=f"checkpoint/{review_type}",
|
|
125
|
+
command="pcl checkpoint record",
|
|
126
|
+
)
|
|
127
|
+
payload = {
|
|
128
|
+
"summary": summary,
|
|
129
|
+
"evidence": evidence,
|
|
130
|
+
"evidence_id": evidence_id,
|
|
131
|
+
"review_type": review_type,
|
|
132
|
+
"status_before": {
|
|
133
|
+
"threshold": before["threshold"],
|
|
134
|
+
"completed_features_since_checkpoint": before["completed_features_since_checkpoint"],
|
|
135
|
+
"completed_feature_ids_since_checkpoint": before["completed_feature_ids_since_checkpoint"],
|
|
136
|
+
"passed_workflow_runs_since_checkpoint": before["passed_workflow_runs_since_checkpoint"],
|
|
137
|
+
"feature_status_counts": before["feature_status_counts"],
|
|
138
|
+
"open_goal_count": before["open_goal_count"],
|
|
139
|
+
"git": before["git"],
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
event_id = append_event(
|
|
143
|
+
conn=conn,
|
|
144
|
+
events_path=paths.events_path,
|
|
145
|
+
event_type="checkpoint_recorded",
|
|
146
|
+
entity_type="checkpoint",
|
|
147
|
+
entity_id=evidence_id,
|
|
148
|
+
payload=payload,
|
|
149
|
+
)
|
|
150
|
+
conn.commit()
|
|
151
|
+
after = checkpoint_status(paths)
|
|
152
|
+
return {
|
|
153
|
+
"ok": True,
|
|
154
|
+
"checkpoint_id": evidence_id,
|
|
155
|
+
"event_id": event_id,
|
|
156
|
+
"evidence_id": evidence_id,
|
|
157
|
+
"review_type": review_type,
|
|
158
|
+
"summary": summary,
|
|
159
|
+
"status_before": payload["status_before"],
|
|
160
|
+
"status_after": {
|
|
161
|
+
"checkpoint_recommended": after["checkpoint_recommended"],
|
|
162
|
+
"completed_features_since_checkpoint": after["completed_features_since_checkpoint"],
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
finally:
|
|
166
|
+
conn.close()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _git_status(paths: ProjectPaths) -> dict[str, Any]:
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["git", "status", "--porcelain"],
|
|
173
|
+
cwd=paths.root,
|
|
174
|
+
check=False,
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
timeout=5,
|
|
178
|
+
)
|
|
179
|
+
except (OSError, subprocess.SubprocessError):
|
|
180
|
+
return {"available": False, "dirty_worktree": False, "dirty_file_count": 0, "porcelain": []}
|
|
181
|
+
if result.returncode != 0:
|
|
182
|
+
return {"available": False, "dirty_worktree": False, "dirty_file_count": 0, "porcelain": []}
|
|
183
|
+
lines = [line for line in result.stdout.splitlines() if line.strip()]
|
|
184
|
+
return {
|
|
185
|
+
"available": True,
|
|
186
|
+
"dirty_worktree": bool(lines),
|
|
187
|
+
"dirty_file_count": len(lines),
|
|
188
|
+
"porcelain": lines[:50],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _done_feature_ids(rows) -> list[str]:
|
|
193
|
+
done: dict[str, None] = {}
|
|
194
|
+
for row in rows:
|
|
195
|
+
try:
|
|
196
|
+
payload = json.loads(str(row["payload_json"] or "{}"))
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
continue
|
|
199
|
+
if isinstance(payload, dict) and payload.get("status") == "done":
|
|
200
|
+
done[str(row["entity_id"])] = None
|
|
201
|
+
return sorted(done)
|