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/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)
|
pcp/commands/__init__.py
ADDED
|
File without changes
|