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
pcp/capture.py ADDED
@@ -0,0 +1,339 @@
1
+ """Conversational drift capture — classifies a session transcript into business-logic
2
+ drift (-> .pcp/brd_items.yaml / .pcp/brd.md, a living BRD distinct from the immutable
3
+ objective.md) and technical input (-> .pcp/decision_log.jsonl).
4
+
5
+ Closes a real gap in the PCP lifecycle: requirement/scope changes surfaced in
6
+ PM/UAT conversation, or technical rationale stated mid-build, were never
7
+ captured anywhere in .pcp/ — only an explicit `pcp pm "<intent>"` call updated
8
+ specs. This module is advisory and additive: it never touches objective.md or
9
+ any spec.yaml/acceptance.yaml directly. Drift against the objective is flagged
10
+ in brd.md for a human to act on via `pcp pm`, not auto-applied.
11
+
12
+ Two trigger points call into run_capture(): the `pcp capture` CLI command
13
+ (wired to a Claude Code SessionEnd hook, for human/PM/UAT sessions) and
14
+ `pcp build`'s per-criterion loop (for autonomous coding-agent sessions).
15
+ """
16
+
17
+ import gzip
18
+ import json
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+
22
+ import yaml
23
+
24
+ from pcp import decision_log
25
+ from pcp import objective_conflicts
26
+ from pcp import telemetry
27
+ from pcp.llm import client as llm
28
+ from pcp.pcp_dir import get_modules_dir
29
+
30
+ MAX_TRANSCRIPT_CHARS = 40000
31
+
32
+ SYSTEM_PROMPT = """\
33
+ You are analyzing a software development session transcript to classify what was \
34
+ discussed into two categories, so nothing gets lost between sessions.
35
+
36
+ 1. business_items — requirement, scope, priority, or product-behavior changes/clarifications \
37
+ (the kind of thing that should update a Business Requirements Document). Only include \
38
+ substantive drift or elaboration, not routine implementation chatter.
39
+ 2. technical_items — implementation choices, tool/library picks, architecture tradeoffs, \
40
+ workarounds, or root causes discussed (useful context for future coding sessions).
41
+
42
+ You are given the program's existing immutable objective, the currently active \
43
+ business requirements already on file (so you can mark supersession instead of duplicating), \
44
+ and the list of existing module names.
45
+
46
+ For each business item, if it appears to conflict with the given objective text, set \
47
+ drift_flag to a short explanation of the conflict; otherwise null. If it clearly replaces \
48
+ an existing active requirement (by id), set supersedes to that id; otherwise null. If the \
49
+ item is clearly about one specific module from the given list, set module to that module's \
50
+ exact name; if it's program-level, spans multiple modules, or you're not confident, set \
51
+ module to null — do not guess.
52
+
53
+ Skip anything that's just routine back-and-forth with no durable decision (e.g. "run the \
54
+ tests", "looks good", debugging noise). If nothing substantive was discussed, return empty lists.
55
+
56
+ You must output ONLY valid JSON — no prose, no markdown, no code fences.
57
+
58
+ Output schema:
59
+ {
60
+ "business_items": [
61
+ {"description": "...", "evidence": "short quoted excerpt", "supersedes": "BRD-001 or null", "drift_flag": "explanation or null", "module": "exact-module-name or null"}
62
+ ],
63
+ "technical_items": [
64
+ {"category": "library-choice|architecture|workaround|root-cause|other", "summary": "...", "evidence": "short quoted excerpt"}
65
+ ]
66
+ }
67
+ """
68
+
69
+
70
+ def extract_conversation_text(transcript_path: Path) -> str:
71
+ """Read a Claude Code session transcript JSONL, keep only human + assistant
72
+ text turns (drop tool_use/tool_result blocks), return as plain text, capped
73
+ to MAX_TRANSCRIPT_CHARS (keeps the most recent content) for token discipline."""
74
+ lines_out = []
75
+ for line in Path(transcript_path).read_text(errors="replace").splitlines():
76
+ line = line.strip()
77
+ if not line:
78
+ continue
79
+ try:
80
+ entry = json.loads(line)
81
+ except json.JSONDecodeError:
82
+ continue
83
+ message = entry.get("message")
84
+ if not isinstance(message, dict):
85
+ continue
86
+ role = message.get("role")
87
+ if role not in ("user", "assistant"):
88
+ continue
89
+ content = message.get("content")
90
+ texts = []
91
+ if isinstance(content, str):
92
+ texts.append(content)
93
+ elif isinstance(content, list):
94
+ for block in content:
95
+ if isinstance(block, dict) and block.get("type") == "text" and block.get("text"):
96
+ texts.append(block["text"])
97
+ text = "\n".join(t for t in texts if t.strip())
98
+ if text.strip():
99
+ lines_out.append(f"{role.capitalize()}: {text.strip()}")
100
+ full = "\n\n".join(lines_out)
101
+ return full[-MAX_TRANSCRIPT_CHARS:] if len(full) > MAX_TRANSCRIPT_CHARS else full
102
+
103
+
104
+ def _load_brd_items(pcp_dir: Path) -> list[dict]:
105
+ path = pcp_dir / "brd_items.yaml"
106
+ if not path.exists():
107
+ return []
108
+ data = yaml.safe_load(path.read_text()) or {}
109
+ return data.get("items", [])
110
+
111
+
112
+ def _known_module_names(pcp_dir: Path) -> list[str]:
113
+ modules_dir = get_modules_dir(pcp_dir)
114
+ if not modules_dir.exists():
115
+ return []
116
+ return sorted(p.name for p in modules_dir.iterdir() if p.is_dir())
117
+
118
+
119
+ def classify_transcript(pcp_dir: Path, conversation_text: str, source: str) -> dict:
120
+ """Single Haiku call classifying conversation_text. Returns
121
+ {"business_items": [...], "technical_items": [...]}."""
122
+ objective = (pcp_dir / "objective.md").read_text() if (pcp_dir / "objective.md").exists() else ""
123
+ active_items = [i for i in _load_brd_items(pcp_dir) if i.get("status") == "active"]
124
+ active_summary = "\n".join(f"- [{i['id']}] {i['description']}" for i in active_items) or "(none yet)"
125
+ known_modules = _known_module_names(pcp_dir)
126
+ modules_summary = ", ".join(known_modules) if known_modules else "(none yet)"
127
+
128
+ user_prompt = "\n\n".join([
129
+ f"## Source\n{source}",
130
+ f"## Program Objective (immutable)\n{objective}",
131
+ f"## Currently Active Business Requirements\n{active_summary}",
132
+ f"## Existing Module Names\n{modules_summary}",
133
+ f"## Conversation Transcript\n{conversation_text}",
134
+ ])
135
+ return llm.call_json(SYSTEM_PROMPT, user_prompt, model=llm.JUDGE_MODEL, pcp_dir=pcp_dir, command="capture-classify")
136
+
137
+
138
+ def _write_brd_md(pcp_dir: Path, items: list[dict], timestamp: str) -> Path:
139
+ active = [i for i in items if i.get("status") == "active"]
140
+ # Same predicate the build gate uses — a resolved or dismissed conflict is
141
+ # not an open one, and must stop being reported as such (see
142
+ # objective_conflicts.is_unresolved_conflict for the divergence this fixes).
143
+ drift = [i for i in active if objective_conflicts.is_unresolved_conflict(i)]
144
+ superseded = [i for i in items if i.get("status") == "superseded"]
145
+
146
+ lines = [
147
+ "# Business Requirements (Living)",
148
+ f"Generated: {timestamp}",
149
+ "",
150
+ "> Auto-generated by `pcp capture`. Distills PM/UAT conversation drift. Do not edit manually.",
151
+ "> See objective.md for the immutable program objective — conflicts here mean objective.md "
152
+ "may need a deliberate update (via `pcp pm`), not that this file is wrong.",
153
+ "",
154
+ ]
155
+
156
+ if drift:
157
+ lines += ["## Drift Flags", ""]
158
+ for i in drift:
159
+ lines.append(f"- **{i['id']}**: {i['description']} — _{i['drift_flag']}_")
160
+ lines.append("")
161
+
162
+ lines += ["## Active Requirements", ""]
163
+ if active:
164
+ for i in active:
165
+ lines.append(f"- **{i['id']}** (`{i.get('source', '?')}`): {i['description']}")
166
+ else:
167
+ lines.append("_None captured yet._")
168
+ lines.append("")
169
+
170
+ if superseded:
171
+ lines += ["## Superseded", ""]
172
+ for i in superseded:
173
+ lines.append(f"- **{i['id']}** → superseded by {i.get('superseded_by')}: {i['description']}")
174
+ lines.append("")
175
+
176
+ out = pcp_dir / "brd.md"
177
+ out.write_text("\n".join(lines))
178
+ return out
179
+
180
+
181
+ def _next_brd_id(items: list[dict]) -> str:
182
+ nums = []
183
+ for i in items:
184
+ try:
185
+ nums.append(int(str(i["id"]).split("-")[-1]))
186
+ except (KeyError, ValueError):
187
+ continue
188
+ return f"BRD-{max(nums, default=0) + 1:03d}"
189
+
190
+
191
+ def _normalize_desc(s: str) -> str:
192
+ return " ".join(s.split()).strip().lower()
193
+
194
+
195
+ def apply_business_items(pcp_dir: Path, items: list[dict], source: str) -> Path:
196
+ """Merge distilled business items into brd_items.yaml, mark supersessions,
197
+ regenerate brd.md. Returns the brd.md path.
198
+
199
+ Dedupes against already-active items by normalized description text. Fixes
200
+ a confirmed bug: the classifier can (and did, in one real run) emit the same
201
+ substantive point twice — once per mention across a long transcript — with
202
+ byte-identical description text; nothing previously stopped both copies
203
+ from becoming separate BRD-NNN entries."""
204
+ existing = _load_brd_items(pcp_dir)
205
+ by_id = {i["id"]: i for i in existing}
206
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
207
+ seen_active = {_normalize_desc(i["description"]) for i in existing if i.get("status") == "active"}
208
+ known_modules = set(_known_module_names(pcp_dir))
209
+
210
+ for item in items:
211
+ desc_key = _normalize_desc(item["description"])
212
+ if desc_key in seen_active:
213
+ continue
214
+ supersedes = item.get("supersedes")
215
+ new_id = _next_brd_id(existing)
216
+ if supersedes and supersedes in by_id:
217
+ by_id[supersedes]["status"] = "superseded"
218
+ by_id[supersedes]["superseded_by"] = new_id
219
+ by_id[supersedes]["last_updated"] = now
220
+ drift_flag = item.get("drift_flag")
221
+ # Deterministic guard, not trust-the-classifier: a hallucinated or
222
+ # stale module name (e.g. from a since-renamed module) is worse than
223
+ # no link at all, so only a name that's REAL right now gets stored.
224
+ claimed_module = item.get("module")
225
+ module = claimed_module if claimed_module in known_modules else None
226
+ new_entry = {
227
+ "id": new_id,
228
+ "description": item["description"],
229
+ "status": "active",
230
+ "superseded_by": None,
231
+ "first_seen": now,
232
+ "last_updated": now,
233
+ "source": source,
234
+ "module": module,
235
+ "drift_flag": drift_flag,
236
+ # Stamped only when flagged -- this is the "proof of edit" hash
237
+ # `pcp build`'s objective-conflict gate compares against
238
+ # objective.md/target_state.md's current content. See
239
+ # objective_conflicts.py for why this exists.
240
+ "objective_hash_at_flag": objective_conflicts.objective_hash(pcp_dir) if drift_flag else None,
241
+ }
242
+ existing.append(new_entry)
243
+ by_id[new_id] = new_entry
244
+ seen_active.add(desc_key)
245
+
246
+ (pcp_dir / "brd_items.yaml").write_text(yaml.dump({"items": existing}, default_flow_style=False))
247
+ return _write_brd_md(pcp_dir, existing, now)
248
+
249
+
250
+ def apply_technical_items(pcp_dir: Path, items: list[dict], source: str, session_id: str | None) -> None:
251
+ for item in items:
252
+ decision_log.record(
253
+ pcp_dir,
254
+ source=source, session_id=session_id,
255
+ category=item.get("category", "other"),
256
+ summary=item.get("summary", ""),
257
+ evidence=item.get("evidence", ""),
258
+ )
259
+
260
+
261
+ def archive_transcript(pcp_dir: Path, transcript_path: Path | None, session_id: str | None) -> str | None:
262
+ """Copies the raw session transcript (gzip-compressed) into
263
+ .pcp/transcripts/ — so the full action-level record (every tool call,
264
+ every edit, not just the classified business/technical summary
265
+ classify_transcript() extracts) durably belongs to the PROJECT, not just
266
+ to Claude Code's own ~/.claude/projects/ retention, which PCP doesn't
267
+ control and which could be pruned independently of this project's life.
268
+
269
+ Returns the relative path (under pcp_dir), or None if there was nothing
270
+ to archive. Honest tradeoff, not silently ignored: long-running sessions
271
+ produce multi-MB transcripts, so this does grow .pcp/ over time — no
272
+ retention/pruning policy exists yet, same as telemetry.jsonl/evidence/
273
+ growing unbounded. Compression (typically 5-10x on JSONL) is the only
274
+ mitigation applied so far."""
275
+ if not transcript_path or not Path(transcript_path).exists():
276
+ return None
277
+ out_dir = Path(pcp_dir) / "transcripts"
278
+ out_dir.mkdir(parents=True, exist_ok=True)
279
+ name = f"{session_id or Path(transcript_path).stem}.jsonl.gz"
280
+ out_path = out_dir / name
281
+ with open(transcript_path, "rb") as src, gzip.open(out_path, "wb") as dst:
282
+ dst.write(src.read())
283
+ return str(out_path.relative_to(pcp_dir))
284
+
285
+
286
+ def find_transcript_for_session(session_id: str) -> Path | None:
287
+ """Locate a Claude Code CLI session's own transcript file — same location
288
+ `claude -p --resume <session_id>` reads from."""
289
+ if not session_id:
290
+ return None
291
+ projects_dir = Path.home() / ".claude" / "projects"
292
+ if not projects_dir.is_dir():
293
+ return None
294
+ matches = list(projects_dir.glob(f"*/{session_id}.jsonl"))
295
+ return matches[0] if matches else None
296
+
297
+
298
+ def run_capture(pcp_dir: Path, transcript_path: Path, source: str, session_id: str | None = None) -> dict:
299
+ """Orchestrates archive + classify + apply. Never raises — advisory, same
300
+ posture as `pcp audit`. Returns a summary dict.
301
+
302
+ Archival happens unconditionally, even for an empty/routine-chatter
303
+ transcript — the raw record is evidence regardless of whether the
304
+ classifier found anything substantive enough to distill into BRD/
305
+ decision-log entries."""
306
+ archived_path = None
307
+ try:
308
+ archived_path = archive_transcript(pcp_dir, transcript_path, session_id)
309
+ if archived_path:
310
+ telemetry.record(
311
+ pcp_dir, cycle="capture", check="transcript-archive", control_id="CTRL-011",
312
+ session_id=session_id, evidence_path=archived_path, result="pass",
313
+ )
314
+ except Exception as e:
315
+ telemetry.record(
316
+ pcp_dir, cycle="capture", check="transcript-archive", control_id="CTRL-011",
317
+ session_id=session_id, result="error", errors=[str(e)], error_count=1,
318
+ )
319
+
320
+ try:
321
+ conversation_text = extract_conversation_text(transcript_path)
322
+ if not conversation_text.strip():
323
+ return {"skipped": "empty transcript", "archived_path": archived_path}
324
+
325
+ result = classify_transcript(pcp_dir, conversation_text, source)
326
+ business_items = result.get("business_items", []) or []
327
+ technical_items = result.get("technical_items", []) or []
328
+
329
+ if business_items:
330
+ apply_business_items(pcp_dir, business_items, source)
331
+ if technical_items:
332
+ apply_technical_items(pcp_dir, technical_items, source, session_id)
333
+
334
+ return {
335
+ "business_count": len(business_items), "technical_count": len(technical_items),
336
+ "archived_path": archived_path,
337
+ }
338
+ except Exception as e:
339
+ return {"skipped": f"capture failed: {e}", "archived_path": archived_path}
pcp/cli.py ADDED
@@ -0,0 +1,104 @@
1
+ """PCP CLI entry point."""
2
+
3
+ import click
4
+ from pcp.commands.validate_strategy import validate_strategy
5
+ from pcp.commands.init import init
6
+ from pcp.commands.scan import scan
7
+ from pcp.commands.diff import diff
8
+ from pcp.commands.check import check
9
+ from pcp.commands.deploy_check import deploy_check
10
+ from pcp.commands.gate import gate
11
+ from pcp.commands.context import context
12
+ from pcp.commands.validate_module import validate_module
13
+ from pcp.commands.report import report
14
+ from pcp.commands.install_hook import install_hook
15
+ from pcp.commands.install_skill import install_skill
16
+ from pcp.commands.status import status
17
+ from pcp.commands.architect_review import architect_review
18
+ from pcp.commands.kickoff import kickoff
19
+ from pcp.commands.pm import pm
20
+ from pcp.commands.build import build
21
+ from pcp.commands.import_project import import_project
22
+ from pcp.commands.self_update import self_update
23
+ from pcp.commands.audit import audit
24
+ from pcp.commands.telemetry_cmd import telemetry_cmd
25
+ from pcp.commands.doctor import doctor
26
+ from pcp.commands.watch import watch
27
+ from pcp.commands.deploy import deploy
28
+ from pcp.commands.capture import capture
29
+ from pcp.commands.provenance import provenance
30
+ from pcp.commands.takeover import takeover
31
+ from pcp.commands.dashboard import dashboard
32
+ from pcp.commands.verify_syntax_fix import verify_syntax_fix
33
+ from pcp.commands.architecture_justification import architecture_justification
34
+ from pcp.commands.design_audit import design_audit
35
+ from pcp.commands.docs import docs
36
+ from pcp.commands.prune import prune
37
+ from pcp.commands.escalations_cmd import escalations_cmd
38
+ from pcp.commands.pressure_test_cmd import pressure_test_cmd
39
+ from pcp.commands.control_audit_cmd import control_audit_cmd
40
+ from pcp.commands.objective_conflicts_cmd import objective_conflicts_cmd
41
+ from pcp.commands.correct_objective import correct_objective
42
+ from pcp.commands.amend import amend
43
+ from pcp.commands.run_log_cmd import run_log_cli
44
+ from pcp.commands.narrative_lint import narrative_lint_cmd
45
+ from pcp.commands.build_status import build_status
46
+ from pcp.commands.verify import verify
47
+ from pcp.commands.build_plan import build_plan_cmd
48
+ from pcp.commands.diff_reduce import diff_reduce
49
+
50
+
51
+ @click.group()
52
+ @click.version_option(package_name="program-context-protocol")
53
+ def cli():
54
+ """PCP — Program Context Protocol.
55
+
56
+ Prevent LLM hallucination and context drift across dev sessions.
57
+ """
58
+
59
+
60
+ cli.add_command(init)
61
+ cli.add_command(scan)
62
+ cli.add_command(diff)
63
+ cli.add_command(check)
64
+ cli.add_command(gate)
65
+ cli.add_command(deploy_check, name="deploy-check")
66
+ cli.add_command(validate_strategy, name="validate-strategy")
67
+ cli.add_command(validate_module, name="validate-module")
68
+ cli.add_command(context)
69
+ cli.add_command(report)
70
+ cli.add_command(install_hook, name="install-hook")
71
+ cli.add_command(install_skill)
72
+ cli.add_command(status)
73
+ cli.add_command(architect_review, name="architect-review")
74
+ cli.add_command(kickoff)
75
+ cli.add_command(pm)
76
+ cli.add_command(build)
77
+ cli.add_command(import_project, name="import")
78
+ cli.add_command(audit)
79
+ cli.add_command(telemetry_cmd)
80
+ cli.add_command(doctor)
81
+ cli.add_command(watch)
82
+ cli.add_command(deploy)
83
+ cli.add_command(capture)
84
+ cli.add_command(provenance)
85
+ cli.add_command(takeover)
86
+ cli.add_command(dashboard)
87
+ cli.add_command(verify_syntax_fix)
88
+ cli.add_command(architecture_justification)
89
+ cli.add_command(design_audit)
90
+ cli.add_command(docs)
91
+ cli.add_command(prune)
92
+ cli.add_command(escalations_cmd)
93
+ cli.add_command(pressure_test_cmd)
94
+ cli.add_command(control_audit_cmd)
95
+ cli.add_command(objective_conflicts_cmd)
96
+ cli.add_command(correct_objective)
97
+ cli.add_command(amend)
98
+ cli.add_command(run_log_cli, name="run-log")
99
+ cli.add_command(narrative_lint_cmd)
100
+ cli.add_command(build_status)
101
+ cli.add_command(verify)
102
+ cli.add_command(build_plan_cmd, name="build-plan")
103
+ cli.add_command(self_update)
104
+ cli.add_command(diff_reduce)
File without changes