program-context-protocol 0.12.4__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.
- pcp/__init__.py +3 -0
- pcp/assertions.py +152 -0
- pcp/attest.py +111 -0
- pcp/build_loop_bypass.py +76 -0
- pcp/build_report.py +54 -0
- pcp/capture.py +339 -0
- pcp/cli.py +104 -0
- pcp/commands/__init__.py +0 -0
- pcp/commands/amend.py +283 -0
- pcp/commands/architect_review.py +291 -0
- pcp/commands/architecture_justification.py +164 -0
- pcp/commands/audit.py +371 -0
- pcp/commands/build.py +4523 -0
- pcp/commands/build_plan.py +153 -0
- pcp/commands/build_status.py +83 -0
- pcp/commands/capture.py +72 -0
- pcp/commands/check.py +584 -0
- pcp/commands/context.py +151 -0
- pcp/commands/control_audit_cmd.py +54 -0
- pcp/commands/correct_objective.py +160 -0
- pcp/commands/dashboard.py +732 -0
- pcp/commands/deploy.py +199 -0
- pcp/commands/deploy_check.py +134 -0
- pcp/commands/design_audit.py +323 -0
- pcp/commands/diff.py +153 -0
- pcp/commands/diff_reduce.py +355 -0
- pcp/commands/docs.py +538 -0
- pcp/commands/doctor.py +820 -0
- pcp/commands/escalations_cmd.py +64 -0
- pcp/commands/gate.py +209 -0
- pcp/commands/import_project.py +404 -0
- pcp/commands/init.py +1634 -0
- pcp/commands/install_hook.py +283 -0
- pcp/commands/install_skill.py +48 -0
- pcp/commands/kickoff.py +772 -0
- pcp/commands/narrative_lint.py +54 -0
- pcp/commands/objective_conflicts_cmd.py +68 -0
- pcp/commands/pm.py +504 -0
- pcp/commands/pressure_test_cmd.py +72 -0
- pcp/commands/provenance.py +313 -0
- pcp/commands/prune.py +179 -0
- pcp/commands/report.py +49 -0
- pcp/commands/run_log_cmd.py +122 -0
- pcp/commands/scan.py +346 -0
- pcp/commands/self_update.py +125 -0
- pcp/commands/status.py +180 -0
- pcp/commands/takeover.py +55 -0
- pcp/commands/telemetry_cmd.py +167 -0
- pcp/commands/validate_module.py +153 -0
- pcp/commands/validate_strategy.py +413 -0
- pcp/commands/verify.py +166 -0
- pcp/commands/verify_syntax_fix.py +74 -0
- pcp/commands/watch.py +372 -0
- pcp/config_audit.py +141 -0
- pcp/context_map.py +124 -0
- pcp/control_audit.py +159 -0
- pcp/coupling.py +178 -0
- pcp/coverage_audit.py +77 -0
- pcp/decision_log.py +134 -0
- pcp/discovery/__init__.py +0 -0
- pcp/discovery/clusters.py +124 -0
- pcp/discovery/graph.py +110 -0
- pcp/discovery/scanner.py +109 -0
- pcp/escalations.py +193 -0
- pcp/evidence.py +30 -0
- pcp/evidence_chain.py +56 -0
- pcp/impact.py +164 -0
- pcp/install_approvals.py +44 -0
- pcp/integrity_audit.py +176 -0
- pcp/librarian.py +89 -0
- pcp/llm/__init__.py +0 -0
- pcp/llm/client.py +183 -0
- pcp/llm/coding_agent_contract.py +104 -0
- pcp/llm/harness/__init__.py +12 -0
- pcp/llm/harness/agy.py +121 -0
- pcp/llm/harness/agy_coding_loop.py +180 -0
- pcp/llm/harness/claude.py +241 -0
- pcp/llm/ledger.py +47 -0
- pcp/narrative_lint.py +229 -0
- pcp/nav_graph.py +226 -0
- pcp/objective_conflicts.py +129 -0
- pcp/operational.py +70 -0
- pcp/orphaned_work.py +262 -0
- pcp/pcp_dir.py +35 -0
- pcp/pcp_status.py +313 -0
- pcp/policy.py +81 -0
- pcp/pressure_test.py +196 -0
- pcp/qa.py +445 -0
- pcp/run_log.py +225 -0
- pcp/schema/__init__.py +0 -0
- pcp/schema/ci_rules.schema.json +106 -0
- pcp/schema/controls.schema.json +39 -0
- pcp/schema/module_acceptance.schema.json +144 -0
- pcp/schema/module_spec.schema.json +78 -0
- pcp/schema/sdlc_phase.schema.json +52 -0
- pcp/schema/validator.py +77 -0
- pcp/skill_data/pcp/SKILL.md +1897 -0
- pcp/spec_write.py +269 -0
- pcp/spend.py +77 -0
- pcp/symbols.py +86 -0
- pcp/telemetry.py +308 -0
- pcp/uat.py +271 -0
- pcp/version_drift.py +222 -0
- program_context_protocol-0.12.4.dist-info/METADATA +123 -0
- program_context_protocol-0.12.4.dist-info/RECORD +109 -0
- program_context_protocol-0.12.4.dist-info/WHEEL +4 -0
- program_context_protocol-0.12.4.dist-info/entry_points.txt +2 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-APACHE +202 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-MIT +21 -0
pcp/commands/status.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""pcp status — generate or refresh pcp.md governance snapshot."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from pcp.pcp_dir import find_pcp_dir, get_modules_dir, NoPCPDir
|
|
12
|
+
from pcp.schema.validator import validate_file, load_yaml
|
|
13
|
+
from pcp.pcp_status import write_pcp_md
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_modules_results(modules_dir: Path, project_root: Path) -> list[dict]:
|
|
19
|
+
"""Reconstruct module results from existing current_state.md or live scan."""
|
|
20
|
+
results = []
|
|
21
|
+
for af in sorted(modules_dir.glob("*/acceptance.yaml")):
|
|
22
|
+
module_name = af.parent.name
|
|
23
|
+
data = load_yaml(af)
|
|
24
|
+
criteria_results = []
|
|
25
|
+
for c in data.get("criteria", []):
|
|
26
|
+
criteria_results.append({
|
|
27
|
+
"id": c["id"],
|
|
28
|
+
"description": c["description"],
|
|
29
|
+
"check": c.get("check", "manual"),
|
|
30
|
+
"status": c.get("status", "pending"),
|
|
31
|
+
"detail": "",
|
|
32
|
+
})
|
|
33
|
+
results.append({"module": module_name, "criteria": criteria_results})
|
|
34
|
+
return results
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_from_current_state(current_state_path: Path) -> dict[str, str]:
|
|
38
|
+
"""Parse status from current_state.md — avoids re-running all checks."""
|
|
39
|
+
if not current_state_path.exists():
|
|
40
|
+
return {}
|
|
41
|
+
statuses = {}
|
|
42
|
+
for line in current_state_path.read_text().splitlines():
|
|
43
|
+
m = re.match(r"- \[([ x])\] ([A-Z0-9_-]+/[A-Z][0-9]+):", line.strip())
|
|
44
|
+
if m:
|
|
45
|
+
statuses[m.group(2)] = "complete" if m.group(1) == "x" else "pending"
|
|
46
|
+
return statuses
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
PM_STATUS_SYSTEM_PROMPT = """\
|
|
50
|
+
You are an expert AI product manager.
|
|
51
|
+
Your task is to generate a plain-English project status report for a non-technical PM based on the provided project context and git history.
|
|
52
|
+
|
|
53
|
+
The status report must be user-friendly, high-level, and avoid code snippets, technical jargon, or raw config file content.
|
|
54
|
+
It must include:
|
|
55
|
+
1. Current SDLC Phase and Completion % (calculated from criteria complete/total).
|
|
56
|
+
2. This Week's Progress (derived from recent git commits and completed criteria).
|
|
57
|
+
3. Current Blockers / PM Actions Needed (e.g. pending manual criteria, inputs/decisions needed).
|
|
58
|
+
4. What's Next (what features/modules will be developed next).
|
|
59
|
+
|
|
60
|
+
Keep it concise, clear, and action-oriented. Use formatting like bold text and bullet points.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_recent_commits(repo_path: Path) -> str:
|
|
65
|
+
import subprocess
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
["git", "log", "--since=7.days.ago", "--oneline"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
cwd=repo_path,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
return "No recent commits."
|
|
74
|
+
return result.stdout
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command()
|
|
78
|
+
@click.option("--path", "project_path", type=click.Path(), default=None)
|
|
79
|
+
@click.option("--rescan", is_flag=True,
|
|
80
|
+
help="Re-run full scan before writing pcp.md (slower but accurate).")
|
|
81
|
+
@click.option("--print", "print_only", is_flag=True,
|
|
82
|
+
help="Print pcp.md to stdout instead of writing file.")
|
|
83
|
+
@click.option("--pm", "pm_mode", is_flag=True,
|
|
84
|
+
help="Generate a plain-English status report for the PM.")
|
|
85
|
+
def status(project_path: str | None, rescan: bool, print_only: bool, pm_mode: bool):
|
|
86
|
+
"""Generate or refresh pcp.md governance snapshot at project root.
|
|
87
|
+
|
|
88
|
+
By default reads from existing current_state.md (fast).
|
|
89
|
+
Use --rescan to re-evaluate all acceptance criteria first.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
93
|
+
except NoPCPDir as e:
|
|
94
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
95
|
+
sys.exit(2)
|
|
96
|
+
|
|
97
|
+
project_root = pcp_dir.parent
|
|
98
|
+
modules_dir = get_modules_dir(pcp_dir)
|
|
99
|
+
|
|
100
|
+
if rescan:
|
|
101
|
+
# Invoke scan command logic inline
|
|
102
|
+
from pcp.commands.scan import _scan_module, _write_current_state, _load_prior_manual_status
|
|
103
|
+
current_state_path = pcp_dir / "current_state.md"
|
|
104
|
+
prior_manual = _load_prior_manual_status(current_state_path)
|
|
105
|
+
modules_results = []
|
|
106
|
+
for af in sorted(modules_dir.glob("*/acceptance.yaml")):
|
|
107
|
+
module_name = af.parent.name
|
|
108
|
+
result = _scan_module(module_name, af, project_root, prior_manual)
|
|
109
|
+
modules_results.append(result)
|
|
110
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
111
|
+
_write_current_state(pcp_dir, modules_results, timestamp)
|
|
112
|
+
else:
|
|
113
|
+
# Fast path: read from current_state.md
|
|
114
|
+
statuses = _parse_from_current_state(pcp_dir / "current_state.md")
|
|
115
|
+
modules_results = _load_modules_results(modules_dir, project_root)
|
|
116
|
+
# Apply parsed statuses
|
|
117
|
+
for m in modules_results:
|
|
118
|
+
for c in m["criteria"]:
|
|
119
|
+
key = f"{m['module'].upper()}/{c['id']}"
|
|
120
|
+
if key in statuses:
|
|
121
|
+
c["status"] = statuses[key]
|
|
122
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
123
|
+
|
|
124
|
+
total = sum(len(m["criteria"]) for m in modules_results)
|
|
125
|
+
complete = sum(1 for m in modules_results for c in m["criteria"] if c["status"] == "complete")
|
|
126
|
+
|
|
127
|
+
pcp_md_path = write_pcp_md(pcp_dir, modules_results, timestamp, total, complete)
|
|
128
|
+
|
|
129
|
+
if pm_mode:
|
|
130
|
+
from pcp.llm import client as llm
|
|
131
|
+
obj_text = (pcp_dir / "objective.md").read_text() if (pcp_dir / "objective.md").exists() else ""
|
|
132
|
+
current_state_text = (pcp_dir / "current_state.md").read_text() if (pcp_dir / "current_state.md").exists() else ""
|
|
133
|
+
sdlc_text = (pcp_dir / "SDLC_phase.yaml").read_text() if (pcp_dir / "SDLC_phase.yaml").exists() else ""
|
|
134
|
+
commits = _get_recent_commits(project_root)
|
|
135
|
+
|
|
136
|
+
score_pct = f"{complete}/{total} ({complete/total:.0%})" if total else "0/0 (0%)"
|
|
137
|
+
|
|
138
|
+
user_prompt = (
|
|
139
|
+
f"Objective:\n{obj_text}\n\n"
|
|
140
|
+
f"Current State & Completion:\nCompletion Score: {score_pct}\n{current_state_text}\n\n"
|
|
141
|
+
f"SDLC Phase Config:\n{sdlc_text}\n\n"
|
|
142
|
+
f"Recent Commits (last 7 days):\n{commits}\n"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
console.print("[dim]Generating plain-English PM status report...[/dim]\n")
|
|
146
|
+
try:
|
|
147
|
+
report_text = llm.call(
|
|
148
|
+
PM_STATUS_SYSTEM_PROMPT, user_prompt,
|
|
149
|
+
model=llm.JUDGE_MODEL, pcp_dir=pcp_dir, command="status-pm",
|
|
150
|
+
)
|
|
151
|
+
click.echo(report_text)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
console.print(f"[red]Error generating PM report:[/red] {e}")
|
|
154
|
+
sys.exit(2)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if print_only:
|
|
158
|
+
click.echo(pcp_md_path.read_text())
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
score = complete / total if total else 0.0
|
|
162
|
+
color = "green" if score >= 0.8 else "yellow" if score >= 0.5 else "red"
|
|
163
|
+
console.print(f"[{color}]{complete}/{total} ({score:.0%})[/{color}] → pcp.md")
|
|
164
|
+
|
|
165
|
+
# Escalation-acknowledgment watchdog — an escalation recorded but never
|
|
166
|
+
# acted on must stay loudly visible, not buried in escalations.yaml.
|
|
167
|
+
from pcp import escalations
|
|
168
|
+
for e in escalations.find_stale(pcp_dir):
|
|
169
|
+
console.print(
|
|
170
|
+
f"[red bold]STALE ESCALATION [{e.get('state', 'unacked')}]:[/red bold] "
|
|
171
|
+
f"{e.get('module')}/{e.get('criterion_id')} ({e.get('category', 'uncategorized')}) "
|
|
172
|
+
f"waiting {e.get('age_hours')}h — ack: pcp escalations --ack {e.get('module')}/{e.get('criterion_id')}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Notification dead-man's-switch — attempts without successes means the
|
|
176
|
+
# pipeline is broken and nobody is being reached.
|
|
177
|
+
from pcp.commands.watch import check_notify_heartbeat
|
|
178
|
+
hb = check_notify_heartbeat(pcp_dir)
|
|
179
|
+
if hb:
|
|
180
|
+
console.print(f"[red bold]{hb}[/red bold]")
|
pcp/commands/takeover.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""pcp takeover — one-shot: preflight -> kickoff -> autonomous build.
|
|
2
|
+
|
|
3
|
+
The single entrypoint for "point pcp at a vision doc and let it run the
|
|
4
|
+
project." Chains the existing doctor/kickoff/build commands rather than
|
|
5
|
+
duplicating their logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import yaml
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from pcp.commands.build import build
|
|
16
|
+
from pcp.commands.doctor import check_environment
|
|
17
|
+
from pcp.commands.kickoff import kickoff
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _current_phase(pcp_dir: Path) -> str | None:
|
|
23
|
+
path = pcp_dir / "SDLC_phase.yaml"
|
|
24
|
+
if not path.exists():
|
|
25
|
+
return None
|
|
26
|
+
return (yaml.safe_load(path.read_text()) or {}).get("current_phase")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
@click.argument("vision_file", type=click.Path(exists=True))
|
|
31
|
+
@click.option("--path", "project_path", type=click.Path(), default=".",
|
|
32
|
+
help="Project root (default: current directory).")
|
|
33
|
+
@click.option("--force", is_flag=True, help="Force overwrite existing .pcp/ directory.")
|
|
34
|
+
def takeover(vision_file: str, project_path: str, force: bool):
|
|
35
|
+
"""Take over a project end-to-end: preflight, kickoff from a vision doc, then build every pending criterion."""
|
|
36
|
+
root = Path(project_path).resolve()
|
|
37
|
+
pcp_dir = root / ".pcp"
|
|
38
|
+
ctx = click.get_current_context()
|
|
39
|
+
|
|
40
|
+
console.print("[bold]Step 1/3 — environment preflight[/bold]")
|
|
41
|
+
check_environment(pcp_dir, fatal_on_missing_required=True)
|
|
42
|
+
|
|
43
|
+
console.print("\n[bold]Step 2/3 — kickoff from vision[/bold]")
|
|
44
|
+
ctx.invoke(kickoff, vision_file=vision_file, project_path=project_path, force=force)
|
|
45
|
+
|
|
46
|
+
phase = _current_phase(pcp_dir)
|
|
47
|
+
if phase in (None, "planning"):
|
|
48
|
+
console.print(
|
|
49
|
+
"\n[yellow]Strategy not approved — stopping before build. "
|
|
50
|
+
"Re-run `pcp takeover` once you're ready.[/yellow]"
|
|
51
|
+
)
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
console.print("\n[bold]Step 3/3 — autonomous build[/bold]")
|
|
55
|
+
ctx.invoke(build, module_name=None, project_path=project_path)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""pcp telemetry — summarize .pcp/telemetry.jsonl for analysis.
|
|
2
|
+
|
|
3
|
+
Closes the loop on "captured to analyse and learn": telemetry.jsonl has the raw
|
|
4
|
+
per-attempt/per-qa-check records, this command rolls them up so the data is
|
|
5
|
+
actually useful without hand-writing a notebook every time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
17
|
+
from pcp import telemetry as telemetry_lib
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command(name="telemetry")
|
|
23
|
+
@click.option("--path", "project_path", type=click.Path(), default=None,
|
|
24
|
+
help="Project root (default: cwd, walks up to find .pcp/).")
|
|
25
|
+
@click.option("--json", "output_json", is_flag=True, help="Print raw per-module aggregates as JSON.")
|
|
26
|
+
def telemetry_cmd(project_path: str | None, output_json: bool):
|
|
27
|
+
"""Summarize .pcp/telemetry.jsonl — per-module retries, QA error rate, languages, cost."""
|
|
28
|
+
try:
|
|
29
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
30
|
+
except NoPCPDir as e:
|
|
31
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
32
|
+
sys.exit(2)
|
|
33
|
+
|
|
34
|
+
records = telemetry_lib.load(pcp_dir)
|
|
35
|
+
if not records:
|
|
36
|
+
console.print("[dim]No .pcp/telemetry.jsonl found — run `pcp build` first.[/dim]")
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
|
|
39
|
+
agg = telemetry_lib.aggregate(records)
|
|
40
|
+
by_module = agg["by_module"]
|
|
41
|
+
|
|
42
|
+
if output_json:
|
|
43
|
+
click.echo(json.dumps({
|
|
44
|
+
k: {**v, "criteria": sorted(filter(None, v["criteria"])), "languages": sorted(v["languages"])}
|
|
45
|
+
for k, v in by_module.items()
|
|
46
|
+
}, indent=2))
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
table = Table(title="PCP Build Telemetry — per module")
|
|
50
|
+
table.add_column("Module")
|
|
51
|
+
table.add_column("Criteria")
|
|
52
|
+
table.add_column("Attempts")
|
|
53
|
+
table.add_column("Avg attempts/criterion")
|
|
54
|
+
table.add_column("QA error rate")
|
|
55
|
+
table.add_column("Tokens in/cache/out")
|
|
56
|
+
table.add_column("Cost")
|
|
57
|
+
table.add_column("Languages")
|
|
58
|
+
|
|
59
|
+
total_cost = 0.0
|
|
60
|
+
for m_name, v in sorted(by_module.items()):
|
|
61
|
+
n_crit = len(v["criteria"]) or 1
|
|
62
|
+
avg_attempts = v["attempts"] / n_crit
|
|
63
|
+
qa_rate = f"{v['qa_blocks']}/{v['qa_total']}" if v["qa_total"] else "—"
|
|
64
|
+
table.add_row(
|
|
65
|
+
m_name, str(len(v["criteria"])), str(v["attempts"]), f"{avg_attempts:.1f}",
|
|
66
|
+
qa_rate, f"{v['tokens_in']:,}/{v['tokens_cache_read']:,}/{v['tokens_out']:,}",
|
|
67
|
+
f"${v['cost']:.2f}", ", ".join(sorted(v["languages"])) or "—",
|
|
68
|
+
)
|
|
69
|
+
total_cost += v["cost"]
|
|
70
|
+
|
|
71
|
+
console.print(table)
|
|
72
|
+
|
|
73
|
+
# Worktree merge conflict rate — comparable against AgenticFlict's
|
|
74
|
+
# (arXiv:2604.03551) 27.67% agent-authored-PR conflict baseline.
|
|
75
|
+
merges = [r for r in records if r.get("check") == "worktree-merge"]
|
|
76
|
+
if merges:
|
|
77
|
+
conflicts = sum(1 for r in merges if r.get("result") == "block")
|
|
78
|
+
console.print(
|
|
79
|
+
f"[dim]Worktree merges: {len(merges)}, conflicts: {conflicts} "
|
|
80
|
+
f"({conflicts / len(merges):.0%}) — agentic-PR literature baseline 27.67% (AgenticFlict)[/dim]"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Output per dollar over time. Nothing reported this, so a real ~6x
|
|
84
|
+
# degradation on Project O went unseen for a week while commits/day
|
|
85
|
+
# rose — the flattering metric was the only visible one.
|
|
86
|
+
weeks = telemetry_lib.productivity_by_week(records)
|
|
87
|
+
if len(weeks) > 1:
|
|
88
|
+
# Written vs landed, side by side. Telemetry alone reads healthy — on
|
|
89
|
+
# Project O it called 2026-W31 the most productive week of the run
|
|
90
|
+
# (+12,342 lines, $0.018/line) while git says non-test code grew by 599.
|
|
91
|
+
# The ratio between them is the signal; neither number is, alone.
|
|
92
|
+
repo = telemetry_lib.repo_net_lines_by_week(pcp_dir.parent)
|
|
93
|
+
landed = repo["by_week"]
|
|
94
|
+
wt = Table(title="Output per dollar — by week")
|
|
95
|
+
wt.add_column("Week")
|
|
96
|
+
wt.add_column("Attempts", justify="right")
|
|
97
|
+
wt.add_column("Spend", justify="right")
|
|
98
|
+
wt.add_column("Lines written", justify="right")
|
|
99
|
+
if landed:
|
|
100
|
+
wt.add_column("Landed (non-test)", justify="right")
|
|
101
|
+
wt.add_column("Survived", justify="right")
|
|
102
|
+
wt.add_column("$/landed line", justify="right")
|
|
103
|
+
else:
|
|
104
|
+
wt.add_column("$/written line", justify="right")
|
|
105
|
+
|
|
106
|
+
for w in weeks:
|
|
107
|
+
row = [w["week"], str(w["attempts"]), f"${w['cost_usd']:.2f}",
|
|
108
|
+
f"{w['net_lines']:+,}"]
|
|
109
|
+
if landed:
|
|
110
|
+
net = landed.get(w["week"])
|
|
111
|
+
if net is None:
|
|
112
|
+
row += ["—", "—", "—"]
|
|
113
|
+
else:
|
|
114
|
+
row.append(f"{net:+,}")
|
|
115
|
+
written = w["net_lines"]
|
|
116
|
+
if net > written:
|
|
117
|
+
# More landed than the build loop recorded writing, so code
|
|
118
|
+
# arrived by some other path — an ad-hoc agent, a hand edit,
|
|
119
|
+
# a bulk merge. That is CTRL-037's story showing up in the
|
|
120
|
+
# numbers, and printing it as "5185% survived" would bury it.
|
|
121
|
+
row.append("[yellow]outside loop[/yellow]")
|
|
122
|
+
elif written > 0:
|
|
123
|
+
row.append(f"{net / written:.0%}")
|
|
124
|
+
else:
|
|
125
|
+
row.append("—")
|
|
126
|
+
# A week that spent money and landed nothing is the single most
|
|
127
|
+
# important row to see — never render it as a tidy $0.00.
|
|
128
|
+
row.append(f"${w['cost_usd'] / net:.2f}" if net > 0
|
|
129
|
+
else "[red]nothing landed[/red]")
|
|
130
|
+
else:
|
|
131
|
+
per = w["usd_per_net_line"]
|
|
132
|
+
row.append(f"${per:.3f}" if per is not None
|
|
133
|
+
else ("[red]no net output[/red]" if w["cost_usd"] > 0 else "—"))
|
|
134
|
+
wt.add_row(*row)
|
|
135
|
+
console.print(wt)
|
|
136
|
+
|
|
137
|
+
if landed:
|
|
138
|
+
console.print(
|
|
139
|
+
"[dim]`Lines written` is every attempt's diff, so it includes superseded "
|
|
140
|
+
"attempts, reverts and test code. `Landed` is net authored non-test source "
|
|
141
|
+
"that survived in git. `Survived` is the ratio — the number actually worth "
|
|
142
|
+
"watching, since a high written count with a low survival rate is churn, "
|
|
143
|
+
"not velocity. `outside loop` means more landed than the build loop recorded "
|
|
144
|
+
"writing, i.e. code arrived by some other path.[/dim]"
|
|
145
|
+
)
|
|
146
|
+
# No silent caps: say what was excluded and why.
|
|
147
|
+
skipped = repo["bulk_commits_skipped"]
|
|
148
|
+
if skipped:
|
|
149
|
+
detail = ", ".join(f"{w} ({n})" for w, n in sorted(skipped.items()))
|
|
150
|
+
console.print(
|
|
151
|
+
f"[dim]Excluded {sum(skipped.values())} bulk commit(s) over "
|
|
152
|
+
f"{repo['bulk_threshold']:,} changed source lines — {detail}. Those are "
|
|
153
|
+
"vendor imports, bulk moves or generated dumps, not one criterion's "
|
|
154
|
+
"work; counting them made survival read 107372% on a real project.[/dim]"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
console.print(
|
|
158
|
+
"[dim]git unavailable — showing written lines only. These count every "
|
|
159
|
+
"attempt's diff including superseded work, so treat $/written line as a "
|
|
160
|
+
"trend, not an accounting figure.[/dim]"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
console.print(
|
|
164
|
+
f"\n[dim]{len(records)} total records — {len(agg['build_records'])} build, "
|
|
165
|
+
f"{len(agg['qa_records'])} qa — total cost ~${total_cost:.2f}[/dim]"
|
|
166
|
+
)
|
|
167
|
+
console.print("[dim]Raw data: .pcp/telemetry.jsonl (one JSON object per line — load into pandas/duckdb for deeper analysis)[/dim]")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""pcp validate-module <name> — per-module spec alignment check."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from pcp.pcp_dir import find_pcp_dir, get_modules_dir, get_objective, get_decomposition, NoPCPDir
|
|
12
|
+
from pcp.schema.validator import validate_file, load_yaml
|
|
13
|
+
from pcp.llm import client as llm
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
SYSTEM_PROMPT = """\
|
|
18
|
+
You are a program-context auditor. Check whether a single module specification \
|
|
19
|
+
is aligned with the program objective and decomposition rationale.
|
|
20
|
+
|
|
21
|
+
Output ONLY valid JSON — no prose, no markdown, no code fences.
|
|
22
|
+
|
|
23
|
+
Output schema:
|
|
24
|
+
{
|
|
25
|
+
"alignment_score": 0.0,
|
|
26
|
+
"aligned": true,
|
|
27
|
+
"gaps": ["string"],
|
|
28
|
+
"contradictions": ["string"],
|
|
29
|
+
"decomposition_conflicts": ["string"],
|
|
30
|
+
"suggestions": ["string"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
alignment_score: 0.0 (no alignment) to 1.0 (perfectly aligned).
|
|
34
|
+
aligned: true if alignment_score >= 0.7 and no contradictions.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_prompt(objective: str, decomposition: str | None, module_name: str, spec: dict) -> str:
|
|
39
|
+
parts = [f"## Program Objective\n\n{objective}\n"]
|
|
40
|
+
if decomposition:
|
|
41
|
+
parts.append(f"## Decomposition Rationale\n\n{decomposition}\n")
|
|
42
|
+
parts.append(f"## Module to Validate: {module_name}\n")
|
|
43
|
+
parts.append(f"```yaml\n{yaml.dump(spec, default_flow_style=False)}```\n")
|
|
44
|
+
parts.append(
|
|
45
|
+
f"Does the '{module_name}' module spec align with the objective and decomposition? "
|
|
46
|
+
"Are its objective_coverage claims accurate? Does it conflict with the decomposition?"
|
|
47
|
+
)
|
|
48
|
+
return "\n".join(parts)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_validate_module(pcp_dir: Path, module_name: str) -> dict | None:
|
|
52
|
+
"""Core module-vs-decomposition alignment check, reusable outside the CLI
|
|
53
|
+
command (e.g. build.py's wave-merge gate). Returns the LLM judge's result
|
|
54
|
+
dict, or None if the module is missing/deprecated/objective.md absent --
|
|
55
|
+
callers decide what "no result" means for their own control flow, same
|
|
56
|
+
posture as run_validate_strategy."""
|
|
57
|
+
modules_dir = get_modules_dir(pcp_dir)
|
|
58
|
+
spec_path = modules_dir / module_name / "spec.yaml"
|
|
59
|
+
if not spec_path.exists():
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
validate_file(spec_path, "module_spec") # schema errors surfaced by the CLI wrapper, not fatal here
|
|
63
|
+
|
|
64
|
+
spec = load_yaml(spec_path)
|
|
65
|
+
if spec.get("deprecated"):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
objective_path = get_objective(pcp_dir)
|
|
69
|
+
if not objective_path.exists():
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
objective = objective_path.read_text()
|
|
73
|
+
decomp_path = get_decomposition(pcp_dir)
|
|
74
|
+
decomposition = decomp_path.read_text() if decomp_path.exists() else None
|
|
75
|
+
|
|
76
|
+
return llm.call_json(
|
|
77
|
+
SYSTEM_PROMPT, _build_prompt(objective, decomposition, module_name, spec),
|
|
78
|
+
model=llm.JUDGE_MODEL, pcp_dir=pcp_dir, command="validate-module",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.command()
|
|
83
|
+
@click.argument("module_name")
|
|
84
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
85
|
+
@click.option("--path", "project_path", type=click.Path(), default=None)
|
|
86
|
+
def validate_module(module_name: str, output_json: bool, project_path: str | None):
|
|
87
|
+
"""Check whether a module spec aligns with the objective and decomposition."""
|
|
88
|
+
try:
|
|
89
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
90
|
+
except NoPCPDir as e:
|
|
91
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
92
|
+
sys.exit(2)
|
|
93
|
+
|
|
94
|
+
modules_dir = get_modules_dir(pcp_dir)
|
|
95
|
+
spec_path = modules_dir / module_name / "spec.yaml"
|
|
96
|
+
|
|
97
|
+
if not spec_path.exists():
|
|
98
|
+
console.print(f"[red]Module '{module_name}' not found:[/red] {spec_path}")
|
|
99
|
+
console.print(f"Available: {', '.join(p.parent.name for p in modules_dir.glob('*/spec.yaml'))}")
|
|
100
|
+
sys.exit(2)
|
|
101
|
+
|
|
102
|
+
errors = validate_file(spec_path, "module_spec")
|
|
103
|
+
if errors:
|
|
104
|
+
console.print(f"[yellow]⚠ schema errors in {module_name}/spec.yaml:[/yellow]")
|
|
105
|
+
for e in errors:
|
|
106
|
+
console.print(f" {e}")
|
|
107
|
+
|
|
108
|
+
spec = load_yaml(spec_path)
|
|
109
|
+
if spec.get("deprecated"):
|
|
110
|
+
console.print(f"[dim]{module_name} is deprecated — skipping.[/dim]")
|
|
111
|
+
sys.exit(0)
|
|
112
|
+
|
|
113
|
+
objective_path = get_objective(pcp_dir)
|
|
114
|
+
if not objective_path.exists():
|
|
115
|
+
console.print("[red]Error:[/red] .pcp/objective.md not found.")
|
|
116
|
+
sys.exit(2)
|
|
117
|
+
|
|
118
|
+
if not output_json:
|
|
119
|
+
console.print(f"[dim]Validating module '{module_name}'...[/dim]")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
result = run_validate_module(pcp_dir, module_name)
|
|
123
|
+
except RuntimeError as e:
|
|
124
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
125
|
+
sys.exit(2)
|
|
126
|
+
except ValueError as e:
|
|
127
|
+
console.print(f"[red]LLM returned invalid JSON:[/red] {e}")
|
|
128
|
+
sys.exit(2)
|
|
129
|
+
|
|
130
|
+
if output_json:
|
|
131
|
+
click.echo(json.dumps(result, indent=2))
|
|
132
|
+
sys.exit(0 if result.get("aligned") else 1)
|
|
133
|
+
|
|
134
|
+
score = result.get("alignment_score", 0.0)
|
|
135
|
+
aligned = result.get("aligned", False)
|
|
136
|
+
color = "green" if aligned else "red"
|
|
137
|
+
|
|
138
|
+
console.print(f"\n[bold]Module:[/bold] {module_name}")
|
|
139
|
+
console.print(f"[bold]Alignment:[/bold] [{color}]{score:.0%}[/{color}] {'✓' if aligned else '✗'}\n")
|
|
140
|
+
|
|
141
|
+
for gap in result.get("gaps", []):
|
|
142
|
+
console.print(f" [yellow]⚠ Gap:[/yellow] {gap}")
|
|
143
|
+
for c in result.get("contradictions", []):
|
|
144
|
+
console.print(f" [red]✗ Contradiction:[/red] {c}")
|
|
145
|
+
for dc in result.get("decomposition_conflicts", []):
|
|
146
|
+
console.print(f" [red]✗ Decomp conflict:[/red] {dc}")
|
|
147
|
+
for s in result.get("suggestions", []):
|
|
148
|
+
console.print(f" [dim]→ {s}[/dim]")
|
|
149
|
+
|
|
150
|
+
if aligned and not result.get("gaps") and not result.get("contradictions"):
|
|
151
|
+
console.print("[green]✓ Module spec is aligned.[/green]")
|
|
152
|
+
|
|
153
|
+
sys.exit(0 if aligned else 1)
|