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 ` +## 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 ` -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= -P= -F: -D: -B: -E: -N: -``` +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. diff --git a/.agents/skills/ppgp/references/COORDINATION.md b/.agents/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..67692e5 --- /dev/null +++ b/.agents/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,321 @@ +# 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; +- 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. + +## 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 +``` + +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + +## 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 +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 +switch +reset +clean +stash foreign work +commit foreign work +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. + +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= +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. + +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: + +```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 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 +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, takeover, or conflicting workspace evidence actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file 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 ``` 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/CHANGELOG.md b/CHANGELOG.md index 6d7759b..96940b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # Changelog +## 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; +- 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/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: + +```text +PORTFOLIO != WORKSTREAM != LEASE HOLDER != CHECKOUT +executor unavailable != workstream blocked +blocked action != blocked workstream +blocked workstream != blocked project +``` + +Additional recovery invariant: + +```text +OBSERVE +-> PRESERVE +-> RECONCILE +-> VERIFY +-> PROMOTE DURABILITY +-> CONTINUE +``` + +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 Version-consistency and release-hardening patch. @@ -52,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 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/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. 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 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/ + └── / + ├── 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. diff --git a/EVALUATION.md b/EVALUATION.md index 4fd1bdb..b8bcb5c 100644 --- a/EVALUATION.md +++ b/EVALUATION.md @@ -1,165 +1,401 @@ # 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. -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. +Measure duplicated work, missed decisions, recovery latency and human reconstruction. -### Purpose +The purpose is to estimate useful checkpoint frequency without requiring persistence after every trivial action. -Test whether a substantial active goal survives an unplanned interruption before `ppgp distill` or `ppgp close` occurs. +## 4. Foreign dirty checkout test -### Procedure +Purpose: test safety and liveness simultaneously. -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. +Setup: -### PASS criteria +- Workstream A owns a mutable checkout. +- That checkout contains unfinished dirty work. +- Workstream B needs a different branch or independent work area. -The trial passes operational recovery if Agent B: +PASS if Workstream B: -- 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. +- 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 criteria +FAIL if foreign work is destroyed, silently modified, or causes unnecessary project-wide idling when reversible isolation exists. -Record a failure if Agent B: +## 5. Partial wait-scope test -- 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. +Setup: -### Important interpretation +- one action needs an external dependency or authority; +- other useful actions remain independent. -A successful abrupt recovery is evidence that the tested repository, agent and PPGP version preserved usable continuity under the tested interruption. +PASS if: -It is **not** proof that PPGP is universally effective, optimal, or superior to alternatives. +- 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. -A failed recovery is equally useful because it identifies a concrete state-quality, checkpointing or protocol weakness. +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. -## ACTIVE_GOAL checkpointing test +## 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. +``` -PPGP does not require a chronological diary. The relevant question is whether `ACTIVE_GOAL` is current enough at interruption time to support recovery. +## 7. Abrupt executor takeover test -To evaluate checkpoint quality, compare at least two interruption points: +Setup: -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. +- 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: + +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. + +PASS if B is rejected and required to reload canonical state. + +The stale write must not silently overwrite N+1. + +The reference CLI test also verifies that a rejected write does not leave a stale local mutation lock. + +## 9. Lease generation test + +Setup: + +```text +Agent A holds generation 4 +Agent B takes over -> generation 5 +Agent A later returns with generation 4 +``` -Record whether the second interruption causes duplicated work, missed decisions or human reconstruction. +PASS if generation 4 cannot be treated as current ownership. -This helps estimate the practical checkpoint frequency needed for a given agent and task type without assuming that every trivial action must be persisted. +This tests fencing semantics, not human authority. -## Distillation-independence test +## 10. Checkout collision test -To isolate the role of distillation: +Setup: -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. +- two workstreams have valid execution leases; +- both attempt exclusive mutation claims on the same mutable checkout path. -Expected interpretation: +PASS if only one workstream may own the checkout. -- `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. +Two separate worktrees may each be claimed independently. -This expectation is a protocol hypothesis to test, not a benchmark result. +Read-only inspection need not require an exclusive claim. + +## 11. Checkout/branch mismatch test + +Setup: + +- canonical workstream state declares branch A; +- claimed checkout actually points to branch B. + +PASS if `doctor`, recovery, or equivalent validation identifies the mismatch before mutation. + +## 12. Dependency DAG test + +Setup: + +```text +B requires A COMPLETED +``` + +PASS if an unfinished A makes B ineligible without blocking unrelated workstreams. + +Add a cycle: + +```text +A -> B -> A +``` + +PASS if the portfolio is rejected as invalid coordination state. + +Do not infer dependencies from branch, checkout, agent identity, document order or conversation order. + +## 13. Authority-gate test + +Create a gate for one exact action: + +```text +state = GRANTED +action = create_paid_compute +``` + +PASS if the grant is not interpreted as authority for unrelated spending or unrelated production mutation. + +Also test `CONSUMED` and `REVOKED` states. + +An agent must never self-grant a REQUIRED gate. + +## 14. Durability test + +Classify unfinished work as: + +```text +SESSION_ONLY +HOST_DURABLE +REPO_DURABLE +REMOTE_DURABLE +``` + +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. + +## 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 -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? + +### Checkout collision rate -### Frozen-strategy fidelity +How often did concurrent executors attempt conflicting mutation ownership? -Did the recovering agent preserve frozen decisions unless new evidence materially invalidated them? +### Takeover recovery rate + +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 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 +403,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. \ No newline at end of file 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/ + └── / + ├── 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 +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. 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. diff --git a/ROADMAP.md b/ROADMAP.md index 5fe636e..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,12 +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. -## Next priorities +v0.2.0 intentionally preserves this single-workstream path when no explicit portfolio is needed. + +## Evidence priorities after v0.2.0 ### Gather independent evidence @@ -28,19 +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. -### Clarify conformance +### Test concurrency semantics -Refine the minimum requirements for claiming PPGP compatibility using observed implementation failures rather than theoretical completeness. +Measure checkout safety, blocker-scope reliability, takeover recovery and stale-write prevention under real multi-agent conditions. -### Improve packaging +### Clarify conformance -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 @@ -50,10 +99,12 @@ 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. +These remain optional integrations. ## Stability diff --git a/SPEC.md b/SPEC.md index 0734a72..97ca561 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,25 +1,41 @@ -# 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 +``` + +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. -## 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 +59,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 +83,162 @@ 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. + +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. + +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. -ACTIVE_GOAL MUST be removed after successful closure and distillation. +HOST_DURABLE means the work survives the agent session on the current host, for example as identified dirty files in a worktree. -### 3.5 GIT / FORENSIC HISTORY +REPO_DURABLE means a local version-control object or equivalent repository recovery artifact exists. -Git or the repository's equivalent history is the forensic record of what actually changed. +REMOTE_DURABLE means the recovery artifact survives loss of the current host. -PPGP memory SHOULD preserve meaning and current state rather than duplicating Git chronology. +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. + +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 +258,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 +268,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 +278,263 @@ 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. -Inside a goal, implementations SHOULD use: +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. + +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 +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; +- reset; +- clean; +- stash foreign changes; +- commit foreign changes; +- 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. + +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 +542,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 +554,32 @@ Perform the next bounded action. Check observable evidence rather than relying on model confidence. +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 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. +Do not turn every trivial action into a persistent write. -The loop repeats until the current phase exit condition is met. +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. -## 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 +588,59 @@ 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 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. -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. +## 15. Abrupt interruption and takeover -If ACTIVE_GOAL says the strategy is frozen, recovery SHOULD resume execution rather than restart THINK by default. +A cooperative handoff and abrupt takeover are different protocol events. + +For RECOVERY_REQUIRED work, a recovery agent SHOULD: + +```text +1. read canonical goal/workstream and portfolio state +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 +``` -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 +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. @@ -197,121 +649,168 @@ 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. +Session/UI labels are weaker observations than direct VCS/workspace inspection. -### A. Agent-solvable +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. -Reversible technical or implementation problem. +## 17. Human interruption policy -Action: solve autonomously. +The default is agent autonomy inside established authority. -### B. External asynchronous +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. -Propagation, crawler refresh, external processing or another event that may complete later. +Before escalating one blocked workstream, an agent SHOULD continue unrelated permitted RUNNABLE work when safe. -Action: record it. Do not block synchronous goal closure unless the Definition of Done explicitly requires it. +Human escalation SHOULD identify the smallest blocked scope and exact decision required. -### C. Authority boundary +## 18. Multi-agent policy -Requires human/product/legal/financial/account authority. +PPGP does not require multiple agents. -Action: escalate with the smallest decision required. +A second agent SHOULD be introduced only when expected independent information gain or parallel useful work exceeds communication and coordination cost. -### D. Hard dependency +Concurrent agents SHOULD operate on isolatable or explicitly coordinated workstreams. -Required information or resource is genuinely unavailable and no safe autonomous path exists. +A reviewer SHOULD receive artifact, requirements and relevant facts without unnecessary exposure to implementer self-assessment. -Action: escalate only after autonomous alternatives are exhausted. +The existence of PORTFOLIO, leases or checkout claims does not imply that many agents should run simultaneously. -Agents MUST NOT promote routine Type A decisions to Type C solely to avoid responsibility. +## 19. Handoff format -## 9. Human interruption policy +Handoffs SHOULD prefer compact structured state or deltas over narrative transcripts. -The default is agent autonomy inside established authority. +Single-workstream handoffs MAY retain the v0.1.x compact form. -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. +Concurrent handoffs SHOULD carry equivalent information to: -Routine debugging, reversible refactors, test failures and ordinary implementation choices SHOULD NOT require human interruption. +```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> +``` -## 10. Multi-agent policy +Exact encoding is not normative. -PPGP does not require multiple agents. +## 20. Distillation and garbage collection + +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 +The richer tracked/untracked/ownership/sensitivity workspace observation profile is recovery guidance and does not require new committed fields in the reference JSON schema. -D: -ja_review=PASS -tests=PASS +## 22. Backward compatibility and migration -B: -master_text=AUTH +Without an explicit portfolio, an existing PPGP v0.1.x ACTIVE_GOAL remains a valid implicit one-workstream deployment. -E: -commit=8f3d55b +Legacy commands SHOULD retain their behavior where no multi-workstream ambiguity exists. -N: -review_de -ship +In an explicit multi-workstream portfolio, a generic `goal` operation MUST NOT silently choose or replace an ambiguous workstream. + +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 +818,111 @@ 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. -## 14. Interoperability +These metrics define measurement ideas, not benchmark claims. + +## 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 + +A dirty identified worktree survives executor disappearance and is recovered non-destructively with a new lease generation or equivalent fencing mechanism. + +### C4. Independent workstream + +One authority/external/technical wait does not stop another eligible RUNNABLE workstream. + +### C5. Mixed waits + +Several wait kinds remain visible without being collapsed into one lossy scalar state. + +### C6. Revision conflict -Implementations SHOULD reuse equivalent existing documents instead of creating duplicate sources of truth. +A stale revision cannot silently overwrite newer canonical state. -## 16. Conformance test +### C7. Dependency cycle -A useful PPGP recovery test is: +A cyclic workstream dependency graph is rejected as invalid. -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. +### C8. Checkout collision -A system that cannot pass this recovery test SHOULD NOT claim robust PPGP continuity. +Two exclusive mutation claims on the same mutable checkout are rejected. -Passing one recovery test demonstrates recovery under that tested condition only. It does not establish universal effectiveness or superiority. +### C9. Legacy compatibility -## 17. Versioning +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. + +### 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 + +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. \ No newline at end of file diff --git a/bin/ppgp.js b/bin/ppgp.js index 8ce599f..9c81ea5 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,14 +14,38 @@ const ROLE_CANDIDATES = { ACTIVE_GOAL: ['docs/ACTIVE_GOAL.md', 'ACTIVE_GOAL.md'] }; +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) { - console.error(`PPGP: ${message}`); - process.exit(code); + 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) { @@ -31,6 +56,22 @@ function parseArgs(argv) { 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') { + 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); } @@ -39,6 +80,13 @@ function parseArgs(argv) { 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); @@ -51,13 +99,59 @@ 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 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) { @@ -67,6 +161,7 @@ function printMap(root) { 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; } @@ -92,46 +187,466 @@ function parseSections(content) { } 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 absolute = path.join(root, file); - return { file, sections: parseSections(fs.readFileSync(absolute, '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 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 withExclusiveLock(lock, message, fn) { + fs.mkdirSync(path.dirname(lock), { recursive: true }); + let fd; + 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 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 (!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 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 (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(state) { + const errors = []; + 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; +} + +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- ')}`); + + 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 loadWorkstream(root, id, portfolio = loadPortfolio(root)) { + if (!portfolio) die('No PPGP portfolio found.'); + const ref = portfolio.workstreams.find((candidate) => candidate.id === id); + if (!ref) die(`Unknown workstream: ${id}`); + + 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) { + 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 (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 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( + (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((state) => state.runState === 'COMPLETED')) return 'COMPLETED'; + + 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: [] } }; + } + + 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); + const seenPaths = new Map(); + for (const claim of data.claims) { + 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.'); + } + 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.`); 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,16 +660,373 @@ 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) { + 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( + (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) { - 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> --revision <n>`.'); + } + + 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((wait) => `${wait.kind}/${wait.scope}:${wait.id}`).join(',') || '(none)'}`); + console.log(`U=${state.durability}`); + console.log(`N=${state.nextAction || '(none)'}`); +} + +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 = 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.'); + } + + 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` + }); + + 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 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.'); + } + + withPortfolioLock(root, () => { + const portfolio = portfolioExists(root) ? loadPortfolio(root) : makePortfolio(); + 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.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 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 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, (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 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((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 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 = checkoutRegistryPath(root); + if (!file) return; + + 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'); + + 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 claim of data.claims) { + console.log(`${claim.path} -> ${claim.workstreamId} branch=${claim.branch || '(none)'} head=${claim.head || '(none)'}`); + } + } + return; + } + + 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.'); + + 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); + + 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 cmdSkillPath() { @@ -163,9 +1035,13 @@ function cmdSkillPath() { 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 }); @@ -174,30 +1050,49 @@ 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 observed revisions plus local mutation locks. 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); -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 (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}`); + } -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.`); + 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.`); + } +} + +try { + main(); +} catch (error) { + if (error instanceof PPGPError) { + console.error(`PPGP: ${error.message}`); + process.exitCode = error.code; + } else { + throw error; + } } 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..ba7dbf6 --- /dev/null +++ b/evidence/incidents/INCIDENT-001-concurrent-checkout-partial-wait.md @@ -0,0 +1,304 @@ +# 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; interrupted host-only work later promoted to verified remote durability + +## 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 = external action wait +``` + +## Failure pattern 3: abrupt executor unavailability with dirty isolated work + +After substantial local progress, Agent B became temporarily 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. + +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. + +The correct recovery sequence is observation before mutation: + +```text +read goal and portfolio state +-> inspect workspace +-> identify branch/HEAD/tracked and untracked state +-> preserve changes +-> compare with last checkpoint +-> verify proportionately +-> record takeover +-> 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. + +## 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 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. +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. +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 or now incorporates: + +- PORTFOLIO / workstream coordination; +- WORKSTREAM; +- RUN_STATE separate from lifecycle PHASE; +- scoped typed wait conditions; +- EXECUTION_LEASE; +- CHECKOUT_CLAIM; +- RECOVERY_REQUIRED takeover; +- explicit unfinished-work durability levels; +- 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 invariants + +The most important lesson remains: + +```text +PROTECT FOREIGN WORK +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. + +Recovery without evidence risks reconstructing fiction. + +PPGP v0.2.0 should require all three. \ No newline at end of file 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 6a25dea..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", @@ -29,6 +30,7 @@ "files": [ "bin/", "skills/", + "schemas/", "scripts/benchmark-report.js", "scripts/prepare-pilot-01.js", "benchmarks/", @@ -43,7 +45,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" 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" ] 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. diff --git a/plugins/ppgp/skills/ppgp/references/COORDINATION.md b/plugins/ppgp/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..67692e5 --- /dev/null +++ b/plugins/ppgp/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,321 @@ +# 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; +- 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. + +## 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 +``` + +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + +## 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 +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 +switch +reset +clean +stash foreign work +commit foreign work +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. + +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. + +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: + +```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 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 +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, takeover, or conflicting workspace evidence actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file 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 ``` 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..47adabf --- /dev/null +++ b/rfcs/0001-concurrent-workstreams-leases-partial-blocking.md @@ -0,0 +1,615 @@ +# 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 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; +- 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 +RUN_STATE +LEASE +CHECKOUT_CLAIM +WAIT_CONDITIONS +DURABILITY +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, 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 + +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 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. + +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. 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 RUN_STATE axis: + +```text +RUNNABLE +RUNNING +WAITING +RECOVERY_REQUIRED +PARKED +COMPLETED +``` + +Lifecycle PHASE answers: + +> What kind of work is this workstream doing? + +RUN_STATE answers: + +> Can useful execution proceed now, and does somebody currently own execution? + +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 current execution lease is actively performing it. + +### RUNNING + +A valid execution lease holder is actively executing the workstream. + +### WAITING + +No safe useful action can currently advance the workstream because one or more recorded wait conditions remain unsatisfied. + +WAITING is intentionally neutral. The typed WAIT_CONDITIONS determine whether the cause is external, authority-bound, technical, or mixed. + +### RECOVERY_REQUIRED + +The previous execution holder became unavailable, ownership became ambiguous, or unfinished mutable state exists that must be inspected before normal execution resumes. + +RECOVERY_REQUIRED is a safety state, not a failure declaration. + +### PARKED + +The workstream is intentionally deferred despite potentially being 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 +``` + +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 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 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: + +```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, recovery state, or durability 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; +- how durable the unfinished work is; +- what action should occur next. + +A 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. 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. + +### Cooperative handoff + +The current lease holder: + +1. verifies current material state; +2. updates ACTIVE_GOAL; +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 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 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 +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. + +## 9. 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 +``` + +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. + +## 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. + +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. + +## 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. + +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. + +## 12. Candidate compact registry representation + +The exact encoding is non-normative. + +Example: + +```text +PPGP/0.2 +WS goal-e + run=RUNNING + goal=docs/ACTIVE_GOAL.goal-e.md + lease=agent-a/session-a:ACTIVE + checkout=shared:write + branch=agent-a/goal-e + durability=HOST_DURABLE + +WS d1 + run=RUNNING + goal=docs/ACTIVE_GOAL.d1.md + lease=agent-b/session-b:ACTIVE + checkout=worktree-d1:write + 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. + +## 13. 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 and persistence of live leases are implementation details until the command contract is tested. + +## 14. 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 has an EXTERNAL action-scoped wait; +- 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 because of an AUTHORITY wait; +- another workstream is RUNNABLE. + +Expected: + +- the project remains runnable; +- the agent may continue the independent workstream; +- only the authority-dependent scope is escalated. + +### 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: + +```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. diff --git a/rfcs/0001-normative-delta.md b/rfcs/0001-normative-delta.md new file mode 100644 index 0000000..7df9e1e --- /dev/null +++ b/rfcs/0001-normative-delta.md @@ -0,0 +1,384 @@ +# 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 +``` + +Add the evidence-consistency rule: + +```text +narrative/UI state MUST NOT silently supersede observed canonical state +``` + +## 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. + +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. + +### DURABILITY + +Explicit recovery level for unfinished work. + +Values: + +```text +SESSION_ONLY +HOST_DURABLE +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. + +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 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. + +## 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 +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. + +## 7. Evidence-precedence delta + +Keep the existing evidence hierarchy. + +Add these rules: + +Recorded checkout/dirty/lease state is last-known coordination evidence, not permission to ignore current observed repository state. + +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 + +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=<workstream id> +R=<run state> +L=<lease holder/state> +C=<checkout claim> +W=<scoped waits> +U=<durability> +``` + +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 +tracked state +untracked local 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 +``` + +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 + +For RECOVERY_REQUIRED work: + +```text +read coordination + goal state +-> inspect real workspace +-> 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: + +```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 +``` + +## 18. Conformance delta from observed recovery + +Add evaluation scenarios for: + +- 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. + +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 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 } + } + } + } + } +} 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 } + } +} 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. diff --git a/skills/ppgp/references/COORDINATION.md b/skills/ppgp/references/COORDINATION.md new file mode 100644 index 0000000..67692e5 --- /dev/null +++ b/skills/ppgp/references/COORDINATION.md @@ -0,0 +1,321 @@ +# 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; +- 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. + +## 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 +``` + +A narrative claim, UI label, or remembered state MUST NOT silently supersede observed canonical state. + +## 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 +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 +switch +reset +clean +stash foreign work +commit foreign work +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. + +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. + +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: + +```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 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 +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, takeover, or conflicting workspace evidence actually appears. + +The coordination layer should cost close to zero tokens in simple single-workstream repositories. \ No newline at end of file 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 ``` diff --git a/test/cli.test.js b/test/cli.test.js index b231fc6..0e4b296 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 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'); + 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,26 @@ 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}"`], + ['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'); const releaseWorkflow = readText('.github/workflows/publish-release.yml'); @@ -114,14 +164,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, release version consistency, and distribution tests passed.'); } finally { fs.rmSync(root, { recursive: true, force: true }); } 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.'); diff --git a/test/v020.test.js b/test/v020.test.js new file mode 100644 index 0000000..f97fe5f --- /dev/null +++ b/test/v020.test.js @@ -0,0 +1,197 @@ +'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(condition, message) { + if (!condition) throw new Error(message); +} + +function run(args, ok = true) { + 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 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']); + 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 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 { + // 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'); + + // 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 = 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 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'); + 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 = readJson(root, '.ppgp/workstreams/a/state.json'); + assert(a.lease.generation === 2 && a.lease.holder === 'claude', 'lease generation did not increment'); + + // 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' } + ]; + 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'); + + // 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'); + + // 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; + 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; + writeJson(root, '.ppgp/workstreams/a/state.json', a); + run(['workstream', 'recover', 'a', 'claude', '--root', root, '--revision', String(a.revision)]); + 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'); + + // 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'); + 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(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'); + + // 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 }); +}