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.
Files changed (109) hide show
  1. pcp/__init__.py +3 -0
  2. pcp/assertions.py +152 -0
  3. pcp/attest.py +111 -0
  4. pcp/build_loop_bypass.py +76 -0
  5. pcp/build_report.py +54 -0
  6. pcp/capture.py +339 -0
  7. pcp/cli.py +104 -0
  8. pcp/commands/__init__.py +0 -0
  9. pcp/commands/amend.py +283 -0
  10. pcp/commands/architect_review.py +291 -0
  11. pcp/commands/architecture_justification.py +164 -0
  12. pcp/commands/audit.py +371 -0
  13. pcp/commands/build.py +4523 -0
  14. pcp/commands/build_plan.py +153 -0
  15. pcp/commands/build_status.py +83 -0
  16. pcp/commands/capture.py +72 -0
  17. pcp/commands/check.py +584 -0
  18. pcp/commands/context.py +151 -0
  19. pcp/commands/control_audit_cmd.py +54 -0
  20. pcp/commands/correct_objective.py +160 -0
  21. pcp/commands/dashboard.py +732 -0
  22. pcp/commands/deploy.py +199 -0
  23. pcp/commands/deploy_check.py +134 -0
  24. pcp/commands/design_audit.py +323 -0
  25. pcp/commands/diff.py +153 -0
  26. pcp/commands/diff_reduce.py +355 -0
  27. pcp/commands/docs.py +538 -0
  28. pcp/commands/doctor.py +820 -0
  29. pcp/commands/escalations_cmd.py +64 -0
  30. pcp/commands/gate.py +209 -0
  31. pcp/commands/import_project.py +404 -0
  32. pcp/commands/init.py +1634 -0
  33. pcp/commands/install_hook.py +283 -0
  34. pcp/commands/install_skill.py +48 -0
  35. pcp/commands/kickoff.py +772 -0
  36. pcp/commands/narrative_lint.py +54 -0
  37. pcp/commands/objective_conflicts_cmd.py +68 -0
  38. pcp/commands/pm.py +504 -0
  39. pcp/commands/pressure_test_cmd.py +72 -0
  40. pcp/commands/provenance.py +313 -0
  41. pcp/commands/prune.py +179 -0
  42. pcp/commands/report.py +49 -0
  43. pcp/commands/run_log_cmd.py +122 -0
  44. pcp/commands/scan.py +346 -0
  45. pcp/commands/self_update.py +125 -0
  46. pcp/commands/status.py +180 -0
  47. pcp/commands/takeover.py +55 -0
  48. pcp/commands/telemetry_cmd.py +167 -0
  49. pcp/commands/validate_module.py +153 -0
  50. pcp/commands/validate_strategy.py +413 -0
  51. pcp/commands/verify.py +166 -0
  52. pcp/commands/verify_syntax_fix.py +74 -0
  53. pcp/commands/watch.py +372 -0
  54. pcp/config_audit.py +141 -0
  55. pcp/context_map.py +124 -0
  56. pcp/control_audit.py +159 -0
  57. pcp/coupling.py +178 -0
  58. pcp/coverage_audit.py +77 -0
  59. pcp/decision_log.py +134 -0
  60. pcp/discovery/__init__.py +0 -0
  61. pcp/discovery/clusters.py +124 -0
  62. pcp/discovery/graph.py +110 -0
  63. pcp/discovery/scanner.py +109 -0
  64. pcp/escalations.py +193 -0
  65. pcp/evidence.py +30 -0
  66. pcp/evidence_chain.py +56 -0
  67. pcp/impact.py +164 -0
  68. pcp/install_approvals.py +44 -0
  69. pcp/integrity_audit.py +176 -0
  70. pcp/librarian.py +89 -0
  71. pcp/llm/__init__.py +0 -0
  72. pcp/llm/client.py +183 -0
  73. pcp/llm/coding_agent_contract.py +104 -0
  74. pcp/llm/harness/__init__.py +12 -0
  75. pcp/llm/harness/agy.py +121 -0
  76. pcp/llm/harness/agy_coding_loop.py +180 -0
  77. pcp/llm/harness/claude.py +241 -0
  78. pcp/llm/ledger.py +47 -0
  79. pcp/narrative_lint.py +229 -0
  80. pcp/nav_graph.py +226 -0
  81. pcp/objective_conflicts.py +129 -0
  82. pcp/operational.py +70 -0
  83. pcp/orphaned_work.py +262 -0
  84. pcp/pcp_dir.py +35 -0
  85. pcp/pcp_status.py +313 -0
  86. pcp/policy.py +81 -0
  87. pcp/pressure_test.py +196 -0
  88. pcp/qa.py +445 -0
  89. pcp/run_log.py +225 -0
  90. pcp/schema/__init__.py +0 -0
  91. pcp/schema/ci_rules.schema.json +106 -0
  92. pcp/schema/controls.schema.json +39 -0
  93. pcp/schema/module_acceptance.schema.json +144 -0
  94. pcp/schema/module_spec.schema.json +78 -0
  95. pcp/schema/sdlc_phase.schema.json +52 -0
  96. pcp/schema/validator.py +77 -0
  97. pcp/skill_data/pcp/SKILL.md +1897 -0
  98. pcp/spec_write.py +269 -0
  99. pcp/spend.py +77 -0
  100. pcp/symbols.py +86 -0
  101. pcp/telemetry.py +308 -0
  102. pcp/uat.py +271 -0
  103. pcp/version_drift.py +222 -0
  104. program_context_protocol-0.12.4.dist-info/METADATA +123 -0
  105. program_context_protocol-0.12.4.dist-info/RECORD +109 -0
  106. program_context_protocol-0.12.4.dist-info/WHEEL +4 -0
  107. program_context_protocol-0.12.4.dist-info/entry_points.txt +2 -0
  108. program_context_protocol-0.12.4.dist-info/licenses/LICENSE-APACHE +202 -0
  109. program_context_protocol-0.12.4.dist-info/licenses/LICENSE-MIT +21 -0
@@ -0,0 +1,153 @@
1
+ """pcp build-plan — deterministic execution plan, no agents spawned.
2
+
3
+ Root of tonight's redesign (2026-07-30): `pcp build`'s Python execution engine
4
+ (worktree-per-criterion, ThreadPoolExecutor, merge/retry-on-conflict) is one of
5
+ TWO independent orchestration implementations in this codebase -- the `/pcp`
6
+ skill's own SKILL.md already documents a second one, built on the Workflow
7
+ tool's `pipeline()`/`parallel()`, and CLAUDE.md describes both as if they were
8
+ the same mechanism. They aren't. The Python engine reinvents coordination the
9
+ harness already provides natively, and pays for it: measured on Project O,
10
+ 5 of 5 completed `query-eval-harness` criteria hit a merge conflict in one run,
11
+ 99% of that run's cost sat on the conflicted criteria, and two criteria were
12
+ still stuck mid-retry two hours in.
13
+
14
+ The concrete root cause, read directly out of the colliding file: every
15
+ criterion in a module has to add its own method to one shared facade class in
16
+ `__init__.py` and remove its own entry from a shared `_PENDING` dict -- small,
17
+ non-overlapping in meaning, but landing in the same file at the same insertion
18
+ anchor, so git's diff sees a conflict where nothing semantically conflicts.
19
+
20
+ This command is the new split: Python stays the planner (deterministic, rung 1,
21
+ reuses the exact wave-computation this project already had -- nothing here is
22
+ new logic, it's `gather_modules_to_build`/`compute_waves`/
23
+ `_compute_criterion_waves` from build.py, called and serialized rather than
24
+ immediately acted on). Python stops being the executor. Emits a plan; spawns
25
+ nothing. Execution -- actually running agents against that plan -- happens
26
+ through the Workflow tool from an orchestrating session (the `/pcp` skill),
27
+ using real harness primitives: `parallel()` for criteria that provably touch
28
+ disjoint files, `pipeline()`/a single-writer step for the ones that don't.
29
+
30
+ **Shared-surface files are the reason a per-criterion `target` isn't enough to
31
+ schedule safely.** A criterion's own new file (`metrics/plan_correctness.py`)
32
+ is genuinely disjoint from another criterion's new file -- true parallelism is
33
+ safe there. But BOTH criteria still touch the module's `__init__.py` to
34
+ register, and no criterion ever declares that as its `target` (it's an
35
+ implicit side effect of the registration convention every `pcp init --module`
36
+ scaffold uses, not something anyone writes down). So `shared_surface_files`
37
+ here is not inferred from declared targets at all -- it's the module's own
38
+ known facade files: `src/modules/<module>/__init__.py` and any file a
39
+ MOD_A00x criterion in that module declares as `target` (MOD_A002's app-registry
40
+ target, MOD_A004's interface-file target). Every criterion in the module is
41
+ marked as touching these implicitly, conservatively, whether or not its own
42
+ declared target says so -- the same asymmetry `_partition_wave_by_file_scope`
43
+ already reasons from: reopening/serializing too much costs parallelism,
44
+ missing a real collision costs a broken merge, and the second is worse.
45
+ """
46
+
47
+ import json
48
+ import sys
49
+ from pathlib import Path
50
+
51
+ import click
52
+ from rich.console import Console
53
+
54
+ from pcp.pcp_dir import find_pcp_dir, NoPCPDir
55
+ from pcp.commands.build import gather_modules_to_build, compute_waves, _compute_criterion_waves
56
+
57
+ console = Console()
58
+
59
+
60
+ def _module_shared_surface(pcp_dir: Path, module_name: str) -> list[str]:
61
+ """The facade files every criterion in this module implicitly touches to
62
+ register, regardless of what each criterion's own `target` says.
63
+
64
+ `src/modules/<module>/__init__.py` is the scaffold's own convention
65
+ (see init.py's MODULE_ACCEPTANCE_TEMPLATE / MOD_A002-A004) -- it exists
66
+ whether or not it's on disk yet, because the first criterion to land
67
+ creates it. MOD_A002 (registers through the application interface) and
68
+ MOD_A004 (interface file) declare their OWN targets explicitly; those are
69
+ pulled in here too, since registering ANY new criterion touches both the
70
+ facade and, if the module has one, the interface file.
71
+ """
72
+ mod_slug = module_name.replace("-", "_")
73
+ surface = {f"src/modules/{mod_slug}/__init__.py"}
74
+ acc_path = pcp_dir / "strategy" / "modules" / module_name / "acceptance.yaml"
75
+ if acc_path.exists():
76
+ from pcp.schema.validator import load_yaml
77
+ try:
78
+ data = load_yaml(acc_path) or {}
79
+ except Exception:
80
+ data = {}
81
+ for c in data.get("criteria", []) or []:
82
+ cid = str(c.get("id") or "")
83
+ if cid.startswith("MOD_") and c.get("target"):
84
+ surface.add(c["target"])
85
+ return sorted(surface)
86
+
87
+
88
+ def build_plan(pcp_dir: Path, module_name: str | None = None) -> dict:
89
+ """{modules: [{name, wave, shared_surface_files, dependencies,
90
+ criterion_waves: [[{id, description, check, target, depends_on,
91
+ touches_shared_surface}, ...], ...]}], total_criteria: int}.
92
+
93
+ Pure aggregation over data build.py already computes -- no new scheduling
94
+ logic, no LLM, nothing spawned. `criterion_waves` is a list of lists: each
95
+ inner list is one dependency wave (from `_compute_criterion_waves`), so
96
+ the consumer knows both "these can run together" and "in what order
97
+ relative to each other" without recomputing anything.
98
+ """
99
+ modules = gather_modules_to_build(pcp_dir, module_name)
100
+ module_waves = compute_waves(modules)
101
+
102
+ out_modules = []
103
+ for mod in modules:
104
+ surface = _module_shared_surface(pcp_dir, mod["name"])
105
+ crit_wave_of = _compute_criterion_waves(mod)
106
+ max_wave = max(crit_wave_of.values()) if crit_wave_of else 0
107
+
108
+ criterion_waves = []
109
+ for w in range(max_wave + 1):
110
+ wave_criteria = [c for c in mod["pending_criteria"] if crit_wave_of.get(c["id"], 0) == w]
111
+ criterion_waves.append([
112
+ {
113
+ "id": c["id"],
114
+ "description": c.get("description", ""),
115
+ "check": c.get("check", "manual"),
116
+ "target": c.get("target"),
117
+ "depends_on": c.get("depends_on") or [],
118
+ "touches_shared_surface": True, # see _module_shared_surface docstring
119
+ }
120
+ for c in wave_criteria
121
+ ])
122
+
123
+ out_modules.append({
124
+ "name": mod["name"],
125
+ "wave": module_waves.get(mod["name"], 0),
126
+ "dependencies": mod["spec"].get("dependencies") or [],
127
+ "shared_surface_files": surface,
128
+ "criterion_waves": criterion_waves,
129
+ })
130
+
131
+ out_modules.sort(key=lambda m: (m["wave"], m["name"]))
132
+ total = sum(len(w) for m in out_modules for w in m["criterion_waves"])
133
+ return {"modules": out_modules, "total_criteria": total}
134
+
135
+
136
+ @click.command(name="build-plan")
137
+ @click.option("--module", "module_name", default=None, help="Limit the plan to one module.")
138
+ @click.option("--path", "project_path", type=click.Path(), default=None,
139
+ help="Project root override.")
140
+ def build_plan_cmd(module_name: str | None, project_path: str | None):
141
+ """Emit the deterministic build plan as JSON. Spawns nothing.
142
+
143
+ Consumed by the `/pcp` skill's Workflow-tool execution -- see this
144
+ module's own docstring for why Python stopped being the executor.
145
+ """
146
+ try:
147
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
148
+ except NoPCPDir as e:
149
+ console.print(f"[red]Error:[/red] {e}")
150
+ sys.exit(2)
151
+
152
+ plan = build_plan(pcp_dir, module_name)
153
+ click.echo(json.dumps(plan, indent=2))
@@ -0,0 +1,83 @@
1
+ """pcp build-status — live view of an in-progress `pcp build` run.
2
+
3
+ Reads `.pcp/build_progress.yaml`, written by build.py's `_write_progress()`
4
+ at each criterion-attempt checkpoint (coding / evaluating gates / done /
5
+ failed). Real gap this closes (2026-07-24, Project O incident):
6
+ `pcp build` gives no way to tell what it's currently doing short of reading
7
+ raw agent output -- backgrounded (`nohup pcp build ... &`) it's fully
8
+ opaque, which is what triggered "i want to see whats happening" and a build
9
+ getting killed over what looked like a stall.
10
+ """
11
+
12
+ import sys
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ import click
18
+ import yaml
19
+ from rich.console import Console
20
+
21
+ from pcp.pcp_dir import find_pcp_dir, NoPCPDir
22
+
23
+ console = Console()
24
+
25
+ STALE_AFTER_SEC = 600 # a step with no update in 10 min is worth flagging
26
+
27
+
28
+ def load_progress(pcp_dir: Path) -> dict | None:
29
+ path = pcp_dir / "build_progress.yaml"
30
+ if not path.exists():
31
+ return None
32
+ try:
33
+ return yaml.safe_load(path.read_text()) or None
34
+ except yaml.YAMLError:
35
+ return None
36
+
37
+
38
+ def format_status(data: dict | None, now: datetime | None = None) -> str:
39
+ """Pure formatting -- testable without a clock or filesystem."""
40
+ if not data:
41
+ return "No build in progress (no .pcp/build_progress.yaml yet)."
42
+
43
+ now = now or datetime.now(timezone.utc)
44
+ line = (
45
+ f"{data.get('module', '?')}/{data.get('criterion_id', '?')} "
46
+ f"attempt {data.get('attempt', '?')} — {data.get('step', '?')}"
47
+ )
48
+ updated_at = data.get("updated_at")
49
+ if updated_at:
50
+ try:
51
+ ts = datetime.strptime(updated_at, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
52
+ age_sec = (now - ts).total_seconds()
53
+ line += f" (updated {int(age_sec)}s ago)"
54
+ if age_sec > STALE_AFTER_SEC and data.get("step") not in ("done", "failed"):
55
+ line += " ⚠ no update in a while — may be stuck"
56
+ except ValueError:
57
+ pass
58
+ return line
59
+
60
+
61
+ @click.command(name="build-status")
62
+ @click.option("--path", "project_path", type=click.Path(), default=None,
63
+ help="Project root (default: cwd, walks up to find .pcp/).")
64
+ @click.option("--watch", is_flag=True, help="Poll and redraw every --interval seconds until Ctrl-C.")
65
+ @click.option("--interval", type=int, default=5, help="Poll interval in seconds for --watch (default 5).")
66
+ def build_status(project_path: str | None, watch: bool, interval: int):
67
+ """Show what an in-progress `pcp build` run is currently doing."""
68
+ try:
69
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
70
+ except NoPCPDir as e:
71
+ console.print(f"[red]Error:[/red] {e}")
72
+ sys.exit(2)
73
+
74
+ if not watch:
75
+ console.print(format_status(load_progress(pcp_dir)))
76
+ sys.exit(0)
77
+
78
+ try:
79
+ while True:
80
+ console.print(format_status(load_progress(pcp_dir)))
81
+ time.sleep(interval)
82
+ except KeyboardInterrupt:
83
+ sys.exit(0)
@@ -0,0 +1,72 @@
1
+ """pcp capture — classify a session transcript into business-logic drift
2
+ (-> .pcp/brd.md) and technical input (-> .pcp/decision_log.jsonl).
3
+
4
+ Advisory, never blocks. Designed to be wired to a Claude Code SessionEnd hook
5
+ (reads the hook's JSON payload from stdin) for human/PM/UAT sessions; `pcp
6
+ build` calls the same underlying pcp.capture.run_capture() directly for its
7
+ own per-criterion agent sessions.
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import click
15
+ from rich.console import Console
16
+
17
+ from pcp.pcp_dir import find_pcp_dir, NoPCPDir
18
+ from pcp.capture import run_capture
19
+
20
+ console = Console()
21
+
22
+
23
+ @click.command()
24
+ @click.option("--path", "project_path", type=click.Path(), default=None,
25
+ help="Project root (default: cwd, walks up to find .pcp/).")
26
+ @click.option("--transcript-file", type=click.Path(exists=True), default=None,
27
+ help="Transcript JSONL to classify (manual/testing use). "
28
+ "Default: read a Claude Code SessionEnd hook payload from stdin.")
29
+ def capture(project_path: str | None, transcript_file: str | None):
30
+ """Classify a session transcript into business/technical drift. Advisory, never blocks."""
31
+ try:
32
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
33
+ except NoPCPDir:
34
+ # Silent — this may run via a hook in any project, not just PCP-managed ones.
35
+ sys.exit(0)
36
+
37
+ transcript_path = None
38
+ session_id = None
39
+
40
+ if transcript_file:
41
+ transcript_path = Path(transcript_file)
42
+ # Claude Code transcripts are always named <session-id>.jsonl (same
43
+ # convention pcp.capture.find_transcript_for_session relies on) — derive
44
+ # it from the filename so manual/testing runs still get real traceability
45
+ # instead of session_id=None / source="session:unknown".
46
+ session_id = transcript_path.stem
47
+ else:
48
+ raw = sys.stdin.read() if not sys.stdin.isatty() else ""
49
+ if raw.strip():
50
+ try:
51
+ payload = json.loads(raw)
52
+ session_id = payload.get("session_id")
53
+ tp = payload.get("transcript_path")
54
+ if tp:
55
+ transcript_path = Path(tp)
56
+ except json.JSONDecodeError:
57
+ pass
58
+
59
+ if not transcript_path or not transcript_path.exists():
60
+ console.print("[dim]pcp capture: no transcript available, skipping.[/dim]")
61
+ sys.exit(0)
62
+
63
+ result = run_capture(pcp_dir, transcript_path, source=f"session:{session_id or 'unknown'}", session_id=session_id)
64
+
65
+ if result.get("skipped"):
66
+ console.print(f"[dim]pcp capture: {result['skipped']}[/dim]")
67
+ else:
68
+ console.print(
69
+ f"[green]pcp capture:[/green] {result['business_count']} business item(s) -> .pcp/brd.md, "
70
+ f"{result['technical_count']} technical item(s) -> .pcp/decision_log.jsonl"
71
+ )
72
+ sys.exit(0)