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