Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0047317
feat: enforce split-contract allowed write sets in pdd sync (#1013)
pdd-bot May 14, 2026
8ca3b18
fix: enforce sync allowed write sets
Serhan-Asad May 15, 2026
7345d01
fix(sync): F1+F2 add IssueContract parser and DEFAULT_SYNC_COMPANION_…
Serhan-Asad May 15, 2026
d23b1ef
fix(sync): F3+F4+F11+F15 wire parse_issue_contract through run_agenti…
Serhan-Asad May 15, 2026
3423c71
fix(sync): F5-F9+F12+F14 rebuild scope guard around shared revert hel…
Serhan-Asad May 15, 2026
4f80645
fix(sync): F13 reuse scope guard in durable runner with companion all…
Serhan-Asad May 15, 2026
604c258
fix(sync): F10 add --no-scope-guard CLI flag to pdd sync
Serhan-Asad May 15, 2026
7b41c91
test(sync): F16 migrate _extract_allowed_write_paths tests to structu…
Serhan-Asad May 15, 2026
241021b
fix(sync): strip only leading './' from contract paths, not arbitrary…
Serhan-Asad May 15, 2026
fc57b44
fix(sync): F1+F2 keep empty contracts as reject-all and tighten fence…
Serhan-Asad May 15, 2026
d297f87
fix(sync): F3 use pathlib-style match for companion allowlist globs
Serhan-Asad May 15, 2026
7de4c4d
fix(sync): F4+F6+F7 union companion allowlist, record contract under …
Serhan-Asad May 15, 2026
eb775a9
fix(sync): F5 use --untracked-files=all and handle untracked dirs in …
Serhan-Asad May 15, 2026
5074a95
fix(sync): F8 emit scope-guard diagnostic to stderr at revert time
Serhan-Asad May 15, 2026
f8c62a2
test(sync): F9 add parse_issue_contract regression coverage
Serhan-Asad May 15, 2026
fea5200
test(sync): F9 add scope-guard runner and CLI flag regression coverage
Serhan-Asad May 15, 2026
9c98bc1
fix(sync): F1+F2+F3 (iter-3) scope companion globs to module_cwd, res…
Serhan-Asad May 15, 2026
336987b
test(sync): iter-3 F1+F2 add sibling-module-companion and run-entry l…
Serhan-Asad May 15, 2026
e8795e8
fix(sync): iter-4 F1 preserve deleted companion artifacts in scope guard
Serhan-Asad May 15, 2026
96746b0
fix(sync): iter-6 B1 preserve pre-existing untracked files in scope g…
Serhan-Asad May 15, 2026
2ca65e9
fix(sync): iter-6 B2 correctly revert staged renames in scope guard
Serhan-Asad May 15, 2026
e2bbed3
fix(sync): iter-6 B3 detect rename source side in durable scope check
Serhan-Asad May 15, 2026
478cf79
fix(sync): iter-7 B4 revert renames atomically (both sides) when eith…
Serhan-Asad May 15, 2026
22de0c0
fix(sync): iter-8 B5+B6 empty contracts revert fully + align prompt w…
Serhan-Asad May 15, 2026
52fed06
fix(sync): iter-9 M-1 scope guard fail-closed boundary via post-rever…
Serhan-Asad May 15, 2026
bd0caa2
fix(sync): iter-10 M-1 validate companion_allowlist anchor to prevent…
Serhan-Asad May 15, 2026
5144b25
fix(sync): iter-12 B-1 json fenced contracts + M-1 prompt drift; defe…
Serhan-Asad May 15, 2026
2b90d88
fix(sync): iter-14 M-1+M-2 anchored companion matcher; defer M-3
Serhan-Asad May 15, 2026
7479613
fix(sync): iter-16 M-1 thread module context into durable scope check
Serhan-Asad May 15, 2026
35b1b80
Merge origin/main into change/issue-1013
Serhan-Asad May 15, 2026
37e3273
fix(sync): iter-18 bullet-list parser + durable baseline + dedupe per…
Serhan-Asad May 15, 2026
7e46cd3
fix(sync): iter-20 detect gitignored out-of-scope writes
Serhan-Asad May 15, 2026
ff52568
fix(sync): iter-22 M-1 clear durable baseline so main-checkout dirty …
Serhan-Asad May 15, 2026
eb02195
fix(sync): iter-24 hash-aware baseline preservation closes clobber gap
Serhan-Asad May 15, 2026
ed5e3e4
fix(sync): iter-26 B-1 close orchestrator-level scope leak in arch co…
Serhan-Asad May 15, 2026
56fc6b1
fix(sync): iter-28 close two orchestrator-level scope bypasses
Serhan-Asad May 15, 2026
1f9fdc0
fix(sync): iter-30 unified orchestrator scope guard (Option A)
Serhan-Asad May 15, 2026
967a853
fix(sync): iter-32 wrap dispatch boundary with orchestrator scope guard
Serhan-Asad May 15, 2026
9a14d74
Merge origin/main into change/issue-1013 (iter-33 follow-up)
Serhan-Asad May 15, 2026
9206ef8
fix(sync): iter-34 M-3 detect deletion of pre-existing untracked base…
Serhan-Asad May 15, 2026
a127f4c
fix(sync): iter-36 close B-1/B-2/B-3 orchestrator parity gaps
Serhan-Asad May 15, 2026
2db79b2
fix(sync): iter-38 fail-closed baseline acquisition at init
Serhan-Asad May 15, 2026
35d4182
fix(sync): iter-40 distinguish unreadable vs missing baseline + durab…
Serhan-Asad May 15, 2026
4a5814c
fix(sync): iter-42 mirror PDD_INTERNAL_PATH_ALLOWLIST into durable ch…
Serhan-Asad May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Fix

- **#1013 sync**: enforce split-contract allowed write sets. When the linked GitHub issue declares an allowed write set (HTML comment `<!-- PDD_ISSUE_CONTRACT ...json... -->`, a fenced "Allowed Write Set" / "Split Contract" block, or a `## Split Contract` heading with an `**Allowed write set:**` label followed by a bullet list), `pdd sync` now reverts tracked changes and removes untracked new files that fall outside the contract after each per-module subprocess, hard-fails the module on out-of-scope artifacts, and surfaces the contract source plus offending paths in checkup/review-loop reports. Companion artifacts under `.pdd/meta/*.json` are auto-allowed; additional companions can be opted in via the contract's `companion_allowlist` field. Use `--no-scope-guard` to opt out for a single run. Issues without a contract marker remain in permissive mode (no enforcement).

## v0.0.238 (2026-05-14)

### Feat
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ Options:
- `--durable-branch TEXT`: Durable mode only. Override the durable checkpoint branch name. Default is `sync/issue-<N>` derived from the GitHub issue. Refused if it resolves to `main`, `master`, or the repository default branch.
- `--no-resume`: Durable mode only. Ignore existing `PDD-Sync-Checkpoint-V1` commit trailers on the durable branch and re-run every selected module. By default, durable sync reads checkpoint trailers (`PDD-Sync-Checkpoint-V1: issue=<N> module=<basename>`) and skips modules already checkpointed for the same issue, which is what makes a cloud rerun safely resume completed work after a partial failure.
- `--durable-max-parallel INT`: Durable mode only. Cap how many module worktrees run concurrently. Defaults to the standard runner concurrency. A total budget still forces sequential execution.
- `--no-scope-guard`: Issue-sync only. Disable the split-contract scope guard for this run. By default, when the linked GitHub issue declares an allowed write set (split contract), `pdd sync` enforces it and rejects out-of-scope generated artifacts. Pass this flag only when intentionally overriding contract enforcement (e.g. recovering from a stale contract). See "Split-Contract Scope Guard" below.

**Durable Issue Sync** (`--durable`):

Expand Down Expand Up @@ -1060,6 +1061,61 @@ Options (agentic mode):

**Cross-Machine Resume**: Workflow state is stored in a hidden GitHub comment, enabling resume from any machine. Use `--no-github-state` to disable.

**Split-Contract Scope Guard** (Issue #1013):

When the linked GitHub issue declares an allowed write set (a "split contract"), `pdd sync` enforces it: each per-module subprocess is followed by a scope check that reverts tracked changes and removes untracked new files that fall outside the contract. Companion artifacts under `.pdd/meta/*.json` are auto-allowed because they are sync's own fingerprint bookkeeping; issues may opt additional companions (e.g. examples or architecture entries) into the allowlist explicitly.

The contract is read from the issue body or any of its comments in one of three forms (tried in priority order — the first match wins):

1. An HTML-comment block (preferred — invisible in rendered Markdown):
```html
<!-- PDD_ISSUE_CONTRACT
{
"allowed_paths": [
"pdd/update_main.py",
"pdd/prompts/update_main_python.prompt",
"tests/test_update_main.py"
],
"companion_allowlist": [".pdd/meta/*.json"]
}
-->
```
2. A fenced code block under a heading like `### Allowed Write Set` or `### Split Contract`:
```text
pdd/update_main.py
pdd/prompts/update_main_python.prompt
tests/test_update_main.py
```
3. A bullet list under an inline `**Allowed write set:**` label (the
real-world shape used by sub-issues such as #1005):

```markdown
## Split Contract
**Command sequence:** change → sync
**Allowed write set:**
- `pdd/update_main.py`
- `pdd/prompts/update_main_python.prompt`
- `tests/test_update_main.py`
**Acceptance criteria:**
- ...
```

The heading regex is the same as form 2; the inline `**Allowed write set:**` label discriminates the bullet list so unrelated bullets earlier in the body (e.g. a `## Files` section) are NOT captured. Each bullet is one repo-relative POSIX path with optional surrounding backticks. The list terminates at the next `**Label:**` (such as `**Acceptance criteria:**`), a `---` rule, another heading, a non-blank non-bullet line, or end of body.

When an out-of-scope change is detected, the run records a hard failure for that module with a diagnostic of the form:

```
Scope guard reverted N out-of-scope file(s) for module '<basename>' (contract source: <source>):
- path/relative/to/repo
- another/path
Allowed write set:
- path/from/contract
Companion allowlist:
- .pdd/meta/*.json
```

This blocks the per-module success record so dependent modules do not schedule on top of an out-of-scope sync, and checkup/review-loop reports surface the failure instead of letting unrelated artifacts land in the PR. When no contract marker is present, the scope guard falls back to permissive mode — no enforcement, no reverts — preserving existing behavior for issues that have not opted in. Use `--no-scope-guard` to disable enforcement for a single run when you intentionally need to override the contract.

### 1a. sync-architecture

Sync `architecture.json` from prompt metadata tags (`<pdd-reason>`, `<pdd-interface>`, and `<pdd-dependency>`). This is useful after editing prompt metadata directly, or after backfilling prompt tags, so the architecture graph and command metadata stay aligned with the prompts.
Expand Down
16 changes: 13 additions & 3 deletions architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@
"name": "clear_workflow_state",
"signature": "(cwd: Path, issue_number: int, workflow_type: str, state_dir: Path, repo_owner: str, repo_name: str, use_github_state: bool = True) -> None",
"returns": "None"
},
{
"name": "parse_issue_contract",
"signature": "(issue_body: Optional[str], issue_comments: Optional[List[str]] = None) -> Optional[IssueContract]",
"returns": "Optional[IssueContract]"
},
{
"name": "_revert_out_of_scope_changes",
"signature": "(cwd: Path, allowed_paths: set[Path]) -> List[Path]",
"returns": "List[Path]"
}
]
}
Expand Down Expand Up @@ -7264,15 +7274,15 @@
"functions": [
{
"name": "run_agentic_sync",
"signature": "(issue_url: str, *, verbose: bool, quiet: bool, budget: Optional[float], skip_verify: bool, skip_tests: bool, dry_run: bool, agentic_mode: bool, no_steer: bool, max_attempts: Optional[int], timeout_adder: float, use_github_state: bool, one_session: bool, reasoning_time: Optional[float], durable: bool, durable_branch: Optional[str], no_resume: bool, durable_max_parallel: Optional[int]) -> Tuple[bool, str, float, str]",
"signature": "(issue_url: str, *, verbose: bool, quiet: bool, budget: Optional[float], skip_verify: bool, skip_tests: bool, dry_run: bool, agentic_mode: bool, no_steer: bool, max_attempts: Optional[int], timeout_adder: float, use_github_state: bool, one_session: bool, reasoning_time: Optional[float], durable: bool, durable_branch: Optional[str], no_resume: bool, durable_max_parallel: Optional[int], scope_guard: bool = True) -> Tuple[bool, str, float, str]",
"returns": "Tuple[bool, str, float, str]",
"sideEffects": [
"None"
]
},
{
"name": "run_global_sync",
"signature": "(*, verbose: bool, quiet: bool, budget: Optional[float], skip_verify: bool, skip_tests: bool, agentic_mode: bool, no_steer: bool, max_attempts: Optional[int], dry_run: bool, target_coverage: Optional[float], one_session: bool, local: bool, timeout_adder: float) -> Tuple[bool, str, float, str]",
"signature": "(*, verbose: bool, quiet: bool, budget: Optional[float], skip_verify: bool, skip_tests: bool, agentic_mode: bool, no_steer: bool, max_attempts: Optional[int], dry_run: bool, target_coverage: Optional[float], one_session: bool, local: bool, timeout_adder: float, scope_guard: bool = True) -> Tuple[bool, str, float, str]",
"returns": "Tuple[bool, str, float, str]",
"sideEffects": [
"Runs AsyncSyncRunner for stale modules unless dry_run=True; timeout_adder is forwarded via sync_options so --timeout-adder stretches the per-module wall-clock cap on the global-sync path the same way it does on run_agentic_sync"
Expand Down Expand Up @@ -7324,7 +7334,7 @@
"functions": [
{
"name": "AsyncSyncRunner",
"signature": "(basenames: List[str], dep_graph: Dict[str, List[str]], sync_options: Dict[str, Any], github_info: Optional[Dict[str, Any]], quiet: bool = False, verbose: bool = False, issue_url: Optional[str] = None, module_cwds: Optional[Dict[str, Path]] = None, initial_cost: float = 0.0)",
"signature": "(basenames: List[str], dep_graph: Dict[str, List[str]], sync_options: Dict[str, Any], github_info: Optional[Dict[str, Any]], quiet: bool = False, verbose: bool = False, issue_url: Optional[str] = None, module_cwds: Optional[Dict[str, Path]] = None, initial_cost: float = 0.0, *, allowed_write_set: Optional[Iterable[str]] = None, companion_allowlist: Optional[Iterable[str]] = None, scope_guard_enabled: bool = True)",
"returns": "AsyncSyncRunner",
"sideEffects": [
"Initializes runner state; total_budget in sync_options forces sequential scheduling and per-child/per-retry remaining-budget caps"
Expand Down
Loading
Loading