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.
Files changed (52) hide show
  1. pcl/__init__.py +1 -0
  2. pcl/__main__.py +4 -0
  3. pcl/agents.py +501 -0
  4. pcl/checkpoints.py +201 -0
  5. pcl/cli.py +1404 -0
  6. pcl/commands.py +1006 -0
  7. pcl/db/migrations/001_initial.sql +180 -0
  8. pcl/db/schema.sql +180 -0
  9. pcl/db.py +49 -0
  10. pcl/decisions.py +275 -0
  11. pcl/errors.py +83 -0
  12. pcl/escalations.py +302 -0
  13. pcl/events.py +41 -0
  14. pcl/evidence.py +25 -0
  15. pcl/exporters.py +77 -0
  16. pcl/guards.py +14 -0
  17. pcl/ids.py +15 -0
  18. pcl/init_project.py +112 -0
  19. pcl/lifecycle.py +1073 -0
  20. pcl/links.py +108 -0
  21. pcl/mcp_server.py +328 -0
  22. pcl/migrations.py +220 -0
  23. pcl/paths.py +65 -0
  24. pcl/renderer.py +823 -0
  25. pcl/reports.py +766 -0
  26. pcl/resources.py +26 -0
  27. pcl/stories.py +762 -0
  28. pcl/templates/dashboard/dashboard.html +165 -0
  29. pcl/templates/project/AGENTS.block.md +16 -0
  30. pcl/templates/project/CLAUDE.block.md +13 -0
  31. pcl/templates/project/gitignore.fragment +13 -0
  32. pcl/templates/project/pcl.yaml +60 -0
  33. pcl/templates/skills/project-control-loop/SKILL.md +120 -0
  34. pcl/templates/workflows/defect_repair.yaml +61 -0
  35. pcl/templates/workflows/executor_smoke.yaml +32 -0
  36. pcl/templates/workflows/feature_coverage.yaml +52 -0
  37. pcl/templates/workflows/regression_loop.yaml +51 -0
  38. pcl/timeutil.py +7 -0
  39. pcl/validators.py +788 -0
  40. pcl/workflow_executor.py +911 -0
  41. pcl/workflow_proposal_validation.py +50 -0
  42. pcl/workflow_proposals.py +442 -0
  43. pcl/workflow_sandbox.py +683 -0
  44. pcl/workflow_verifier.py +333 -0
  45. pcl/workflow_yaml.py +190 -0
  46. pcl/workflows.py +569 -0
  47. project_loop_harness-0.1.2.dist-info/METADATA +361 -0
  48. project_loop_harness-0.1.2.dist-info/RECORD +52 -0
  49. project_loop_harness-0.1.2.dist-info/WHEEL +5 -0
  50. project_loop_harness-0.1.2.dist-info/entry_points.txt +3 -0
  51. project_loop_harness-0.1.2.dist-info/licenses/LICENSE +21 -0
  52. 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)