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/commands/init.py ADDED
@@ -0,0 +1,1634 @@
1
+ """pcp init — scaffold .pcp/ directory in a project."""
2
+
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def _pascal_case(name: str) -> str:
14
+ return "".join(part.capitalize() for part in re.split(r"[_-]", name) if part)
15
+
16
+ OBJECTIVE_TEMPLATE = """\
17
+ # Program Objective
18
+
19
+ ## Why This Exists
20
+
21
+ [Describe the problem this program solves.]
22
+
23
+ ## What Success Looks Like
24
+
25
+ 1. [Measurable outcome 1]
26
+ 2. [Measurable outcome 2]
27
+
28
+ ## Out of Scope
29
+
30
+ - [What this program does NOT do]
31
+ """
32
+
33
+ TARGET_STATE_TEMPLATE = """\
34
+ # Target State
35
+
36
+ [Describe what "done" looks like from a user/business perspective.
37
+ This is the ideal end state of the entire program.]
38
+ """
39
+
40
+ ARCHITECTURE_TEMPLATE = """\
41
+ # Architecture
42
+
43
+ ## Tech Stack
44
+
45
+ | Layer | Choice | Why |
46
+ |---|---|---|
47
+ | | | |
48
+
49
+ ## Key Constraints
50
+
51
+ - [Constraint 1]
52
+
53
+ ## Design Decisions
54
+
55
+ - [Decision and rationale]
56
+ """
57
+
58
+ CI_RULES_TEMPLATE = """\
59
+ version: "1.0"
60
+ rules:
61
+ # ── Modularity rules (generated by pcp init — do not remove) ──────────────
62
+ # Philosophy: every module is a guest. It can leave without drama.
63
+ # It can arrive without surgery. Nothing else should know about it
64
+ # except the application registry.
65
+
66
+ - id: MOD_001
67
+ name: "No direct cross-module implementation imports"
68
+ check: ast_pattern
69
+ # matches: ../../other-module/src/ or ../other-module/src/
70
+ pattern: "from \\\\.\\\\./(\\\\.\\\\./)?[^/]+/src/"
71
+ severity: hard_block
72
+ message: "Modules must communicate through interfaces/, not by importing each other's src/. Move shared types to interfaces/."
73
+ scope: ["*.py", "*.js", "*.jsx", "*.ts", "*.tsx", "*.go", "*.rb", "*.java", "*.php", "*.cs", "*.rs"]
74
+
75
+ - id: MOD_002
76
+ name: "No hardcoded module names as string literals in orchestrator"
77
+ check: ast_pattern
78
+ # catches brittle module-name strings that create implicit coupling
79
+ pattern: "require\\\\(['\\\"]\\\\.\\\\./"
80
+ severity: hard_block
81
+ message: "Use the module registry pattern — app.register(Module) not require('../module')."
82
+ scope: ["*.js", "*.jsx", "*.ts", "*.tsx"]
83
+
84
+ - id: MOD_003
85
+ name: "Every module must have a public interface file"
86
+ check: file_exists
87
+ target: "src/interfaces/I{module}.ts"
88
+ severity: hard_block
89
+ message: "Create src/interfaces/I{module}.ts defining what this module exposes. Other modules import only from here."
90
+
91
+ - id: MOD_004
92
+ name: "No shared global mutable state between modules"
93
+ check: ast_pattern
94
+ pattern: "global\\\\s+[A-Za-z_]\\\\w*\\\\s*="
95
+ severity: hard_block
96
+ message: "Global mutable state creates invisible coupling. Pass state through the registry or event bus."
97
+
98
+ - id: MOD_005
99
+ name: "Every module must have a feature flag"
100
+ check: file_exists
101
+ target: "src/modules/{module}/feature_flag.env"
102
+ severity: hard_block
103
+ message: "Create feature_flag.env with FEATURE_{MODULE}_ENABLED=false. All modules ship dark by default."
104
+
105
+ # ── Spec protection (generated by pcp init — do not remove) ───────────────
106
+ - id: SPEC_001
107
+ name: "Human-authorized spec files must not be written by an unattended agent"
108
+ check: protected_path
109
+ description: "These files are human-AUTHORIZED, not human-typed. Enforced only inside a pcp-build agent session (PCP_AGENT_SESSION=1), which is the unattended case. A human-present session updating a spec through its gated write path (`pcp correct-objective`, `pcp pm`, `pcp amend` — each shows a real diff and requires approval) is never blocked, and is the correct way to apply a spec change agreed in conversation."
110
+ severity: hard_block
111
+ scope:
112
+ - ".pcp/objective.md"
113
+ - ".pcp/target_state.md"
114
+ - ".pcp/architecture.md"
115
+ - ".pcp/ci_rules.yaml"
116
+ - ".pcp/controls.yaml"
117
+ - ".pcp/SDLC_phase.yaml"
118
+ - ".pcp/strategy/decomposition.md"
119
+ - ".pcp/strategy/dependency_map.md"
120
+ - ".pcp/strategy/modules/*/spec.yaml"
121
+ - ".pcp/strategy/modules/*/acceptance.yaml"
122
+
123
+ # ── Security rules (generated by pcp init — do not remove) ────────────────
124
+ - id: SEC_001
125
+ name: "No hardcoded secrets, API keys, or tokens"
126
+ check: ast_pattern
127
+ pattern: "(password|secret|api[_-]?key|access[_-]?key|token)\\\\s*[:=]\\\\s*['\\\"][^'\\\"]{8,}['\\\"]"
128
+ severity: hard_block
129
+ message: "Load secrets from environment variables or a secrets manager, never hardcode them."
130
+ scope: ["*.py", "*.js", "*.jsx", "*.ts", "*.tsx", "*.go", "*.rb", "*.java", "*.php", "*.cs", "*.rs"]
131
+
132
+ - id: SEC_002
133
+ name: "No dynamic code evaluation on untrusted input"
134
+ check: ast_pattern
135
+ pattern: "\\\\b(eval|exec)\\\\s*\\\\("
136
+ severity: hard_block
137
+ message: "Dynamic code evaluation on untrusted input is a code-injection vector. Use a safe parser or explicit dispatch instead."
138
+ scope: ["*.py", "*.js", "*.jsx", "*.ts", "*.tsx", "*.go", "*.rb", "*.java", "*.php", "*.cs", "*.rs"]
139
+
140
+ - id: SEC_003
141
+ name: "No disabled TLS/certificate verification"
142
+ check: ast_pattern
143
+ pattern: "(verify\\\\s*=\\\\s*False|rejectUnauthorized\\\\s*:\\\\s*false|NODE_TLS_REJECT_UNAUTHORIZED\\\\s*=\\\\s*['\\\"]?0|ssl_verify\\\\s*=\\\\s*False|check_hostname\\\\s*=\\\\s*False)"
144
+ severity: hard_block
145
+ message: "Disabling certificate verification allows MITM attacks. Fix the underlying cert/trust-store issue instead."
146
+ scope: ["*.py", "*.js", "*.jsx", "*.ts", "*.tsx", "*.go", "*.rb", "*.java", "*.php", "*.cs", "*.rs"]
147
+
148
+ - id: SEC_004
149
+ name: "No string-built SQL passed to execute()/query()"
150
+ check: ast_pattern
151
+ pattern: "\\\\.(execute|query)\\\\(\\\\s*(f['\\\"]|['\\\"][^'\\\"]*['\\\"]\\\\s*\\\\+)"
152
+ severity: hard_block
153
+ message: "Use parameterized queries (placeholders + a params argument), not f-strings or string concatenation."
154
+ scope: ["*.py", "*.js", "*.jsx", "*.ts", "*.tsx", "*.go", "*.rb", "*.java", "*.php", "*.cs", "*.rs"]
155
+
156
+ # ── Project rules (add yours below) ──────────────────────────────────────
157
+ """
158
+
159
+ CONTROLS_TEMPLATE = """\
160
+ version: "1.0"
161
+ # Canonical catalog of gates PCP enforces on this project. This is the control
162
+ # catalog a future audit-evidence document (`pcp provenance`) reads to build a
163
+ # per-file/per-criterion compliance card. control ids here are written verbatim
164
+ # into .pcp/telemetry.jsonl's `control_id` field by every gate call site — do
165
+ # not rename an id without updating the code that emits it.
166
+ #
167
+ # ssdf_practice references NIST SP 800-218 (Secure Software Development
168
+ # Framework) practice ids. This is an informed mapping to give the catalog a
169
+ # recognizable compliance vocabulary — it is NOT a certified SSDF/CMMC
170
+ # assessment. Treat it as a jumpstart, not a certificate.
171
+ controls:
172
+ - id: CTRL-001
173
+ name: "Test suite must pass"
174
+ layer: build-loop
175
+ mechanism: "qa.run_test_suite"
176
+ tool: "pytest | npm test | go test (auto-detected)"
177
+ enforcement: hard_block
178
+ description: "Full regression suite runs after every criterion's agent attempt and again at each wave-merge boundary. Blocks on failure. Skips (never blocks) if no test runner is detected — that skip is recorded, not silently treated as a pass."
179
+ ssdf_practice: ["PW.7.2"]
180
+
181
+ - id: CTRL-002
182
+ name: "Lint clean on changed files"
183
+ layer: build-loop
184
+ mechanism: "qa.run_lint"
185
+ tool: "ruff | eslint (auto-detected)"
186
+ enforcement: hard_block
187
+ description: "Lint scoped to the files changed by this criterion's attempt."
188
+ ssdf_practice: ["PW.7.2"]
189
+
190
+ - id: CTRL-003
191
+ name: "SAST + secret scan clean"
192
+ layer: build-loop
193
+ mechanism: "qa.run_sast"
194
+ tool: "semgrep"
195
+ enforcement: hard_block
196
+ description: "Static analysis + secret scan scoped to changed files. Skips (never blocks) if semgrep is not installed."
197
+ ssdf_practice: ["PW.7.2", "PW.8.1"]
198
+
199
+ - id: CTRL-004
200
+ name: "Layer 1 structural/AST rules"
201
+ layer: layer1
202
+ mechanism: "check.py + ci_rules.yaml (ast_pattern, file_exists)"
203
+ tool: "pcp check (pre-commit)"
204
+ enforcement: hard_block
205
+ description: "Deterministic pre-commit gate — modularity (MOD_*) and security (SEC_*) rules. A human can override a specific commit with `[pcp-bypass: reason]`; every bypass is logged (see CTRL-010) and this control's status for that commit is recorded as bypassed, not passed."
206
+ ssdf_practice: ["PS.1.1", "PW.7.1"]
207
+
208
+ - id: CTRL-005
209
+ name: "Architect review (diff vs persona/architecture/KB)"
210
+ layer: build-loop
211
+ mechanism: "architect_review.py, called per-criterion and at wave boundaries"
212
+ tool: "LLM judge (JUDGE_MODEL)"
213
+ enforcement: hard_block
214
+ description: "BLOCK-severity findings stop the build; a failed/erroring judge call is recorded as an error, not silently counted as a pass or a block."
215
+ ssdf_practice: ["PW.1.3", "PW.4.1"]
216
+
217
+ - id: CTRL-006
218
+ name: "PR alignment vs objective/target_state"
219
+ layer: layer2
220
+ mechanism: "gate.py `pcp gate` (PR-time) and build.py's in-loop advisory check"
221
+ tool: "LLM judge (JUDGE_MODEL)"
222
+ enforcement: advisory
223
+ description: "Never hard-blocks merge — score + regressions are logged for review."
224
+ ssdf_practice: ["PW.2.1"]
225
+
226
+ - id: CTRL-007
227
+ name: "Wave contract validation"
228
+ layer: wave-merge
229
+ mechanism: "build.py:_run_wave_merge, dependency completeness check"
230
+ tool: "n/a (deterministic)"
231
+ enforcement: hard_block
232
+ description: "Every declared inter-module dependency must have 100% complete acceptance criteria before the next dependency wave starts."
233
+ ssdf_practice: ["PS.1.1"]
234
+
235
+ - id: CTRL-008
236
+ name: "Strategy coverage & coupling"
237
+ layer: wave-merge
238
+ mechanism: "validate_strategy.py, re-run at each wave boundary"
239
+ tool: "LLM judge (coverage) + networkx graph math (coupling)"
240
+ enforcement: hard_block
241
+ description: "Coverage gaps or severe coupling violations (circular deps, God modules, shared state) block the next wave."
242
+ ssdf_practice: ["PW.1.1"]
243
+
244
+ - id: CTRL-009
245
+ name: "Deploy phase exit criteria"
246
+ layer: layer3
247
+ mechanism: "deploy_check.py, SDLC_phase.yaml exit_criteria"
248
+ tool: "n/a (deterministic)"
249
+ enforcement: hard_block
250
+ description: "Deterministic phase-exit gate — the only gate positioned right before a deploy action."
251
+ ssdf_practice: ["PO.3.2", "PW.9.1"]
252
+
253
+ - id: CTRL-010
254
+ name: "Bypass accountability"
255
+ layer: cross-cutting
256
+ mechanism: "check.py bypass log"
257
+ tool: "n/a"
258
+ enforcement: override
259
+ description: "A human can override CTRL-004 per-commit with `[pcp-bypass: reason]`. Every bypass is appended to bypass_log.yaml (reason + rules bypassed) and to telemetry.jsonl (result: bypassed) — never silent, always auditable."
260
+ ssdf_practice: ["PO.5.1"]
261
+
262
+ - id: CTRL-011
263
+ name: "Session transcript archival"
264
+ layer: cross-cutting
265
+ mechanism: "capture.py archive_transcript()"
266
+ tool: "n/a"
267
+ enforcement: advisory
268
+ description: "Every session transcript pcp capture is invoked on is copied (gzip-compressed) into .pcp/transcripts/ before classification runs — the full raw action record, not just the classifier's distilled BRD/decision-log summary, becomes part of this project's own durable evidence instead of living only in Claude Code's own ~/.claude/projects/ retention."
269
+ ssdf_practice: ["PO.3.2"]
270
+
271
+ - id: CTRL-012
272
+ name: "Ticket automation (analyze/approve/reject/submit)"
273
+ layer: cross-cutting
274
+ mechanism: "program-context-protocol-team: tickets/analysis.py, tickets/actions.py"
275
+ tool: "LLM judge (JUDGE_MODEL) for analysis; Temporal for submission"
276
+ enforcement: advisory
277
+ description: "A ticket ingested via program-context-protocol-team's webhook is analyzed (root-cause hypothesis + fix options), and every human decision (approve one option / reject) is recorded here, with the LLM's full raw analysis response stored as evidence — same as any other QA judge call. A human always decides; nothing in this control auto-picks an option."
278
+ ssdf_practice: ["PW.4.1", "RV.1.1"]
279
+
280
+ - id: CTRL-013
281
+ name: "Design consistency check"
282
+ layer: build-loop
283
+ mechanism: "build.py _run_design_consistency_check()"
284
+ tool: "n/a — deterministic regex, no external tool"
285
+ enforcement: advisory
286
+ description: "PCP Design lifecycle, stage 4 (Verify). For a UI-facing criterion, once .pcp/design_system.md has real established color tokens, flags hardcoded hex color literals in that criterion's target file — a heuristic signal the screen may not be using the project's own design system, not proof either way. Never blocks; a legitimate reason to hardcode a specific value is common (e.g. a brand-mandated exact color)."
287
+ ssdf_practice: ["PW.7.1"]
288
+
289
+ - id: CTRL-014
290
+ name: "logic_tier drift at wave-merge"
291
+ layer: wave-merge
292
+ mechanism: "build.py:_run_wave_tier_drift_check(), 5th wave-merge sub-check"
293
+ tool: "n/a — deterministic regex, no external tool"
294
+ enforcement: hard_block
295
+ description: "A completed criterion declaring logic_tier 1-5 (deterministic through cached-reuse — no runtime LLM call expected by definition) whose own target file demonstrably imports an LLM SDK is a real signal the declared decision no longer matches what was actually built. Only rung 6 (deep-think LLM) is expected to import one. Does not re-examine build_vs_buy — no comparably cheap deterministic signal exists for that field yet."
296
+ ssdf_practice: ["PW.1.1"]
297
+
298
+ - id: CTRL-015
299
+ name: "design_justification substance check"
300
+ layer: build-loop
301
+ mechanism: "build.py _run_design_justification_check(), JUDGE_MODEL + adversarial verify"
302
+ tool: "LLM judge (JUDGE_MODEL)"
303
+ enforcement: hard_block
304
+ description: "Active structural-forcing for PCP Design lifecycle stage 4 — design_audit.py's Feature Exposure Ladder is a passive post-hoc rollup (pure keyword/presence logic, a checklist full of junk strings still classifies as rung 3/4). This check runs during the build itself: judges whether a UI-facing criterion's submitted checklist_passed/jtbd_framing/deviations_from_system reflects real design thinking or was lazily filled just to pass validation, re-verified adversarially before it can block the criterion the same way architect-review/gate findings do."
305
+ ssdf_practice: ["PW.7.1"]
306
+
307
+ - id: CTRL-016
308
+ name: "build_vs_buy drift at wave-merge"
309
+ layer: wave-merge
310
+ mechanism: "build.py:_run_wave_build_vs_buy_drift_check(), 6th wave-merge sub-check"
311
+ tool: "n/a — deterministic Python import analysis, no external tool"
312
+ enforcement: hard_block
313
+ description: "A completed criterion declaring build_vs_buy=reuse_whole or fork_adapt implies an external dependency should be visible in its target file — zero external imports despite that claim is a real signal the declared decision no longer matches what was built. Deliberately narrower than the logic_tier drift check (CTRL-014): build_fresh is NOT checked in the other direction (package names routinely differ from import names — pyyaml/yaml, beautifulsoup4/bs4 — too noisy to trust as a hard gate), and reuse_partial/reimplement_from_reference are skipped entirely (no distinguishing import signature either way)."
314
+ ssdf_practice: ["PW.1.1"]
315
+
316
+ - id: CTRL-017
317
+ name: "build_vs_buy rationale substance check"
318
+ layer: build-loop
319
+ mechanism: "build.py _run_build_vs_buy_justification_check(), deterministic placeholder/word-count check"
320
+ tool: "n/a — deterministic regex/word-count, no external tool"
321
+ enforcement: hard_block
322
+ description: "Structural-forcing for build_vs_buy, same enforcement posture design_justification (CTRL-015) already gets — build_vs_buy's rationale is schema-required to be present but was never checked for substance, so a placeholder string like the literal unfilled prompt template text passed validation as a real decision. Deterministic, not an LLM judge call: unlike design_justification (UI-facing criteria only), build_vs_buy is required on EVERY criterion, so an LLM call here on every attempt would violate Token Discipline for a field that mostly just needs a placeholder-text check, not genuine semantic judgment."
323
+ ssdf_practice: ["PW.1.1"]
324
+
325
+ - id: CTRL-018
326
+ name: "Build-agent scope guard (over-reach allowlist)"
327
+ layer: build-loop
328
+ mechanism: "build.py _run_scope_check() — git-diff changed files vs the module's declared surface (all criterion target files, .pcp/strategy/modules/<module>/, .pcp/design_system.md, test files)"
329
+ tool: "n/a — deterministic path matching over git diff, no external tool"
330
+ enforcement: advisory
331
+ description: "Over-reach guard: a criterion agent modifying files outside its own module's declared surface is the classic unattended-loop failure mode (loop touches unrelated code). PCP had a denylist (protected_path) but no allowlist until now. Warn-only by default — legitimate cross-cutting writes exist (module registries, shared interfaces), so the false-positive rate should be measured on real builds before enforcement; PCP_BUILD_SCOPE_MODE=block upgrades it to a hard per-attempt gate, =off disables (recorded as skipped, never silently)."
332
+ ssdf_practice: ["PS.1.1", "PW.7.1"]
333
+
334
+ - id: CTRL-019
335
+ name: "Logic-tier mechanism presence (positive check)"
336
+ layer: wave-merge
337
+ mechanism: "build.py _run_wave_tier_presence_check() — rungs 2-5 target files checked for their declared rung's mechanism signature (solver/ML/retrieval/cache imports; stdlib lru_cache counts for rung 5)"
338
+ tool: "n/a — deterministic import analysis, no external tool"
339
+ enforcement: advisory
340
+ description: "CTRL-014 checks the negative for rungs <=5 (no LLM SDK); this checks the positive: a rung-2 criterion with no solver import, rung-4 with no retrieval dependency, rung-5 with no cache layer is likely a tier declared but not implemented at that tier. Advisory — the mechanism may live in a shared helper the target imports; earns hard-block only after a measured false-positive rate."
341
+ ssdf_practice: ["PW.1.1"]
342
+
343
+ - id: CTRL-021
344
+ name: "Context-route staleness"
345
+ layer: wave-merge
346
+ mechanism: "context_map.validate() — every route in .pcp/context_map.yaml must resolve to at least one existing file (per known module for {module} templates, or via fallback)"
347
+ tool: "n/a — deterministic path resolution, no external tool"
348
+ enforcement: advisory
349
+ description: "The context routing table is itself a drift surface: files evolve, routes go stale, and a stale route silently starves agents of the context they need — worse and less visible than over-feeding, since a missing rule breaks a gate invisibly while an extra file only costs tokens. Checked at wave boundaries and by pcp doctor."
350
+ ssdf_practice: ["PW.1.1"]
351
+
352
+ - id: CTRL-020
353
+ name: "Rung-necessity challenge (over/under-declaration)"
354
+ layer: wave-merge
355
+ mechanism: "build.py _run_wave_rung_necessity_check() — deterministic judgment-keyword contradiction check for rung-1 declarations, plus ONE batched judge call per wave over completed rung-6 criteria (could a cheaper rung serve?)"
356
+ tool: "judge model (rung-6 half only; rung-1 half is keyword-deterministic)"
357
+ enforcement: advisory
358
+ description: "The Decision Integrity gap: nothing previously challenged a criterion lazily declared rung 6 that a truth table could serve, or rung 1 with judgment-shaped language in its own description. The rung-6 half is the ladder's one irreducibly semantic gate, so it gets the same posture as coverage_score — advisory, recorded, never trusted blindly, never blocking."
359
+ ssdf_practice: ["PW.1.1"]
360
+
361
+ - id: CTRL-022
362
+ name: "Accessibility (WCAG) scan"
363
+ layer: build-loop
364
+ mechanism: "build.py _run_a11y_check(), uat.check_axe() -- @axe-core/cli via npx"
365
+ tool: "axe-core"
366
+ enforcement: advisory
367
+ description: "PCP Design lifecycle, stage 4 addendum. Only fires on a UI-facing criterion that declares a url -- deterministic WCAG scan of the rendered page, no LLM involved. Advisory, same warn-first rollout posture as CTRL-013/018/019/020: a11y defects are real and common in AI-generated UI, but false-positive rate on real projects isn't measured yet."
368
+ ssdf_practice: ["PW.7.1"]
369
+
370
+ - id: CTRL-023
371
+ name: "Visual quality (checklist-anchored VLM judge)"
372
+ layer: build-loop
373
+ mechanism: "build.py _run_visual_quality_check(), uat.check_visual_quality() -- llm.call_json_with_images against a fresh screenshot"
374
+ tool: "judge model (JUDGE_MODEL)"
375
+ enforcement: advisory
376
+ description: "PCP Design lifecycle, stage 4 addendum, from a 2026-07-18 research pass on the intent-to-UI articulation gap: a checklist-anchored VLM judge measures ~94% human-correlation vs. ~21% for a bare freeform review, so this deliberately judges a fixed checklist rather than asking 'does this look good.' Only fires on a UI-facing criterion that declares a url; compares against the criterion's own reference_image when one is declared. Advisory only -- an LLM visual judgment should never hard-block a build until its own false-positive rate is measured on real projects, same posture every other judge-model gate in this catalog started at."
377
+ ssdf_practice: ["PW.7.1"]
378
+
379
+ - id: CTRL-024
380
+ name: "Module spec alignment (Two Validation Passes, Pass 1)"
381
+ layer: wave-merge
382
+ mechanism: "validate_module.py run_validate_module(), called per-module in build.py _run_wave_merge"
383
+ tool: "judge model (JUDGE_MODEL)"
384
+ enforcement: advisory
385
+ description: "Closes an orphan found 2026-07-20: `pcp validate-module` existed as a fully-built standalone command but was never actually called from anywhere in the enforcement lifecycle -- only `validate-strategy` (Pass 2: modules vs objective) was wired into the wave-merge gate; Pass 1 (does each module's own spec still align with the objective/decomposition) had no automatic trigger at all. Advisory, same warn-first rollout posture as every other judge-model gate in this catalog -- false-positive rate on real projects not measured yet."
386
+ ssdf_practice: ["PW.1.1"]
387
+
388
+ - id: CTRL-025
389
+ name: "Navigation depth (click-depth outliers)"
390
+ layer: wave-merge
391
+ mechanism: "build.py _run_nav_depth_check(), self-declared nav_depth field audited against PCP_NAV_DEPTH_THRESHOLD"
392
+ tool: "n/a (deterministic)"
393
+ enforcement: advisory
394
+ description: "How many clicks/taps from the app's primary entry point to reach a feature -- self-declared per criterion like logic_tier/build_vs_buy, not computed via real routing-graph analysis (a bigger, per-framework build this field doesn't earn on its own). Flags criteria missing the field entirely and criteria exceeding the 3-click threshold (default, PCP_NAV_DEPTH_THRESHOLD). Advisory -- a declared value is still a claim, not a measurement."
395
+ ssdf_practice: ["PW.7.1"]
396
+
397
+ - id: CTRL-026
398
+ name: "Feature customization structural check"
399
+ layer: build-loop
400
+ mechanism: "build.py _run_customization_check(), design_justification.customizable + customization_notes"
401
+ tool: "n/a (deterministic)"
402
+ enforcement: advisory
403
+ description: "A criterion declaring design_justification.customizable=true should show SOME settings-shaped signal in its own target file (setting/preference/config keyword) -- a bare true with no such signal reads the same way a lazily-filled design_justification does. Deterministic keyword scan, not a semantic judge call -- cheap, same Token Discipline posture as CTRL-017's build_vs_buy placeholder check."
404
+ ssdf_practice: ["PW.7.1"]
405
+
406
+ - id: CTRL-027
407
+ name: "Top menu bar convention (desktop_app archetype only)"
408
+ layer: wave-merge
409
+ mechanism: "build.py _run_menu_bar_check(), .pcp/design_conventions.yaml ui_archetype + top_menu_bar.required_menus"
410
+ tool: "n/a (deterministic)"
411
+ enforcement: advisory
412
+ description: "File/Edit/View/Help-style top menu bar is a desktop-app convention, not a universal one -- this check stays completely inert (never even runs) unless a human explicitly sets ui_archetype: desktop_app in .pcp/design_conventions.yaml (default web_app). When active, deterministic substring scan of UI-facing criteria target files for each required menu label; missing ones flagged advisory."
413
+ ssdf_practice: ["PW.7.1"]
414
+
415
+ - id: CTRL-028
416
+ name: "UI kit recipe completeness + import verification"
417
+ layer: wave-merge
418
+ mechanism: "build.py _run_wave_ui_kit_check(), .pcp/ui_kit_recipes.yaml organism->component mapping + screen_archetypes->required-organisms recipe"
419
+ tool: "n/a (deterministic)"
420
+ enforcement: advisory
421
+ description: "Build-vs-buy for PCP's UI building blocks: PCP does not build or maintain component code -- shadcn/ui (MIT, vendored per-project, official MCP server for search/retrieval) already solves that. PCP owns the thin layer on top: which organisms an archetype needs (recipe completeness -- a criterion declaring screen_archetypes=[dashboard] should show chart-panel/data-table/kpi-tile among its ui_organisms), and whether a declared organism shows real evidence of use (import-path substring match against the criterion's own target file, same mechanism CTRL-019 already uses for logic_tier). Advisory -- a real import can legitimately not match the hint (re-export, alias), so this is a review signal, not proof of non-use. Stays inert (no telemetry record) unless .pcp/ui_kit_recipes.yaml exists."
422
+ ssdf_practice: ["PW.7.1"]
423
+
424
+ - id: CTRL-029
425
+ name: "Generic lazy-marker scan"
426
+ layer: build-loop
427
+ mechanism: "build.py _run_lazy_marker_check() -- regex scan of ALL changed files for TODO/FIXME/XXX/HACK/placeholder/not-implemented markers and stub function bodies (pass/... only)"
428
+ tool: "n/a (deterministic regex)"
429
+ enforcement: advisory
430
+ description: "Lazy-agent mitigation backlog item 3 (2026-07-20): PCP previously only checked for placeholder text narrowly, inside build_vs_buy/design_justification's own free-text fields (CTRL-017/015). This is the general form -- any changed file, any lazy-shaped marker or stub body -- surfaced as a count/location for a human to judge, since a TODO or a stub can be legitimate. Advisory only, never blocks."
431
+ ssdf_practice: ["PW.7.1"]
432
+
433
+ - id: CTRL-030
434
+ name: "Integrity Auditor (retrospective statistical drift)"
435
+ layer: wave-merge
436
+ mechanism: "integrity_audit.py analyze() -- generalizes coverage_audit.py's drift-detection pattern across telemetry.jsonl: fast completions vs. declared logic_tier, per-module placeholder-flag concentration, recurring unresolved findings, uniform/templated evidence"
437
+ tool: "n/a (deterministic, statistical)"
438
+ enforcement: advisory
439
+ description: "Swarm-role backlog's Integrity Auditor (2026-07-20): retrospective-only -- reads already-complete criteria across the whole project, can't correct what's already built, only flags for human review. Deterministic-first, same posture as every other statistical-drift check in this catalog; the genuinely ambiguous tail (an LLM judging the flagged pattern) is not built in this pass. Runs at wave boundaries, not per-criterion -- the value is seeing patterns across many completed criteria no single-criterion CTRL check can see by design."
440
+ ssdf_practice: ["PW.1.1"]
441
+
442
+ - id: CTRL-031
443
+ name: "module_logic_breakdown built-code verification"
444
+ layer: wave-merge
445
+ mechanism: "build.py _run_wave_logic_breakdown_check() -- keyword-overlap between a module's declared spec.module_logic_breakdown and the combined description+target-file content of its OWN completed criteria"
446
+ tool: "n/a (deterministic keyword scan)"
447
+ enforcement: advisory
448
+ description: "Lazy-agent backlog item 9's verification half: kickoff/pm already keyword-check a declared breakdown item against a module's own criteria descriptions BEFORE build (check_module_logic_breakdown_coverage); this re-checks AFTER build, against completed criteria's actual target-file content -- does code exist that plausibly reflects each declared internal component, not just that a criterion was worded to mention it. Deterministic, not the CTRL-015-style LLM judge the backlog item originally sketched -- the semantic half (does the code genuinely FULFILL the component) stays deferred. Inert unless a module declares module_logic_breakdown."
449
+ ssdf_practice: ["PW.1.1"]
450
+
451
+ - id: CTRL-032
452
+ name: "Architect pre-flight (pre-implementation sanity check)"
453
+ layer: build-loop
454
+ mechanism: "build.py _run_architect_preflight() -- one judge-model call reviewing a HIGH-RISK criterion's declared logic_tier/build_vs_buy/module context BEFORE any code is written, injected as advisory context into attempt 1's own prompt"
455
+ tool: "judge model (JUDGE_MODEL)"
456
+ enforcement: advisory
457
+ description: "Swarm-role backlog's genuinely new lifecycle point: PCP's existing architect-review (CTRL-005) is post-hoc only -- it reviews the diff after code exists. This runs BEFORE, for high-risk criteria only (logic_tier >= 5, or build_vs_buy of reuse_whole/fork_adapt). Advisory prompt-injection in this pass, not the block_findings channel the backlog originally sketched -- PCP's attempt loop has no separate plan-then-code step, so routing this into block_findings would mean skipping a whole attempt with zero code written, a real behavior change to the 3-attempt contract. Upgrade path is the same L1-report-first rollout every other check in this catalog followed."
458
+ ssdf_practice: ["PW.1.1"]
459
+
460
+ - id: CTRL-033
461
+ name: "ci_rules.yaml contract completeness"
462
+ layer: wave-merge
463
+ mechanism: "build.py _run_wave_contract_completeness_check() -- every hard_block rule in ci_rules.yaml missing an ABC-style contract block (preconditions/invariants/recovery) is flagged"
464
+ tool: "n/a (deterministic)"
465
+ enforcement: advisory
466
+ description: "Agent Behavioral Contracts reference pattern (arXiv:2602.22302, 2026-07-20 research pass -- see docs/research-rigidity-vs-reliability-2026-07.md). ci_rules.yaml's rules gained an optional `contract` block (preconditions/invariants/recovery -- Governance is already the existing `severity` field, not duplicated). This is the presence check, same posture CTRL-019 already uses for logic_tier: a hard_block rule with no contract just relies on the flat severity gate, exactly as it always did -- advisory, never blocks, project-wide (one ci_rules.yaml, not per-module)."
467
+ ssdf_practice: ["PW.1.1"]
468
+
469
+ - id: CTRL-034
470
+ name: "Install-only fast path (direct priorart match)"
471
+ layer: build-loop
472
+ mechanism: "build.py _run_install_only() -- criterion/module declares install_only+install_command; human approval (or --yes), then run install_command, then Layer 1 + full test suite as a deterministic smoke test. No LLM calls, no TDD/architect-review cycle."
473
+ tool: "n/a (deterministic)"
474
+ enforcement: hard_block
475
+ description: "When priorart already confirms an existing package is a direct match for a criterion (or whole module), running the full TDD/architect-review/LLM-gate cycle just to write a thin wrapper around an install is pure waste. This fast path skips the coding-agent session entirely -- but never skips verification: a human must confirm the match (install_approvals.yaml, hash-chained like bypass_log.yaml), and the install still has to pass a real smoke test (Layer 1 + full regression suite) before the criterion is marked complete. A decline or a failed smoke test falls through to the normal full build path, never a silent skip."
476
+ ssdf_practice: ["PW.4.1", "PW.7.2"]
477
+
478
+ - id: CTRL-035
479
+ name: "Objective-conflict gate"
480
+ layer: build-loop
481
+ mechanism: "objective_conflicts.reconcile() -- build.py's first preflight step, before any module/agent work starts. An active brd_items.yaml entry with a live drift_flag (set by capture.py's classifier when a captured business item conflicts with objective.md's text) is stamped with a SHA-256 hash of objective.md+target_state.md's content at flag time. If current content still matches that hash, the conflict is unresolved and blocks. If the hash no longer matches, the file was actually edited since the flag was raised -- auto-clears deterministically."
482
+ tool: "n/a (deterministic hash check; flagging itself is Haiku-classified in capture.py)"
483
+ enforcement: hard_block
484
+ description: "2026-07-22 incident, Project O dogfood: a business objective correction was discussed and agreed, objective.md/target_state.md never got rewritten to match, and a 30+-agent build cycle two days later built exactly the rejected shape end-to-end -- every other gate passed, because every gate validates the build against objective.md as given, never against whether objective.md is still true. `pcp objective-conflicts` lists/dismisses flags; `pcp correct-objective` is the human-gated resolution path (LLM proposes an objective.md/target_state.md rewrite from the stated correction, human approves the real diff, then it's written -- same pattern pcp kickoff/pm already use for module specs). `pcp build` also self-captures its own live session (CLAUDE_CODE_SESSION_ID) before this gate even runs, so a correction discussed in the same still-open session that authorizes the build gets a chance to be caught."
485
+ ssdf_practice: ["PW.1.1", "PW.4.1"]
486
+
487
+ - id: CTRL-036
488
+ name: "Narrative lint (CLAUDE.md vs tracked state)"
489
+ layer: wave-merge
490
+ mechanism: "narrative_lint.py run() -- deterministic stale-dated-reference and missing-referenced-file scan of every CLAUDE.md-family file, plus ONE batched judge call comparing status-shaped narrative lines (Pending/Open Decision/Planned/etc.) against current_state.md+architecture.md for semantic contradiction"
491
+ tool: "judge model (JUDGE_MODEL, contradiction check only; the two file-scan sub-checks are deterministic)"
492
+ enforcement: advisory
493
+ description: "2026-07-24 fleet evidence: a context-hygiene pass across 4 projects (Project E, Project W, Project A, Project M) found narrative prose in CLAUDE.md -- stage descriptions, 'Open Decisions,' 'Pending' lists -- drifted from tracked state 3-for-3 in projects checked (largest: ~65 lines of dead launch-weekend content still live 7 weeks post-launch, undetected). Nothing in PCP's existing gate catalog checks free-text prose against current_state.md/architecture.md -- every other gate validates code against spec. Deterministic sub-checks port `~/.claude/scripts/session-hygiene-check.sh`'s mechanical checks into PCP's own enforcement lifecycle (telemetry, not just a SessionStart print); the semantic contradiction check is the one irreducibly judgment-shaped part, same rung-6 posture as CTRL-020's rung-necessity check -- one batched call, advisory, fails open. Standalone via `pcp narrative-lint`; also runs at every wave-merge boundary."
494
+ ssdf_practice: ["PW.1.1"]
495
+
496
+ - id: CTRL-037
497
+ name: "Build-loop bypass detector"
498
+ layer: cross-cutting
499
+ mechanism: "build_loop_bypass.py check() -- git log commit dates vs telemetry.jsonl's last entry timestamp, run by doctor.py's check_environment() (pcp build/watch/deploy's own automatic preflight) and the interactive `pcp doctor`"
500
+ tool: "n/a (deterministic)"
501
+ enforcement: advisory
502
+ description: "2026-07-24 incident (Project O, 3rd recurrence: 07-08, mid-July, 07-21-onward): pcp build's formal gated loop stopped being invoked, not by decision -- a real Postgres schema-bloat bug got killed and never relaunched -- while 31 commits landed via `pcp pm` + ad-hoc work over the next 3 days, telemetry.jsonl silent the whole time. Nothing previously surfaced that drift at the moment it started; it only became visible via manual transcript archaeology. Flags when commits continue past telemetry's last record by more than PCP_BUILD_LOOP_BYPASS_THRESHOLD_DAYS (default 3) -- inert until a project has at least one real telemetry record to compare against."
503
+ ssdf_practice: ["PW.1.1"]
504
+ """
505
+
506
+ CONTEXT_MAP_TEMPLATE = """\
507
+ version: "1.0"
508
+ # Deterministic context routing: which files an agent reads per scenario.
509
+ # Human-editable; consumed by pcp build's prompt builder. Rules that keep
510
+ # routing safe (see logic_tier_guide.md's sibling doc rationale):
511
+ # - scenarios are detected from schema fields, never by an LLM
512
+ # - intent files (objective/spec) are routed WHOLE, never fragmented
513
+ # - sliced state must be a GENERATED projection (docs/built.md), never a
514
+ # hand-maintained copy -- copies diverge, projections can't
515
+ # `{module}` is substituted per-criterion. `fallback` used only when no
516
+ # primary file exists. Routes resolving to zero files are flagged (CTRL-021).
517
+ routes:
518
+ always:
519
+ files:
520
+ - .pcp/objective.md
521
+ - .pcp/architecture.md
522
+ - .pcp/architect_persona.md
523
+ module_state:
524
+ files:
525
+ - .pcp/strategy/modules/{module}/docs/built.md
526
+ fallback:
527
+ - .pcp/current_state.md
528
+ ui_facing:
529
+ files:
530
+ - .pcp/design_system.md
531
+ logic_tier_declared:
532
+ files:
533
+ - .pcp/logic_tier_guide.md
534
+ """
535
+
536
+ LOGIC_TIER_GUIDE_TEMPLATE = """\
537
+ # Logic-Tier Selection & Implementation Guide
538
+
539
+ Read this when choosing a criterion's `logic_tier` or implementing a criterion
540
+ whose tier is already declared. Scaffolded by `pcp init`; doctrine, not
541
+ project-specific — edit only if your project genuinely disagrees with it.
542
+
543
+ ## Choosing a rung: classify the CORRECTNESS ORACLE, not the task
544
+
545
+ Ask "how would I know an output is correct?", never "what kind of task is this":
546
+
547
+ | Correctness means... | Rung |
548
+ |---|---|
549
+ | It satisfies rules I can write down completely | 1 |
550
+ | It's the best feasible option under known constraints | 2 |
551
+ | It matches what historically happened in similar cases | 3 |
552
+ | It's faithful to what our documents/records actually say | 4 |
553
+ | Same as last time for the same question (overlay on any rung) | 5 |
554
+ | A reasonable human would accept it — and another human might accept a different answer | 6 |
555
+
556
+ ## The walk (cheapest first — stop at the first rung whose litmus passes)
557
+
558
+ 0. DECOMPOSE FIRST. A criterion is rarely one decision. "Categorize expenses
559
+ and flag anomalies with explanations" = categorization (rung 1 or 3) +
560
+ anomaly scoring (3) + explanation text (6). Classify each decision point
561
+ separately; declare the highest rung actually present; implement each
562
+ sub-decision at its own rung.
563
+ 1. Rung 1 litmus: could you write unit tests asserting EXACT outputs for every
564
+ input class right now (~<=20 rule branches, zero "it depends")?
565
+ Danger sign: judgment verbs in the description (recommend/interpret/assess).
566
+ 2. Rung 2 litmus: you cannot enumerate the answers but CAN enumerate the
567
+ constraints and a scoring function — "best/cheapest/shortest X subject to Y".
568
+ Danger sign: "optimization" with no writable objective = preference judgment.
569
+ 3. Rung 3 litmus: (input -> correct outcome) history exists or is cheap to
570
+ collect, hundreds of rows minimum. Countable before any code is written.
571
+ 4. Rung 4 litmus: the answer already exists as text in a bounded corpus you
572
+ control — the job is retrieval and assembly, not knowledge creation.
573
+ 5. Rung 5 is an OVERLAY, not a destination: repetitive question stream ->
574
+ cache in front of whichever rung serves it.
575
+ 6. Rung 6 only after 1-4 each failed FOR A STATED REASON — the reasons go in
576
+ build_vs_buy.rationale. Schema-cage the output, define the refusal path,
577
+ and accept: shape is reproducible, content is not.
578
+
579
+ ## Implementing each rung + where to search BEFORE building
580
+
581
+ ### Rung 1 — deterministic
582
+ Process: rules as DATA (table/dict/config), never nested ifs; boundary +
583
+ property tests; exhaustive tests where the domain is finite.
584
+ Search first: domain rules are usually maintained packages — pycountry,
585
+ dateutil, babel, holidays. Never hand-code tax tables, timezone rules, ISO enums.
586
+
587
+ ### Rung 2 — solver
588
+ Process: formalize on paper (variables, domains, constraints, objective);
589
+ brute-force a TINY instance first — that is your solver's ground truth; then
590
+ OR-Tools CP-SAT by default, pulp/CBC for LP/MIP, z3 for logic/SMT, networkx
591
+ for graph problems. Done = solver output matches brute force on small instances.
592
+ Search first: OR-Tools examples gallery, MiniZinc model library (pattern
593
+ catalog), OR Stack Exchange for the model shape.
594
+
595
+ ### Rung 3 — statistical/ML
596
+ Process: data audit (row count, label quality, leakage) -> dumb baseline
597
+ (majority class / logistic) -> sklearn pipeline -> holdout eval -> confidence
598
+ threshold WITH a declared fallback path (below threshold -> rung 6 or human).
599
+ Search first: prefer PRETRAINED over training — HuggingFace for text/vision;
600
+ Kaggle and papers-with-code under the task's name; sklearn before anything exotic.
601
+
602
+ ### Rung 4 — RAG
603
+ Process: corpus inventory (contents, freshness, owner) -> try grep/BM25
604
+ (rank_bm25, free) BEFORE embeddings -> hybrid only if BM25 measurably fails ->
605
+ retrieval-score threshold with an explicit "no answer" refusal (refusal beats
606
+ hallucination) -> cite the source in the output -> decompose the synthesized
607
+ answer into individual claims and check each against the retrieved context
608
+ (entailed / contradicted / unsupported) before returning it -- retrieval-score
609
+ threshold only proves retrieval worked, not that the synthesis stayed
610
+ faithful to what was retrieved. Where the corpus is structured (exact field
611
+ names, not free text), this is a cheap deterministic string/value match, not
612
+ an LLM judge call; reserve an LLM claim-check for synthesis that actually
613
+ combines/derives from multiple fields.
614
+ Search first: semantic-router; a vector DB only when BM25 fails on real queries.
615
+
616
+ ### Rung 5 — cached reuse
617
+ Process: define the cache key (normalized input; embedding similarity only for
618
+ paraphrase tolerance) -> invalidation tied to the UNDERLYING DATA's change
619
+ events, not just TTL -> spot-check recompute a sample % -> collision test.
620
+ Search first: functools.lru_cache -> diskcache/redis -> GPTCache-class only
621
+ when semantic matching is proven necessary.
622
+
623
+ ### Rung 6 — LLM (last resort)
624
+ Process: output schema FIRST (Pydantic/JSON Schema) -> prompt = role + inputs
625
+ + schema + refusal rule -> Instructor/Outlines validation -> low temperature ->
626
+ verify pass when stakes are high -> every call logged with cost.
627
+ Search guidance is INVERTED here: search for a way OFF rung 6 — recheck rungs
628
+ 1-5, then look for a bounded sub-decision split before accepting rung 6.
629
+
630
+ ## Always, at every rung
631
+ Record the candidates you actually checked in build_vs_buy.candidates_considered
632
+ — the search step and the audit trail are the same act. A criterion whose
633
+ rationale names nothing checked is a criterion where nothing was checked.
634
+ """
635
+
636
+ PRETOOLUSE_GUARD_TEMPLATE = """\
637
+ #!/bin/sh
638
+ # PCP tool-call-time guard (scaffolded by pcp init, 2026-07-17).
639
+ #
640
+ # Commit-time gates (pcp check) catch a protected-path edit only AFTER the
641
+ # agent already made it. This PreToolUse hook denies the Edit/Write at the
642
+ # moment the agent attempts it — the layer Endor Agent Governance and
643
+ # Microsoft's agent-governance-toolkit operate at. DENY-only by design: a
644
+ # hook that auto-ALLOWS anything is an approval bypass and gets hard-blocked
645
+ # by Claude Code's own permission layer (see CLAUDE.md Hard Rules history).
646
+ #
647
+ # NOT wired automatically. To enable, add to .claude/settings.json yourself:
648
+ # "hooks": {"PreToolUse": [{"matcher": "Edit|Write",
649
+ # "hooks": [{"type": "command", "command": "sh .pcp/hooks/pretooluse_guard.sh"}]}]}
650
+ #
651
+ # Requires jq. Exits 0 (no opinion) when jq is missing or input is unparseable.
652
+ command -v jq >/dev/null 2>&1 || exit 0
653
+ INPUT=$(cat)
654
+ FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
655
+ [ -z "$FILE" ] && exit 0
656
+ if [ "$PCP_AGENT_SESSION" = "1" ]; then
657
+ case "$FILE" in
658
+ *.pcp/objective.md|*.pcp/target_state.md|*.pcp/architecture.md|*.pcp/ci_rules.yaml|*.pcp/controls.yaml|*.pcp/SDLC_phase.yaml|*.pcp/strategy/decomposition.md|*.pcp/strategy/dependency_map.md|*.pcp/strategy/modules/*/spec.yaml|*.pcp/strategy/modules/*/acceptance.yaml)
659
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"PCP: unattended agent sessions may not edit human-authorized spec files. Route the change to the human, who applies it via pcp correct-objective / pcp pm / pcp amend (diff shown, approved, then written). See .pcp/ci_rules.yaml protected_path."}}\\n'
660
+ exit 0
661
+ ;;
662
+ esac
663
+ fi
664
+ exit 0
665
+ """
666
+
667
+ BUILD_LOOP_WARNING_TEMPLATE = """\
668
+ #!/usr/bin/env python3
669
+ # PCP build-loop warning (scaffolded by pcp init, 2026-07-24).
670
+ #
671
+ # Real Project O finding: CTRL-037 (pcp doctor) catches build-loop
672
+ # bypass only in retrospect -- after the fact, only if someone runs
673
+ # pcp doctor. This is the real-time version: fires the moment an Edit/Write
674
+ # happens outside pcp build's own gated agent loop (PCP_AGENT_SESSION != 1),
675
+ # so the warning lands when it's actually useful -- before the work
676
+ # happens, not days later. Purely informational: no permissionDecision is
677
+ # ever set, so this can never block, ask, or grant a bypass -- it cannot
678
+ # hit the same hard-block Claude Code's permission layer applies to a hook
679
+ # that tries to auto-approve something (see verify-syntax-fix.sh's own
680
+ # history). One warning per session, not per edit -- noise defeats the
681
+ # point.
682
+ #
683
+ # NOT wired automatically. To enable, add to .claude/settings.json yourself:
684
+ # "hooks": {"PreToolUse": [{"matcher": "Edit|Write",
685
+ # "hooks": [{"type": "command", "command": "python3 .pcp/hooks/build_loop_warning.py"}]}]}
686
+ import json
687
+ import os
688
+ import sys
689
+ from pathlib import Path
690
+
691
+
692
+ def main() -> None:
693
+ try:
694
+ payload = json.load(sys.stdin)
695
+ except Exception:
696
+ sys.exit(0)
697
+
698
+ cwd = payload.get("cwd") or "."
699
+ project_root = Path(cwd).resolve()
700
+ pcp_dir = project_root / ".pcp"
701
+ if not pcp_dir.is_dir():
702
+ sys.exit(0) # not a PCP-governed project
703
+
704
+ if os.environ.get("PCP_AGENT_SESSION") == "1":
705
+ sys.exit(0) # inside pcp build's own gated loop already
706
+
707
+ session_id = payload.get("session_id") or os.environ.get("CLAUDE_CODE_SESSION_ID") or "unknown"
708
+ marker = pcp_dir / f".build_loop_warning_shown_{session_id}"
709
+ if marker.exists():
710
+ sys.exit(0) # already warned this session
711
+ try:
712
+ marker.write_text("")
713
+ except OSError:
714
+ pass
715
+
716
+ msg = (
717
+ "PCP: this edit is happening outside pcp build's gated loop -- "
718
+ "no architect-review/QA/telemetry will be recorded for this change. "
719
+ "Run `pcp build` for a verified build, or continue if this is intentional "
720
+ "ad-hoc work (`pcp build-status` / .pcp/build_report.md show what pcp build "
721
+ "gives you that this path skips)."
722
+ )
723
+ print(json.dumps({
724
+ "systemMessage": msg,
725
+ "hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": msg},
726
+ }))
727
+ sys.exit(0)
728
+
729
+
730
+ if __name__ == "__main__":
731
+ main()
732
+ """
733
+
734
+ SESSION_UPDATE_CHECK_TEMPLATE = """\
735
+ #!/usr/bin/env python3
736
+ # PCP update-available notice (scaffolded by pcp init, 2026-07-31).
737
+ #
738
+ # Announce-only, on purpose: `pcp self-update` (git pull --ff-only, human-
739
+ # run) is the only thing that ever changes code. This hook just tells you
740
+ # an update exists at session start -- it never fetches-and-pulls itself,
741
+ # never runs in the background, and never touches the working tree. Same
742
+ # reasoning as build_loop_warning.py: a hook that can silently change
743
+ # behavior for a session already in progress is the risk this project
744
+ # spent real effort removing elsewhere (the 2026-07-27 cron/curl launch
745
+ # blocker) -- fixing it here only to reintroduce the shape at a different
746
+ # trigger (session start instead of a timer) would be the same mistake.
747
+ #
748
+ # NOT wired automatically. To enable, add to .claude/settings.json yourself:
749
+ # "hooks": {"SessionStart": [{"hooks": [{"type": "command",
750
+ # "command": "python3 .pcp/hooks/session_update_check.py"}]}]}
751
+ import json
752
+ import subprocess
753
+ import sys
754
+ from pathlib import Path
755
+
756
+
757
+ def main() -> None:
758
+ try:
759
+ payload = json.load(sys.stdin)
760
+ except Exception:
761
+ payload = {}
762
+
763
+ cwd = payload.get("cwd") or "."
764
+ project_root = Path(cwd).resolve()
765
+ if not (project_root / ".pcp").is_dir():
766
+ sys.exit(0) # not a PCP-governed project
767
+
768
+ try:
769
+ result = subprocess.run(
770
+ ["pcp", "self-update", "--check"],
771
+ capture_output=True, text=True, timeout=15,
772
+ )
773
+ except Exception:
774
+ sys.exit(0) # offline, pcp not on PATH, etc. -- stay silent, never block boot
775
+
776
+ msg = (result.stdout or "").strip()
777
+ if not msg or "update available" not in msg.lower():
778
+ sys.exit(0) # current, unknown, or unavailable -- nothing worth announcing
779
+
780
+ print(json.dumps({
781
+ "systemMessage": msg,
782
+ "hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": msg},
783
+ }))
784
+ sys.exit(0)
785
+
786
+
787
+ if __name__ == "__main__":
788
+ main()
789
+ """
790
+
791
+ GITIGNORE_TEMPLATE = """\
792
+ # Build artifacts — committing these breaks PCP's parallel-module worktree
793
+ # merges (and pollutes diffs the gates judge). Scaffolded by `pcp init` only
794
+ # when the project had no .gitignore at all; edit freely, it's yours now.
795
+ __pycache__/
796
+ *.pyc
797
+ *.pyo
798
+ .pytest_cache/
799
+ .coverage
800
+ htmlcov/
801
+ node_modules/
802
+ dist/
803
+ build/
804
+ *.egg-info/
805
+ .venv/
806
+ venv/
807
+ .DS_Store
808
+ .pcp/.build_loop_warning_shown_*
809
+
810
+ # Agent-session-local config. Claude Code writes these per working directory,
811
+ # and a worktree-scoped value (TMPDIR, granted permissions) differs in every
812
+ # parallel build worktree. Committing them turns every wave merge into an
813
+ # add/add conflict on a scratch config file — 2026-07-25 Project O,
814
+ # where three criteria that had all passed their gates could not be merged.
815
+ .claude/settings.json
816
+ .claude/settings.local.json
817
+
818
+ # PCP's own operational writes. These are auto-generated audit artifacts, and
819
+ # PCP appends to them in the MAIN .pcp/ throughout a build by design. Tracking
820
+ # them means a parallel-build merge hits "your local changes would be
821
+ # overwritten by merge" and refuses — 2026-07-27 Project S dogfood, where a
822
+ # criterion that had passed every gate could not be merged because the token
823
+ # ledger had moved underneath it. Governance SPECS (objective, strategy/,
824
+ # ci_rules, controls) stay tracked deliberately; only the run-time logs go.
825
+ .pcp/token_ledger.yaml
826
+ .pcp/telemetry.jsonl
827
+ .pcp/decision_log.jsonl
828
+ .pcp/coverage_audit.jsonl
829
+ .pcp/build_progress.yaml
830
+ .pcp/run_ledger.jsonl
831
+ .pcp/hidden_coupling.json
832
+ .pcp/notify_heartbeat.yaml
833
+ .pcp/escalations.yaml
834
+ .pcp/prune_log.yaml
835
+ .pcp/brd.md
836
+ .pcp/brd_items.yaml
837
+ .pcp/evidence/
838
+ .pcp/transcripts/
839
+
840
+ # pytest-testmon's per-test dependency cache — a build artifact, per-worktree.
841
+ .testmondata
842
+ .testmondata-journal
843
+ """
844
+
845
+ SDLC_PHASE_TEMPLATE = """\
846
+ version: "1.0"
847
+ current_phase: alpha
848
+ phases:
849
+ - name: alpha
850
+ exit_criteria:
851
+ - id: E001
852
+ description: "Core functionality implemented"
853
+ check: manual
854
+ status: pending
855
+ """
856
+
857
+ DECOMPOSITION_TEMPLATE = """\
858
+ # Strategy Decomposition
859
+
860
+ ## How the Objective Breaks Down
861
+
862
+ [Explain how the program objective decomposes into modules and why.]
863
+
864
+ ## Module Dependency Order
865
+
866
+ 1. [module-a] — [reason]
867
+ 2. [module-b] — depends on module-a
868
+
869
+ ## Inter-Module Contracts
870
+
871
+ [Describe what each module provides to others.]
872
+ """
873
+
874
+ MODULE_SPEC_TEMPLATE = """\
875
+ version: "1.0"
876
+ module: {name}
877
+ description: "What this module does (10 words minimum)."
878
+ objective_coverage:
879
+ - "Which part of objective.md this covers"
880
+ dependencies: []
881
+ constraints:
882
+ - "List constraints here"
883
+ """
884
+
885
+ MODULE_ACCEPTANCE_TEMPLATE = """\
886
+ version: "1.0"
887
+ module: {name}
888
+ criteria:
889
+ # ── Modularity criteria (generated by pcp init — do not remove) ──────────
890
+ - id: MOD_A001
891
+ description: "Module can be removed without breaking other modules (drop test)"
892
+ check: test_passes
893
+ test: "tests/modularity/test_drop_{name}.sh"
894
+ notes: "Temporarily disable module, run full test suite of other modules — must pass"
895
+ status: pending
896
+
897
+ - id: MOD_A002
898
+ description: "Module registers through application interface, not via direct import"
899
+ check: ast_pattern
900
+ target: "src/main"
901
+ pattern: "register\\\\({name}\\\\)|plugin\\\\({name}\\\\)|mount\\\\({name}\\\\)"
902
+ status: pending
903
+
904
+ - id: MOD_A003
905
+ description: "Module has a feature flag (FEATURE_{name_upper}_ENABLED), default false"
906
+ check: file_exists
907
+ target: "src/modules/{name}/feature_flag.env"
908
+ status: pending
909
+
910
+ - id: MOD_A004
911
+ description: "Module interface file exists and defines typed public surface"
912
+ check: file_exists
913
+ target: "src/interfaces/I{name_pascal}.ts"
914
+ status: pending
915
+
916
+ # ── Implementation criteria (add yours below) ─────────────────────────────
917
+ - id: A001
918
+ description: "Core implementation exists"
919
+ check: manual
920
+ status: pending
921
+ """
922
+
923
+ ARCHITECT_PERSONA_TEMPLATE = """\
924
+ # Architect Persona
925
+
926
+ ## Core Philosophy
927
+
928
+ Every module is a guest in the codebase. It can leave without drama.
929
+ It can arrive without surgery. Nothing else should know about it
930
+ except the application registry.
931
+
932
+ Vibe coders pivot. Architecture must absorb pivots without surgery.
933
+ Modularity is not a preference — it is a hard constraint.
934
+
935
+ ## Modularity Invariants (always enforced, not project-specific)
936
+
937
+ BLOCK:
938
+ - Any direct import of another module's `src/` directory from outside that module
939
+ - Any module that imports from another module except through the `interfaces/` contract
940
+ - Any global mutable state shared between modules (singleton patterns, global stores)
941
+ - Any module that directly instantiates another module (use dependency injection via registry)
942
+ - Feature code shipped without a feature flag (`FEATURE_<MODULE>_ENABLED`)
943
+ - A module whose tests require another module to be present (tests must be isolated)
944
+ - Circular module dependencies (A depends on B depends on A)
945
+
946
+ WARN:
947
+ - A module with more than 3 entries in its `dependencies:` field (God module risk)
948
+ - A module that owns more than 3 database tables (God module risk)
949
+ - A module that directly modifies another module's database tables
950
+ - A non-trivial module (auth, payment, queue, scheduler, parser, embeddings,
951
+ state-machine, ETL-class complexity, OR a mature UI subsystem — canvas/
952
+ diagram editor, rich text editor, spreadsheet grid, drag-drop builder)
953
+ scaffolded with no prior-art check logged in `.pcp/decision_log.jsonl` —
954
+ run `/priorart <description>` first and record reuse-as-dependency /
955
+ fork-adapt / reference-pattern-only / build-fresh + license rationale
956
+ before building
957
+
958
+ ## Project-Specific Principles
959
+
960
+ BLOCK:
961
+ - [Add project-specific hard constraints here]
962
+ - Example: "No direct database calls outside repository layer"
963
+ - Example: "All external API calls go through a dedicated client module"
964
+
965
+ WARN:
966
+ - [Add project-specific design smells]
967
+ - Example: "Missing error boundary at integration points"
968
+ - Example: "Synchronous calls to external APIs without timeout"
969
+
970
+ ## What I'm Lenient About
971
+
972
+ - [Things explicitly out of scope for this phase]
973
+ - Example: "Test coverage % in alpha phase"
974
+ - Example: "Internal naming conventions within a module (each module owns its internals)"
975
+
976
+ ## Review Output
977
+ Rate each finding: BLOCK | WARN | NOTE
978
+ BLOCK = must fix before merge
979
+ WARN = fix before ship
980
+ NOTE = track, non-blocking
981
+ """
982
+
983
+ DESIGN_SYSTEM_TEMPLATE = """\
984
+ # Design System
985
+
986
+ _Empty scaffold. Filled in by the `pcp-ui-design` skill the first time a UI-facing
987
+ acceptance criterion is implemented -- not hand-written up front, since the right
988
+ tokens depend on the product's actual domain, not a generic guess made before any
989
+ real screen exists. See `~/.claude/skills/pcp-ui-design/SKILL.md`._
990
+
991
+ _Real gap this exists to prevent: each UI-facing criterion built independently by a
992
+ fresh agent session, with no shared design memory, produces a different vanilla-
993
+ default look per screen. Once this file has real content, every later screen reads
994
+ it and reuses the same system instead of deciding fresh._
995
+
996
+ ## Color
997
+
998
+ (not yet established)
999
+
1000
+ ## Type
1001
+
1002
+ (not yet established)
1003
+
1004
+ ## Layout Conventions
1005
+
1006
+ (not yet established)
1007
+
1008
+ ## Component Notes
1009
+
1010
+ (grows over time -- a new reusable pattern gets recorded here whenever a screen establishes one)
1011
+ """
1012
+
1013
+ PCP_CLAUDE_BLOCK_START = "<!-- PCP:BEGIN — auto-managed by `pcp init`, do not hand-edit this block -->"
1014
+ PCP_CLAUDE_BLOCK_END = "<!-- PCP:END -->"
1015
+
1016
+ PCP_CLAUDE_BLOCK_BODY = """\
1017
+ # PCP Governance (this project is PCP-managed)
1018
+
1019
+ This project uses PCP (Program Context Protocol). Every session — new or
1020
+ resumed — is governed by `.pcp/`. Read the relevant files before acting;
1021
+ do not restate or duplicate their content here.
1022
+
1023
+ ## There is one track, not two
1024
+
1025
+ There is no "just chatting" mode separate from "the PCP track" in this
1026
+ project. Whether the human is describing a bug, debating architecture,
1027
+ correcting scope, or typing `/pcp` explicitly — you are the PCP PM for this
1028
+ conversation, always, by default. A business-level statement made in casual
1029
+ conversation is exactly as real as one made inside a formal PM workflow; it
1030
+ does not become governed only once someone remembers to invoke a skill.
1031
+
1032
+ Concretely, in any conversation on this project:
1033
+ - If something stated changes what should be built (scope, priority, a
1034
+ correction to prior direction) — even said in passing — that is a PM-track
1035
+ event. Reflect it into `.pcp/` in the same turn, don't defer it: run
1036
+ `pcp capture` against the live session (or note the item directly) so it
1037
+ lands in `brd_items.yaml`/`decision_log.jsonl`.
1038
+ - If it contradicts `.pcp/objective.md` or `target_state.md`'s actual text,
1039
+ say so plainly and either run `pcp correct-objective "<correction>"` (human
1040
+ approves the real diff) right then, or explicitly tell the human the spec
1041
+ is now stale and must be corrected before you treat any later "go ahead"
1042
+ as build authorization. Never let a stale objective sit undetected while
1043
+ the conversation moves on to something else — see `pcp objective-conflicts`
1044
+ (CTRL-035; also enforced as a hard block inside `pcp build` itself).
1045
+ - Do not wait for an explicit `/pcp` invocation to apply this — it is the
1046
+ default posture for every conversation in a PCP-managed project, not an
1047
+ opt-in mode.
1048
+
1049
+ (2026-07-22 incident: a business correction was discussed and agreed, nothing
1050
+ in that conversation treated it as PM-track, `objective.md` was never
1051
+ rewritten, and a full build cycle two days later built exactly the rejected
1052
+ thing. This section exists because that gap was real, not hypothetical.)
1053
+
1054
+ ## Read first, every session
1055
+
1056
+ - `.pcp/objective.md` — WHY this program exists. Immutable.
1057
+ - `.pcp/target_state.md` — WHAT done looks like.
1058
+ - `.pcp/architecture.md` — tech decisions + constraints.
1059
+ - `.pcp/architect_persona.md` — modularity + review rules.
1060
+ - `.pcp/current_state.md` — auto-generated reality snapshot.
1061
+ - `.pcp/diff.md` — auto-computed gap vs target.
1062
+ - `.pcp/strategy/decomposition.md` and `.pcp/strategy/modules/*/spec.yaml` — module specs.
1063
+
1064
+ ## Hard rules (non-negotiable, all sessions)
1065
+
1066
+ 1. **Spec files are human-APPROVED only** — `objective.md`, `target_state.md`,
1067
+ `architecture.md`, `strategy/decomposition.md`, `strategy/dependency_map.md`,
1068
+ `ci_rules.yaml`, `controls.yaml`, `SDLC_phase.yaml`, `modules/*/spec.yaml`,
1069
+ `modules/*/acceptance.yaml`.
1070
+ **Approved, not hand-typed.** A tool proposes the change, you review a real
1071
+ diff, you approve, then it is written. An agent must never write one unattended, and
1072
+ `pcp build`'s coding agent is hard-blocked from all of them. But when a human
1073
+ is in the session and asks for a spec update, do it — via the gated command
1074
+ for that file, which shows a real diff and requires approval before writing:
1075
+
1076
+ | File | Command |
1077
+ |---|---|
1078
+ | `objective.md`, `target_state.md` | `pcp correct-objective "<correction>"` |
1079
+ | `modules/*/spec.yaml`, `acceptance.yaml` | `pcp pm "<intent>"` |
1080
+ | everything else above | `pcp amend <file> "<change>"` |
1081
+
1082
+ Refusing to update a spec because it is "human-written" is a bug, not
1083
+ compliance. The only wrong move is writing one without a diff and approval.
1084
+ 2. **`current_state.md` is always auto-generated** by `pcp scan`. Never
1085
+ hand-write it.
1086
+ 3. **`diff.md` is always auto-computed** by `pcp diff`. Never edit it.
1087
+ 4. **Modularity is a hard constraint** — every module is a guest; it can
1088
+ leave without drama and arrive without surgery. No direct cross-module
1089
+ `src/` imports, no shared global mutable state, no module without a
1090
+ feature flag and an `interfaces/` contract. See `ci_rules.yaml` MOD_001-005.
1091
+ 5. Before committing, run `pcp check` (Layer 1). Before a PR, `pcp gate`
1092
+ (Layer 2, advisory). Before deploy, `pcp deploy-check` (Layer 3, hard).
1093
+ Bypass only via `[pcp-bypass: reason]` in the commit message — it is
1094
+ logged to `bypass_log.yaml`, never silent.
1095
+ 6. If work here changes program strategy (drop/add a module, change
1096
+ objective coverage), run `pcp validate-strategy` before proceeding.
1097
+ 7. Before scaffolding a non-trivial module (auth, payment, queue, scheduler,
1098
+ parser, embeddings, state-machine, ETL-class complexity — OR a mature UI
1099
+ subsystem: canvas/diagram editor, rich text editor, spreadsheet grid,
1100
+ drag-drop builder), run `/priorart <description>` first — check for
1101
+ existing projects to reuse, fork, or reference before building from
1102
+ scratch. Log the decision (reuse-as-dependency / fork-adapt /
1103
+ reference-pattern-only / build-fresh + license) via `pcp capture` so it
1104
+ lands in `.pcp/decision_log.jsonl`. Skip for trivial modules (helpers,
1105
+ config parsers, glue code).
1106
+
1107
+ ## Session start checklist
1108
+
1109
+ 1. Read `.pcp/current_state.md` and `.pcp/diff.md` to know what's actually
1110
+ built vs. what's left — don't assume from memory or git log alone.
1111
+ 2. Read `.pcp/pcp.md` if present — it's the human-facing status rollup.
1112
+ 3. If mid-build, check `.pcp/telemetry.jsonl` (or run `pcp telemetry`) for
1113
+ prior attempt history on the current module before retrying.
1114
+ """
1115
+
1116
+
1117
+ def render_pcp_claude_block() -> str:
1118
+ return f"{PCP_CLAUDE_BLOCK_START}\n{PCP_CLAUDE_BLOCK_BODY}{PCP_CLAUDE_BLOCK_END}"
1119
+
1120
+
1121
+ def upsert_pcp_claude_block(claude_md_path: Path) -> bool:
1122
+ """Insert or refresh the PCP governance block in a project's CLAUDE.md.
1123
+
1124
+ Preserves any human-authored content outside the marker pair. Returns
1125
+ True if the file was created or changed.
1126
+ """
1127
+ block = render_pcp_claude_block()
1128
+ if not claude_md_path.exists():
1129
+ claude_md_path.parent.mkdir(parents=True, exist_ok=True)
1130
+ claude_md_path.write_text(block + "\n")
1131
+ return True
1132
+
1133
+ existing = claude_md_path.read_text()
1134
+ if PCP_CLAUDE_BLOCK_START in existing and PCP_CLAUDE_BLOCK_END in existing:
1135
+ pre = existing.split(PCP_CLAUDE_BLOCK_START)[0]
1136
+ post = existing.split(PCP_CLAUDE_BLOCK_END)[1]
1137
+ new_content = pre + block + post
1138
+ else:
1139
+ new_content = (existing.rstrip("\n") + "\n\n" + block + "\n") if existing.strip() else block + "\n"
1140
+
1141
+ if new_content == existing:
1142
+ return False
1143
+ claude_md_path.write_text(new_content)
1144
+ return True
1145
+
1146
+
1147
+ ADR_EXAMPLE = """\
1148
+ # ADR-001: [Short Title]
1149
+
1150
+ ## Status
1151
+ Accepted | Superseded | Deprecated
1152
+
1153
+ ## Context
1154
+ [Why this decision was needed]
1155
+
1156
+ ## Decision
1157
+ [What was decided]
1158
+
1159
+ ## Rationale
1160
+ [Why this option over alternatives]
1161
+
1162
+ ## Consequences
1163
+ [What this enables. What it constrains.]
1164
+ """
1165
+
1166
+ POLICY_ESCALATION_TEMPLATE = """\
1167
+ package pcp.escalation
1168
+
1169
+ # Confidence/stakes-gated routing: PCP's own asymmetric-failure-cost
1170
+ # principle (never silently treat "unsure" as "safe to automate") --
1171
+ # low confidence or an explicitly high-stakes action always escalates to
1172
+ # a human rather than resolving through the agent by default.
1173
+
1174
+ default route := "agent"
1175
+
1176
+ route := "human" if input.confidence_score < 0.65
1177
+ route := "human" if input.high_stakes == true
1178
+ """
1179
+
1180
+ POLICY_BYPASS_TEMPLATE = """\
1181
+ package pcp.bypass
1182
+
1183
+ # Formalizes what ci_rules.yaml's [pcp-bypass: reason] mechanism treats as
1184
+ # ad-hoc otherwise -- rejects placeholder reasons before pcp check treats
1185
+ # a reason as adequate to log and accept a Layer 1 bypass.
1186
+
1187
+ default approved := false
1188
+
1189
+ approved if {
1190
+ count(trim_space(input.reason)) > 0
1191
+ not lower(trim_space(input.reason)) in {"reason", "todo", "test", "fixme"}
1192
+ }
1193
+ """
1194
+
1195
+ POLICY_DEPLOY_TEMPLATE = """\
1196
+ package pcp.deploy
1197
+
1198
+ # Deploy-time policy (Harness policy-gate reference pattern, 2026-07-17).
1199
+ # Consulted by `pcp deploy` before the approval prompt. All rules
1200
+ # human-editable; a missing/failed OPA evaluation NEVER blocks a deploy
1201
+ # (advisory layer, same posture as escalation.rego).
1202
+
1203
+ # Freeze windows: block deploys on listed UTC weekdays (0=Mon..6=Sun).
1204
+ # Default: no frozen days. Example: freeze_days := {4, 5, 6} freezes Fri-Sun.
1205
+ freeze_days := set()
1206
+
1207
+ deny contains msg if {
1208
+ input.utc_weekday in freeze_days
1209
+ msg := sprintf("deploy freeze window: weekday %d is frozen", [input.utc_weekday])
1210
+ }
1211
+
1212
+ # Require rollback configured for any deploy touching risk-flagged criteria.
1213
+ deny contains msg if {
1214
+ input.risk_flag_count > 0
1215
+ not input.rollback_configured
1216
+ msg := "risk-flagged release with no rollback command configured — configure one via pcp doctor"
1217
+ }
1218
+ """
1219
+
1220
+ UI_KIT_RECIPES_TEMPLATE = """\
1221
+ version: "1.0"
1222
+
1223
+ # PCP's UI building-blocks layer, added 2026-07-20. PCP does NOT build or
1224
+ # maintain UI component code -- that's a solved problem (shadcn/ui on Base
1225
+ # UI primitives: MIT, copy-paste-vendored per project via `npx shadcn add
1226
+ # <name>`, official MCP server for search/retrieval, already the dominant
1227
+ # stack among AI UI-generation tools). Verified current as of 2026-07-20 --
1228
+ # re-check ui.shadcn.com/docs/components before trusting these names if
1229
+ # this file is old; shadcn's own catalog evolves (toast -> sonner already
1230
+ # happened once).
1231
+ #
1232
+ # What PCP owns instead: which organisms an archetype needs (the recipe),
1233
+ # where to look for evidence they were actually used (import_path_hint,
1234
+ # consumed by build.py's CTRL-028), and how organisms combine into a screen
1235
+ # (the rules below). See CLAUDE.md's "UI Building Blocks" section for the
1236
+ # full rationale -- this is the build-vs-buy decision recorded there.
1237
+ kit: shadcn
1238
+
1239
+ # organism -> where its evidence is expected to live. import_path_hint is a
1240
+ # SUBSTRING match against a criterion's target file, not an exact resolver
1241
+ # (no tsconfig-paths parsing) -- a real re-export/alias can legitimately
1242
+ # not match, so CTRL-028 treats a miss as advisory, not proof of non-use.
1243
+ # component: null means shadcn has no single installable primitive for
1244
+ # this organism -- it's a documented HAND-ASSEMBLED pattern (shadcn's own
1245
+ # docs say so explicitly for data-table and form), composed from the
1246
+ # primitives listed in the comment. import_path_hint still anchors on the
1247
+ # closest real primitive so the check has *something* concrete to look for.
1248
+ organisms:
1249
+ primary-nav:
1250
+ component: "sidebar"
1251
+ import_path_hint: "components/ui/sidebar"
1252
+ position: "top bar or left rail"
1253
+ data-table:
1254
+ component: null # composed: `table` primitive + TanStack Table, per shadcn's own docs -- not a single install
1255
+ import_path_hint: "components/ui/table"
1256
+ position: "main content, below filters"
1257
+ filter-bar:
1258
+ component: null # composed: input + select, no single shadcn primitive
1259
+ import_path_hint: "components/ui/input"
1260
+ position: "top of content, above the table"
1261
+ modal:
1262
+ component: "dialog"
1263
+ import_path_hint: "components/ui/dialog"
1264
+ position: "centered overlay"
1265
+ toast:
1266
+ component: "sonner" # NOT "toast" -- shadcn deprecated its own toast component in favor of sonner
1267
+ import_path_hint: "components/ui/sonner"
1268
+ position: "top-right or bottom-center, transient"
1269
+ status-badge:
1270
+ component: "badge"
1271
+ import_path_hint: "components/ui/badge"
1272
+ position: "inline, next to the item it describes"
1273
+ kpi-tile:
1274
+ component: "card"
1275
+ import_path_hint: "components/ui/card"
1276
+ position: "top row of a dashboard"
1277
+ chart-panel:
1278
+ component: "chart"
1279
+ import_path_hint: "components/ui/chart"
1280
+ position: "main content area"
1281
+ form-field-group:
1282
+ component: null # composed: field/label primitives + react-hook-form, per shadcn's own docs -- not a single install
1283
+ import_path_hint: "components/ui/field"
1284
+ position: "main content, vertical stack"
1285
+ search-box:
1286
+ component: "input"
1287
+ import_path_hint: "components/ui/input"
1288
+ position: "top-left or top-center of content"
1289
+ empty-state:
1290
+ component: "empty"
1291
+ import_path_hint: "components/ui/empty"
1292
+ position: "fills the content area it replaces"
1293
+ comment-thread:
1294
+ component: null # composed: card + avatar + textarea, genuinely custom -- no anchor primitive close enough to check
1295
+ import_path_hint: null
1296
+ position: "right rail or bottom panel"
1297
+
1298
+ # screen_archetype -> required ui_organisms (the recipe CTRL-028 checks
1299
+ # completeness against). canvas_editor deliberately has none -- it triggers
1300
+ # /priorart per the existing global Prior-Art Check rule instead of a
1301
+ # prescribed recipe (a diagram/canvas editor is its own mature-tool
1302
+ # decision, not a shadcn-organism composition).
1303
+ archetypes:
1304
+ dashboard: ["kpi-tile", "chart-panel", "data-table"]
1305
+ data_entry_form: ["form-field-group"]
1306
+ list_table: ["data-table", "filter-bar", "empty-state"]
1307
+ detail_view: ["kpi-tile"]
1308
+ search_filter: ["search-box", "filter-bar", "empty-state"]
1309
+ settings: ["form-field-group"]
1310
+ chat: ["comment-thread"]
1311
+ canvas_editor: []
1312
+ wizard: ["form-field-group"]
1313
+ auth: ["form-field-group"]
1314
+
1315
+ # Composition rules -- documented here for the build agent to read; not yet
1316
+ # separately enforced by their own CTRL (recipe-completeness + import-
1317
+ # verification above are what CTRL-028 actually checks). Promote a rule to
1318
+ # its own deterministic check if/when one proves worth enforcing on its own.
1319
+ rules:
1320
+ max_primary_actions_per_screen: 1
1321
+ require_empty_state_for: ["data-table", "chat"]
1322
+ require_confirmation_for_destructive_actions: true
1323
+ """
1324
+
1325
+ DESIGN_CONVENTIONS_TEMPLATE = """\
1326
+ version: "1.0"
1327
+
1328
+ # UI archetype -- what KIND of product this is, for conventions that only
1329
+ # apply to some archetypes (a File/Edit/View menu bar is a desktop-app
1330
+ # convention; a SaaS dashboard or mobile-style portal has no reason to have
1331
+ # one). Default web_app: the menu-bar check below stays inert until a human
1332
+ # deliberately opts a project into a different archetype.
1333
+ #
1334
+ # One of: web_app | desktop_app | mobile | cli
1335
+ ui_archetype: web_app
1336
+
1337
+ # Only enforced (deterministic substring scan, no LLM) when ui_archetype is
1338
+ # desktop_app -- CTRL-027 in build.py's wave-merge. Advisory, warn-first,
1339
+ # same rollout posture as every other check in this catalog.
1340
+ top_menu_bar:
1341
+ required_menus: ["File", "Edit", "View", "Help"]
1342
+ """
1343
+
1344
+ POLICY_TIER_DISTRIBUTION_TEMPLATE = """\
1345
+ package pcp.tier_distribution
1346
+
1347
+ # Logic-tier distribution bands (2026-07-18). How much of the product is
1348
+ # allowed to live at rung 6 (LLM, nondeterministic) before validate-strategy
1349
+ # colors the mix yellow/red? Advisory only -- this encodes a team's
1350
+ # predictability budget, human-editable here instead of buried in Python.
1351
+ # Defaults mirror validate_strategy.py's fallback bands.
1352
+
1353
+ color := "green" if input.rung6_share <= 0.35
1354
+ else := "yellow" if input.rung6_share <= 0.6
1355
+ else := "red"
1356
+ """
1357
+
1358
+ POLICY_COUPLING_TEMPLATE = """\
1359
+ package pcp.coupling
1360
+
1361
+ # Mirrors validate_strategy.py's display thresholds (green >= 0.8,
1362
+ # yellow >= 0.6, else red) -- human-editable here instead of buried in
1363
+ # Python.
1364
+
1365
+ coupling_color := "green" if input.coupling_score >= 0.8
1366
+ else := "yellow" if input.coupling_score >= 0.6
1367
+ else := "red"
1368
+ """
1369
+
1370
+ DOMAIN_KB_TEMPLATE = """\
1371
+ # Domain Knowledge: [Technology/Area]
1372
+
1373
+ ## Known Failure Modes
1374
+
1375
+ - [Pattern that causes bugs in this domain]
1376
+
1377
+ ## Invariants
1378
+
1379
+ - [Things that must always be true]
1380
+
1381
+ ## Gotchas
1382
+
1383
+ - [Non-obvious behaviours or edge cases]
1384
+ """
1385
+
1386
+
1387
+ def _write(path: Path, content: str, force: bool) -> bool:
1388
+ if path.exists() and not force:
1389
+ return False
1390
+ path.parent.mkdir(parents=True, exist_ok=True)
1391
+ path.write_text(content)
1392
+ return True
1393
+
1394
+
1395
+ RECOMMENDED_PERMISSIONS_TEMPLATE = """\
1396
+ # Recommended Claude Code Permissions for PCP
1397
+
1398
+ Found the hard way, once: an agent correcting a spec/target-path drift issue in
1399
+ `.pcp/` one file at a time (via the Edit tool) is exactly the kind of routine,
1400
+ low-risk PCP maintenance that still triggers a permission prompt on every
1401
+ single file under Claude Code's default settings. Pre-approving it narrows the
1402
+ friction to just this one path, not a blanket edit allowance.
1403
+
1404
+ **This is advisory, not applied automatically** -- PCP does not (and will not)
1405
+ edit `.claude/settings.json` itself. Review the snippet below and merge it
1406
+ into your own `.claude/settings.json` or `.claude/settings.local.json` if you
1407
+ want it.
1408
+
1409
+ ## Recommended: allow edits under `.pcp/`
1410
+
1411
+ ```json
1412
+ {
1413
+ "permissions": {
1414
+ "allow": ["Edit(/.pcp/**)"]
1415
+ }
1416
+ }
1417
+ ```
1418
+
1419
+ Narrower alternative, if you'd rather scope it to just spec/acceptance files
1420
+ instead of all of `.pcp/`:
1421
+
1422
+ ```json
1423
+ {
1424
+ "permissions": {
1425
+ "allow": ["Edit(/.pcp/strategy/modules/**/spec.yaml)", "Edit(/.pcp/strategy/modules/**/acceptance.yaml)"]
1426
+ }
1427
+ }
1428
+ ```
1429
+
1430
+ ## Optional: `acceptEdits` permission mode
1431
+
1432
+ For a session dedicated to PCP spec maintenance specifically (not general
1433
+ coding), consider switching that session to `acceptEdits` mode instead of
1434
+ adding the rule above project-wide. Session-scoped, reverts when the session
1435
+ ends -- a good fit if this kind of correction is occasional rather than
1436
+ routine for your workflow.
1437
+
1438
+ ## If you use a `git branch -D` deny rule
1439
+
1440
+ PCP's own worktree/branch-reset code (`pcp build`'s parallel module builds,
1441
+ and any PCP-provided demo/setup scripts) uses the non-destructive
1442
+ `git checkout -B <branch> <start-point>` form, never `git branch -D` --
1443
+ a standing deny rule on `-D` will not conflict with anything PCP itself does.
1444
+
1445
+ ## Optional: keep CLAUDE.md's PCP context block fresh via a SessionStart hook
1446
+
1447
+ `pcp context --inject` writes a marked block into CLAUDE.md (objective,
1448
+ architecture, current state, pending gaps) -- Claude Code reads CLAUDE.md
1449
+ every session regardless, but nothing calls `pcp context --inject` for you.
1450
+ If you want that block to stay current automatically instead of running it
1451
+ by hand, add a `SessionStart` hook:
1452
+
1453
+ ```json
1454
+ {
1455
+ "hooks": {
1456
+ "SessionStart": [
1457
+ {"hooks": [{"type": "command", "command": "pcp context --inject --path \\"$CLAUDE_PROJECT_DIR\\""}]}
1458
+ ]
1459
+ }
1460
+ }
1461
+ ```
1462
+
1463
+ Same posture as everything else in this file: advisory, not applied by PCP itself.
1464
+
1465
+ ## Optional: real-time build-loop warning (2026-07-24)
1466
+
1467
+ `.pcp/hooks/build_loop_warning.py` fires the moment an Edit/Write happens
1468
+ outside `pcp build`'s own gated agent loop -- a real-time companion to
1469
+ CTRL-037 (`pcp doctor`), which only catches this in retrospect. Purely
1470
+ informational: never sets a permission decision, so it can't block or force
1471
+ an "ask" prompt, one warning per session. To enable:
1472
+
1473
+ ```json
1474
+ {
1475
+ "hooks": {
1476
+ "PreToolUse": [
1477
+ {"matcher": "Edit|Write", "hooks": [{"type": "command", "command": "python3 .pcp/hooks/build_loop_warning.py"}]}
1478
+ ]
1479
+ }
1480
+ }
1481
+ ```
1482
+
1483
+ ## Optional: PCP update-available notice at session start (2026-07-31)
1484
+
1485
+ `.pcp/hooks/session_update_check.py` runs `pcp self-update --check` (fetch +
1486
+ compare, read-only) at the start of a session and prints a one-line notice
1487
+ if a newer PCP is available. It never pulls anything itself -- updating
1488
+ always stays a separate, explicit `pcp self-update`. To enable:
1489
+
1490
+ ```json
1491
+ {
1492
+ "hooks": {
1493
+ "SessionStart": [
1494
+ {"hooks": [{"type": "command", "command": "python3 .pcp/hooks/session_update_check.py"}]}
1495
+ ]
1496
+ }
1497
+ }
1498
+ ```
1499
+ """
1500
+
1501
+
1502
+ def _write_permission_recommendations(root: Path, force: bool) -> Path | None:
1503
+ """Advisory only -- writes a recommendation file, never touches
1504
+ .claude/settings.json itself (that edit is permanently off-limits for an
1505
+ agent to make on a human's behalf, confirmed the hard way: Claude Code's
1506
+ own permission layer hard-blocks an agent wiring anything into its own
1507
+ settings.json, even a narrow, clearly-scoped addition). Surfacing this at
1508
+ `pcp init` time means a new PCP user sees it on day one, instead of
1509
+ discovering it painfully after hitting the exact friction it describes --
1510
+ which is how this file's own content was originally found."""
1511
+ path = root / ".pcp" / "RECOMMENDED_PERMISSIONS.md"
1512
+ if _write(path, RECOMMENDED_PERMISSIONS_TEMPLATE, force):
1513
+ return path
1514
+ return None
1515
+
1516
+
1517
+ @click.command()
1518
+ @click.option("--path", "project_path", type=click.Path(), default=".",
1519
+ help="Project root (default: current directory).")
1520
+ @click.option("--module", "module_name", default=None,
1521
+ help="Also scaffold a module under strategy/modules/<name>/.")
1522
+ @click.option("--force", is_flag=True, help="Overwrite existing files.")
1523
+ def init(project_path: str, module_name: str | None, force: bool):
1524
+ """Scaffold .pcp/ directory in a project."""
1525
+ root = Path(project_path).resolve()
1526
+ pcp = root / ".pcp"
1527
+
1528
+ files = {
1529
+ pcp / "objective.md": OBJECTIVE_TEMPLATE,
1530
+ pcp / "target_state.md": TARGET_STATE_TEMPLATE,
1531
+ pcp / "architecture.md": ARCHITECTURE_TEMPLATE,
1532
+ pcp / "ci_rules.yaml": CI_RULES_TEMPLATE,
1533
+ pcp / "controls.yaml": CONTROLS_TEMPLATE,
1534
+ pcp / "SDLC_phase.yaml": SDLC_PHASE_TEMPLATE,
1535
+ pcp / "strategy" / "decomposition.md": DECOMPOSITION_TEMPLATE,
1536
+ pcp / "architect_persona.md": ARCHITECT_PERSONA_TEMPLATE,
1537
+ pcp / "design_system.md": DESIGN_SYSTEM_TEMPLATE,
1538
+ pcp / "kb" / "adr" / "ADR-001-example.md": ADR_EXAMPLE,
1539
+ pcp / "kb" / "domain" / "general.md": DOMAIN_KB_TEMPLATE,
1540
+ pcp / "policies" / "escalation.rego": POLICY_ESCALATION_TEMPLATE,
1541
+ pcp / "policies" / "bypass_approval.rego": POLICY_BYPASS_TEMPLATE,
1542
+ pcp / "policies" / "coupling_threshold.rego": POLICY_COUPLING_TEMPLATE,
1543
+ pcp / "policies" / "deploy_policy.rego": POLICY_DEPLOY_TEMPLATE,
1544
+ pcp / "hooks" / "pretooluse_guard.sh": PRETOOLUSE_GUARD_TEMPLATE,
1545
+ pcp / "hooks" / "build_loop_warning.py": BUILD_LOOP_WARNING_TEMPLATE,
1546
+ pcp / "hooks" / "session_update_check.py": SESSION_UPDATE_CHECK_TEMPLATE,
1547
+ pcp / "policies" / "tier_distribution.rego": POLICY_TIER_DISTRIBUTION_TEMPLATE,
1548
+ pcp / "logic_tier_guide.md": LOGIC_TIER_GUIDE_TEMPLATE,
1549
+ pcp / "context_map.yaml": CONTEXT_MAP_TEMPLATE,
1550
+ pcp / "design_conventions.yaml": DESIGN_CONVENTIONS_TEMPLATE,
1551
+ pcp / "ui_kit_recipes.yaml": UI_KIT_RECIPES_TEMPLATE,
1552
+ }
1553
+
1554
+ if module_name:
1555
+ mod_dir = pcp / "strategy" / "modules" / module_name
1556
+ files[mod_dir / "spec.yaml"] = MODULE_SPEC_TEMPLATE.format(name=module_name)
1557
+ files[mod_dir / "acceptance.yaml"] = MODULE_ACCEPTANCE_TEMPLATE.format(
1558
+ name=module_name, name_upper=module_name.upper(), name_pascal=_pascal_case(module_name),
1559
+ )
1560
+
1561
+ created = []
1562
+ skipped = []
1563
+ for path, content in files.items():
1564
+ if _write(path, content, force):
1565
+ created.append(path.relative_to(root))
1566
+ else:
1567
+ skipped.append(path.relative_to(root))
1568
+
1569
+ for p in created:
1570
+ console.print(f" [green]created[/green] {p}")
1571
+ for p in skipped:
1572
+ console.print(f" [dim]skipped[/dim] {p} (exists, use --force to overwrite)")
1573
+
1574
+ claude_md = root / "CLAUDE.md"
1575
+ if upsert_pcp_claude_block(claude_md):
1576
+ console.print(f" [green]updated[/green] CLAUDE.md (PCP governance block)")
1577
+ else:
1578
+ console.print(f" [dim]unchanged[/dim] CLAUDE.md (PCP governance block already current)")
1579
+
1580
+ gitattributes = root / ".gitattributes"
1581
+ ga_lines = [
1582
+ ".pcp/current_state.md merge=ours",
1583
+ ".pcp/diff.md merge=ours",
1584
+ ".pcp/bypass_log.yaml merge=union",
1585
+ ]
1586
+ existing = gitattributes.read_text() if gitattributes.exists() else ""
1587
+ additions = [l for l in ga_lines if l not in existing]
1588
+ if additions:
1589
+ with open(gitattributes, "a") as f:
1590
+ f.write("\n" + "\n".join(additions) + "\n")
1591
+ console.print(f" [green]updated[/green] .gitattributes")
1592
+
1593
+ # Layer 1 enforcement wired up automatically, not left as a manual step a
1594
+ # human could forget -- "this project has .pcp/" and "this project is
1595
+ # actually governed" used to be two separate facts. Cron side effects
1596
+ # (global intervention aggregation, skill upgrade check) deliberately
1597
+ # NOT included here -- see install_git_hook()'s own docstring.
1598
+ from pcp.commands.install_hook import install_git_hook
1599
+ hook_installed, hook_msg = install_git_hook(root)
1600
+ if hook_installed:
1601
+ console.print(f" [green]installed[/green] commit-msg hook ({hook_msg})")
1602
+ else:
1603
+ console.print(f" [dim]hook not installed[/dim] ({hook_msg})")
1604
+
1605
+ perms_path = _write_permission_recommendations(root, force)
1606
+ if perms_path:
1607
+ console.print(f" [green]written[/green] {perms_path.relative_to(root)} (advisory -- not applied automatically)")
1608
+
1609
+ # Build-artifact .gitignore, only when the project has none at all.
1610
+ # Found dogfooding 2026-07-17 (round 4): a kickoff-scaffolded project with
1611
+ # no .gitignore let build agents commit __pycache__/*.pyc alongside their
1612
+ # work, and the parallel-module worktree merge then aborted on "local
1613
+ # changes would be overwritten" — by bytecode files. Never appends to or
1614
+ # modifies an existing .gitignore (that's the human's file).
1615
+ gitignore_path = root / ".gitignore"
1616
+ if not gitignore_path.exists():
1617
+ gitignore_path.write_text(GITIGNORE_TEMPLATE)
1618
+ console.print(" [green]written[/green] .gitignore (build-artifact patterns)")
1619
+
1620
+ console.print(f"\n[bold]PCP initialised at {pcp}[/bold]")
1621
+ if perms_path:
1622
+ console.print(
1623
+ f"[dim]Recommended Claude Code permission rules written to {perms_path.relative_to(root)} -- "
1624
+ "review and merge into your own .claude/settings.json if you want fewer per-edit prompts "
1625
+ "when PCP corrects spec/target-path drift.[/dim]"
1626
+ )
1627
+ console.print("\nNext steps:")
1628
+ console.print(" 1. Edit [cyan].pcp/objective.md[/cyan] — describe WHY this program exists")
1629
+ console.print(" 2. Edit [cyan].pcp/strategy/decomposition.md[/cyan] — break objective into modules")
1630
+ console.print(" 3. Run [cyan]pcp init --module <name>[/cyan] for each module")
1631
+ console.print(" 4. Run [cyan]pcp validate-strategy[/cyan] to check coverage")
1632
+ console.print(" 5. Edit [cyan].pcp/architect_persona.md[/cyan] — define architecture principles")
1633
+ console.print(" 6. Add ADRs to [cyan].pcp/kb/adr/[/cyan] — document architecture decisions")
1634
+ console.print(" 7. Run [cyan]pcp architect-review[/cyan] — review changes against persona")