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,151 @@
1
+ """pcp context — surface .pcp/ context to any LLM.
2
+
3
+ Not wired to fire automatically at session start on its own -- `--inject`
4
+ writes/updates a marked block in CLAUDE.md, which Claude Code already reads
5
+ every session, but nothing calls this command for you. Wire a SessionStart
6
+ hook yourself (snippet in .pcp/RECOMMENDED_PERMISSIONS.md) if you want the
7
+ block kept fresh automatically; same manual-opt-in posture as the
8
+ PreToolUse spec guard -- PCP doesn't edit .claude/settings.json itself.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import click
17
+ from rich.console import Console
18
+
19
+ from pcp.pcp_dir import find_pcp_dir, NoPCPDir
20
+
21
+ console = Console(stderr=True)
22
+
23
+ CLAUDE_MD_MARKER_START = "<!-- pcp:context:start -->"
24
+ CLAUDE_MD_MARKER_END = "<!-- pcp:context:end -->"
25
+
26
+
27
+ def _read_optional(path: Path) -> str:
28
+ return path.read_text().strip() if path.exists() else ""
29
+
30
+
31
+ def _build_context(pcp_dir: Path) -> dict:
32
+ return {
33
+ "objective": _read_optional(pcp_dir / "objective.md"),
34
+ "architecture": _read_optional(pcp_dir / "architecture.md"),
35
+ "current_state": _read_optional(pcp_dir / "current_state.md"),
36
+ "diff": _read_optional(pcp_dir / "diff.md"),
37
+ "target_state": _read_optional(pcp_dir / "target_state.md"),
38
+ }
39
+
40
+
41
+ def _max_generated_chars() -> int:
42
+ """Per-section budget for the AUTO-GENERATED sections only. Read at call
43
+ time, not frozen at import, so a test can monkeypatch it."""
44
+ return int(os.environ.get("PCP_CONTEXT_MAX_GENERATED_CHARS", "20000"))
45
+
46
+
47
+ def _generated_section(title: str, body: str, rel_path: str, budget: int) -> list[str]:
48
+ """current_state.md and diff.md are auto-generated and grow with module
49
+ count -- on a many-module project they run to six figures of characters
50
+ each. `--inject` writes this block into CLAUDE.md, which every session then
51
+ reads in full, so pasting them whole silently makes the single most
52
+ expensive file in the repo unboundedly large (2026-07-25: ~269k chars of
53
+ generated state across the two on a real dogfood project).
54
+
55
+ Over budget, emit a POINTER instead of the body. That is not a silent
56
+ truncation -- the size and the path are both stated, and an agent that
57
+ needs the detail can read the file, which is the same projection rule
58
+ context_map.yaml already applies to sliced state."""
59
+ if len(body) <= budget:
60
+ return [f"## {title}\n", body, ""]
61
+ return [
62
+ f"## {title}\n",
63
+ f"_Omitted from this block: `{rel_path}` is {len(body):,} chars, over the "
64
+ f"{budget:,}-char budget for auto-generated sections. Read the file directly "
65
+ f"for the detail. Raise `PCP_CONTEXT_MAX_GENERATED_CHARS` to inline it anyway._",
66
+ "",
67
+ ]
68
+
69
+
70
+ def _render_markdown(ctx: dict) -> str:
71
+ parts = ["# PCP Project Context\n"]
72
+ budget = _max_generated_chars()
73
+
74
+ # Intent files go in WHOLE, always -- fragmenting spec collapses
75
+ # faithfulness (context_map.py's second design law). Only the generated
76
+ # projections below are budgeted.
77
+ if ctx["objective"]:
78
+ parts += ["## Objective\n", ctx["objective"], ""]
79
+
80
+ if ctx["architecture"]:
81
+ parts += ["## Architecture\n", ctx["architecture"], ""]
82
+
83
+ if ctx["current_state"]:
84
+ parts += _generated_section(
85
+ "Current State", ctx["current_state"], ".pcp/current_state.md", budget)
86
+ else:
87
+ parts += ["## Current State\n", "_Not generated yet. Run `pcp scan`._", ""]
88
+
89
+ if ctx["diff"]:
90
+ parts += _generated_section("Pending Gaps", ctx["diff"], ".pcp/diff.md", budget)
91
+
92
+ return "\n".join(parts)
93
+
94
+
95
+ def _inject_into_claude_md(project_root: Path, markdown: str) -> None:
96
+ claude_md = project_root / "CLAUDE.md"
97
+ block = f"{CLAUDE_MD_MARKER_START}\n{markdown}\n{CLAUDE_MD_MARKER_END}"
98
+
99
+ if not claude_md.exists():
100
+ claude_md.write_text(f"{block}\n")
101
+ console.print(f"[green]created[/green] CLAUDE.md with pcp context block")
102
+ return
103
+
104
+ existing = claude_md.read_text()
105
+ start = existing.find(CLAUDE_MD_MARKER_START)
106
+ end = existing.find(CLAUDE_MD_MARKER_END)
107
+
108
+ if start != -1 and end != -1:
109
+ updated = existing[:start] + block + existing[end + len(CLAUDE_MD_MARKER_END):]
110
+ claude_md.write_text(updated)
111
+ console.print(f"[green]updated[/green] CLAUDE.md pcp context block")
112
+ else:
113
+ with open(claude_md, "a") as f:
114
+ f.write(f"\n{block}\n")
115
+ console.print(f"[green]appended[/green] pcp context block to CLAUDE.md")
116
+
117
+
118
+ @click.command()
119
+ @click.option("--path", "project_path", type=click.Path(), default=None,
120
+ help="Project root (default: cwd, walks up to find .pcp/).")
121
+ @click.option("--json", "output_json", is_flag=True,
122
+ help="Output as JSON for IDE tool integrations.")
123
+ @click.option("--inject", is_flag=True,
124
+ help="Write context into CLAUDE.md instead of stdout.")
125
+ def context(project_path: str | None, output_json: bool, inject: bool):
126
+ """Output .pcp/ context for LLM consumption at session start.
127
+
128
+ Pipe to your IDE or LLM: pcp context | pbcopy
129
+ Inject into CLAUDE.md: pcp context --inject
130
+ """
131
+ try:
132
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
133
+ except NoPCPDir as e:
134
+ console.print(f"[red]Error:[/red] {e}")
135
+ sys.exit(2)
136
+
137
+ project_root = pcp_dir.parent
138
+ ctx = _build_context(pcp_dir)
139
+
140
+ if output_json:
141
+ click.echo(json.dumps(ctx, indent=2))
142
+ return
143
+
144
+ markdown = _render_markdown(ctx)
145
+
146
+ if inject:
147
+ _inject_into_claude_md(project_root, markdown)
148
+ return
149
+
150
+ # Default: print to stdout (pipe-friendly)
151
+ click.echo(markdown)
@@ -0,0 +1,54 @@
1
+ """pcp control-audit -- self-evaluation gap closed 2026-07-21. See
2
+ control_audit.py's module docstring for the full rationale."""
3
+
4
+ import json
5
+ import sys
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, NoPCPDir
12
+ from pcp import control_audit
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.command(name="control-audit")
18
+ @click.option("--path", "project_path", type=click.Path(), default=None,
19
+ help="Project root (default: cwd, walks up to find .pcp/).")
20
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
21
+ @click.option("--sync", "sync", is_flag=True,
22
+ help="Additive-only: append any control from the currently-installed pcp package's catalog "
23
+ "that's missing from this project's controls.yaml. Never rewrites or removes an existing entry.")
24
+ def control_audit_cmd(project_path: str | None, output_json: bool, sync: bool):
25
+ """Which cataloged controls have never fired -- retire/merge review candidates."""
26
+ try:
27
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
28
+ except NoPCPDir as e:
29
+ console.print(f"[red]Error:[/red] {e}")
30
+ sys.exit(2)
31
+
32
+ if sync:
33
+ added = control_audit.sync_catalog(pcp_dir)
34
+ if not added:
35
+ console.print("[green]controls.yaml already current[/green] -- no missing control ids "
36
+ "(or no controls.yaml exists yet; run `pcp init` to create one).")
37
+ else:
38
+ console.print(f"[green]✓[/green] added {len(added)} missing control(s) to controls.yaml: "
39
+ f"{', '.join(sorted(added))}")
40
+ if output_json:
41
+ click.echo(json.dumps({"added": added}, indent=2))
42
+ return
43
+
44
+ audit = control_audit.write_control_audit(pcp_dir)
45
+
46
+ if output_json:
47
+ click.echo(json.dumps(audit, indent=2))
48
+ return
49
+
50
+ never_fired = {k: v for k, v in audit.items() if v["signal"] == "never-fired"}
51
+ console.print(f"[bold]Control Catalog Audit[/bold] — {len(audit)} controls, {len(never_fired)} never-fired")
52
+ for cid, v in sorted(never_fired.items()):
53
+ console.print(f" [yellow]{cid}[/yellow] {v['name']} — {v['total_runs']} runs, 0 findings")
54
+ console.print("[dim]Full report: .pcp/control_audit.md[/dim]")
@@ -0,0 +1,160 @@
1
+ """pcp correct-objective — the human-authorized write path for objective.md/
2
+ target_state.md.
3
+
4
+ Hard Rule #2 ("spec files are human-authorized only") protects against an
5
+ autonomous build-agent silently drifting the spec (`protected_path` hard-blocks
6
+ any write while `PCP_AGENT_SESSION=1`). It was never meant to mean "Ganesh
7
+ hand-types the markdown diff himself" -- `pcp kickoff`/`pcp pm` already write
8
+ module-level spec.yaml/acceptance.yaml this same way: LLM proposes the change
9
+ from stated intent, a human reviews and approves, then it's written. objective.md
10
+ and target_state.md never got the equivalent command, so in practice a business
11
+ correction discussed and agreed in conversation had nowhere to go except a
12
+ human manually opening the file -- and the 2026-07-22 Project O incident
13
+ is exactly what happens when that manual step is skipped and nothing catches
14
+ it: a build cycle runs to completion against a stale objective.
15
+
16
+ Deliberately separate from `pcp pm`: pm never touches the program-level spec
17
+ files, only module specs -- keeping this a distinct, explicit command means
18
+ running it always shows a real diff of the two most consequential files in
19
+ the project, never gets silently bundled into a routine module-intent call.
20
+
21
+ The propose/diff/approve/write mechanic itself now lives in `spec_write.py`,
22
+ shared with `pcp amend` — which gives the same treatment to the six other
23
+ protected files that had no write path at all (2026-07-25).
24
+ """
25
+
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ import click
30
+ import yaml
31
+ from rich.console import Console
32
+
33
+ from pcp.pcp_dir import find_pcp_dir, NoPCPDir
34
+ from pcp.llm import client as llm
35
+ from pcp import objective_conflicts
36
+ from pcp import spec_write
37
+ from pcp.spec_write import SpecTarget
38
+ from pcp.commands.validate_strategy import (
39
+ _build_user_prompt as build_val_prompt,
40
+ SYSTEM_PROMPT as VAL_SYSTEM_PROMPT,
41
+ _render_results as render_val_results,
42
+ )
43
+
44
+ console = Console()
45
+
46
+ SYSTEM_PROMPT = """\
47
+ You are the program's spec author. A human has communicated a business \
48
+ correction, during conversation, that must now be reflected in the immutable \
49
+ program objective. You are given the CURRENT objective.md and target_state.md \
50
+ in full.
51
+
52
+ Rewrite them to incorporate the correction faithfully:
53
+ - Keep everything NOT affected by the correction exactly as-is -- do not \
54
+ rephrase, reorder, or "improve" unrelated sections.
55
+ - Change only what the correction actually requires.
56
+ - Do not invent new scope beyond what the correction states.
57
+ - If the correction only affects one of the two files, still return both \
58
+ (the untouched one unchanged).
59
+
60
+ You must output ONLY valid JSON — no prose, no markdown, no code fences.
61
+
62
+ Output schema:
63
+ {
64
+ "objective_md": "full replacement content of objective.md",
65
+ "target_state_md": "full replacement content of target_state.md",
66
+ "summary": "one paragraph: exactly what changed and why, for the audit trail"
67
+ }
68
+ """
69
+
70
+
71
+ def _load_conflict(pcp_dir: Path, item_id: str) -> dict | None:
72
+ path = pcp_dir / "brd_items.yaml"
73
+ if not path.exists():
74
+ return None
75
+ data = yaml.safe_load(path.read_text()) or {}
76
+ for item in data.get("items", []):
77
+ if item.get("id") == item_id:
78
+ return item
79
+ return None
80
+
81
+
82
+ @click.command("correct-objective")
83
+ @click.argument("correction", required=False, default=None)
84
+ @click.option("--from-conflict", "from_conflict", default=None, metavar="ITEM_ID",
85
+ help="Pull the correction text from an unresolved brd_items.yaml conflict (see `pcp objective-conflicts`) instead of a positional argument.")
86
+ @click.option("--path", "project_path", type=click.Path(), default=None)
87
+ @click.option("--yes", "yes", is_flag=True, help="Skip the interactive diff-approval prompt (scripted/CI use).")
88
+ def correct_objective(correction: str | None, from_conflict: str | None, project_path: str | None, yes: bool):
89
+ """Propose + human-approve a rewrite of objective.md/target_state.md from a stated business correction."""
90
+ try:
91
+ pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
92
+ except NoPCPDir as e:
93
+ console.print(f"[red]Error:[/red] {e}")
94
+ sys.exit(2)
95
+
96
+ conflict_item = None
97
+ if from_conflict:
98
+ conflict_item = _load_conflict(pcp_dir, from_conflict)
99
+ if not conflict_item:
100
+ console.print(f"[red]Error:[/red] no brd_items.yaml entry with id {from_conflict}")
101
+ sys.exit(2)
102
+ correction = f"{conflict_item.get('description', '')} -- conflict noted: {conflict_item.get('drift_flag', '')}"
103
+ elif not correction:
104
+ console.print("[red]Error:[/red] pass a correction as an argument, or --from-conflict ITEM_ID")
105
+ sys.exit(2)
106
+
107
+ obj_path = pcp_dir / "objective.md"
108
+ ts_path = pcp_dir / "target_state.md"
109
+
110
+ user_prompt = "\n\n".join([
111
+ f"## Correction\n{correction}",
112
+ f"## Current objective.md\n{obj_path.read_text() if obj_path.exists() else ''}",
113
+ f"## Current target_state.md\n{ts_path.read_text() if ts_path.exists() else ''}",
114
+ ])
115
+
116
+ outcome = spec_write.propose_and_write(
117
+ pcp_dir,
118
+ [
119
+ SpecTarget(name="objective.md", path=obj_path, key="objective_md"),
120
+ SpecTarget(name="target_state.md", path=ts_path, key="target_state_md"),
121
+ ],
122
+ SYSTEM_PROMPT,
123
+ user_prompt,
124
+ command="correct-objective",
125
+ intent=correction,
126
+ yes=yes,
127
+ no_change_hint="If this is unexpected, restate the correction more concretely.",
128
+ )
129
+ if not outcome.written:
130
+ sys.exit(0)
131
+ new_objective = outcome.result["objective_md"]
132
+
133
+ # Auto-clears any objective_conflicts entry whose flagged hash no longer
134
+ # matches — including conflict_item itself, since the file just changed.
135
+ still_unresolved = objective_conflicts.reconcile(pcp_dir)
136
+ if conflict_item and not any(c.get("id") == conflict_item.get("id") for c in still_unresolved):
137
+ console.print(f"[green]✓[/green] conflict {conflict_item['id']} resolved.")
138
+
139
+ # Objective just moved — every module's spec should be re-checked against
140
+ # it before anyone runs `pcp build`, same as kickoff/pm always do.
141
+ console.print("\n[bold]Running validate-strategy against the new objective...[/bold]")
142
+ decomposition_path = pcp_dir / "strategy" / "decomposition.md"
143
+ decomposition = decomposition_path.read_text() if decomposition_path.exists() else ""
144
+ all_specs = {}
145
+ for spec_path in sorted((pcp_dir / "strategy" / "modules").glob("*/spec.yaml")):
146
+ try:
147
+ all_specs[spec_path.parent.name] = yaml.safe_load(spec_path.read_text()) or {}
148
+ except Exception:
149
+ pass
150
+ try:
151
+ val_prompt = build_val_prompt(new_objective, decomposition, all_specs)
152
+ val_result = llm.call_json(VAL_SYSTEM_PROMPT, val_prompt, model=llm.JUDGE_MODEL, pcp_dir=pcp_dir, command="correct-objective-validate")
153
+ render_val_results(pcp_dir, val_result, output_json=False)
154
+ except Exception as e:
155
+ console.print(f"[yellow]Warning: could not run validate-strategy automatically: {e}[/yellow]")
156
+
157
+ console.print(
158
+ "\n[dim]Existing module specs may now be stale against the new objective (see validate-strategy "
159
+ "output above) -- run `pcp pm \"...\"` per affected module before `pcp build`.[/dim]"
160
+ )