program-context-protocol 0.12.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcp/__init__.py +3 -0
- pcp/assertions.py +152 -0
- pcp/attest.py +111 -0
- pcp/build_loop_bypass.py +76 -0
- pcp/build_report.py +54 -0
- pcp/capture.py +339 -0
- pcp/cli.py +104 -0
- pcp/commands/__init__.py +0 -0
- pcp/commands/amend.py +283 -0
- pcp/commands/architect_review.py +291 -0
- pcp/commands/architecture_justification.py +164 -0
- pcp/commands/audit.py +371 -0
- pcp/commands/build.py +4523 -0
- pcp/commands/build_plan.py +153 -0
- pcp/commands/build_status.py +83 -0
- pcp/commands/capture.py +72 -0
- pcp/commands/check.py +584 -0
- pcp/commands/context.py +151 -0
- pcp/commands/control_audit_cmd.py +54 -0
- pcp/commands/correct_objective.py +160 -0
- pcp/commands/dashboard.py +732 -0
- pcp/commands/deploy.py +199 -0
- pcp/commands/deploy_check.py +134 -0
- pcp/commands/design_audit.py +323 -0
- pcp/commands/diff.py +153 -0
- pcp/commands/diff_reduce.py +355 -0
- pcp/commands/docs.py +538 -0
- pcp/commands/doctor.py +820 -0
- pcp/commands/escalations_cmd.py +64 -0
- pcp/commands/gate.py +209 -0
- pcp/commands/import_project.py +404 -0
- pcp/commands/init.py +1634 -0
- pcp/commands/install_hook.py +283 -0
- pcp/commands/install_skill.py +48 -0
- pcp/commands/kickoff.py +772 -0
- pcp/commands/narrative_lint.py +54 -0
- pcp/commands/objective_conflicts_cmd.py +68 -0
- pcp/commands/pm.py +504 -0
- pcp/commands/pressure_test_cmd.py +72 -0
- pcp/commands/provenance.py +313 -0
- pcp/commands/prune.py +179 -0
- pcp/commands/report.py +49 -0
- pcp/commands/run_log_cmd.py +122 -0
- pcp/commands/scan.py +346 -0
- pcp/commands/self_update.py +125 -0
- pcp/commands/status.py +180 -0
- pcp/commands/takeover.py +55 -0
- pcp/commands/telemetry_cmd.py +167 -0
- pcp/commands/validate_module.py +153 -0
- pcp/commands/validate_strategy.py +413 -0
- pcp/commands/verify.py +166 -0
- pcp/commands/verify_syntax_fix.py +74 -0
- pcp/commands/watch.py +372 -0
- pcp/config_audit.py +141 -0
- pcp/context_map.py +124 -0
- pcp/control_audit.py +159 -0
- pcp/coupling.py +178 -0
- pcp/coverage_audit.py +77 -0
- pcp/decision_log.py +134 -0
- pcp/discovery/__init__.py +0 -0
- pcp/discovery/clusters.py +124 -0
- pcp/discovery/graph.py +110 -0
- pcp/discovery/scanner.py +109 -0
- pcp/escalations.py +193 -0
- pcp/evidence.py +30 -0
- pcp/evidence_chain.py +56 -0
- pcp/impact.py +164 -0
- pcp/install_approvals.py +44 -0
- pcp/integrity_audit.py +176 -0
- pcp/librarian.py +89 -0
- pcp/llm/__init__.py +0 -0
- pcp/llm/client.py +183 -0
- pcp/llm/coding_agent_contract.py +104 -0
- pcp/llm/harness/__init__.py +12 -0
- pcp/llm/harness/agy.py +121 -0
- pcp/llm/harness/agy_coding_loop.py +180 -0
- pcp/llm/harness/claude.py +241 -0
- pcp/llm/ledger.py +47 -0
- pcp/narrative_lint.py +229 -0
- pcp/nav_graph.py +226 -0
- pcp/objective_conflicts.py +129 -0
- pcp/operational.py +70 -0
- pcp/orphaned_work.py +262 -0
- pcp/pcp_dir.py +35 -0
- pcp/pcp_status.py +313 -0
- pcp/policy.py +81 -0
- pcp/pressure_test.py +196 -0
- pcp/qa.py +445 -0
- pcp/run_log.py +225 -0
- pcp/schema/__init__.py +0 -0
- pcp/schema/ci_rules.schema.json +106 -0
- pcp/schema/controls.schema.json +39 -0
- pcp/schema/module_acceptance.schema.json +144 -0
- pcp/schema/module_spec.schema.json +78 -0
- pcp/schema/sdlc_phase.schema.json +52 -0
- pcp/schema/validator.py +77 -0
- pcp/skill_data/pcp/SKILL.md +1897 -0
- pcp/spec_write.py +269 -0
- pcp/spend.py +77 -0
- pcp/symbols.py +86 -0
- pcp/telemetry.py +308 -0
- pcp/uat.py +271 -0
- pcp/version_drift.py +222 -0
- program_context_protocol-0.12.4.dist-info/METADATA +123 -0
- program_context_protocol-0.12.4.dist-info/RECORD +109 -0
- program_context_protocol-0.12.4.dist-info/WHEEL +4 -0
- program_context_protocol-0.12.4.dist-info/entry_points.txt +2 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-APACHE +202 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-MIT +21 -0
pcp/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")
|