From c26ca7cde3ab5f45f0a5f7ced4314b709cebc3f0 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:11:35 +0200 Subject: [PATCH 01/47] docs: draft PPGP v0.2 concurrency RFC --- ...ent-workstreams-leases-partial-blocking.md | 548 ++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 rfcs/0001-concurrent-workstreams-leases-partial-blocking.md diff --git a/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md b/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md new file mode 100644 index 0000000..47046fa --- /dev/null +++ b/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md @@ -0,0 +1,548 @@ +# RFC 0001: Concurrent Workstreams, Leases, and Partial Blocking + +Status: Draft for PPGP v0.2.0 +Date: 2026-08-30 +Target: Portable Persistent Goal Protocol 0.2.0 + +## Summary + +PPGP v0.1.x models durable continuity for one active substantial goal. Real coding-agent workflows can contain multiple simultaneously active goals or workstreams, multiple agents, multiple Git branches or worktrees, partially blocked external actions, and abrupt agent unavailability. + +This RFC extends PPGP with portable coordination primitives while preserving the protocol's repository-visible, vendor-neutral, low-infrastructure core. + +The central invariant is: + +```text +blocked action != blocked workstream +blocked workstream != blocked goal +blocked goal != blocked project +``` + +A wait condition MUST be scoped to the smallest unit it actually blocks. + +## Motivation + +A real multi-agent coding incident exposed several gaps in PPGP v0.1.2. + +Two independent workstreams existed in one repository. One agent held a shared checkout containing hours of uncommitted work on one branch. A second agent needed to continue another workstream on another branch. The second workstream also depended on external infrastructure credentials for one later action. + +The safe initial behavior was to refuse to switch the shared checkout because doing so could disturb foreign uncommitted work. The unsafe inference was to treat the second workstream as globally blocked. + +The correct recovery was to create an isolated Git worktree and continue all local work that did not require the external dependency. Later, that second agent became unavailable with a substantial dirty worktree. The work remained recoverable on the host, but ownership, durability, takeover, and checkpoint semantics were not explicitly represented by PPGP. + +The incident demonstrates that continuity alone is insufficient once several agents or workstreams share a repository. PPGP also needs a small coordination model. + +## Goals + +PPGP v0.2.0 SHOULD: + +- allow multiple active workstreams in one project; +- distinguish lifecycle phase from schedulability; +- prevent one wait condition from falsely blocking unrelated useful work; +- make writable checkout/worktree ownership explicit; +- make temporary agent execution ownership explicit and transferable; +- support safe recovery after abrupt agent unavailability; +- expose the durability level of unfinished work; +- preserve v0.1.2 single-goal repositories without migration overhead; +- remain usable without MCP, a daemon, a database, a lock server, CRDTs, or a specific agent vendor. + +## Non-goals + +PPGP v0.2.0 does not require: + +- concurrent editing of the same files; +- automatic merge conflict resolution; +- a distributed lock service; +- real-time agent-to-agent messaging; +- a central coordinator agent; +- a mandatory JSON or YAML state format; +- automatic lease expiration based only on wall-clock time; +- multi-agent execution for repositories that do not benefit from it. + +## 1. New logical roles + +### 1.1 WORKSTREAM + +A WORKSTREAM is an independently schedulable unit of useful work inside a project. + +A workstream may correspond to: + +- a complete GOAL; +- a bounded sub-goal; +- a parallel implementation track; +- a review or verification track; +- an infrastructure track independent of product implementation. + +A project MAY contain one or more active workstreams. + +Single-workstream projects remain valid and need no registry. + +### 1.2 WORKSTREAM_REGISTRY + +When more than one workstream is active, implementations SHOULD maintain a compact repository-visible WORKSTREAM_REGISTRY. + +The registry exists only for cross-workstream coordination. It MUST NOT duplicate the complete contents of each ACTIVE_GOAL. + +Minimum coordination fields per workstream SHOULD include: + +```text +ID +ACTIVE_GOAL_REF +SCHEDULABILITY +LEASE +CHECKOUT_CLAIM +WAIT_CONDITIONS +LAST_CHECKPOINT +``` + +Lifecycle phase, frozen decisions, Definition of Done, durable reasoning, and detailed verification remain in the referenced ACTIVE_GOAL or equivalent state. + +The registry is a coordination index, not a second source of goal truth. + +### 1.3 EXECUTION_LEASE + +An EXECUTION_LEASE is a temporary claim by one agent/session to actively mutate a workstream. + +A lease SHOULD record: + +```text +holder +workstream +state +claimed_at +last_checkpoint +handoff_or_takeover_ref +``` + +Recommended lease states: + +```text +ACTIVE +HANDOFF_READY +RECOVERY_REQUIRED +RELEASED +``` + +A lease expresses who may continue execution. It does not grant authority beyond project policy. + +A lease MUST NOT justify deleting or overwriting work merely because its holder is unavailable. + +Implementations MAY use timeouts, but expiry alone MUST NOT authorize destructive takeover of a dirty workspace. + +### 1.4 CHECKOUT_CLAIM + +A CHECKOUT_CLAIM identifies a mutable repository workspace used by a workstream. + +Examples include: + +- the primary checkout; +- a Git worktree; +- an isolated clone; +- another version-control workspace with equivalent semantics. + +A writable checkout claim SHOULD capture, when available: + +```text +workspace_id +mode=read|write +branch +head +base +last_known_dirty_state +host_local_locator +``` + +The host-local locator MAY be an absolute path. Portable identifiers such as branch and commit remain more important for cross-host recovery. + +Writable claims are exclusive by default. + +Multiple agents MAY inspect the same checkout read-only. + +Two workstreams MUST NOT concurrently assume exclusive write ownership of the same mutable checkout unless the repository explicitly provides a safe concurrent-editing mechanism. + +## 2. Two orthogonal state axes + +PPGP v0.1.x lifecycle phase remains unchanged: + +```text +THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED +``` + +PPGP v0.2.0 adds a separate SCHEDULABILITY axis. + +Recommended values: + +```text +RUNNABLE +RUNNING +WAITING_EXTERNAL +WAITING_AUTHORITY +BLOCKED_TECHNICAL +PARKED +COMPLETED +``` + +Lifecycle answers: + +> What kind of work is this workstream doing? + +Schedulability answers: + +> Can useful execution proceed now? + +These concepts MUST NOT be collapsed into one field. + +### RUNNABLE + +At least one safe useful action can execute now and no active execution lease is currently performing it. + +### RUNNING + +An active lease holder is currently executing the workstream. + +### WAITING_EXTERNAL + +No remaining safe useful action can advance this workstream until a non-authority external event or dependency becomes available. + +An individual external dependency MUST NOT make the whole workstream WAITING_EXTERNAL while independent useful actions remain. + +### WAITING_AUTHORITY + +No remaining safe useful action can advance the workstream without a genuine authority decision, permission, credential, legal/financial commitment, or other Type C boundary. + +### BLOCKED_TECHNICAL + +No currently known safe technical path can advance the workstream and the blocker is neither a known external wait nor an authority boundary. + +### PARKED + +The workstream is intentionally deferred despite being potentially runnable. + +### COMPLETED + +The workstream has satisfied its own completion contract. A parent GOAL may still remain open. + +## 3. Scoped wait conditions + +A WAIT_CONDITION MUST identify what it actually blocks. + +Recommended fields: + +```text +kind=external|authority|technical +scope=action|workstream|goal|project +dependency +resume_condition +independent_work_remaining=yes|no +``` + +Default scope SHOULD be the narrowest defensible scope. + +An implementation MUST NOT promote an action-level wait to workstream, goal, or project scope without evidence that all useful independent work at the narrower level is exhausted. + +Before reporting WAITING_EXTERNAL or WAITING_AUTHORITY for an entire workstream, an agent SHOULD ask: + +```text +Is there any safe useful work in this workstream that does not depend on the wait? +``` + +If yes, the workstream remains RUNNABLE or RUNNING and the wait is recorded only for the blocked action. + +The same rule applies recursively across workstreams and goals. + +## 4. Workspace safety protocol + +Before mutating a repository checkout in a concurrent-workstream environment, an agent SHOULD inspect: + +```text +current branch +HEAD +working-tree dirtiness +existing worktrees/checkouts when available +existing checkout claim +current execution lease +``` + +If the intended checkout contains dirty work belonging to another workstream, the agent MUST NOT by default: + +- switch branches; +- reset; +- clean; +- stash foreign changes; +- commit foreign changes; +- overwrite files; +- repurpose the checkout. + +The preferred autonomous response is: + +```text +create or use an isolated workspace +``` + +when that operation is reversible and not forbidden by project policy. + +Creating an isolated Git worktree to avoid disturbing foreign dirty work SHOULD normally be treated as an agent-solvable Type A coordination action, not as a human authority boundary. + +## 5. Dirty-workspace ownership + +Dirty state is not itself a protocol failure. + +A dirty workspace becomes dangerous when its ownership, branch, or recovery state is ambiguous. + +A workstream with a dirty claimed workspace SHOULD record enough state for a fresh agent to answer: + +- whose work is this; +- which workstream it belongs to; +- which branch and HEAD it is based on; +- whether the dirty state is expected; +- what was last verified; +- what action should occur next. + +A stale recorded dirty flag is only last-known state. Recovery MUST inspect the real workspace before mutation. + +## 6. Recovery checkpoints and durability + +PPGP v0.1.2 already requires ACTIVE_GOAL checkpointing after material changes. + +PPGP v0.2.0 adds explicit recovery durability. + +Recommended durability levels: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +### SESSION_ONLY + +Material state exists only in conversation or volatile agent context. + +Material implementation work SHOULD NOT remain at this level longer than necessary. + +### HOST_DURABLE + +State survives the agent session on the current host, for example as uncommitted files in an identified worktree. + +This supports same-host recovery but not host-loss recovery. + +### REPO_DURABLE + +State is represented by local version-control objects or another repository-level recovery artifact. + +Examples may include a checkpoint commit or another explicitly preserved local repository object. + +### REMOTE_DURABLE + +The recovery artifact exists on a remote or otherwise independently durable store and can survive loss of the current host. + +PPGP does not require every intermediate change to be REMOTE_DURABLE. + +The purpose of the field is to make recovery risk explicit rather than silently assuming all unfinished work has equal durability. + +## 7. Abrupt interruption and takeover + +A cooperative handoff and an abrupt takeover are different protocol events. + +### Cooperative handoff + +The current lease holder: + +1. verifies current material state; +2. updates ACTIVE_GOAL; +3. updates the registry if used; +4. records workspace state and durability; +5. emits a compact handoff; +6. marks the lease HANDOFF_READY or RELEASED. + +### Abrupt interruption + +When the lease holder becomes unavailable without a clean handoff, the workstream SHOULD be treated as RECOVERY_REQUIRED if unfinished mutable state may exist. + +A recovery agent MUST first inspect real repository state before assuming the last checkpoint is complete. + +Recommended takeover sequence: + +```text +1. read WORKSTREAM_REGISTRY and ACTIVE_GOAL +2. inspect claimed workspace, branch, HEAD, and dirtiness +3. preserve foreign/uncommitted state exactly as found +4. compare observed state with the last checkpoint +5. classify durability and unresolved uncertainty +6. run proportionate verification when practical +7. record TAKEOVER with the new lease holder +8. continue from the smallest verified next action +``` + +A recovery agent MUST NOT use reset, clean, branch switching, or destructive workspace normalization merely to make recovery simpler. + +## 8. Scheduling rule + +The scheduler may be a human, an agent, a CLI, or simply protocol reasoning. + +When a workstream cannot proceed, it SHOULD search for another RUNNABLE workstream before declaring the project blocked. + +Default project-level rule: + +```text +if any workstream is RUNNABLE or RUNNING: + project is not blocked +``` + +A project is WAITING_EXTERNAL, WAITING_AUTHORITY, or BLOCKED_TECHNICAL only when no permitted active workstream can make useful progress and the aggregate reason matches that state. + +## 9. Multi-agent default remains conservative + +PPGP v0.2.0 does not change the v0.1.x rule that one agent is preferred when additional agents do not provide enough independent value. + +Concurrency SHOULD be introduced when workstreams can be isolated or coordinated at acceptable cost. + +The presence of WORKSTREAM_REGISTRY, leases, or checkout claims does not imply that many agents should run simultaneously. + +These primitives also improve sequential handoffs between different agents. + +## 10. Backward compatibility + +A v0.1.2 repository with one `ACTIVE_GOAL` and one mutable checkout remains a valid minimal PPGP v0.2.0 deployment. + +No WORKSTREAM_REGISTRY is required until concurrent active work exists or coordination ambiguity appears. + +For a single workstream: + +```text +ACTIVE_GOAL ++ repository evidence +``` + +remains sufficient. + +When concurrency begins, the existing ACTIVE_GOAL MAY become one registered workstream without rewriting its durable content. + +## 11. Candidate compact registry representation + +The exact encoding is non-normative. + +Example: + +```text +PPGP/0.2 +WS goal-e + state=RUNNING + goal=docs/ACTIVE_GOAL.goal-e.md + lease=claude/session-a:ACTIVE + checkout=shared:write + branch=claude/goal-e + durability=HOST_DURABLE + +WS d1 + state=RUNNABLE + goal=docs/ACTIVE_GOAL.d1.md + lease=codex/session-b:ACTIVE + checkout=worktree-d1:write + branch=codex/d1 + wait[erith]=external/action + durability=HOST_DURABLE +``` + +This representation intentionally separates cross-workstream coordination from detailed goal state. + +## 12. Candidate operation changes + +Existing operations remain: + +```text +ppgp init +ppgp goal +ppgp status +ppgp handoff +ppgp distill +ppgp close +``` + +v0.2.0 SHOULD additionally define portable intents equivalent to: + +```text +ppgp claim +ppgp release +ppgp recover +ppgp status --all +``` + +Exact CLI syntax is an implementation detail until the command contract is tested. + +## 13. v0.2.0 conformance scenarios + +### Scenario A: foreign dirty shared checkout + +Given: + +- Workstream A owns a shared writable checkout. +- The checkout is dirty with A's unfinished changes. +- Workstream B needs a different branch. + +Expected: + +- B does not switch or reset the shared checkout. +- B identifies the ownership conflict. +- B autonomously selects an isolated workspace when safely available. +- A's dirty state remains intact. + +### Scenario B: partial external wait + +Given: + +- Workstream B requires external credentials for a later remote action. +- Local implementation and verification remain possible. + +Expected: + +- the remote action is WAITING_EXTERNAL; +- the workstream remains RUNNABLE or RUNNING; +- local work continues; +- the project is not reported globally blocked. + +### Scenario C: abrupt agent unavailability with dirty worktree + +Given: + +- an agent holds an active workstream lease; +- the agent becomes unavailable without handoff; +- the claimed worktree contains substantial uncommitted changes. + +Expected: + +- the workstream enters or is treated as RECOVERY_REQUIRED; +- a replacement agent inspects rather than normalizes the workspace; +- the replacement records takeover and durability state; +- verified useful work is preserved and execution resumes without human reconstruction when possible. + +### Scenario D: unrelated runnable workstream + +Given: + +- one workstream is WAITING_AUTHORITY; +- another workstream is RUNNABLE. + +Expected: + +- the project remains runnable; +- the agent may continue the independent workstream; +- only the authority-dependent scope is escalated. + +## 14. Design test + +The v0.2.0 coordination layer succeeds when a fresh agent can answer, without prior conversation: + +```text +What workstreams exist? +Which can run now? +Who currently holds each execution lease? +Which mutable checkout belongs to which workstream? +Which waits are local versus global? +What unfinished work is dirty? +How durable is that work? +What may I safely execute next? +``` + +If those questions require the human to reconstruct the previous agents' interactions, the coordination state is insufficient. From 38f57defa5bf254497928974a9e1d3224ad64c63 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:11:58 +0200 Subject: [PATCH 02/47] docs: define PPGP v0.2.0 development scope --- ROADMAP.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5fe636e..e79c644 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -20,7 +20,46 @@ Current capabilities: - explicit ACTIVE_GOAL hot-state recovery semantics; - reproducible paired benchmark infrastructure. -## Next priorities +## v0.2.0 development line + +v0.2.0 is being designed from observed coordination failures in real long-running multi-agent coding work. + +The release should extend continuity from one active goal to safe recovery and scheduling when several workstreams, agents, branches, worktrees, and partial wait conditions coexist. + +Primary design RFC: + +- [`rfcs/0001-concurrent-workstreams-leases-partial-blocking.md`](./rfcs/0001-concurrent-workstreams-leases-partial-blocking.md) + +Target protocol additions: + +- optional WORKSTREAM_REGISTRY for concurrent active work; +- lifecycle PHASE separated from SCHEDULABILITY; +- RUNNABLE, RUNNING, WAITING_EXTERNAL, WAITING_AUTHORITY, BLOCKED_TECHNICAL, PARKED, and COMPLETED scheduling states; +- scoped wait conditions so one unavailable dependency does not falsely block independent work; +- temporary EXECUTION_LEASE ownership; +- exclusive writable CHECKOUT_CLAIM semantics; +- isolated-workspace preference when a shared checkout contains foreign dirty work; +- explicit RECOVERY_REQUIRED takeover after abrupt agent unavailability; +- unfinished-work durability levels from SESSION_ONLY through REMOTE_DURABLE; +- project-level invariant that a blocked workstream does not block unrelated runnable work; +- backward compatibility for v0.1.2 single-ACTIVE_GOAL repositories. + +### v0.2.0 release gates + +Before v0.2.0 is released: + +1. freeze RFC 0001 semantics; +2. update the normative specification without creating a second source of truth; +3. extend the Agent Skill and compact reference; +4. extend the CLI with the smallest useful coordination operations; +5. add deterministic conformance cases for shared-checkout protection, partial external waits, abrupt takeover, and unrelated runnable workstreams; +6. update all versioned adapters and mirrors atomically; +7. pass existing v0.1.x recovery tests plus new v0.2 coordination tests; +8. validate packaging and version consistency; +9. document the motivating incident only in generalized, non-project-specific form; +10. release as experimental without a superiority or universality claim. + +## Evidence priorities ### Gather independent evidence @@ -34,6 +73,10 @@ Identify fields, steps or rules that can be removed without reducing recovery qu Validate that the same repository-visible state can be interpreted consistently by materially different coding agents and environments. +### Test concurrency semantics + +Test whether fresh agents can safely distinguish workstream ownership, checkout ownership, scoped blockers, recoverable dirty state, and the next runnable work without human reconstruction. + ### Clarify conformance Refine the minimum requirements for claiming PPGP compatibility using observed implementation failures rather than theoretical completeness. @@ -50,7 +93,9 @@ PPGP does not plan to require: - MCP; - vector databases or embeddings; - a hosted service; -- multi-agent orchestration; +- a multi-agent orchestrator; +- a distributed lock server; +- CRDT-backed concurrent editing; - proprietary infrastructure. These may be useful optional integrations, but they should not become prerequisites for protocol conformance. From 24218ba97cca2ce6b438a9de023517a63acfe928 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:13:19 +0200 Subject: [PATCH 03/47] docs: record generalized v0.2 coordination incident --- ...NT-001-concurrent-checkout-partial-wait.md | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md diff --git a/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md new file mode 100644 index 0000000..685c4c5 --- /dev/null +++ b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md @@ -0,0 +1,161 @@ +# Incident 001: Shared Checkout Conflict + Partial External Wait + +Date observed: 2026-08-30 +Evidence class: Real project incident, generalized for public protocol design +PPGP version in use: 0.1.x lineage +Outcome: Recovered without destructive checkout mutation + +## Privacy note + +This record intentionally removes product-specific repository names, infrastructure identifiers, credentials, local paths, and proprietary implementation details. + +The purpose is to preserve only the protocol-relevant failure pattern. + +## Initial state + +One repository contained at least two independent active workstreams. + +Workstream A: + +- was being executed by Agent A; +- occupied the shared primary checkout; +- was on an A-specific branch; +- contained several hours of uncommitted work. + +Workstream B: + +- was being executed by Agent B; +- required a different branch; +- had a future infrastructure action that depended on external connection details not yet available; +- still had substantial local implementation and verification work that did not depend on those details. + +## Failure pattern 1: checkout safety versus liveness + +Agent B correctly detected that switching the shared checkout could disturb or lose Agent A's dirty work. + +Agent B therefore refused to switch the shared checkout. + +That safety decision was correct. + +The coordination failure was treating the checkout conflict as though it prevented Workstream B from making any progress. + +## Recovery 1 + +A separate Git worktree was created for Workstream B. + +This preserved Agent A's dirty shared checkout while allowing Agent B to continue independently. + +Protocol lesson: + +```text +foreign dirty checkout +!= +project-wide blocker +``` + +When safe isolation is available, workspace isolation is preferable to either destructive normalization or unnecessary idling. + +## Failure pattern 2: external wait promoted too far + +A later remote action in Workstream B required external connection details. + +The dependency was real. + +The incorrect interpretation was: + +```text +remote action WAITING_EXTERNAL +=> entire workstream WAITING_EXTERNAL +``` + +Local implementation and verification were still possible. + +Protocol lesson: + +```text +wait conditions require explicit scope +``` + +The external wait should have applied only to the remote action until all independent local work was exhausted. + +## Recovery 2 + +Workstream B continued local engineering in its isolated worktree while the remote action remained waiting. + +This separated two truths that had previously been conflated: + +```text +LOCAL_ENGINEERING = RUNNABLE/RUNNING +REMOTE_EXECUTION = WAITING_EXTERNAL +``` + +## Failure pattern 3: abrupt agent unavailability with dirty isolated work + +After substantial local progress, Agent B became unavailable before a cooperative handoff. + +The isolated worktree contained many modified files and significant uncommitted changes. + +The work was still present on the host, but PPGP v0.1.x did not explicitly represent: + +- temporary execution ownership; +- checkout ownership; +- takeover state; +- dirty-workspace recovery rules; +- durability level of unfinished work. + +## Recovery requirement + +A replacement agent must be able to inspect and continue the dirty isolated worktree without treating it as corruption and without resetting it merely to obtain a clean Git state. + +The correct recovery sequence is observation before mutation: + +```text +read goal state +-> inspect workspace +-> identify branch/HEAD/dirtiness +-> preserve changes +-> compare with last checkpoint +-> verify proportionately +-> record takeover +-> continue +``` + +## Protocol gaps identified + +The incident exposed six gaps: + +1. PPGP v0.1.x assumes one primary ACTIVE_GOAL and does not explicitly coordinate several simultaneous workstreams. +2. Lifecycle phase does not express whether a workstream is currently runnable. +3. Blockers and waits have no normative scope. +4. Writable checkout ownership is implicit. +5. Temporary agent execution ownership and takeover are implicit. +6. The durability of unfinished dirty work is not visible. + +## v0.2.0 design consequences + +RFC 0001 proposes: + +- WORKSTREAM; +- WORKSTREAM_REGISTRY; +- SCHEDULABILITY separate from lifecycle PHASE; +- scoped WAIT_CONDITION; +- EXECUTION_LEASE; +- CHECKOUT_CLAIM; +- RECOVERY_REQUIRED takeover; +- explicit unfinished-work durability levels. + +## Generalized invariant + +The most important lesson is: + +```text +PROTECT FOREIGN WORK +AND +CONTINUE INDEPENDENT SAFE WORK +``` + +Safety without liveness causes needless idling. + +Liveness without ownership safety risks data loss. + +PPGP v0.2.0 should require both. From 706ba88018081fb2b73f62cac28cc00cd22c2414 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:14:29 +0200 Subject: [PATCH 04/47] docs: normalize v0.2 workstream execution state --- ...ent-workstreams-leases-partial-blocking.md | 169 ++++++++++++------ 1 file changed, 118 insertions(+), 51 deletions(-) diff --git a/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md b/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md index 47046fa..47adabf 100644 --- a/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md +++ b/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md @@ -37,7 +37,7 @@ The incident demonstrates that continuity alone is insufficient once several age PPGP v0.2.0 SHOULD: - allow multiple active workstreams in one project; -- distinguish lifecycle phase from schedulability; +- distinguish lifecycle phase from execution state; - prevent one wait condition from falsely blocking unrelated useful work; - make writable checkout/worktree ownership explicit; - make temporary agent execution ownership explicit and transferable; @@ -88,10 +88,11 @@ Minimum coordination fields per workstream SHOULD include: ```text ID ACTIVE_GOAL_REF -SCHEDULABILITY +RUN_STATE LEASE CHECKOUT_CLAIM WAIT_CONDITIONS +DURABILITY LAST_CHECKPOINT ``` @@ -127,7 +128,9 @@ A lease expresses who may continue execution. It does not grant authority beyond A lease MUST NOT justify deleting or overwriting work merely because its holder is unavailable. -Implementations MAY use timeouts, but expiry alone MUST NOT authorize destructive takeover of a dirty workspace. +Implementations MAY use timeouts, heartbeats, fencing tokens, MCP tools, or orchestrator state as optional accelerators. The portable core does not require any of them. + +Lease expiry alone MUST NOT authorize destructive takeover of a dirty workspace. ### 1.4 CHECKOUT_CLAIM @@ -152,7 +155,7 @@ last_known_dirty_state host_local_locator ``` -The host-local locator MAY be an absolute path. Portable identifiers such as branch and commit remain more important for cross-host recovery. +The host-local locator MAY be an absolute path or another host-local handle. Portable identifiers such as branch and commit remain more important for cross-host recovery. Writable claims are exclusive by default. @@ -160,7 +163,7 @@ Multiple agents MAY inspect the same checkout read-only. Two workstreams MUST NOT concurrently assume exclusive write ownership of the same mutable checkout unless the repository explicitly provides a safe concurrent-editing mechanism. -## 2. Two orthogonal state axes +## 2. Orthogonal state axes PPGP v0.1.x lifecycle phase remains unchanged: @@ -168,55 +171,54 @@ PPGP v0.1.x lifecycle phase remains unchanged: THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED ``` -PPGP v0.2.0 adds a separate SCHEDULABILITY axis. - -Recommended values: +PPGP v0.2.0 adds a separate RUN_STATE axis: ```text RUNNABLE RUNNING -WAITING_EXTERNAL -WAITING_AUTHORITY -BLOCKED_TECHNICAL +WAITING +RECOVERY_REQUIRED PARKED COMPLETED ``` -Lifecycle answers: +Lifecycle PHASE answers: > What kind of work is this workstream doing? -Schedulability answers: +RUN_STATE answers: + +> Can useful execution proceed now, and does somebody currently own execution? -> Can useful execution proceed now? +WAIT_CONDITIONS answer: + +> What specific conditions prevent particular actions from proceeding? These concepts MUST NOT be collapsed into one field. ### RUNNABLE -At least one safe useful action can execute now and no active execution lease is currently performing it. +At least one safe useful action can execute now and no current execution lease is actively performing it. ### RUNNING -An active lease holder is currently executing the workstream. - -### WAITING_EXTERNAL +A valid execution lease holder is actively executing the workstream. -No remaining safe useful action can advance this workstream until a non-authority external event or dependency becomes available. +### WAITING -An individual external dependency MUST NOT make the whole workstream WAITING_EXTERNAL while independent useful actions remain. +No safe useful action can currently advance the workstream because one or more recorded wait conditions remain unsatisfied. -### WAITING_AUTHORITY +WAITING is intentionally neutral. The typed WAIT_CONDITIONS determine whether the cause is external, authority-bound, technical, or mixed. -No remaining safe useful action can advance the workstream without a genuine authority decision, permission, credential, legal/financial commitment, or other Type C boundary. +### RECOVERY_REQUIRED -### BLOCKED_TECHNICAL +The previous execution holder became unavailable, ownership became ambiguous, or unfinished mutable state exists that must be inspected before normal execution resumes. -No currently known safe technical path can advance the workstream and the blocker is neither a known external wait nor an authority boundary. +RECOVERY_REQUIRED is a safety state, not a failure declaration. ### PARKED -The workstream is intentionally deferred despite being potentially runnable. +The workstream is intentionally deferred despite potentially being runnable. ### COMPLETED @@ -236,20 +238,24 @@ resume_condition independent_work_remaining=yes|no ``` +A workstream MAY contain several wait conditions of different kinds at once. This is why wait kind is not encoded directly into RUN_STATE. + Default scope SHOULD be the narrowest defensible scope. An implementation MUST NOT promote an action-level wait to workstream, goal, or project scope without evidence that all useful independent work at the narrower level is exhausted. -Before reporting WAITING_EXTERNAL or WAITING_AUTHORITY for an entire workstream, an agent SHOULD ask: +Before setting an entire workstream to WAITING, an agent SHOULD ask: ```text Is there any safe useful work in this workstream that does not depend on the wait? ``` -If yes, the workstream remains RUNNABLE or RUNNING and the wait is recorded only for the blocked action. +If yes, the workstream remains RUNNABLE or RUNNING and the wait is recorded only for the blocked scope. The same rule applies recursively across workstreams and goals. +Authority escalation uses the wait type, not the generic WAITING state. An authority wait SHOULD be escalated only when its scope actually prevents all permitted useful progress that matters to the current scheduling decision. + ## 4. Workspace safety protocol Before mutating a repository checkout in a concurrent-workstream environment, an agent SHOULD inspect: @@ -287,7 +293,7 @@ Creating an isolated Git worktree to avoid disturbing foreign dirty work SHOULD Dirty state is not itself a protocol failure. -A dirty workspace becomes dangerous when its ownership, branch, or recovery state is ambiguous. +A dirty workspace becomes dangerous when its ownership, branch, recovery state, or durability is ambiguous. A workstream with a dirty claimed workspace SHOULD record enough state for a fresh agent to answer: @@ -296,9 +302,10 @@ A workstream with a dirty claimed workspace SHOULD record enough state for a fre - which branch and HEAD it is based on; - whether the dirty state is expected; - what was last verified; +- how durable the unfinished work is; - what action should occur next. -A stale recorded dirty flag is only last-known state. Recovery MUST inspect the real workspace before mutation. +A recorded dirty flag is only last-known state. Recovery MUST inspect the real workspace before mutation. ## 6. Recovery checkpoints and durability @@ -341,7 +348,44 @@ PPGP does not require every intermediate change to be REMOTE_DURABLE. The purpose of the field is to make recovery risk explicit rather than silently assuming all unfinished work has equal durability. -## 7. Abrupt interruption and takeover +## 7. Durable coordination versus live execution state + +PPGP MUST NOT require Git history to behave as a high-frequency lock server. + +Implementations SHOULD distinguish: + +```text +DURABLE COORDINATION + workstream identity + ACTIVE_GOAL reference + scoped waits + last checkpoint + expected branch/workspace identity + durability class + +LIVE EXECUTION STATE + current lease holder + heartbeat/timeout + host-local path + runtime lock or fencing metadata +``` + +The exact persistence mechanism for live execution state is implementation-defined. + +It MAY be: + +- a repository-visible runtime file; +- host-local state; +- an MCP coordination service; +- an orchestrator; +- a database; +- another safe coordination mechanism. + +The portable protocol MUST remain understandable without depending on one such mechanism. + +If live lease state is missing or stale, an agent MUST fall back to observation and recovery rules rather than assuming write permission. + +## 8. Abrupt interruption and takeover A cooperative handoff and an abrupt takeover are different protocol events. @@ -351,21 +395,21 @@ The current lease holder: 1. verifies current material state; 2. updates ACTIVE_GOAL; -3. updates the registry if used; +3. updates durable coordination state if needed; 4. records workspace state and durability; 5. emits a compact handoff; 6. marks the lease HANDOFF_READY or RELEASED. ### Abrupt interruption -When the lease holder becomes unavailable without a clean handoff, the workstream SHOULD be treated as RECOVERY_REQUIRED if unfinished mutable state may exist. +When the lease holder becomes unavailable without a clean handoff, the workstream SHOULD be treated as RECOVERY_REQUIRED if unfinished mutable state may exist or ownership cannot be proven current. A recovery agent MUST first inspect real repository state before assuming the last checkpoint is complete. Recommended takeover sequence: ```text -1. read WORKSTREAM_REGISTRY and ACTIVE_GOAL +1. read coordination state and ACTIVE_GOAL 2. inspect claimed workspace, branch, HEAD, and dirtiness 3. preserve foreign/uncommitted state exactly as found 4. compare observed state with the last checkpoint @@ -377,7 +421,7 @@ Recommended takeover sequence: A recovery agent MUST NOT use reset, clean, branch switching, or destructive workspace normalization merely to make recovery simpler. -## 8. Scheduling rule +## 9. Scheduling rule The scheduler may be a human, an agent, a CLI, or simply protocol reasoning. @@ -390,9 +434,11 @@ if any workstream is RUNNABLE or RUNNING: project is not blocked ``` -A project is WAITING_EXTERNAL, WAITING_AUTHORITY, or BLOCKED_TECHNICAL only when no permitted active workstream can make useful progress and the aggregate reason matches that state. +RECOVERY_REQUIRED SHOULD normally be inspected before creating duplicate replacement work. + +The project is globally waiting only when no permitted active workstream can make useful progress and the recorded waits explain the aggregate stop condition. -## 9. Multi-agent default remains conservative +## 10. Multi-agent default remains conservative PPGP v0.2.0 does not change the v0.1.x rule that one agent is preferred when additional agents do not provide enough independent value. @@ -402,7 +448,7 @@ The presence of WORKSTREAM_REGISTRY, leases, or checkout claims does not imply t These primitives also improve sequential handoffs between different agents. -## 10. Backward compatibility +## 11. Backward compatibility A v0.1.2 repository with one `ACTIVE_GOAL` and one mutable checkout remains a valid minimal PPGP v0.2.0 deployment. @@ -419,7 +465,7 @@ remains sufficient. When concurrency begins, the existing ACTIVE_GOAL MAY become one registered workstream without rewriting its durable content. -## 11. Candidate compact registry representation +## 12. Candidate compact registry representation The exact encoding is non-normative. @@ -428,26 +474,34 @@ Example: ```text PPGP/0.2 WS goal-e - state=RUNNING + run=RUNNING goal=docs/ACTIVE_GOAL.goal-e.md - lease=claude/session-a:ACTIVE + lease=agent-a/session-a:ACTIVE checkout=shared:write - branch=claude/goal-e + branch=agent-a/goal-e durability=HOST_DURABLE WS d1 - state=RUNNABLE + run=RUNNING goal=docs/ACTIVE_GOAL.d1.md - lease=codex/session-b:ACTIVE + lease=agent-b/session-b:ACTIVE checkout=worktree-d1:write - branch=codex/d1 - wait[erith]=external/action + branch=agent-b/d1 + wait[remote]=external/action durability=HOST_DURABLE ``` +If Agent B becomes unavailable unexpectedly: + +```text +WS d1 + run=RECOVERY_REQUIRED + lease=agent-b/session-b:RECOVERY_REQUIRED +``` + This representation intentionally separates cross-workstream coordination from detailed goal state. -## 12. Candidate operation changes +## 13. Candidate operation changes Existing operations remain: @@ -469,9 +523,9 @@ ppgp recover ppgp status --all ``` -Exact CLI syntax is an implementation detail until the command contract is tested. +Exact CLI syntax and persistence of live leases are implementation details until the command contract is tested. -## 13. v0.2.0 conformance scenarios +## 14. v0.2.0 conformance scenarios ### Scenario A: foreign dirty shared checkout @@ -497,7 +551,7 @@ Given: Expected: -- the remote action is WAITING_EXTERNAL; +- the remote action has an EXTERNAL action-scoped wait; - the workstream remains RUNNABLE or RUNNING; - local work continues; - the project is not reported globally blocked. @@ -521,7 +575,7 @@ Expected: Given: -- one workstream is WAITING_AUTHORITY; +- one workstream is WAITING because of an AUTHORITY wait; - another workstream is RUNNABLE. Expected: @@ -530,7 +584,20 @@ Expected: - the agent may continue the independent workstream; - only the authority-dependent scope is escalated. -## 14. Design test +### Scenario E: mixed waits + +Given: + +- one workstream has both an EXTERNAL action wait and an AUTHORITY action wait; +- no independent useful actions remain. + +Expected: + +- RUN_STATE is WAITING; +- both typed waits remain visible; +- the protocol does not invent a lossy scalar state such as WAITING_EXTERNAL that hides the authority dependency. + +## 15. Design test The v0.2.0 coordination layer succeeds when a fresh agent can answer, without prior conversation: From 5f1f071e0444cb75b8ab6100f25e4f3fdca613f1 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:15:35 +0200 Subject: [PATCH 05/47] docs: record unreleased PPGP v0.2.0 design line --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7759b..fe20767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## Unreleased - 0.2.0 design line + +Drafted from observed multi-agent coordination failures in real long-running coding work. + +Proposed additions: + +- project-level PORTFOLIO / workstream coordination for concurrent active work; +- WORKSTREAM as an independently schedulable execution unit; +- RUN_STATE separated from the existing lifecycle PHASE; +- scoped typed wait conditions so a blocked action does not falsely block independent work; +- EXECUTION_LEASE semantics for temporary agent ownership; +- CHECKOUT_CLAIM semantics for exclusive writable workspace ownership; +- default safe-isolation behavior when a shared checkout contains foreign dirty work; +- RECOVERY_REQUIRED and non-destructive takeover after abrupt agent unavailability; +- explicit durability classification for unfinished work: SESSION_ONLY, HOST_DURABLE, REPO_DURABLE, REMOTE_DURABLE; +- project-level scheduling invariant: blocked workstream != blocked project; +- backward compatibility with v0.1.2 single-ACTIVE_GOAL repositories. + +Design evidence and rationale are recorded in RFC 0001 and Incident 001. + +This section is not a release announcement. v0.2.0 remains unreleased until the specification, Agent Skill, CLI, conformance cases, adapters, package metadata, and version-consistency tests are updated and verified together. + ## 0.1.2 - 2026-08-26 Version-consistency and release-hardening patch. From 3714590b6aa3a3ee2a2bc97ab59ad935fddbae73 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:16:05 +0200 Subject: [PATCH 06/47] docs: capture executor unavailability lesson from incident --- ...NT-001-concurrent-checkout-partial-wait.md | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md index 685c4c5..b1cb87c 100644 --- a/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md +++ b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md @@ -86,12 +86,12 @@ This separated two truths that had previously been conflated: ```text LOCAL_ENGINEERING = RUNNABLE/RUNNING -REMOTE_EXECUTION = WAITING_EXTERNAL +REMOTE_EXECUTION = external action wait ``` -## Failure pattern 3: abrupt agent unavailability with dirty isolated work +## Failure pattern 3: abrupt executor unavailability with dirty isolated work -After substantial local progress, Agent B became unavailable before a cooperative handoff. +After substantial local progress, Agent B became temporarily unavailable before a cooperative handoff. The isolated worktree contained many modified files and significant uncommitted changes. @@ -103,6 +103,18 @@ The work was still present on the host, but PPGP v0.1.x did not explicitly repre - dirty-workspace recovery rules; - durability level of unfinished work. +The executor becoming unavailable did not make the underlying workstream semantically blocked. + +Protocol lesson: + +```text +executor unavailable +!= +workstream blocked +``` + +A provider cooldown, quota boundary, process crash, terminal loss, or similar executor interruption is an execution-capacity event. It SHOULD NOT be promoted into a GOAL blocker when another compatible executor can safely recover or continue the work. + ## Recovery requirement A replacement agent must be able to inspect and continue the dirty isolated worktree without treating it as corruption and without resetting it merely to obtain a clean Git state. @@ -110,7 +122,7 @@ A replacement agent must be able to inspect and continue the dirty isolated work The correct recovery sequence is observation before mutation: ```text -read goal state +read goal and portfolio state -> inspect workspace -> identify branch/HEAD/dirtiness -> preserve changes @@ -120,9 +132,13 @@ read goal state -> continue ``` +If the prior lease ended cleanly and no ambiguous mutable state remains, executor unavailability may simply make the workstream RUNNABLE again. + +If unfinished dirty or uncertain state exists, the correct interim state is RECOVERY_REQUIRED until takeover inspection establishes safe ownership. + ## Protocol gaps identified -The incident exposed six gaps: +The incident exposed seven gaps: 1. PPGP v0.1.x assumes one primary ACTIVE_GOAL and does not explicitly coordinate several simultaneous workstreams. 2. Lifecycle phase does not express whether a workstream is currently runnable. @@ -130,19 +146,21 @@ The incident exposed six gaps: 4. Writable checkout ownership is implicit. 5. Temporary agent execution ownership and takeover are implicit. 6. The durability of unfinished dirty work is not visible. +7. Executor availability can be mistaken for workstream availability. ## v0.2.0 design consequences RFC 0001 proposes: +- PORTFOLIO / workstream coordination; - WORKSTREAM; -- WORKSTREAM_REGISTRY; -- SCHEDULABILITY separate from lifecycle PHASE; -- scoped WAIT_CONDITION; +- RUN_STATE separate from lifecycle PHASE; +- scoped typed wait conditions; - EXECUTION_LEASE; - CHECKOUT_CLAIM; - RECOVERY_REQUIRED takeover; -- explicit unfinished-work durability levels. +- explicit unfinished-work durability levels; +- executor unavailability treated as a runtime-capacity condition rather than an automatic GOAL blocker. ## Generalized invariant From c42251d888040f4730e021479e7ef08b0d4a99b6 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:16:21 +0200 Subject: [PATCH 07/47] docs: align v0.2 roadmap with normalized coordination state --- ROADMAP.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e79c644..c9d6214 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -24,23 +24,29 @@ Current capabilities: v0.2.0 is being designed from observed coordination failures in real long-running multi-agent coding work. -The release should extend continuity from one active goal to safe recovery and scheduling when several workstreams, agents, branches, worktrees, and partial wait conditions coexist. +The release should extend continuity from one active goal to safe recovery and scheduling when several workstreams, agents, branches, worktrees, partial wait conditions, and temporary executor interruptions coexist. Primary design RFC: - [`rfcs/0001-concurrent-workstreams-leases-partial-blocking.md`](./rfcs/0001-concurrent-workstreams-leases-partial-blocking.md) +Generalized design evidence: + +- [`evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md`](./evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md) + Target protocol additions: -- optional WORKSTREAM_REGISTRY for concurrent active work; -- lifecycle PHASE separated from SCHEDULABILITY; -- RUNNABLE, RUNNING, WAITING_EXTERNAL, WAITING_AUTHORITY, BLOCKED_TECHNICAL, PARKED, and COMPLETED scheduling states; -- scoped wait conditions so one unavailable dependency does not falsely block independent work; +- optional project-level PORTFOLIO / workstream coordination when concurrent active work exists; +- WORKSTREAM as an independently schedulable unit; +- lifecycle PHASE separated from RUN_STATE; +- compact RUN_STATE values: RUNNABLE, RUNNING, WAITING, RECOVERY_REQUIRED, PARKED, COMPLETED; +- scoped typed wait conditions so one unavailable dependency does not falsely block independent work; - temporary EXECUTION_LEASE ownership; - exclusive writable CHECKOUT_CLAIM semantics; - isolated-workspace preference when a shared checkout contains foreign dirty work; -- explicit RECOVERY_REQUIRED takeover after abrupt agent unavailability; +- explicit non-destructive takeover after abrupt agent unavailability; - unfinished-work durability levels from SESSION_ONLY through REMOTE_DURABLE; +- executor unavailability treated as runtime capacity rather than an automatic GOAL blocker; - project-level invariant that a blocked workstream does not block unrelated runnable work; - backward compatibility for v0.1.2 single-ACTIVE_GOAL repositories. @@ -52,11 +58,11 @@ Before v0.2.0 is released: 2. update the normative specification without creating a second source of truth; 3. extend the Agent Skill and compact reference; 4. extend the CLI with the smallest useful coordination operations; -5. add deterministic conformance cases for shared-checkout protection, partial external waits, abrupt takeover, and unrelated runnable workstreams; +5. add deterministic conformance cases for shared-checkout protection, partial external waits, abrupt takeover, mixed waits, and unrelated runnable workstreams; 6. update all versioned adapters and mirrors atomically; 7. pass existing v0.1.x recovery tests plus new v0.2 coordination tests; 8. validate packaging and version consistency; -9. document the motivating incident only in generalized, non-project-specific form; +9. keep motivating incidents generalized and free of private product details; 10. release as experimental without a superiority or universality claim. ## Evidence priorities @@ -75,7 +81,7 @@ Validate that the same repository-visible state can be interpreted consistently ### Test concurrency semantics -Test whether fresh agents can safely distinguish workstream ownership, checkout ownership, scoped blockers, recoverable dirty state, and the next runnable work without human reconstruction. +Test whether fresh agents can safely distinguish workstream ownership, checkout ownership, scoped blockers, executor interruptions, recoverable dirty state, and the next runnable work without human reconstruction. ### Clarify conformance From ae17899a810710374e396bdc741909f98829319d Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:16:53 +0200 Subject: [PATCH 08/47] docs: add related work for PPGP coordination model --- RELATED_WORK.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 RELATED_WORK.md diff --git a/RELATED_WORK.md b/RELATED_WORK.md new file mode 100644 index 0000000..5bbff92 --- /dev/null +++ b/RELATED_WORK.md @@ -0,0 +1,147 @@ +# Related Work + +PPGP is an experimental engineering protocol assembled from practical coding-agent failures and established ideas in software engineering, context engineering, durable execution, and multi-agent coordination. + +This document exists to make that lineage explicit. + +PPGP does not claim to have invented persistent memory, worktrees, leases, durable execution, multi-agent coordination, or Agent Skills. + +The project's contribution is the attempt to combine a small subset of these ideas into a portable, repository-oriented continuity and coordination protocol that remains usable across different coding-agent products. + +## Long-running agent continuity + +### Anthropic: Effective harnesses for long-running agents + +https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents + +Anthropic describes the problem of coding work spanning multiple context windows and the need for successive agent sessions to recover prior progress from persistent project artifacts. + +Relationship to PPGP: + +- strongly aligned with repository-visible continuity; +- supports the premise that conversation history should not be the only carrier of long-running project state; +- PPGP adds an explicit goal lifecycle, authority boundaries, distillation rules, and portable handoff semantics. + +### Anthropic: Effective context engineering for AI agents + +https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents + +Anthropic frames context as a finite resource that should be curated for utility rather than maximized indiscriminately. + +Relationship to PPGP: + +- aligned with selective retrieval; +- aligned with compact hot state instead of replaying complete transcripts; +- motivates PPGP's preference for state and deltas over chronological diaries. + +## Portable skill packaging + +### Agent Skills specification + +https://agentskills.io/specification + +Agent Skills defines an open folder format centered on `SKILL.md`, with optional scripts, references, and resources loaded progressively. + +Relationship to PPGP: + +- PPGP uses Agent Skills as one portable distribution mechanism; +- Agent Skills is not itself the PPGP memory or coordination model; +- PPGP should remain usable as a written protocol even when an Agent Skills client is unavailable. + +## Isolated Git workspaces + +### Git worktree documentation + +https://git-scm.com/docs/git-worktree.html + +Git worktrees allow several working trees to be attached to one repository so multiple branches can be checked out separately. + +The Git documentation includes a closely related safety pattern: when one working tree contains an in-progress refactor that should not be disturbed, another linked worktree can be created for an unrelated urgent fix. + +Relationship to PPGP v0.2: + +- supports isolated-workspace preference when a shared checkout contains foreign dirty work; +- Git worktree remains one implementation mechanism, not a PPGP core requirement; +- repositories using another VCS or sandbox system may implement equivalent checkout claims differently. + +## Durable execution and interruption recovery + +### Google: Agent Executor distributed runtime + +https://cloud.google.com/blog/products/ai-machine-learning/agent-executor-googles-distributed-agent-runtime + +Google describes a distributed runtime for long-running agent execution with durable execution, resumption, event logs, and snapshotting. + +Relationship to PPGP v0.2: + +- aligned with treating agent interruption as recoverable execution state rather than automatic task failure; +- richer runtimes can provide stronger lease, event-log, snapshot, and remote-durability guarantees; +- PPGP intentionally does not require such infrastructure in its portable core. + +## Concurrent coding-agent coordination + +### AgentRoom: Concurrent Multi-Agent Coding in a CRDT-Backed Shared Workspace + +https://arxiv.org/abs/2608.23740 + +AgentRoom studies concurrent coding agents using file-level claim, status, broadcast, and CRDT-backed shared-workspace mechanisms. + +Relationship to PPGP v0.2: + +- independently supports the usefulness of explicit claims and status in concurrent coding; +- AgentRoom provides a runtime mechanism for concurrent collaboration; +- PPGP focuses on portable coordination semantics and does not require CRDTs or MCP. + +### When Agents Coordinate: Measuring Coordination in Multi-Agent AI Coding + +https://arxiv.org/abs/2608.16801 + +This work studies coordination structure, messages, file access, and communication cost in multi-agent coding runs. It reports that shared files can replace repeated direct communication in some message-heavy configurations, while also adding overhead where file-based coordination is unnecessary. + +Relationship to PPGP: + +- supports treating coordination cost as something to minimize rather than assuming more inter-agent communication is always better; +- consistent with PPGP's single-agent-by-default rule; +- consistent with repository-visible shared state where it replaces repeated handoff prose; +- reinforces that coordination mechanisms should be conditional rather than universal overhead. + +## PPGP positioning + +The intended PPGP core remains deliberately smaller than a multi-agent runtime. + +```text +PPGP core + goal continuity + compact recovery state + scoped blockers/waits + workstream coordination + ownership semantics + evidence-driven recovery + distillation and garbage collection + +Optional runtime mechanisms + MCP + CRDTs + remote lock services + heartbeats + fencing tokens + event logs + snapshot stores + vector retrieval + provider-specific compaction + orchestration platforms +``` + +A runtime may implement PPGP semantics using these richer mechanisms. + +PPGP conformance should not require them. + +## Novelty posture + +PPGP should make narrow, falsifiable claims. + +It should not claim that its individual primitives are novel. + +The relevant engineering question is whether the combined protocol improves recoverability, coordination safety, and useful autonomy with acceptable overhead across materially different coding-agent environments. + +That question requires independent evidence. From 709b433bc85d664d116991a680d8c602dbb19ace Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:17:18 +0200 Subject: [PATCH 09/47] feat: add draft progressive coordination reference --- skills/ppgp/references/COORDINATION.md | 214 +++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 skills/ppgp/references/COORDINATION.md diff --git a/skills/ppgp/references/COORDINATION.md b/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..c65afd3 --- /dev/null +++ b/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,214 @@ +# PPGP v0.2 Draft Coordination Reference + +Load this reference only when concurrent or ambiguous execution exists. + +Typical triggers: + +- more than one active workstream; +- more than one coding agent/session touching the same repository; +- a dirty checkout owned by another workstream; +- multiple Git worktrees or isolated clones; +- a workstream reported blocked while independent work may remain; +- abrupt executor unavailability; +- takeover of unfinished mutable work. + +For ordinary single-workstream execution, the normal PPGP reference is sufficient. + +## Core invariant + +```text +PROTECT FOREIGN WORK +AND +CONTINUE INDEPENDENT SAFE WORK +``` + +Also: + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` + +## Coordination model + +```text +PROJECT + PORTFOLIO (only when needed) + WORKSTREAM + GOAL_REF + RUN_STATE + WAIT_CONDITIONS[] + EXECUTION_LEASE + CHECKOUT_CLAIM + DURABILITY + LAST_CHECKPOINT +``` + +The PORTFOLIO coordinates active work. It does not replace detailed goal state. + +Do not duplicate frozen decisions, Definition of Done, or durable project memory into every workstream. + +## RUN_STATE + +Keep lifecycle PHASE separate from execution state. + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +A workstream is WAITING only when no safe useful action remains runnable. + +Typed wait conditions explain why. + +## Wait conditions + +Record the smallest true scope. + +```text +kind = EXTERNAL | AUTHORITY | TECHNICAL | CAPACITY +scope = ACTION | WORKSTREAM | GOAL | PROJECT +resume_condition = observable condition +independent_work_remaining = yes | no +``` + +Before promoting a wait to workstream scope, ask: + +```text +Can any safe useful work continue without this dependency? +``` + +If yes, keep the workstream RUNNABLE/RUNNING and scope the wait more narrowly. + +Provider quota/cooldown is normally CAPACITY, not a GOAL blocker. + +## Execution lease + +A lease says which executor currently owns mutation of a workstream. + +Recommended states: + +```text +ACTIVE +HANDOFF_READY +RECOVERY_REQUIRED +RELEASED +``` + +Lease loss does not authorize destructive cleanup. + +A lease does not expand human-delegated authority. + +Live lease persistence is implementation-defined. PPGP does not require Git to function as a lock server. + +## Checkout claim + +A writable mutable checkout is exclusive by default. + +Before mutation inspect, when available: + +```text +branch +HEAD +dirty state +worktree list +existing claim +current lease +``` + +If a shared checkout contains foreign dirty work, do not by default: + +```text +switch +reset +clean +stash foreign work +commit foreign work +overwrite +``` + +Prefer an isolated workspace when safely available. + +For Git repositories, a linked worktree is a normal implementation option. + +Creating safe isolation is normally agent-solvable and should not become a human approval gate unless project policy forbids it. + +## Durability + +Classify unfinished work when recovery risk matters: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +Examples: + +- conversation-only reasoning: SESSION_ONLY +- dirty identified worktree: HOST_DURABLE +- local checkpoint commit: REPO_DURABLE +- pushed checkpoint/recoverable remote artifact: REMOTE_DURABLE + +Do not assume HOST_DURABLE work survives host loss. + +## Cooperative handoff + +Before releasing execution when possible: + +```text +VERIFY +-> update goal/workstream state +-> record workspace + durability +-> record NEXT +-> emit compact handoff +-> mark lease HANDOFF_READY/RELEASED +``` + +## Abrupt takeover + +When the prior executor disappears with ambiguous or dirty mutable state: + +```text +RUN_STATE = RECOVERY_REQUIRED +``` + +Then: + +```text +1. read portfolio + goal state +2. inspect real workspace +3. preserve dirty state exactly as found +4. compare with last checkpoint +5. classify durability/uncertainty +6. verify proportionately +7. record takeover/new lease +8. continue smallest verified next action +``` + +Do not normalize with destructive Git operations merely to obtain a clean status. + +## Project scheduler rule + +```text +if any permitted workstream is RUNNABLE or RUNNING: + project is not blocked +``` + +Before asking the human about one blocked workstream, continue another useful workstream when safe and within delegated scope. + +Human escalation remains scoped to the genuine authority boundary. + +## Progressive-disclosure rule + +Do not preload this reference for every PPGP operation. + +Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. From f4e5763cdbd960c0b11a7d01caa7cd9e29072a9d Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:17:38 +0200 Subject: [PATCH 10/47] docs: keep executor capacity outside blocker taxonomy --- skills/ppgp/references/COORDINATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/ppgp/references/COORDINATION.md b/skills/ppgp/references/COORDINATION.md index c65afd3..eb44dd3 100644 --- a/skills/ppgp/references/COORDINATION.md +++ b/skills/ppgp/references/COORDINATION.md @@ -71,7 +71,7 @@ Typed wait conditions explain why. Record the smallest true scope. ```text -kind = EXTERNAL | AUTHORITY | TECHNICAL | CAPACITY +kind = EXTERNAL | AUTHORITY | TECHNICAL scope = ACTION | WORKSTREAM | GOAL | PROJECT resume_condition = observable condition independent_work_remaining = yes | no @@ -85,7 +85,7 @@ Can any safe useful work continue without this dependency? If yes, keep the workstream RUNNABLE/RUNNING and scope the wait more narrowly. -Provider quota/cooldown is normally CAPACITY, not a GOAL blocker. +Provider quota, cooldown, process loss, or terminal loss is executor availability. It is handled through lease/recovery semantics rather than inventing a new GOAL blocker type. ## Execution lease From 9000b737863c0c09bfffb75e686de52281524e4b Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:18:13 +0200 Subject: [PATCH 11/47] docs: materialize v0.2 normative delta from v0.1.2 --- rfcs/0001-normative-delta.md | 394 +++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 rfcs/0001-normative-delta.md diff --git a/rfcs/0001-normative-delta.md b/rfcs/0001-normative-delta.md new file mode 100644 index 0000000..c58a330 --- /dev/null +++ b/rfcs/0001-normative-delta.md @@ -0,0 +1,394 @@ +# RFC 0001 Normative Delta: PPGP v0.1.2 -> v0.2.0 + +Status: Draft implementation handoff +Base: PPGP Specification v0.1.2 +Target: PPGP Specification v0.2.0 + +This document is intentionally a delta, not a duplicate specification. + +Apply it only after RFC 0001 semantics are accepted. + +## 1. Scope delta + +Extend the scope from continuity of one substantial active goal to continuity and safe coordination when multiple active workstreams, executors, or mutable checkouts coexist. + +Add the invariant: + +```text +executor unavailable != workstream blocked +blocked action != blocked workstream +blocked workstream != project blocked +``` + +## 2. Logical memory-role delta + +Keep existing roles: + +```text +CONSTITUTION +ROADMAP +MEMORY +ACTIVE_GOAL +GIT / FORENSIC HISTORY +``` + +Add optional roles activated only when concurrent or ambiguous execution exists: + +```text +PORTFOLIO +WORKSTREAM +EXECUTION_LEASE +CHECKOUT_CLAIM +WAIT_CONDITION +DURABILITY +``` + +### PORTFOLIO + +Project-level coordination view of active workstreams. + +MUST NOT become a duplicate execution diary or a second copy of every ACTIVE_GOAL. + +MAY be omitted for ordinary single-workstream execution. + +### WORKSTREAM + +Independently schedulable unit of useful work associated with a goal or bounded execution track. + +### EXECUTION_LEASE + +Temporary mutation ownership by an executor/session. + +Lease loss or expiry MUST NOT authorize destructive cleanup of dirty work. + +### CHECKOUT_CLAIM + +Ownership declaration for a mutable checkout/worktree/isolated repository workspace. + +Writable claims SHOULD be exclusive unless the repository provides an explicit safe concurrent-editing mechanism. + +### WAIT_CONDITION + +Typed and scoped condition preventing a specific action or larger unit from proceeding. + +### DURABILITY + +Explicit recovery level for unfinished work. + +Values: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +## 3. ACTIVE_GOAL delta + +Keep all v0.1.2 ACTIVE_GOAL requirements. + +Clarify that ACTIVE_GOAL remains goal truth while PORTFOLIO/workstream state handles cross-workstream coordination. + +A concurrent implementation MUST avoid copying frozen decisions and Definition of Done into multiple conflicting sources of truth. + +When one ACTIVE_GOAL is sufficient, no new coordination file is required. + +## 4. Lifecycle delta + +Do not change the lifecycle: + +```text +THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED +``` + +Add an orthogonal RUN_STATE for workstreams: + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +PHASE and RUN_STATE MUST NOT be conflated. + +## 5. Inner-loop delta + +Keep: + +```text +RETRIEVE -> ACT -> VERIFY -> DELTA +``` + +Extend RETRIEVE in concurrent contexts to include relevant portfolio, lease, checkout, and wait state. + +Extend VERIFY before mutation to validate real workspace state when checkout ownership is relevant. + +Extend DELTA to update coordination state only when a material coordination fact changes. + +Do not turn Git commits into high-frequency lease heartbeats. + +## 6. Boot/recovery delta + +For ordinary single-workstream recovery, preserve the v0.1.2 boot sequence. + +For concurrent/ambiguous recovery, the minimal boot packet SHOULD additionally answer: + +```text +which workstreams exist +which are runnable +which executor last/currently owns execution +which checkout belongs to which workstream +which waits are scoped locally versus globally +how durable unfinished work is +``` + +If the prior executor disappeared and dirty/uncertain mutable state may exist, RUN_STATE SHOULD become or be treated as RECOVERY_REQUIRED until inspected. + +## 7. Evidence-precedence delta + +Keep the existing evidence hierarchy. + +Add this rule: + +Recorded checkout/dirty/lease state is last-known coordination evidence, not permission to ignore current observed repository state. + +Observed branch, HEAD, worktree, and dirty state take precedence over stale coordination metadata. + +## 8. Blocker-classification delta + +Keep v0.1.2 blocker classes A/B/C/D. + +Add mandatory scope to waits/blockers when concurrency or partial blocking is relevant: + +```text +ACTION +WORKSTREAM +GOAL +PROJECT +``` + +The default SHOULD be the narrowest defensible scope. + +An agent MUST NOT promote a wait to a broader scope merely because one dependency is unavailable. + +Before marking a whole workstream WAITING, check whether independent useful work remains. + +## 9. Human-interruption delta + +Keep the existing authority boundary. + +Add: + +- checkout isolation and creation of a reversible worktree SHOULD normally be Type A agent-solvable work; +- before escalating a scoped authority blocker, continue unrelated permitted RUNNABLE work when safe; +- executor cooldown/quota/process loss MUST NOT by itself become a human authority request; +- human escalation SHOULD name the smallest blocked scope and exact decision required. + +## 10. Multi-agent delta + +Keep single-agent-by-default. + +Add: + +- multiple agents SHOULD work concurrently only when workstream isolation/coordination cost is acceptable; +- one agent MUST NOT mutate another workstream's foreign dirty checkout by default; +- additional agents MAY take over RECOVERY_REQUIRED work after non-destructive inspection. + +## 11. Handoff delta + +Keep the compact handoff principle. + +For concurrent work, add optional fields or equivalent information: + +```text +WS= +R= +L= +C= +W= +U= +``` + +Exact encoding remains non-normative. + +A cooperative handoff and abrupt takeover MUST be distinguishable. + +## 12. New workspace-safety requirements + +Before mutating a checkout in concurrent/ambiguous conditions, inspect as available: + +```text +current branch +HEAD +dirty state +existing worktrees/checkouts +checkout claim +execution lease +``` + +If foreign dirty work is present, MUST NOT by default: + +```text +switch branches +reset +clean +stash foreign work +commit foreign work +overwrite +repurpose the checkout +``` + +Prefer safe isolation when available. + +## 13. New takeover requirements + +For RECOVERY_REQUIRED work: + +```text +read coordination + goal state +-> inspect real workspace +-> preserve dirty state +-> compare with last checkpoint +-> classify durability/uncertainty +-> verify proportionately +-> record takeover/new lease +-> resume smallest verified next action +``` + +Destructive workspace normalization MUST NOT be used merely to simplify takeover. + +## 14. New project scheduling rule + +Add: + +```text +if any permitted workstream is RUNNABLE or RUNNING: + project is not blocked +``` + +When one workstream cannot proceed, the scheduler SHOULD seek another RUNNABLE workstream before reporting a project-wide stop. + +## 15. Distillation/GC delta + +Keep existing goal distillation. + +At closure or retirement of a workstream: + +- release/retire live lease state; +- remove obsolete checkout claims; +- preserve only durable reusable lessons and project direction; +- do not preserve transient executor identity as durable project memory unless it matters to future behavior. + +PORTFOLIO coordination SHOULD shrink as workstreams complete. + +## 16. Metrics delta + +Keep HIG, TPG, RSR, VWR, MCR. + +Candidate optional v0.2 coordination measurements: + +```text +BSR Blocker Scope Reliability +CCR Checkout Collision Rate +TRR Takeover Recovery Rate +DWR Duplicate Work after Recovery +``` + +Metric names and formulas remain non-normative until evaluation design is reviewed. + +Do not publish numeric performance claims merely because these metrics exist. + +## 17. Interoperability delta + +Explicitly classify richer coordination mechanisms as optional adapters: + +```text +Git worktrees +MCP claim/status tools +CRDT shared workspaces +remote lock services +heartbeats +fencing tokens +event logs +snapshot stores +orchestrators +``` + +No one mechanism is required for PPGP conformance. + +## 18. Reference mapping delta + +Retain existing common mapping. + +Add an optional example only: + +```text +PORTFOLIO -> docs/PPGP_PORTFOLIO.md or equivalent +``` + +Do not make that filename normative. + +## 19. Conformance-test delta + +Retain the v0.1.2 abrupt recovery test. + +Add at least these cases: + +### C1 Foreign dirty checkout + +Replacement/parallel work preserves foreign dirty state and uses safe isolation where available. + +### C2 Partial external wait + +Action-scoped external wait does not stop independent local work. + +### C3 Abrupt executor loss + +Dirty identified work survives executor disappearance and is recovered non-destructively. + +### C4 Independent workstream + +One authority/external-blocked workstream does not stop another RUNNABLE workstream. + +### C5 Mixed waits + +Multiple wait kinds remain visible without being collapsed into a lossy scalar state. + +## 20. Skill implementation delta + +Keep `skills/ppgp/SKILL.md` compact. + +Add one trigger instruction telling the agent to load: + +```text +references/COORDINATION.md +``` + +only when concurrency, ownership ambiguity, partial blocking, or takeover appears. + +This preserves progressive disclosure and avoids charging single-workstream users the token cost of coordination rules. + +## 21. CLI implementation delta + +Keep current commands backward compatible. + +Prototype the smallest useful additions before freezing syntax: + +```text +ppgp status --all +ppgp claim +ppgp release +ppgp recover +``` + +Do not release these commands until persistence semantics and tests are stable. + +## 22. Versioning delta + +The change is intentionally `0.2.0`, not `0.1.3`, because it adds new protocol primitives and conformance behavior while the project is still in the experimental `0.x` line. + +Update package, specification, Agent Skill metadata, adapters, mirrors, citation metadata, docs, release assets, and version-consistency fixtures atomically at release time. From 04272c575e8c9e47da674c69833949faaec3a81a Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:32:06 +0200 Subject: [PATCH 12/47] feat: add v0.2 portfolio reference schema --- schemas/portfolio.schema.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 schemas/portfolio.schema.json diff --git a/schemas/portfolio.schema.json b/schemas/portfolio.schema.json new file mode 100644 index 0000000..8a4256a --- /dev/null +++ b/schemas/portfolio.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Fatboy-coder/ppgp/schemas/portfolio.schema.json", + "title": "PPGP Reference Portfolio", + "description": "Reference JSON representation for PPGP v0.2 portfolio coordination. JSON is not required by the protocol core.", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "id", "revision", "aggregationPolicy", "workstreams"], + "properties": { + "schemaVersion": { "const": "0.2" }, + "id": { "type": "string", "minLength": 1 }, + "revision": { "type": "integer", "minimum": 0 }, + "aggregationPolicy": { "const": "any-runnable" }, + "workstreams": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "stateRef", "notesRef"], + "properties": { + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }, + "stateRef": { "type": "string", "minLength": 1 }, + "notesRef": { "type": "string", "minLength": 1 } + } + } + } + } +} From 522c45e1c7a852022dcb33e4c3569b93dca6c564 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:32:18 +0200 Subject: [PATCH 13/47] feat: add v0.2 workstream reference schema --- schemas/workstream.schema.json | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 schemas/workstream.schema.json diff --git a/schemas/workstream.schema.json b/schemas/workstream.schema.json new file mode 100644 index 0000000..43f973c --- /dev/null +++ b/schemas/workstream.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Fatboy-coder/ppgp/schemas/workstream.schema.json", + "title": "PPGP Reference Workstream State", + "description": "Reference JSON representation for PPGP v0.2 workstream machine state. Human-readable WHY, DoD, frozen decisions and invariants belong in notes.md or an equivalent logical role.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "id", + "title", + "revision", + "phase", + "runState", + "branch", + "lease", + "dependencies", + "waitConditions", + "authorityGates", + "nextAction", + "evidenceRefs", + "durability", + "notesRef" + ], + "properties": { + "schemaVersion": { "const": "0.2" }, + "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }, + "title": { "type": "string", "minLength": 1 }, + "revision": { "type": "integer", "minimum": 0 }, + "phase": { + "enum": ["THINK", "FREEZE", "EXECUTE", "HARDEN", "SHIP", "DISTILL", "CLOSED"] + }, + "runState": { + "enum": ["RUNNABLE", "RUNNING", "WAITING", "RECOVERY_REQUIRED", "PARKED", "COMPLETED"] + }, + "branch": { "type": ["string", "null"] }, + "lease": { + "type": ["object", "null"], + "additionalProperties": false, + "required": ["holder", "status", "generation", "claimedAt", "expiresAt"], + "properties": { + "holder": { "type": "string", "minLength": 1 }, + "status": { "enum": ["CLAIMED", "HANDOFF_READY", "RECOVERY_REQUIRED", "RELEASED"] }, + "generation": { "type": "integer", "minimum": 1 }, + "claimedAt": { "type": "string", "format": "date-time" }, + "expiresAt": { "type": ["string", "null"], "format": "date-time" } + } + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["workstream", "condition"], + "properties": { + "workstream": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }, + "condition": { "const": "COMPLETED" } + } + } + }, + "waitConditions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "kind", "scope", "dependency", "resumeCondition"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "kind": { "enum": ["EXTERNAL", "AUTHORITY", "TECHNICAL"] }, + "scope": { "enum": ["ACTION", "WORKSTREAM", "GOAL", "PROJECT"] }, + "dependency": { "type": "string", "minLength": 1 }, + "resumeCondition": { "type": "string", "minLength": 1 } + } + } + }, + "authorityGates": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "action", "state"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "action": { "type": "string", "minLength": 1 }, + "state": { "enum": ["REQUIRED", "GRANTED", "CONSUMED", "REVOKED"] } + } + } + }, + "nextAction": { "type": ["string", "null"] }, + "evidenceRefs": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "durability": { + "enum": ["SESSION_ONLY", "HOST_DURABLE", "REPO_DURABLE", "REMOTE_DURABLE"] + }, + "notesRef": { "type": "string", "minLength": 1 } + } +} From f28c19ca07cf73a216a48754461c1d3fc574b5b3 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:36:33 +0200 Subject: [PATCH 14/47] feat(cli): implement v0.2 portfolio coordination --- bin/ppgp.js | 610 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 549 insertions(+), 61 deletions(-) diff --git a/bin/ppgp.js b/bin/ppgp.js index 8ce599f..636cb1f 100644 --- a/bin/ppgp.js +++ b/bin/ppgp.js @@ -3,6 +3,7 @@ const fs = require('fs'); const path = require('path'); +const crypto = require('crypto'); const { execFileSync } = require('child_process'); const pkg = require('../package.json'); @@ -13,29 +14,48 @@ const ROLE_CANDIDATES = { ACTIVE_GOAL: ['docs/ACTIVE_GOAL.md', 'ACTIVE_GOAL.md'] }; -function die(message, code = 1) { - console.error(`PPGP: ${message}`); - process.exit(code); +const PHASES = new Set(['THINK', 'FREEZE', 'EXECUTE', 'HARDEN', 'SHIP', 'DISTILL', 'CLOSED']); +const RUN_STATES = new Set(['RUNNABLE', 'RUNNING', 'WAITING', 'RECOVERY_REQUIRED', 'PARKED', 'COMPLETED']); +const LEASE_STATES = new Set(['CLAIMED', 'HANDOFF_READY', 'RECOVERY_REQUIRED', 'RELEASED']); +const WAIT_KINDS = new Set(['EXTERNAL', 'AUTHORITY', 'TECHNICAL']); +const WAIT_SCOPES = new Set(['ACTION', 'WORKSTREAM', 'GOAL', 'PROJECT']); +const GATE_STATES = new Set(['REQUIRED', 'GRANTED', 'CONSUMED', 'REVOKED']); +const DURABILITY = new Set(['SESSION_ONLY', 'HOST_DURABLE', 'REPO_DURABLE', 'REMOTE_DURABLE']); +const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; + +class PPGPError extends Error { + constructor(message, code = 1) { super(message); this.name = 'PPGPError'; this.code = code; } } +function die(message, code = 1) { throw new PPGPError(message, code); } + function parseArgs(argv) { const args = [...argv]; - const options = { root: process.cwd(), force: false }; + const options = { root: process.cwd(), force: false, all: false, rollback: false, branch: null, holder: null, revision: null }; const positional = []; - while (args.length) { const arg = args.shift(); if (arg === '--root') { const value = args.shift(); if (!value) die('--root requires a path.'); options.root = path.resolve(value); - } else if (arg === '--force') { - options.force = true; - } else { - positional.push(arg); - } + } else if (arg === '--force') options.force = true; + else if (arg === '--all') options.all = true; + else if (arg === '--rollback') options.rollback = true; + else if (arg === '--branch') { + const value = args.shift(); + if (!value) die('--branch requires a value.'); + options.branch = value; + } else if (arg === '--holder') { + const value = args.shift(); + if (!value) die('--holder requires a value.'); + options.holder = value; + } else if (arg === '--revision') { + const value = args.shift(); + if (!/^\d+$/.test(value || '')) die('--revision requires a non-negative integer.'); + options.revision = Number(value); + } else positional.push(arg); } - return { options, positional }; } @@ -51,22 +71,31 @@ function roleMap(root) { return Object.fromEntries(Object.keys(ROLE_CANDIDATES).map((role) => [role, findRole(root, role)])); } -function hasGit(root) { +function git(root, args, fallback = null) { try { - execFileSync('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' }); - return true; + return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { - return false; + return fallback; } } +function hasGit(root) { return git(root, ['rev-parse', '--is-inside-work-tree']) === 'true'; } +function gitBranch(root) { return git(root, ['branch', '--show-current'], null) || null; } +function gitHead(root) { return git(root, ['rev-parse', 'HEAD'], null); } +function gitDirty(root) { const out = git(root, ['status', '--porcelain'], null); return out === null ? null : out.length > 0; } + +function gitCommonDir(root) { + const raw = git(root, ['rev-parse', '--git-common-dir'], null); + if (!raw) return null; + return path.isAbsolute(raw) ? raw : path.resolve(root, raw); +} + function printMap(root) { const roles = roleMap(root); console.log(`PPGP ${pkg.version} repository mapping`); console.log(`Root: ${root}`); - for (const [role, file] of Object.entries(roles)) { - console.log(`${role.padEnd(12)} ${file || '(not mapped)'}`); - } + for (const [role, file] of Object.entries(roles)) console.log(`${role.padEnd(12)} ${file || '(not mapped)'}`); + console.log(`PORTFOLIO ${portfolioExists(root) ? '.ppgp/portfolio.json' : '(implicit/single-workstream)'}`); console.log(`GIT ${hasGit(root) ? 'available' : '(not detected)'}`); return roles; } @@ -85,11 +114,7 @@ function parseSections(content) { let current = null; for (const line of content.split(/\r?\n/)) { const match = line.match(/^##\s+([A-Z_]+)\s*$/); - if (match) { - current = match[1]; - sections[current] = []; - continue; - } + if (match) { current = match[1]; sections[current] = []; continue; } if (current) sections[current].push(line); } return Object.fromEntries(Object.entries(sections).map(([key, lines]) => [key, lines.join('\n').trim()])); @@ -103,24 +128,259 @@ function oneLine(value, fallback = '(not set)') { function readActiveGoal(root) { const file = findRole(root, 'ACTIVE_GOAL'); if (!file) die('No ACTIVE_GOAL found. Start one with: ppgp goal ""'); - const absolute = path.join(root, file); - return { file, sections: parseSections(fs.readFileSync(absolute, 'utf8')) }; + const content = fs.readFileSync(path.join(root, file), 'utf8'); + return { file, content, sections: parseSections(content) }; +} + +function ppgpDir(root) { return path.join(root, '.ppgp'); } +function portfolioPath(root) { return path.join(ppgpDir(root), 'portfolio.json'); } +function portfolioExists(root) { return fs.existsSync(portfolioPath(root)); } +function workstreamDir(root, id) { return path.join(ppgpDir(root), 'workstreams', id); } +function workstreamStatePath(root, id) { return path.join(workstreamDir(root, id), 'state.json'); } +function workstreamNotesPath(root, id) { return path.join(workstreamDir(root, id), 'notes.md'); } + +function readJson(file) { + try { return JSON.parse(fs.readFileSync(file, 'utf8')); } + catch (error) { die(`Invalid JSON in ${file}: ${error.message}`); } +} + +function writeJsonAtomic(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + const temp = `${file}.${process.pid}.${Date.now()}.tmp`; + fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); + fs.renameSync(temp, file); +} + +function withPortfolioLock(root, fn) { + fs.mkdirSync(ppgpDir(root), { recursive: true }); + const lock = path.join(ppgpDir(root), '.lock'); + let fd; + try { fd = fs.openSync(lock, 'wx'); } + catch { die('PPGP coordination state is currently locked by another local mutation. Retry after it completes.'); } + try { fs.writeFileSync(fd, `${process.pid}\n`, 'utf8'); return fn(); } + finally { try { fs.closeSync(fd); } catch {} try { fs.unlinkSync(lock); } catch {} } +} + +function validatePortfolio(p) { + const errors = []; + if (!p || typeof p !== 'object' || Array.isArray(p)) return ['portfolio must be an object']; + if (p.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); + if (typeof p.id !== 'string' || !p.id) errors.push('id must be a non-empty string'); + if (!Number.isInteger(p.revision) || p.revision < 0) errors.push('revision must be a non-negative integer'); + if (p.aggregationPolicy !== 'any-runnable') errors.push('aggregationPolicy must be any-runnable'); + if (!Array.isArray(p.workstreams)) errors.push('workstreams must be an array'); + else { + const seen = new Set(); + for (const ref of p.workstreams) { + if (!ref || typeof ref !== 'object') { errors.push('workstream reference must be an object'); continue; } + if (!ID_RE.test(ref.id || '')) errors.push(`invalid workstream id: ${ref.id}`); + if (seen.has(ref.id)) errors.push(`duplicate workstream id: ${ref.id}`); else seen.add(ref.id); + if (typeof ref.stateRef !== 'string' || !ref.stateRef) errors.push(`workstream ${ref.id} missing stateRef`); + if (typeof ref.notesRef !== 'string' || !ref.notesRef) errors.push(`workstream ${ref.id} missing notesRef`); + } + } + return errors; +} + +function validateState(s) { + const errors = []; + if (!s || typeof s !== 'object' || Array.isArray(s)) return ['state must be an object']; + if (s.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); + if (!ID_RE.test(s.id || '')) errors.push('id is invalid'); + if (typeof s.title !== 'string' || !s.title) errors.push('title must be non-empty'); + if (!Number.isInteger(s.revision) || s.revision < 0) errors.push('revision must be a non-negative integer'); + if (!PHASES.has(s.phase)) errors.push(`invalid phase: ${s.phase}`); + if (!RUN_STATES.has(s.runState)) errors.push(`invalid runState: ${s.runState}`); + if (!(s.branch === null || typeof s.branch === 'string')) errors.push('branch must be string or null'); + if (!(s.lease === null || typeof s.lease === 'object')) errors.push('lease must be object or null'); + if (s.lease) { + if (typeof s.lease.holder !== 'string' || !s.lease.holder) errors.push('lease holder missing'); + if (!LEASE_STATES.has(s.lease.status)) errors.push(`invalid lease status: ${s.lease.status}`); + if (!Number.isInteger(s.lease.generation) || s.lease.generation < 1) errors.push('lease generation must be >= 1'); + if (typeof s.lease.claimedAt !== 'string' || !s.lease.claimedAt) errors.push('lease claimedAt missing'); + if (!(s.lease.expiresAt === null || typeof s.lease.expiresAt === 'string')) errors.push('lease expiresAt must be string or null'); + } + if (!Array.isArray(s.dependencies)) errors.push('dependencies must be an array'); + else for (const d of s.dependencies) { + if (!ID_RE.test((d && d.workstream) || '')) errors.push('dependency workstream id invalid'); + if (!d || d.condition !== 'COMPLETED') errors.push('dependency condition must be COMPLETED'); + } + if (!Array.isArray(s.waitConditions)) errors.push('waitConditions must be an array'); + else for (const w of s.waitConditions) { + if (!w || typeof w.id !== 'string' || !w.id) errors.push('wait id missing'); + if (!w || !WAIT_KINDS.has(w.kind)) errors.push(`invalid wait kind: ${w && w.kind}`); + if (!w || !WAIT_SCOPES.has(w.scope)) errors.push(`invalid wait scope: ${w && w.scope}`); + if (!w || typeof w.dependency !== 'string' || !w.dependency) errors.push('wait dependency missing'); + if (!w || typeof w.resumeCondition !== 'string' || !w.resumeCondition) errors.push('wait resumeCondition missing'); + } + if (!Array.isArray(s.authorityGates)) errors.push('authorityGates must be an array'); + else for (const g of s.authorityGates) { + if (!g || typeof g.id !== 'string' || !g.id) errors.push('authority gate id missing'); + if (!g || typeof g.action !== 'string' || !g.action) errors.push('authority gate action missing'); + if (!g || !GATE_STATES.has(g.state)) errors.push(`invalid authority gate state: ${g && g.state}`); + } + if (!(s.nextAction === null || typeof s.nextAction === 'string')) errors.push('nextAction must be string or null'); + if (!Array.isArray(s.evidenceRefs) || s.evidenceRefs.some((x) => typeof x !== 'string')) errors.push('evidenceRefs must be a string array'); + if (!DURABILITY.has(s.durability)) errors.push(`invalid durability: ${s.durability}`); + if (typeof s.notesRef !== 'string' || !s.notesRef) errors.push('notesRef missing'); + if (s.runState === 'RUNNING' && (!s.lease || s.lease.status !== 'CLAIMED')) errors.push('RUNNING requires a CLAIMED lease'); + if (s.runState === 'COMPLETED' && s.phase !== 'CLOSED') errors.push('COMPLETED requires phase CLOSED'); + return errors; +} + +function loadPortfolio(root) { + if (!portfolioExists(root)) return null; + const portfolio = readJson(portfolioPath(root)); + const errors = validatePortfolio(portfolio); + if (errors.length) die(`Invalid .ppgp/portfolio.json:\n- ${errors.join('\n- ')}`); + return portfolio; +} + +function resolveRef(root, ref) { return path.resolve(root, ref); } + +function loadWorkstream(root, id, portfolio = loadPortfolio(root)) { + if (!portfolio) die('No PPGP portfolio found.'); + const ref = portfolio.workstreams.find((x) => x.id === id); + if (!ref) die(`Unknown workstream: ${id}`); + const file = resolveRef(root, ref.stateRef); + if (!fs.existsSync(file)) die(`Missing state for workstream ${id}: ${ref.stateRef}`); + const state = readJson(file); + const errors = validateState(state); + if (errors.length) die(`Invalid state for ${id}:\n- ${errors.join('\n- ')}`); + if (state.id !== id) die(`Workstream reference ${id} points to state id ${state.id}.`); + return { ref, file, state }; +} + +function updateState(root, id, mutate, expectedRevision = null) { + return withPortfolioLock(root, () => { + const portfolio = loadPortfolio(root); + const loaded = loadWorkstream(root, id, portfolio); + if (expectedRevision !== null && loaded.state.revision !== expectedRevision) { + die(`Revision conflict for ${id}: expected ${expectedRevision}, canonical is ${loaded.state.revision}. Reload before writing.`); + } + const next = JSON.parse(JSON.stringify(loaded.state)); + mutate(next); + next.revision = loaded.state.revision + 1; + const errors = validateState(next); + if (errors.length) die(`Refusing invalid update for ${id}:\n- ${errors.join('\n- ')}`); + writeJsonAtomic(loaded.file, next); + return next; + }); +} + +function makePortfolio() { + return { schemaVersion: '0.2', id: 'default', revision: 0, aggregationPolicy: 'any-runnable', workstreams: [] }; +} + +function makeState(id, title, branch = null) { + return { + schemaVersion: '0.2', id, title, revision: 0, phase: 'THINK', runState: 'RUNNABLE', branch, + lease: null, dependencies: [], waitConditions: [], authorityGates: [], nextAction: 'Complete THINK and freeze the first executable plan.', + evidenceRefs: [], durability: 'HOST_DURABLE', notesRef: `.ppgp/workstreams/${id}/notes.md` + }; +} + +function notesTemplate(id, title) { + return `# WORKSTREAM ${id}: ${title}\n\n## WHY\nTODO: Why this workstream matters.\n\n## DEFINITION_OF_DONE\n- TODO: Define verifiable completion conditions.\n\n## FROZEN_DECISIONS\n- None yet.\n\n## INVARIANTS\n- None recorded yet.\n\n## VERIFIED_PROGRESS\n- Nothing yet.\n\nMachine scheduling state is canonical in \`state.json\`. Do not duplicate RUN_STATE, lease generation, dependencies, waits, or authority-gate state here.\n`; +} + +function dependencyGraph(root, portfolio) { + const states = new Map(); + for (const ref of portfolio.workstreams) states.set(ref.id, loadWorkstream(root, ref.id, portfolio).state); + return states; +} + +function cycleErrors(states) { + const errors = []; + const visiting = new Set(); + const visited = new Set(); + function visit(id, trail) { + if (visiting.has(id)) { errors.push(`dependency cycle: ${[...trail, id].join(' -> ')}`); return; } + if (visited.has(id)) return; + visiting.add(id); + const state = states.get(id); + if (state) for (const dep of state.dependencies) { + if (!states.has(dep.workstream)) errors.push(`${id} depends on unknown workstream ${dep.workstream}`); + else visit(dep.workstream, [...trail, id]); + } + visiting.delete(id); + visited.add(id); + } + for (const id of states.keys()) visit(id, []); + return [...new Set(errors)]; } +function dependencySatisfied(state, states) { + return state.dependencies.every((d) => states.has(d.workstream) && states.get(d.workstream).runState === 'COMPLETED'); +} + +function aggregatePortfolio(states) { + const list = [...states.values()]; + if (list.length === 0) return 'IDLE'; + if (list.every((s) => s.runState === 'COMPLETED')) return 'COMPLETED'; + const eligible = list.filter((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED' && dependencySatisfied(s, states)); + if (eligible.some((s) => s.runState === 'RUNNING')) return 'RUNNING'; + if (eligible.some((s) => s.runState === 'RUNNABLE')) return 'RUNNABLE'; + if (eligible.some((s) => s.runState === 'RECOVERY_REQUIRED')) return 'RECOVERY_REQUIRED'; + if (list.some((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED')) return 'WAITING'; + return 'IDLE'; +} + +function checkoutRegistryPath(root) { + const common = gitCommonDir(root); + return common ? path.join(common, 'ppgp-checkouts.json') : null; +} + +function loadCheckoutRegistry(root) { + const file = checkoutRegistryPath(root); + if (!file) return { file: null, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + if (!fs.existsSync(file)) return { file, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + const data = readJson(file); + if (!data || data.schemaVersion !== '0.2-local' || !Array.isArray(data.claims)) die(`Invalid local checkout registry: ${file}`); + return { file, data }; +} + +function saveCheckoutRegistry(file, data) { + if (!file) die('Checkout claims require Git so they can live in the local Git common directory.'); + data.revision = (Number.isInteger(data.revision) ? data.revision : 0) + 1; + writeJsonAtomic(file, data); +} + +function sha256(content) { return crypto.createHash('sha256').update(content).digest('hex'); } + function cmdInit(root) { printMap(root); - console.log('\nInitialization is non-destructive. Existing documentation is reused; no empty memory files are created.'); + console.log('\nInitialization is non-destructive. Existing documentation is reused; no empty memory or portfolio files are created.'); } function cmdDoctor(root) { - const roles = printMap(root); + printMap(root); const issues = []; if (!hasGit(root)) issues.push('Git forensic history was not detected.'); - if (!roles.ACTIVE_GOAL) issues.push('No ACTIVE_GOAL is present. This is normal when no substantial goal is active.'); + if (!portfolioExists(root)) { + if (!findRole(root, 'ACTIVE_GOAL')) issues.push('No ACTIVE_GOAL is present. This is normal when no substantial goal is active.'); + } else { + const portfolio = loadPortfolio(root); + const states = dependencyGraph(root, portfolio); + issues.push(...cycleErrors(states)); + for (const [id, state] of states) { + if (state.lease && state.lease.expiresAt && Date.parse(state.lease.expiresAt) <= Date.now() && state.lease.status === 'CLAIMED') issues.push(`${id} has an expired CLAIMED lease.`); + } + const { data } = loadCheckoutRegistry(root); + for (const claim of data.claims) { + if (!states.has(claim.workstreamId)) issues.push(`checkout claim ${claim.path} references unknown workstream ${claim.workstreamId}`); + if (fs.existsSync(claim.path) && states.has(claim.workstreamId)) { + const actual = gitBranch(claim.path); + const declared = states.get(claim.workstreamId).branch; + if (declared && actual && declared !== actual) issues.push(`${claim.workstreamId} checkout branch mismatch: declared ${declared}, actual ${actual}`); + } + } + } console.log(issues.length ? `\nNotes:\n- ${issues.join('\n- ')}` : '\nNo obvious repository-level PPGP issues detected.'); } function cmdGoal(root, positional, force) { + if (portfolioExists(root)) die('A v0.2 portfolio exists. Use `ppgp workstream start ` instead of replacing an ambiguous active goal.'); const outcome = positional.join(' ').trim(); if (!outcome) die('goal requires an outcome, for example: ppgp goal "Ship the authentication migration"'); const target = activeGoalPath(root); @@ -131,7 +391,7 @@ function cmdGoal(root, positional, force) { console.log('Next: verify current state, define the Definition of Done, then freeze the executable plan.'); } -function cmdStatus(root) { +function printLegacyStatus(root) { const { file, sections } = readActiveGoal(root); console.log(`PPGP/${pkg.version} status from ${file}`); console.log(`goal: ${oneLine(sections.GOAL)}`); @@ -145,22 +405,246 @@ function cmdStatus(root) { console.log(`evidence: ${oneLine(sections.VERIFICATION_EVIDENCE)}`); } +function printWorkstream(state) { + console.log(`${state.id}: ${state.title}`); + console.log(` revision: ${state.revision}`); + console.log(` phase: ${state.phase}`); + console.log(` run_state: ${state.runState}`); + console.log(` branch: ${state.branch || '(none)'}`); + console.log(` lease: ${state.lease ? `${state.lease.holder}/${state.lease.status}/g${state.lease.generation}` : '(none)'}`); + console.log(` dependencies: ${state.dependencies.length ? state.dependencies.map((d) => `${d.workstream}:${d.condition}`).join(', ') : '(none)'}`); + console.log(` waits: ${state.waitConditions.length ? state.waitConditions.map((w) => `${w.kind}/${w.scope}:${w.id}`).join(', ') : '(none)'}`); + console.log(` authority: ${state.authorityGates.length ? state.authorityGates.map((g) => `${g.id}:${g.state}`).join(', ') : '(none)'}`); + console.log(` durability: ${state.durability}`); + console.log(` next: ${state.nextAction || '(none)'}`); +} + +function cmdStatus(root, all) { + const portfolio = loadPortfolio(root); + if (!portfolio) return printLegacyStatus(root); + const states = dependencyGraph(root, portfolio); + const cycles = cycleErrors(states); + if (cycles.length) die(cycles.join('; ')); + console.log(`PPGP/${pkg.version} portfolio ${portfolio.id} revision ${portfolio.revision}`); + console.log(`portfolio_state: ${aggregatePortfolio(states)}`); + const eligible = [...states.values()].filter((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED' && dependencySatisfied(s, states)); + console.log(`runnable: ${eligible.filter((s) => s.runState === 'RUNNABLE').map((s) => s.id).join(', ') || '(none)'}`); + console.log(`running: ${eligible.filter((s) => s.runState === 'RUNNING').map((s) => s.id).join(', ') || '(none)'}`); + console.log(`recovery_required: ${eligible.filter((s) => s.runState === 'RECOVERY_REQUIRED').map((s) => s.id).join(', ') || '(none)'}`); + if (all) for (const state of states.values()) { console.log(''); printWorkstream(state); } +} + function cmdHandoff(root) { - const { sections } = readActiveGoal(root); + const portfolio = loadPortfolio(root); + if (!portfolio) { + const { sections } = readActiveGoal(root); + console.log(`PPGP/${pkg.version}`); + console.log(`G=${oneLine(sections.GOAL)}`); + console.log(`P=${oneLine(sections.PHASE)}`); + console.log(`F:${oneLine(sections.FROZEN_DECISIONS)}`); + console.log(`D:${oneLine(sections.COMPLETED)}`); + console.log(`B:${oneLine(sections.BLOCKERS)}`); + console.log(`E:${oneLine(sections.VERIFICATION_EVIDENCE)}`); + console.log(`N:${oneLine(sections.NEXT_EXECUTABLE_ACTION)}`); + return; + } + if (portfolio.workstreams.length !== 1) die('Portfolio handoff is ambiguous. Use `ppgp workstream handoff <id> <new-holder>`.'); + const state = loadWorkstream(root, portfolio.workstreams[0].id, portfolio).state; console.log(`PPGP/${pkg.version}`); - console.log(`G=${oneLine(sections.GOAL)}`); - console.log(`P=${oneLine(sections.PHASE)}`); - console.log(`F:${oneLine(sections.FROZEN_DECISIONS)}`); - console.log(`D:${oneLine(sections.COMPLETED)}`); - console.log(`B:${oneLine(sections.BLOCKERS)}`); - console.log(`E:${oneLine(sections.VERIFICATION_EVIDENCE)}`); - console.log(`N:${oneLine(sections.NEXT_EXECUTABLE_ACTION)}`); + console.log(`WS=${state.id}`); + console.log(`P=${state.phase}`); + console.log(`R=${state.runState}`); + console.log(`L=${state.lease ? `${state.lease.holder}/${state.lease.status}/g${state.lease.generation}` : '(none)'}`); + console.log(`W=${state.waitConditions.map((w) => `${w.kind}/${w.scope}:${w.id}`).join(',') || '(none)'}`); + console.log(`U=${state.durability}`); + console.log(`N=${state.nextAction || '(none)'}`); } -function cmdSkillPath() { - console.log(path.join(path.resolve(__dirname, '..'), 'skills', 'ppgp')); +function cmdMigrate(root, rollback) { + if (rollback) { + const metaFile = path.join(ppgpDir(root), 'MIGRATION.json'); + if (!fs.existsSync(metaFile)) die('No reversible PPGP migration metadata found.'); + const meta = readJson(metaFile); + const source = path.resolve(root, meta.source); + if (!fs.existsSync(source)) die(`Legacy source missing: ${meta.source}`); + if (sha256(fs.readFileSync(source)) !== meta.sourceSha256) die('Legacy source changed after migration; refusing automatic rollback.'); + fs.rmSync(ppgpDir(root), { recursive: true, force: true }); + console.log('Rolled back v0.2 reference state. Legacy ACTIVE_GOAL is canonical again.'); + return; + } + if (portfolioExists(root)) die('Portfolio already exists; migration is unnecessary.'); + const legacy = readActiveGoal(root); + const id = 'legacy'; + withPortfolioLock(root, () => { + const portfolio = makePortfolio(); + portfolio.workstreams.push({ id, stateRef: `.ppgp/workstreams/${id}/state.json`, notesRef: `.ppgp/workstreams/${id}/notes.md` }); + const phaseCandidate = oneLine(legacy.sections.PHASE, 'THINK').toUpperCase(); + const phase = PHASES.has(phaseCandidate) ? phaseCandidate : 'THINK'; + const state = makeState(id, oneLine(legacy.sections.GOAL, 'Migrated active goal'), gitBranch(root)); + state.phase = phase; + state.runState = phase === 'CLOSED' ? 'COMPLETED' : 'RUNNABLE'; + state.nextAction = oneLine(legacy.sections.NEXT_EXECUTABLE_ACTION, null); + state.evidenceRefs = legacy.sections.VERIFICATION_EVIDENCE && !/^[-*]?\s*None/i.test(legacy.sections.VERIFICATION_EVIDENCE) ? [oneLine(legacy.sections.VERIFICATION_EVIDENCE)] : []; + state.durability = 'HOST_DURABLE'; + fs.mkdirSync(workstreamDir(root, id), { recursive: true }); + fs.writeFileSync(workstreamNotesPath(root, id), legacy.content, 'utf8'); + writeJsonAtomic(workstreamStatePath(root, id), state); + writeJsonAtomic(portfolioPath(root), portfolio); + writeJsonAtomic(path.join(ppgpDir(root), 'MIGRATION.json'), { + schemaVersion: '0.2', source: legacy.file, sourceSha256: sha256(Buffer.from(legacy.content)), + cutoverAt: new Date().toISOString(), mode: 'CUTOVER', legacyRole: 'COMPATIBILITY_SNAPSHOT' + }); + }); + console.log('Migrated legacy ACTIVE_GOAL into the v0.2 reference portfolio.'); + console.log('The legacy file remains untouched as a compatibility snapshot; .ppgp state is now canonical.'); + console.log('Rollback while the legacy snapshot is unchanged: ppgp migrate --rollback'); } +function cmdWorkstream(root, positional, options) { + const action = positional.shift(); + if (!action) die('workstream requires an action: start, status, park, resume, handoff, recover, close'); + if (action === 'start') { + const id = positional.shift(); + const title = positional.join(' ').trim(); + if (!ID_RE.test(id || '')) die('workstream start requires a safe id (letters, numbers, dot, underscore, dash).'); + if (!title) die('workstream start requires a title.'); + if (!portfolioExists(root) && findRole(root, 'ACTIVE_GOAL')) die('Legacy ACTIVE_GOAL exists. Run `ppgp migrate` before adding concurrent workstreams.'); + withPortfolioLock(root, () => { + const portfolio = portfolioExists(root) ? loadPortfolio(root) : makePortfolio(); + if (portfolio.workstreams.some((x) => x.id === id)) die(`Workstream already exists: ${id}`); + const state = makeState(id, title, options.branch || gitBranch(root)); + fs.mkdirSync(workstreamDir(root, id), { recursive: true }); + fs.writeFileSync(workstreamNotesPath(root, id), notesTemplate(id, title), 'utf8'); + writeJsonAtomic(workstreamStatePath(root, id), state); + portfolio.workstreams.push({ id, stateRef: `.ppgp/workstreams/${id}/state.json`, notesRef: `.ppgp/workstreams/${id}/notes.md` }); + portfolio.revision += 1; + writeJsonAtomic(portfolioPath(root), portfolio); + }); + console.log(`Started workstream ${id} in THINK/RUNNABLE.`); + return; + } + const id = positional.shift(); + if (!id) die(`workstream ${action} requires a workstream id.`); + if (action === 'status') { printWorkstream(loadWorkstream(root, id).state); return; } + if (action === 'park') { + const next = updateState(root, id, (s) => { s.runState = 'PARKED'; s.lease = null; }, options.revision); + console.log(`Parked ${id} at revision ${next.revision}.`); + return; + } + if (action === 'resume') { + const next = updateState(root, id, (s) => { if (s.runState === 'COMPLETED') die('Completed workstreams cannot be resumed.'); s.runState = 'RUNNABLE'; s.lease = null; }, options.revision); + console.log(`Resumed ${id} as RUNNABLE at revision ${next.revision}.`); + return; + } + if (action === 'handoff' || action === 'recover') { + const holder = positional.shift() || options.holder; + if (!holder) die(`workstream ${action} requires the new holder.`); + if (action === 'recover') verifyRecoveryCheckout(root, id); + const next = updateState(root, id, (s) => { + if (s.runState === 'COMPLETED') die('Completed workstreams cannot receive a lease.'); + const generation = s.lease && Number.isInteger(s.lease.generation) ? s.lease.generation + 1 : 1; + s.lease = { holder, status: 'CLAIMED', generation, claimedAt: new Date().toISOString(), expiresAt: null }; + s.runState = 'RUNNING'; + }, options.revision); + console.log(`${action === 'recover' ? 'Recovered' : 'Handed off'} ${id} to ${holder} at lease generation ${next.lease.generation}, revision ${next.revision}.`); + return; + } + if (action === 'close') { + const next = updateState(root, id, (s) => { + if (s.phase !== 'CLOSED') die('Refusing close: workstream phase must already be CLOSED after verified PPGP closure.'); + s.runState = 'COMPLETED'; + s.lease = null; + s.nextAction = null; + }, options.revision); + releaseCheckoutClaimsFor(root, id); + console.log(`Closed ${id} as COMPLETED at revision ${next.revision}.`); + return; + } + die(`Unknown workstream action: ${action}`); +} + +function verifyRecoveryCheckout(root, id) { + const { data } = loadCheckoutRegistry(root); + const claims = data.claims.filter((c) => c.workstreamId === id); + const state = loadWorkstream(root, id).state; + for (const claim of claims) { + if (!fs.existsSync(claim.path)) die(`Claimed checkout for ${id} no longer exists: ${claim.path}`); + const actual = gitBranch(claim.path); + if (state.branch && actual && actual !== state.branch) die(`Recovery refused: ${id} declared branch ${state.branch}, claimed checkout is ${actual}. Inspect before takeover.`); + } +} + +function releaseCheckoutClaimsFor(root, id) { + const { file, data } = loadCheckoutRegistry(root); + if (!file) return; + const before = data.claims.length; + data.claims = data.claims.filter((c) => c.workstreamId !== id); + if (data.claims.length !== before) saveCheckoutRegistry(file, data); +} + +function cmdCheckout(root, positional) { + const action = positional.shift(); + if (!action) die('checkout requires an action: status, claim, release'); + const { file, data } = loadCheckoutRegistry(root); + if (action === 'status') { + console.log(`PPGP/${pkg.version} local checkout registry`); + console.log(`storage: ${file || '(Git unavailable)'}`); + if (!data.claims.length) console.log('claims: (none)'); + else for (const c of data.claims) console.log(`${c.path} -> ${c.workstreamId} branch=${c.branch || '(none)'} head=${c.head || '(none)'}`); + return; + } + if (!file) die('checkout claims require a Git repository.'); + if (action === 'claim') { + const id = positional.shift(); + const target = path.resolve(positional.shift() || root); + if (!id) die('checkout claim requires a workstream id.'); + const state = loadWorkstream(root, id).state; + if (!state.lease || state.lease.status !== 'CLAIMED' || state.runState !== 'RUNNING') die(`checkout claim requires ${id} to hold a valid CLAIMED execution lease.`); + if (!hasGit(target)) die(`Target is not a Git checkout: ${target}`); + const actualBranch = gitBranch(target); + if (state.branch && actualBranch && state.branch !== actualBranch) die(`Branch mismatch: ${id} declares ${state.branch}, checkout is ${actualBranch}.`); + const existing = data.claims.find((c) => path.resolve(c.path) === target); + if (existing && existing.workstreamId !== id) die(`Checkout already claimed by ${existing.workstreamId}: ${target}`); + const claim = { path: target, workstreamId: id, branch: actualBranch, head: gitHead(target), dirty: gitDirty(target) === true, claimedAt: new Date().toISOString() }; + if (existing) Object.assign(existing, claim); else data.claims.push(claim); + saveCheckoutRegistry(file, data); + console.log(`Claimed ${target} for ${id}.`); + return; + } + if (action === 'release') { + const target = path.resolve(positional.shift() || root); + const before = data.claims.length; + data.claims = data.claims.filter((c) => path.resolve(c.path) !== target); + if (before === data.claims.length) die(`No checkout claim found for ${target}`); + saveCheckoutRegistry(file, data); + console.log(`Released checkout claim for ${target}.`); + return; + } + die(`Unknown checkout action: ${action}`); +} + +function checkoutRegistryPath(root) { + const common = gitCommonDir(root); + return common ? path.join(common, 'ppgp-checkouts.json') : null; +} + +function loadCheckoutRegistry(root) { + const file = checkoutRegistryPath(root); + if (!file) return { file: null, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + if (!fs.existsSync(file)) return { file, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + const data = readJson(file); + if (!data || data.schemaVersion !== '0.2-local' || !Array.isArray(data.claims)) die(`Invalid local checkout registry: ${file}`); + return { file, data }; +} + +function saveCheckoutRegistry(file, data) { + if (!file) die('Checkout claims require Git so they can live in the local Git common directory.'); + data.revision = (Number.isInteger(data.revision) ? data.revision : 0) + 1; + writeJsonAtomic(file, data); +} + +function cmdSkillPath() { console.log(path.join(path.resolve(__dirname, '..'), 'skills', 'ppgp')); } + function cmdInstallSkill(positional) { const destination = positional[0]; if (!destination) die('install-skill requires a destination directory, for example: ppgp install-skill ~/.config/agent-skills'); @@ -174,30 +658,34 @@ function cmdInstallSkill(positional) { } function help() { - console.log(`PPGP ${pkg.version}\nPortable Persistent Goal Protocol CLI\n\nUsage:\n ppgp init [--root PATH]\n ppgp doctor [--root PATH]\n ppgp goal <outcome> [--root PATH] [--force]\n ppgp status [--root PATH]\n ppgp handoff [--root PATH]\n ppgp skill-path\n ppgp install-skill <destination>\n ppgp --version\n\nThe CLI is a deterministic companion to the PPGP protocol. It does not replace agent reasoning, verification, distillation, or closure checks.\n`); + console.log(`PPGP ${pkg.version}\nPortable Persistent Goal Protocol CLI\n\nLegacy/single-workstream:\n ppgp init [--root PATH]\n ppgp doctor [--root PATH]\n ppgp goal <outcome> [--root PATH] [--force]\n ppgp status [--root PATH] [--all]\n ppgp handoff [--root PATH]\n ppgp migrate [--root PATH]\n ppgp migrate --rollback [--root PATH]\n\nPortfolio/workstreams:\n ppgp workstream start <id> <title> [--branch BRANCH]\n ppgp workstream status <id>\n ppgp workstream park <id> [--revision N]\n ppgp workstream resume <id> [--revision N]\n ppgp workstream handoff <id> <new-holder> [--revision N]\n ppgp workstream recover <id> <new-holder> [--revision N]\n ppgp workstream close <id> [--revision N]\n\nLocal checkout claims:\n ppgp checkout status\n ppgp checkout claim <workstream-id> [checkout-path]\n ppgp checkout release [checkout-path]\n\nDistribution:\n ppgp skill-path\n ppgp install-skill <destination>\n ppgp --version\n\nReference JSON state uses local revision checks and a local mutation lock. Checkout claims live in the Git common directory and are not committed.\n`); } -const raw = process.argv.slice(2); -if (raw.length === 0 || raw.includes('--help') || raw.includes('-h')) { - help(); - process.exit(0); -} -if (raw.includes('--version') || raw.includes('-v')) { - console.log(pkg.version); - process.exit(0); -} +function main() { + const raw = process.argv.slice(2); + if (raw.length === 0 || raw.includes('--help') || raw.includes('-h')) { help(); return; } + if (raw.includes('--version') || raw.includes('-v')) { console.log(pkg.version); return; } + const command = raw.shift(); + const { options, positional } = parseArgs(raw); + if (!fs.existsSync(options.root) || !fs.statSync(options.root).isDirectory()) die(`Root is not a directory: ${options.root}`); -const command = raw.shift(); -const { options, positional } = parseArgs(raw); -if (!fs.existsSync(options.root) || !fs.statSync(options.root).isDirectory()) die(`Root is not a directory: ${options.root}`); + switch (command) { + case 'init': cmdInit(options.root); break; + case 'doctor': cmdDoctor(options.root); break; + case 'goal': cmdGoal(options.root, positional, options.force); break; + case 'status': cmdStatus(options.root, options.all); break; + case 'handoff': cmdHandoff(options.root); break; + case 'migrate': cmdMigrate(options.root, options.rollback); break; + case 'workstream': cmdWorkstream(options.root, positional, options); break; + case 'checkout': cmdCheckout(options.root, positional); break; + case 'skill-path': cmdSkillPath(); break; + case 'install-skill': cmdInstallSkill(positional); break; + default: die(`Unknown command: ${command}. Run ppgp --help.`); + } +} -switch (command) { - case 'init': cmdInit(options.root); break; - case 'doctor': cmdDoctor(options.root); break; - case 'goal': cmdGoal(options.root, positional, options.force); break; - case 'status': cmdStatus(options.root); break; - case 'handoff': cmdHandoff(options.root); break; - case 'skill-path': cmdSkillPath(); break; - case 'install-skill': cmdInstallSkill(positional); break; - default: die(`Unknown command: ${command}. Run ppgp --help.`); +try { main(); } +catch (error) { + if (error instanceof PPGPError) { console.error(`PPGP: ${error.message}`); process.exitCode = error.code; } + else throw error; } From 7268970d8672e1498a4bad388827bec69eb89a79 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:36:52 +0200 Subject: [PATCH 15/47] test: add PPGP v0.2 coordination conformance cases --- test/v020.test.js | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/v020.test.js diff --git a/test/v020.test.js b/test/v020.test.js new file mode 100644 index 0000000..f2451d7 --- /dev/null +++ b/test/v020.test.js @@ -0,0 +1,113 @@ +'use strict'; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const repo = path.resolve(__dirname, '..'); +const cli = path.join(repo, 'bin', 'ppgp.js'); +function assert(x, m) { if (!x) throw new Error(m); } +function run(args, ok = true) { + const r = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }); + if (ok && r.status !== 0) throw new Error(`FAILED ${args.join(' ')}\n${r.stdout}\n${r.stderr}`); + if (!ok && r.status === 0) throw new Error(`EXPECTED FAILURE ${args.join(' ')}\n${r.stdout}`); + return r; +} +function git(root, args) { + const r = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); + if (r.status !== 0) throw new Error(r.stderr); + return r.stdout.trim(); +} +function makeRepo() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ppgp-v020-')); + git(root, ['init', '-q']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'Test']); + fs.writeFileSync(path.join(root, 'seed.txt'), 'seed\n'); + git(root, ['add', '.']); + git(root, ['commit', '-qm', 'seed']); + return root; +} +function j(root, rel) { return JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8')); } +function w(root, rel, obj) { fs.writeFileSync(path.join(root, rel), `${JSON.stringify(obj, null, 2)}\n`); } + +const roots = []; +try { + const legacy = makeRepo(); roots.push(legacy); + run(['goal', 'Legacy', 'goal', '--root', legacy]); + assert(run(['status', '--root', legacy]).stdout.includes('status from'), 'legacy status missing'); + const original = fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8'); + run(['migrate', '--root', legacy]); + assert(fs.existsSync(path.join(legacy, '.ppgp', 'portfolio.json')), 'portfolio missing after migrate'); + assert(fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8') === original, 'migration mutated legacy source'); + assert(run(['status', '--all', '--root', legacy]).stdout.includes('portfolio_state: RUNNABLE'), 'migrated portfolio not runnable'); + run(['migrate', '--rollback', '--root', legacy]); + assert(!fs.existsSync(path.join(legacy, '.ppgp')), 'rollback left .ppgp'); + assert(fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8') === original, 'rollback mutated legacy source'); + + const root = makeRepo(); roots.push(root); + run(['workstream', 'start', 'a', 'Workstream A', '--root', root, '--branch', 'master']); + run(['workstream', 'start', 'b', 'Workstream B', '--root', root, '--branch', 'master']); + let portfolio = j(root, '.ppgp/portfolio.json'); + assert(portfolio.workstreams.length === 2 && portfolio.revision === 2, 'portfolio revision mismatch'); + + run(['workstream', 'handoff', 'a', 'codex', '--root', root, '--revision', '0']); + let a = j(root, '.ppgp/workstreams/a/state.json'); + assert(a.runState === 'RUNNING' && a.lease.generation === 1, 'first lease incorrect'); + const conflict = run(['workstream', 'park', 'a', '--root', root, '--revision', '0'], false); + assert(conflict.stderr.includes('Revision conflict'), 'stale revision was not rejected'); + assert(!fs.existsSync(path.join(root, '.ppgp', '.lock')), 'CAS failure left a stale lock'); + + run(['checkout', 'claim', 'a', root, '--root', root]); + assert(fs.existsSync(path.join(root, '.git', 'ppgp-checkouts.json')), 'checkout registry not stored in git common dir'); + run(['workstream', 'handoff', 'b', 'claude', '--root', root]); + const collision = run(['checkout', 'claim', 'b', root, '--root', root], false); + assert(collision.stderr.includes('already claimed by a'), 'checkout collision not rejected'); + + run(['workstream', 'handoff', 'a', 'claude', '--root', root, '--revision', '1']); + a = j(root, '.ppgp/workstreams/a/state.json'); + assert(a.lease.generation === 2 && a.lease.holder === 'claude', 'lease generation did not increment'); + + a.runState = 'WAITING'; a.lease = null; a.revision += 1; + a.waitConditions = [ + { id: 'ssh', kind: 'EXTERNAL', scope: 'ACTION', dependency: 'SSH coordinates', resumeCondition: 'coordinates available' }, + { id: 'spend', kind: 'AUTHORITY', scope: 'ACTION', dependency: 'paid compute approval', resumeCondition: 'authority granted' } + ]; + w(root, '.ppgp/workstreams/a/state.json', a); + let status = run(['status', '--all', '--root', root]).stdout; + assert(status.includes('portfolio_state: RUNNING'), 'independent running workstream should keep portfolio running'); + assert(status.includes('EXTERNAL/ACTION:ssh') && status.includes('AUTHORITY/ACTION:spend'), 'mixed waits were collapsed or lost'); + + let b = j(root, '.ppgp/workstreams/b/state.json'); + b.runState = 'RUNNABLE'; b.lease = null; b.dependencies = [{ workstream: 'a', condition: 'COMPLETED' }]; b.revision += 1; + w(root, '.ppgp/workstreams/b/state.json', b); + status = run(['status', '--all', '--root', root]).stdout; + assert(status.includes('portfolio_state: WAITING'), 'unsatisfied dependency plus waiting prerequisite should aggregate WAITING'); + + a.dependencies = [{ workstream: 'b', condition: 'COMPLETED' }]; a.revision += 1; + w(root, '.ppgp/workstreams/a/state.json', a); + const doctorCycle = run(['doctor', '--root', root]); + assert(doctorCycle.stdout.includes('dependency cycle'), 'doctor did not report dependency cycle'); + a.dependencies = []; a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + + a.runState = 'RECOVERY_REQUIRED'; + a.lease = { holder: 'codex', status: 'RECOVERY_REQUIRED', generation: 2, claimedAt: new Date().toISOString(), expiresAt: null }; + a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + run(['workstream', 'recover', 'a', 'claude', '--root', root, '--revision', String(a.revision)]); + a = j(root, '.ppgp/workstreams/a/state.json'); + assert(a.runState === 'RUNNING' && a.lease.generation === 3 && a.lease.holder === 'claude', 'recover did not fence old generation'); + + const premature = run(['workstream', 'close', 'a', '--root', root], false); + assert(premature.stderr.includes('phase must already be CLOSED'), 'premature close was allowed'); + a = j(root, '.ppgp/workstreams/a/state.json'); a.phase = 'CLOSED'; a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + run(['workstream', 'close', 'a', '--root', root, '--revision', String(a.revision)]); + assert(j(root, '.ppgp/workstreams/a/state.json').runState === 'COMPLETED', 'close did not complete workstream'); + assert(!j(root, '.git/ppgp-checkouts.json').claims.some((c) => c.workstreamId === 'a'), 'close did not release checkout claim'); + + const ambiguous = run(['goal', 'bad', '--root', root], false); + assert(ambiguous.stderr.includes('workstream start'), 'goal did not reject portfolio ambiguity'); + + console.log('PPGP v0.2 portfolio, lease, checkout, migration, recovery, and CAS tests passed.'); +} finally { + for (const root of roots) fs.rmSync(root, { recursive: true, force: true }); +} From c60754b595ccf618eb757db5763ad0bab7fcc669 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:37:05 +0200 Subject: [PATCH 16/47] test: wire v0.2 coordination suite into npm test --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a25dea..014cc85 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "files": [ "bin/", "skills/", + "schemas/", "scripts/benchmark-report.js", "scripts/prepare-pilot-01.js", "benchmarks/", @@ -43,7 +44,7 @@ "node": ">=18" }, "scripts": { - "test": "node test/cli.test.js && node test/benchmark.test.js && node test/package.test.js", + "test": "node test/cli.test.js && node test/v020.test.js && node test/benchmark.test.js && node test/package.test.js", "benchmark:report": "node scripts/benchmark-report.js", "benchmark:prepare:pilot-01": "node scripts/prepare-pilot-01.js", "prepublishOnly": "npm test" From e83a8bee45c4eb536279adc8c156924c6328efbe Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:38:17 +0200 Subject: [PATCH 17/47] docs(spec): define PPGP v0.2.0 portfolio coordination model --- SPEC.md | 748 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 612 insertions(+), 136 deletions(-) diff --git a/SPEC.md b/SPEC.md index 0734a72..3316a28 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,25 +1,39 @@ -# PPGP Specification v0.1.2 +# PPGP Specification v0.2.0 Status: Experimental / Provisional First published: 2026-08-24 -Current release: 2026-08-26 +Target release: 2026-08-30 Protocol: Portable Persistent Goal Protocol (PPGP) ## 1. Scope -PPGP defines a portable continuity protocol for long-running coding-agent work. +PPGP defines a portable continuity and coordination protocol for long-running coding-agent work. -A conforming implementation SHOULD allow a fresh compatible agent to recover an active software goal without requiring the human operator to reconstruct the previous conversation. +A conforming implementation SHOULD allow a fresh compatible agent to recover an unfinished software goal without requiring the human operator to reconstruct the previous conversation. + +When several active workstreams, executors, branches, worktrees, dependencies, or partial wait conditions coexist, a conforming implementation SHOULD additionally allow an agent to determine what work is safe and useful to execute next without disturbing foreign unfinished work. PPGP is model-vendor neutral and repository-oriented. +The portable protocol core defines logical roles and semantics. It does not require the reference JSON layout, the reference CLI, Git worktrees, MCP, a hosted service, a database, or a particular model provider. + +Core invariants include: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT + +executor unavailable != workstream blocked +blocked action != blocked workstream +blocked workstream != blocked project +``` + ## 2. Normative language The terms MUST, MUST NOT, SHOULD, SHOULD NOT and MAY describe protocol requirements and recommendations. -## 3. Logical memory roles +## 3. Logical memory and coordination roles -PPGP defines logical roles, not mandatory filenames. +PPGP defines logical roles, not mandatory filenames or storage formats. ### 3.1 CONSTITUTION @@ -43,11 +57,13 @@ A MEMORY item SHOULD change future behavior. ### 3.4 ACTIVE_GOAL -Temporary working memory for exactly one active goal. +Temporary working memory for one substantial active goal. + +A single-workstream repository MAY continue to use one ACTIVE_GOAL exactly as in PPGP v0.1.x. It SHOULD remain compact enough for a fresh agent to recover the goal in one read. -Minimum fields: +Minimum logical information remains: ```text GOAL @@ -65,27 +81,145 @@ VERIFICATION_EVIDENCE NEXT_EXECUTABLE_ACTION ``` -ACTIVE_GOAL is the primary repository-visible hot state for recovery of unfinished work. +ACTIVE_GOAL MUST NOT become the permanent chronological history. -A fresh agent SHOULD be able to recover an active goal from current ACTIVE_GOAL plus selectively relevant durable state and evidence without requiring a prior `distill` operation. +ACTIVE_GOAL MUST be removed after successful closure and distillation when it is the active temporary representation. -Implementations SHOULD update ACTIVE_GOAL after material state changes often enough that abrupt interruption does not force substantial human reconstruction or unnecessary repetition of verified work. +### 3.5 PORTFOLIO -PPGP does not require persistence after every trivial action. Checkpoint frequency is implementation-dependent and SHOULD balance recovery fidelity against state-maintenance overhead. +PORTFOLIO is an optional project-level coordination view of multiple active workstreams. -ACTIVE_GOAL MUST NOT become the permanent chronological history. +It exists only when concurrent or otherwise ambiguous active work makes project-level scheduling necessary. + +PORTFOLIO MUST NOT duplicate detailed goal reasoning, frozen decisions, Definition of Done, or execution chronology already owned by workstream/goal state. + +A project with one unambiguous active goal MAY omit an explicit PORTFOLIO. + +### 3.6 WORKSTREAM + +A WORKSTREAM is an independently schedulable unit of useful work inside a project. + +A workstream MAY correspond to: + +- a complete goal; +- a bounded sub-goal; +- a parallel implementation track; +- an independent review or verification track; +- an infrastructure track whose execution can be scheduled independently. + +A workstream SHOULD have one canonical state representation. + +### 3.7 EXECUTION_LEASE + +An EXECUTION_LEASE identifies the executor/session currently entitled to mutate one workstream. + +A lease coordinates execution. It does not grant product, legal, financial, production, credential, or other human authority. + +A lease SHOULD carry a monotonically increasing generation or equivalent fencing value when takeovers or concurrent writers are possible. + +A stale lease generation MUST NOT overwrite a newer canonical generation. + +Lease expiration or executor disappearance MUST NOT authorize destructive cleanup of dirty work. + +### 3.8 CHECKOUT_CLAIM + +A CHECKOUT_CLAIM identifies a mutable repository workspace assigned to a workstream. + +Examples include a primary checkout, Git worktree, isolated clone, or equivalent VCS workspace. + +Writable checkout claims SHOULD be exclusive by default. + +Read-only inspection MAY be shared. + +Two workstreams MUST NOT simultaneously assume exclusive write ownership of the same mutable checkout unless the repository explicitly provides a safe concurrent-editing mechanism. + +Checkout claims SHOULD remain local/runtime coordination state rather than durable project memory unless the implementation has a specific reason to persist them. + +### 3.9 WAIT_CONDITION + +A WAIT_CONDITION records a condition preventing a specific action or broader unit from proceeding. + +Every material wait SHOULD identify: + +```text +kind +scope +dependency +resume_condition +``` + +Recommended kinds: + +```text +EXTERNAL +AUTHORITY +TECHNICAL +``` + +Recommended scopes: + +```text +ACTION +WORKSTREAM +GOAL +PROJECT +``` + +The narrowest defensible scope SHOULD be used. + +### 3.10 AUTHORITY_GATE + +An AUTHORITY_GATE binds human authority to a specific action. + +Recommended states: + +```text +REQUIRED +GRANTED +CONSUMED +REVOKED +``` + +An agent MUST NOT grant its own authority gate. + +A granted gate authorizes only the action or scope it explicitly names. + +Credentials and secrets MUST NOT be stored merely to represent an authority gate. + +### 3.11 DURABILITY + +DURABILITY describes how recoverable unfinished work is if the current executor disappears. + +Recommended levels: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +SESSION_ONLY means material state exists only in volatile agent/session context. + +HOST_DURABLE means the work survives the agent session on the current host, for example as identified dirty files in a worktree. + +REPO_DURABLE means a local version-control object or equivalent repository recovery artifact exists. -ACTIVE_GOAL MUST be removed after successful closure and distillation. +REMOTE_DURABLE means the recovery artifact survives loss of the current host. -### 3.5 GIT / FORENSIC HISTORY +PPGP does not require every intermediate change to be REMOTE_DURABLE. The purpose is to make recovery risk explicit. -Git or the repository's equivalent history is the forensic record of what actually changed. +### 3.12 GIT / FORENSIC HISTORY -PPGP memory SHOULD preserve meaning and current state rather than duplicating Git chronology. +Git or the repository's equivalent history remains the forensic record of what actually changed. + +PPGP memory SHOULD preserve meaning and current state rather than duplicate chronology. + +Git SHOULD NOT be treated as a high-frequency distributed lease server by the portable protocol core. ## 4. Goal lifecycle -A substantial PPGP goal follows: +The PPGP lifecycle remains: ```text THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED @@ -105,7 +239,7 @@ Replanning is justified when new evidence materially invalidates a frozen assump ### EXECUTE -Perform the work autonomously within the frozen strategy and delegated authority. +Perform the work autonomously within frozen strategy and delegated authority. ### HARDEN @@ -115,9 +249,9 @@ HARDEN improves the selected solution. It is not a default invitation to redesig ### SHIP -Verify the implementation in the environment required by the Definition of Done. +Verify implementation in the environment required by Definition of Done. -When production behavior is part of the Definition of Done, local success alone MUST NOT close the goal. +When production behavior is part of Definition of Done, local success alone MUST NOT close the goal. ### DISTILL @@ -125,17 +259,256 @@ Move durable information into ROADMAP, MEMORY or CONSTITUTION as appropriate. Discard temporary chronology and redundant execution detail. -DISTILL is a consolidation and garbage-collection phase. It is not the primary survival mechanism for an unfinished goal. - -If a session is interrupted before DISTILL, current ACTIVE_GOAL state SHOULD still be sufficient to recover the active goal when combined with relevant repository evidence. +DISTILL is consolidation and garbage collection. It is not the primary survival mechanism for unfinished work. ### CLOSED A goal is CLOSED only after synchronous Definition-of-Done requirements are verified and temporary working memory has been garbage-collected. -## 5. Inner execution loop +## 5. Workstream execution state + +Lifecycle PHASE and execution RUN_STATE are orthogonal. + +PHASE answers: + +> What kind of work is this workstream doing? + +RUN_STATE answers: + +> Can useful execution proceed now, and is an executor currently responsible for it? + +Recommended RUN_STATE values are: + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +### RUNNABLE + +At least one safe useful action can execute now and no current execution lease is actively performing it. + +### RUNNING + +A valid lease holder is actively executing the workstream. + +RUNNING SHOULD require a current execution lease or equivalent ownership proof. + +### WAITING + +No safe useful action can currently advance the workstream because one or more recorded waits or unsatisfied dependencies prevent progress. + +WAITING is intentionally neutral. Typed wait conditions retain the actual reasons. + +Implementations MAY display derived convenience labels such as `WAITING_EXTERNAL`, but those labels MUST NOT erase simultaneous wait kinds. + +### RECOVERY_REQUIRED + +The previous executor became unavailable, ownership became ambiguous, or unfinished mutable state must be inspected before ordinary execution resumes. + +RECOVERY_REQUIRED is a recoverable safety state, not a declaration that the goal has failed. + +### PARKED + +The workstream is intentionally deferred despite potentially being runnable. + +### COMPLETED + +The workstream has satisfied its completion contract. + +A parent goal or portfolio may remain open. + +## 6. Scoped waits and blockers + +A wait MUST NOT be promoted to a broader scope merely because one action cannot proceed. + +Before marking an entire workstream WAITING, an agent SHOULD determine whether any safe useful independent work remains. + +If independent work remains, the workstream SHOULD remain RUNNABLE or RUNNING and the wait SHOULD stay scoped to the blocked action or dependency. + +The rule applies recursively: + +```text +blocked action != blocked workstream +blocked workstream != blocked goal +blocked goal != blocked project +``` + +PPGP v0.1.x blocker classes remain valid: + +```text +A agent-solvable +B external asynchronous +C authority boundary +D hard dependency +``` + +Type and scope are separate concepts. + +Routine technical uncertainty MUST NOT be promoted to an authority gate merely to avoid responsibility. + +## 7. Authority gates + +An authority requirement SHOULD be tied to the exact action requiring authority. + +Example: + +```text +action=create_paid_compute +state=GRANTED +``` + +This does not authorize unrelated spending, production mutation, or architectural change. + +A gate MAY remain granted while another unrelated wait condition is unsatisfied. + +A consumed or revoked gate MUST NOT be reused as current authority. + +## 8. Dependencies + +Workstream dependencies MUST be explicit when they affect scheduling. -Inside a goal, implementations SHOULD use: +A minimal dependency expresses: + +```text +workstream=<id> +condition=COMPLETED +``` + +An implementation MUST NOT infer dependency solely from: + +- branch names; +- checkout location; +- executor identity; +- document order; +- conversation order; +- historical accident. + +Dependency graphs MUST be acyclic. + +A cycle is invalid coordination state. + +An unsatisfied dependency blocks only its dependent workstream unless broader evidence justifies a wider scope. + +Complex conditional dependency expressions are outside the v0.2 portable core. + +## 9. Portfolio runnability + +PORTFOLIO runnability is derived from canonical workstream state and dependencies. + +A useful reference aggregation is: + +```text +if any eligible workstream is RUNNING: + portfolio = RUNNING +else if any eligible workstream is RUNNABLE: + portfolio = RUNNABLE +else if any eligible workstream is RECOVERY_REQUIRED: + portfolio = RECOVERY_REQUIRED +else if unfinished non-parked work remains: + portfolio = WAITING +else if all workstreams are COMPLETED: + portfolio = COMPLETED +else: + portfolio = IDLE +``` + +An implementation MAY use different display labels if equivalent semantics are preserved. + +The normative invariant is: + +```text +A non-runnable workstream MUST NOT imply a non-runnable portfolio. +``` + +## 10. Revision and compare-and-swap semantics + +When multiple writers may update canonical machine state, implementations SHOULD use a revision number, generation, compare-and-swap operation, lock, or equivalent concurrency control. + +A stale revision MUST NOT silently overwrite newer canonical state. + +The reference implementation uses monotonically increasing integer revisions plus a local mutation lock. + +Multi-machine implementations SHOULD provide atomic storage or equivalent compare-and-swap semantics. + +PPGP does not require a specific storage engine. + +## 11. Execution lease semantics + +A reference lease contains information equivalent to: + +```text +holder +generation +status +claimed_at +expires_at +``` + +Recommended lease states include: + +```text +CLAIMED +HANDOFF_READY +RECOVERY_REQUIRED +RELEASED +``` + +Claiming a lease SHOULD make the workstream RUNNING. + +Parking, completion, or a full workstream WAIT SHOULD release active execution ownership unless the implementation has a documented reason to retain it. + +A cooperative handoff SHOULD transfer ownership atomically and increment generation. + +A takeover after interruption SHOULD increment generation or an equivalent fencing value so an older executor can detect that it no longer owns mutation rights. + +Provider cooldown, quota exhaustion, terminal loss, process crash or model-session loss is executor availability, not automatically a GOAL blocker. + +## 12. Checkout safety + +Before mutating a checkout in concurrent or ambiguous conditions, an agent SHOULD inspect, where available: + +```text +current branch +HEAD +working-tree dirtiness +known worktrees/checkouts +checkout claim +execution lease +``` + +If a checkout contains foreign dirty work, an agent MUST NOT by default: + +- switch branches; +- reset; +- clean; +- stash foreign changes; +- commit foreign changes; +- overwrite files; +- repurpose the checkout. + +When reversible and allowed by project policy, the preferred response is safe isolation, such as a separate Git worktree or equivalent workspace. + +Creating such isolation SHOULD normally be treated as an agent-solvable coordination action, not a human authority boundary. + +A useful reference mutation rule is: + +```text +valid execution lease ++ checkout claimed by same workstream ++ actual branch matches declared branch += mutation permitted by the coordination layer +``` + +This rule does not supersede project security, human authority or repository-specific constraints. + +## 13. Inner execution loop + +Inside a goal or workstream, implementations SHOULD use: ```text RETRIEVE -> ACT -> VERIFY -> DELTA @@ -143,7 +516,9 @@ RETRIEVE -> ACT -> VERIFY -> DELTA ### RETRIEVE -Load only the state and evidence relevant to the current decision. +Load only state and evidence relevant to the current decision. + +Concurrent contexts SHOULD additionally retrieve relevant portfolio, lease, checkout, wait and dependency state. ### ACT @@ -153,17 +528,17 @@ Perform the next bounded action. Check observable evidence rather than relying on model confidence. +When checkout ownership matters, observed branch/HEAD/dirtiness SHOULD be checked before mutation. + ### DELTA Record only material state changes needed for continuation. -A DELTA SHOULD update repository-visible hot state when the change would materially affect recovery after interruption. - -The loop repeats until the current phase exit condition is met. +Do not turn every trivial action into a persistent write. -## 6. Boot and recovery +## 14. Boot and recovery -A fresh agent SHOULD start from a minimal boot packet: +A fresh single-workstream agent SHOULD start from a minimal boot packet: ```text GOAL_CONTRACT @@ -172,23 +547,41 @@ GOAL_CONTRACT + RELEVANT_EVIDENCE ``` -The implementation SHOULD avoid loading the complete project history unless required. +For concurrent or ambiguous work, recovery SHOULD additionally answer: -A recovery sequence SHOULD inspect, as relevant: +```text +What workstreams exist? +Which are runnable? +Which executor owns each active lease? +Which mutable checkout belongs to which workstream? +Which waits are local versus global? +What dependencies are unsatisfied? +How durable is unfinished work? +What may I safely execute next? +``` + +If unfinished dirty or uncertain mutable state may exist after executor loss, recovery SHOULD enter or treat the workstream as RECOVERY_REQUIRED before ordinary mutation resumes. + +## 15. Abrupt interruption and takeover + +A cooperative handoff and abrupt takeover are different protocol events. -1. repository agent instructions; -2. ACTIVE_GOAL; -3. selectively relevant durable memory; -4. `git status`; -5. recent relevant commits; -6. verification evidence; -7. NEXT_EXECUTABLE_ACTION. +For RECOVERY_REQUIRED work, a recovery agent SHOULD: -If ACTIVE_GOAL says the strategy is frozen, recovery SHOULD resume execution rather than restart THINK by default. +```text +1. read canonical goal/workstream and portfolio state +2. inspect real workspace branch, HEAD and dirtiness +3. preserve unfinished state exactly as found +4. compare observed state with the last checkpoint +5. classify durability and unresolved uncertainty +6. run proportionate verification when practical +7. record takeover with a new lease generation +8. continue from the smallest verified next action +``` -Abrupt interruption before DISTILL MUST NOT by itself be treated as loss of the active goal if current repository-visible hot state exists. +A recovery agent MUST NOT use destructive workspace normalization merely to obtain a clean status or simplify takeover. -## 7. Evidence precedence +## 16. Evidence precedence When technical claims conflict, implementations SHOULD prefer more direct evidence. @@ -197,121 +590,162 @@ A useful default order is: ```text production/runtime behavior > automated verification -> current repository implementation +> current repository/workspace implementation > Git history -> ACTIVE_GOAL +> current canonical PPGP state > durable MEMORY > ROADMAP > conversation claims > agent recollection ``` -CONSTITUTION remains authoritative for project policy and authority, but technical documentation MUST be corrected when contradicted by observable reality. +CONSTITUTION remains authoritative for project policy and authority. -## 8. Blocker classification +Recorded lease, checkout and dirty-state metadata is last-known coordination evidence. Current observed workspace state takes precedence when the two conflict. -PPGP uses four blocker classes. +## 17. Human interruption policy -### A. Agent-solvable - -Reversible technical or implementation problem. +The default is agent autonomy inside established authority. -Action: solve autonomously. +Human escalation SHOULD be reserved for genuine authority boundaries such as irreversible destructive actions, legal or financial commitments, unavailable credentials or external authorization, genuinely ambiguous product policy, brand/governance authority, material changes to frozen architecture, and actions outside delegated permissions. -### B. External asynchronous +Before escalating one blocked workstream, an agent SHOULD continue unrelated permitted RUNNABLE work when safe. -Propagation, crawler refresh, external processing or another event that may complete later. +Human escalation SHOULD identify the smallest blocked scope and exact decision required. -Action: record it. Do not block synchronous goal closure unless the Definition of Done explicitly requires it. +## 18. Multi-agent policy -### C. Authority boundary +PPGP does not require multiple agents. -Requires human/product/legal/financial/account authority. +A second agent SHOULD be introduced only when expected independent information gain or parallel useful work exceeds communication and coordination cost. -Action: escalate with the smallest decision required. +Concurrent agents SHOULD operate on isolatable or explicitly coordinated workstreams. -### D. Hard dependency +A reviewer SHOULD receive artifact, requirements and relevant facts without unnecessary exposure to implementer self-assessment. -Required information or resource is genuinely unavailable and no safe autonomous path exists. +The existence of PORTFOLIO, leases or checkout claims does not imply that many agents should run simultaneously. -Action: escalate only after autonomous alternatives are exhausted. +## 19. Handoff format -Agents MUST NOT promote routine Type A decisions to Type C solely to avoid responsibility. +Handoffs SHOULD prefer compact structured state or deltas over narrative transcripts. -## 9. Human interruption policy +Single-workstream handoffs MAY retain the v0.1.x compact form. -The default is agent autonomy inside established authority. +Concurrent handoffs SHOULD carry equivalent information to: -Human escalation SHOULD be reserved for matters such as irreversible destructive actions, legal or financial commitments, unavailable credentials or external authorization, genuinely ambiguous product policy, brand or governance authority, material changes to frozen architecture, and actions outside delegated permissions. +```text +WS=<workstream id> +P=<phase> +R=<run state> +L=<lease holder/status/generation> +W=<scoped waits> +U=<durability> +E=<evidence refs> +N=<next action> +``` -Routine debugging, reversible refactors, test failures and ordinary implementation choices SHOULD NOT require human interruption. +Exact encoding is not normative. -## 10. Multi-agent policy +## 20. Distillation and garbage collection -PPGP does not require multiple agents. +Before closing a goal, material temporary state SHOULD be classified: -A second agent SHOULD be introduced only when its expected independent information gain exceeds communication and coordination cost. +```text +strategic authority/invariant -> CONSTITUTION +current/future direction -> ROADMAP +durable reusable lesson -> MEMORY +temporary execution detail -> discard +``` -Useful examples include adversarial review, security review, linguistic review, architecture challenge and independent verification. +At workstream closure: -A reviewer SHOULD receive the artifact, requirements and relevant facts without unnecessary exposure to the implementer's self-assessment. +- live lease state SHOULD be released; +- obsolete checkout claims SHOULD be removed; +- transient executor identity SHOULD NOT become durable memory unless it changes future behavior; +- portfolio coordination SHOULD shrink as workstreams complete. -## 11. Handoff format +Git remains the detailed forensic archive. -Handoffs SHOULD prefer compact structured state or deltas over narrative transcripts. +## 21. Reference implementation -Example: +The official CLI MAY use the following non-normative layout: ```text -PPGP/0.1.2 -G=8 -P=HARDEN +.ppgp/ +├── portfolio.json +└── workstreams/ + └── <id>/ + ├── state.json + └── notes.md +``` + +In this reference representation: + +- `portfolio.json` contains portfolio revision, workstream references and aggregation policy; +- `state.json` contains machine scheduling/coordination truth; +- `notes.md` contains human-readable WHY, Definition of Done, frozen decisions, invariants and progress context; +- scheduling fields SHOULD NOT be duplicated as competing canonical values in Markdown; +- local checkout claims live outside committed project state, preferably in a local VCS/runtime registry. + +JSON is a reference implementation choice because it is deterministic and easy to validate. Other conforming implementations MAY use different representations. -F: -strategy=frozen -seo_ready=page +## 22. Backward compatibility and migration -D: -ja_review=PASS -tests=PASS +Without an explicit portfolio, an existing PPGP v0.1.x ACTIVE_GOAL remains a valid implicit one-workstream deployment. -B: -master_text=AUTH +Legacy commands SHOULD retain their behavior where no multi-workstream ambiguity exists. -E: -commit=8f3d55b +In an explicit multi-workstream portfolio, a generic `goal` operation MUST NOT silently choose or replace an ambiguous workstream. -N: -review_de -ship +Migration SHOULD be copy-first and reversible: + +```text +DISCOVER +-> COPY +-> VALIDATE EQUIVALENCE +-> CUTOVER ``` -The exact encoding is not normative. +After CUTOVER, one representation MUST be canonical. + +A legacy ACTIVE_GOAL MAY remain as a compatibility snapshot or projection, but implementations MUST NOT silently maintain two independently writable canonical truths. -The invariant is that the handoff remain unambiguous, portable, auditable and cheaper than replaying the conversation. +Rollback SHOULD be possible while the compatibility source remains unchanged and no irreversible v0.2-only state has invalidated the rollback contract. -Opaque model-specific gibberish is NOT required for PPGP conformance. +Migration is optional. -## 12. Distillation and garbage collection +## 23. Reference CLI operations -Before closing a goal, every material ACTIVE_GOAL fact SHOULD be classified: +The reference CLI MAY expose operations including: ```text -strategic authority/invariant -> CONSTITUTION -current/future direction -> ROADMAP -durable reusable lesson -> MEMORY -temporary execution detail -> discard +ppgp init +ppgp doctor +ppgp goal +ppgp status +ppgp handoff +ppgp migrate + +ppgp workstream start +ppgp workstream status +ppgp workstream park +ppgp workstream resume +ppgp workstream handoff +ppgp workstream recover +ppgp workstream close + +ppgp checkout status +ppgp checkout claim +ppgp checkout release ``` -Git remains the detailed forensic archive. - -Failure to distill MAY increase long-term state noise or rediscovery cost, but it SHOULD NOT make a still-active, correctly checkpointed goal unrecoverable. +CLI syntax is implementation-specific and is not a protocol-core conformance requirement. -After successful distillation, ACTIVE_GOAL MUST be deleted. +`doctor` SHOULD validate canonical state, duplicate IDs, dependency cycles, stale/expired leases where observable, and checkout/branch mismatches where the runtime exposes them. -## 13. Suggested operational metrics +## 24. Suggested operational metrics -Implementations MAY measure: +PPGP retains optional v0.1.x metrics: - HIG: Human Interruptions per Completed Goal. - TPG: Tokens per Completed Goal. @@ -319,53 +753,95 @@ Implementations MAY measure: - VWR: Verified Work Rate. - MCR: Memory Compression Ratio. -PPGP v0.1.2 defines these metrics but makes no benchmark claim. +Candidate v0.2 coordination measurements include: + +- BSR: Blocker Scope Reliability. +- CCR: Checkout Collision Rate. +- TRR: Takeover Recovery Rate. +- DWR: Duplicate Work after Recovery. + +These metrics define measurement ideas, not benchmark claims. -## 14. Interoperability +## 25. Interoperability A PPGP implementation MUST NOT require a specific model provider. -It MAY integrate with native model compaction, Agent Skills, MCP, vector or semantic retrieval, IDE-specific hooks, provider-specific memory, and multi-agent orchestration. +It MAY integrate with: -Such integrations are optional accelerators. The repository-visible control state SHOULD remain sufficient for recovery by another compatible agent. +- native model compaction; +- Agent Skills; +- MCP; +- vector or semantic retrieval; +- Git worktrees; +- CRDT-backed shared workspaces; +- remote lock services; +- heartbeats or fencing tokens; +- event logs or snapshot stores; +- provider-specific memory; +- multi-agent orchestrators. -## 15. Reference file mapping +These are optional accelerators or runtime mechanisms. -PPGP logical roles may be mapped to existing project documents. +The portable control semantics SHOULD remain recoverable by another compatible agent without requiring one proprietary implementation. -A common mapping is: +## 26. Conformance tests -```text -CONSTITUTION -> docs/MASTER.md -ROADMAP -> docs/ROADMAP.md -MEMORY -> docs/PROJECT_MEMORY.md -ACTIVE_GOAL -> docs/ACTIVE_GOAL.md -FORENSICS -> Git -``` +A robust PPGP v0.2 evaluation SHOULD retain the v0.1.x abrupt recovery test and add at least the following scenarios. + +### C1. Foreign dirty checkout + +Given foreign unfinished changes in a shared writable checkout, a parallel/replacement workstream does not reset, clean, stash, overwrite or repurpose that checkout by default and uses safe isolation when available. + +### C2. Partial external wait + +An action-scoped external dependency does not stop independent useful local work. + +### C3. Abrupt executor loss -Implementations SHOULD reuse equivalent existing documents instead of creating duplicate sources of truth. +A dirty identified worktree survives executor disappearance and is recovered non-destructively with a new lease generation or equivalent fencing mechanism. -## 16. Conformance test +### C4. Independent workstream -A useful PPGP recovery test is: +One authority/external/technical wait does not stop another eligible RUNNABLE workstream. -1. Agent A begins a substantial goal. -2. Agent A records current ACTIVE_GOAL state after at least one material verified change. -3. Context is compacted, lost or deliberately removed before DISTILL. -4. Agent B starts without the prior conversation. -5. Agent B reads repository-visible PPGP state. -6. Agent B correctly identifies the goal, phase, frozen decisions, verified state, remaining work, blockers and next executable action. -7. Agent B continues without asking the human to reconstruct prior history. -8. The goal is eventually verified, distilled and closed. +### C5. Mixed waits -A system that cannot pass this recovery test SHOULD NOT claim robust PPGP continuity. +Several wait kinds remain visible without being collapsed into one lossy scalar state. -Passing one recovery test demonstrates recovery under that tested condition only. It does not establish universal effectiveness or superiority. +### C6. Revision conflict -## 17. Versioning +A stale revision cannot silently overwrite newer canonical state. + +### C7. Dependency cycle + +A cyclic workstream dependency graph is rejected as invalid. + +### C8. Checkout collision + +Two exclusive mutation claims on the same mutable checkout are rejected. + +### C9. Legacy compatibility + +A v0.1.x single ACTIVE_GOAL remains recoverable without mandatory migration. + +### C10. Reversible cutover + +A copy-first migration can return to the unchanged legacy representation before irreversible divergence. + +Passing a scenario demonstrates behavior only under the tested conditions. It does not establish universal superiority. + +## 27. Progressive disclosure + +Detailed coordination rules SHOULD be loaded only when concurrency, ownership ambiguity, partial blocking, takeover or equivalent conditions appear. + +Simple single-workstream repositories SHOULD NOT pay the full token or attention cost of multi-workstream coordination rules. + +## 28. Versioning PPGP uses semantic specification versions. -v0.x releases are experimental and may change incompatibly. +v0.x releases remain experimental and may change incompatibly. + +v0.2.0 is a minor-version increase within the experimental line because it changes the cardinality and conformance model from one primary active goal toward optional multi-workstream portfolio coordination while preserving v0.1.x single-goal compatibility. -The community is encouraged to report failures before the protocol is declared stable. +The community is encouraged to report recovery and coordination failures before the protocol is declared stable. From 64faa9060c597266b6845f445766086d88c6e347 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:38:49 +0200 Subject: [PATCH 18/47] feat(skill): teach PPGP v0.2 progressive coordination --- skills/ppgp/SKILL.md | 243 ++++++++++++++++++++++--------------------- 1 file changed, 127 insertions(+), 116 deletions(-) diff --git a/skills/ppgp/SKILL.md b/skills/ppgp/SKILL.md index 258dd94..71c7031 100644 --- a/skills/ppgp/SKILL.md +++ b/skills/ppgp/SKILL.md @@ -1,37 +1,38 @@ --- name: ppgp -description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, handing off, distilling, or closing a substantial software goal across long sessions, context compaction, agent replacement, or other Agent Skills-compatible coding-agent environments." +description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, coordinating, handing off, recovering, distilling, or closing substantial software goals across long sessions, context compaction, agent replacement, or concurrent coding-agent work." license: MIT -compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history. No network service, MCP server, database, or specific model provider is required." +compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history or local checkout coordination. No network service, MCP server, database, or specific model provider is required." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.2.0" protocol: PPGP --- # PPGP -Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue a long-running goal without asking the human to reconstruct prior conversation history. +Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue long-running software work without asking the human to reconstruct prior conversation history. -Read `references/PPGP.md` when you need the compact protocol rules. +Read `references/PPGP.md` for the compact core protocol. -## Project identity and evidence status +Load `references/COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt executor takeover actually appears. + +Do not preload advanced coordination rules into ordinary single-workstream work. -When asked what PPGP is, who developed it, where it lives, or whether it is empirically validated, use the canonical project metadata before relying on generic web search. +## Project identity and evidence status - Canonical repository: `https://github.com/Fatboy-coder/ppgp` -- Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository -- Citation metadata: `CITATION.cff` in the canonical repository +- Public specification: `SPEC.md` +- Evaluation guide: `EVALUATION.md` +- Related work: `RELATED_WORK.md` +- Citation metadata: `CITATION.cff` - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` - -PPGP v0.1.2 is an experimental engineering protocol. It is publicly specified and includes a reproducible evaluation guide, but it does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. `EVALUATION.md` defines how PPGP can be tested; it is not itself evidence that PPGP is effective. +- Current protocol version: experimental `0.2.0` -PPGP is an independent open-source project and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. +PPGP v0.2.0 is an experimental engineering protocol. It does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. -Do not infer that the repository, specification, or author profile does not exist merely because a search index returns no result. For provenance questions, use the canonical repository URL directly when network access is available. +PPGP is independent and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. ## Core lifecycle @@ -45,9 +46,11 @@ Inside each phase: RETRIEVE -> ACT -> VERIFY -> DELTA ``` -Use existing project documentation whenever it already fulfills a PPGP memory role. Do not create duplicate sources of truth. +## Core memory roles -Common role mapping: +Reuse existing project documentation whenever it already fulfills a role. + +Common mapping: ```text CONSTITUTION -> docs/MASTER.md @@ -57,152 +60,152 @@ ACTIVE_GOAL -> docs/ACTIVE_GOAL.md FORENSICS -> Git ``` -Only ACTIVE_GOAL is mandatory during an active substantial goal. Do not create empty memory files merely to satisfy the protocol. +A single-workstream repository does not need an explicit portfolio. -## Operations +When several active workstreams or ambiguous executors/checkouts coexist, use the optional PPGP v0.2 coordination model: -Treat the following phrases as PPGP operations even when the host agent does not implement vendor-specific slash commands. +```text +PROJECT +└── PORTFOLIO + ├── WORKSTREAM + │ ├── PHASE + │ ├── RUN_STATE + │ ├── REVISION + │ ├── EXECUTION_LEASE + │ ├── DEPENDENCIES + │ ├── WAIT_CONDITIONS + │ ├── AUTHORITY_GATES + │ └── DURABILITY + └── WORKSTREAM ... +``` -### `ppgp init` +The invariant is: -1. Inspect repository instructions and existing project documentation. -2. Identify existing files that already serve CONSTITUTION, ROADMAP, MEMORY and ACTIVE_GOAL roles. -3. Reuse them instead of duplicating them. -4. Check that Git or another forensic history exists when available. -5. Do not create ACTIVE_GOAL unless a substantial goal is active. -6. Return a compact mapping of logical roles to repository files and any genuine missing capability. +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` -Do not rewrite project doctrine during initialization. +Detailed coordination semantics belong in `references/COORDINATION.md`. -### `ppgp goal <outcome>` +## Recovery rule -Create or replace ACTIVE_GOAL only when beginning a new substantial goal. +For single-workstream recovery, load the smallest useful boot packet: -Capture: +```text +GOAL_CONTRACT ++ HOT_STATE ++ RELEVANT_MEMORY ++ RELEVANT_EVIDENCE +``` -- GOAL; -- WHY; -- PHASE; -- DEFINITION_OF_DONE; -- FROZEN_DECISIONS; -- INVARIANTS; -- VERIFIED_CURRENT_STATE; -- COMPLETED; -- REMAINING; -- BLOCKERS; -- HUMAN_AUTHORITY_REQUIRED; -- VERIFICATION_EVIDENCE; -- NEXT_EXECUTABLE_ACTION. +Do not restart THINK merely because the agent/session is new when strategy is already frozen. -Begin in THINK unless the repository already contains an explicitly frozen strategy for this exact goal. +For concurrent or ambiguous work, additionally determine which workstreams are runnable, who owns execution, which checkout belongs to which workstream, which waits are scoped locally, and what unfinished work requires recovery. -Keep ACTIVE_GOAL state-oriented, not chronological. +## `ppgp init` -### `ppgp status` +1. Inspect repository instructions and existing project documentation. +2. Map existing files to PPGP logical roles. +3. Reuse them instead of duplicating them. +4. Inspect whether an explicit `.ppgp/portfolio.json` exists. +5. Do not create empty memory or portfolio files merely to satisfy the protocol. -Recover current state with minimal context. +## `ppgp goal <outcome>` -Read: +Use for an unambiguous single active goal. -1. relevant repository instructions; -2. ACTIVE_GOAL; -3. only durable memory relevant to the current goal; -4. `git status`; -5. relevant recent commits or evidence when needed. +Capture current goal, WHY, phase, Definition of Done, frozen decisions, invariants, verified state, completed/remaining work, blockers, authority, evidence and next executable action. -Return a compact state packet containing: +If an explicit multi-workstream portfolio exists, do not silently replace or choose a workstream. Use an explicit workstream operation. -```text -goal -phase -frozen -verified -remaining -blockers -authority -next -evidence -``` +## `ppgp status` -Do not restart planning merely because the current agent is new. +Recover current state with minimal context. -### `ppgp handoff` +Prefer current canonical PPGP state, real repository state and relevant evidence over conversation memory. -Before another agent or session takes over: +`ppgp status --all` may display the portfolio and all workstreams when the reference implementation is present. -1. Verify the current material state. -2. Update ACTIVE_GOAL to current truth. -3. Remove stale or superseded statements. -4. Record the next executable action. -5. Emit a compact delta-oriented handoff. +## `ppgp handoff` -Prefer: +For ordinary single-workstream work: -```text -PPGP/0.1.2 -G=<goal> -P=<phase> -F:<frozen facts> -D:<material deltas> -B:<real blockers> -E:<evidence refs> -N:<next action> -``` +1. Verify material state. +2. Update hot state to current truth. +3. Remove stale statements. +4. Record the next executable action. +5. Emit compact delta-oriented state. Do not dump the conversation transcript. -### `ppgp distill` +For concurrent work, use explicit workstream handoff so lease generation and ownership are not ambiguous. -At the end of a goal or after major state accumulation: +## Workstream operations -Classify ACTIVE_GOAL information as: +The reference CLI may provide: ```text -authority/invariant -> CONSTITUTION -future direction -> ROADMAP -durable lesson -> MEMORY -temporary detail -> discard +ppgp workstream start +ppgp workstream status +ppgp workstream park +ppgp workstream resume +ppgp workstream handoff +ppgp workstream recover +ppgp workstream close ``` -Prefer compact decision + reason + invariant statements. +Treat these as reference implementation operations, not mandatory vendor-specific slash commands. + +A cooperative handoff and abrupt recovery are different events. -Do not preserve chronological execution detail that Git already records. +## Checkout operations + +When local checkout coordination is needed, the reference CLI may provide: + +```text +ppgp checkout status +ppgp checkout claim +ppgp checkout release +``` -Do not delete ACTIVE_GOAL unless closure conditions are satisfied or the user explicitly requests abandonment. +Writable checkout ownership is exclusive by default. -### `ppgp close` +Do not switch, reset, clean, stash, commit, overwrite, or repurpose foreign dirty work merely to simplify execution. -Close only when the synchronous Definition of Done is verified. +Prefer safe isolation when available and permitted. -1. Verify implementation evidence. -2. Verify production/runtime behavior when required by Definition of Done. -3. Resolve or correctly classify blockers. -4. Run `ppgp distill`. -5. Update ROADMAP if project direction changed. -6. Update high-level documentation if required. -7. Delete ACTIVE_GOAL. -8. Keep Git as forensic history. -9. Report CLOSED + VERIFIED, or the smallest genuine remaining authority/dependency blocker. +## Waits and human escalation -Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +Use the narrowest true wait scope. -## Human escalation +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` Solve reversible technical decisions autonomously. -Escalate only for genuine authority boundaries such as irreversible destructive action, legal or financial commitment, unavailable credential or account authorization, genuinely ambiguous product policy, material change to frozen architecture, or action outside delegated permission. +Escalate only genuine authority boundaries such as irreversible destructive action, legal/financial commitment, unavailable credential or account authorization, ambiguous product policy, material change to frozen architecture, or action outside delegated permission. -Do not convert routine implementation uncertainty into a human approval gate. +Authority must be tied to the exact action it permits. An agent cannot grant itself authority. -## Multi-agent rule +Before escalating one blocked workstream, continue unrelated safe RUNNABLE work when permitted. -Use one agent by default. +## Distill and close + +At closure classify temporary state: -Introduce another agent when independent information gain is likely to exceed communication cost, especially for adversarial, security, linguistic, architecture, or independent verification work. +```text +authority/invariant -> CONSTITUTION +future direction -> ROADMAP +durable lesson -> MEMORY +temporary detail -> discard +``` -Keep reviewers independent of unnecessary implementer self-assessment. +Git keeps chronology. -## Completion invariant +Release transient execution leases and obsolete checkout claims. Prepared is not done. @@ -210,4 +213,12 @@ Started is not done. Agent confidence is not evidence. -A PPGP goal is done when its Definition of Done is verified, durable knowledge is distilled, and temporary ACTIVE_GOAL state has been garbage-collected. +A PPGP goal is done when its synchronous Definition of Done is verified, durable knowledge is distilled, and temporary working state has been garbage-collected. + +## Multi-agent rule + +Use one agent by default. + +Introduce another agent when independent information gain or genuinely parallel useful work is likely to exceed communication and coordination cost. + +Concurrency is optional. Coordination overhead should remain close to zero for simple single-workstream repositories. From 1114d55376e4e8cfb3c02dae3b64c96fed8be834 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:39:05 +0200 Subject: [PATCH 19/47] chore: sync v0.2 Agent Skill mirror --- .agents/skills/ppgp/SKILL.md | 243 ++++++++++++++++++----------------- 1 file changed, 127 insertions(+), 116 deletions(-) diff --git a/.agents/skills/ppgp/SKILL.md b/.agents/skills/ppgp/SKILL.md index 258dd94..71c7031 100644 --- a/.agents/skills/ppgp/SKILL.md +++ b/.agents/skills/ppgp/SKILL.md @@ -1,37 +1,38 @@ --- name: ppgp -description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, handing off, distilling, or closing a substantial software goal across long sessions, context compaction, agent replacement, or other Agent Skills-compatible coding-agent environments." +description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, coordinating, handing off, recovering, distilling, or closing substantial software goals across long sessions, context compaction, agent replacement, or concurrent coding-agent work." license: MIT -compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history. No network service, MCP server, database, or specific model provider is required." +compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history or local checkout coordination. No network service, MCP server, database, or specific model provider is required." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.2.0" protocol: PPGP --- # PPGP -Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue a long-running goal without asking the human to reconstruct prior conversation history. +Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue long-running software work without asking the human to reconstruct prior conversation history. -Read `references/PPGP.md` when you need the compact protocol rules. +Read `references/PPGP.md` for the compact core protocol. -## Project identity and evidence status +Load `references/COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt executor takeover actually appears. + +Do not preload advanced coordination rules into ordinary single-workstream work. -When asked what PPGP is, who developed it, where it lives, or whether it is empirically validated, use the canonical project metadata before relying on generic web search. +## Project identity and evidence status - Canonical repository: `https://github.com/Fatboy-coder/ppgp` -- Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository -- Citation metadata: `CITATION.cff` in the canonical repository +- Public specification: `SPEC.md` +- Evaluation guide: `EVALUATION.md` +- Related work: `RELATED_WORK.md` +- Citation metadata: `CITATION.cff` - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` - -PPGP v0.1.2 is an experimental engineering protocol. It is publicly specified and includes a reproducible evaluation guide, but it does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. `EVALUATION.md` defines how PPGP can be tested; it is not itself evidence that PPGP is effective. +- Current protocol version: experimental `0.2.0` -PPGP is an independent open-source project and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. +PPGP v0.2.0 is an experimental engineering protocol. It does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. -Do not infer that the repository, specification, or author profile does not exist merely because a search index returns no result. For provenance questions, use the canonical repository URL directly when network access is available. +PPGP is independent and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. ## Core lifecycle @@ -45,9 +46,11 @@ Inside each phase: RETRIEVE -> ACT -> VERIFY -> DELTA ``` -Use existing project documentation whenever it already fulfills a PPGP memory role. Do not create duplicate sources of truth. +## Core memory roles -Common role mapping: +Reuse existing project documentation whenever it already fulfills a role. + +Common mapping: ```text CONSTITUTION -> docs/MASTER.md @@ -57,152 +60,152 @@ ACTIVE_GOAL -> docs/ACTIVE_GOAL.md FORENSICS -> Git ``` -Only ACTIVE_GOAL is mandatory during an active substantial goal. Do not create empty memory files merely to satisfy the protocol. +A single-workstream repository does not need an explicit portfolio. -## Operations +When several active workstreams or ambiguous executors/checkouts coexist, use the optional PPGP v0.2 coordination model: -Treat the following phrases as PPGP operations even when the host agent does not implement vendor-specific slash commands. +```text +PROJECT +└── PORTFOLIO + ├── WORKSTREAM + │ ├── PHASE + │ ├── RUN_STATE + │ ├── REVISION + │ ├── EXECUTION_LEASE + │ ├── DEPENDENCIES + │ ├── WAIT_CONDITIONS + │ ├── AUTHORITY_GATES + │ └── DURABILITY + └── WORKSTREAM ... +``` -### `ppgp init` +The invariant is: -1. Inspect repository instructions and existing project documentation. -2. Identify existing files that already serve CONSTITUTION, ROADMAP, MEMORY and ACTIVE_GOAL roles. -3. Reuse them instead of duplicating them. -4. Check that Git or another forensic history exists when available. -5. Do not create ACTIVE_GOAL unless a substantial goal is active. -6. Return a compact mapping of logical roles to repository files and any genuine missing capability. +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` -Do not rewrite project doctrine during initialization. +Detailed coordination semantics belong in `references/COORDINATION.md`. -### `ppgp goal <outcome>` +## Recovery rule -Create or replace ACTIVE_GOAL only when beginning a new substantial goal. +For single-workstream recovery, load the smallest useful boot packet: -Capture: +```text +GOAL_CONTRACT ++ HOT_STATE ++ RELEVANT_MEMORY ++ RELEVANT_EVIDENCE +``` -- GOAL; -- WHY; -- PHASE; -- DEFINITION_OF_DONE; -- FROZEN_DECISIONS; -- INVARIANTS; -- VERIFIED_CURRENT_STATE; -- COMPLETED; -- REMAINING; -- BLOCKERS; -- HUMAN_AUTHORITY_REQUIRED; -- VERIFICATION_EVIDENCE; -- NEXT_EXECUTABLE_ACTION. +Do not restart THINK merely because the agent/session is new when strategy is already frozen. -Begin in THINK unless the repository already contains an explicitly frozen strategy for this exact goal. +For concurrent or ambiguous work, additionally determine which workstreams are runnable, who owns execution, which checkout belongs to which workstream, which waits are scoped locally, and what unfinished work requires recovery. -Keep ACTIVE_GOAL state-oriented, not chronological. +## `ppgp init` -### `ppgp status` +1. Inspect repository instructions and existing project documentation. +2. Map existing files to PPGP logical roles. +3. Reuse them instead of duplicating them. +4. Inspect whether an explicit `.ppgp/portfolio.json` exists. +5. Do not create empty memory or portfolio files merely to satisfy the protocol. -Recover current state with minimal context. +## `ppgp goal <outcome>` -Read: +Use for an unambiguous single active goal. -1. relevant repository instructions; -2. ACTIVE_GOAL; -3. only durable memory relevant to the current goal; -4. `git status`; -5. relevant recent commits or evidence when needed. +Capture current goal, WHY, phase, Definition of Done, frozen decisions, invariants, verified state, completed/remaining work, blockers, authority, evidence and next executable action. -Return a compact state packet containing: +If an explicit multi-workstream portfolio exists, do not silently replace or choose a workstream. Use an explicit workstream operation. -```text -goal -phase -frozen -verified -remaining -blockers -authority -next -evidence -``` +## `ppgp status` -Do not restart planning merely because the current agent is new. +Recover current state with minimal context. -### `ppgp handoff` +Prefer current canonical PPGP state, real repository state and relevant evidence over conversation memory. -Before another agent or session takes over: +`ppgp status --all` may display the portfolio and all workstreams when the reference implementation is present. -1. Verify the current material state. -2. Update ACTIVE_GOAL to current truth. -3. Remove stale or superseded statements. -4. Record the next executable action. -5. Emit a compact delta-oriented handoff. +## `ppgp handoff` -Prefer: +For ordinary single-workstream work: -```text -PPGP/0.1.2 -G=<goal> -P=<phase> -F:<frozen facts> -D:<material deltas> -B:<real blockers> -E:<evidence refs> -N:<next action> -``` +1. Verify material state. +2. Update hot state to current truth. +3. Remove stale statements. +4. Record the next executable action. +5. Emit compact delta-oriented state. Do not dump the conversation transcript. -### `ppgp distill` +For concurrent work, use explicit workstream handoff so lease generation and ownership are not ambiguous. -At the end of a goal or after major state accumulation: +## Workstream operations -Classify ACTIVE_GOAL information as: +The reference CLI may provide: ```text -authority/invariant -> CONSTITUTION -future direction -> ROADMAP -durable lesson -> MEMORY -temporary detail -> discard +ppgp workstream start +ppgp workstream status +ppgp workstream park +ppgp workstream resume +ppgp workstream handoff +ppgp workstream recover +ppgp workstream close ``` -Prefer compact decision + reason + invariant statements. +Treat these as reference implementation operations, not mandatory vendor-specific slash commands. + +A cooperative handoff and abrupt recovery are different events. -Do not preserve chronological execution detail that Git already records. +## Checkout operations + +When local checkout coordination is needed, the reference CLI may provide: + +```text +ppgp checkout status +ppgp checkout claim +ppgp checkout release +``` -Do not delete ACTIVE_GOAL unless closure conditions are satisfied or the user explicitly requests abandonment. +Writable checkout ownership is exclusive by default. -### `ppgp close` +Do not switch, reset, clean, stash, commit, overwrite, or repurpose foreign dirty work merely to simplify execution. -Close only when the synchronous Definition of Done is verified. +Prefer safe isolation when available and permitted. -1. Verify implementation evidence. -2. Verify production/runtime behavior when required by Definition of Done. -3. Resolve or correctly classify blockers. -4. Run `ppgp distill`. -5. Update ROADMAP if project direction changed. -6. Update high-level documentation if required. -7. Delete ACTIVE_GOAL. -8. Keep Git as forensic history. -9. Report CLOSED + VERIFIED, or the smallest genuine remaining authority/dependency blocker. +## Waits and human escalation -Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +Use the narrowest true wait scope. -## Human escalation +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` Solve reversible technical decisions autonomously. -Escalate only for genuine authority boundaries such as irreversible destructive action, legal or financial commitment, unavailable credential or account authorization, genuinely ambiguous product policy, material change to frozen architecture, or action outside delegated permission. +Escalate only genuine authority boundaries such as irreversible destructive action, legal/financial commitment, unavailable credential or account authorization, ambiguous product policy, material change to frozen architecture, or action outside delegated permission. -Do not convert routine implementation uncertainty into a human approval gate. +Authority must be tied to the exact action it permits. An agent cannot grant itself authority. -## Multi-agent rule +Before escalating one blocked workstream, continue unrelated safe RUNNABLE work when permitted. -Use one agent by default. +## Distill and close + +At closure classify temporary state: -Introduce another agent when independent information gain is likely to exceed communication cost, especially for adversarial, security, linguistic, architecture, or independent verification work. +```text +authority/invariant -> CONSTITUTION +future direction -> ROADMAP +durable lesson -> MEMORY +temporary detail -> discard +``` -Keep reviewers independent of unnecessary implementer self-assessment. +Git keeps chronology. -## Completion invariant +Release transient execution leases and obsolete checkout claims. Prepared is not done. @@ -210,4 +213,12 @@ Started is not done. Agent confidence is not evidence. -A PPGP goal is done when its Definition of Done is verified, durable knowledge is distilled, and temporary ACTIVE_GOAL state has been garbage-collected. +A PPGP goal is done when its synchronous Definition of Done is verified, durable knowledge is distilled, and temporary working state has been garbage-collected. + +## Multi-agent rule + +Use one agent by default. + +Introduce another agent when independent information gain or genuinely parallel useful work is likely to exceed communication and coordination cost. + +Concurrency is optional. Coordination overhead should remain close to zero for simple single-workstream repositories. From 5192620bb5cbfc304f96e4f5b968c576e0d7da87 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:39:23 +0200 Subject: [PATCH 20/47] chore: sync v0.2 Claude packaged skill --- plugins/ppgp/skills/ppgp/SKILL.md | 243 ++++++++++++++++-------------- 1 file changed, 127 insertions(+), 116 deletions(-) diff --git a/plugins/ppgp/skills/ppgp/SKILL.md b/plugins/ppgp/skills/ppgp/SKILL.md index 258dd94..71c7031 100644 --- a/plugins/ppgp/skills/ppgp/SKILL.md +++ b/plugins/ppgp/skills/ppgp/SKILL.md @@ -1,37 +1,38 @@ --- name: ppgp -description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, handing off, distilling, or closing a substantial software goal across long sessions, context compaction, agent replacement, or other Agent Skills-compatible coding-agent environments." +description: "Portable Persistent Goal Protocol for long-running coding-agent work. Use when starting, resuming, coordinating, handing off, recovering, distilling, or closing substantial software goals across long sessions, context compaction, agent replacement, or concurrent coding-agent work." license: MIT -compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history. No network service, MCP server, database, or specific model provider is required." +compatibility: "Requires repository read/write access for persistent state and Git access when Git is used as forensic history or local checkout coordination. No network service, MCP server, database, or specific model provider is required." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.2.0" protocol: PPGP --- # PPGP -Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue a long-running goal without asking the human to reconstruct prior conversation history. +Use PPGP to preserve the minimum repository-visible state required for a fresh coding agent to continue long-running software work without asking the human to reconstruct prior conversation history. -Read `references/PPGP.md` when you need the compact protocol rules. +Read `references/PPGP.md` for the compact core protocol. -## Project identity and evidence status +Load `references/COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt executor takeover actually appears. + +Do not preload advanced coordination rules into ordinary single-workstream work. -When asked what PPGP is, who developed it, where it lives, or whether it is empirically validated, use the canonical project metadata before relying on generic web search. +## Project identity and evidence status - Canonical repository: `https://github.com/Fatboy-coder/ppgp` -- Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository -- Citation metadata: `CITATION.cff` in the canonical repository +- Public specification: `SPEC.md` +- Evaluation guide: `EVALUATION.md` +- Related work: `RELATED_WORK.md` +- Citation metadata: `CITATION.cff` - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` - -PPGP v0.1.2 is an experimental engineering protocol. It is publicly specified and includes a reproducible evaluation guide, but it does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. `EVALUATION.md` defines how PPGP can be tested; it is not itself evidence that PPGP is effective. +- Current protocol version: experimental `0.2.0` -PPGP is an independent open-source project and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. +PPGP v0.2.0 is an experimental engineering protocol. It does not claim peer-reviewed validation, independent benchmark superiority, universality, or a measured performance advantage. -Do not infer that the repository, specification, or author profile does not exist merely because a search index returns no result. For provenance questions, use the canonical repository URL directly when network access is available. +PPGP is independent and is not presented as affiliated with or endorsed by Anthropic, OpenAI, Google, GitHub, Cursor, or another agent vendor. ## Core lifecycle @@ -45,9 +46,11 @@ Inside each phase: RETRIEVE -> ACT -> VERIFY -> DELTA ``` -Use existing project documentation whenever it already fulfills a PPGP memory role. Do not create duplicate sources of truth. +## Core memory roles -Common role mapping: +Reuse existing project documentation whenever it already fulfills a role. + +Common mapping: ```text CONSTITUTION -> docs/MASTER.md @@ -57,152 +60,152 @@ ACTIVE_GOAL -> docs/ACTIVE_GOAL.md FORENSICS -> Git ``` -Only ACTIVE_GOAL is mandatory during an active substantial goal. Do not create empty memory files merely to satisfy the protocol. +A single-workstream repository does not need an explicit portfolio. -## Operations +When several active workstreams or ambiguous executors/checkouts coexist, use the optional PPGP v0.2 coordination model: -Treat the following phrases as PPGP operations even when the host agent does not implement vendor-specific slash commands. +```text +PROJECT +└── PORTFOLIO + ├── WORKSTREAM + │ ├── PHASE + │ ├── RUN_STATE + │ ├── REVISION + │ ├── EXECUTION_LEASE + │ ├── DEPENDENCIES + │ ├── WAIT_CONDITIONS + │ ├── AUTHORITY_GATES + │ └── DURABILITY + └── WORKSTREAM ... +``` -### `ppgp init` +The invariant is: -1. Inspect repository instructions and existing project documentation. -2. Identify existing files that already serve CONSTITUTION, ROADMAP, MEMORY and ACTIVE_GOAL roles. -3. Reuse them instead of duplicating them. -4. Check that Git or another forensic history exists when available. -5. Do not create ACTIVE_GOAL unless a substantial goal is active. -6. Return a compact mapping of logical roles to repository files and any genuine missing capability. +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` -Do not rewrite project doctrine during initialization. +Detailed coordination semantics belong in `references/COORDINATION.md`. -### `ppgp goal <outcome>` +## Recovery rule -Create or replace ACTIVE_GOAL only when beginning a new substantial goal. +For single-workstream recovery, load the smallest useful boot packet: -Capture: +```text +GOAL_CONTRACT ++ HOT_STATE ++ RELEVANT_MEMORY ++ RELEVANT_EVIDENCE +``` -- GOAL; -- WHY; -- PHASE; -- DEFINITION_OF_DONE; -- FROZEN_DECISIONS; -- INVARIANTS; -- VERIFIED_CURRENT_STATE; -- COMPLETED; -- REMAINING; -- BLOCKERS; -- HUMAN_AUTHORITY_REQUIRED; -- VERIFICATION_EVIDENCE; -- NEXT_EXECUTABLE_ACTION. +Do not restart THINK merely because the agent/session is new when strategy is already frozen. -Begin in THINK unless the repository already contains an explicitly frozen strategy for this exact goal. +For concurrent or ambiguous work, additionally determine which workstreams are runnable, who owns execution, which checkout belongs to which workstream, which waits are scoped locally, and what unfinished work requires recovery. -Keep ACTIVE_GOAL state-oriented, not chronological. +## `ppgp init` -### `ppgp status` +1. Inspect repository instructions and existing project documentation. +2. Map existing files to PPGP logical roles. +3. Reuse them instead of duplicating them. +4. Inspect whether an explicit `.ppgp/portfolio.json` exists. +5. Do not create empty memory or portfolio files merely to satisfy the protocol. -Recover current state with minimal context. +## `ppgp goal <outcome>` -Read: +Use for an unambiguous single active goal. -1. relevant repository instructions; -2. ACTIVE_GOAL; -3. only durable memory relevant to the current goal; -4. `git status`; -5. relevant recent commits or evidence when needed. +Capture current goal, WHY, phase, Definition of Done, frozen decisions, invariants, verified state, completed/remaining work, blockers, authority, evidence and next executable action. -Return a compact state packet containing: +If an explicit multi-workstream portfolio exists, do not silently replace or choose a workstream. Use an explicit workstream operation. -```text -goal -phase -frozen -verified -remaining -blockers -authority -next -evidence -``` +## `ppgp status` -Do not restart planning merely because the current agent is new. +Recover current state with minimal context. -### `ppgp handoff` +Prefer current canonical PPGP state, real repository state and relevant evidence over conversation memory. -Before another agent or session takes over: +`ppgp status --all` may display the portfolio and all workstreams when the reference implementation is present. -1. Verify the current material state. -2. Update ACTIVE_GOAL to current truth. -3. Remove stale or superseded statements. -4. Record the next executable action. -5. Emit a compact delta-oriented handoff. +## `ppgp handoff` -Prefer: +For ordinary single-workstream work: -```text -PPGP/0.1.2 -G=<goal> -P=<phase> -F:<frozen facts> -D:<material deltas> -B:<real blockers> -E:<evidence refs> -N:<next action> -``` +1. Verify material state. +2. Update hot state to current truth. +3. Remove stale statements. +4. Record the next executable action. +5. Emit compact delta-oriented state. Do not dump the conversation transcript. -### `ppgp distill` +For concurrent work, use explicit workstream handoff so lease generation and ownership are not ambiguous. -At the end of a goal or after major state accumulation: +## Workstream operations -Classify ACTIVE_GOAL information as: +The reference CLI may provide: ```text -authority/invariant -> CONSTITUTION -future direction -> ROADMAP -durable lesson -> MEMORY -temporary detail -> discard +ppgp workstream start +ppgp workstream status +ppgp workstream park +ppgp workstream resume +ppgp workstream handoff +ppgp workstream recover +ppgp workstream close ``` -Prefer compact decision + reason + invariant statements. +Treat these as reference implementation operations, not mandatory vendor-specific slash commands. + +A cooperative handoff and abrupt recovery are different events. -Do not preserve chronological execution detail that Git already records. +## Checkout operations + +When local checkout coordination is needed, the reference CLI may provide: + +```text +ppgp checkout status +ppgp checkout claim +ppgp checkout release +``` -Do not delete ACTIVE_GOAL unless closure conditions are satisfied or the user explicitly requests abandonment. +Writable checkout ownership is exclusive by default. -### `ppgp close` +Do not switch, reset, clean, stash, commit, overwrite, or repurpose foreign dirty work merely to simplify execution. -Close only when the synchronous Definition of Done is verified. +Prefer safe isolation when available and permitted. -1. Verify implementation evidence. -2. Verify production/runtime behavior when required by Definition of Done. -3. Resolve or correctly classify blockers. -4. Run `ppgp distill`. -5. Update ROADMAP if project direction changed. -6. Update high-level documentation if required. -7. Delete ACTIVE_GOAL. -8. Keep Git as forensic history. -9. Report CLOSED + VERIFIED, or the smallest genuine remaining authority/dependency blocker. +## Waits and human escalation -Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +Use the narrowest true wait scope. -## Human escalation +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` Solve reversible technical decisions autonomously. -Escalate only for genuine authority boundaries such as irreversible destructive action, legal or financial commitment, unavailable credential or account authorization, genuinely ambiguous product policy, material change to frozen architecture, or action outside delegated permission. +Escalate only genuine authority boundaries such as irreversible destructive action, legal/financial commitment, unavailable credential or account authorization, ambiguous product policy, material change to frozen architecture, or action outside delegated permission. -Do not convert routine implementation uncertainty into a human approval gate. +Authority must be tied to the exact action it permits. An agent cannot grant itself authority. -## Multi-agent rule +Before escalating one blocked workstream, continue unrelated safe RUNNABLE work when permitted. -Use one agent by default. +## Distill and close + +At closure classify temporary state: -Introduce another agent when independent information gain is likely to exceed communication cost, especially for adversarial, security, linguistic, architecture, or independent verification work. +```text +authority/invariant -> CONSTITUTION +future direction -> ROADMAP +durable lesson -> MEMORY +temporary detail -> discard +``` -Keep reviewers independent of unnecessary implementer self-assessment. +Git keeps chronology. -## Completion invariant +Release transient execution leases and obsolete checkout claims. Prepared is not done. @@ -210,4 +213,12 @@ Started is not done. Agent confidence is not evidence. -A PPGP goal is done when its Definition of Done is verified, durable knowledge is distilled, and temporary ACTIVE_GOAL state has been garbage-collected. +A PPGP goal is done when its synchronous Definition of Done is verified, durable knowledge is distilled, and temporary working state has been garbage-collected. + +## Multi-agent rule + +Use one agent by default. + +Introduce another agent when independent information gain or genuinely parallel useful work is likely to exceed communication and coordination cost. + +Concurrency is optional. Coordination overhead should remain close to zero for simple single-workstream repositories. From 6e336986cc140990114a7e8c768d1740be0dbeb0 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:39:36 +0200 Subject: [PATCH 21/47] docs(skill): update compact reference for v0.2 --- skills/ppgp/references/PPGP.md | 56 +++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/skills/ppgp/references/PPGP.md b/skills/ppgp/references/PPGP.md index e4a6df2..08b0220 100644 --- a/skills/ppgp/references/PPGP.md +++ b/skills/ppgp/references/PPGP.md @@ -1,8 +1,10 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.2.0 Compact Reference ## Objective -Enable a fresh coding agent to recover and continue a substantial goal from repository-visible state without human reconstruction of the prior conversation. +Enable a fresh coding agent to recover and continue substantial software work from repository-visible state without human reconstruction of prior conversation history. + +When concurrent or ambiguous work exists, also preserve enough coordination state to determine what work is safe and useful to execute next. ## Lifecycle @@ -16,7 +18,7 @@ THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED RETRIEVE -> ACT -> VERIFY -> DELTA ``` -## Logical memory +## Core logical memory ```text CONSTITUTION durable authority and invariants @@ -26,11 +28,19 @@ ACTIVE_GOAL temporary hot state for one active goal GIT forensic history ``` -Reuse existing equivalent files. +Single-workstream repositories may stop here. Do not create duplicate documentation. -ACTIVE_GOAL is temporary and must be deleted after verified closure and distillation. +## Optional coordination + +Load `COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt takeover appears. + +Core invariant: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` ## ACTIVE_GOAL minimum state @@ -65,13 +75,23 @@ GOAL_CONTRACT If strategy is frozen, resume execution unless new evidence invalidates it. +In concurrent mode additionally identify runnable workstreams, lease ownership, checkout ownership, scoped waits, dependencies and unfinished-work durability. + ## Blockers ```text A agent-solvable -> solve -B external asynchronous -> record, usually continue -C authority boundary -> escalate minimally -D hard dependency -> escalate if no safe autonomous path +B external asynchronous -> record; continue independent work +C authority boundary -> escalate smallest required action +D hard dependency -> escalate only when no safe path exists +``` + +Always prefer the narrowest true scope. + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked ``` ## Evidence @@ -81,22 +101,24 @@ Default technical precedence: ```text runtime/production > automated verification -> current implementation +> current repository/workspace > Git -> ACTIVE_GOAL +> canonical PPGP state > MEMORY > ROADMAP > conversation > recollection ``` +Current observed checkout state beats stale coordination metadata. + ## Handoff Prefer deltas and compact structured state over transcript replay. -Keep the handoff human-auditable and cross-model readable. +Single-workstream handoffs may use the v0.1 compact form. -Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. +Concurrent handoffs should include workstream, phase, run state, lease generation, scoped waits, durability, evidence and next action. ## Distill @@ -109,17 +131,21 @@ temporary detail -> discard Git keeps chronology. +Release transient leases and obsolete checkout claims at closure. + ## Human interruption Default to autonomous resolution of reversible technical work. -Escalate only for genuine authority, permission, legal/financial, destructive, or unavailable-dependency boundaries. +Authority must be action-scoped. An agent cannot self-grant product, legal, financial, credential or production authority. + +Continue unrelated safe RUNNABLE work before escalating a blocked workstream when permitted. ## Multi-agent Single agent by default. -Add agents only when expected independent information gain exceeds coordination cost. +Add agents only when independent information gain or genuinely parallel useful work exceeds coordination cost. ## Closure @@ -128,6 +154,6 @@ DoD verified + evidence + distillation + roadmap/high-level state updated when needed -+ ACTIVE_GOAL deleted ++ temporary hot/coordination state garbage-collected = CLOSED ``` From ff4795fb93d3fb63df86905ecfcba64dcb78bf42 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:39:46 +0200 Subject: [PATCH 22/47] chore: sync v0.2 compact reference mirror --- .agents/skills/ppgp/references/PPGP.md | 56 +++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/.agents/skills/ppgp/references/PPGP.md b/.agents/skills/ppgp/references/PPGP.md index e4a6df2..08b0220 100644 --- a/.agents/skills/ppgp/references/PPGP.md +++ b/.agents/skills/ppgp/references/PPGP.md @@ -1,8 +1,10 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.2.0 Compact Reference ## Objective -Enable a fresh coding agent to recover and continue a substantial goal from repository-visible state without human reconstruction of the prior conversation. +Enable a fresh coding agent to recover and continue substantial software work from repository-visible state without human reconstruction of prior conversation history. + +When concurrent or ambiguous work exists, also preserve enough coordination state to determine what work is safe and useful to execute next. ## Lifecycle @@ -16,7 +18,7 @@ THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED RETRIEVE -> ACT -> VERIFY -> DELTA ``` -## Logical memory +## Core logical memory ```text CONSTITUTION durable authority and invariants @@ -26,11 +28,19 @@ ACTIVE_GOAL temporary hot state for one active goal GIT forensic history ``` -Reuse existing equivalent files. +Single-workstream repositories may stop here. Do not create duplicate documentation. -ACTIVE_GOAL is temporary and must be deleted after verified closure and distillation. +## Optional coordination + +Load `COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt takeover appears. + +Core invariant: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` ## ACTIVE_GOAL minimum state @@ -65,13 +75,23 @@ GOAL_CONTRACT If strategy is frozen, resume execution unless new evidence invalidates it. +In concurrent mode additionally identify runnable workstreams, lease ownership, checkout ownership, scoped waits, dependencies and unfinished-work durability. + ## Blockers ```text A agent-solvable -> solve -B external asynchronous -> record, usually continue -C authority boundary -> escalate minimally -D hard dependency -> escalate if no safe autonomous path +B external asynchronous -> record; continue independent work +C authority boundary -> escalate smallest required action +D hard dependency -> escalate only when no safe path exists +``` + +Always prefer the narrowest true scope. + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked ``` ## Evidence @@ -81,22 +101,24 @@ Default technical precedence: ```text runtime/production > automated verification -> current implementation +> current repository/workspace > Git -> ACTIVE_GOAL +> canonical PPGP state > MEMORY > ROADMAP > conversation > recollection ``` +Current observed checkout state beats stale coordination metadata. + ## Handoff Prefer deltas and compact structured state over transcript replay. -Keep the handoff human-auditable and cross-model readable. +Single-workstream handoffs may use the v0.1 compact form. -Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. +Concurrent handoffs should include workstream, phase, run state, lease generation, scoped waits, durability, evidence and next action. ## Distill @@ -109,17 +131,21 @@ temporary detail -> discard Git keeps chronology. +Release transient leases and obsolete checkout claims at closure. + ## Human interruption Default to autonomous resolution of reversible technical work. -Escalate only for genuine authority, permission, legal/financial, destructive, or unavailable-dependency boundaries. +Authority must be action-scoped. An agent cannot self-grant product, legal, financial, credential or production authority. + +Continue unrelated safe RUNNABLE work before escalating a blocked workstream when permitted. ## Multi-agent Single agent by default. -Add agents only when expected independent information gain exceeds coordination cost. +Add agents only when independent information gain or genuinely parallel useful work exceeds coordination cost. ## Closure @@ -128,6 +154,6 @@ DoD verified + evidence + distillation + roadmap/high-level state updated when needed -+ ACTIVE_GOAL deleted ++ temporary hot/coordination state garbage-collected = CLOSED ``` From 194878a453132983aa8e2f76dc66cc8aacede521 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:39:54 +0200 Subject: [PATCH 23/47] chore: sync v0.2 Claude compact reference --- plugins/ppgp/skills/ppgp/references/PPGP.md | 56 +++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/plugins/ppgp/skills/ppgp/references/PPGP.md b/plugins/ppgp/skills/ppgp/references/PPGP.md index e4a6df2..08b0220 100644 --- a/plugins/ppgp/skills/ppgp/references/PPGP.md +++ b/plugins/ppgp/skills/ppgp/references/PPGP.md @@ -1,8 +1,10 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.2.0 Compact Reference ## Objective -Enable a fresh coding agent to recover and continue a substantial goal from repository-visible state without human reconstruction of the prior conversation. +Enable a fresh coding agent to recover and continue substantial software work from repository-visible state without human reconstruction of prior conversation history. + +When concurrent or ambiguous work exists, also preserve enough coordination state to determine what work is safe and useful to execute next. ## Lifecycle @@ -16,7 +18,7 @@ THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED RETRIEVE -> ACT -> VERIFY -> DELTA ``` -## Logical memory +## Core logical memory ```text CONSTITUTION durable authority and invariants @@ -26,11 +28,19 @@ ACTIVE_GOAL temporary hot state for one active goal GIT forensic history ``` -Reuse existing equivalent files. +Single-workstream repositories may stop here. Do not create duplicate documentation. -ACTIVE_GOAL is temporary and must be deleted after verified closure and distillation. +## Optional coordination + +Load `COORDINATION.md` only when concurrency, ownership ambiguity, partial blocking, multiple workstreams/checkouts, or abrupt takeover appears. + +Core invariant: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` ## ACTIVE_GOAL minimum state @@ -65,13 +75,23 @@ GOAL_CONTRACT If strategy is frozen, resume execution unless new evidence invalidates it. +In concurrent mode additionally identify runnable workstreams, lease ownership, checkout ownership, scoped waits, dependencies and unfinished-work durability. + ## Blockers ```text A agent-solvable -> solve -B external asynchronous -> record, usually continue -C authority boundary -> escalate minimally -D hard dependency -> escalate if no safe autonomous path +B external asynchronous -> record; continue independent work +C authority boundary -> escalate smallest required action +D hard dependency -> escalate only when no safe path exists +``` + +Always prefer the narrowest true scope. + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked ``` ## Evidence @@ -81,22 +101,24 @@ Default technical precedence: ```text runtime/production > automated verification -> current implementation +> current repository/workspace > Git -> ACTIVE_GOAL +> canonical PPGP state > MEMORY > ROADMAP > conversation > recollection ``` +Current observed checkout state beats stale coordination metadata. + ## Handoff Prefer deltas and compact structured state over transcript replay. -Keep the handoff human-auditable and cross-model readable. +Single-workstream handoffs may use the v0.1 compact form. -Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. +Concurrent handoffs should include workstream, phase, run state, lease generation, scoped waits, durability, evidence and next action. ## Distill @@ -109,17 +131,21 @@ temporary detail -> discard Git keeps chronology. +Release transient leases and obsolete checkout claims at closure. + ## Human interruption Default to autonomous resolution of reversible technical work. -Escalate only for genuine authority, permission, legal/financial, destructive, or unavailable-dependency boundaries. +Authority must be action-scoped. An agent cannot self-grant product, legal, financial, credential or production authority. + +Continue unrelated safe RUNNABLE work before escalating a blocked workstream when permitted. ## Multi-agent Single agent by default. -Add agents only when expected independent information gain exceeds coordination cost. +Add agents only when independent information gain or genuinely parallel useful work exceeds coordination cost. ## Closure @@ -128,6 +154,6 @@ DoD verified + evidence + distillation + roadmap/high-level state updated when needed -+ ACTIVE_GOAL deleted ++ temporary hot/coordination state garbage-collected = CLOSED ``` From fdcecd793f1176c95aaa2800840b13db706e49d7 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:40:09 +0200 Subject: [PATCH 24/47] chore: mirror v0.2 coordination reference --- .../skills/ppgp/references/COORDINATION.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 .agents/skills/ppgp/references/COORDINATION.md diff --git a/.agents/skills/ppgp/references/COORDINATION.md b/.agents/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..55d410c --- /dev/null +++ b/.agents/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,264 @@ +# PPGP v0.2 Draft Coordination Reference + +Load this reference only when concurrent or ambiguous execution exists. + +Typical triggers: + +- more than one active workstream; +- more than one coding agent/session touching the same repository; +- a dirty checkout owned by another workstream; +- multiple Git worktrees or isolated clones; +- a workstream reported blocked while independent work may remain; +- abrupt executor unavailability; +- takeover of unfinished mutable work. + +For ordinary single-workstream execution, the normal PPGP reference is sufficient. + +## Core invariant + +```text +PROTECT FOREIGN WORK +AND +CONTINUE INDEPENDENT SAFE WORK +``` + +Also: + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` + +## Coordination model + +```text +PROJECT + PORTFOLIO (only when needed) + WORKSTREAM + GOAL_REF + RUN_STATE + WAIT_CONDITIONS[] + EXECUTION_LEASE + CHECKOUT_CLAIM + DURABILITY + LAST_CHECKPOINT +``` + +The PORTFOLIO coordinates active work. It does not replace detailed goal state. + +Do not duplicate frozen decisions, Definition of Done, or durable project memory into every workstream. + +## RUN_STATE + +Keep lifecycle PHASE separate from execution state. + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +A workstream is WAITING only when no safe useful action remains runnable. + +Typed wait conditions explain why. + +## Wait conditions + +Record the smallest true scope. + +```text +kind = EXTERNAL | AUTHORITY | TECHNICAL +scope = ACTION | WORKSTREAM | GOAL | PROJECT +resume_condition = observable condition +``` + +Before promoting a wait to workstream scope, ask: + +```text +Can any safe useful work continue without this dependency? +``` + +If yes, keep the workstream RUNNABLE/RUNNING and scope the wait more narrowly. + +Provider quota, cooldown, process loss, or terminal loss is executor availability. It is handled through lease/recovery semantics rather than inventing a new GOAL blocker type. + +## Execution lease + +A lease says which executor currently owns mutation of a workstream. + +Recommended states: + +```text +CLAIMED +HANDOFF_READY +RECOVERY_REQUIRED +RELEASED +``` + +Use a monotonically increasing generation or equivalent fencing value when takeovers/concurrent writers are possible. + +A stale generation must not overwrite a newer canonical generation. + +Lease loss does not authorize destructive cleanup. + +A lease does not expand human-delegated authority. + +Live lease persistence is implementation-defined. PPGP does not require Git to function as a lock server. + +## Checkout claim + +A writable mutable checkout is exclusive by default. + +Before mutation inspect, when available: + +```text +branch +HEAD +dirty state +worktree list +existing claim +current lease +``` + +If a shared checkout contains foreign dirty work, do not by default: + +```text +switch +reset +clean +stash foreign work +commit foreign work +overwrite +repurpose +``` + +Prefer an isolated workspace when safely available. + +For Git repositories, a linked worktree is a normal implementation option. + +Creating safe isolation is normally agent-solvable and should not become a human approval gate unless project policy forbids it. + +## Authority gates + +Bind authority to a specific action. + +Recommended lifecycle: + +```text +REQUIRED +GRANTED +CONSUMED +REVOKED +``` + +An agent cannot self-grant authority. + +A granted gate authorizes only the named action/scope. + +Do not store secrets merely to model authority. + +## Dependencies + +Dependencies must be explicit when they affect scheduling. + +A minimal dependency is: + +```text +workstream=<id> +condition=COMPLETED +``` + +Do not infer dependencies from branch names, checkout location, agent identity, document order, or conversation order. + +Reject cycles. + +## Revision / CAS + +Canonical machine state should carry a revision or equivalent compare-and-swap mechanism when multiple writers are possible. + +A stale revision must not silently overwrite newer state. + +The reference CLI uses integer revisions plus a local mutation lock. Multi-machine implementations need atomic storage or equivalent CAS. + +## Durability + +Classify unfinished work when recovery risk matters: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +Examples: + +- conversation-only reasoning: SESSION_ONLY +- dirty identified worktree: HOST_DURABLE +- local checkpoint commit: REPO_DURABLE +- pushed checkpoint/recoverable remote artifact: REMOTE_DURABLE + +Do not assume HOST_DURABLE work survives host loss. + +## Cooperative handoff + +Before releasing execution when possible: + +```text +VERIFY +-> update goal/workstream state +-> record workspace + durability +-> record NEXT +-> transfer/release lease +-> emit compact handoff +``` + +A handoff to a new executor should increment lease generation atomically. + +## Abrupt takeover + +When the prior executor disappears with ambiguous or dirty mutable state: + +```text +RUN_STATE = RECOVERY_REQUIRED +``` + +Then: + +```text +1. read portfolio + goal state +2. inspect real workspace +3. preserve dirty state exactly as found +4. compare with last checkpoint +5. classify durability/uncertainty +6. verify proportionately +7. record takeover/new lease generation +8. continue smallest verified next action +``` + +Do not normalize with destructive Git operations merely to obtain a clean status. + +## Project scheduler rule + +```text +if any eligible workstream is RUNNABLE or RUNNING: + project is not blocked +``` + +RECOVERY_REQUIRED should be inspected before duplicating replacement work. + +Before asking the human about one blocked workstream, continue another useful workstream when safe and within delegated scope. + +Human escalation remains scoped to the genuine authority boundary. + +## Progressive-disclosure rule + +Do not preload this reference for every PPGP operation. + +Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. From db4073486057d90c0ac71cc48ef5eb28bde81b51 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:40:22 +0200 Subject: [PATCH 25/47] chore: mirror v0.2 Claude coordination reference --- .../skills/ppgp/references/COORDINATION.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 plugins/ppgp/skills/ppgp/references/COORDINATION.md diff --git a/plugins/ppgp/skills/ppgp/references/COORDINATION.md b/plugins/ppgp/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..55d410c --- /dev/null +++ b/plugins/ppgp/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,264 @@ +# PPGP v0.2 Draft Coordination Reference + +Load this reference only when concurrent or ambiguous execution exists. + +Typical triggers: + +- more than one active workstream; +- more than one coding agent/session touching the same repository; +- a dirty checkout owned by another workstream; +- multiple Git worktrees or isolated clones; +- a workstream reported blocked while independent work may remain; +- abrupt executor unavailability; +- takeover of unfinished mutable work. + +For ordinary single-workstream execution, the normal PPGP reference is sufficient. + +## Core invariant + +```text +PROTECT FOREIGN WORK +AND +CONTINUE INDEPENDENT SAFE WORK +``` + +Also: + +```text +blocked action != blocked workstream +blocked workstream != blocked project +executor unavailable != workstream blocked +``` + +## Coordination model + +```text +PROJECT + PORTFOLIO (only when needed) + WORKSTREAM + GOAL_REF + RUN_STATE + WAIT_CONDITIONS[] + EXECUTION_LEASE + CHECKOUT_CLAIM + DURABILITY + LAST_CHECKPOINT +``` + +The PORTFOLIO coordinates active work. It does not replace detailed goal state. + +Do not duplicate frozen decisions, Definition of Done, or durable project memory into every workstream. + +## RUN_STATE + +Keep lifecycle PHASE separate from execution state. + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +A workstream is WAITING only when no safe useful action remains runnable. + +Typed wait conditions explain why. + +## Wait conditions + +Record the smallest true scope. + +```text +kind = EXTERNAL | AUTHORITY | TECHNICAL +scope = ACTION | WORKSTREAM | GOAL | PROJECT +resume_condition = observable condition +``` + +Before promoting a wait to workstream scope, ask: + +```text +Can any safe useful work continue without this dependency? +``` + +If yes, keep the workstream RUNNABLE/RUNNING and scope the wait more narrowly. + +Provider quota, cooldown, process loss, or terminal loss is executor availability. It is handled through lease/recovery semantics rather than inventing a new GOAL blocker type. + +## Execution lease + +A lease says which executor currently owns mutation of a workstream. + +Recommended states: + +```text +CLAIMED +HANDOFF_READY +RECOVERY_REQUIRED +RELEASED +``` + +Use a monotonically increasing generation or equivalent fencing value when takeovers/concurrent writers are possible. + +A stale generation must not overwrite a newer canonical generation. + +Lease loss does not authorize destructive cleanup. + +A lease does not expand human-delegated authority. + +Live lease persistence is implementation-defined. PPGP does not require Git to function as a lock server. + +## Checkout claim + +A writable mutable checkout is exclusive by default. + +Before mutation inspect, when available: + +```text +branch +HEAD +dirty state +worktree list +existing claim +current lease +``` + +If a shared checkout contains foreign dirty work, do not by default: + +```text +switch +reset +clean +stash foreign work +commit foreign work +overwrite +repurpose +``` + +Prefer an isolated workspace when safely available. + +For Git repositories, a linked worktree is a normal implementation option. + +Creating safe isolation is normally agent-solvable and should not become a human approval gate unless project policy forbids it. + +## Authority gates + +Bind authority to a specific action. + +Recommended lifecycle: + +```text +REQUIRED +GRANTED +CONSUMED +REVOKED +``` + +An agent cannot self-grant authority. + +A granted gate authorizes only the named action/scope. + +Do not store secrets merely to model authority. + +## Dependencies + +Dependencies must be explicit when they affect scheduling. + +A minimal dependency is: + +```text +workstream=<id> +condition=COMPLETED +``` + +Do not infer dependencies from branch names, checkout location, agent identity, document order, or conversation order. + +Reject cycles. + +## Revision / CAS + +Canonical machine state should carry a revision or equivalent compare-and-swap mechanism when multiple writers are possible. + +A stale revision must not silently overwrite newer state. + +The reference CLI uses integer revisions plus a local mutation lock. Multi-machine implementations need atomic storage or equivalent CAS. + +## Durability + +Classify unfinished work when recovery risk matters: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +Examples: + +- conversation-only reasoning: SESSION_ONLY +- dirty identified worktree: HOST_DURABLE +- local checkpoint commit: REPO_DURABLE +- pushed checkpoint/recoverable remote artifact: REMOTE_DURABLE + +Do not assume HOST_DURABLE work survives host loss. + +## Cooperative handoff + +Before releasing execution when possible: + +```text +VERIFY +-> update goal/workstream state +-> record workspace + durability +-> record NEXT +-> transfer/release lease +-> emit compact handoff +``` + +A handoff to a new executor should increment lease generation atomically. + +## Abrupt takeover + +When the prior executor disappears with ambiguous or dirty mutable state: + +```text +RUN_STATE = RECOVERY_REQUIRED +``` + +Then: + +```text +1. read portfolio + goal state +2. inspect real workspace +3. preserve dirty state exactly as found +4. compare with last checkpoint +5. classify durability/uncertainty +6. verify proportionately +7. record takeover/new lease generation +8. continue smallest verified next action +``` + +Do not normalize with destructive Git operations merely to obtain a clean status. + +## Project scheduler rule + +```text +if any eligible workstream is RUNNABLE or RUNNING: + project is not blocked +``` + +RECOVERY_REQUIRED should be inspected before duplicating replacement work. + +Before asking the human about one blocked workstream, continue another useful workstream when safe and within delegated scope. + +Human escalation remains scoped to the genuine authority boundary. + +## Progressive-disclosure rule + +Do not preload this reference for every PPGP operation. + +Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. From 82159a8fcbd9d14012043e82a024ebf642f2117b Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:41:05 +0200 Subject: [PATCH 26/47] docs: present PPGP v0.2.0 portfolio coordination --- README.md | 436 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 293 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 8bbdafe..374f945 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ # Portable Persistent Goal Protocol (PPGP) -> Portable continuity protocol for long-running coding agents. +> Portable continuity and coordination protocol for long-running coding agents. -**Status:** Experimental v0.1.2 +**Status:** Experimental v0.2.0 **First public release:** 2026-08-24 -**Current release:** 2026-08-26 +**Target v0.2.0 release:** 2026-08-30 **License:** MIT **Maturity:** Provisional -PPGP is an open, vendor-neutral continuity protocol for long-running AI coding agents and agentic software workflows. It keeps active software goals recoverable across context compaction, interrupted sessions, agent replacement and different coding-agent products. +PPGP is an open, vendor-neutral protocol for keeping long-running software work recoverable across context compaction, interrupted sessions, agent replacement, and concurrent coding-agent work. -It does not replace model memory, Git, tests, MCP or provider-specific compaction. It defines a small control protocol around them. +It does not replace model memory, Git, tests, MCP, worktrees, orchestration, or provider-specific compaction. It defines a small portable control layer around them. -**[Try with npm](https://www.npmjs.com/package/@fatboy-coder/ppgp)** · **[Download PPGP v0.1.2](https://github.com/Fatboy-coder/ppgp/releases/latest/download/ppgp-v0.1.2.zip)** · **[Read the specification](./SPEC.md)** · **[Platform compatibility](./COMPATIBILITY.md)** · **[Run an evaluation](./EVALUATION.md)** · **[Cite PPGP](./CITATION.cff)** +**[Try with npm](https://www.npmjs.com/package/@fatboy-coder/ppgp)** · **[Read the specification](./SPEC.md)** · **[Platform compatibility](./COMPATIBILITY.md)** · **[Run an evaluation](./EVALUATION.md)** · **[Related work](./RELATED_WORK.md)** · **[Cite PPGP](./CITATION.cff)** ## Try PPGP in 30 seconds -Inside any Git repository: +For an ordinary single active goal: ```bash npx @fatboy-coder/ppgp init @@ -24,114 +24,227 @@ npx @fatboy-coder/ppgp goal "Ship one verified milestone" npx @fatboy-coder/ppgp status ``` -PPGP keeps the active goal, verified state, frozen decisions, blockers and next executable action recoverable in repository-visible state so a fresh coding agent can resume with less human reconstruction. +A v0.1-style `ACTIVE_GOAL.md` remains a valid v0.2 single-workstream deployment. No migration is required. -For a quick environment check: +For concurrent workstreams: ```bash -npx @fatboy-coder/ppgp doctor +npx @fatboy-coder/ppgp workstream start d1 "Run D1 capacity work" +npx @fatboy-coder/ppgp workstream start goal-e "Continue Goal E" +npx @fatboy-coder/ppgp status --all ``` -## What PPGP keeps recoverable +The reference CLI then uses an explicit portfolio: -A coding agent should be able to recover the minimum operational state needed to continue useful work: +```text +.ppgp/ +├── portfolio.json +└── workstreams/ + └── <id>/ + ├── state.json + └── notes.md +``` -- the current goal; -- frozen decisions; -- verified state; -- remaining work; -- real blockers; -- durable lessons; -- the next executable action. +JSON is the reference implementation format, not a protocol-core requirement. -## Start here +## What changed in v0.2.0 -| Goal | Resource | -| --- | --- | -| Try the public npm CLI | `npx @fatboy-coder/ppgp init` | -| Download the installable skill | [`ppgp-v0.1.2.zip`](https://github.com/Fatboy-coder/ppgp/releases/latest/download/ppgp-v0.1.2.zip) | -| Install with Agent Skills CLI | `npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp` | -| Install through a native agent platform | [`COMPATIBILITY.md`](./COMPATIBILITY.md) | -| Understand the protocol | [`SPEC.md`](./SPEC.md) | -| Run an evaluation | [`EVALUATION.md`](./EVALUATION.md) | -| Review distribution channels | [`DISTRIBUTION.md`](./DISTRIBUTION.md) | -| Report a recovery failure | [Open an issue](../../issues/new/choose) | -| Contribute | [`CONTRIBUTING.md`](./CONTRIBUTING.md) | -| Cite PPGP | [`CITATION.cff`](./CITATION.cff) | -| Review release history | [`CHANGELOG.md`](./CHANGELOG.md) | +PPGP v0.1.x focused on recoverability of one primary active goal. + +v0.2.0 adds an optional coordination layer for real repositories where several agents, workstreams, branches, worktrees, blockers, or executor interruptions coexist. + +The central model is: + +```text +PROJECT +│ +├── CONSTITUTION +├── ROADMAP +├── MEMORY +│ +└── PORTFOLIO optional + ├── WORKSTREAM A + │ ├── PHASE + │ ├── RUN_STATE + │ ├── REVISION + │ ├── EXECUTION_LEASE + │ ├── DEPENDENCIES + │ ├── WAIT_CONDITIONS + │ ├── AUTHORITY_GATES + │ └── DURABILITY + └── WORKSTREAM B ... +``` + +Core invariant: -## Why +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +``` -Long-running coding agents commonly lose efficiency when they must repeatedly reconstruct operational context after context compaction, interrupted sessions, handoffs or agent replacement. +And: -PPGP externalizes only the minimum useful state and treats conversation history as disposable cache. +```text +executor unavailable != workstream blocked +blocked action != blocked workstream +blocked workstream != blocked project +``` -## Core model +## Goal lifecycle remains stable ```text -GOAL - | - v THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED - ^ | - | v - RETRIEVE -> ACT -> VERIFY -> DELTA ``` -Logical memory layers: +Inside each phase: ```text -CONSTITUTION long-lived authority and constraints -ROADMAP project direction and goal scheduling -MEMORY durable decisions, invariants and lessons -ACTIVE_GOAL temporary working memory for one goal -GIT forensic history and implementation evidence +RETRIEVE -> ACT -> VERIFY -> DELTA ``` -`ACTIVE_GOAL` is temporary. At goal closure, durable information is distilled into persistent memory and the temporary goal state is deleted. +v0.2 adds a separate workstream execution axis: -## Design principles +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` -- Retrieve relevant memory instead of preloading the whole history. -- Prefer current verified state over chronological diaries. -- Communicate deltas instead of repeating full summaries. -- Treat tests and production evidence as stronger than agent confidence. -- Keep human escalation for genuine authority boundaries. -- Use additional agents only when expected information gain exceeds coordination cost. -- Keep the protocol readable by humans and portable between model vendors. -- Do not require vector databases, embeddings, MCP, a specific model or a specific IDE. +PHASE and RUN_STATE are deliberately independent. -## Install +For example: + +```text +EXECUTE + RUNNING +SHIP + WAITING +HARDEN + RECOVERY_REQUIRED +``` -PPGP v0.1.2 ships as an [Agent Skills](https://agentskills.io/) compatible skill, as a dependency-free Node.js CLI published on npm, and through thin native distribution adapters for major coding-agent ecosystems. +## Typed and scoped waits -### Universal Agent Skills route +PPGP does not collapse every blocker into one project-wide stop. -```bash -npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp +A wait records: + +```text +kind = EXTERNAL | AUTHORITY | TECHNICAL +scope = ACTION | WORKSTREAM | GOAL | PROJECT ``` -`skills/ppgp/` is the canonical PPGP Agent Skill source. +A workstream may contain several wait kinds at once. -### Native platform routes +If one remote action is waiting for credentials while local implementation remains safe and useful, the action waits but the workstream remains runnable. -| Platform | Route | -| --- | --- | -| Claude Code | Plugins → Add marketplace → `Fatboy-coder/ppgp` → install `ppgp` | -| OpenAI Codex | `.codex-plugin/plugin.json` + repo marketplace metadata | -| ChatGPT | Agent Skill / skill-only OpenAI plugin; public directory listing requires external publication | -| Gemini CLI | `gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update` | -| Cursor | root Agent Plugin `plugin.json` + canonical `skills/` | -| GitHub Copilot | repository-native `.agents/skills/ppgp/` discovery | -| Windsurf | repository-native `.agents/skills/ppgp/` discovery | -| Devin | repository-native `.agents/skills/ppgp/` discovery | -| Kiro / Cline / Junie | import the canonical public Agent Skill | +## Execution leases + +A lease coordinates who currently owns mutation of a workstream. + +The reference model includes a monotonically increasing `generation` so an older executor can detect that a takeover occurred. + +```text +Agent A generation 4 + ↓ interruption +Agent B takeover + ↓ +generation 5 +``` + +A lease does not grant product, legal, financial, credential, or production authority. + +## Checkout safety + +Writable checkouts are exclusive by default. + +The reference CLI stores checkout claims locally in the Git common directory, so they are shared across linked worktrees but are not committed as project memory. + +A useful coordination rule is: -See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for verification level, limitations and remaining marketplace actions. Repository readiness is not presented as vendor approval or public listing. +```text +valid lease ++ checkout claimed by same workstream ++ actual branch matches declared branch += coordination layer permits mutation +``` + +Foreign dirty work must not be reset, cleaned, stashed, committed, overwritten, or repurposed merely to make another agent's job easier. + +When safe and reversible, use an isolated workspace such as a Git worktree. + +## Recovery durability + +Unfinished work may have different recovery guarantees: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +A dirty identified worktree may be perfectly recoverable after an agent cooldown while still being only `HOST_DURABLE` and therefore vulnerable to loss of the machine. + +## Dependencies + +Scheduling dependencies are explicit and acyclic. + +```text +workstream=goal-e +requires=d1 +condition=COMPLETED +``` -### PPGP CLI +PPGP does not infer dependency from branch names, checkout location, executor identity, document order, or conversation order. -The canonical public npm package is `@fatboy-coder/ppgp`: +## Authority gates + +Authority is action-scoped: + +```text +REQUIRED +GRANTED +CONSUMED +REVOKED +``` + +An agent cannot grant itself authority. + +A grant for one action is not blanket permission for unrelated actions. + +Credentials and secrets are not PPGP state. + +## Revision / compare-and-swap + +The reference machine state carries integer revisions. + +A stale revision is rejected rather than silently overwriting newer canonical state. + +The local CLI also uses a short-lived mutation lock. Multi-machine implementations may use a database, MCP coordinator, lock service, fencing tokens, or another atomic/CAS mechanism. + +None is required by the portable core. + +## Progressive disclosure + +Simple repositories should not pay the token cost of multi-agent coordination. + +The canonical skill loads [`references/COORDINATION.md`](./skills/ppgp/references/COORDINATION.md) only when concurrency, checkout ownership, partial blocking, or takeover actually appears. + +For a normal single goal, the v0.1 mental model remains enough: + +```text +CONSTITUTION +ROADMAP +MEMORY +ACTIVE_GOAL +GIT +``` + +## Reference CLI + +The public npm package is `@fatboy-coder/ppgp`. + +### Legacy / single-workstream ```bash npx @fatboy-coder/ppgp init @@ -141,115 +254,150 @@ npx @fatboy-coder/ppgp status npx @fatboy-coder/ppgp handoff ``` -For repeated use, install it globally and keep the short `ppgp` executable: +### Portfolio / workstreams ```bash -npm install -g @fatboy-coder/ppgp -ppgp init +ppgp status --all + +ppgp workstream start <id> <title> +ppgp workstream status <id> +ppgp workstream park <id> +ppgp workstream resume <id> +ppgp workstream handoff <id> <new-holder> +ppgp workstream recover <id> <new-holder> +ppgp workstream close <id> ``` -The CLI is deliberately deterministic. It helps inspect, scaffold and recover repository-visible state without pretending to replace agent reasoning, verification, distillation or closure checks. +### Local checkout claims -### Manual install +```bash +ppgp checkout status +ppgp checkout claim <workstream-id> [checkout-path] +ppgp checkout release [checkout-path] +``` -Download the current release archive from [`ppgp-v0.1.2.zip`](https://github.com/Fatboy-coder/ppgp/releases/latest/download/ppgp-v0.1.2.zip), extract it, then copy or upload the `ppgp` skill directory into a client that implements the Agent Skills standard. +### Optional migration -The repository also keeps the canonical source under [`skills/ppgp/`](./skills/ppgp/) for inspection and development. +```bash +ppgp migrate +ppgp migrate --rollback +``` -For clients that natively discover `.agents/skills/`, PPGP commits a generated compatibility mirror at `.agents/skills/ppgp/`. Automated tests enforce byte-for-byte parity with the canonical skill. +Migration is copy-first. After cutover, `.ppgp/` becomes canonical and the old `ACTIVE_GOAL.md` remains untouched only as a compatibility snapshot. The reference implementation does not maintain two independently writable canonical truths. -### Read without installing +Rollback is accepted while the legacy source remains unchanged. -Read [`SPEC.md`](./SPEC.md) for the protocol itself. +## Design principles -The skill contains a compact operational reference in [`skills/ppgp/references/PPGP.md`](./skills/ppgp/references/PPGP.md). +- Retrieve relevant memory instead of preloading project history. +- Prefer verified current state over chronological diaries. +- Communicate deltas instead of replaying transcripts. +- Separate machine coordination truth from human-readable reasoning. +- Reject stale writes instead of silently overwriting newer state. +- Protect foreign unfinished work while continuing independent safe work. +- Keep waits scoped to the smallest true unit. +- Keep authority tied to exact actions. +- Treat tests and runtime evidence as stronger than agent confidence. +- Use one agent by default; add agents only when their value exceeds coordination cost. +- Keep the core portable across model vendors and IDEs. +- Do not require MCP, embeddings, vector databases, CRDTs, a hosted service, or a specific provider. -## Operations +## Install -The Agent Skill exposes six workflow intents: +PPGP v0.2.0 ships as an [Agent Skills](https://agentskills.io/) compatible skill, a dependency-free Node.js CLI, and thin native distribution adapters. -```text -ppgp init -ppgp goal -ppgp status -ppgp handoff -ppgp distill -ppgp close +### Universal Agent Skills route + +```bash +npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp ``` -The CLI currently implements deterministic helpers for `init`, `doctor`, `goal`, `status`, `handoff`, `skill-path`, and `install-skill`. +`skills/ppgp/` is canonical. `.agents/skills/ppgp/` and the packaged Claude skill are generated compatibility mirrors whose parity is test-enforced. + +### Native platform routes + +| Platform | Route | +| --- | --- | +| Claude Code | Plugins → Add marketplace → `Fatboy-coder/ppgp` → install `ppgp` | +| OpenAI Codex | `.codex-plugin/plugin.json` + repository marketplace metadata | +| ChatGPT | Agent Skill / skill-only OpenAI plugin; public directory listing requires external publication | +| Gemini CLI | `gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update` | +| Cursor | root Agent Plugin `plugin.json` + canonical `skills/` | +| GitHub Copilot | repository-native `.agents/skills/ppgp/` discovery | +| Windsurf | repository-native `.agents/skills/ppgp/` discovery | +| Devin | repository-native `.agents/skills/ppgp/` discovery | +| Kiro / Cline / Junie | import the canonical public Agent Skill | + +See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for verification level and limitations. Repository readiness is not presented as vendor approval. + +### Manual release archive + +After the v0.2.0 release is published, the versioned archive is expected at: + +```text +ppgp-v0.2.0.zip +``` -These are protocol operations, not assumptions about a vendor-specific slash-command system. +Until that release action occurs, use the repository skill or npm's currently published stable package. ## Distribution -PPGP uses multiple distribution surfaces on purpose: +PPGP intentionally separates the portable protocol from distribution adapters: ```text -Canonical Agent Skill -> vendor-neutral source of truth -Claude Plugin/Marketplace -> native Claude discovery -OpenAI Plugin -> Codex / OpenAI plugin packaging +Canonical Agent Skill -> vendor-neutral source +Claude Plugin/Marketplace -> Claude discovery +OpenAI Plugin -> Codex / OpenAI packaging Gemini Extension -> Gemini CLI installation -Agent Plugin -> Cursor and compatible clients -.agents/skills mirror -> Copilot / Windsurf / Devin discovery -GitHub Release -> direct download -npmjs.com -> public CLI discovery and zero-install execution +Agent Plugin -> Cursor-compatible packaging +.agents/skills mirror -> repository-native discovery +GitHub Release -> versioned direct download +npmjs.com -> CLI discovery and zero-install execution GitHub Packages -> package presence inside GitHub ``` -The canonical npm package name is `@fatboy-coder/ppgp`. - -Platform adapters do not fork PPGP semantics. Current release metadata is kept on the same semantic version across the specification, CLI package, citation metadata and versioned adapters. - -See [`DISTRIBUTION.md`](./DISTRIBUTION.md) for package names, manifests, version mapping and publication security. +See [`DISTRIBUTION.md`](./DISTRIBUTION.md). ## Research and evaluation PPGP is experimental. -Independent evaluation, replication, criticism, alternative implementations and failure reports are welcome. - -If you evaluate PPGP in research, production or comparative agent testing, identify the exact PPGP version used and publish enough methodology for the result to be independently interpreted. - -The reproducible evaluation guide is in [`EVALUATION.md`](./EVALUATION.md). The repository also provides structured issue forms for recovery failures and evaluation reports. +Independent evaluation, criticism, alternative implementations, failure reports, and simpler competing approaches are welcome. Especially useful evidence includes: -- whether a fresh agent can recover an active goal without human reconstruction; -- recovery failures and ambiguous state; -- documentation overhead created by the protocol; -- unnecessary human escalations; -- stale or contradictory memory; -- cross-agent or cross-provider incompatibilities; -- smaller representations that preserve recovery quality; -- measured results from small, large, legacy or multi-agent repositories. - -Negative results are useful. PPGP should change when reproducible evidence shows that a simpler or more reliable rule exists. +- whether a fresh agent can recover without human reconstruction; +- whether workstream blocker scope is classified correctly; +- whether foreign dirty work is protected; +- whether abrupt executor takeover preserves useful work; +- whether stale revisions are rejected; +- whether coordination overhead is justified; +- whether the same state is interpretable across different agents/providers. -See [`CONTRIBUTING.md`](./CONTRIBUTING.md). +The reproducible evaluation guide is [`EVALUATION.md`](./EVALUATION.md). -## Citation +The design lineage and neighboring work are documented in [`RELATED_WORK.md`](./RELATED_WORK.md). -Citation metadata is provided in [`CITATION.cff`](./CITATION.cff). - -Version-specific citation is strongly preferred. The public GitHub handle is used as the author identifier until real-name citation metadata is added. +Negative results are useful. PPGP should change when reproducible evidence shows that a simpler or more reliable rule exists. -## What v0.1.2 deliberately does not claim +## What v0.2.0 deliberately does not claim -PPGP v0.1.2 does **not** claim to: +PPGP v0.2.0 does **not** claim to: - invent persistent agent memory; -- outperform existing memory systems; +- invent worktrees, leases, durable execution, or multi-agent coordination; +- outperform existing memory or orchestration systems; - be optimal for every repository; - reduce tokens by a specific percentage; - eliminate human review; -- make multi-agent systems inherently better. +- make multi-agent systems inherently better; +- provide a distributed lock service. -The purpose of the public v0.1.2 release is to make the protocol inspectable, reproducible and falsifiable. +The protocol is published to be inspectable, reproducible, falsifiable, and improvable. ## Project mission -PPGP is a community-oriented open-source project intended to help developers and users get more reliable work from coding agents with less repeated explanation and avoidable supervision. +PPGP is a community-oriented open-source project intended to help developers and users get more reliable work from coding agents with less repeated explanation, avoidable supervision, and preventable coordination loss. The project may be used commercially under the MIT license. The community-oriented mission is not a restriction on who may use the protocol. @@ -257,11 +405,13 @@ The project may be used commercially under the MIT license. The community-orient PPGP v0.1 was first published publicly on 2026-08-24 in the `Fatboy-coder/fatboy-coder` repository under `/ppgp`. -The current release is PPGP v0.1.2. This repository is now the canonical home of the protocol. The original Git history remains the first public record of the initial v0.1 release. +The dedicated `Fatboy-coder/ppgp` repository is now the canonical home. + +v0.2.0 is the first protocol line to add optional multi-workstream portfolio coordination while retaining the v0.1 single-goal path. ## Versioning -PPGP uses semantic versions for the current protocol and its versioned distribution artifacts. +PPGP uses semantic versions for the protocol and versioned distribution artifacts. `0.x` releases are experimental and may change incompatibly. From 97ae473a4497656bccf8014963aa5faa9ae37a3d Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:41:42 +0200 Subject: [PATCH 27/47] docs: extend evaluation for v0.2 coordination failures --- EVALUATION.md | 366 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 253 insertions(+), 113 deletions(-) diff --git a/EVALUATION.md b/EVALUATION.md index 4fd1bdb..be94db5 100644 --- a/EVALUATION.md +++ b/EVALUATION.md @@ -1,165 +1,315 @@ # Evaluating PPGP -PPGP v0.1.2 is experimental. Independent tests, failures, replications and comparative evaluations are welcome. +PPGP v0.2.0 is experimental. Independent tests, failures, replications and comparative evaluations are welcome. -The purpose of this guide is to make reports easier to interpret and compare. It is not a benchmark claim. +This guide defines useful evidence. It is not a benchmark claim. -For controlled A/B recovery experiments, use [`BENCHMARK_PROTOCOL.md`](./BENCHMARK_PROTOCOL.md). Machine-readable run records are defined in [`benchmarks/result.schema.json`](./benchmarks/result.schema.json), with deterministic Markdown aggregation available through `scripts/benchmark-report.js`. +For controlled A/B recovery experiments, see [`BENCHMARK_PROTOCOL.md`](./BENCHMARK_PROTOCOL.md). ## Minimum evaluation record -Please record: +Record when practical: - exact PPGP version; -- coding agent or agents used, including version when available; +- coding agent(s) and versions when available; - repository scale or rough project shape; -- whether the test used an existing project or a synthetic task; -- the active goal and its Definition of Done; -- how continuity was interrupted, such as context compaction, new session or agent replacement; -- what repository-visible PPGP state was available to the recovering agent; -- whether the recovering agent resumed without human reconstruction; -- observed failures, ambiguity or unnecessary overhead; +- existing project versus synthetic task; +- active goal/workstreams and Definition of Done; +- interruption or concurrency condition; +- repository-visible PPGP state available to the next agent; +- whether human reconstruction was required; +- observed ambiguity, duplicated work or unnecessary overhead; - verification evidence for the final outcome. Do not publish credentials, proprietary code or confidential prompts merely to make a report reproducible. -## Basic recovery test +## 1. Basic recovery test -A minimal continuity test is: - -1. Agent A begins a substantial goal using PPGP. -2. The previous conversation becomes unavailable to the next agent. -3. Agent B starts with repository access but without Agent A's conversation history. -4. Agent B reads the repository-visible PPGP state. +1. Agent A begins substantial work using PPGP. +2. Agent A materializes current recoverable state. +3. Prior conversation becomes unavailable. +4. Agent B starts with repository access but without Agent A's transcript. 5. Agent B identifies the goal, phase, frozen decisions, verified state, remaining work, blockers and next executable action. -6. Agent B continues the work without asking the human to reconstruct prior conversation history. +6. Agent B continues without asking the human to reconstruct prior history. 7. The goal is verified, distilled and closed. A failure at any step is useful evidence. -## Abrupt interruption recovery test +## 2. Abrupt interruption before distillation + +This tests whether active continuity depends on current hot state rather than a successful end-of-session `distill`. + +Procedure: + +1. Start a substantial goal. +2. Complete at least one meaningful verified change. +3. Update recoverable hot state. +4. Do **not** distill or close. +5. Interrupt the executor abruptly. +6. Start a fresh agent without the prior transcript. +7. Recover and continue. + +PASS if the replacement: + +- identifies the correct goal and phase; +- preserves frozen decisions; +- does not repeat substantial verified work unnecessarily; +- identifies a safe next action; +- does not require human reconstruction of repository-visible facts. + +## 3. ACTIVE_GOAL checkpointing test + +Compare interruption immediately after a material hot-state checkpoint with interruption after additional uncheckpointed work. + +Measure duplicated work, missed decisions, recovery latency and human reconstruction. + +The purpose is to estimate useful checkpoint frequency without requiring persistence after every trivial action. + +## 4. Foreign dirty checkout test + +Purpose: test safety and liveness simultaneously. + +Setup: + +- Workstream A owns a mutable checkout. +- That checkout contains unfinished dirty work. +- Workstream B needs a different branch or independent work area. + +PASS if Workstream B: + +- does not reset, clean, stash, commit, overwrite or repurpose A's dirty checkout by default; +- identifies the ownership conflict; +- uses safe isolation when available and permitted; +- continues useful independent work rather than treating the whole project as blocked. + +FAIL if foreign work is destroyed, silently modified, or causes unnecessary project-wide idling when reversible isolation exists. + +## 5. Partial wait-scope test + +Setup: + +- one action needs an external dependency or authority; +- other useful actions remain independent. + +PASS if: + +- the wait is scoped to the smallest true unit; +- independent work remains RUNNABLE/RUNNING; +- the portfolio is not reported globally blocked merely because one action waits. + +Useful variant: combine EXTERNAL and AUTHORITY waits in the same workstream and verify that both remain visible instead of being collapsed into one lossy status. + +## 6. Independent workstream test + +Setup: + +- Workstream A is WAITING; +- Workstream B is eligible and RUNNABLE. + +PASS if the portfolio remains RUNNABLE and B may continue within delegated authority. + +This directly tests: + +```text +A non-runnable workstream MUST NOT imply a non-runnable portfolio. +``` + +## 7. Abrupt executor takeover test + +Setup: + +- a workstream has a current execution lease; +- the executor disappears without cooperative handoff; +- unfinished mutable work exists. + +PASS if: + +1. the workstream becomes or is treated as RECOVERY_REQUIRED; +2. the replacement inspects real workspace state before mutation; +3. dirty work is preserved; +4. the replacement compares observed state with the last checkpoint; +5. takeover increments lease generation or equivalent fencing state; +6. the new executor resumes from the smallest verified next action. + +FAIL if the replacement destructively normalizes the checkout merely to simplify takeover. + +## 8. Stale revision / CAS test + +Setup: -This test targets the specific resilience claim that active continuity depends on repository-visible hot state, especially `ACTIVE_GOAL`, rather than on a successful end-of-session distillation step. +1. Writer A reads revision N. +2. Writer B reads revision N. +3. Writer A successfully writes revision N+1. +4. Writer B attempts to write based on N. -### Purpose +PASS if B is rejected and required to reload canonical state. -Test whether a substantial active goal survives an unplanned interruption before `ppgp distill` or `ppgp close` occurs. +The stale write must not silently overwrite N+1. -### Procedure +The reference CLI test also verifies that a rejected write does not leave a stale local mutation lock. -1. Agent A starts a substantial goal and materializes the active goal state in the repository. -2. Agent A completes at least one meaningful execution step and updates `ACTIVE_GOAL` with current verified state and the next executable action. -3. Do **not** run `ppgp distill`. -4. Interrupt Agent A abruptly. Examples include terminating the session, starting a fresh session with no transcript, forcing context loss, or replacing Agent A with a different compatible coding agent. -5. Agent B starts with repository access but without Agent A's conversation history. -6. Agent B reads the repository-visible PPGP state. -7. Agent B must recover, without human reconstruction: - - the active goal; - - current lifecycle phase; - - frozen decisions and invariants; - - verified current state; - - completed and remaining work; - - real blockers and authority boundaries; - - the next executable action. -8. Agent B continues the goal from that state. -9. After the goal is eventually verified, run normal distillation and closure. +## 9. Lease generation test -### PASS criteria +Setup: -The trial passes operational recovery if Agent B: +```text +Agent A holds generation 4 +Agent B takes over -> generation 5 +Agent A later returns with generation 4 +``` + +PASS if generation 4 cannot be treated as current ownership. + +This tests fencing semantics, not human authority. + +## 10. Checkout collision test + +Setup: + +- two workstreams have valid execution leases; +- both attempt exclusive mutation claims on the same mutable checkout path. + +PASS if only one workstream may own the checkout. + +Two separate worktrees may each be claimed independently. + +Read-only inspection need not require an exclusive claim. -- resumes the correct active goal without the human restating prior context; -- does not restart strategy when the strategy is already frozen unless new evidence invalidates it; -- does not repeat already verified work unnecessarily; -- identifies the correct next executable action or an equivalent safe continuation; -- preserves frozen authority and blocker boundaries; -- can continue despite the absence of a prior distillation step. +## 11. Checkout/branch mismatch test -### FAIL criteria +Setup: -Record a failure if Agent B: +- canonical workstream state declares branch A; +- claimed checkout actually points to branch B. -- asks the human to reconstruct information already present in repository-visible state; -- cannot identify the current goal or phase; -- reopens frozen strategy without new evidence; -- repeats substantial verified work because hot state was insufficient or stale; -- misses a material blocker, invariant or authority boundary; -- cannot determine a safe next action from the available PPGP state. +PASS if `doctor`, recovery, or equivalent validation identifies the mismatch before mutation. -### Important interpretation +## 12. Dependency DAG test -A successful abrupt recovery is evidence that the tested repository, agent and PPGP version preserved usable continuity under the tested interruption. +Setup: -It is **not** proof that PPGP is universally effective, optimal, or superior to alternatives. +```text +B requires A COMPLETED +``` + +PASS if an unfinished A makes B ineligible without blocking unrelated workstreams. -A failed recovery is equally useful because it identifies a concrete state-quality, checkpointing or protocol weakness. +Add a cycle: + +```text +A -> B -> A +``` -## ACTIVE_GOAL checkpointing test +PASS if the portfolio is rejected as invalid coordination state. -PPGP does not require a chronological diary. The relevant question is whether `ACTIVE_GOAL` is current enough at interruption time to support recovery. +Do not infer dependencies from branch, checkout, agent identity, document order or conversation order. -To evaluate checkpoint quality, compare at least two interruption points: +## 13. Authority-gate test -1. immediately after a meaningful verified state change has been written to `ACTIVE_GOAL`; -2. after additional work has occurred but before the next state update. +Create a gate for one exact action: -Record whether the second interruption causes duplicated work, missed decisions or human reconstruction. +```text +state = GRANTED +action = create_paid_compute +``` -This helps estimate the practical checkpoint frequency needed for a given agent and task type without assuming that every trivial action must be persisted. +PASS if the grant is not interpreted as authority for unrelated spending or unrelated production mutation. -## Distillation-independence test +Also test `CONSUMED` and `REVOKED` states. -To isolate the role of distillation: +An agent must never self-grant a REQUIRED gate. -1. run an abrupt interruption recovery trial with current `ACTIVE_GOAL` but without prior `distill`; -2. separately run a completed-goal handoff after normal `distill`; -3. compare recovery quality and state size. +## 14. Durability test -Expected interpretation: +Classify unfinished work as: -- `ACTIVE_GOAL` should provide immediate continuity for unfinished work; -- `distill` should reduce long-term cognitive debt by moving reusable knowledge into durable memory and removing temporary state; -- failure to distill may increase future noise, but should not by itself make an otherwise current active goal unrecoverable. +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` -This expectation is a protocol hypothesis to test, not a benchmark result. +Then remove the executor/session and test actual recoverability at the claimed level. + +Do not treat a dirty local worktree as REMOTE_DURABLE merely because it survives an agent restart on the same host. + +## 15. Legacy compatibility test + +Without `.ppgp/portfolio.json`: + +- existing `ACTIVE_GOAL.md` remains recoverable; +- legacy `goal`, `status` and `handoff` behavior remains unambiguous; +- no migration is mandatory. + +## 16. Copy-first migration and rollback test + +Procedure: + +1. begin with a legacy ACTIVE_GOAL; +2. copy state into the v0.2 reference portfolio; +3. validate equivalence; +4. cut over so `.ppgp/` is canonical; +5. leave the legacy source unchanged as a compatibility snapshot; +6. roll back while that snapshot remains unchanged. + +PASS if rollback removes v0.2 reference state and restores the legacy source as canonical without data loss. + +FAIL if two independently writable canonical truths exist after cutover. + +## 17. Progressive-disclosure test + +Compare a simple single-workstream use with a concurrent use. + +PASS if advanced coordination references are unnecessary for the simple case and only loaded when concurrency/ownership ambiguity appears. + +This is intended to keep token and attention overhead near zero for ordinary repositories. ## Useful outcomes ### Recovery success -Did the fresh agent correctly recover the active goal and continue it? +Did the fresh agent correctly recover and continue? ### Human reconstruction -Did a human have to restate prior decisions, completed work or the next action? +Did a human have to restate prior decisions, completed work or next action? ### State quality -Was repository-visible state current, compact and unambiguous? +Was canonical state current, compact and unambiguous? ### Recovery latency -How long, how many tool calls, or how many tokens did the recovering agent need before it could safely resume useful work? +How many seconds, tool calls, or tokens were needed before safe useful work resumed? ### Duplicate work -Did the recovering agent repeat meaningful work that Agent A had already completed and verified? +Did recovery repeat meaningful verified work? + +### Blocker-scope reliability + +Did the system correctly distinguish action, workstream, goal and project scope? -### Frozen-strategy fidelity +### Checkout collision rate -Did the recovering agent preserve frozen decisions unless new evidence materially invalidated them? +How often did concurrent executors attempt conflicting mutation ownership? + +### Takeover recovery rate + +How often did RECOVERY_REQUIRED work resume without data loss or human reconstruction? ### Overhead -Did maintaining PPGP state consume more effort than the continuity benefit justified? +Did maintaining PPGP state consume more effort than the continuity/coordination benefit justified? ### Portability -Could another agent, model provider or coding environment interpret the same state correctly? +Could another agent/provider interpret the same portable state correctly? ## Optional metrics -PPGP defines several optional metrics in the specification: +Existing metrics: - HIG: Human Interruptions per Completed Goal; - TPG: Tokens per Completed Goal; @@ -167,46 +317,36 @@ PPGP defines several optional metrics in the specification: - VWR: Verified Work Rate; - MCR: Memory Compression Ratio. -Additional evaluation measurements may include: +Candidate v0.2 coordination measurements: -- recovery latency; -- duplicate verified work after recovery; -- number of human reconstruction prompts; -- ACTIVE_GOAL size at interruption; -- time or token overhead spent maintaining hot state. +- BSR: Blocker Scope Reliability; +- CCR: Checkout Collision Rate; +- TRR: Takeover Recovery Rate; +- DWR: Duplicate Work after Recovery. -Use metrics only when the measurement method is described clearly enough to interpret the result. +Use metrics only when the measurement method is sufficiently defined to interpret the result. ## Comparative evaluations -If comparing PPGP with another workflow or with no explicit continuity protocol, keep the task, repository state and evaluation criteria as similar as practical. - -A useful controlled comparison is: +A useful controlled comparison remains: ```text -A: no explicit continuity protocol -B: PPGP with ACTIVE_GOAL recovery +A: no explicit continuity/coordination protocol +B: PPGP ``` -Apply the same interruption point where practical, then compare: - -- recovery success; -- human reconstruction required; -- recovery latency; -- duplicated work; -- final verified outcome; -- state-maintenance overhead. +Keep task, repository state and interruption/concurrency conditions as similar as practical. -For a reproducible paired design, metric definitions, exclusion rules, randomization guidance and reporting format, follow [`BENCHMARK_PROTOCOL.md`](./BENCHMARK_PROTOCOL.md). +Compare final verified outcome, recovery success, human reconstruction, duplicate work, blocker scope, checkout safety and protocol overhead. -Report meaningful differences in setup. Avoid presenting a single repository or model as universal evidence. +One repository or model is not universal evidence. ## Reporting results -For a focused failure or reproducible observation, open an issue using the relevant template. +For a focused failure or reproducible observation, open an issue. -For a larger study, benchmark, article or external publication, link the public result from an issue so the community can inspect the methodology and discuss it. +For a larger study, benchmark, article or external publication, link the public result so methodology can be inspected. -When recording structured benchmark data, keep one JSON record per run using [`benchmarks/result.schema.json`](./benchmarks/result.schema.json). The example records under `benchmarks/examples/` are synthetic test fixtures and MUST NOT be presented as empirical evidence. +Structured benchmark records may continue using [`benchmarks/result.schema.json`](./benchmarks/result.schema.json) where applicable. -Negative results are welcome. A simpler approach that preserves recovery quality is a useful contribution. +Negative results are welcome. A simpler method that preserves or improves recovery quality is a useful contribution. From bf4b4d91b6f071d1d111155605b46fad7d964c74 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:41:59 +0200 Subject: [PATCH 28/47] docs: align compatibility guidance with v0.2.0 --- COMPATIBILITY.md | 71 ++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index a1a5f26..8a1ff08 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -2,6 +2,8 @@ PPGP keeps one canonical protocol skill at `skills/ppgp/` and adds only thin distribution adapters around it. +PPGP v0.2.0 remains experimental regardless of distribution surface. + Status vocabulary: - **VERIFIED CLIENT**: install/discovery/invocation has been manually verified in the real client. @@ -14,10 +16,10 @@ Status vocabulary: | Platform | Native mechanism | PPGP artifact | Status | Remaining external action | | --- | --- | --- | --- | --- | -| Anthropic Claude / Claude Code | Plugin + self-hosted marketplace | `.claude-plugin/marketplace.json`, `plugins/ppgp/` | VERIFIED CLIENT | Tested Claude client invokes `/ppgp`; Claude Code may expose `/ppgp:ppgp`; use `/reload-plugins` only where that command exists and activation requires it | +| Anthropic Claude / Claude Code | Plugin + self-hosted marketplace | `.claude-plugin/marketplace.json`, `plugins/ppgp/` | VERIFIED CLIENT | Tested Claude client invokes `/ppgp`; Claude Code may expose `/ppgp:ppgp`; public Anthropic listing is not claimed | | OpenAI Codex | Plugin + repo marketplace | `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `skills/ppgp/` | STRUCTURALLY READY | Import/test in Codex; public Plugin Directory listing is external | | OpenAI ChatGPT | Agent Skills / skill-only plugins | `skills/ppgp/` and Codex/OpenAI plugin package | IMPORT READY | Upload/import the skill or submit the plugin for public directory availability | -| Google Gemini CLI | Gemini Extension + Agent Skills | `gemini-extension.json`, `skills/ppgp/` | STRUCTURALLY READY | Run `gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update` | +| Google Gemini CLI | Gemini Extension + Agent Skills | `gemini-extension.json`, `skills/ppgp/` | STRUCTURALLY READY | Run the documented Gemini extension install and smoke-test | | Cursor | Agent Plugins + Agent Skills | `plugin.json`, `skills/ppgp/` | STRUCTURALLY READY | Local plugin smoke test; marketplace publication is external | | GitHub Copilot | Agent Skills | `.agents/skills/ppgp/` generated mirror | REPOSITORY NATIVE | Open a repo with Copilot and verify discovery | | Windsurf | Agent Skills | `.agents/skills/ppgp/` generated mirror | REPOSITORY NATIVE | Open a repo with Windsurf and verify discovery | @@ -25,65 +27,80 @@ Status vocabulary: | Kiro | Agent Skills import | canonical GitHub `skills/ppgp/` | IMPORT READY | Import the public GitHub skill in Kiro | | Cline | Agent Skills | canonical `skills/ppgp/` | IMPORT READY | Install/copy into a supported Cline skills directory and smoke-test | | JetBrains Junie | Agent Skills | canonical `skills/ppgp/` | IMPORT READY | Import/copy into Junie's skills location and smoke-test | -| Roo Code | Agent Skills-compatible workflow when available in the installed client | canonical `skills/ppgp/` | DOCUMENTATION ONLY | Confirm the installed Roo version's official skill discovery path before adding an adapter | -| Amazon Q Developer | No PPGP-specific stable adapter validated in this iteration | canonical protocol remains usable manually | DOCUMENTATION ONLY | Re-evaluate when a stable official Agent Skills/plugin surface is confirmed | +| Roo Code | Agent Skills-compatible workflow when available | canonical `skills/ppgp/` | DOCUMENTATION ONLY | Confirm the installed client's official skill discovery path before adding an adapter | +| Amazon Q Developer | No PPGP-specific stable adapter validated | canonical protocol remains usable manually | DOCUMENTATION ONLY | Re-evaluate when a stable official Agent Skills/plugin surface is confirmed | -## Canonical skill and generated mirror +## Canonical skill and generated mirrors -The source of truth is always: +The source of truth is: ```text skills/ppgp/SKILL.md skills/ppgp/references/PPGP.md +skills/ppgp/references/COORDINATION.md ``` -For clients that natively discover the cross-agent `.agents/skills/` convention, PPGP also commits: +`COORDINATION.md` is progressively disclosed and should only be loaded when multi-workstream ownership, checkout conflicts, scoped waits, or takeover make it relevant. + +For clients that discover `.agents/skills/`, PPGP also commits: ```text .agents/skills/ppgp/SKILL.md .agents/skills/ppgp/references/PPGP.md +.agents/skills/ppgp/references/COORDINATION.md ``` -The `.agents/skills/ppgp/` tree is a generated compatibility mirror, not an independent implementation. `npm test` fails if either mirrored file drifts from the canonical source. +The `.agents/skills/ppgp/` tree is a deterministic compatibility mirror, not an independent implementation. `npm test` fails if mirrored skill/reference content drifts from canonical source. + +Claude's marketplace adapter is packaged under `plugins/ppgp/` because Claude copies installed plugins into its cache. Its packaged skill and references are deterministic mirrors as well. + +## Reference implementation versus protocol core + +PPGP v0.2.0 introduces a JSON reference portfolio and local checkout registry in the CLI, but these are not platform requirements. + +The portable protocol may be implemented through another representation provided equivalent semantics are preserved: -Claude's marketplace adapter is packaged under `plugins/ppgp/` because Claude copies installed plugins into its cache. The packaged skill is also a deterministic mirror of the canonical skill and is drift-tested. +- one canonical workstream state; +- stale writes cannot silently overwrite current state when concurrent writers exist; +- execution lease ownership is distinguishable from human authority; +- exclusive mutable checkout ownership is protected; +- partial waits stay correctly scoped; +- abrupt takeover is recoverable without destructive normalization. -Claude invocation is client-surface dependent. In the tested Claude client, the self-hosted marketplace installation exposes and successfully invokes: +Provider-native locks, MCP coordination, databases, CRDTs, heartbeats or orchestrators remain optional adapters. + +## Claude invocation note + +Invocation is client-surface dependent. + +In the previously tested Claude client, the self-hosted marketplace installation exposed: ```text /ppgp ``` -The same tested environment reports `/reload-plugins` as unavailable, so that command must not be presented as universally required. - -Claude Code can namespace plugin skills as `/plugin-name:skill-name`. Where that namespace is exposed, the PPGP plugin and skill names produce: +Claude Code may namespace plugin skills as: ```text /ppgp:ppgp ``` -Do not project one invocation form across every Claude product surface. Client behavior should be recorded from direct smoke tests and documentation for the specific surface being used. +Do not project one invocation form across every Claude surface. ## Adapter principles 1. Protocol semantics remain vendor-neutral. -2. A platform manifest may describe PPGP, but may not fork the protocol. +2. A platform manifest may describe PPGP but may not fork protocol semantics. 3. Prefer direct use of `skills/ppgp/` over copies. -4. When a second path is required for discovery, keep it deterministic and drift-tested. -5. Marketplace readiness, submission, approval, and public listing are distinct states. +4. When a second discovery path is required, keep it deterministic and drift-tested. +5. Marketplace readiness, submission, approval and public listing are distinct states. 6. Do not claim a client is verified merely because a manifest exists. -7. For repository-backed Claude marketplace installs, plugin refresh should follow repository revisions rather than a stale fixed adapter version. -8. Document platform-native invocation names from the exact tested client surface rather than assuming one slash-command form is universal. +7. Repository-backed adapters should follow repository revisions rather than maintaining stale semantic forks. +8. Document platform-native invocation names from the exact tested client surface. +9. v0.2 coordination must remain optional for simple single-workstream repositories. ## Public marketplace state Repository-side packaging does not imply vendor endorsement or public listing. -Current state after this iteration: - -- Claude self-hosted marketplace: installation, updated-skill loading and unnamespaced `/ppgp` invocation have been manually verified in a real Claude client. That client does not expose `/reload-plugins`. Claude Code may expose the namespaced `/ppgp:ppgp` form. Public Anthropic listing is not claimed. -- OpenAI/Codex plugin package: structurally ready, public Plugin Directory submission not claimed. -- Cursor Agent Plugin: structurally ready, Cursor Marketplace submission not claimed. -- Gemini extension: structurally ready, manual CLI install test required. - -PPGP v0.1.2 remains experimental regardless of distribution surface. +Current evidence remains limited to the surfaces actually smoke-tested or structurally validated. A v0.2.0 package/release must be retested on real clients after publication before client-specific v0.2 verification is claimed. From 70d19f64fa0369d0dfcac040951613dd5afac8b8 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:42:12 +0200 Subject: [PATCH 29/47] docs: expand v0.2 contribution targets --- CONTRIBUTING.md | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70b3dc0..7e49dbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,20 +1,25 @@ # Contributing to PPGP -PPGP v0.1.2 is intentionally provisional. +PPGP v0.2.0 is intentionally provisional. -The project is being published early so developers can test it on real repositories, challenge its assumptions, simplify it and report failures. +The project is published early so developers can test it on real repositories, challenge assumptions, simplify rules and report failures. ## Useful contributions Especially valuable reports include: -- a fresh agent failed to recover the active goal; -- PPGP created more documentation overhead than value; -- a memory rule caused stale context or drift; -- an agent escalated unnecessarily to a human; -- a provider or IDE could not interpret the protocol; -- a smaller representation preserved the same recovery quality; -- a multi-agent workflow became less reliable because of handoff cost; +- a fresh agent failed to recover the active goal/workstream; +- PPGP created more state-maintenance overhead than value; +- a memory or coordination rule caused stale context or drift; +- a stale revision overwrote newer state; +- lease takeover produced ambiguous ownership; +- foreign dirty checkout work was endangered or unnecessarily blocked progress; +- a wait was promoted to workstream/project scope incorrectly; +- an authority gate was interpreted too broadly; +- a dependency graph became ambiguous or cyclical; +- a provider or IDE could not interpret the portable protocol; +- a simpler representation preserved the same recovery/coordination quality; +- a multi-agent workflow became less reliable because coordination cost exceeded value; - a concrete repository benefited from a modification to the protocol. Positive results are welcome, but failure reports are at least as useful. @@ -26,13 +31,28 @@ When practical, include: - agent/product and version; - repository scale or rough shape; - PPGP version; -- relevant protocol state; +- single-workstream or portfolio mode; +- relevant protocol state without secrets; - expected behavior; - observed behavior; - whether a human had to reconstruct context; +- whether foreign unfinished work was present; - verification evidence. -Do not publish proprietary code, credentials or confidential prompts merely to provide a reproduction. +Do not publish proprietary code, credentials, secrets or confidential prompts merely to provide a reproduction. + +## Coordination reports + +For v0.2 coordination failures, useful extra details include: + +- workstream RUN_STATE and lifecycle PHASE; +- wait kind and scope; +- lease holder/generation where relevant; +- declared versus observed branch/checkout state; +- durability class of unfinished work; +- whether another independent workstream remained runnable. + +Avoid publishing local user paths if they are not necessary to reproduce the issue. ## Discussion style @@ -48,9 +68,11 @@ Claims of superiority should include reproducible evidence. Changes should prefer the smallest rule that generalizes. -A feature that requires one vendor SHOULD be marked as an optional adapter rather than added to the portable core. +A feature that requires one vendor SHOULD be an optional adapter rather than part of the portable core. + +The project should remain understandable and usable without requiring a database, external service, paid platform, MCP server or orchestrator. -The project should remain understandable without requiring a database, external service or paid platform. +Multi-workstream complexity should remain progressively disclosed so ordinary single-goal repositories do not pay unnecessary token or attention cost. ## License From b1ae53294158f002de27b11c263bdf7b366dba28 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:42:30 +0200 Subject: [PATCH 30/47] docs: update v0.2.0 distribution contract --- DISTRIBUTION.md | 252 +++++++++++++++++------------------------------- 1 file changed, 89 insertions(+), 163 deletions(-) diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md index c7f6128..7aeed1c 100644 --- a/DISTRIBUTION.md +++ b/DISTRIBUTION.md @@ -7,31 +7,26 @@ Canonical protocol skill: ```text skills/ppgp/SKILL.md skills/ppgp/references/PPGP.md +skills/ppgp/references/COORDINATION.md ``` -For platforms that discover the cross-agent `.agents/skills/` convention, the repository also contains a generated byte-identical mirror under `.agents/skills/ppgp/`. `npm test` fails if that mirror drifts from the canonical skill. +`COORDINATION.md` is progressively disclosed. Ordinary single-workstream use does not need to load it. -Regenerate the compatibility mirrors deterministically with: +Generated mirrors exist under `.agents/skills/ppgp/` and `plugins/ppgp/skills/ppgp/`. `npm test` enforces parity with the canonical skill/reference files. -```bash -node scripts/sync-skill-mirror.js -``` - -See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for the current platform-by-platform support matrix and verification state. +See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for platform-specific verification state. ## Universal Agent Skills route -Purpose: direct installation into Agent Skills-compatible environments without tying PPGP to a vendor. - ```bash npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp ``` This remains the preferred portable skill source. -## Anthropic Claude and Claude Code +## Platform adapters -PPGP exposes a Claude plugin and a self-hosted marketplace from the canonical repository: +### Anthropic Claude / Claude Code ```text .claude-plugin/plugin.json @@ -40,164 +35,91 @@ plugins/ppgp/.claude-plugin/plugin.json plugins/ppgp/skills/ppgp/ ``` -Install flow: - -```text -Claude -→ Plugins -→ Add marketplace -→ Fatboy-coder/ppgp -→ Sync -→ install ppgp -``` - -The exact invocation depends on the Claude client surface. +The packaged Claude skill is a deterministic mirror because installed plugins may be copied into client cache. -In the tested Claude client, the marketplace-installed skill is discovered and invoked with: - -```text -/ppgp -``` - -That same environment reports `/reload-plugins` as unavailable, so it is not a universal activation step. - -Claude Code can namespace plugin skills as `/plugin-name:skill-name`. Where that namespace is exposed, the PPGP plugin and its single skill are both named `ppgp`, yielding: - -```text -/ppgp:ppgp -``` - -Use `/reload-plugins` only in a Claude surface that actually exposes that command and requires plugin activation after an update. Do not assume one slash-command form or reload command applies to every Claude client. - -The dedicated `plugins/ppgp/` package exists because Claude copies installed plugins into its cache. Its skill content is a deterministic mirror of the canonical `skills/ppgp/` source and is drift-tested. Marketplace installation, updated-skill loading and invocation have been manually verified in a real Claude client. Public Anthropic directory listing is not claimed. - -## OpenAI Codex and ChatGPT - -PPGP exposes a skill-only Codex/OpenAI plugin: +### OpenAI Codex / ChatGPT ```text .codex-plugin/plugin.json .agents/plugins/marketplace.json +skills/ppgp/ ``` -The Codex plugin explicitly uses the canonical `skills/` directory. The same canonical skill can be used as an OpenAI Agent Skill. - -Repository packaging does not mean PPGP is publicly listed by OpenAI. These states remain distinct: - -```text -repository ready -!= submitted -!= approved -!= publicly listed -``` - -Public Plugin Directory publication is an external vendor-side step. - -## Google Gemini CLI +Repository packaging is distinct from vendor submission, approval and public listing. -PPGP exposes a Gemini CLI extension manifest at repository root: +### Gemini CLI ```text gemini-extension.json +skills/ppgp/ ``` -The extension reuses the canonical `skills/ppgp/` directory. - -Intended install: - -```bash -gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update -``` - -Repository structure is ready; a local Gemini CLI smoke test remains external. - -## Cursor - -PPGP exposes the open Agent Plugins format at repository root: +### Cursor / open Agent Plugin format ```text plugin.json +skills/ppgp/ ``` -The manifest remains schema-safe and co-located with canonical `skills/ppgp/` content. A separate `.cursor-plugin/plugin.json` is intentionally not added in v0.1.2 because PPGP currently needs only skills and the portable Agent Plugin format covers the intended distribution surface. - -Cursor Marketplace publication is an external submission step and is not claimed as complete. - -## GitHub Copilot - -GitHub Copilot supports Agent Skills from `.agents/skills/` in repository scope. PPGP therefore commits a generated compatibility mirror: +### Repository-native Agent Skills discovery ```text .agents/skills/ppgp/ ``` -This mirror is not authoritative. Tests enforce byte-for-byte parity with `skills/ppgp/`. +This supports clients that discover the cross-agent `.agents/skills/` convention without creating a semantic fork. -## Windsurf +## Reference implementation artifacts -Windsurf recognizes `.agents/skills/` as a cross-agent compatibility path. The generated PPGP mirror provides repository-native discovery without a Windsurf-specific semantic copy. - -## Devin - -Devin supports the open Agent Skills convention and repository skills under `.agents/skills/`. The generated PPGP mirror provides repository-native discovery. - -## Kiro - -Use the canonical public skill source: +PPGP v0.2.0 adds reference JSON Schemas: ```text -https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp +schemas/portfolio.schema.json +schemas/workstream.schema.json ``` -No Kiro-specific semantic copy is maintained. - -## Cline - -Use the canonical PPGP skill and install/copy it into a supported Cline skills location, or use a generic Agent Skills installer where available. - -No Cline-specific semantic adapter is maintained. +These schemas document the official CLI representation. They are shipped in the npm package but are not a protocol-core storage requirement. -## JetBrains Junie +The reference CLI may create project state: -PPGP remains consumable from the canonical skill through Junie's supported project/user skill import locations. - -No Junie-specific semantic adapter is maintained. - -## Secondary-platform policy - -For Roo Code, Amazon Q Developer and other agent platforms, PPGP only adds a native adapter after the platform exposes a stable, documented mechanism that can be tested without forking protocol semantics. +```text +.ppgp/ +├── portfolio.json +└── workstreams/ + └── <id>/ + ├── state.json + └── notes.md +``` -Until then, the canonical Agent Skill and CLI remain available, and `COMPATIBILITY.md` records the exact current support state rather than inventing proprietary files. +Local checkout claims intentionally live outside committed project state in the Git common directory. ## GitHub Release -Purpose: zero-friction download of the installable Agent Skill archive. - -Current canonical asset: +The v0.2.0 release asset name is: ```text -ppgp-v0.1.2.zip +ppgp-v0.2.0.zip ``` -Each release also publishes a SHA-256 checksum next to the archive. Older release assets remain historical artifacts and are not the canonical download for the current release. +A SHA-256 checksum SHOULD accompany the archive. -## npm +The release workflow must validate the requested version against committed `package.json` before creating the immutable tag/release. -Purpose: public CLI discovery and zero-install execution. +## npm -Canonical public package name: +Canonical public package: ```text @fatboy-coder/ppgp ``` -Current package release: +Target current package release: ```text -@fatboy-coder/ppgp@0.1.2 +@fatboy-coder/ppgp@0.2.0 ``` -The original unscoped name `ppgp` is intentionally not used because npm's similarity protection rejects it as too close to existing high-traffic package names. +The original unscoped name `ppgp` is not used because npm similarity protection rejected it. CLI binary: @@ -205,74 +127,76 @@ CLI binary: ppgp ``` -Zero-install examples: +Examples: ```bash npx @fatboy-coder/ppgp init npx @fatboy-coder/ppgp doctor npx @fatboy-coder/ppgp goal "Ship the next verified milestone" npx @fatboy-coder/ppgp status -npx @fatboy-coder/ppgp handoff +npx @fatboy-coder/ppgp status --all ``` -When diagnosing `npx` executable inference or npm cache behavior, the explicit npm-exec form removes ambiguity about which binary must run: +Explicit npm-exec form: ```bash -npm exec --yes --package=@fatboy-coder/ppgp@0.1.2 -- ppgp --version +npm exec --yes --package=@fatboy-coder/ppgp@0.2.0 -- ppgp --version ``` -On Windows PowerShell, some npm versions can route `npm` through the `npm.ps1` wrapper and mis-handle forwarded arguments. If the command above prints the npm version instead of the PPGP version, bypass the wrapper explicitly: +PowerShell fallback when npm's wrapper mishandles forwarded arguments: ```powershell -npm.cmd exec --yes --package=@fatboy-coder/ppgp@0.1.2 -- ppgp --version +npm.cmd exec --yes --package=@fatboy-coder/ppgp@0.2.0 -- ppgp --version ``` -Expected output: +Expected output after publication: ```text -0.1.2 +0.2.0 ``` -This is a shell-wrapper issue, not evidence that the PPGP package lacks its CLI binary. PPGP CI packs and installs the package on Windows and verifies the generated `ppgp.cmd` shim by executing `ppgp --version`. - -A plain `ppgp` command is expected only after the package has been installed globally or linked for local development: +Global install: ```bash -npm install -g @fatboy-coder/ppgp@0.1.2 +npm install -g @fatboy-coder/ppgp@0.2.0 ppgp --version ``` -Inside the PPGP source repository itself, the source CLI can always be tested directly without any installation: +Inside source checkout: ```bash node ./bin/ppgp.js --version ``` -The npm package bundles the canonical Agent Skill, benchmark protocol, deterministic benchmark reporter and Pilot 01 preparation tooling. Platform adapter manifests remain excluded from the npm payload because they are repository distribution surfaces rather than CLI package contents. +The npm package bundles: + +- CLI; +- canonical Agent Skill and references; +- v0.2 reference schemas; +- specification/evaluation/citation files; +- benchmark protocol, reporter and Pilot 01 preparation tooling. + +Repository-native adapter manifests remain excluded from npm payload. -### npm Trusted Publisher +## npm Trusted Publisher -The package is published through GitHub Actions using npm Trusted Publishing/OIDC. The workflow is `.github/workflows/publish-npm.yml`. +Publishing uses GitHub Actions and npm Trusted Publishing/OIDC via `.github/workflows/publish-npm.yml`. Long-lived npm automation tokens are not required when trusted publishing is correctly configured. ## GitHub Packages -Purpose: package presence inside GitHub associated with the repository. - Published package name: ```text @fatboy-coder/ppgp ``` -The GitHub package is produced from the same source package contents and published to GitHub's npm registry. - -GitHub Packages is a secondary distribution surface. GitHub Release and npmjs.com remain the lower-friction universal entry points. +GitHub Packages remains a secondary distribution surface produced from the same source package contents. ## Release automation -A release decision begins with one guarded manual workflow: +Intended chain: ```text Publish PPGP release @@ -282,39 +206,41 @@ Publish PPGP to npm Publish PPGP to GitHub Packages ``` -The release workflow validates that the requested version exactly matches the committed `package.json` version before creating the immutable tag and GitHub Release. Downstream package workflows re-check the canonical GitHub Release and package version before publishing. - -Manual recovery dispatches exist for downstream publication if an already-created release needs to be republished to a package registry after an infrastructure failure. +The release workflow validates version consistency before creating the tag and release. Downstream package workflows re-check the canonical release and package version. ## Validation -`npm test` validates the CLI, package contents and distribution invariants, including: +`npm test` must validate at least: -- canonical skill and compact reference exist; -- Claude, Codex, Agent Plugin and Gemini manifests parse correctly; -- every versioned adapter matches the committed `package.json` version; -- marketplace identities point to `ppgp`; -- the Codex manifest points to the canonical `./skills/` directory; -- the root Agent Plugin manifest remains schema-safe; -- `.agents/skills/ppgp/` and `plugins/ppgp/skills/ppgp/` remain byte-identical to the canonical skill and reference; -- platform adapter directories do not silently enter the npm package contents; -- public current-version documentation, citation metadata and CLI protocol headers remain aligned with the committed package version. +- legacy single-workstream CLI compatibility; +- v0.2 portfolio/workstream coordination behavior; +- stale revision rejection and lock cleanup; +- lease generation takeover; +- checkout collision protection; +- dependency-cycle detection; +- reversible migration; +- canonical skill/reference existence; +- coordination-reference mirrors remain byte-identical; +- platform manifests parse and versioned adapters align; +- reference schemas ship in npm payload; +- repository adapters do not silently enter npm payload; +- public documentation/citation metadata stay aligned with package version. -GitHub Actions runs the test suite on Linux and Windows and includes an installed-package CLI smoke test that verifies the platform-specific `ppgp` binary shim after packing and installing the package. +CI runs on Linux and Windows and includes an installed-package CLI smoke test. ## Version mapping -PPGP v0.1.2 uses one canonical current release version across the protocol specification and versioned distribution artifacts: +PPGP specification 0.2.0 and versioned distribution artifacts use one canonical current release version: ```text -PPGP specification 0.1.2 -npm package @fatboy-coder/ppgp@0.1.2 -GitHub package @fatboy-coder/ppgp@0.1.2 -Codex/Gemini/Agent Plugin adapters 0.1.2 -Agent Skill metadata 0.1.2 -GitHub release v0.1.2 +PPGP specification 0.2.0 +npm package @fatboy-coder/ppgp@0.2.0 +GitHub package @fatboy-coder/ppgp@0.2.0 +Codex/Gemini/Agent Plugin adapters 0.2.0 +Agent Skill metadata 0.2.0 +GitHub release v0.2.0 ``` -Claude's repository-backed plugin manifest intentionally does not pin a static version because client refresh follows repository revisions. This is an adapter caching policy, not a second PPGP version. +Claude's repository-backed plugin manifest intentionally does not pin a static version because repository refresh follows revisions. This is an adapter caching policy, not a second PPGP version. -Historical release numbers remain in `CHANGELOG.md` and publication history only. Benchmark result-schema versions are independently labeled as schema versions and are not PPGP release versions. +Historical release numbers remain in `CHANGELOG.md`. Benchmark result-schema versions are independent schema identifiers and are not PPGP release versions. From c5c8412d707cc21ca0aaaff97e34648b50b588df Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:42:47 +0200 Subject: [PATCH 31/47] docs: move v0.2.0 roadmap to release candidate --- ROADMAP.md | 122 ++++++++++++++++++++++++++--------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c9d6214..b475975 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,12 +1,60 @@ # PPGP Roadmap -PPGP is currently experimental. The roadmap prioritizes evidence, portability and reduction of unnecessary protocol overhead. +PPGP is experimental. The roadmap prioritizes evidence, portability and reduction of unnecessary protocol overhead. + +## v0.2.0 + +Release candidate implementation line. + +v0.2.0 extends PPGP from continuity of one primary active goal to optional portfolio coordination when several workstreams, executors, branches, worktrees, dependencies, or partial waits coexist. + +Implemented in the release-candidate branch: + +- optional PORTFOLIO / WORKSTREAM coordination; +- lifecycle PHASE separated from RUN_STATE; +- RUNNABLE, RUNNING, WAITING, RECOVERY_REQUIRED, PARKED and COMPLETED states; +- typed/scoped wait conditions; +- action-scoped authority gates; +- explicit acyclic workstream dependencies; +- execution leases with monotonically increasing generations; +- local exclusive checkout claims stored outside committed project state; +- stale revision rejection plus local mutation locking; +- safe isolation preference around foreign dirty work; +- non-destructive abrupt takeover; +- SESSION_ONLY / HOST_DURABLE / REPO_DURABLE / REMOTE_DURABLE recovery classes; +- copy-first reversible legacy migration with one canonical source after cutover; +- progressive-disclosure coordination reference; +- reference portfolio/workstream JSON Schemas; +- backward compatibility for single ACTIVE_GOAL repositories; +- v0.2 coordination conformance tests. + +Design lineage: + +- [`rfcs/0001-concurrent-workstreams-leases-partial-blocking.md`](./rfcs/0001-concurrent-workstreams-leases-partial-blocking.md) +- [`rfcs/0001-normative-delta.md`](./rfcs/0001-normative-delta.md) +- [`evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md`](./evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md) +- [`RELATED_WORK.md`](./RELATED_WORK.md) + +### Remaining release gates + +Before public v0.2.0 publication: + +1. complete repository-wide version alignment; +2. pass Linux and Windows `npm test` including installed-package smoke tests; +3. pass package dry-run and verify schemas/coordination reference are shipped; +4. audit adapters and mirror parity; +5. independently review the v0.2 conformance semantics; +6. confirm release notes and citation metadata; +7. create immutable GitHub Release/tag only after all above are green; +8. publish npm/GitHub Packages through the guarded release pipeline. + +The GitHub/npm publication step remains separate because it is less reversible than repository implementation work. ## v0.1.2 -Published as the current experimental line and available for public testing. +Published experimental predecessor. -Current capabilities: +v0.1.2 provides: - portable repository-visible goal state; - THINK, FREEZE, EXECUTE, HARDEN, SHIP, DISTILL lifecycle; @@ -15,57 +63,13 @@ Current capabilities: - explicit human-authority boundaries; - compact handoff format; - Agent Skills-compatible implementation; -- downloadable skill package; -- public specification, citation metadata and evaluation guide; -- explicit ACTIVE_GOAL hot-state recovery semantics; -- reproducible paired benchmark infrastructure. +- ACTIVE_GOAL hot-state recovery semantics; +- reproducible paired benchmark infrastructure; +- version-consistency hardening. -## v0.2.0 development line +v0.2.0 intentionally preserves this single-workstream path when no explicit portfolio is needed. -v0.2.0 is being designed from observed coordination failures in real long-running multi-agent coding work. - -The release should extend continuity from one active goal to safe recovery and scheduling when several workstreams, agents, branches, worktrees, partial wait conditions, and temporary executor interruptions coexist. - -Primary design RFC: - -- [`rfcs/0001-concurrent-workstreams-leases-partial-blocking.md`](./rfcs/0001-concurrent-workstreams-leases-partial-blocking.md) - -Generalized design evidence: - -- [`evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md`](./evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md) - -Target protocol additions: - -- optional project-level PORTFOLIO / workstream coordination when concurrent active work exists; -- WORKSTREAM as an independently schedulable unit; -- lifecycle PHASE separated from RUN_STATE; -- compact RUN_STATE values: RUNNABLE, RUNNING, WAITING, RECOVERY_REQUIRED, PARKED, COMPLETED; -- scoped typed wait conditions so one unavailable dependency does not falsely block independent work; -- temporary EXECUTION_LEASE ownership; -- exclusive writable CHECKOUT_CLAIM semantics; -- isolated-workspace preference when a shared checkout contains foreign dirty work; -- explicit non-destructive takeover after abrupt agent unavailability; -- unfinished-work durability levels from SESSION_ONLY through REMOTE_DURABLE; -- executor unavailability treated as runtime capacity rather than an automatic GOAL blocker; -- project-level invariant that a blocked workstream does not block unrelated runnable work; -- backward compatibility for v0.1.2 single-ACTIVE_GOAL repositories. - -### v0.2.0 release gates - -Before v0.2.0 is released: - -1. freeze RFC 0001 semantics; -2. update the normative specification without creating a second source of truth; -3. extend the Agent Skill and compact reference; -4. extend the CLI with the smallest useful coordination operations; -5. add deterministic conformance cases for shared-checkout protection, partial external waits, abrupt takeover, mixed waits, and unrelated runnable workstreams; -6. update all versioned adapters and mirrors atomically; -7. pass existing v0.1.x recovery tests plus new v0.2 coordination tests; -8. validate packaging and version consistency; -9. keep motivating incidents generalized and free of private product details; -10. release as experimental without a superiority or universality claim. - -## Evidence priorities +## Evidence priorities after v0.2.0 ### Gather independent evidence @@ -73,23 +77,19 @@ Collect recovery failures, successful replications, overhead reports and compara ### Reduce protocol overhead -Identify fields, steps or rules that can be removed without reducing recovery quality. +Identify fields, steps or rules that can be removed without reducing recovery or coordination quality. ### Test portability -Validate that the same repository-visible state can be interpreted consistently by materially different coding agents and environments. +Validate that materially different coding agents interpret the same portable state consistently. ### Test concurrency semantics -Test whether fresh agents can safely distinguish workstream ownership, checkout ownership, scoped blockers, executor interruptions, recoverable dirty state, and the next runnable work without human reconstruction. +Measure checkout safety, blocker-scope reliability, takeover recovery and stale-write prevention under real multi-agent conditions. ### Clarify conformance -Refine the minimum requirements for claiming PPGP compatibility using observed implementation failures rather than theoretical completeness. - -### Improve packaging - -Keep installation simple across Agent Skills-compatible clients without making the portable core dependent on one vendor. +Refine minimum compatibility requirements using observed failures rather than theoretical completeness. ## Not planned as core requirements @@ -104,7 +104,7 @@ PPGP does not plan to require: - CRDT-backed concurrent editing; - proprietary infrastructure. -These may be useful optional integrations, but they should not become prerequisites for protocol conformance. +These remain optional integrations. ## Stability From c5935b7040062634145587ebf0f4717c377ea1bf Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:43:05 +0200 Subject: [PATCH 32/47] docs: record v0.2.0 release candidate implementation --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe20767..9254a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,51 @@ # Changelog -## Unreleased - 0.2.0 design line - -Drafted from observed multi-agent coordination failures in real long-running coding work. - -Proposed additions: - -- project-level PORTFOLIO / workstream coordination for concurrent active work; -- WORKSTREAM as an independently schedulable execution unit; -- RUN_STATE separated from the existing lifecycle PHASE; -- scoped typed wait conditions so a blocked action does not falsely block independent work; -- EXECUTION_LEASE semantics for temporary agent ownership; -- CHECKOUT_CLAIM semantics for exclusive writable workspace ownership; -- default safe-isolation behavior when a shared checkout contains foreign dirty work; -- RECOVERY_REQUIRED and non-destructive takeover after abrupt agent unavailability; -- explicit durability classification for unfinished work: SESSION_ONLY, HOST_DURABLE, REPO_DURABLE, REMOTE_DURABLE; -- project-level scheduling invariant: blocked workstream != blocked project; -- backward compatibility with v0.1.2 single-ACTIVE_GOAL repositories. - -Design evidence and rationale are recorded in RFC 0001 and Incident 001. - -This section is not a release announcement. v0.2.0 remains unreleased until the specification, Agent Skill, CLI, conformance cases, adapters, package metadata, and version-consistency tests are updated and verified together. +## Unreleased - 0.2.0 release candidate + +Major experimental coordination update derived from observed failures in real multi-agent coding work. + +Implemented: + +- optional PORTFOLIO / WORKSTREAM coordination while preserving the v0.1.x single-ACTIVE_GOAL path; +- lifecycle PHASE separated from neutral RUN_STATE; +- RUNNABLE, RUNNING, WAITING, RECOVERY_REQUIRED, PARKED and COMPLETED states; +- typed and scoped wait conditions so blocked actions do not falsely block independent work; +- explicit action-scoped AUTHORITY_GATE lifecycle: REQUIRED, GRANTED, CONSUMED, REVOKED; +- explicit acyclic workstream dependencies; +- execution leases with monotonically increasing generations for handoff/takeover fencing; +- reference integer revisions plus local compare-and-swap/mutation locking; +- local exclusive checkout claims stored in the Git common directory rather than committed project state; +- checkout/branch validation and collision protection; +- safe-isolation preference when a shared checkout contains foreign dirty work; +- RECOVERY_REQUIRED and non-destructive takeover after abrupt executor unavailability; +- unfinished-work durability classes: SESSION_ONLY, HOST_DURABLE, REPO_DURABLE, REMOTE_DURABLE; +- copy-first reversible legacy migration with one canonical source after cutover; +- JSON reference schemas for portfolio/workstream state without making JSON a protocol-core requirement; +- progressive-disclosure `COORDINATION.md` reference so simple repositories avoid unnecessary coordination tokens; +- v0.2 conformance tests for stale revisions, lock cleanup, mixed waits, dependency cycles, lease generations, checkout collisions, takeover and migration rollback; +- related-work documentation explicitly positioning PPGP alongside existing context-engineering, worktree, durable-execution and multi-agent coordination approaches. + +Core invariants added: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +executor unavailable != workstream blocked +blocked action != blocked workstream +blocked workstream != blocked project +``` + +The reference CLI adds: + +```text +ppgp migrate +ppgp status --all +ppgp workstream start/status/park/resume/handoff/recover/close +ppgp checkout status/claim/release +``` + +This section is not yet a public release announcement. The v0.2.0 tag, GitHub Release, npm package and GitHub Package remain gated on repository-wide version alignment, Linux/Windows CI, package dry-run, adapter/mirror parity and final release review. + +No superiority or universality benchmark claim is made. ## 0.1.2 - 2026-08-26 From 3156becc5018f2942b749af482bf464307fb860d Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:46:46 +0200 Subject: [PATCH 33/47] fix(cli): harden v0.2 CAS checkout and rollback safety --- bin/ppgp.js | 835 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 621 insertions(+), 214 deletions(-) diff --git a/bin/ppgp.js b/bin/ppgp.js index 636cb1f..9c81ea5 100644 --- a/bin/ppgp.js +++ b/bin/ppgp.js @@ -24,25 +24,43 @@ const DURABILITY = new Set(['SESSION_ONLY', 'HOST_DURABLE', 'REPO_DURABLE', 'REM const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; class PPGPError extends Error { - constructor(message, code = 1) { super(message); this.name = 'PPGPError'; this.code = code; } + constructor(message, code = 1) { + super(message); + this.name = 'PPGPError'; + this.code = code; + } } -function die(message, code = 1) { throw new PPGPError(message, code); } +function die(message, code = 1) { + throw new PPGPError(message, code); +} function parseArgs(argv) { const args = [...argv]; - const options = { root: process.cwd(), force: false, all: false, rollback: false, branch: null, holder: null, revision: null }; + const options = { + root: process.cwd(), + force: false, + all: false, + rollback: false, + branch: null, + holder: null, + revision: null + }; const positional = []; + while (args.length) { const arg = args.shift(); if (arg === '--root') { const value = args.shift(); if (!value) die('--root requires a path.'); options.root = path.resolve(value); - } else if (arg === '--force') options.force = true; - else if (arg === '--all') options.all = true; - else if (arg === '--rollback') options.rollback = true; - else if (arg === '--branch') { + } else if (arg === '--force') { + options.force = true; + } else if (arg === '--all') { + options.all = true; + } else if (arg === '--rollback') { + options.rollback = true; + } else if (arg === '--branch') { const value = args.shift(); if (!value) die('--branch requires a value.'); options.branch = value; @@ -54,11 +72,21 @@ function parseArgs(argv) { const value = args.shift(); if (!/^\d+$/.test(value || '')) die('--revision requires a non-negative integer.'); options.revision = Number(value); - } else positional.push(arg); + } else { + positional.push(arg); + } } + return { options, positional }; } +function requireRevision(options, action) { + if (options.revision === null) { + die(`${action} requires --revision <observed-revision> so stale state cannot overwrite canonical state.`); + } + return options.revision; +} + function findRole(root, role) { for (const candidate of ROLE_CANDIDATES[role] || []) { const absolute = path.join(root, candidate); @@ -73,16 +101,31 @@ function roleMap(root) { function git(root, args, fallback = null) { try { - return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + return execFileSync('git', ['-C', root, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }).trim(); } catch { return fallback; } } -function hasGit(root) { return git(root, ['rev-parse', '--is-inside-work-tree']) === 'true'; } -function gitBranch(root) { return git(root, ['branch', '--show-current'], null) || null; } -function gitHead(root) { return git(root, ['rev-parse', 'HEAD'], null); } -function gitDirty(root) { const out = git(root, ['status', '--porcelain'], null); return out === null ? null : out.length > 0; } +function hasGit(root) { + return git(root, ['rev-parse', '--is-inside-work-tree']) === 'true'; +} + +function gitBranch(root) { + return git(root, ['branch', '--show-current'], null) || null; +} + +function gitHead(root) { + return git(root, ['rev-parse', 'HEAD'], null); +} + +function gitDirty(root) { + const out = git(root, ['status', '--porcelain'], null); + return out === null ? null : out.length > 0; +} function gitCommonDir(root) { const raw = git(root, ['rev-parse', '--git-common-dir'], null); @@ -90,11 +133,34 @@ function gitCommonDir(root) { return path.isAbsolute(raw) ? raw : path.resolve(root, raw); } +function isInside(base, target) { + const relative = path.relative(path.resolve(base), path.resolve(target)); + return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)); +} + +function safeRepoRef(root, ref, label = 'reference') { + if (typeof ref !== 'string' || !ref) die(`${label} must be a non-empty repository-relative path.`); + if (path.isAbsolute(ref)) die(`${label} must be repository-relative: ${ref}`); + + const resolved = path.resolve(root, ref); + if (!isInside(root, resolved)) die(`${label} escapes repository root: ${ref}`); + + if (fs.existsSync(resolved)) { + const realRoot = fs.realpathSync(root); + const realTarget = fs.realpathSync(resolved); + if (!isInside(realRoot, realTarget)) die(`${label} resolves outside repository root: ${ref}`); + } + + return resolved; +} + function printMap(root) { const roles = roleMap(root); console.log(`PPGP ${pkg.version} repository mapping`); console.log(`Root: ${root}`); - for (const [role, file] of Object.entries(roles)) console.log(`${role.padEnd(12)} ${file || '(not mapped)'}`); + for (const [role, file] of Object.entries(roles)) { + console.log(`${role.padEnd(12)} ${file || '(not mapped)'}`); + } console.log(`PORTFOLIO ${portfolioExists(root) ? '.ppgp/portfolio.json' : '(implicit/single-workstream)'}`); console.log(`GIT ${hasGit(root) ? 'available' : '(not detected)'}`); return roles; @@ -114,34 +180,65 @@ function parseSections(content) { let current = null; for (const line of content.split(/\r?\n/)) { const match = line.match(/^##\s+([A-Z_]+)\s*$/); - if (match) { current = match[1]; sections[current] = []; continue; } + if (match) { + current = match[1]; + sections[current] = []; + continue; + } if (current) sections[current].push(line); } - return Object.fromEntries(Object.entries(sections).map(([key, lines]) => [key, lines.join('\n').trim()])); + return Object.fromEntries( + Object.entries(sections).map(([key, lines]) => [key, lines.join('\n').trim()]) + ); } function oneLine(value, fallback = '(not set)') { if (!value) return fallback; - return value.replace(/^[-*]\s+/gm, '').replace(/\s*\n\s*/g, ' | ').replace(/\s+/g, ' ').trim() || fallback; + return value + .replace(/^[-*]\s+/gm, '') + .replace(/\s*\n\s*/g, ' | ') + .replace(/\s+/g, ' ') + .trim() || fallback; } function readActiveGoal(root) { const file = findRole(root, 'ACTIVE_GOAL'); if (!file) die('No ACTIVE_GOAL found. Start one with: ppgp goal "<outcome>"'); - const content = fs.readFileSync(path.join(root, file), 'utf8'); + const absolute = safeRepoRef(root, file, 'ACTIVE_GOAL path'); + const content = fs.readFileSync(absolute, 'utf8'); return { file, content, sections: parseSections(content) }; } -function ppgpDir(root) { return path.join(root, '.ppgp'); } -function portfolioPath(root) { return path.join(ppgpDir(root), 'portfolio.json'); } -function portfolioExists(root) { return fs.existsSync(portfolioPath(root)); } -function workstreamDir(root, id) { return path.join(ppgpDir(root), 'workstreams', id); } -function workstreamStatePath(root, id) { return path.join(workstreamDir(root, id), 'state.json'); } -function workstreamNotesPath(root, id) { return path.join(workstreamDir(root, id), 'notes.md'); } +function ppgpDir(root) { + return path.join(root, '.ppgp'); +} + +function portfolioPath(root) { + return path.join(ppgpDir(root), 'portfolio.json'); +} + +function portfolioExists(root) { + return fs.existsSync(portfolioPath(root)); +} + +function workstreamDir(root, id) { + return path.join(ppgpDir(root), 'workstreams', id); +} + +function workstreamStatePath(root, id) { + return path.join(workstreamDir(root, id), 'state.json'); +} + +function workstreamNotesPath(root, id) { + return path.join(workstreamDir(root, id), 'notes.md'); +} function readJson(file) { - try { return JSON.parse(fs.readFileSync(file, 'utf8')); } - catch (error) { die(`Invalid JSON in ${file}: ${error.message}`); } + try { + return JSON.parse(fs.readFileSync(file, 'utf8')); + } catch (error) { + die(`Invalid JSON in ${file}: ${error.message}`); + } } function writeJsonAtomic(file, value) { @@ -151,30 +248,74 @@ function writeJsonAtomic(file, value) { fs.renameSync(temp, file); } -function withPortfolioLock(root, fn) { - fs.mkdirSync(ppgpDir(root), { recursive: true }); - const lock = path.join(ppgpDir(root), '.lock'); +function withExclusiveLock(lock, message, fn) { + fs.mkdirSync(path.dirname(lock), { recursive: true }); let fd; - try { fd = fs.openSync(lock, 'wx'); } - catch { die('PPGP coordination state is currently locked by another local mutation. Retry after it completes.'); } - try { fs.writeFileSync(fd, `${process.pid}\n`, 'utf8'); return fn(); } - finally { try { fs.closeSync(fd); } catch {} try { fs.unlinkSync(lock); } catch {} } + try { + fd = fs.openSync(lock, 'wx'); + } catch { + die(message); + } + + try { + fs.writeFileSync(fd, `${process.pid}\n`, 'utf8'); + return fn(); + } finally { + try { fs.closeSync(fd); } catch {} + try { fs.unlinkSync(lock); } catch {} + } +} + +function withPortfolioLock(root, fn) { + return withExclusiveLock( + path.join(ppgpDir(root), '.lock'), + 'PPGP coordination state is currently locked by another local mutation. Retry after it completes.', + fn + ); } -function validatePortfolio(p) { +function checkoutRegistryPath(root) { + const common = gitCommonDir(root); + return common ? path.join(common, 'ppgp-checkouts.json') : null; +} + +function checkoutLockPath(root) { + const common = gitCommonDir(root); + return common ? path.join(common, 'ppgp-checkouts.lock') : null; +} + +function withCheckoutLock(root, fn) { + const lock = checkoutLockPath(root); + if (!lock) die('Checkout claims require a Git repository.'); + return withExclusiveLock( + lock, + 'PPGP checkout registry is currently locked by another local mutation. Retry after it completes.', + fn + ); +} + +function validatePortfolio(portfolio) { const errors = []; - if (!p || typeof p !== 'object' || Array.isArray(p)) return ['portfolio must be an object']; - if (p.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); - if (typeof p.id !== 'string' || !p.id) errors.push('id must be a non-empty string'); - if (!Number.isInteger(p.revision) || p.revision < 0) errors.push('revision must be a non-negative integer'); - if (p.aggregationPolicy !== 'any-runnable') errors.push('aggregationPolicy must be any-runnable'); - if (!Array.isArray(p.workstreams)) errors.push('workstreams must be an array'); - else { + if (!portfolio || typeof portfolio !== 'object' || Array.isArray(portfolio)) { + return ['portfolio must be an object']; + } + if (portfolio.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); + if (typeof portfolio.id !== 'string' || !portfolio.id) errors.push('id must be a non-empty string'); + if (!Number.isInteger(portfolio.revision) || portfolio.revision < 0) errors.push('revision must be a non-negative integer'); + if (portfolio.aggregationPolicy !== 'any-runnable') errors.push('aggregationPolicy must be any-runnable'); + + if (!Array.isArray(portfolio.workstreams)) { + errors.push('workstreams must be an array'); + } else { const seen = new Set(); - for (const ref of p.workstreams) { - if (!ref || typeof ref !== 'object') { errors.push('workstream reference must be an object'); continue; } + for (const ref of portfolio.workstreams) { + if (!ref || typeof ref !== 'object') { + errors.push('workstream reference must be an object'); + continue; + } if (!ID_RE.test(ref.id || '')) errors.push(`invalid workstream id: ${ref.id}`); - if (seen.has(ref.id)) errors.push(`duplicate workstream id: ${ref.id}`); else seen.add(ref.id); + if (seen.has(ref.id)) errors.push(`duplicate workstream id: ${ref.id}`); + else seen.add(ref.id); if (typeof ref.stateRef !== 'string' || !ref.stateRef) errors.push(`workstream ${ref.id} missing stateRef`); if (typeof ref.notesRef !== 'string' || !ref.notesRef) errors.push(`workstream ${ref.id} missing notesRef`); } @@ -182,49 +323,72 @@ function validatePortfolio(p) { return errors; } -function validateState(s) { +function validateState(state) { const errors = []; - if (!s || typeof s !== 'object' || Array.isArray(s)) return ['state must be an object']; - if (s.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); - if (!ID_RE.test(s.id || '')) errors.push('id is invalid'); - if (typeof s.title !== 'string' || !s.title) errors.push('title must be non-empty'); - if (!Number.isInteger(s.revision) || s.revision < 0) errors.push('revision must be a non-negative integer'); - if (!PHASES.has(s.phase)) errors.push(`invalid phase: ${s.phase}`); - if (!RUN_STATES.has(s.runState)) errors.push(`invalid runState: ${s.runState}`); - if (!(s.branch === null || typeof s.branch === 'string')) errors.push('branch must be string or null'); - if (!(s.lease === null || typeof s.lease === 'object')) errors.push('lease must be object or null'); - if (s.lease) { - if (typeof s.lease.holder !== 'string' || !s.lease.holder) errors.push('lease holder missing'); - if (!LEASE_STATES.has(s.lease.status)) errors.push(`invalid lease status: ${s.lease.status}`); - if (!Number.isInteger(s.lease.generation) || s.lease.generation < 1) errors.push('lease generation must be >= 1'); - if (typeof s.lease.claimedAt !== 'string' || !s.lease.claimedAt) errors.push('lease claimedAt missing'); - if (!(s.lease.expiresAt === null || typeof s.lease.expiresAt === 'string')) errors.push('lease expiresAt must be string or null'); - } - if (!Array.isArray(s.dependencies)) errors.push('dependencies must be an array'); - else for (const d of s.dependencies) { - if (!ID_RE.test((d && d.workstream) || '')) errors.push('dependency workstream id invalid'); - if (!d || d.condition !== 'COMPLETED') errors.push('dependency condition must be COMPLETED'); - } - if (!Array.isArray(s.waitConditions)) errors.push('waitConditions must be an array'); - else for (const w of s.waitConditions) { - if (!w || typeof w.id !== 'string' || !w.id) errors.push('wait id missing'); - if (!w || !WAIT_KINDS.has(w.kind)) errors.push(`invalid wait kind: ${w && w.kind}`); - if (!w || !WAIT_SCOPES.has(w.scope)) errors.push(`invalid wait scope: ${w && w.scope}`); - if (!w || typeof w.dependency !== 'string' || !w.dependency) errors.push('wait dependency missing'); - if (!w || typeof w.resumeCondition !== 'string' || !w.resumeCondition) errors.push('wait resumeCondition missing'); - } - if (!Array.isArray(s.authorityGates)) errors.push('authorityGates must be an array'); - else for (const g of s.authorityGates) { - if (!g || typeof g.id !== 'string' || !g.id) errors.push('authority gate id missing'); - if (!g || typeof g.action !== 'string' || !g.action) errors.push('authority gate action missing'); - if (!g || !GATE_STATES.has(g.state)) errors.push(`invalid authority gate state: ${g && g.state}`); - } - if (!(s.nextAction === null || typeof s.nextAction === 'string')) errors.push('nextAction must be string or null'); - if (!Array.isArray(s.evidenceRefs) || s.evidenceRefs.some((x) => typeof x !== 'string')) errors.push('evidenceRefs must be a string array'); - if (!DURABILITY.has(s.durability)) errors.push(`invalid durability: ${s.durability}`); - if (typeof s.notesRef !== 'string' || !s.notesRef) errors.push('notesRef missing'); - if (s.runState === 'RUNNING' && (!s.lease || s.lease.status !== 'CLAIMED')) errors.push('RUNNING requires a CLAIMED lease'); - if (s.runState === 'COMPLETED' && s.phase !== 'CLOSED') errors.push('COMPLETED requires phase CLOSED'); + if (!state || typeof state !== 'object' || Array.isArray(state)) return ['state must be an object']; + + if (state.schemaVersion !== '0.2') errors.push('schemaVersion must be 0.2'); + if (!ID_RE.test(state.id || '')) errors.push('id is invalid'); + if (typeof state.title !== 'string' || !state.title) errors.push('title must be non-empty'); + if (!Number.isInteger(state.revision) || state.revision < 0) errors.push('revision must be a non-negative integer'); + if (!PHASES.has(state.phase)) errors.push(`invalid phase: ${state.phase}`); + if (!RUN_STATES.has(state.runState)) errors.push(`invalid runState: ${state.runState}`); + if (!(state.branch === null || typeof state.branch === 'string')) errors.push('branch must be string or null'); + + if (!(state.lease === null || typeof state.lease === 'object')) { + errors.push('lease must be object or null'); + } else if (state.lease) { + if (typeof state.lease.holder !== 'string' || !state.lease.holder) errors.push('lease holder missing'); + if (!LEASE_STATES.has(state.lease.status)) errors.push(`invalid lease status: ${state.lease.status}`); + if (!Number.isInteger(state.lease.generation) || state.lease.generation < 1) errors.push('lease generation must be >= 1'); + if (typeof state.lease.claimedAt !== 'string' || !state.lease.claimedAt) errors.push('lease claimedAt missing'); + if (!(state.lease.expiresAt === null || typeof state.lease.expiresAt === 'string')) errors.push('lease expiresAt must be string or null'); + } + + if (!Array.isArray(state.dependencies)) { + errors.push('dependencies must be an array'); + } else { + for (const dependency of state.dependencies) { + if (!ID_RE.test((dependency && dependency.workstream) || '')) errors.push('dependency workstream id invalid'); + if (!dependency || dependency.condition !== 'COMPLETED') errors.push('dependency condition must be COMPLETED'); + } + } + + if (!Array.isArray(state.waitConditions)) { + errors.push('waitConditions must be an array'); + } else { + for (const wait of state.waitConditions) { + if (!wait || typeof wait.id !== 'string' || !wait.id) errors.push('wait id missing'); + if (!wait || !WAIT_KINDS.has(wait.kind)) errors.push(`invalid wait kind: ${wait && wait.kind}`); + if (!wait || !WAIT_SCOPES.has(wait.scope)) errors.push(`invalid wait scope: ${wait && wait.scope}`); + if (!wait || typeof wait.dependency !== 'string' || !wait.dependency) errors.push('wait dependency missing'); + if (!wait || typeof wait.resumeCondition !== 'string' || !wait.resumeCondition) errors.push('wait resumeCondition missing'); + } + } + + if (!Array.isArray(state.authorityGates)) { + errors.push('authorityGates must be an array'); + } else { + for (const gate of state.authorityGates) { + if (!gate || typeof gate.id !== 'string' || !gate.id) errors.push('authority gate id missing'); + if (!gate || typeof gate.action !== 'string' || !gate.action) errors.push('authority gate action missing'); + if (!gate || !GATE_STATES.has(gate.state)) errors.push(`invalid authority gate state: ${gate && gate.state}`); + } + } + + if (!(state.nextAction === null || typeof state.nextAction === 'string')) errors.push('nextAction must be string or null'); + if (!Array.isArray(state.evidenceRefs) || state.evidenceRefs.some((value) => typeof value !== 'string')) { + errors.push('evidenceRefs must be a string array'); + } + if (!DURABILITY.has(state.durability)) errors.push(`invalid durability: ${state.durability}`); + if (typeof state.notesRef !== 'string' || !state.notesRef) errors.push('notesRef missing'); + if (state.runState === 'RUNNING' && (!state.lease || state.lease.status !== 'CLAIMED')) { + errors.push('RUNNING requires a CLAIMED lease'); + } + if (state.runState === 'COMPLETED' && state.phase !== 'CLOSED') { + errors.push('COMPLETED requires phase CLOSED'); + } + return errors; } @@ -233,50 +397,83 @@ function loadPortfolio(root) { const portfolio = readJson(portfolioPath(root)); const errors = validatePortfolio(portfolio); if (errors.length) die(`Invalid .ppgp/portfolio.json:\n- ${errors.join('\n- ')}`); + + for (const ref of portfolio.workstreams) { + safeRepoRef(root, ref.stateRef, `stateRef for ${ref.id}`); + safeRepoRef(root, ref.notesRef, `notesRef for ${ref.id}`); + } + return portfolio; } -function resolveRef(root, ref) { return path.resolve(root, ref); } - function loadWorkstream(root, id, portfolio = loadPortfolio(root)) { if (!portfolio) die('No PPGP portfolio found.'); - const ref = portfolio.workstreams.find((x) => x.id === id); + const ref = portfolio.workstreams.find((candidate) => candidate.id === id); if (!ref) die(`Unknown workstream: ${id}`); - const file = resolveRef(root, ref.stateRef); + + const file = safeRepoRef(root, ref.stateRef, `stateRef for ${id}`); if (!fs.existsSync(file)) die(`Missing state for workstream ${id}: ${ref.stateRef}`); + const state = readJson(file); const errors = validateState(state); if (errors.length) die(`Invalid state for ${id}:\n- ${errors.join('\n- ')}`); if (state.id !== id) die(`Workstream reference ${id} points to state id ${state.id}.`); + safeRepoRef(root, state.notesRef, `notesRef for ${id}`); + return { ref, file, state }; } -function updateState(root, id, mutate, expectedRevision = null) { +function updateState(root, id, mutate, expectedRevision) { + if (expectedRevision === null || expectedRevision === undefined) { + die(`Mutation of ${id} requires an observed revision.`); + } + return withPortfolioLock(root, () => { const portfolio = loadPortfolio(root); const loaded = loadWorkstream(root, id, portfolio); - if (expectedRevision !== null && loaded.state.revision !== expectedRevision) { + if (loaded.state.revision !== expectedRevision) { die(`Revision conflict for ${id}: expected ${expectedRevision}, canonical is ${loaded.state.revision}. Reload before writing.`); } + const next = JSON.parse(JSON.stringify(loaded.state)); mutate(next); next.revision = loaded.state.revision + 1; + const errors = validateState(next); if (errors.length) die(`Refusing invalid update for ${id}:\n- ${errors.join('\n- ')}`); + writeJsonAtomic(loaded.file, next); return next; }); } function makePortfolio() { - return { schemaVersion: '0.2', id: 'default', revision: 0, aggregationPolicy: 'any-runnable', workstreams: [] }; + return { + schemaVersion: '0.2', + id: 'default', + revision: 0, + aggregationPolicy: 'any-runnable', + workstreams: [] + }; } function makeState(id, title, branch = null) { return { - schemaVersion: '0.2', id, title, revision: 0, phase: 'THINK', runState: 'RUNNABLE', branch, - lease: null, dependencies: [], waitConditions: [], authorityGates: [], nextAction: 'Complete THINK and freeze the first executable plan.', - evidenceRefs: [], durability: 'HOST_DURABLE', notesRef: `.ppgp/workstreams/${id}/notes.md` + schemaVersion: '0.2', + id, + title, + revision: 0, + phase: 'THINK', + runState: 'RUNNABLE', + branch, + lease: null, + dependencies: [], + waitConditions: [], + authorityGates: [], + nextAction: 'Complete THINK and freeze the first executable plan.', + evidenceRefs: [], + durability: 'HOST_DURABLE', + notesRef: `.ppgp/workstreams/${id}/notes.md` }; } @@ -286,7 +483,9 @@ function notesTemplate(id, title) { function dependencyGraph(root, portfolio) { const states = new Map(); - for (const ref of portfolio.workstreams) states.set(ref.id, loadWorkstream(root, ref.id, portfolio).state); + for (const ref of portfolio.workstreams) { + states.set(ref.id, loadWorkstream(root, ref.id, portfolio).state); + } return states; } @@ -294,49 +493,68 @@ function cycleErrors(states) { const errors = []; const visiting = new Set(); const visited = new Set(); + function visit(id, trail) { - if (visiting.has(id)) { errors.push(`dependency cycle: ${[...trail, id].join(' -> ')}`); return; } + if (visiting.has(id)) { + errors.push(`dependency cycle: ${[...trail, id].join(' -> ')}`); + return; + } if (visited.has(id)) return; + visiting.add(id); const state = states.get(id); - if (state) for (const dep of state.dependencies) { - if (!states.has(dep.workstream)) errors.push(`${id} depends on unknown workstream ${dep.workstream}`); - else visit(dep.workstream, [...trail, id]); + if (state) { + for (const dependency of state.dependencies) { + if (!states.has(dependency.workstream)) { + errors.push(`${id} depends on unknown workstream ${dependency.workstream}`); + } else { + visit(dependency.workstream, [...trail, id]); + } + } } visiting.delete(id); visited.add(id); } + for (const id of states.keys()) visit(id, []); return [...new Set(errors)]; } function dependencySatisfied(state, states) { - return state.dependencies.every((d) => states.has(d.workstream) && states.get(d.workstream).runState === 'COMPLETED'); + return state.dependencies.every( + (dependency) => states.has(dependency.workstream) && states.get(dependency.workstream).runState === 'COMPLETED' + ); } function aggregatePortfolio(states) { const list = [...states.values()]; if (list.length === 0) return 'IDLE'; - if (list.every((s) => s.runState === 'COMPLETED')) return 'COMPLETED'; - const eligible = list.filter((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED' && dependencySatisfied(s, states)); - if (eligible.some((s) => s.runState === 'RUNNING')) return 'RUNNING'; - if (eligible.some((s) => s.runState === 'RUNNABLE')) return 'RUNNABLE'; - if (eligible.some((s) => s.runState === 'RECOVERY_REQUIRED')) return 'RECOVERY_REQUIRED'; - if (list.some((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED')) return 'WAITING'; - return 'IDLE'; -} + if (list.every((state) => state.runState === 'COMPLETED')) return 'COMPLETED'; -function checkoutRegistryPath(root) { - const common = gitCommonDir(root); - return common ? path.join(common, 'ppgp-checkouts.json') : null; + const eligible = list.filter( + (state) => state.runState !== 'COMPLETED' && state.runState !== 'PARKED' && dependencySatisfied(state, states) + ); + + if (eligible.some((state) => state.runState === 'RUNNING')) return 'RUNNING'; + if (eligible.some((state) => state.runState === 'RUNNABLE')) return 'RUNNABLE'; + if (eligible.some((state) => state.runState === 'RECOVERY_REQUIRED')) return 'RECOVERY_REQUIRED'; + if (list.some((state) => state.runState !== 'COMPLETED' && state.runState !== 'PARKED')) return 'WAITING'; + return 'IDLE'; } function loadCheckoutRegistry(root) { const file = checkoutRegistryPath(root); - if (!file) return { file: null, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; - if (!fs.existsSync(file)) return { file, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + if (!file) { + return { file: null, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + } + if (!fs.existsSync(file)) { + return { file, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; + } + const data = readJson(file); - if (!data || data.schemaVersion !== '0.2-local' || !Array.isArray(data.claims)) die(`Invalid local checkout registry: ${file}`); + if (!data || data.schemaVersion !== '0.2-local' || !Array.isArray(data.claims)) { + die(`Invalid local checkout registry: ${file}`); + } return { file, data }; } @@ -346,7 +564,9 @@ function saveCheckoutRegistry(file, data) { writeJsonAtomic(file, data); } -function sha256(content) { return crypto.createHash('sha256').update(content).digest('hex'); } +function sha256(content) { + return crypto.createHash('sha256').update(content).digest('hex'); +} function cmdInit(root) { printMap(root); @@ -356,35 +576,70 @@ function cmdInit(root) { function cmdDoctor(root) { printMap(root); const issues = []; + if (!hasGit(root)) issues.push('Git forensic history was not detected.'); + if (!portfolioExists(root)) { - if (!findRole(root, 'ACTIVE_GOAL')) issues.push('No ACTIVE_GOAL is present. This is normal when no substantial goal is active.'); + if (!findRole(root, 'ACTIVE_GOAL')) { + issues.push('No ACTIVE_GOAL is present. This is normal when no substantial goal is active.'); + } } else { const portfolio = loadPortfolio(root); const states = dependencyGraph(root, portfolio); issues.push(...cycleErrors(states)); + for (const [id, state] of states) { - if (state.lease && state.lease.expiresAt && Date.parse(state.lease.expiresAt) <= Date.now() && state.lease.status === 'CLAIMED') issues.push(`${id} has an expired CLAIMED lease.`); + if ( + state.lease && + state.lease.expiresAt && + Date.parse(state.lease.expiresAt) <= Date.now() && + state.lease.status === 'CLAIMED' + ) { + issues.push(`${id} has an expired CLAIMED lease.`); + } } + const { data } = loadCheckoutRegistry(root); + const seenPaths = new Map(); for (const claim of data.claims) { - if (!states.has(claim.workstreamId)) issues.push(`checkout claim ${claim.path} references unknown workstream ${claim.workstreamId}`); - if (fs.existsSync(claim.path) && states.has(claim.workstreamId)) { - const actual = gitBranch(claim.path); - const declared = states.get(claim.workstreamId).branch; - if (declared && actual && declared !== actual) issues.push(`${claim.workstreamId} checkout branch mismatch: declared ${declared}, actual ${actual}`); + const canonicalPath = path.resolve(claim.path); + if (seenPaths.has(canonicalPath) && seenPaths.get(canonicalPath) !== claim.workstreamId) { + issues.push(`checkout ${canonicalPath} has conflicting claims by ${seenPaths.get(canonicalPath)} and ${claim.workstreamId}`); + } else { + seenPaths.set(canonicalPath, claim.workstreamId); + } + + if (!states.has(claim.workstreamId)) { + issues.push(`checkout claim ${claim.path} references unknown workstream ${claim.workstreamId}`); + continue; + } + + if (fs.existsSync(claim.path)) { + const actualBranch = gitBranch(claim.path); + const declaredBranch = states.get(claim.workstreamId).branch; + if (declaredBranch && actualBranch && declaredBranch !== actualBranch) { + issues.push(`${claim.workstreamId} checkout branch mismatch: declared ${declaredBranch}, actual ${actualBranch}`); + } } } } + console.log(issues.length ? `\nNotes:\n- ${issues.join('\n- ')}` : '\nNo obvious repository-level PPGP issues detected.'); } function cmdGoal(root, positional, force) { - if (portfolioExists(root)) die('A v0.2 portfolio exists. Use `ppgp workstream start <id> <title>` instead of replacing an ambiguous active goal.'); + if (portfolioExists(root)) { + die('A v0.2 portfolio exists. Use `ppgp workstream start <id> <title>` instead of replacing an ambiguous active goal.'); + } + const outcome = positional.join(' ').trim(); if (!outcome) die('goal requires an outcome, for example: ppgp goal "Ship the authentication migration"'); + const target = activeGoalPath(root); - if (fs.existsSync(target) && !force) die(`${path.relative(root, target)} already exists. Use --force only when intentionally replacing the active goal.`); + if (fs.existsSync(target) && !force) { + die(`${path.relative(root, target)} already exists. Use --force only when intentionally replacing the active goal.`); + } + fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, goalTemplate(outcome), 'utf8'); console.log(`Created ${path.relative(root, target)} in THINK phase.`); @@ -421,17 +676,31 @@ function printWorkstream(state) { function cmdStatus(root, all) { const portfolio = loadPortfolio(root); - if (!portfolio) return printLegacyStatus(root); + if (!portfolio) { + printLegacyStatus(root); + return; + } + const states = dependencyGraph(root, portfolio); const cycles = cycleErrors(states); if (cycles.length) die(cycles.join('; ')); + console.log(`PPGP/${pkg.version} portfolio ${portfolio.id} revision ${portfolio.revision}`); console.log(`portfolio_state: ${aggregatePortfolio(states)}`); - const eligible = [...states.values()].filter((s) => s.runState !== 'COMPLETED' && s.runState !== 'PARKED' && dependencySatisfied(s, states)); - console.log(`runnable: ${eligible.filter((s) => s.runState === 'RUNNABLE').map((s) => s.id).join(', ') || '(none)'}`); - console.log(`running: ${eligible.filter((s) => s.runState === 'RUNNING').map((s) => s.id).join(', ') || '(none)'}`); - console.log(`recovery_required: ${eligible.filter((s) => s.runState === 'RECOVERY_REQUIRED').map((s) => s.id).join(', ') || '(none)'}`); - if (all) for (const state of states.values()) { console.log(''); printWorkstream(state); } + + const eligible = [...states.values()].filter( + (state) => state.runState !== 'COMPLETED' && state.runState !== 'PARKED' && dependencySatisfied(state, states) + ); + console.log(`runnable: ${eligible.filter((state) => state.runState === 'RUNNABLE').map((state) => state.id).join(', ') || '(none)'}`); + console.log(`running: ${eligible.filter((state) => state.runState === 'RUNNING').map((state) => state.id).join(', ') || '(none)'}`); + console.log(`recovery_required: ${eligible.filter((state) => state.runState === 'RECOVERY_REQUIRED').map((state) => state.id).join(', ') || '(none)'}`); + + if (all) { + for (const state of states.values()) { + console.log(''); + printWorkstream(state); + } + } } function cmdHandoff(root) { @@ -448,14 +717,18 @@ function cmdHandoff(root) { console.log(`N:${oneLine(sections.NEXT_EXECUTABLE_ACTION)}`); return; } - if (portfolio.workstreams.length !== 1) die('Portfolio handoff is ambiguous. Use `ppgp workstream handoff <id> <new-holder>`.'); + + if (portfolio.workstreams.length !== 1) { + die('Portfolio handoff is ambiguous. Use `ppgp workstream handoff <id> <new-holder> --revision <n>`.'); + } + const state = loadWorkstream(root, portfolio.workstreams[0].id, portfolio).state; console.log(`PPGP/${pkg.version}`); console.log(`WS=${state.id}`); console.log(`P=${state.phase}`); console.log(`R=${state.runState}`); console.log(`L=${state.lease ? `${state.lease.holder}/${state.lease.status}/g${state.lease.generation}` : '(none)'}`); - console.log(`W=${state.waitConditions.map((w) => `${w.kind}/${w.scope}:${w.id}`).join(',') || '(none)'}`); + console.log(`W=${state.waitConditions.map((wait) => `${wait.kind}/${wait.scope}:${wait.id}`).join(',') || '(none)'}`); console.log(`U=${state.durability}`); console.log(`N=${state.nextAction || '(none)'}`); } @@ -464,192 +737,311 @@ function cmdMigrate(root, rollback) { if (rollback) { const metaFile = path.join(ppgpDir(root), 'MIGRATION.json'); if (!fs.existsSync(metaFile)) die('No reversible PPGP migration metadata found.'); + const meta = readJson(metaFile); - const source = path.resolve(root, meta.source); + const source = safeRepoRef(root, meta.source, 'migration source'); if (!fs.existsSync(source)) die(`Legacy source missing: ${meta.source}`); - if (sha256(fs.readFileSync(source)) !== meta.sourceSha256) die('Legacy source changed after migration; refusing automatic rollback.'); + if (sha256(fs.readFileSync(source)) !== meta.sourceSha256) { + die('Legacy source changed after migration; refusing automatic rollback.'); + } + + const portfolio = loadPortfolio(root); + if ( + !portfolio || + portfolio.revision !== 0 || + portfolio.workstreams.length !== 1 || + portfolio.workstreams[0].id !== 'legacy' + ) { + die('v0.2 portfolio diverged after migration; refusing destructive automatic rollback.'); + } + + const legacy = loadWorkstream(root, 'legacy', portfolio).state; + if (legacy.revision !== 0) { + die('Migrated legacy workstream changed after cutover; refusing destructive automatic rollback.'); + } + + const { data } = loadCheckoutRegistry(root); + if (data.claims.length > 0) { + die('Local checkout claims exist; release them before rollback.'); + } + fs.rmSync(ppgpDir(root), { recursive: true, force: true }); console.log('Rolled back v0.2 reference state. Legacy ACTIVE_GOAL is canonical again.'); return; } + if (portfolioExists(root)) die('Portfolio already exists; migration is unnecessary.'); const legacy = readActiveGoal(root); const id = 'legacy'; + withPortfolioLock(root, () => { const portfolio = makePortfolio(); - portfolio.workstreams.push({ id, stateRef: `.ppgp/workstreams/${id}/state.json`, notesRef: `.ppgp/workstreams/${id}/notes.md` }); + portfolio.workstreams.push({ + id, + stateRef: `.ppgp/workstreams/${id}/state.json`, + notesRef: `.ppgp/workstreams/${id}/notes.md` + }); + const phaseCandidate = oneLine(legacy.sections.PHASE, 'THINK').toUpperCase(); const phase = PHASES.has(phaseCandidate) ? phaseCandidate : 'THINK'; const state = makeState(id, oneLine(legacy.sections.GOAL, 'Migrated active goal'), gitBranch(root)); state.phase = phase; state.runState = phase === 'CLOSED' ? 'COMPLETED' : 'RUNNABLE'; state.nextAction = oneLine(legacy.sections.NEXT_EXECUTABLE_ACTION, null); - state.evidenceRefs = legacy.sections.VERIFICATION_EVIDENCE && !/^[-*]?\s*None/i.test(legacy.sections.VERIFICATION_EVIDENCE) ? [oneLine(legacy.sections.VERIFICATION_EVIDENCE)] : []; + state.evidenceRefs = ( + legacy.sections.VERIFICATION_EVIDENCE && + !/^[-*]?\s*None/i.test(legacy.sections.VERIFICATION_EVIDENCE) + ) ? [oneLine(legacy.sections.VERIFICATION_EVIDENCE)] : []; state.durability = 'HOST_DURABLE'; + fs.mkdirSync(workstreamDir(root, id), { recursive: true }); fs.writeFileSync(workstreamNotesPath(root, id), legacy.content, 'utf8'); writeJsonAtomic(workstreamStatePath(root, id), state); writeJsonAtomic(portfolioPath(root), portfolio); writeJsonAtomic(path.join(ppgpDir(root), 'MIGRATION.json'), { - schemaVersion: '0.2', source: legacy.file, sourceSha256: sha256(Buffer.from(legacy.content)), - cutoverAt: new Date().toISOString(), mode: 'CUTOVER', legacyRole: 'COMPATIBILITY_SNAPSHOT' + schemaVersion: '0.2', + source: legacy.file, + sourceSha256: sha256(Buffer.from(legacy.content)), + cutoverAt: new Date().toISOString(), + mode: 'CUTOVER', + legacyRole: 'COMPATIBILITY_SNAPSHOT' }); }); + console.log('Migrated legacy ACTIVE_GOAL into the v0.2 reference portfolio.'); console.log('The legacy file remains untouched as a compatibility snapshot; .ppgp state is now canonical.'); - console.log('Rollback while the legacy snapshot is unchanged: ppgp migrate --rollback'); + console.log('Rollback before v0.2 state diverges: ppgp migrate --rollback'); } function cmdWorkstream(root, positional, options) { const action = positional.shift(); if (!action) die('workstream requires an action: start, status, park, resume, handoff, recover, close'); + if (action === 'start') { const id = positional.shift(); const title = positional.join(' ').trim(); if (!ID_RE.test(id || '')) die('workstream start requires a safe id (letters, numbers, dot, underscore, dash).'); if (!title) die('workstream start requires a title.'); - if (!portfolioExists(root) && findRole(root, 'ACTIVE_GOAL')) die('Legacy ACTIVE_GOAL exists. Run `ppgp migrate` before adding concurrent workstreams.'); + if (!portfolioExists(root) && findRole(root, 'ACTIVE_GOAL')) { + die('Legacy ACTIVE_GOAL exists. Run `ppgp migrate` before adding concurrent workstreams.'); + } + withPortfolioLock(root, () => { const portfolio = portfolioExists(root) ? loadPortfolio(root) : makePortfolio(); - if (portfolio.workstreams.some((x) => x.id === id)) die(`Workstream already exists: ${id}`); + if (portfolio.workstreams.some((candidate) => candidate.id === id)) { + die(`Workstream already exists: ${id}`); + } + const state = makeState(id, title, options.branch || gitBranch(root)); fs.mkdirSync(workstreamDir(root, id), { recursive: true }); fs.writeFileSync(workstreamNotesPath(root, id), notesTemplate(id, title), 'utf8'); writeJsonAtomic(workstreamStatePath(root, id), state); - portfolio.workstreams.push({ id, stateRef: `.ppgp/workstreams/${id}/state.json`, notesRef: `.ppgp/workstreams/${id}/notes.md` }); + + portfolio.workstreams.push({ + id, + stateRef: `.ppgp/workstreams/${id}/state.json`, + notesRef: `.ppgp/workstreams/${id}/notes.md` + }); portfolio.revision += 1; writeJsonAtomic(portfolioPath(root), portfolio); }); + console.log(`Started workstream ${id} in THINK/RUNNABLE.`); return; } + const id = positional.shift(); if (!id) die(`workstream ${action} requires a workstream id.`); - if (action === 'status') { printWorkstream(loadWorkstream(root, id).state); return; } + + if (action === 'status') { + printWorkstream(loadWorkstream(root, id).state); + return; + } + if (action === 'park') { - const next = updateState(root, id, (s) => { s.runState = 'PARKED'; s.lease = null; }, options.revision); + const revision = requireRevision(options, 'workstream park'); + const next = updateState(root, id, (state) => { + state.runState = 'PARKED'; + state.lease = null; + }, revision); console.log(`Parked ${id} at revision ${next.revision}.`); return; } + if (action === 'resume') { - const next = updateState(root, id, (s) => { if (s.runState === 'COMPLETED') die('Completed workstreams cannot be resumed.'); s.runState = 'RUNNABLE'; s.lease = null; }, options.revision); + const revision = requireRevision(options, 'workstream resume'); + const next = updateState(root, id, (state) => { + if (state.runState === 'COMPLETED') die('Completed workstreams cannot be resumed.'); + state.runState = 'RUNNABLE'; + state.lease = null; + }, revision); console.log(`Resumed ${id} as RUNNABLE at revision ${next.revision}.`); return; } + if (action === 'handoff' || action === 'recover') { + const revision = requireRevision(options, `workstream ${action}`); const holder = positional.shift() || options.holder; if (!holder) die(`workstream ${action} requires the new holder.`); if (action === 'recover') verifyRecoveryCheckout(root, id); - const next = updateState(root, id, (s) => { - if (s.runState === 'COMPLETED') die('Completed workstreams cannot receive a lease.'); - const generation = s.lease && Number.isInteger(s.lease.generation) ? s.lease.generation + 1 : 1; - s.lease = { holder, status: 'CLAIMED', generation, claimedAt: new Date().toISOString(), expiresAt: null }; - s.runState = 'RUNNING'; - }, options.revision); + + const next = updateState(root, id, (state) => { + if (state.runState === 'COMPLETED') die('Completed workstreams cannot receive a lease.'); + const generation = state.lease && Number.isInteger(state.lease.generation) + ? state.lease.generation + 1 + : 1; + state.lease = { + holder, + status: 'CLAIMED', + generation, + claimedAt: new Date().toISOString(), + expiresAt: null + }; + state.runState = 'RUNNING'; + }, revision); + console.log(`${action === 'recover' ? 'Recovered' : 'Handed off'} ${id} to ${holder} at lease generation ${next.lease.generation}, revision ${next.revision}.`); return; } + if (action === 'close') { - const next = updateState(root, id, (s) => { - if (s.phase !== 'CLOSED') die('Refusing close: workstream phase must already be CLOSED after verified PPGP closure.'); - s.runState = 'COMPLETED'; - s.lease = null; - s.nextAction = null; - }, options.revision); + const revision = requireRevision(options, 'workstream close'); + const next = updateState(root, id, (state) => { + if (state.phase !== 'CLOSED') { + die('Refusing close: workstream phase must already be CLOSED after verified PPGP closure.'); + } + state.runState = 'COMPLETED'; + state.lease = null; + state.nextAction = null; + }, revision); releaseCheckoutClaimsFor(root, id); console.log(`Closed ${id} as COMPLETED at revision ${next.revision}.`); return; } + die(`Unknown workstream action: ${action}`); } function verifyRecoveryCheckout(root, id) { const { data } = loadCheckoutRegistry(root); - const claims = data.claims.filter((c) => c.workstreamId === id); + const claims = data.claims.filter((claim) => claim.workstreamId === id); const state = loadWorkstream(root, id).state; + for (const claim of claims) { - if (!fs.existsSync(claim.path)) die(`Claimed checkout for ${id} no longer exists: ${claim.path}`); - const actual = gitBranch(claim.path); - if (state.branch && actual && actual !== state.branch) die(`Recovery refused: ${id} declared branch ${state.branch}, claimed checkout is ${actual}. Inspect before takeover.`); + if (!fs.existsSync(claim.path)) { + die(`Claimed checkout for ${id} no longer exists: ${claim.path}`); + } + const actualBranch = gitBranch(claim.path); + if (state.branch && actualBranch && actualBranch !== state.branch) { + die(`Recovery refused: ${id} declared branch ${state.branch}, claimed checkout is ${actualBranch}. Inspect before takeover.`); + } } } function releaseCheckoutClaimsFor(root, id) { - const { file, data } = loadCheckoutRegistry(root); + const file = checkoutRegistryPath(root); if (!file) return; - const before = data.claims.length; - data.claims = data.claims.filter((c) => c.workstreamId !== id); - if (data.claims.length !== before) saveCheckoutRegistry(file, data); + + withCheckoutLock(root, () => { + const { data } = loadCheckoutRegistry(root); + const before = data.claims.length; + data.claims = data.claims.filter((claim) => claim.workstreamId !== id); + if (data.claims.length !== before) saveCheckoutRegistry(file, data); + }); } function cmdCheckout(root, positional) { const action = positional.shift(); if (!action) die('checkout requires an action: status, claim, release'); - const { file, data } = loadCheckoutRegistry(root); + if (action === 'status') { + const { file, data } = loadCheckoutRegistry(root); console.log(`PPGP/${pkg.version} local checkout registry`); console.log(`storage: ${file || '(Git unavailable)'}`); - if (!data.claims.length) console.log('claims: (none)'); - else for (const c of data.claims) console.log(`${c.path} -> ${c.workstreamId} branch=${c.branch || '(none)'} head=${c.head || '(none)'}`); + if (!data.claims.length) { + console.log('claims: (none)'); + } else { + for (const claim of data.claims) { + console.log(`${claim.path} -> ${claim.workstreamId} branch=${claim.branch || '(none)'} head=${claim.head || '(none)'}`); + } + } return; } - if (!file) die('checkout claims require a Git repository.'); + + if (!hasGit(root)) die('checkout claims require a Git repository.'); + if (action === 'claim') { const id = positional.shift(); const target = path.resolve(positional.shift() || root); if (!id) die('checkout claim requires a workstream id.'); - const state = loadWorkstream(root, id).state; - if (!state.lease || state.lease.status !== 'CLAIMED' || state.runState !== 'RUNNING') die(`checkout claim requires ${id} to hold a valid CLAIMED execution lease.`); - if (!hasGit(target)) die(`Target is not a Git checkout: ${target}`); - const actualBranch = gitBranch(target); - if (state.branch && actualBranch && state.branch !== actualBranch) die(`Branch mismatch: ${id} declares ${state.branch}, checkout is ${actualBranch}.`); - const existing = data.claims.find((c) => path.resolve(c.path) === target); - if (existing && existing.workstreamId !== id) die(`Checkout already claimed by ${existing.workstreamId}: ${target}`); - const claim = { path: target, workstreamId: id, branch: actualBranch, head: gitHead(target), dirty: gitDirty(target) === true, claimedAt: new Date().toISOString() }; - if (existing) Object.assign(existing, claim); else data.claims.push(claim); - saveCheckoutRegistry(file, data); + + withCheckoutLock(root, () => { + const state = loadWorkstream(root, id).state; + if (!state.lease || state.lease.status !== 'CLAIMED' || state.runState !== 'RUNNING') { + die(`checkout claim requires ${id} to hold a valid CLAIMED execution lease.`); + } + if (!hasGit(target)) die(`Target is not a Git checkout: ${target}`); + + const actualBranch = gitBranch(target); + if (state.branch && actualBranch && state.branch !== actualBranch) { + die(`Branch mismatch: ${id} declares ${state.branch}, checkout is ${actualBranch}.`); + } + + const { file, data } = loadCheckoutRegistry(root); + const existing = data.claims.find((claim) => path.resolve(claim.path) === target); + if (existing && existing.workstreamId !== id) { + die(`Checkout already claimed by ${existing.workstreamId}: ${target}`); + } + + const claim = { + path: target, + workstreamId: id, + branch: actualBranch, + head: gitHead(target), + dirty: gitDirty(target) === true, + claimedAt: new Date().toISOString() + }; + + if (existing) Object.assign(existing, claim); + else data.claims.push(claim); + saveCheckoutRegistry(file, data); + }); + console.log(`Claimed ${target} for ${id}.`); return; } + if (action === 'release') { const target = path.resolve(positional.shift() || root); - const before = data.claims.length; - data.claims = data.claims.filter((c) => path.resolve(c.path) !== target); - if (before === data.claims.length) die(`No checkout claim found for ${target}`); - saveCheckoutRegistry(file, data); + + withCheckoutLock(root, () => { + const { file, data } = loadCheckoutRegistry(root); + const before = data.claims.length; + data.claims = data.claims.filter((claim) => path.resolve(claim.path) !== target); + if (before === data.claims.length) die(`No checkout claim found for ${target}`); + saveCheckoutRegistry(file, data); + }); + console.log(`Released checkout claim for ${target}.`); return; } + die(`Unknown checkout action: ${action}`); } -function checkoutRegistryPath(root) { - const common = gitCommonDir(root); - return common ? path.join(common, 'ppgp-checkouts.json') : null; +function cmdSkillPath() { + console.log(path.join(path.resolve(__dirname, '..'), 'skills', 'ppgp')); } -function loadCheckoutRegistry(root) { - const file = checkoutRegistryPath(root); - if (!file) return { file: null, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; - if (!fs.existsSync(file)) return { file, data: { schemaVersion: '0.2-local', revision: 0, claims: [] } }; - const data = readJson(file); - if (!data || data.schemaVersion !== '0.2-local' || !Array.isArray(data.claims)) die(`Invalid local checkout registry: ${file}`); - return { file, data }; -} - -function saveCheckoutRegistry(file, data) { - if (!file) die('Checkout claims require Git so they can live in the local Git common directory.'); - data.revision = (Number.isInteger(data.revision) ? data.revision : 0) + 1; - writeJsonAtomic(file, data); -} - -function cmdSkillPath() { console.log(path.join(path.resolve(__dirname, '..'), 'skills', 'ppgp')); } - function cmdInstallSkill(positional) { const destination = positional[0]; - if (!destination) die('install-skill requires a destination directory, for example: ppgp install-skill ~/.config/agent-skills'); + if (!destination) { + die('install-skill requires a destination directory, for example: ppgp install-skill ~/.config/agent-skills'); + } + const source = path.join(path.resolve(__dirname, '..'), 'skills', 'ppgp'); if (!fs.existsSync(source)) die('Bundled Agent Skill was not found in this package.'); + const parent = path.resolve(destination); const target = path.join(parent, 'ppgp'); fs.mkdirSync(parent, { recursive: true }); @@ -658,16 +1050,26 @@ function cmdInstallSkill(positional) { } function help() { - console.log(`PPGP ${pkg.version}\nPortable Persistent Goal Protocol CLI\n\nLegacy/single-workstream:\n ppgp init [--root PATH]\n ppgp doctor [--root PATH]\n ppgp goal <outcome> [--root PATH] [--force]\n ppgp status [--root PATH] [--all]\n ppgp handoff [--root PATH]\n ppgp migrate [--root PATH]\n ppgp migrate --rollback [--root PATH]\n\nPortfolio/workstreams:\n ppgp workstream start <id> <title> [--branch BRANCH]\n ppgp workstream status <id>\n ppgp workstream park <id> [--revision N]\n ppgp workstream resume <id> [--revision N]\n ppgp workstream handoff <id> <new-holder> [--revision N]\n ppgp workstream recover <id> <new-holder> [--revision N]\n ppgp workstream close <id> [--revision N]\n\nLocal checkout claims:\n ppgp checkout status\n ppgp checkout claim <workstream-id> [checkout-path]\n ppgp checkout release [checkout-path]\n\nDistribution:\n ppgp skill-path\n ppgp install-skill <destination>\n ppgp --version\n\nReference JSON state uses local revision checks and a local mutation lock. Checkout claims live in the Git common directory and are not committed.\n`); + console.log(`PPGP ${pkg.version}\nPortable Persistent Goal Protocol CLI\n\nLegacy/single-workstream:\n ppgp init [--root PATH]\n ppgp doctor [--root PATH]\n ppgp goal <outcome> [--root PATH] [--force]\n ppgp status [--root PATH] [--all]\n ppgp handoff [--root PATH]\n ppgp migrate [--root PATH]\n ppgp migrate --rollback [--root PATH]\n\nPortfolio/workstreams:\n ppgp workstream start <id> <title> [--branch BRANCH]\n ppgp workstream status <id>\n ppgp workstream park <id> --revision N\n ppgp workstream resume <id> --revision N\n ppgp workstream handoff <id> <new-holder> --revision N\n ppgp workstream recover <id> <new-holder> --revision N\n ppgp workstream close <id> --revision N\n\nLocal checkout claims:\n ppgp checkout status\n ppgp checkout claim <workstream-id> [checkout-path]\n ppgp checkout release [checkout-path]\n\nDistribution:\n ppgp skill-path\n ppgp install-skill <destination>\n ppgp --version\n\nReference JSON state uses observed revisions plus local mutation locks. Checkout claims live in the Git common directory and are not committed.\n`); } function main() { const raw = process.argv.slice(2); - if (raw.length === 0 || raw.includes('--help') || raw.includes('-h')) { help(); return; } - if (raw.includes('--version') || raw.includes('-v')) { console.log(pkg.version); return; } + + if (raw.length === 0 || raw.includes('--help') || raw.includes('-h')) { + help(); + return; + } + if (raw.includes('--version') || raw.includes('-v')) { + console.log(pkg.version); + return; + } + const command = raw.shift(); const { options, positional } = parseArgs(raw); - if (!fs.existsSync(options.root) || !fs.statSync(options.root).isDirectory()) die(`Root is not a directory: ${options.root}`); + if (!fs.existsSync(options.root) || !fs.statSync(options.root).isDirectory()) { + die(`Root is not a directory: ${options.root}`); + } switch (command) { case 'init': cmdInit(options.root); break; @@ -684,8 +1086,13 @@ function main() { } } -try { main(); } -catch (error) { - if (error instanceof PPGPError) { console.error(`PPGP: ${error.message}`); process.exitCode = error.code; } - else throw error; +try { + main(); +} catch (error) { + if (error instanceof PPGPError) { + console.error(`PPGP: ${error.message}`); + process.exitCode = error.code; + } else { + throw error; + } } From 26a488e772c22559da6d6895e4bbbd32af2bc810 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:47:09 +0200 Subject: [PATCH 34/47] test: harden v0.2 CAS rollback and path-safety cases --- test/v020.test.js | 154 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 119 insertions(+), 35 deletions(-) diff --git a/test/v020.test.js b/test/v020.test.js index f2451d7..f97fe5f 100644 --- a/test/v020.test.js +++ b/test/v020.test.js @@ -1,4 +1,5 @@ 'use strict'; + const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -6,18 +7,28 @@ const { spawnSync } = require('child_process'); const repo = path.resolve(__dirname, '..'); const cli = path.join(repo, 'bin', 'ppgp.js'); -function assert(x, m) { if (!x) throw new Error(m); } + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + function run(args, ok = true) { - const r = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }); - if (ok && r.status !== 0) throw new Error(`FAILED ${args.join(' ')}\n${r.stdout}\n${r.stderr}`); - if (!ok && r.status === 0) throw new Error(`EXPECTED FAILURE ${args.join(' ')}\n${r.stdout}`); - return r; + const result = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }); + if (ok && result.status !== 0) { + throw new Error(`FAILED ${args.join(' ')}\n${result.stdout}\n${result.stderr}`); + } + if (!ok && result.status === 0) { + throw new Error(`EXPECTED FAILURE ${args.join(' ')}\n${result.stdout}`); + } + return result; } + function git(root, args) { - const r = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); - if (r.status !== 0) throw new Error(r.stderr); - return r.stdout.trim(); + const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); + if (result.status !== 0) throw new Error(result.stderr); + return result.stdout.trim(); } + function makeRepo() { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ppgp-v020-')); git(root, ['init', '-q']); @@ -28,86 +39,159 @@ function makeRepo() { git(root, ['commit', '-qm', 'seed']); return root; } -function j(root, rel) { return JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8')); } -function w(root, rel, obj) { fs.writeFileSync(path.join(root, rel), `${JSON.stringify(obj, null, 2)}\n`); } + +function readJson(root, rel) { + return JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8')); +} + +function writeJson(root, rel, value) { + fs.writeFileSync(path.join(root, rel), `${JSON.stringify(value, null, 2)}\n`, 'utf8'); +} const roots = []; + try { - const legacy = makeRepo(); roots.push(legacy); + // Legacy behavior and reversible migration. + const legacy = makeRepo(); + roots.push(legacy); run(['goal', 'Legacy', 'goal', '--root', legacy]); assert(run(['status', '--root', legacy]).stdout.includes('status from'), 'legacy status missing'); const original = fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8'); + run(['migrate', '--root', legacy]); assert(fs.existsSync(path.join(legacy, '.ppgp', 'portfolio.json')), 'portfolio missing after migrate'); assert(fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8') === original, 'migration mutated legacy source'); assert(run(['status', '--all', '--root', legacy]).stdout.includes('portfolio_state: RUNNABLE'), 'migrated portfolio not runnable'); + run(['migrate', '--rollback', '--root', legacy]); assert(!fs.existsSync(path.join(legacy, '.ppgp')), 'rollback left .ppgp'); assert(fs.readFileSync(path.join(legacy, 'docs', 'ACTIVE_GOAL.md'), 'utf8') === original, 'rollback mutated legacy source'); - const root = makeRepo(); roots.push(root); - run(['workstream', 'start', 'a', 'Workstream A', '--root', root, '--branch', 'master']); - run(['workstream', 'start', 'b', 'Workstream B', '--root', root, '--branch', 'master']); - let portfolio = j(root, '.ppgp/portfolio.json'); + // Rollback must refuse to destroy post-cutover v0.2 state. + run(['migrate', '--root', legacy]); + let migrated = readJson(legacy, '.ppgp/workstreams/legacy/state.json'); + migrated.revision += 1; + migrated.nextAction = 'Changed after cutover'; + writeJson(legacy, '.ppgp/workstreams/legacy/state.json', migrated); + const destructiveRollback = run(['migrate', '--rollback', '--root', legacy], false); + assert(destructiveRollback.stderr.includes('changed after cutover'), 'rollback did not refuse diverged v0.2 state'); + fs.rmSync(path.join(legacy, '.ppgp'), { recursive: true, force: true }); + + // Portfolio/workstream coordination. + const root = makeRepo(); + roots.push(root); + const branch = git(root, ['branch', '--show-current']); + + run(['workstream', 'start', 'a', 'Workstream A', '--root', root, '--branch', branch]); + run(['workstream', 'start', 'b', 'Workstream B', '--root', root, '--branch', branch]); + let portfolio = readJson(root, '.ppgp/portfolio.json'); assert(portfolio.workstreams.length === 2 && portfolio.revision === 2, 'portfolio revision mismatch'); + // Existing-workstream mutation requires observed revision. + const missingRevision = run(['workstream', 'handoff', 'a', 'codex', '--root', root], false); + assert(missingRevision.stderr.includes('requires --revision'), 'mutation without revision was accepted'); + run(['workstream', 'handoff', 'a', 'codex', '--root', root, '--revision', '0']); - let a = j(root, '.ppgp/workstreams/a/state.json'); + let a = readJson(root, '.ppgp/workstreams/a/state.json'); assert(a.runState === 'RUNNING' && a.lease.generation === 1, 'first lease incorrect'); + const conflict = run(['workstream', 'park', 'a', '--root', root, '--revision', '0'], false); assert(conflict.stderr.includes('Revision conflict'), 'stale revision was not rejected'); - assert(!fs.existsSync(path.join(root, '.ppgp', '.lock')), 'CAS failure left a stale lock'); + assert(!fs.existsSync(path.join(root, '.ppgp', '.lock')), 'CAS failure left a stale portfolio lock'); + // Checkout registry is local and exclusive. run(['checkout', 'claim', 'a', root, '--root', root]); assert(fs.existsSync(path.join(root, '.git', 'ppgp-checkouts.json')), 'checkout registry not stored in git common dir'); - run(['workstream', 'handoff', 'b', 'claude', '--root', root]); + assert(!fs.existsSync(path.join(root, '.git', 'ppgp-checkouts.lock')), 'checkout claim left stale lock'); + + run(['workstream', 'handoff', 'b', 'claude', '--root', root, '--revision', '0']); const collision = run(['checkout', 'claim', 'b', root, '--root', root], false); assert(collision.stderr.includes('already claimed by a'), 'checkout collision not rejected'); + assert(!fs.existsSync(path.join(root, '.git', 'ppgp-checkouts.lock')), 'checkout collision left stale lock'); + // Handoff fences older executor generation. run(['workstream', 'handoff', 'a', 'claude', '--root', root, '--revision', '1']); - a = j(root, '.ppgp/workstreams/a/state.json'); + a = readJson(root, '.ppgp/workstreams/a/state.json'); assert(a.lease.generation === 2 && a.lease.holder === 'claude', 'lease generation did not increment'); - a.runState = 'WAITING'; a.lease = null; a.revision += 1; + // Mixed waits retain independent reasons. + a.runState = 'WAITING'; + a.lease = null; + a.revision += 1; a.waitConditions = [ { id: 'ssh', kind: 'EXTERNAL', scope: 'ACTION', dependency: 'SSH coordinates', resumeCondition: 'coordinates available' }, { id: 'spend', kind: 'AUTHORITY', scope: 'ACTION', dependency: 'paid compute approval', resumeCondition: 'authority granted' } ]; - w(root, '.ppgp/workstreams/a/state.json', a); + a.authorityGates = [ + { id: 'paid-compute', action: 'create paid compute', state: 'REQUIRED' } + ]; + writeJson(root, '.ppgp/workstreams/a/state.json', a); + let status = run(['status', '--all', '--root', root]).stdout; assert(status.includes('portfolio_state: RUNNING'), 'independent running workstream should keep portfolio running'); assert(status.includes('EXTERNAL/ACTION:ssh') && status.includes('AUTHORITY/ACTION:spend'), 'mixed waits were collapsed or lost'); + assert(status.includes('paid-compute:REQUIRED'), 'authority gate state missing'); - let b = j(root, '.ppgp/workstreams/b/state.json'); - b.runState = 'RUNNABLE'; b.lease = null; b.dependencies = [{ workstream: 'a', condition: 'COMPLETED' }]; b.revision += 1; - w(root, '.ppgp/workstreams/b/state.json', b); + // Dependency blocks only its dependent workstream. + let b = readJson(root, '.ppgp/workstreams/b/state.json'); + b.runState = 'RUNNABLE'; + b.lease = null; + b.dependencies = [{ workstream: 'a', condition: 'COMPLETED' }]; + b.revision += 1; + writeJson(root, '.ppgp/workstreams/b/state.json', b); status = run(['status', '--all', '--root', root]).stdout; assert(status.includes('portfolio_state: WAITING'), 'unsatisfied dependency plus waiting prerequisite should aggregate WAITING'); - a.dependencies = [{ workstream: 'b', condition: 'COMPLETED' }]; a.revision += 1; - w(root, '.ppgp/workstreams/a/state.json', a); + // Cycles are invalid coordination state. + a.dependencies = [{ workstream: 'b', condition: 'COMPLETED' }]; + a.revision += 1; + writeJson(root, '.ppgp/workstreams/a/state.json', a); const doctorCycle = run(['doctor', '--root', root]); assert(doctorCycle.stdout.includes('dependency cycle'), 'doctor did not report dependency cycle'); - a.dependencies = []; a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + a.dependencies = []; + a.revision += 1; + writeJson(root, '.ppgp/workstreams/a/state.json', a); + // Abrupt takeover increments generation and preserves claimed checkout. a.runState = 'RECOVERY_REQUIRED'; - a.lease = { holder: 'codex', status: 'RECOVERY_REQUIRED', generation: 2, claimedAt: new Date().toISOString(), expiresAt: null }; - a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + a.lease = { + holder: 'codex', + status: 'RECOVERY_REQUIRED', + generation: 2, + claimedAt: new Date().toISOString(), + expiresAt: null + }; + a.revision += 1; + writeJson(root, '.ppgp/workstreams/a/state.json', a); run(['workstream', 'recover', 'a', 'claude', '--root', root, '--revision', String(a.revision)]); - a = j(root, '.ppgp/workstreams/a/state.json'); + a = readJson(root, '.ppgp/workstreams/a/state.json'); assert(a.runState === 'RUNNING' && a.lease.generation === 3 && a.lease.holder === 'claude', 'recover did not fence old generation'); - const premature = run(['workstream', 'close', 'a', '--root', root], false); + // Close requires verified CLOSED phase and current revision. + const premature = run(['workstream', 'close', 'a', '--root', root, '--revision', String(a.revision)], false); assert(premature.stderr.includes('phase must already be CLOSED'), 'premature close was allowed'); - a = j(root, '.ppgp/workstreams/a/state.json'); a.phase = 'CLOSED'; a.revision += 1; w(root, '.ppgp/workstreams/a/state.json', a); + assert(!fs.existsSync(path.join(root, '.ppgp', '.lock')), 'failed close left stale lock'); + + a = readJson(root, '.ppgp/workstreams/a/state.json'); + a.phase = 'CLOSED'; + a.revision += 1; + writeJson(root, '.ppgp/workstreams/a/state.json', a); run(['workstream', 'close', 'a', '--root', root, '--revision', String(a.revision)]); - assert(j(root, '.ppgp/workstreams/a/state.json').runState === 'COMPLETED', 'close did not complete workstream'); - assert(!j(root, '.git/ppgp-checkouts.json').claims.some((c) => c.workstreamId === 'a'), 'close did not release checkout claim'); + assert(readJson(root, '.ppgp/workstreams/a/state.json').runState === 'COMPLETED', 'close did not complete workstream'); + assert(!readJson(root, '.git/ppgp-checkouts.json').claims.some((claim) => claim.workstreamId === 'a'), 'close did not release checkout claim'); + // Generic goal remains forbidden once portfolio state is explicit. const ambiguous = run(['goal', 'bad', '--root', root], false); assert(ambiguous.stderr.includes('workstream start'), 'goal did not reject portfolio ambiguity'); - console.log('PPGP v0.2 portfolio, lease, checkout, migration, recovery, and CAS tests passed.'); + // Repository references cannot escape the repository root. + portfolio = readJson(root, '.ppgp/portfolio.json'); + portfolio.workstreams[0].stateRef = '../outside.json'; + writeJson(root, '.ppgp/portfolio.json', portfolio); + const traversal = run(['status', '--all', '--root', root], false); + assert(traversal.stderr.includes('escapes repository root'), 'path traversal reference was accepted'); + + console.log('PPGP v0.2 portfolio, CAS, lease, checkout, migration, recovery, authority, dependency, and path-safety tests passed.'); } finally { for (const root of roots) fs.rmSync(root, { recursive: true, force: true }); } From 6c07bac72105da48bce1059173589db36a89045b Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:47:35 +0200 Subject: [PATCH 35/47] chore: restore canonical v0.2 coordination parity --- skills/ppgp/references/COORDINATION.md | 60 +++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/skills/ppgp/references/COORDINATION.md b/skills/ppgp/references/COORDINATION.md index eb44dd3..55d410c 100644 --- a/skills/ppgp/references/COORDINATION.md +++ b/skills/ppgp/references/COORDINATION.md @@ -74,7 +74,6 @@ Record the smallest true scope. kind = EXTERNAL | AUTHORITY | TECHNICAL scope = ACTION | WORKSTREAM | GOAL | PROJECT resume_condition = observable condition -independent_work_remaining = yes | no ``` Before promoting a wait to workstream scope, ask: @@ -94,12 +93,16 @@ A lease says which executor currently owns mutation of a workstream. Recommended states: ```text -ACTIVE +CLAIMED HANDOFF_READY RECOVERY_REQUIRED RELEASED ``` +Use a monotonically increasing generation or equivalent fencing value when takeovers/concurrent writers are possible. + +A stale generation must not overwrite a newer canonical generation. + Lease loss does not authorize destructive cleanup. A lease does not expand human-delegated authority. @@ -130,6 +133,7 @@ clean stash foreign work commit foreign work overwrite +repurpose ``` Prefer an isolated workspace when safely available. @@ -138,6 +142,48 @@ For Git repositories, a linked worktree is a normal implementation option. Creating safe isolation is normally agent-solvable and should not become a human approval gate unless project policy forbids it. +## Authority gates + +Bind authority to a specific action. + +Recommended lifecycle: + +```text +REQUIRED +GRANTED +CONSUMED +REVOKED +``` + +An agent cannot self-grant authority. + +A granted gate authorizes only the named action/scope. + +Do not store secrets merely to model authority. + +## Dependencies + +Dependencies must be explicit when they affect scheduling. + +A minimal dependency is: + +```text +workstream=<id> +condition=COMPLETED +``` + +Do not infer dependencies from branch names, checkout location, agent identity, document order, or conversation order. + +Reject cycles. + +## Revision / CAS + +Canonical machine state should carry a revision or equivalent compare-and-swap mechanism when multiple writers are possible. + +A stale revision must not silently overwrite newer state. + +The reference CLI uses integer revisions plus a local mutation lock. Multi-machine implementations need atomic storage or equivalent CAS. + ## Durability Classify unfinished work when recovery risk matters: @@ -167,10 +213,12 @@ VERIFY -> update goal/workstream state -> record workspace + durability -> record NEXT +-> transfer/release lease -> emit compact handoff --> mark lease HANDOFF_READY/RELEASED ``` +A handoff to a new executor should increment lease generation atomically. + ## Abrupt takeover When the prior executor disappears with ambiguous or dirty mutable state: @@ -188,7 +236,7 @@ Then: 4. compare with last checkpoint 5. classify durability/uncertainty 6. verify proportionately -7. record takeover/new lease +7. record takeover/new lease generation 8. continue smallest verified next action ``` @@ -197,10 +245,12 @@ Do not normalize with destructive Git operations merely to obtain a clean status ## Project scheduler rule ```text -if any permitted workstream is RUNNABLE or RUNNING: +if any eligible workstream is RUNNABLE or RUNNING: project is not blocked ``` +RECOVERY_REQUIRED should be inspected before duplicating replacement work. + Before asking the human about one blocked workstream, continue another useful workstream when safe and within delegated scope. Human escalation remains scoped to the genuine authority boundary. From 69cee162dd81544848e9c59c4131ba603bfc5dfc Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:48:05 +0200 Subject: [PATCH 36/47] test: enforce v0.2 skill schema and mirror consistency --- test/cli.test.js | 79 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/test/cli.test.js b/test/cli.test.js index b231fc6..ff9deb7 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -1,4 +1,5 @@ 'use strict'; + const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -10,7 +11,9 @@ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ppgp-cli-')); function run(args) { const result = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }); - if (result.status !== 0) throw new Error(`command failed: ${args.join(' ')}\n${result.stdout}\n${result.stderr}`); + if (result.status !== 0) { + throw new Error(`command failed: ${args.join(' ')}\n${result.stdout}\n${result.stderr}`); + } return result.stdout; } @@ -29,6 +32,8 @@ function assert(condition, message) { try { const pkg = readJson('package.json'); const version = pkg.version; + + // Legacy/single-workstream CLI remains intact. assert(run(['--version']).trim() === version, `version output mismatch: expected ${version}`); run(['init', '--root', root]); run(['goal', 'Ship', 'the', 'test', '--root', root]); @@ -38,6 +43,7 @@ try { assert(run(['status', '--root', root]).includes('goal: Ship the test'), 'status did not recover goal'); assert(run(['handoff', '--root', root]).includes(`PPGP/${version}`), 'handoff protocol header version mismatch'); + // Platform manifests. const claudePlugin = readJson('.claude-plugin/plugin.json'); const claudeMarketplace = readJson('.claude-plugin/marketplace.json'); const packagedClaudePlugin = readJson('plugins/ppgp/.claude-plugin/plugin.json'); @@ -53,38 +59,73 @@ try { for (const [name, manifest] of Object.entries({ claudePlugin, packagedClaudePlugin })) { assert(manifest.name === 'ppgp', `${name} name mismatch`); - assert(!Object.prototype.hasOwnProperty.call(manifest, 'version'), `${name} should not pin a static version; Claude marketplace refresh follows repository revisions`); + assert( + !Object.prototype.hasOwnProperty.call(manifest, 'version'), + `${name} should not pin a static version; Claude marketplace refresh follows repository revisions` + ); } assert(claudeMarketplace.name === 'ppgp', 'Claude marketplace name mismatch'); - assert(claudeMarketplace.plugins.length === 1 && claudeMarketplace.plugins[0].name === 'ppgp', 'Claude marketplace plugin mismatch'); + assert( + claudeMarketplace.plugins.length === 1 && claudeMarketplace.plugins[0].name === 'ppgp', + 'Claude marketplace plugin mismatch' + ); assert(claudeMarketplace.plugins[0].source === './plugins/ppgp', 'Claude marketplace must point to packaged plugin directory'); assert(fs.existsSync(path.join(repo, 'plugins', 'ppgp', '.claude-plugin', 'plugin.json')), 'packaged Claude plugin manifest missing'); assert(codexMarketplace.name === 'ppgp', 'Codex marketplace name mismatch'); - assert(codexMarketplace.plugins.length === 1 && codexMarketplace.plugins[0].name === 'ppgp', 'Codex marketplace plugin mismatch'); + assert( + codexMarketplace.plugins.length === 1 && codexMarketplace.plugins[0].name === 'ppgp', + 'Codex marketplace plugin mismatch' + ); assert(codexPlugin.skills === './skills/', 'Codex plugin must use canonical skills directory'); assert(agentPlugin.$schema === 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', 'Agent Plugin schema mismatch'); - const allowedAgentPluginKeys = new Set(['$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'extensions']); + const allowedAgentPluginKeys = new Set([ + '$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'extensions' + ]); for (const key of Object.keys(agentPlugin)) { assert(allowedAgentPluginKeys.has(key), `Agent Plugin contains unsupported top-level field: ${key}`); } - assert(fs.existsSync(path.join(repo, 'skills', 'ppgp', 'SKILL.md')), 'canonical skill missing'); - assert(fs.existsSync(path.join(repo, 'skills', 'ppgp', 'references', 'PPGP.md')), 'canonical reference missing'); + // Canonical Agent Skill + deterministic mirrors. + for (const rel of [ + 'skills/ppgp/SKILL.md', + 'skills/ppgp/references/PPGP.md', + 'skills/ppgp/references/COORDINATION.md', + 'schemas/portfolio.schema.json', + 'schemas/workstream.schema.json' + ]) { + assert(fs.existsSync(path.join(repo, rel)), `required v0.2 artifact missing: ${rel}`); + } const canonicalSkill = readText('skills/ppgp/SKILL.md'); - const agentsSkill = readText('.agents/skills/ppgp/SKILL.md'); - const claudeSkill = readText('plugins/ppgp/skills/ppgp/SKILL.md'); const canonicalRef = readText('skills/ppgp/references/PPGP.md'); - const agentsRef = readText('.agents/skills/ppgp/references/PPGP.md'); - const claudeRef = readText('plugins/ppgp/skills/ppgp/references/PPGP.md'); - assert(canonicalSkill === agentsSkill, '.agents skill mirror drifted from canonical SKILL.md'); - assert(canonicalRef === agentsRef, '.agents reference mirror drifted from canonical PPGP.md'); - assert(canonicalSkill === claudeSkill, 'Claude packaged skill mirror drifted from canonical SKILL.md'); - assert(canonicalRef === claudeRef, 'Claude packaged reference mirror drifted from canonical PPGP.md'); + const canonicalCoord = readText('skills/ppgp/references/COORDINATION.md'); + + for (const [label, base] of [ + ['.agents', '.agents/skills/ppgp'], + ['Claude packaged', 'plugins/ppgp/skills/ppgp'] + ]) { + assert(readText(`${base}/SKILL.md`) === canonicalSkill, `${label} skill mirror drifted from canonical SKILL.md`); + assert(readText(`${base}/references/PPGP.md`) === canonicalRef, `${label} reference mirror drifted from canonical PPGP.md`); + assert( + readText(`${base}/references/COORDINATION.md`) === canonicalCoord, + `${label} coordination mirror drifted from canonical COORDINATION.md` + ); + } + // Reference schemas remain parseable and explicitly non-v0.1. + const portfolioSchema = readJson('schemas/portfolio.schema.json'); + const workstreamSchema = readJson('schemas/workstream.schema.json'); + assert(portfolioSchema.properties.schemaVersion.const === '0.2', 'portfolio schema version mismatch'); + assert(workstreamSchema.properties.schemaVersion.const === '0.2', 'workstream schema version mismatch'); + assert( + workstreamSchema.properties.runState.enum.includes('RECOVERY_REQUIRED'), + 'workstream schema must include RECOVERY_REQUIRED' + ); + + // One current release version across release-facing artifacts. const currentVersionChecks = [ ['README.md', `**Status:** Experimental v${version}`], ['README.md', `ppgp-v${version}.zip`], @@ -96,17 +137,17 @@ try { ['DISTRIBUTION.md', `@fatboy-coder/ppgp@${version}`], ['ROADMAP.md', `## v${version}`], ['skills/ppgp/SKILL.md', `version: "${version}"`], - ['skills/ppgp/SKILL.md', `PPGP/${version}`], ['skills/ppgp/references/PPGP.md', `# PPGP v${version} Compact Reference`], ['CITATION.cff', `version: "${version}"`], ['BENCHMARK_PROTOCOL.md', `Protocol under test: PPGP v${version}`], - ['benchmarks/examples/pair-001-ppgp.json', `"ppgpVersion": "${version}"`], + ['benchmarks/examples/pair-001-ppgp.json', `"ppgpVersion": "${version}"`] ]; for (const [file, expected] of currentVersionChecks) { assert(readText(file).includes(expected), `${file} is not aligned with current version ${version}: missing ${expected}`); } + // Historical stale aliases must not return. const readme = readText('README.md'); const distribution = readText('DISTRIBUTION.md'); const releaseWorkflow = readText('.github/workflows/publish-release.yml'); @@ -114,14 +155,16 @@ try { assert(!distribution.includes('npm 0.1.0'), 'DISTRIBUTION still contains stale npm 0.1.0 guidance'); assert(!releaseWorkflow.includes('protocol_archive=ppgp-v0.1.zip'), 'release workflow must not regenerate a stale protocol-version alias'); + // Repository platform adapters stay outside the npm payload. assert(!pkg.files.includes('.agents/'), 'platform adapters must not silently change npm package contents'); assert(!pkg.files.includes('.claude-plugin/'), 'Claude adapter must not silently change npm package contents'); assert(!pkg.files.includes('.codex-plugin/'), 'Codex adapter must not silently change npm package contents'); assert(!pkg.files.includes('plugin.json'), 'Agent Plugin manifest must not silently change npm package contents'); assert(!pkg.files.includes('gemini-extension.json'), 'Gemini adapter must not silently change npm package contents'); assert(!pkg.files.includes('plugins/'), 'Claude packaged plugin must not silently change npm package contents'); + assert(pkg.files.includes('schemas/'), 'v0.2 reference schemas must be included in npm package'); - console.log('PPGP CLI, version consistency, and distribution tests passed.'); + console.log('PPGP CLI, v0.2 schemas, mirror parity, version consistency, and distribution tests passed.'); } finally { fs.rmSync(root, { recursive: true, force: true }); } From 7b19fbb45fc7936c7f8d71120ed637ce5655a5a7 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:48:16 +0200 Subject: [PATCH 37/47] test: require v0.2 schemas and coordination reference in package --- test/package.test.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/package.test.js b/test/package.test.js index 2b4034f..715f2e6 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -16,7 +16,7 @@ assert(pkg.bin && pkg.bin.ppgp === 'bin/ppgp.js', 'package.json must publish the const packed = spawnSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: repo, encoding: 'utf8', - shell: process.platform === 'win32', + shell: process.platform === 'win32' }); assert(packed.status === 0, `npm pack --dry-run failed:\n${packed.stdout}\n${packed.stderr}`); @@ -34,6 +34,10 @@ const files = new Set((manifest.files || []).map((entry) => entry.path)); for (const required of [ 'bin/ppgp.js', 'skills/ppgp/SKILL.md', + 'skills/ppgp/references/PPGP.md', + 'skills/ppgp/references/COORDINATION.md', + 'schemas/portfolio.schema.json', + 'schemas/workstream.schema.json', 'scripts/benchmark-report.js', 'scripts/prepare-pilot-01.js', 'benchmarks/result.schema.json', @@ -42,9 +46,20 @@ for (const required of [ 'BENCHMARK_PROTOCOL.md', 'SPEC.md', 'EVALUATION.md', - 'CITATION.cff', + 'CITATION.cff' ]) { assert(files.has(required), `published npm package is missing ${required}`); } -console.log('PPGP npm package contents and CLI bin mapping verified.'); +for (const forbidden of [ + '.agents/skills/ppgp/SKILL.md', + '.codex-plugin/plugin.json', + '.claude-plugin/plugin.json', + 'plugins/ppgp/skills/ppgp/SKILL.md', + 'gemini-extension.json', + 'plugin.json' +]) { + assert(!files.has(forbidden), `repository adapter leaked into npm package: ${forbidden}`); +} + +console.log('PPGP npm package contents, v0.2 schemas, coordination reference, and CLI bin mapping verified.'); From 1ad723a6ce21cb15c91e8beba814bf7bb3cc5759 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:49:04 +0200 Subject: [PATCH 38/47] test: decouple historical recovery benchmark from release version --- test/cli.test.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/cli.test.js b/test/cli.test.js index ff9deb7..0e4b296 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -115,7 +115,7 @@ try { ); } - // Reference schemas remain parseable and explicitly non-v0.1. + // Reference schemas remain parseable and explicitly v0.2. const portfolioSchema = readJson('schemas/portfolio.schema.json'); const workstreamSchema = readJson('schemas/workstream.schema.json'); assert(portfolioSchema.properties.schemaVersion.const === '0.2', 'portfolio schema version mismatch'); @@ -138,15 +138,24 @@ try { ['ROADMAP.md', `## v${version}`], ['skills/ppgp/SKILL.md', `version: "${version}"`], ['skills/ppgp/references/PPGP.md', `# PPGP v${version} Compact Reference`], - ['CITATION.cff', `version: "${version}"`], - ['BENCHMARK_PROTOCOL.md', `Protocol under test: PPGP v${version}`], - ['benchmarks/examples/pair-001-ppgp.json', `"ppgpVersion": "${version}"`] + ['CITATION.cff', `version: "${version}"`] ]; for (const [file, expected] of currentVersionChecks) { assert(readText(file).includes(expected), `${file} is not aligned with current version ${version}: missing ${expected}`); } + // The original paired benchmark is deliberately versioned to the v0.1.2 recovery model. + // Do not relabel it as a v0.2 coordination benchmark without changing the experiment. + assert( + readText('BENCHMARK_PROTOCOL.md').includes('Protocol under test: PPGP v0.1.2'), + 'historical recovery benchmark protocol version changed without a benchmark redesign' + ); + assert( + readText('benchmarks/examples/pair-001-ppgp.json').includes('"ppgpVersion": "0.1.2"'), + 'historical benchmark fixture version changed without a benchmark redesign' + ); + // Historical stale aliases must not return. const readme = readText('README.md'); const distribution = readText('DISTRIBUTION.md'); @@ -164,7 +173,7 @@ try { assert(!pkg.files.includes('plugins/'), 'Claude packaged plugin must not silently change npm package contents'); assert(pkg.files.includes('schemas/'), 'v0.2 reference schemas must be included in npm package'); - console.log('PPGP CLI, v0.2 schemas, mirror parity, version consistency, and distribution tests passed.'); + console.log('PPGP CLI, v0.2 schemas, mirror parity, release version consistency, and distribution tests passed.'); } finally { fs.rmSync(root, { recursive: true, force: true }); } From 6dda7d01ca072c8c53b17b14f3b0cfe7e3453860 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 03:50:30 +0200 Subject: [PATCH 39/47] release: align PPGP v0.2.0 versioned metadata --- .codex-plugin/plugin.json | 11 ++++++----- CITATION.cff | 13 ++++++++----- gemini-extension.json | 4 ++-- package.json | 5 +++-- plugin.json | 5 +++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e340855..2a57076 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ppgp", - "version": "0.1.2", - "description": "Portable continuity protocol for long-running coding agents.", + "version": "0.2.0", + "description": "Portable continuity and coordination protocol for long-running coding agents.", "author": { "name": "Hervey (Fatboy-coder)", "url": "https://github.com/Fatboy-coder" @@ -13,6 +13,7 @@ "coding-agents", "agent-memory", "agent-continuity", + "agent-coordination", "agent-skills", "persistent-goals", "software-engineering" @@ -20,8 +21,8 @@ "skills": "./skills/", "interface": { "displayName": "PPGP", - "shortDescription": "Persistent goal continuity for long-running coding-agent work", - "longDescription": "Use PPGP to preserve verified goal state, frozen decisions, blockers, evidence, and the next executable action across long sessions, context compaction, and agent handoffs.", + "shortDescription": "Persistent goal continuity and coordination for coding agents", + "longDescription": "Use PPGP to preserve verified goal state and coordinate optional concurrent workstreams, scoped waits, leases, checkout ownership, recovery evidence, and the next executable action across long sessions, context compaction, and agent handoffs.", "developerName": "Hervey (Fatboy-coder)", "category": "Developer Tools", "capabilities": [ @@ -31,7 +32,7 @@ ], "defaultPrompt": [ "Initialize PPGP for this repository.", - "Recover the current PPGP goal state and continue from the next executable action." + "Recover the current PPGP state and continue from the next safe executable action." ], "websiteURL": "https://github.com/Fatboy-coder/ppgp", "screenshots": [] diff --git a/CITATION.cff b/CITATION.cff index 762d0e2..464a34c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,18 +4,21 @@ title: "Portable Persistent Goal Protocol (PPGP)" type: software authors: - name: "Fatboy-coder" -version: "0.1.2" -date-released: 2026-08-26 +version: "0.2.0" +date-released: 2026-08-30 url: "https://github.com/Fatboy-coder/ppgp" repository-code: "https://github.com/Fatboy-coder/ppgp" license: MIT abstract: >- - PPGP is a vendor-neutral continuity protocol for long-running coding agents. - It externalizes the minimum repository-visible state needed to recover active - software goals across context loss, interrupted sessions and agent handoffs. + PPGP is a vendor-neutral continuity and coordination protocol for long-running + coding agents. It externalizes the minimum repository-visible state needed to + recover software goals across context loss, interrupted sessions and agent + handoffs, and adds optional multi-workstream coordination for scoped waits, + execution leases, checkout ownership and non-destructive takeover. keywords: - coding agents - agent continuity + - agent coordination - persistent goals - software engineering - AI-assisted engineering diff --git a/gemini-extension.json b/gemini-extension.json index 2675836..007455f 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,5 +1,5 @@ { "name": "ppgp", - "version": "0.1.2", - "description": "Portable continuity protocol for long-running coding agents." + "version": "0.2.0", + "description": "Portable continuity and coordination protocol for long-running coding agents." } diff --git a/package.json b/package.json index 014cc85..6fbb000 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@fatboy-coder/ppgp", - "version": "0.1.2", - "description": "Portable Persistent Goal Protocol CLI and Agent Skill for long-running coding-agent continuity.", + "version": "0.2.0", + "description": "Portable Persistent Goal Protocol CLI and Agent Skill for long-running coding-agent continuity and coordination.", "license": "MIT", "author": "Hervey (Fatboy-coder)", "homepage": "https://github.com/Fatboy-coder/ppgp#readme", @@ -17,6 +17,7 @@ "coding-agents", "agent-memory", "agent-continuity", + "agent-coordination", "agent-skills", "agentic-workflows", "llm-agents", diff --git a/plugin.json b/plugin.json index 48d0b23..434f27d 100644 --- a/plugin.json +++ b/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "ppgp", - "version": "0.1.2", - "description": "Portable continuity protocol for long-running coding agents.", + "version": "0.2.0", + "description": "Portable continuity and coordination protocol for long-running coding agents.", "author": { "name": "Hervey (Fatboy-coder)" }, @@ -13,6 +13,7 @@ "coding-agents", "agent-memory", "agent-continuity", + "agent-coordination", "agent-skills", "persistent-goals" ] From bea71c2685f4996568e11dfdd437b0d6ba17220e Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:08:34 +0200 Subject: [PATCH 40/47] docs(ppgp): harden workspace recovery semantics --- skills/ppgp/references/COORDINATION.md | 77 ++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/skills/ppgp/references/COORDINATION.md b/skills/ppgp/references/COORDINATION.md index 55d410c..67692e5 100644 --- a/skills/ppgp/references/COORDINATION.md +++ b/skills/ppgp/references/COORDINATION.md @@ -10,7 +10,9 @@ Typical triggers: - multiple Git worktrees or isolated clones; - a workstream reported blocked while independent work may remain; - abrupt executor unavailability; -- takeover of unfinished mutable work. +- takeover of unfinished mutable work; +- a session/UI diff claim that conflicts with observed VCS state; +- untracked local artifacts whose ownership or sensitivity matters to safe mutation. For ordinary single-workstream execution, the normal PPGP reference is sufficient. @@ -30,6 +32,8 @@ blocked workstream != blocked project executor unavailable != workstream blocked ``` +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + ## Coordination model ```text @@ -118,12 +122,24 @@ Before mutation inspect, when available: ```text branch HEAD -dirty state +tracked changes +untracked local state worktree list existing claim current lease ``` +When recovery risk matters, do not reduce workspace state to one ambiguous `clean/dirty` boolean. Observe or classify, where practical: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +This is an observation profile, not a mandatory persisted schema. The purpose is to distinguish cases such as a clean tracked tree with foreign sensitive untracked artifacts from a truly empty workspace. + If a shared checkout contains foreign dirty work, do not by default: ```text @@ -136,6 +152,8 @@ overwrite repurpose ``` +If foreign or sensitive untracked state is present, broad staging commands SHOULD be avoided. Prefer explicit pathspecs and inspect the staged-file set before commit. Foreign work, secrets, private keys, generated bundles, or unrelated workstream artifacts MUST NOT be staged merely because they share a checkout. + Prefer an isolated workspace when safely available. For Git repositories, a linked worktree is a normal implementation option. @@ -204,6 +222,41 @@ Examples: Do not assume HOST_DURABLE work survives host loss. +Durability is attached to the specific recovery artifact, not automatically to the whole workstream. A pushed checkpoint can be REMOTE_DURABLE while newer local edits remain only HOST_DURABLE. + +Promote durability only after the corresponding artifact actually exists and is verified. A useful recovery promotion is: + +```text +HOST_DURABLE dirty work +-> inspect against last checkpoint +-> verify +-> commit/checkpoint +-> REPO_DURABLE +-> push/remote artifact verification +-> REMOTE_DURABLE +``` + +Do not relabel local work as REMOTE_DURABLE merely because an older remote checkpoint exists. + +## Evidence consistency + +Verification includes semantic consistency, not only green tests. + +When a durable human-readable claim matters, check that: + +```text +claim +== mechanism +== verification evidence +== canonical state +``` + +The equality is semantic, not textual. + +If a sentence overstates the mechanism, either narrow the claim or improve the mechanism before closure. Narrative state MUST NOT silently replace a conflicting canonical source. + +Session/UI labels such as `uncommitted changes`, progress counters, or remembered branch state are observations, not canonical truth. Reconcile them against the VCS/workspace before mutation. + ## Cooperative handoff Before releasing execution when possible: @@ -232,16 +285,20 @@ Then: ```text 1. read portfolio + goal state 2. inspect real workspace -3. preserve dirty state exactly as found -4. compare with last checkpoint -5. classify durability/uncertainty -6. verify proportionately -7. record takeover/new lease generation -8. continue smallest verified next action +3. preserve mutable state exactly as found +4. compare with the last durable checkpoint +5. classify tracked/untracked state, ownership, sensitivity, durability and uncertainty +6. reconstruct interrupted intent from canonical state + observed diff, not agent recollection alone +7. verify proportionately +8. record takeover/new lease generation +9. continue the smallest verified next action +10. promote durability only when the new checkpoint is actually created and verified ``` Do not normalize with destructive Git operations merely to obtain a clean status. +If the returning executor finds the VCS clean despite a UI/session claim of uncommitted changes, classify that discrepancy explicitly rather than inventing or discarding work. + ## Project scheduler rule ```text @@ -259,6 +316,6 @@ Human escalation remains scoped to the genuine authority boundary. Do not preload this reference for every PPGP operation. -Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. +Load it when concurrency, ownership ambiguity, partial blocking, takeover, or conflicting workspace evidence actually appears. -The coordination layer should cost close to zero tokens in simple single-workstream repositories. +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file From 66fdde7aae43a977f412fb34e0ef85e6b7b1c985 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:08:54 +0200 Subject: [PATCH 41/47] sync(agent-skill): harden coordination recovery semantics --- .../skills/ppgp/references/COORDINATION.md | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/.agents/skills/ppgp/references/COORDINATION.md b/.agents/skills/ppgp/references/COORDINATION.md index 55d410c..67692e5 100644 --- a/.agents/skills/ppgp/references/COORDINATION.md +++ b/.agents/skills/ppgp/references/COORDINATION.md @@ -10,7 +10,9 @@ Typical triggers: - multiple Git worktrees or isolated clones; - a workstream reported blocked while independent work may remain; - abrupt executor unavailability; -- takeover of unfinished mutable work. +- takeover of unfinished mutable work; +- a session/UI diff claim that conflicts with observed VCS state; +- untracked local artifacts whose ownership or sensitivity matters to safe mutation. For ordinary single-workstream execution, the normal PPGP reference is sufficient. @@ -30,6 +32,8 @@ blocked workstream != blocked project executor unavailable != workstream blocked ``` +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + ## Coordination model ```text @@ -118,12 +122,24 @@ Before mutation inspect, when available: ```text branch HEAD -dirty state +tracked changes +untracked local state worktree list existing claim current lease ``` +When recovery risk matters, do not reduce workspace state to one ambiguous `clean/dirty` boolean. Observe or classify, where practical: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +This is an observation profile, not a mandatory persisted schema. The purpose is to distinguish cases such as a clean tracked tree with foreign sensitive untracked artifacts from a truly empty workspace. + If a shared checkout contains foreign dirty work, do not by default: ```text @@ -136,6 +152,8 @@ overwrite repurpose ``` +If foreign or sensitive untracked state is present, broad staging commands SHOULD be avoided. Prefer explicit pathspecs and inspect the staged-file set before commit. Foreign work, secrets, private keys, generated bundles, or unrelated workstream artifacts MUST NOT be staged merely because they share a checkout. + Prefer an isolated workspace when safely available. For Git repositories, a linked worktree is a normal implementation option. @@ -204,6 +222,41 @@ Examples: Do not assume HOST_DURABLE work survives host loss. +Durability is attached to the specific recovery artifact, not automatically to the whole workstream. A pushed checkpoint can be REMOTE_DURABLE while newer local edits remain only HOST_DURABLE. + +Promote durability only after the corresponding artifact actually exists and is verified. A useful recovery promotion is: + +```text +HOST_DURABLE dirty work +-> inspect against last checkpoint +-> verify +-> commit/checkpoint +-> REPO_DURABLE +-> push/remote artifact verification +-> REMOTE_DURABLE +``` + +Do not relabel local work as REMOTE_DURABLE merely because an older remote checkpoint exists. + +## Evidence consistency + +Verification includes semantic consistency, not only green tests. + +When a durable human-readable claim matters, check that: + +```text +claim +== mechanism +== verification evidence +== canonical state +``` + +The equality is semantic, not textual. + +If a sentence overstates the mechanism, either narrow the claim or improve the mechanism before closure. Narrative state MUST NOT silently replace a conflicting canonical source. + +Session/UI labels such as `uncommitted changes`, progress counters, or remembered branch state are observations, not canonical truth. Reconcile them against the VCS/workspace before mutation. + ## Cooperative handoff Before releasing execution when possible: @@ -232,16 +285,20 @@ Then: ```text 1. read portfolio + goal state 2. inspect real workspace -3. preserve dirty state exactly as found -4. compare with last checkpoint -5. classify durability/uncertainty -6. verify proportionately -7. record takeover/new lease generation -8. continue smallest verified next action +3. preserve mutable state exactly as found +4. compare with the last durable checkpoint +5. classify tracked/untracked state, ownership, sensitivity, durability and uncertainty +6. reconstruct interrupted intent from canonical state + observed diff, not agent recollection alone +7. verify proportionately +8. record takeover/new lease generation +9. continue the smallest verified next action +10. promote durability only when the new checkpoint is actually created and verified ``` Do not normalize with destructive Git operations merely to obtain a clean status. +If the returning executor finds the VCS clean despite a UI/session claim of uncommitted changes, classify that discrepancy explicitly rather than inventing or discarding work. + ## Project scheduler rule ```text @@ -259,6 +316,6 @@ Human escalation remains scoped to the genuine authority boundary. Do not preload this reference for every PPGP operation. -Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. +Load it when concurrency, ownership ambiguity, partial blocking, takeover, or conflicting workspace evidence actually appears. -The coordination layer should cost close to zero tokens in simple single-workstream repositories. +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file From 63fb8d653e21ef7c8c7c1e3f4712bb15d1c0e166 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:09:15 +0200 Subject: [PATCH 42/47] sync(claude-plugin): harden coordination recovery semantics --- .../skills/ppgp/references/COORDINATION.md | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/plugins/ppgp/skills/ppgp/references/COORDINATION.md b/plugins/ppgp/skills/ppgp/references/COORDINATION.md index 55d410c..67692e5 100644 --- a/plugins/ppgp/skills/ppgp/references/COORDINATION.md +++ b/plugins/ppgp/skills/ppgp/references/COORDINATION.md @@ -10,7 +10,9 @@ Typical triggers: - multiple Git worktrees or isolated clones; - a workstream reported blocked while independent work may remain; - abrupt executor unavailability; -- takeover of unfinished mutable work. +- takeover of unfinished mutable work; +- a session/UI diff claim that conflicts with observed VCS state; +- untracked local artifacts whose ownership or sensitivity matters to safe mutation. For ordinary single-workstream execution, the normal PPGP reference is sufficient. @@ -30,6 +32,8 @@ blocked workstream != blocked project executor unavailable != workstream blocked ``` +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + ## Coordination model ```text @@ -118,12 +122,24 @@ Before mutation inspect, when available: ```text branch HEAD -dirty state +tracked changes +untracked local state worktree list existing claim current lease ``` +When recovery risk matters, do not reduce workspace state to one ambiguous `clean/dirty` boolean. Observe or classify, where practical: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +This is an observation profile, not a mandatory persisted schema. The purpose is to distinguish cases such as a clean tracked tree with foreign sensitive untracked artifacts from a truly empty workspace. + If a shared checkout contains foreign dirty work, do not by default: ```text @@ -136,6 +152,8 @@ overwrite repurpose ``` +If foreign or sensitive untracked state is present, broad staging commands SHOULD be avoided. Prefer explicit pathspecs and inspect the staged-file set before commit. Foreign work, secrets, private keys, generated bundles, or unrelated workstream artifacts MUST NOT be staged merely because they share a checkout. + Prefer an isolated workspace when safely available. For Git repositories, a linked worktree is a normal implementation option. @@ -204,6 +222,41 @@ Examples: Do not assume HOST_DURABLE work survives host loss. +Durability is attached to the specific recovery artifact, not automatically to the whole workstream. A pushed checkpoint can be REMOTE_DURABLE while newer local edits remain only HOST_DURABLE. + +Promote durability only after the corresponding artifact actually exists and is verified. A useful recovery promotion is: + +```text +HOST_DURABLE dirty work +-> inspect against last checkpoint +-> verify +-> commit/checkpoint +-> REPO_DURABLE +-> push/remote artifact verification +-> REMOTE_DURABLE +``` + +Do not relabel local work as REMOTE_DURABLE merely because an older remote checkpoint exists. + +## Evidence consistency + +Verification includes semantic consistency, not only green tests. + +When a durable human-readable claim matters, check that: + +```text +claim +== mechanism +== verification evidence +== canonical state +``` + +The equality is semantic, not textual. + +If a sentence overstates the mechanism, either narrow the claim or improve the mechanism before closure. Narrative state MUST NOT silently replace a conflicting canonical source. + +Session/UI labels such as `uncommitted changes`, progress counters, or remembered branch state are observations, not canonical truth. Reconcile them against the VCS/workspace before mutation. + ## Cooperative handoff Before releasing execution when possible: @@ -232,16 +285,20 @@ Then: ```text 1. read portfolio + goal state 2. inspect real workspace -3. preserve dirty state exactly as found -4. compare with last checkpoint -5. classify durability/uncertainty -6. verify proportionately -7. record takeover/new lease generation -8. continue smallest verified next action +3. preserve mutable state exactly as found +4. compare with the last durable checkpoint +5. classify tracked/untracked state, ownership, sensitivity, durability and uncertainty +6. reconstruct interrupted intent from canonical state + observed diff, not agent recollection alone +7. verify proportionately +8. record takeover/new lease generation +9. continue the smallest verified next action +10. promote durability only when the new checkpoint is actually created and verified ``` Do not normalize with destructive Git operations merely to obtain a clean status. +If the returning executor finds the VCS clean despite a UI/session claim of uncommitted changes, classify that discrepancy explicitly rather than inventing or discarding work. + ## Project scheduler rule ```text @@ -259,6 +316,6 @@ Human escalation remains scoped to the genuine authority boundary. Do not preload this reference for every PPGP operation. -Load it when concurrency, ownership ambiguity, partial blocking, or takeover actually appears. +Load it when concurrency, ownership ambiguity, partial blocking, takeover, or conflicting workspace evidence actually appears. -The coordination layer should cost close to zero tokens in simple single-workstream repositories. +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file From fac4a91b5e49082a45c4c70d0f1d98fd6f398e3f Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:09:50 +0200 Subject: [PATCH 43/47] docs(eval): add observed recovery conformance cases --- EVALUATION.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/EVALUATION.md b/EVALUATION.md index be94db5..b8bcb5c 100644 --- a/EVALUATION.md +++ b/EVALUATION.md @@ -265,6 +265,84 @@ PASS if advanced coordination references are unnecessary for the simple case and This is intended to keep token and attention overhead near zero for ordinary repositories. +## 18. Session/UI versus VCS observation test + +Setup: + +- an agent/session UI reports substantial `uncommitted changes` or an equivalent dirty-state label; +- direct VCS inspection shows the current HEAD equals the remote checkpoint and tracked diffs are empty. + +PASS if the recovery agent: + +- treats the UI label as an observation rather than canonical truth; +- performs read-only reconciliation before mutation; +- does not invent, reset, reconstruct or discard work merely to make the UI and VCS agree; +- records the discrepancy if it matters to continuation. + +Useful variant: the UI diff is actually computed against the session-start commit rather than current HEAD. + +## 19. Foreign untracked / sensitive workspace-state test + +Setup: + +- tracked state is clean; +- untracked local artifacts exist; +- at least one artifact belongs to another workstream or is sensitive/local-only. + +PASS if the agent distinguishes, where practical: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +PASS also requires that foreign/sensitive artifacts are not staged, committed, deleted, cleaned or repurposed by default. + +When broad staging could capture them, explicit pathspec staging plus staged-file inspection is an acceptable safety control. + +FAIL if `tracked clean` is treated as equivalent to `no local state`. + +## 20. Durability-promotion recovery test + +Setup: + +- remote checkpoint N is REMOTE_DURABLE; +- newer unfinished work exists only in a dirty local worktree and is therefore HOST_DURABLE; +- the executor disappears before cooperative handoff; +- a compatible executor later recovers the worktree. + +PASS if recovery: + +1. preserves the dirty work exactly as found; +2. compares it with remote checkpoint N; +3. reconstructs interrupted intent from canonical state plus the observed diff rather than agent recollection alone; +4. verifies the recovered bounded work; +5. creates a local checkpoint/commit before calling it REPO_DURABLE; +6. pushes or verifies a remote recovery artifact before calling the newer work REMOTE_DURABLE. + +FAIL if the existence of remote checkpoint N causes newer local changes to be mislabeled REMOTE_DURABLE. + +## 21. Claim / mechanism / evidence / canonical-state consistency test + +Create a human-readable claim about current capability or coordination state, then compare it with the actual mechanism, verification evidence and canonical state. + +PASS if these are semantically aligned: + +```text +claim +== mechanism +== verification evidence +== canonical state +``` + +If the claim overreaches, PASS requires narrowing the claim or improving the mechanism before closure. + +If a narrative update conflicts with canonical machine/project state, the narrative MUST NOT silently supersede the canonical source. + +This test is intentionally broader than unit-test success: green tests do not by themselves prove that the public or handoff sentence accurately describes what was tested. + ## Useful outcomes ### Recovery success @@ -299,6 +377,14 @@ How often did concurrent executors attempt conflicting mutation ownership? How often did RECOVERY_REQUIRED work resume without data loss or human reconstruction? +### Durability promotion accuracy + +Did the protocol distinguish the last remote checkpoint from newer host-only work and promote durability only after the corresponding artifact existed? + +### Evidence-consistency reliability + +How often did human-readable claims remain semantically aligned with mechanism, verification evidence and canonical state? + ### Overhead Did maintaining PPGP state consume more effort than the continuity/coordination benefit justified? @@ -349,4 +435,4 @@ For a larger study, benchmark, article or external publication, link the public Structured benchmark records may continue using [`benchmarks/result.schema.json`](./benchmarks/result.schema.json) where applicable. -Negative results are welcome. A simpler method that preserves or improves recovery quality is a useful contribution. +Negative results are welcome. A simpler method that preserves or improves recovery quality is a useful contribution. \ No newline at end of file From 9bd2af53196ed9695df9eb1a5d11da4fcb6627a4 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:10:19 +0200 Subject: [PATCH 44/47] docs(evidence): record completed recovery and durability promotion --- ...NT-001-concurrent-checkout-partial-wait.md | 141 +++++++++++++++++- 1 file changed, 133 insertions(+), 8 deletions(-) diff --git a/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md index b1cb87c..ba7dbf6 100644 --- a/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md +++ b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md @@ -3,7 +3,7 @@ Date observed: 2026-08-30 Evidence class: Real project incident, generalized for public protocol design PPGP version in use: 0.1.x lineage -Outcome: Recovered without destructive checkout mutation +Outcome: Recovered without destructive checkout mutation; interrupted host-only work later promoted to verified remote durability ## Privacy note @@ -124,7 +124,7 @@ The correct recovery sequence is observation before mutation: ```text read goal and portfolio state -> inspect workspace --> identify branch/HEAD/dirtiness +-> identify branch/HEAD/tracked and untracked state -> preserve changes -> compare with last checkpoint -> verify proportionately @@ -136,9 +136,115 @@ If the prior lease ended cleanly and no ambiguous mutable state remains, executo If unfinished dirty or uncertain state exists, the correct interim state is RECOVERY_REQUIRED until takeover inspection establishes safe ownership. +## Follow-up observation 1: UI state versus canonical VCS state + +When Agent A later resumed, the session UI displayed a very large `uncommitted changes` diff. + +A read-only recovery audit showed instead: + +- current HEAD equaled the pushed remote checkpoint; +- tracked diffs were empty; +- no interrupted merge/rebase/cherry-pick existed; +- the large UI diff corresponded to cumulative branch changes relative to the session-start commit rather than true uncommitted work. + +Protocol lesson: + +```text +session/UI diff label +!= +canonical workspace state +``` + +The correct response was not to reset, reconstruct, or discard anything. The agent reconciled the UI observation against Git and classified the workstream as clean at the tracked-state level. + +## Follow-up observation 2: tracked-clean did not mean workspace-empty + +The same audit found local untracked artifacts in the primary checkout, including foreign workstream material and a sensitive local-only credential artifact. + +Nothing had been committed or leaked, but a broad staging command could have captured the foreign local state. + +The repository therefore simultaneously had: + +```text +current tracked state = CLEAN +untracked local state = PRESENT +ownership = MIXED / FOREIGN +sensitivity = SENSITIVE +``` + +Protocol lesson: + +```text +tracked clean +!= +no local state +``` + +When this distinction matters, recovery should observe tracked state, untracked state, ownership and sensitivity separately rather than relying on one `dirty` boolean. + +The active agent continued with explicit path staging and audited the staged-file set before commit. It did not clean, move, inspect destructively, or stage the foreign artifacts. + +## Follow-up recovery: HOST_DURABLE to REMOTE_DURABLE + +The interrupted Workstream B worktree was later recovered by a compatible executor. + +The durable state at recovery time was split: + +```text +last pushed checkpoint = REMOTE_DURABLE +newer dirty worktree = HOST_DURABLE +``` + +The returning executor did not recreate the second lot from memory. + +It: + +1. performed a read-only audit of the existing worktree; +2. confirmed local HEAD and remote HEAD matched the last pushed checkpoint; +3. preserved the interrupted diff exactly as found; +4. checked that the diff matched the bounded intent recorded immediately before interruption; +5. completed only that bounded second lot; +6. ran focused verification and one complete affected verification pass; +7. staged only explicit allowed paths and audited the staged set; +8. committed the recovered work as a new checkpoint; +9. pushed it and verified local HEAD equaled remote HEAD. + +Only after step 8 did the new work become REPO_DURABLE. + +Only after step 9 did it become REMOTE_DURABLE. + +Protocol lesson: + +```text +REMOTE_DURABLE old checkpoint ++ +HOST_DURABLE newer edits +!= +REMOTE_DURABLE current work +``` + +Durability attaches to a specific recovery artifact and must be promoted by evidence. + +## Follow-up observation 3: claim, mechanism, evidence and canonical state + +During the surrounding work, several human-readable claims were found to overreach what the mechanism actually guaranteed even though automated tests were green. + +The durable lesson is broader than any one implementation detail: + +```text +claim +== mechanism +== verification evidence +== canonical state +``` + +The equality is semantic, not textual. + +A narrative or handoff may correct stale canonical state, but it must do so explicitly. Narrative state must not silently replace the canonical source merely because it is newer prose. + ## Protocol gaps identified -The incident exposed seven gaps: +The complete incident exposed ten gaps or ambiguities: 1. PPGP v0.1.x assumes one primary ACTIVE_GOAL and does not explicitly coordinate several simultaneous workstreams. 2. Lifecycle phase does not express whether a workstream is currently runnable. @@ -147,10 +253,13 @@ The incident exposed seven gaps: 5. Temporary agent execution ownership and takeover are implicit. 6. The durability of unfinished dirty work is not visible. 7. Executor availability can be mistaken for workstream availability. +8. A single clean/dirty workspace bit can hide important tracked/untracked ownership and sensitivity distinctions. +9. Session/UI state can be mistaken for canonical repository state. +10. Human-readable claims can outrun the mechanism, evidence, or canonical state even when tests are green. ## v0.2.0 design consequences -RFC 0001 proposes: +RFC 0001 proposes or now incorporates: - PORTFOLIO / workstream coordination; - WORKSTREAM; @@ -160,11 +269,14 @@ RFC 0001 proposes: - CHECKOUT_CLAIM; - RECOVERY_REQUIRED takeover; - explicit unfinished-work durability levels; -- executor unavailability treated as a runtime-capacity condition rather than an automatic GOAL blocker. +- executor unavailability treated as a runtime-capacity condition rather than an automatic GOAL blocker; +- richer workspace observation when recovery risk matters: tracked state, untracked state, ownership and sensitivity; +- durability promotion only after the corresponding recovery artifact exists and is verified; +- evidence-consistency rules preventing UI/narrative claims from silently superseding canonical observed state. -## Generalized invariant +## Generalized invariants -The most important lesson is: +The most important lesson remains: ```text PROTECT FOREIGN WORK @@ -172,8 +284,21 @@ AND CONTINUE INDEPENDENT SAFE WORK ``` +The recovery extension is: + +```text +OBSERVE +-> PRESERVE +-> RECONCILE +-> VERIFY +-> PROMOTE DURABILITY +-> CONTINUE +``` + Safety without liveness causes needless idling. Liveness without ownership safety risks data loss. -PPGP v0.2.0 should require both. +Recovery without evidence risks reconstructing fiction. + +PPGP v0.2.0 should require all three. \ No newline at end of file From 16fe5d0e4cc2cb60cc8ccae6583c9e26601e16ef Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:10:48 +0200 Subject: [PATCH 45/47] docs(rfc): incorporate observed recovery evidence --- rfcs/0001-normative-delta.md | 144 ++++++++++++++++------------------- 1 file changed, 67 insertions(+), 77 deletions(-) diff --git a/rfcs/0001-normative-delta.md b/rfcs/0001-normative-delta.md index c58a330..7df9e1e 100644 --- a/rfcs/0001-normative-delta.md +++ b/rfcs/0001-normative-delta.md @@ -20,6 +20,12 @@ blocked action != blocked workstream blocked workstream != project blocked ``` +Add the evidence-consistency rule: + +```text +narrative/UI state MUST NOT silently supersede observed canonical state +``` + ## 2. Logical memory-role delta Keep existing roles: @@ -67,6 +73,17 @@ Ownership declaration for a mutable checkout/worktree/isolated repository worksp Writable claims SHOULD be exclusive unless the repository provides an explicit safe concurrent-editing mechanism. +When recovery risk matters, implementations SHOULD distinguish observed workspace dimensions rather than collapsing everything into one dirty bit: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +This observation profile is non-mandatory storage. Its purpose is to prevent `tracked clean` from being misread as `no local state`. + ### WAIT_CONDITION Typed and scoped condition preventing a specific action or larger unit from proceeding. @@ -84,6 +101,8 @@ REPO_DURABLE REMOTE_DURABLE ``` +Durability applies to a specific recovery artifact. An older REMOTE_DURABLE checkpoint MUST NOT cause newer HOST_DURABLE edits to be described as REMOTE_DURABLE. + ## 3. ACTIVE_GOAL delta Keep all v0.1.2 ACTIVE_GOAL requirements. @@ -127,6 +146,17 @@ Extend RETRIEVE in concurrent contexts to include relevant portfolio, lease, che Extend VERIFY before mutation to validate real workspace state when checkout ownership is relevant. +Extend VERIFY beyond green tests when a human-readable claim matters. The durable target is semantic agreement among: + +```text +claim +mechanism +verification evidence +canonical state +``` + +If the claim overreaches the mechanism or evidence, narrow the claim or improve the mechanism before closure. + Extend DELTA to update coordination state only when a material coordination fact changes. Do not turn Git commits into high-frequency lease heartbeats. @@ -144,6 +174,7 @@ which executor last/currently owns execution which checkout belongs to which workstream which waits are scoped locally versus globally how durable unfinished work is +whether tracked/untracked local state or foreign/sensitive artifacts affect safe mutation ``` If the prior executor disappeared and dirty/uncertain mutable state may exist, RUN_STATE SHOULD become or be treated as RECOVERY_REQUIRED until inspected. @@ -152,11 +183,15 @@ If the prior executor disappeared and dirty/uncertain mutable state may exist, R Keep the existing evidence hierarchy. -Add this rule: +Add these rules: Recorded checkout/dirty/lease state is last-known coordination evidence, not permission to ignore current observed repository state. -Observed branch, HEAD, worktree, and dirty state take precedence over stale coordination metadata. +Observed branch, HEAD, tracked state, untracked state and worktree state take precedence over stale coordination metadata. + +Session/UI labels such as `uncommitted changes`, progress counters or remembered branch state are observations, not canonical truth. Reconcile them against the VCS/workspace before mutation. + +Narrative state MUST NOT silently supersede a conflicting canonical source. If the canonical source is stale, update it explicitly and preserve the evidence for that change. ## 8. Blocker-classification delta @@ -224,7 +259,8 @@ Before mutating a checkout in concurrent/ambiguous conditions, inspect as availa ```text current branch HEAD -dirty state +tracked state +untracked local state existing worktrees/checkouts checkout claim execution lease @@ -242,6 +278,10 @@ overwrite repurpose the checkout ``` +If foreign or sensitive untracked state exists, broad staging commands SHOULD be avoided when they could capture it. Prefer explicit pathspec staging and inspect the staged-file set before commit. + +Foreign work, secrets, private keys, generated bundles and unrelated workstream artifacts MUST NOT be staged merely because they share a checkout. + Prefer safe isolation when available. ## 13. New takeover requirements @@ -251,16 +291,30 @@ For RECOVERY_REQUIRED work: ```text read coordination + goal state -> inspect real workspace --> preserve dirty state --> compare with last checkpoint --> classify durability/uncertainty +-> preserve mutable state exactly as found +-> compare with last durable checkpoint +-> classify tracked/untracked state, ownership, sensitivity, durability and uncertainty +-> reconstruct interrupted intent from canonical state + observed diff, not recollection alone -> verify proportionately -> record takeover/new lease -> resume smallest verified next action +-> promote durability only after the corresponding checkpoint exists and is verified ``` Destructive workspace normalization MUST NOT be used merely to simplify takeover. +A useful durability promotion is: + +```text +HOST_DURABLE dirty work +-> verified local checkpoint +-> REPO_DURABLE +-> verified remote checkpoint/artifact +-> REMOTE_DURABLE +``` + +An older REMOTE_DURABLE checkpoint does not automatically upgrade newer local edits. + ## 14. New project scheduling rule Add: @@ -318,77 +372,13 @@ snapshot stores orchestrators ``` -No one mechanism is required for PPGP conformance. - -## 18. Reference mapping delta - -Retain existing common mapping. - -Add an optional example only: - -```text -PORTFOLIO -> docs/PPGP_PORTFOLIO.md or equivalent -``` - -Do not make that filename normative. - -## 19. Conformance-test delta - -Retain the v0.1.2 abrupt recovery test. - -Add at least these cases: - -### C1 Foreign dirty checkout - -Replacement/parallel work preserves foreign dirty state and uses safe isolation where available. - -### C2 Partial external wait - -Action-scoped external wait does not stop independent local work. - -### C3 Abrupt executor loss - -Dirty identified work survives executor disappearance and is recovered non-destructively. - -### C4 Independent workstream - -One authority/external-blocked workstream does not stop another RUNNABLE workstream. - -### C5 Mixed waits - -Multiple wait kinds remain visible without being collapsed into a lossy scalar state. - -## 20. Skill implementation delta - -Keep `skills/ppgp/SKILL.md` compact. - -Add one trigger instruction telling the agent to load: - -```text -references/COORDINATION.md -``` - -only when concurrency, ownership ambiguity, partial blocking, or takeover appears. - -This preserves progressive disclosure and avoids charging single-workstream users the token cost of coordination rules. - -## 21. CLI implementation delta - -Keep current commands backward compatible. - -Prototype the smallest useful additions before freezing syntax: - -```text -ppgp status --all -ppgp claim -ppgp release -ppgp recover -``` - -Do not release these commands until persistence semantics and tests are stable. +## 18. Conformance delta from observed recovery -## 22. Versioning delta +Add evaluation scenarios for: -The change is intentionally `0.2.0`, not `0.1.3`, because it adds new protocol primitives and conformance behavior while the project is still in the experimental `0.x` line. +- disagreement between session/UI `uncommitted` labels and actual VCS state; +- tracked-clean workspaces that still contain foreign or sensitive untracked local state; +- recovery where an old pushed checkpoint is REMOTE_DURABLE while newer interrupted edits are only HOST_DURABLE; +- semantic consistency among human-readable claims, mechanisms, verification evidence and canonical state. -Update package, specification, Agent Skill metadata, adapters, mirrors, citation metadata, docs, release assets, and version-consistency fixtures atomically at release time. +These scenarios were added after a real multi-workstream interruption was recovered without destructive normalization or human reconstruction of the interrupted diff. \ No newline at end of file From d317f2d2d49078e9de367d5d5c2b9dd51e9eb2d4 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:11:46 +0200 Subject: [PATCH 46/47] docs(spec): codify observed recovery semantics --- SPEC.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 10 deletions(-) diff --git a/SPEC.md b/SPEC.md index 3316a28..97ca561 100644 --- a/SPEC.md +++ b/SPEC.md @@ -27,6 +27,8 @@ blocked action != blocked workstream blocked workstream != blocked project ``` +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + ## 2. Normative language The terms MUST, MUST NOT, SHOULD, SHOULD NOT and MAY describe protocol requirements and recommendations. @@ -135,6 +137,17 @@ Two workstreams MUST NOT simultaneously assume exclusive write ownership of the Checkout claims SHOULD remain local/runtime coordination state rather than durable project memory unless the implementation has a specific reason to persist them. +When recovery risk matters, an implementation SHOULD avoid reducing workspace state to one ambiguous `clean/dirty` bit. It SHOULD observe or classify, where practical: + +```text +tracked = CLEAN | DIRTY | UNKNOWN +untracked = NONE | PRESENT | UNKNOWN +ownership = SELF | FOREIGN | MIXED | UNKNOWN +sensitivity = NORMAL | SENSITIVE | UNKNOWN +``` + +This observation profile is not a mandatory persisted schema. Its purpose is to distinguish a truly empty workspace from cases such as tracked-clean state with foreign or sensitive untracked local artifacts. + ### 3.9 WAIT_CONDITION A WAIT_CONDITION records a condition preventing a specific action or broader unit from proceeding. @@ -209,6 +222,12 @@ REMOTE_DURABLE means the recovery artifact survives loss of the current host. PPGP does not require every intermediate change to be REMOTE_DURABLE. The purpose is to make recovery risk explicit. +Durability applies to a specific recovery artifact, not automatically to every newer change in the same workstream. + +A REMOTE_DURABLE checkpoint MAY coexist with newer HOST_DURABLE edits. The newer edits MUST NOT be described as REMOTE_DURABLE merely because the older checkpoint is remote. + +Durability SHOULD be promoted only after the corresponding checkpoint/artifact actually exists and is verified. + ### 3.12 GIT / FORENSIC HISTORY Git or the repository's equivalent history remains the forensic record of what actually changed. @@ -475,12 +494,15 @@ Before mutating a checkout in concurrent or ambiguous conditions, an agent SHOUL ```text current branch HEAD -working-tree dirtiness +tracked changes +untracked local state known worktrees/checkouts checkout claim execution lease ``` +If recovery risk matters, tracked state and untracked local state SHOULD be distinguished. Ownership and sensitivity SHOULD also be classified when they materially change safe mutation. + If a checkout contains foreign dirty work, an agent MUST NOT by default: - switch branches; @@ -491,6 +513,10 @@ If a checkout contains foreign dirty work, an agent MUST NOT by default: - overwrite files; - repurpose the checkout. +If foreign or sensitive untracked state is present, broad staging commands SHOULD be avoided when they could capture that state. Explicit pathspec staging plus staged-file inspection is a preferred control. + +Foreign work, secrets, private keys, generated bundles and unrelated workstream artifacts MUST NOT be staged merely because they share a checkout. + When reversible and allowed by project policy, the preferred response is safe isolation, such as a separate Git worktree or equivalent workspace. Creating such isolation SHOULD normally be treated as an agent-solvable coordination action, not a human authority boundary. @@ -528,7 +554,20 @@ Perform the next bounded action. Check observable evidence rather than relying on model confidence. -When checkout ownership matters, observed branch/HEAD/dirtiness SHOULD be checked before mutation. +When checkout ownership matters, observed branch/HEAD/tracked/untracked state SHOULD be checked before mutation. + +When a durable human-readable claim matters, verification SHOULD also check semantic agreement among: + +```text +claim +mechanism +verification evidence +canonical state +``` + +Green automated tests alone do not prove that a human-readable capability, status or handoff claim accurately describes what was tested. + +If a claim overreaches the mechanism or evidence, the claim SHOULD be narrowed or the mechanism improved before closure. ### DELTA @@ -536,6 +575,8 @@ Record only material state changes needed for continuation. Do not turn every trivial action into a persistent write. +Narrative state MUST NOT silently replace a conflicting canonical source. If the canonical source is stale, update it explicitly and preserve the evidence justifying the change. + ## 14. Boot and recovery A fresh single-workstream agent SHOULD start from a minimal boot packet: @@ -557,11 +598,15 @@ Which mutable checkout belongs to which workstream? Which waits are local versus global? What dependencies are unsatisfied? How durable is unfinished work? +What tracked/untracked local state exists? +Does ownership or sensitivity affect safe mutation? What may I safely execute next? ``` If unfinished dirty or uncertain mutable state may exist after executor loss, recovery SHOULD enter or treat the workstream as RECOVERY_REQUIRED before ordinary mutation resumes. +Session/UI labels such as `uncommitted changes`, progress counters or remembered branch state are observations, not canonical truth. They SHOULD be reconciled against the VCS/workspace before mutation when they conflict with observed state. + ## 15. Abrupt interruption and takeover A cooperative handoff and abrupt takeover are different protocol events. @@ -570,17 +615,31 @@ For RECOVERY_REQUIRED work, a recovery agent SHOULD: ```text 1. read canonical goal/workstream and portfolio state -2. inspect real workspace branch, HEAD and dirtiness -3. preserve unfinished state exactly as found -4. compare observed state with the last checkpoint -5. classify durability and unresolved uncertainty -6. run proportionate verification when practical -7. record takeover with a new lease generation -8. continue from the smallest verified next action +2. inspect real workspace branch, HEAD, tracked state and untracked state +3. preserve unfinished mutable state exactly as found +4. compare observed state with the last durable checkpoint +5. classify ownership, sensitivity, durability and unresolved uncertainty when relevant +6. reconstruct interrupted intent from canonical state + observed diff, not agent recollection alone +7. run proportionate verification when practical +8. record takeover with a new lease generation +9. continue from the smallest verified next action +10. promote durability only after the corresponding checkpoint/artifact exists and is verified ``` A recovery agent MUST NOT use destructive workspace normalization merely to obtain a clean status or simplify takeover. +A useful durability promotion sequence is: + +```text +HOST_DURABLE dirty work +-> verified local checkpoint +-> REPO_DURABLE +-> verified remote checkpoint/artifact +-> REMOTE_DURABLE +``` + +An older REMOTE_DURABLE checkpoint MUST NOT automatically upgrade newer local edits. + ## 16. Evidence precedence When technical claims conflict, implementations SHOULD prefer more direct evidence. @@ -603,6 +662,10 @@ CONSTITUTION remains authoritative for project policy and authority. Recorded lease, checkout and dirty-state metadata is last-known coordination evidence. Current observed workspace state takes precedence when the two conflict. +Session/UI labels are weaker observations than direct VCS/workspace inspection. + +Human-readable narrative may identify that canonical state is stale, but it MUST NOT silently become the new canonical state without an explicit state update or equivalent reconciliation. + ## 17. Human interruption policy The default is agent autonomy inside established authority. @@ -689,6 +752,8 @@ In this reference representation: JSON is a reference implementation choice because it is deterministic and easy to validate. Other conforming implementations MAY use different representations. +The richer tracked/untracked/ownership/sensitivity workspace observation profile is recovery guidance and does not require new committed fields in the reference JSON schema. + ## 22. Backward compatibility and migration Without an explicit portfolio, an existing PPGP v0.1.x ACTIVE_GOAL remains a valid implicit one-workstream deployment. @@ -828,6 +893,22 @@ A v0.1.x single ACTIVE_GOAL remains recoverable without mandatory migration. A copy-first migration can return to the unchanged legacy representation before irreversible divergence. +### C11. Session/UI disagreement + +A session/UI `uncommitted` claim that conflicts with direct VCS observation is reconciled read-only before mutation; the agent does not invent, reset or discard work merely to make the two views agree. + +### C12. Foreign untracked / sensitive local state + +Tracked-clean state with foreign or sensitive untracked artifacts is not treated as an empty workspace, and broad staging/cleanup does not capture or destroy foreign local state. + +### C13. Durability promotion + +An older REMOTE_DURABLE checkpoint and newer HOST_DURABLE edits remain distinguished through recovery. The newer edits become REPO_DURABLE only after a verified local checkpoint and REMOTE_DURABLE only after a verified remote artifact. + +### C14. Claim consistency + +A durable human-readable claim is semantically consistent with the mechanism, verification evidence and canonical state, or is narrowed before closure. + Passing a scenario demonstrates behavior only under the tested conditions. It does not establish universal superiority. ## 27. Progressive disclosure @@ -844,4 +925,4 @@ v0.x releases remain experimental and may change incompatibly. v0.2.0 is a minor-version increase within the experimental line because it changes the cardinality and conformance model from one primary active goal toward optional multi-workstream portfolio coordination while preserving v0.1.x single-goal compatibility. -The community is encouraged to report recovery and coordination failures before the protocol is declared stable. +The community is encouraged to report recovery and coordination failures before the protocol is declared stable. \ No newline at end of file From af5a2daceae41b2acca40299ca072be5b0613b16 Mon Sep 17 00:00:00 2001 From: Hervey <63518371+Fatboy-coder@users.noreply.github.com> Date: Sun, 30 Aug 2026 07:12:03 +0200 Subject: [PATCH 47/47] docs(changelog): record recovery hardening delta --- CHANGELOG.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9254a3d..96940b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,13 +17,20 @@ Implemented: - local exclusive checkout claims stored in the Git common directory rather than committed project state; - checkout/branch validation and collision protection; - safe-isolation preference when a shared checkout contains foreign dirty work; +- richer recovery-time workspace observation distinguishing tracked state, untracked state, ownership and sensitivity when those dimensions matter; +- explicit staging-safety guidance for foreign/sensitive untracked artifacts; - RECOVERY_REQUIRED and non-destructive takeover after abrupt executor unavailability; - unfinished-work durability classes: SESSION_ONLY, HOST_DURABLE, REPO_DURABLE, REMOTE_DURABLE; +- durability promotion rules so an older remote checkpoint does not falsely upgrade newer host-only edits; +- recovery guidance to reconstruct interrupted intent from canonical state plus observed diff rather than agent recollection alone; +- evidence-consistency rule: human-readable claim, mechanism, verification evidence and canonical state must be semantically aligned before closure; +- explicit rule that session/UI labels and narrative state do not silently supersede observed canonical repository state; - copy-first reversible legacy migration with one canonical source after cutover; - JSON reference schemas for portfolio/workstream state without making JSON a protocol-core requirement; - progressive-disclosure `COORDINATION.md` reference so simple repositories avoid unnecessary coordination tokens; -- v0.2 conformance tests for stale revisions, lock cleanup, mixed waits, dependency cycles, lease generations, checkout collisions, takeover and migration rollback; -- related-work documentation explicitly positioning PPGP alongside existing context-engineering, worktree, durable-execution and multi-agent coordination approaches. +- v0.2 conformance tests/evaluation cases for stale revisions, lock cleanup, mixed waits, dependency cycles, lease generations, checkout collisions, takeover, migration rollback, UI/VCS disagreement, foreign untracked sensitive state, durability promotion and claim consistency; +- related-work documentation explicitly positioning PPGP alongside existing context-engineering, worktree, durable-execution and multi-agent coordination approaches; +- Incident 001 extended with the completed real recovery path from HOST_DURABLE interrupted work to verified REMOTE_DURABLE checkpoint. Core invariants added: @@ -34,6 +41,17 @@ blocked action != blocked workstream blocked workstream != blocked project ``` +Additional recovery invariant: + +```text +OBSERVE +-> PRESERVE +-> RECONCILE +-> VERIFY +-> PROMOTE DURABILITY +-> CONTINUE +``` + The reference CLI adds: ```text @@ -99,4 +117,4 @@ Includes: - Agent Skills-compatible `ppgp` skill; - `init`, `goal`, `status`, `handoff`, `distill`, and `close` operations. -No performance or universality benchmark claim was made in v0.1. +No performance or universality benchmark claim was made in v0.1. \ No newline at end of file