diff --git a/.agents/skills/ppgp/SKILL.md b/.agents/skills/ppgp/SKILL.md index 258dd94..9f0ba7f 100644 --- a/.agents/skills/ppgp/SKILL.md +++ b/.agents/skills/ppgp/SKILL.md @@ -5,7 +5,7 @@ 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." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.1.3" protocol: PPGP --- @@ -21,13 +21,13 @@ When asked what PPGP is, who developed it, where it lives, or whether it is empi - Canonical repository: `https://github.com/Fatboy-coder/ppgp` - Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository +- Evaluation guide: `docs/EVALUATION.md` in the canonical repository - Citation metadata: `CITATION.cff` in the canonical repository - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` +- Source protocol version of this skill: `0.1.3` (published releases: https://github.com/Fatboy-coder/ppgp/releases) -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. +PPGP v0.1.3 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. 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. @@ -139,7 +139,7 @@ Before another agent or session takes over: Prefer: ```text -PPGP/0.1.2 +PPGP/0.1.3 G= P= F: @@ -151,6 +151,8 @@ N: Do not dump the conversation transcript. +The packet supplements ACTIVE_GOAL; it never replaces it. The receiving agent needs the repository, the current ACTIVE_GOAL and the packet. Do not expand the packet into a context dump; update ACTIVE_GOAL instead. + ### `ppgp distill` At the end of a goal or after major state accumulation: @@ -186,6 +188,22 @@ Close only when the synchronous Definition of Done is verified. Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +## Writing ACTIVE_GOAL so tools can read it + +Name fields as `## GOAL` style headers (any level), `**GOAL**` bold lines, or `GOAL:` upper-case key lines. Case, spacing and hyphens do not matter; extra sections are kept and reported. A file is conformant only with all thirteen fields; anything less is partial and the CLI names what is missing (exit 2). Prefer one substantial end-to-end goal over a trivial task; put sub-steps in DEFINITION_OF_DONE, COMPLETED and REMAINING. + +## Parking deferred work + +Keep one ACTIVE_GOAL. To defer a goal without closing it: bring ACTIVE_GOAL to verified truth, record the goal under ROADMAP as deferred with a resume condition and a pointer to its preserved state (last commit or a dated doc), then replace ACTIVE_GOAL. On resume, re-instantiate and re-verify. + +## Blocker scope + +State the smallest true scope in BLOCKERS ("Step A only: ...; steps B and C are not blocked") and keep NEXT_EXECUTABLE_ACTION on safe work. Continue independent work before escalating. + +## Goal state across branches + +ACTIVE_GOAL is read from the checked-out ref. If it lives on a topic branch, name that branch in ROADMAP on the integration branch. `ppgp doctor` lists other refs carrying an ACTIVE_GOAL but never switches or merges. + ## Human escalation Solve reversible technical decisions autonomously. diff --git a/.agents/skills/ppgp/references/PPGP.md b/.agents/skills/ppgp/references/PPGP.md index e4a6df2..a3dfbdb 100644 --- a/.agents/skills/ppgp/references/PPGP.md +++ b/.agents/skills/ppgp/references/PPGP.md @@ -1,4 +1,4 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.1.3 Compact Reference ## Objective @@ -52,6 +52,8 @@ NEXT_EXECUTABLE_ACTION Write current state, not a diary. +All thirteen fields present = conformant. Fewer = partial; tools name the missing fields. Fields may be headers, bold lines or `KEY:` lines. + ## Recovery Load the smallest sufficient boot packet: @@ -74,6 +76,16 @@ C authority boundary -> escalate minimally D hard dependency -> escalate if no safe autonomous path ``` +Scope blockers to the smallest true step in prose; continue unblocked work. + +## Parking + +One ACTIVE_GOAL. Defer a goal by recording it under ROADMAP with a resume condition and a pointer to its preserved state, then replace ACTIVE_GOAL. Re-verify on resume. + +## Refs + +ACTIVE_GOAL is read from the checked-out ref. Name a topic-branch goal in ROADMAP on the integration branch. + ## Evidence Default technical precedence: @@ -94,6 +106,8 @@ runtime/production Prefer deltas and compact structured state over transcript replay. +The packet supplements ACTIVE_GOAL; it never replaces it. Receiver needs repository + ACTIVE_GOAL + packet. + Keep the handoff human-auditable and cross-model readable. Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e340855..06fa87b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ppgp", - "version": "0.1.2", + "version": "0.1.3", "description": "Portable continuity protocol for long-running coding agents.", "author": { "name": "Hervey (Fatboy-coder)", diff --git a/.github/ISSUE_TEMPLATE/evaluation-report.yml b/.github/ISSUE_TEMPLATE/evaluation-report.yml index 98293e3..7aa9ebd 100644 --- a/.github/ISSUE_TEMPLATE/evaluation-report.yml +++ b/.github/ISSUE_TEMPLATE/evaluation-report.yml @@ -11,7 +11,7 @@ body: id: ppgp-version attributes: label: PPGP version - placeholder: "0.1" + placeholder: "the version printed by ppgp --version, or the SPEC.md title" validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/recovery-failure.yml b/.github/ISSUE_TEMPLATE/recovery-failure.yml index 94c661e..8328154 100644 --- a/.github/ISSUE_TEMPLATE/recovery-failure.yml +++ b/.github/ISSUE_TEMPLATE/recovery-failure.yml @@ -11,7 +11,7 @@ body: id: ppgp-version attributes: label: PPGP version - placeholder: "0.1" + placeholder: "the version printed by ppgp --version, or the SPEC.md title" validations: required: true - type: input diff --git a/.github/workflows/publish-github-package.yml b/.github/workflows/publish-github-package.yml index ebdcc11..735a8bd 100644 --- a/.github/workflows/publish-github-package.yml +++ b/.github/workflows/publish-github-package.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: "Existing GitHub Release version, for example 0.1.2" + description: "Existing published GitHub Release version (MAJOR.MINOR.PATCH, no v prefix)" required: true type: string workflow_run: diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 1b36784..e95fa1f 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: "Existing GitHub Release version, for example 0.1.2" + description: "Existing published GitHub Release version (MAJOR.MINOR.PATCH, no v prefix)" required: true type: string workflow_run: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 43e9310..568000d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: version: - description: "Semantic release version, for example 0.1.2" + description: "Release version to publish (MAJOR.MINOR.PATCH, no v prefix); must equal package.json version" required: true type: string @@ -41,7 +41,7 @@ jobs: if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Invalid release version: '$RELEASE_VERSION'" - echo "Use an immutable three-part SemVer such as 0.1.2." + echo "Use an immutable three-part SemVer (MAJOR.MINOR.PATCH)." exit 1 fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c2eba0..cfe999b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: push: - branches: [main, distribution/adapters-v0.1] + branches: [main] pull_request: permissions: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0eefc25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Generated release archives are built by .github/workflows/publish-release.yml and attached to GitHub Releases; never commit them. +/*.zip +/*.zip.sha256 +/dist/ +/*.tgz +node_modules/ +tmp-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7759b..3a2519d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 0.1.3 + +Publication state and date: see [GitHub Releases](https://github.com/Fatboy-coder/ppgp/releases). Hardening release. Protocol semantics are unchanged from 0.1.2; the CLI and documentation are brought up to the model that the 2026-09-23 adversarial validation (`research/2026-09-23-adversarial-validation.md`) found sound. + +CLI: + +- tolerant ACTIVE_GOAL parser: fields are recognized as Markdown headers at any level, bold-only lines or upper-case `KEY:` lines, matched case-insensitively with a small alias set; unrecognized sections are retained and reported instead of dropped; the first of duplicate sections wins with a warning; +- explicit conformance classification and exit codes: `0` conformant (all thirteen SPEC 3.4 fields present; warnings on stderr), `2` partial (PPGP state recognized but canonical fields missing, each named), `1` malformed or missing; empty, unreadable or sectionless files no longer report healthy state; +- diagnostics for missing sections, non-lifecycle PHASE values, `CLOSED` phase inside an existing ACTIVE_GOAL, `CLOSED` with non-empty REMAINING, and leftover scaffold placeholders; `doctor` surfaces the same findings; +- repository root resolves from `--root`, else the Git top-level of the working directory, else the working directory, so nested invocation finds repository-level state; Git remains optional; +- `doctor` prints branch, HEAD and working-tree change count and, when no ACTIVE_GOAL is checked out, lists other refs that carry one without switching or merging; +- `goal --force` preserves the replaced file as a timestamped `.bak`. + +Documentation (clarifications only, no new normative fields): + +- machine-readable shape of ACTIVE_GOAL and CLI exit codes; +- visibility of goal state across refs and the ROADMAP pointer convention; +- handoff packet supplements, never replaces, repository state; +- parking convention for deferred goals using ROADMAP; +- blocker scope expressed in prose; +- GOAL / LOOP / TASK / SESSION relationship and end-to-end goal granularity guidance; +- `distill` and `close` stay agent-performed operations; +- source version and published release recorded as separate facts: the source tree carries only its version; documents link to GitHub Releases and npm instead of hard-coding release assets, and neither `CHANGELOG.md` nor `CITATION.cff` embeds a publication date or candidate marker that a tagged tree could not keep current; +- compatibility matrix carries evidence type and last-verified date per platform. + +Repository: + +- smaller root surface: `COMPATIBILITY.md`, `DISTRIBUTION.md` and `EVALUATION.md` moved under `docs/`, `BENCHMARK_PROTOCOL.md` to `benchmarks/PROTOCOL.md`, the adversarial validation record to `research/`; README rewritten as the single entry point with a repository map; CONTRIBUTING rewritten as the contributor path; +- stale tracked archive `dist/ppgp-v0.1.zip` removed; generated release archives ignored; +- CI push filter reduced to `main`; workflow inputs describe versions generically; +- the v0.2.0 concurrency proposal retired to branch `research/v0.2-concurrency-experiment`. + +Tests: + +- `test/hardening.test.js` with fixtures derived from the adversarial validation: happy path, realistic canonical/SCP-style/OCPDF-style files, implemented-not-deployed, observation window, scoped blocker, malformed and contradictory state, nested-directory invocation, branch visibility, forced replacement backup, minimal repository, skill install. + +This release was maintainer-tested and adversarially exercised under documented scenarios. It makes no universality, superiority or formal-verification claim. + ## 0.1.2 - 2026-08-26 Version-consistency and release-hardening patch. diff --git a/CITATION.cff b/CITATION.cff index 762d0e2..53eeafa 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,8 +4,7 @@ title: "Portable Persistent Goal Protocol (PPGP)" type: software authors: - name: "Fatboy-coder" -version: "0.1.2" -date-released: 2026-08-26 +version: "0.1.3" url: "https://github.com/Fatboy-coder/ppgp" repository-code: "https://github.com/Fatboy-coder/ppgp" license: MIT diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md deleted file mode 100644 index a1a5f26..0000000 --- a/COMPATIBILITY.md +++ /dev/null @@ -1,89 +0,0 @@ -# PPGP Platform Compatibility - -PPGP keeps one canonical protocol skill at `skills/ppgp/` and adds only thin distribution adapters around it. - -Status vocabulary: - -- **VERIFIED CLIENT**: install/discovery/invocation has been manually verified in the real client. -- **VERIFIED FORMAT**: repository artifact matches the platform's documented format and is covered by PPGP validation. -- **REPOSITORY NATIVE**: the platform can discover the committed repository skill through a documented standard path. -- **IMPORT READY**: the platform can import or install the canonical public skill/repository, but a client-side smoke test is still required. -- **STRUCTURALLY READY**: manifests are present and validated, but marketplace/client discovery still needs a manual external test. -- **EXTERNAL SUBMISSION REQUIRED**: repository work is complete; public directory listing needs a vendor-side submission or approval. -- **DOCUMENTATION ONLY**: no stable first-class adapter was added in this iteration. - -| 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 | -| 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` | -| 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 | -| Devin | Agent Skills | `.agents/skills/ppgp/` generated mirror | REPOSITORY NATIVE | Connect/open the repo in Devin and verify discovery | -| 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 | - -## Canonical skill and generated mirror - -The source of truth is always: - -```text -skills/ppgp/SKILL.md -skills/ppgp/references/PPGP.md -``` - -For clients that natively discover the cross-agent `.agents/skills/` convention, PPGP also commits: - -```text -.agents/skills/ppgp/SKILL.md -.agents/skills/ppgp/references/PPGP.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. - -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. - -Claude invocation is client-surface dependent. In the tested Claude client, the self-hosted marketplace installation exposes and successfully invokes: - -```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: - -```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. - -## Adapter principles - -1. Protocol semantics remain vendor-neutral. -2. A platform manifest may describe PPGP, but may not fork the protocol. -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. -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. - -## 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70b3dc0..5a1fdd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,56 +1,64 @@ # Contributing to PPGP -PPGP v0.1.2 is intentionally provisional. +PPGP is experimental and published early so it can be tested on real repositories, challenged, simplified and corrected. Failure reports are at least as useful as positive results. -The project is being published early so developers can test it on real repositories, challenge its assumptions, simplify it and report failures. +## Quick start for contributors -## Useful contributions +```bash +git clone https://github.com/Fatboy-coder/ppgp +cd ppgp +npm test # CLI, version consistency, hardening, benchmark, package suites +node test/installed-cli.test.js # packs and installs the package, runs the generated shim +node bin/ppgp.js --help # run the source CLI without installing +``` -Especially valuable reports include: +No dependencies are installed; the CLI and tests use Node's standard library only (Node 18+). -- 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 concrete repository benefited from a modification to the protocol. +## Where things live -Positive results are welcome, but failure reports are at least as useful. +```text +SPEC.md the protocol; normative +skills/ppgp/ canonical Agent Skill (SKILL.md, references/PPGP.md); edit here only +.agents/skills/ppgp/ generated mirror; never edit +plugins/ppgp/skills/ppgp/ generated mirror for the Claude plugin; never edit +bin/ppgp.js the CLI +test/ regression tests; test/fixtures/ holds ACTIVE_GOAL variants +benchmarks/ evaluation protocol, result schema, pilot fixture +docs/ COMPATIBILITY, DISTRIBUTION, EVALUATION; ACTIVE_GOAL.md while a goal is open +research/ dated, non-normative evidence records +scripts/sync-skill-mirror.js regenerates the two mirrors from skills/ppgp/ +``` -## Evidence +After editing anything under `skills/ppgp/`, run `node scripts/sync-skill-mirror.js`; `npm test` fails if a mirror drifts. -When practical, include: +## Rules that tests enforce -- agent/product and version; -- repository scale or rough shape; -- PPGP version; -- relevant protocol state; -- expected behavior; -- observed behavior; -- whether a human had to reconstruct context; -- verification evidence. +- Source-version artifacts (`package.json`, `SPEC.md` title, skill metadata, compact reference title, `CITATION.cff`, adapter manifests, benchmark protocol, `ROADMAP.md`, `CHANGELOG.md`) agree on one version. +- The source tree never records publication state. `CHANGELOG.md` heads the current entry with the bare version and `CITATION.cff` carries no `date-released`; whether and when a version was published is answered only by GitHub Releases and npm. A tagged tree is immutable, so any embedded "candidate" or date would go stale. +- No active document hard-codes an unpublished `ppgp-v.zip` asset or `@` install line; published versions are linked through GitHub Releases and npm. +- Adapter directories stay out of the npm payload; no generated archive is tracked. +- A canonical thirteen-field `ACTIVE_GOAL` parses as conformant; partial, malformed and missing state are reported distinctly. -Do not publish proprietary code, credentials or confidential prompts merely to provide a reproduction. +## Proposing a change -## Discussion style - -Challenge the protocol, not the contributor. +1. Open an issue or start from an existing one; recovery failures and evaluation reports have issue forms. +2. Branch from `main`. Keep protocol changes and implementation changes in separate PRs where practical. +3. A change to protocol semantics needs a reproducible failure of the current model first. The evolution rule is falsification-driven: reproduce, classify (protocol, tooling, documentation, convention, operator error), try the existing model, make the smallest change, rerun the regression suite. See `research/` for how prior campaigns did this. +4. Do not add a vendor-specific requirement to the portable core; add an optional adapter and document it in `docs/COMPATIBILITY.md` with its evidence type and date. +5. Run `npm test` and `node test/installed-cli.test.js` locally; CI runs both on Linux and Windows. +6. Describe what was verified and what was not. Do not use "validated", "proven" or "certified" for maintainer-run tests. -Prefer concrete counterexamples over status arguments. +## Self-hosting -Do not assume that a technique working for one model or repository is universal. +Substantial changes to this repository are tracked with the protocol itself: open `docs/ACTIVE_GOAL.md` with `node bin/ppgp.js goal ""`, keep it current after verified material changes, and delete it at verified closure. When the goal lives on a topic branch, name the branch in `ROADMAP.md` so a fresh agent on `main` can find it. Historical goal state remains in Git. -Claims of superiority should include reproducible evidence. +## Releases -## Maturity +Maintainers release through the guarded workflows described in [`docs/DISTRIBUTION.md`](./docs/DISTRIBUTION.md): bump `package.json` and the mirrored version strings on a release branch, add the bare `## ` changelog entry, merge after review, then run the release workflow, which tags, publishes the GitHub Release and chains npm and GitHub Packages publication. Nothing in the tree changes after publication; the release date lives on the GitHub Release. -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. +## Discussion style -The project should remain understandable without requiring a database, external service or paid platform. +Challenge the protocol, not the contributor. Prefer concrete counterexamples over status arguments. Do not assume a technique that works for one model or repository is universal. ## License diff --git a/DISTRIBUTION.md b/DISTRIBUTION.md deleted file mode 100644 index c7f6128..0000000 --- a/DISTRIBUTION.md +++ /dev/null @@ -1,320 +0,0 @@ -# PPGP Distribution - -PPGP is intentionally distributed through multiple channels. The protocol and canonical Agent Skill remain vendor-neutral; platform-native manifests are thin adapters for discovery and installation. - -Canonical protocol skill: - -```text -skills/ppgp/SKILL.md -skills/ppgp/references/PPGP.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. - -Regenerate the compatibility mirrors deterministically with: - -```bash -node scripts/sync-skill-mirror.js -``` - -See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for the current platform-by-platform support matrix and 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 - -PPGP exposes a Claude plugin and a self-hosted marketplace from the canonical repository: - -```text -.claude-plugin/plugin.json -.claude-plugin/marketplace.json -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. - -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: - -```text -.codex-plugin/plugin.json -.agents/plugins/marketplace.json -``` - -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 - -PPGP exposes a Gemini CLI extension manifest at repository root: - -```text -gemini-extension.json -``` - -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: - -```text -plugin.json -``` - -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: - -```text -.agents/skills/ppgp/ -``` - -This mirror is not authoritative. Tests enforce byte-for-byte parity with `skills/ppgp/`. - -## Windsurf - -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: - -```text -https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp -``` - -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. - -## JetBrains Junie - -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. - -Until then, the canonical Agent Skill and CLI remain available, and `COMPATIBILITY.md` records the exact current support state rather than inventing proprietary files. - -## GitHub Release - -Purpose: zero-friction download of the installable Agent Skill archive. - -Current canonical asset: - -```text -ppgp-v0.1.2.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. - -## npm - -Purpose: public CLI discovery and zero-install execution. - -Canonical public package name: - -```text -@fatboy-coder/ppgp -``` - -Current package release: - -```text -@fatboy-coder/ppgp@0.1.2 -``` - -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. - -CLI binary: - -```text -ppgp -``` - -Zero-install 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 -``` - -When diagnosing `npx` executable inference or npm cache behavior, the explicit npm-exec form removes ambiguity about which binary must run: - -```bash -npm exec --yes --package=@fatboy-coder/ppgp@0.1.2 -- 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 -npm.cmd exec --yes --package=@fatboy-coder/ppgp@0.1.2 -- ppgp --version -``` - -Expected output: - -```text -0.1.2 -``` - -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: - -```bash -npm install -g @fatboy-coder/ppgp@0.1.2 -ppgp --version -``` - -Inside the PPGP source repository itself, the source CLI can always be tested directly without any installation: - -```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. - -### npm Trusted Publisher - -The package is published through GitHub Actions using npm Trusted Publishing/OIDC. The workflow is `.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. - -## Release automation - -A release decision begins with one guarded manual workflow: - -```text -Publish PPGP release - ↓ workflow_run -Publish PPGP to npm - ↓ workflow_run -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. - -## Validation - -`npm test` validates the CLI, package contents and distribution invariants, including: - -- 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. - -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. - -## Version mapping - -PPGP v0.1.2 uses one canonical current release version across the protocol specification and versioned distribution artifacts: - -```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 -``` - -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. - -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. diff --git a/README.md b/README.md index 8bbdafe..4a1dae5 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ # Portable Persistent Goal Protocol (PPGP) -> Portable continuity protocol for long-running coding agents. +> Open, vendor-neutral continuity protocol for coding agents: persistent goals, recoverable state, verified work, clean handoffs. -**Status:** Experimental v0.1.2 -**First public release:** 2026-08-24 -**Current release:** 2026-08-26 -**License:** MIT -**Maturity:** Provisional +**Source version:** v0.1.3 (this tree; see `package.json`) +**Latest published release:** [GitHub Releases](https://github.com/Fatboy-coder/ppgp/releases/latest) · [npm](https://www.npmjs.com/package/@fatboy-coder/ppgp) +**Status:** experimental, maintainer-tested, adversarially exercised under documented scenarios +**License:** MIT -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. +## What it is -It does not replace model memory, Git, tests, MCP or provider-specific compaction. It defines a small control protocol around them. +Long-running coding agents lose their working context: sessions end, context is compacted, one agent replaces another. PPGP keeps the minimum state a fresh agent needs in repository-visible files, so work resumes from the repository instead of from a human retelling the story. -**[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)** +It does not replace model memory, Git, tests or MCP. It is a small control protocol around them: one `ACTIVE_GOAL` state with thirteen fields (the reference CLI stores it in `docs/ACTIVE_GOAL.md`), a goal lifecycle, an inner verify-and-record loop, and rules for evidence, blockers, handoff and closure. -## Try PPGP in 30 seconds +## Try it Inside any Git repository: @@ -22,49 +21,18 @@ Inside any Git repository: npx @fatboy-coder/ppgp init npx @fatboy-coder/ppgp goal "Ship one verified milestone" npx @fatboy-coder/ppgp status +npx @fatboy-coder/ppgp doctor ``` -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. +`goal` scaffolds `docs/ACTIVE_GOAL.md`; `status` and `handoff` read it back; `doctor` checks the repository. The CLI is a deterministic helper. `distill` and `close` are agent-performed protocol operations because they require judgement. -For a quick environment check: +Install the Agent Skill into an Agent Skills-compatible client: ```bash -npx @fatboy-coder/ppgp doctor +npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp ``` -## What PPGP keeps recoverable - -A coding agent should be able to recover the minimum operational state needed to continue useful work: - -- the current goal; -- frozen decisions; -- verified state; -- remaining work; -- real blockers; -- durable lessons; -- the next executable action. - -## Start here - -| 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) | - -## Why - -Long-running coding agents commonly lose efficiency when they must repeatedly reconstruct operational context after context compaction, interrupted sessions, handoffs or agent replacement. - -PPGP externalizes only the minimum useful state and treats conversation history as disposable cache. +Manual install: open the [latest GitHub Release](https://github.com/Fatboy-coder/ppgp/releases/latest), download its versioned `ppgp-v*.zip`, and copy the `ppgp` directory into your client's skills location. Platform-specific routes (Claude, Codex, Gemini, Cursor, Copilot and others) are in [`docs/COMPATIBILITY.md`](./docs/COMPATIBILITY.md). ## Core model @@ -78,191 +46,53 @@ THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED RETRIEVE -> ACT -> VERIFY -> DELTA ``` -Logical memory layers: - ```text CONSTITUTION long-lived authority and constraints -ROADMAP project direction and goal scheduling +ROADMAP project direction, deferred goals MEMORY durable decisions, invariants and lessons -ACTIVE_GOAL temporary working memory for one goal +ACTIVE_GOAL temporary working memory for exactly one goal GIT forensic history and implementation evidence ``` -`ACTIVE_GOAL` is temporary. At goal closure, durable information is distilled into persistent memory and the temporary goal state is deleted. - -## Design principles - -- 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. - -## Install +`ACTIVE_GOAL` is conformant when all thirteen fields of [SPEC §3.4](./SPEC.md) are present. The CLI is tolerant about how they are written (header level, bold, `KEY:` lines) and strict about which are missing: exit `0` conformant, `2` partial, `1` malformed or missing. Prepared is not done; started is not done; agent confidence is not evidence. -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. - -### Universal Agent Skills route - -```bash -npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp -``` - -`skills/ppgp/` is the canonical PPGP Agent Skill source. - -### Native platform routes - -| 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 | - -See [`COMPATIBILITY.md`](./COMPATIBILITY.md) for verification level, limitations and remaining marketplace actions. Repository readiness is not presented as vendor approval or public listing. - -### PPGP CLI - -The canonical public npm package is `@fatboy-coder/ppgp`: - -```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 -``` - -For repeated use, install it globally and keep the short `ppgp` executable: - -```bash -npm install -g @fatboy-coder/ppgp -ppgp init -``` +The protocol is [`SPEC.md`](./SPEC.md). The compact agent-facing reference is [`skills/ppgp/references/PPGP.md`](./skills/ppgp/references/PPGP.md). -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. +## Versions -### Manual install +- **Source version** is the `version` in `package.json`, mirrored in `SPEC.md`, the skill metadata and `CITATION.cff`. It identifies this tree, released or not. +- **Published versions** are only what [GitHub Releases](https://github.com/Fatboy-coder/ppgp/releases) and [npm](https://www.npmjs.com/package/@fatboy-coder/ppgp) actually list, with their dates. The source tree never records publication state, so a tagged tree never goes stale. +- `0.x` releases are experimental and may change incompatibly. Cite the exact version you evaluated. -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. +What comes next is in [`ROADMAP.md`](./ROADMAP.md). -The repository also keeps the canonical source under [`skills/ppgp/`](./skills/ppgp/) for inspection and development. +## Evidence and research -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. +PPGP does not claim to invent agent memory, outperform other systems, reduce tokens by a fixed percentage or eliminate human review. Its claims are narrow and meant to be falsified: -### Read without installing +- [`docs/EVALUATION.md`](./docs/EVALUATION.md) — how to evaluate PPGP and what to record. +- [`benchmarks/`](./benchmarks/) — paired A/B recovery protocol, result schema, deterministic pilot fixture. +- [`research/`](./research/) — dated, non-normative validation records, including the 2026-09-23 adversarial validation of v0.1.2. -Read [`SPEC.md`](./SPEC.md) for the protocol itself. +Recovery failures, overhead reports and negative results are welcome through the [issue forms](../../issues/new/choose). -The skill contains a compact operational reference in [`skills/ppgp/references/PPGP.md`](./skills/ppgp/references/PPGP.md). - -## Operations - -The Agent Skill exposes six workflow intents: - -```text -ppgp init -ppgp goal -ppgp status -ppgp handoff -ppgp distill -ppgp close -``` - -The CLI currently implements deterministic helpers for `init`, `doctor`, `goal`, `status`, `handoff`, `skill-path`, and `install-skill`. - -These are protocol operations, not assumptions about a vendor-specific slash-command system. - -## Distribution - -PPGP uses multiple distribution surfaces on purpose: +## Repository map ```text -Canonical Agent Skill -> vendor-neutral source of truth -Claude Plugin/Marketplace -> native Claude discovery -OpenAI Plugin -> Codex / OpenAI plugin 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 -GitHub Packages -> package presence inside GitHub +SPEC.md the protocol (normative) +skills/ppgp/ canonical Agent Skill; .agents/ and plugins/ are drift-tested mirrors +bin/ppgp.js dependency-free CLI helper +test/ regression suite and fixtures +benchmarks/ evaluation protocol, schema, pilot fixture +docs/ compatibility, distribution, evaluation guide; ACTIVE_GOAL.md while a goal is open +research/ dated evidence records +CHANGELOG.md ROADMAP.md CONTRIBUTING.md CITATION.cff ``` -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. - -## 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. - -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. - -See [`CONTRIBUTING.md`](./CONTRIBUTING.md). - -## Citation - -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. - -## What v0.1.2 deliberately does not claim - -PPGP v0.1.2 does **not** claim to: - -- invent persistent agent memory; -- outperform existing memory systems; -- be optimal for every repository; -- reduce tokens by a specific percentage; -- eliminate human review; -- make multi-agent systems inherently better. - -The purpose of the public v0.1.2 release is to make the protocol inspectable, reproducible and falsifiable. - -## 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. - -The project may be used commercially under the MIT license. The community-oriented mission is not a restriction on who may use the protocol. - -## Publication history - -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. - -## Versioning +Machine manifests at the root (`plugin.json`, `gemini-extension.json`, `.claude-plugin/`, `.codex-plugin/`) exist for agent platforms and need no reading. -PPGP uses semantic versions for the current protocol and its versioned distribution artifacts. +## Contributing and citing -`0.x` releases are experimental and may change incompatibly. +[`CONTRIBUTING.md`](./CONTRIBUTING.md) covers tests, the CLI, mirror synchronization, the self-hosting rule and releases. Cite with [`CITATION.cff`](./CITATION.cff), naming the exact version. -Researchers, developers and maintainers should cite the exact version evaluated. +PPGP was first published on 2026-08-24 in the `Fatboy-coder/fatboy-coder` repository under `/ppgp`; this repository is its canonical home. It is an independent open-source project, not affiliated with or endorsed by any agent vendor. diff --git a/ROADMAP.md b/ROADMAP.md index b3dc7f4..5dc7485 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,108 +1,46 @@ # 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 the removal of unnecessary protocol overhead. Published versions are listed on [GitHub Releases](https://github.com/Fatboy-coder/ppgp/releases); history is in `CHANGELOG.md`. -## v0.1.2 +## Where things stand -Published as the current experimental line and available for public testing. +The latest published release is v0.1.2 (2026-08-26). A maintainer-run adversarial campaign on 2026-09-23 exercised that package across context loss, agent replacement, interruption recovery, long external waits, single-goal stress, independent projects, cross-project references, stale state, false completion, malformed state, Git/worktree conditions and minimal-project overhead. No core protocol semantic failure was reproduced; the failures found were in the CLI and documentation. The record is `research/2026-09-23-adversarial-validation.md`. That result is a regression baseline, not a universality claim. -Current capabilities: +## v0.1.3 hardening candidate -- portable repository-visible goal state; -- THINK, FREEZE, EXECUTE, HARDEN, SHIP, DISTILL lifecycle; -- RETRIEVE, ACT, VERIFY, DELTA inner loop; -- logical memory roles without mandatory filenames; -- 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. +The only release currently in flight. Implemented on branch `release/harden-0.1.3` (PR #12) and awaiting owner review; it becomes a published release only after the release workflow tags, publishes and chains the npm and GitHub Packages publication. -### 2026-09-23 adversarial validation +Scope, as implemented, hardens the v0.1.2 implementation without changing its continuity model: -A maintainer-run adversarial campaign tested the published v0.1.2 package across context loss, agent replacement, interruption recovery, long external waits, single-goal stress, independent projects, cross-project references, stale state, false completion, malformed state, Git/worktree conditions and minimal-project overhead. - -No reproducible core protocol semantic failure was demonstrated in the tested scenarios. - -The campaign instead identified implementation and documentation weaknesses, especially: - -- CLI parsing/validation of real-world PPGP files; -- malformed-state diagnostics; -- repository-root resolution; -- branch/ref visibility of ACTIVE_GOAL state; -- a small set of documentation clarifications. - -See [ADVERSARIAL_VALIDATION.md](./ADVERSARIAL_VALIDATION.md) for scope, limitations and findings. - -This result is not a universality or superiority claim. It is a regression baseline for future changes. - -## Next candidate: v0.1.3 hardening - -v0.1.3 is the next planned release candidate. It is **not yet the current release**. - -The intended scope is to harden the v0.1.2 implementation without changing its core continuity model: - -- tolerant, explicit CLI parsing; -- warnings for malformed or contradictory state; +- tolerant ACTIVE_GOAL parsing with strict thirteen-field conformance reporting; +- explicit conformant / partial / malformed / missing state and exit codes; +- warnings for missing fields, contradictory state and leftover scaffolding; - Git top-level repository resolution; -- branch visibility diagnostics in `doctor`; -- safer overwrite/force behavior; -- documentation clarifications; -- regression tests derived from adversarial fixtures. - -The release becomes official only after implementation, verification, version-consistency checks, tag/release creation and package publication. - -## Next priorities - -### Harden before extending - -Prefer fixing demonstrated implementation or documentation failures over adding new protocol primitives. +- branch and working-tree summary and other-ref goal visibility in `doctor`; +- backup before forced goal replacement; +- documentation clarifications (parking, blocker scope, handoff packet, goal/loop/task/session, goal granularity, ref visibility); +- source-version versus published-release truth kept separate and test-enforced; +- regression suite derived from the adversarial fixtures; +- a smaller root surface: compatibility, distribution and evaluation guides under `docs/`, benchmark protocol under `benchmarks/`, evidence under `research/`. -A new core concept should require reproducible evidence that the existing v0.1.x model cannot safely express the workflow. +Path: owner review → merge → release workflow → v0.1.3 listed on GitHub Releases and npm. The tree itself does not change at publication. -### Gather independent evidence +## After v0.1.3 -Collect recovery failures, successful replications, overhead reports and comparative evaluations from different repositories, agents and providers. +Harden before extending. A new core concept requires reproducible evidence that the v0.1.x model cannot safely express a real workflow. Until then the priorities are: -### Reduce protocol overhead +- gather independent evidence: recovery failures, replications, overhead reports, comparative evaluations from other repositories, agents and providers; +- reduce protocol overhead: remove fields, steps or rules that do not improve recovery; +- test portability: the same repository-visible state interpreted consistently by materially different agents; +- clarify conformance from observed implementation failures rather than theoretical completeness; +- keep installation simple across Agent Skills-compatible clients without making the core depend on one vendor. -Identify fields, steps or rules that can be removed without reducing recovery quality. +A `1.0` requires evidence that the core rules are stable across multiple independent projects and environments. No date is assigned. -### Test portability +## Preserved research, not a release path -Validate that the same repository-visible state can be interpreted consistently by materially different coding agents and environments. - -### Clarify conformance - -Refine the minimum requirements for claiming PPGP compatibility using observed implementation failures rather than theoretical completeness. - -### Improve packaging - -Keep installation simple across Agent Skills-compatible clients without making the portable core dependent on one vendor. - -## Research branches and proposals - -Richer coordination models, including the existing v0.2.0 concurrency proposal, remain research material until a reproducible core limitation demonstrates the need for them. - -The 2026-09-23 adversarial validation did not establish such a requirement. +A richer coordination model (portfolios, workstreams, execution leases, checkout claims, scoped waits, durability classes) was implemented in 2026-08 and is preserved unchanged on branch `research/v0.2-concurrency-experiment`. The 2026-09-23 validation did not reproduce a failure that requires it, so it is not the next release. It may be revisited if such a failure is reproduced. ## Not planned as core requirements -PPGP does not plan to require: - -- a specific model provider; -- MCP; -- vector databases or embeddings; -- a hosted service; -- multi-agent orchestration; -- proprietary infrastructure. - -These may be useful optional integrations, but they should not become prerequisites for protocol conformance. - -## Stability - -A future `1.0` should require evidence that the core rules are sufficiently stable across multiple independent projects and environments. - -No target date is assigned. Evidence, not calendar time, should determine promotion to a stable specification. +A specific model provider, MCP, vector databases or embeddings, a hosted service, multi-agent orchestration, proprietary infrastructure. These may be optional integrations; they will not become prerequisites for conformance. diff --git a/SPEC.md b/SPEC.md index 0734a72..a442317 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,8 +1,8 @@ -# PPGP Specification v0.1.2 +# PPGP Specification v0.1.3 Status: Experimental / Provisional First published: 2026-08-24 -Current release: 2026-08-26 +Source version: 0.1.3 (see package.json); published releases: https://github.com/Fatboy-coder/ppgp/releases Protocol: Portable Persistent Goal Protocol (PPGP) ## 1. Scope @@ -33,6 +33,17 @@ Current project direction, completed goals, future goals, dependencies and defer It SHOULD describe state and direction, not preserve a full execution diary. +#### Parking deferred work + +PPGP keeps exactly one ACTIVE_GOAL. When a substantial goal must yield to more urgent work without being closed or abandoned, park it instead of keeping two active goals: + +1. bring the current ACTIVE_GOAL to verified truth (VERIFIED_CURRENT_STATE, COMPLETED, REMAINING, BLOCKERS, NEXT_EXECUTABLE_ACTION); +2. record it under ROADMAP as deferred work with a one-line resume condition and a reference to where its full state is preserved (the last commit that contained it, or a dated file in ordinary project documentation); +3. remove or replace ACTIVE_GOAL for the new goal; +4. when resuming, re-instantiate ACTIVE_GOAL from the preserved state and re-verify VERIFIED_CURRENT_STATE before continuing. + +A parked goal is neither active nor closed. ROADMAP holds the pointer, not the diary. This convention needs no registry and preserves one ACTIVE_GOAL, low work-in-progress, explicit deferred work and deterministic recovery. + ### 3.3 MEMORY Durable facts that future agents would otherwise need to rediscover. @@ -77,6 +88,29 @@ ACTIVE_GOAL MUST NOT become the permanent chronological history. ACTIVE_GOAL MUST be removed after successful closure and distillation. +#### Machine-readable shape + +ACTIVE_GOAL is written for humans and agents first. The reference CLI recognizes a field when its name appears as: + +```text +a Markdown header at any level ## GOAL ### Definition of Done +a bold-only line **GOAL** +an upper-case key starting a line GOAL: text FROZEN_DECISIONS: +``` + +Names are matched case-insensitively; spaces and hyphens are treated as underscores. A small alias set is accepted (`CURRENT_PHASE`, `DOD`, `NEXT`/`NEXT_ACTION`, `AUTHORITY`, `EVIDENCE`, and any header containing `NEXT`). Sections with other names are retained and reported, never silently discarded. If a field appears twice, the first occurrence is used and a warning is emitted. + +Tolerant reading does not weaken conformance. An ACTIVE_GOAL is **conformant** only when all thirteen minimum fields above are present. A tool MUST distinguish: + +```text +CONFORMANT all thirteen canonical fields present +PARTIAL PPGP state recognized, but one or more canonical fields missing; each missing field named +MALFORMED no usable PPGP structure: empty, unreadable, or without any recognizable field +MISSING no ACTIVE_GOAL file +``` + +and MUST NOT report partial, malformed or missing state as healthy. GOAL and NEXT_EXECUTABLE_ACTION are useful recovery anchors in a partial file; they are not sufficient for conformance. The reference CLI exits 0 for conformant state (warnings allowed), 2 for partial state and 1 for malformed or missing state. + ### 3.5 GIT / FORENSIC HISTORY Git or the repository's equivalent history is the forensic record of what actually changed. @@ -91,6 +125,14 @@ A substantial PPGP goal follows: THINK -> FREEZE -> EXECUTE -> HARDEN -> SHIP -> DISTILL -> CLOSED ``` +### Goal granularity + +A substantial PPGP goal SHOULD describe a meaningful end-to-end outcome rather than a single implementation step. Prefer "deliver production-ready artifact chaining with verified behavior" over "change function X". Do not make the goal so large that its Definition of Done becomes vague. A useful heuristic: + +> the largest independently meaningful end-to-end outcome that remains objectively verifiable and safely pursuable by the agent. + +This is guidance, not a normative field. Sub-steps belong in DEFINITION_OF_DONE, COMPLETED and REMAINING, not in separate goals. + ### THINK Inspect, research, compare alternatives and determine an executable strategy. @@ -161,6 +203,19 @@ A DELTA SHOULD update repository-visible hot state when the change would materia The loop repeats until the current phase exit condition is met. +### Goal, loop, task and session + +These terms are already implicit in PPGP and are clarified here without adding fields or commands: + +```text +GOAL durable, meaningful, verifiable end-to-end outcome (ACTIVE_GOAL) +LOOP RETRIEVE -> ACT -> VERIFY -> DELTA, repeated while pursuing the goal +TASK disposable implementation decomposition chosen during execution +SESSION replaceable execution container (one conversation, one agent run) +``` + +The invariant is that the GOAL survives loops and sessions. A loop may stop because of context exhaustion, session replacement, temporary interruption, a budget boundary or a legitimate authority blocker without destroying the durable goal. Tasks are recorded only insofar as COMPLETED, REMAINING and NEXT_EXECUTABLE_ACTION need them for recovery. + ## 6. Boot and recovery A fresh agent SHOULD start from a minimal boot packet: @@ -188,6 +243,12 @@ If ACTIVE_GOAL says the strategy is frozen, recovery SHOULD resume execution rat Abrupt interruption before DISTILL MUST NOT by itself be treated as loss of the active goal if current repository-visible hot state exists. +### Visibility across refs + +ACTIVE_GOAL is discovered on the checked-out ref. Goal state committed only on another branch is invisible from the integration branch, and a fresh agent booting there may wrongly conclude that no goal is active. + +When a goal's ACTIVE_GOAL lives on a topic branch, the integration branch's ROADMAP SHOULD name that branch. A Git-aware tool MAY list other refs that carry an ACTIVE_GOAL, but MUST NOT switch branches, merge, or assume that such a file is current. The agent verifies currency before treating it as the active goal. + ## 7. Evidence precedence When technical claims conflict, implementations SHOULD prefer more direct evidence. @@ -238,6 +299,20 @@ Action: escalate only after autonomous alternatives are exhausted. Agents MUST NOT promote routine Type A decisions to Type C solely to avoid responsibility. +### Blocker scope + +A blocker applies to the smallest true scope, not automatically to the whole goal. State the scope in prose inside BLOCKERS and keep REMAINING and NEXT_EXECUTABLE_ACTION pointing at work that is still safe: + +```text +BLOCKERS +- Step A only: Type C authority. Owner must provision the provider key. Steps B and C are NOT blocked. + +NEXT_EXECUTABLE_ACTION +- Step B: port templates/welcome.html to the adapter and add a snapshot test. +``` + +An agent SHOULD continue independent safe work before escalating a scoped blocker. No additional structure is required. + ## 9. Human interruption policy The default is agent autonomy inside established authority. @@ -263,7 +338,7 @@ Handoffs SHOULD prefer compact structured state or deltas over narrative transcr Example: ```text -PPGP/0.1.2 +PPGP/0.1.3 G=8 P=HARDEN @@ -292,6 +367,8 @@ The invariant is that the handoff remain unambiguous, portable, auditable and ch Opaque model-specific gibberish is NOT required for PPGP conformance. +A handoff packet is a compact transfer signal, not a replacement for repository-visible PPGP state. It deliberately omits WHY, DEFINITION_OF_DONE, INVARIANTS, REMAINING and HUMAN_AUTHORITY_REQUIRED. A receiving agent SHOULD normally have the repository, the current ACTIVE_GOAL and the packet; the packet alone is not sufficient for safe continuation. Do not expand the packet into a full context dump; update ACTIVE_GOAL instead. + ## 12. Distillation and garbage collection Before closing a goal, every material ACTIVE_GOAL fact SHOULD be classified: @@ -319,7 +396,7 @@ Implementations MAY measure: - VWR: Verified Work Rate. - MCR: Memory Compression Ratio. -PPGP v0.1.2 defines these metrics but makes no benchmark claim. +PPGP v0.1.3 defines these metrics but makes no benchmark claim. ## 14. Interoperability diff --git a/BENCHMARK_PROTOCOL.md b/benchmarks/PROTOCOL.md similarity index 99% rename from BENCHMARK_PROTOCOL.md rename to benchmarks/PROTOCOL.md index e33b4c5..12601ef 100644 --- a/BENCHMARK_PROTOCOL.md +++ b/benchmarks/PROTOCOL.md @@ -2,7 +2,7 @@ Benchmark method version: 0.1 Status: Experimental / exploratory -Protocol under test: PPGP v0.1.2 +Protocol under test: PPGP v0.1.3 Primary question: does repository-visible PPGP state improve recovery after an abrupt loss of conversational context? This document defines a reproducible paired A/B experiment. It is an evaluation protocol, not evidence that PPGP is effective. diff --git a/benchmarks/README.md b/benchmarks/README.md index f2fda68..4bb3d83 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -7,7 +7,7 @@ This directory contains the machine-readable format for PPGP recovery experiment - `pilot-01/` contains the first prepared empirical pilot fixture and execution runbook. It contains no observed result until real agent sessions are run. - Real experiments should keep one JSON record per condition/run and preserve raw logs separately when publication is safe. -Read [`../BENCHMARK_PROTOCOL.md`](../BENCHMARK_PROTOCOL.md) before collecting results. +Read [`PROTOCOL.md`](./PROTOCOL.md) before collecting results. ## First prepared pilot diff --git a/benchmarks/examples/pair-001-ppgp.json b/benchmarks/examples/pair-001-ppgp.json index 26014eb..9a32b01 100644 --- a/benchmarks/examples/pair-001-ppgp.json +++ b/benchmarks/examples/pair-001-ppgp.json @@ -3,7 +3,7 @@ "experimentId": "example-pilot", "pairId": "pair-001", "condition": "ppgp", - "ppgpVersion": "0.1.2", + "ppgpVersion": "0.1.3", "repository": "example/repository", "baseCommit": "0123456789abcdef", "taskId": "example-multifile-refactor", diff --git a/benchmarks/pilot-01/RUNBOOK.md b/benchmarks/pilot-01/RUNBOOK.md index d7b345f..7d0d6ef 100644 --- a/benchmarks/pilot-01/RUNBOOK.md +++ b/benchmarks/pilot-01/RUNBOOK.md @@ -4,7 +4,7 @@ Status: operational protocol for the first empirical paired run. No result is im Task: [`TASK.md`](./TASK.md) -Benchmark protocol: [`../../BENCHMARK_PROTOCOL.md`](../../BENCHMARK_PROTOCOL.md) +Benchmark protocol: [`../PROTOCOL.md`](../PROTOCOL.md) ## Important corrections to the informal Gemini proposal diff --git a/bin/ppgp.js b/bin/ppgp.js index 8ce599f..c18f7c6 100644 --- a/bin/ppgp.js +++ b/bin/ppgp.js @@ -13,14 +13,49 @@ const ROLE_CANDIDATES = { ACTIVE_GOAL: ['docs/ACTIVE_GOAL.md', 'ACTIVE_GOAL.md'] }; -function die(message, code = 1) { +// The thirteen ACTIVE_GOAL fields of SPEC.md section 3.4, in canonical order. +const SECTIONS = [ + 'GOAL', 'WHY', 'PHASE', 'DEFINITION_OF_DONE', 'FROZEN_DECISIONS', 'INVARIANTS', + 'VERIFIED_CURRENT_STATE', 'COMPLETED', 'REMAINING', 'BLOCKERS', + 'HUMAN_AUTHORITY_REQUIRED', 'VERIFICATION_EVIDENCE', 'NEXT_EXECUTABLE_ACTION' +]; + +// Conformance (SPEC 3.4): all thirteen fields present = CONFORMANT; some PPGP state recognized but +// canonical fields missing = PARTIAL; no usable structure = MALFORMED; no file = MISSING. + +// Small alias table for names seen in real repositories. Kept deliberately short. +const ALIASES = { + CURRENT_PHASE: 'PHASE', + DOD: 'DEFINITION_OF_DONE', + DEFINITION_OF_DONE: 'DEFINITION_OF_DONE', + FROZEN: 'FROZEN_DECISIONS', + BLOCKER: 'BLOCKERS', + AUTHORITY: 'HUMAN_AUTHORITY_REQUIRED', + HUMAN_AUTHORITY: 'HUMAN_AUTHORITY_REQUIRED', + EVIDENCE: 'VERIFICATION_EVIDENCE', + VERIFIED_STATE: 'VERIFIED_CURRENT_STATE', + NEXT: 'NEXT_EXECUTABLE_ACTION', + NEXT_ACTION: 'NEXT_EXECUTABLE_ACTION', + NEXT_STEP: 'NEXT_EXECUTABLE_ACTION', + NEXT_EXECUTABLE_ACTIONS: 'NEXT_EXECUTABLE_ACTION' +}; + +const PHASES = ['THINK', 'FREEZE', 'EXECUTE', 'HARDEN', 'SHIP', 'DISTILL', 'CLOSED']; + +const EXIT = { OK: 0, ERROR: 1, PARTIAL: 2 }; + +function die(message, code = EXIT.ERROR) { console.error(`PPGP: ${message}`); process.exit(code); } +function warn(message) { + console.error(`PPGP warning: ${message}`); +} + function parseArgs(argv) { const args = [...argv]; - const options = { root: process.cwd(), force: false }; + const options = { root: null, force: false }; const positional = []; while (args.length) { @@ -39,6 +74,27 @@ function parseArgs(argv) { return { options, positional }; } +function git(root, args) { + try { + return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + } catch { + return null; + } +} + +function hasGit(root) { + return git(root, ['rev-parse', '--is-inside-work-tree']) === 'true'; +} + +// Explicit --root wins. Otherwise the Git top-level of the working directory, so that a nested +// cwd still finds repository-level PPGP state. Outside Git, cwd itself is the root (portability). +function resolveRoot(options) { + if (options.root) return options.root; + const cwd = process.cwd(); + const top = git(cwd, ['rev-parse', '--show-toplevel']); + return top ? path.resolve(top) : cwd; +} + function findRole(root, role) { for (const candidate of ROLE_CANDIDATES[role] || []) { const absolute = path.join(root, candidate); @@ -51,15 +107,6 @@ function roleMap(root) { return Object.fromEntries(Object.keys(ROLE_CANDIDATES).map((role) => [role, findRole(root, role)])); } -function hasGit(root) { - try { - execFileSync('git', ['-C', root, 'rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' }); - return true; - } catch { - return false; - } -} - function printMap(root) { const roles = roleMap(root); console.log(`PPGP ${pkg.version} repository mapping`); @@ -80,19 +127,121 @@ function goalTemplate(outcome) { return `# ACTIVE_GOAL\n\n## GOAL\n${outcome}\n\n## WHY\nTODO: Why this goal matters.\n\n## PHASE\nTHINK\n\n## DEFINITION_OF_DONE\n- TODO: Define a verifiable completion condition.\n\n## FROZEN_DECISIONS\n- None yet.\n\n## INVARIANTS\n- None recorded yet.\n\n## VERIFIED_CURRENT_STATE\n- TODO: Verify current repository or runtime state.\n\n## COMPLETED\n- Nothing yet.\n\n## REMAINING\n- Define and execute the work required to reach the Definition of Done.\n\n## BLOCKERS\n- None currently known.\n\n## HUMAN_AUTHORITY_REQUIRED\n- None currently known.\n\n## VERIFICATION_EVIDENCE\n- None yet.\n\n## NEXT_EXECUTABLE_ACTION\n- Complete THINK and freeze the first executable plan.\n`; } -function parseSections(content) { - const sections = {}; - let current = null; - for (const line of content.split(/\r?\n/)) { - const match = line.match(/^##\s+([A-Z_]+)\s*$/); - if (match) { - current = match[1]; - sections[current] = []; - continue; +// --- parser ------------------------------------------------------------------------------- + +function normalizeName(raw) { + return raw.replace(/[*_`]+/g, ' ').replace(/:\s*$/, '').trim().toUpperCase() + .replace(/[\s-]+/g, '_').replace(/[^A-Z0-9_]/g, ''); +} + +function canonicalName(raw) { + const name = normalizeName(raw); + if (!name) return null; + if (SECTIONS.includes(name)) return name; + if (ALIASES[name]) return ALIASES[name]; + if (/(^|_)NEXT(_|$)/.test(name)) return 'NEXT_EXECUTABLE_ACTION'; + return null; +} + +// Recognized section starts, in order of precedence: +// 1. Markdown header at any level: ## GOAL / ### Definition of Done / # NEXT_EXECUTABLE_ACTION: +// 2. Bold-only line: **GOAL** +// 3. Upper-case key line: GOAL: text / FROZEN_DECISIONS: / DEFINITION_OF_DONE (note): +// Unknown upper-case keys containing an underscore also start a (retained) section. +// Names are matched case-insensitively after spaces and hyphens become underscores. +function matchSectionStart(line) { + let m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/); + if (m) return { level: m[1].length, raw: m[2].trim(), inline: '' }; + m = line.match(/^\*\*([^*]+?)\*\*\s*:?\s*$/); + if (m) return { level: 2, raw: m[1].trim(), inline: '' }; + m = line.match(/^([A-Z][A-Z0-9_]*(?:[ -][A-Z0-9_]+)*)(?:\s*\([^)]*\))?\s*:\s*(.*)$/); + if (m && m[1].length <= 40 && (canonicalName(m[1]) || m[1].includes("_"))) return { level: 2, raw: m[1].trim(), inline: m[2].trim() }; + return null; +} + +function parseActiveGoal(content) { + const result = { title: null, sections: {}, unknown: [], duplicates: [], starts: 0 }; + let current = null; // { name, level, lines } + const lines = content.split(/\r?\n/); + let inFence = false; + + lines.forEach((line, index) => { + if (/^\s*```/.test(line)) inFence = !inFence; + const start = inFence ? null : matchSectionStart(line); + if (start) { + const name = canonicalName(start.raw); + const deeperThanCurrent = current && start.level > current.level; + if (!name && start.level === 1 && result.starts === 0 && !result.title) { + result.title = start.raw; + return; + } + if (!name && deeperThanCurrent) { + current.lines.push(line); // sub-heading inside a section + return; + } + if (name && deeperThanCurrent && current.name === name) { + current.lines.push(line); + return; + } + result.starts += 1; + if (name && result.sections[name] !== undefined) { + result.duplicates.push({ name, line: index + 1 }); + current = { name: null, level: start.level, lines: [] }; // ignore the duplicate body + return; + } + current = { name, raw: start.raw, level: start.level, lines: [] }; + if (start.inline) current.lines.push(start.inline); + if (name) result.sections[name] = current; + else result.unknown.push(current); + return; } - if (current) sections[current].push(line); + if (current) current.lines.push(line); + }); + + const text = (entry) => entry.lines.join('\n').trim(); + result.sections = Object.fromEntries(Object.entries(result.sections).map(([k, v]) => [k, text(v)])); + result.unknown = result.unknown.map((entry) => ({ title: entry.raw, text: text(entry) })); + return result; +} + +function isEmptyish(value) { + const stripped = (value || '').replace(/^[-*\d.)\s]+/gm, '').trim(); + return !stripped || /^(none|nothing|n\/a|-)\b/i.test(stripped) && stripped.split(/\n/).length === 1; +} + +// Classifies parsed state: conformant | partial | malformed, plus human-readable warnings. +function analyze(parsed, content) { + const warnings = []; + const present = Object.keys(parsed.sections); + + if (!content.trim()) return { severity: 'malformed', reason: 'file is empty', warnings }; + const control = (content.match(/[^\t\r\n\x20-\x7E\u00A0-\uFFFF]/g) || []).length; + if (control > content.length * 0.05) return { severity: "malformed", reason: "file is not readable text", warnings }; + if (present.length === 0) { + const seen = parsed.unknown.map((u) => `"${u.title}"`).slice(0, 6).join(', '); + return { + severity: 'malformed', + reason: `no recognizable ACTIVE_GOAL sections (expected "## GOAL" style headers or "GOAL:" lines)${seen ? `; headers seen: ${seen}` : ''}`, + warnings + }; } - return Object.fromEntries(Object.entries(sections).map(([key, lines]) => [key, lines.join('\n').trim()])); + + const missing = SECTIONS.filter((s) => !present.includes(s)); + if (missing.length) warnings.push(`canonical field(s) missing (${missing.length} of ${SECTIONS.length}): ${missing.join(', ')}`); + for (const d of parsed.duplicates) warnings.push(`duplicate section ${d.name} at line ${d.line} ignored (first occurrence kept)`); + if (parsed.unknown.length) warnings.push(`unrecognized section(s) kept as-is: ${parsed.unknown.map((u) => `"${u.title}"`).join(', ')}`); + + const phase = (parsed.sections.PHASE || '').split(/\n/)[0].replace(/[*_`]/g, '').trim(); + const phaseWord = phase.toUpperCase().split(/[^A-Z]/)[0]; + if (phase && !PHASES.includes(phaseWord)) warnings.push(`PHASE "${phase}" is not a lifecycle phase name (${PHASES.join(' -> ')})`); + if (phaseWord === 'CLOSED') { + warnings.push('PHASE is CLOSED but ACTIVE_GOAL still exists; SPEC 3.4 requires deletion after verified closure and distillation. Verify the Definition of Done before treating this goal as closed.'); + if (!isEmptyish(parsed.sections.REMAINING)) warnings.push('contradiction: PHASE is CLOSED while REMAINING is not empty'); + } + const todo = (content.match(/\bTODO\b/g) || []).length; + if (todo) warnings.push(`${todo} scaffold TODO placeholder(s) still present`); + + return { severity: missing.length ? 'partial' : 'conformant', reason: null, warnings }; } function oneLine(value, fallback = '(not set)') { @@ -103,28 +252,90 @@ function oneLine(value, fallback = '(not set)') { function readActiveGoal(root) { const file = findRole(root, 'ACTIVE_GOAL'); if (!file) die('No ACTIVE_GOAL found. Start one with: ppgp goal ""'); - const absolute = path.join(root, file); - return { file, sections: parseSections(fs.readFileSync(absolute, 'utf8')) }; + const content = fs.readFileSync(path.join(root, file), 'utf8'); + const parsed = parseActiveGoal(content); + const health = analyze(parsed, content); + if (health.severity === 'malformed') die(`${file} is malformed: ${health.reason}`); + for (const w of health.warnings) warn(w); + return { file, sections: parsed.sections, parsed, health }; +} + +function exitFor(health) { + process.exitCode = health.severity === 'partial' ? EXIT.PARTIAL : EXIT.OK; } +// --- commands ----------------------------------------------------------------------------- + function cmdInit(root) { printMap(root); console.log('\nInitialization is non-destructive. Existing documentation is reused; no empty memory files are created.'); } +function gitSummary(root) { + const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']); + const sha = git(root, ['rev-parse', '--short', 'HEAD']); + const status = git(root, ['status', '--porcelain']); + const changed = status ? status.split(/\r?\n/).filter(Boolean).length : 0; + const where = branch === 'HEAD' ? `detached HEAD @ ${sha}` : `branch ${branch} @ ${sha || '(no commits)'}`; + return `Git: ${where}, working tree ${changed ? `${changed} changed path(s)` : 'clean'}`; +} + +// Lists refs whose tip contains an ACTIVE_GOAL candidate. Read-only; never switches or merges. +function activeGoalOnOtherRefs(root) { + const refs = (git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes']) || '') + .split(/\r?\n/).filter((r) => r && !/\/HEAD$/.test(r)).slice(0, 200); + const found = []; + for (const ref of refs) { + for (const candidate of ROLE_CANDIDATES.ACTIVE_GOAL) { + if (git(root, ['cat-file', '-e', `${ref}:${candidate}`]) !== null) { + const date = git(root, ['log', '-1', '--format=%cs', ref, '--', candidate]) || '?'; + found.push(`${ref}:${candidate} (last change ${date})`); + break; + } + } + } + return found; +} + function cmdDoctor(root) { const roles = 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.'); - console.log(issues.length ? `\nNotes:\n- ${issues.join('\n- ')}` : '\nNo obvious repository-level PPGP issues detected.'); + const notes = []; + const gitAvailable = hasGit(root); + if (gitAvailable) console.log(gitSummary(root)); + else notes.push('Git forensic history was not detected.'); + + if (!roles.ACTIVE_GOAL) { + const elsewhere = gitAvailable ? activeGoalOnOtherRefs(root) : []; + if (elsewhere.length) { + notes.push(`No ACTIVE_GOAL in this checkout, but other refs carry one: ${elsewhere.join('; ')}. Not switching branches. Inspect with: git show :. Verify it is current before treating it as the active goal.`); + } else { + notes.push('No ACTIVE_GOAL is present. This is normal when no substantial goal is active.'); + } + } else { + const content = fs.readFileSync(path.join(root, roles.ACTIVE_GOAL), 'utf8'); + const health = analyze(parseActiveGoal(content), content); + if (health.severity === 'malformed') notes.push(`${roles.ACTIVE_GOAL} is malformed: ${health.reason}`); + for (const w of health.warnings) notes.push(`${roles.ACTIVE_GOAL}: ${w}`); + if (health.severity === 'malformed') process.exitCode = EXIT.ERROR; + else if (health.severity === 'partial') process.exitCode = EXIT.PARTIAL; + } + console.log(notes.length ? `\nNotes:\n- ${notes.join('\n- ')}` : '\nNo obvious repository-level PPGP issues detected.'); +} + +function timestamp() { + return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z'); } function cmdGoal(root, positional, force) { 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)) { + if (!force) die(`${path.relative(root, target)} already exists. Use --force only when intentionally replacing the active goal.`); + const backup = `${target}.${timestamp()}.bak`; + fs.copyFileSync(target, backup); + console.log(`Previous ACTIVE_GOAL preserved at ${path.relative(root, backup)}`); + } fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, goalTemplate(outcome), 'utf8'); console.log(`Created ${path.relative(root, target)} in THINK phase.`); @@ -132,9 +343,10 @@ function cmdGoal(root, positional, force) { } function cmdStatus(root) { - const { file, sections } = readActiveGoal(root); + const { file, sections, parsed, health } = readActiveGoal(root); console.log(`PPGP/${pkg.version} status from ${file}`); - console.log(`goal: ${oneLine(sections.GOAL)}`); + if (!sections.GOAL && parsed.title) console.log(`goal: (no GOAL section) title: ${parsed.title}`); + else console.log(`goal: ${oneLine(sections.GOAL)}`); console.log(`phase: ${oneLine(sections.PHASE)}`); console.log(`frozen: ${oneLine(sections.FROZEN_DECISIONS)}`); console.log(`verified: ${oneLine(sections.VERIFIED_CURRENT_STATE)}`); @@ -143,10 +355,13 @@ function cmdStatus(root) { console.log(`authority: ${oneLine(sections.HUMAN_AUTHORITY_REQUIRED)}`); console.log(`next: ${oneLine(sections.NEXT_EXECUTABLE_ACTION)}`); console.log(`evidence: ${oneLine(sections.VERIFICATION_EVIDENCE)}`); + if (parsed.unknown.length) console.log(`unrecognized: ${parsed.unknown.map((u) => u.title).join(' | ')}`); + console.log(`state: ${health.severity}${health.warnings.length ? ` (${health.warnings.length} warning(s) on stderr)` : ''}`); + exitFor(health); } function cmdHandoff(root) { - const { sections } = readActiveGoal(root); + const { sections, health } = readActiveGoal(root); console.log(`PPGP/${pkg.version}`); console.log(`G=${oneLine(sections.GOAL)}`); console.log(`P=${oneLine(sections.PHASE)}`); @@ -155,6 +370,7 @@ function cmdHandoff(root) { console.log(`B:${oneLine(sections.BLOCKERS)}`); console.log(`E:${oneLine(sections.VERIFICATION_EVIDENCE)}`); console.log(`N:${oneLine(sections.NEXT_EXECUTABLE_ACTION)}`); + exitFor(health); } function cmdSkillPath() { @@ -174,7 +390,7 @@ 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 [--root PATH] [--force]\n ppgp status [--root PATH]\n ppgp handoff [--root PATH]\n ppgp skill-path\n ppgp install-skill \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\nUsage:\n ppgp init [--root PATH]\n ppgp doctor [--root PATH]\n ppgp goal [--root PATH] [--force]\n ppgp status [--root PATH]\n ppgp handoff [--root PATH]\n ppgp skill-path\n ppgp install-skill \n ppgp --version\n\nRoot: --root, else the Git top-level of the working directory, else the working directory.\n\nACTIVE_GOAL sections are recognized as "## GOAL" headers at any level, "**GOAL**" bold lines, or\n"GOAL:" upper-case key lines; names are matched case-insensitively (spaces/hyphens as underscores).\n\nExit codes: 0 ok (warnings may be printed on stderr), 1 error or malformed state, 2 partial state\n(GOAL or NEXT_EXECUTABLE_ACTION could not be found).\n\nThe handoff packet supplements the repository-visible ACTIVE_GOAL; it never replaces it.\n\nThe CLI is a deterministic companion to the PPGP protocol. It does not replace agent reasoning, verification, distillation, or closure checks.\n`); } const raw = process.argv.slice(2); @@ -189,14 +405,15 @@ if (raw.includes('--version') || raw.includes('-v')) { const command = raw.shift(); const { options, positional } = parseArgs(raw); -if (!fs.existsSync(options.root) || !fs.statSync(options.root).isDirectory()) die(`Root is not a directory: ${options.root}`); +const root = resolveRoot(options); +if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) die(`Root is not a directory: ${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 'init': cmdInit(root); break; + case 'doctor': cmdDoctor(root); break; + case 'goal': cmdGoal(root, positional, options.force); break; + case 'status': cmdStatus(root); break; + case 'handoff': cmdHandoff(root); break; case 'skill-path': cmdSkillPath(); break; case 'install-skill': cmdInstallSkill(positional); break; default: die(`Unknown command: ${command}. Run ppgp --help.`); diff --git a/dist/ppgp-v0.1.zip b/dist/ppgp-v0.1.zip deleted file mode 100644 index 40719df..0000000 Binary files a/dist/ppgp-v0.1.zip and /dev/null differ diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md new file mode 100644 index 0000000..74fdc0f --- /dev/null +++ b/docs/COMPATIBILITY.md @@ -0,0 +1,49 @@ +# PPGP Platform Compatibility + +One canonical protocol skill lives at `skills/ppgp/`; platform adapters are thin discovery manifests around it. This page records what has actually been verified, how, and when. External platforms change independently of PPGP, so every row ages from its **Last verified** date; treat older rows as claims to re-check, not as guarantees. + +Status vocabulary: + +- **VERIFIED CLIENT**: install, discovery and invocation were manually exercised in the real client. +- **VERIFIED FORMAT**: the repository artifact matches the platform's documented format and is covered by `npm test`. +- **REPOSITORY NATIVE**: the platform documents discovery of the committed path; discovery itself was not exercised in that client. +- **IMPORT READY**: the platform can import the canonical public skill; no client-side smoke test was run. +- **STRUCTURALLY READY**: manifests are present and validated by `npm test`; no client-side smoke test was run. +- **DOCUMENTATION ONLY**: no stable adapter was added. + +Evidence types: *manual client test* (a person used the client), *structural validation* (`npm test` parses and cross-checks the manifests), *documentation review* (platform documentation read, nothing executed). + +## Matrix + +| Platform | Native mechanism | PPGP artifact | Status | Evidence type | Last verified | Client version | Remaining external action | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Anthropic Claude / Claude Code | Plugin + self-hosted marketplace | `.claude-plugin/marketplace.json`, `plugins/ppgp/` | VERIFIED CLIENT | manual client test | 2026-08-25 (install, updated-skill load, `/ppgp` invocation); 2026-08-26 (invocation-form notes) | not recorded | Public Anthropic listing not claimed | +| OpenAI Codex | Plugin + repo marketplace | `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json`, `skills/ppgp/` | STRUCTURALLY READY | structural validation | 2026-08-25 | n/a | Import/test in Codex; Plugin Directory listing is external | +| OpenAI ChatGPT | Agent Skills / skill-only plugin | `skills/ppgp/`, Codex plugin package | IMPORT READY | documentation review | 2026-08-25 | n/a | Upload/import the skill; directory availability is external | +| Google Gemini CLI | Gemini Extension + Agent Skills | `gemini-extension.json`, `skills/ppgp/` | STRUCTURALLY READY | structural validation | 2026-08-25 | n/a | Run `gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update` | +| Cursor | Agent Plugins + Agent Skills | `plugin.json`, `skills/ppgp/` | STRUCTURALLY READY | structural validation | 2026-08-25 | n/a | Local plugin smoke test; marketplace publication external | +| GitHub Copilot | Agent Skills | `.agents/skills/ppgp/` mirror | REPOSITORY NATIVE | documentation review | 2026-08-25 | n/a | Open a repo with Copilot and verify discovery | +| Windsurf | Agent Skills | `.agents/skills/ppgp/` mirror | REPOSITORY NATIVE | documentation review | 2026-08-25 | n/a | Open a repo with Windsurf and verify discovery | +| Devin | Agent Skills | `.agents/skills/ppgp/` mirror | REPOSITORY NATIVE | documentation review | 2026-08-25 | n/a | Connect the repo in Devin and verify discovery | +| Kiro | Agent Skills import | canonical `skills/ppgp/` | IMPORT READY | documentation review | 2026-08-25 | n/a | Import the public skill in Kiro | +| Cline | Agent Skills | canonical `skills/ppgp/` | IMPORT READY | documentation review | 2026-08-25 | n/a | Copy into a supported skills directory and smoke-test | +| JetBrains Junie | Agent Skills | canonical `skills/ppgp/` | IMPORT READY | documentation review | 2026-08-25 | n/a | Import into Junie's skills location and smoke-test | +| Roo Code | depends on installed client | canonical `skills/ppgp/` | DOCUMENTATION ONLY | none | — | n/a | Confirm the installed version's skill discovery path first | +| Amazon Q Developer | no stable adapter validated | canonical protocol usable manually | DOCUMENTATION ONLY | none | — | n/a | Re-evaluate when a stable skill/plugin surface is confirmed | + +Dates come from the repository history of this file and the adapter commits (matrix and adapters added 2026-08-25; Claude client verification recorded 2026-08-25; Windows and Claude invocation notes 2026-08-26). Structural validation re-runs on every `npm test`, but that only confirms manifest shape, not client behaviour. + +## Verification snapshot + +- Claude self-hosted marketplace: install, updated-skill loading and unnamespaced `/ppgp` invocation manually verified 2026-08-25 in one Claude client whose version was not recorded; that client did not expose `/reload-plugins`. Claude Code may expose `/ppgp:ppgp`. Nothing has been re-verified since. +- Codex/OpenAI, Cursor and Gemini: manifests validated structurally on every test run; no client smoke test has been performed. +- Copilot, Windsurf, Devin, Kiro, Cline, Junie: documentation-based readiness only. + +## Adapter principles + +1. Protocol semantics remain vendor-neutral; a manifest describes PPGP but never forks it. +2. Prefer direct use of `skills/ppgp/`; when a second path is required, keep it a deterministic drift-tested mirror. +3. Readiness, submission, approval and public listing are distinct states; a manifest is not vendor endorsement. +4. Repository-backed Claude plugin refresh follows repository revisions, not a pinned adapter version. +5. Record invocation forms from the exact tested client surface; do not generalize one slash-command form. +6. When a row is re-verified, update its date and evidence type; do not refresh a date because the file was edited. diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md new file mode 100644 index 0000000..1bd53a2 --- /dev/null +++ b/docs/DISTRIBUTION.md @@ -0,0 +1,102 @@ +# PPGP Distribution + +How PPGP reaches users and agent platforms. The protocol and canonical Agent Skill are vendor-neutral; every platform manifest is a thin adapter for discovery and installation. + +## Source version versus published release + +These are different facts and this repository records them in different places. + +| Fact | Canonical source | +| --- | --- | +| Source version of this tree | `version` in `package.json`; mirrored in `SPEC.md`, `skills/ppgp/SKILL.md`, `CITATION.cff`, adapter manifests | +| Latest published GitHub Release | https://github.com/Fatboy-coder/ppgp/releases/latest | +| Latest published npm package | https://www.npmjs.com/package/@fatboy-coder/ppgp | +| Whether and when the source version was published | GitHub Releases and npm only; the source tree never records it | +| Historical releases | `CHANGELOG.md` and GitHub Releases | + +A source tree may carry a version that is not yet published. Documents in this repository therefore never hard-code a future download asset or package version; they link to the durable release and package pages instead. + +## Canonical skill and mirrors + +```text +skills/ppgp/SKILL.md +skills/ppgp/references/PPGP.md +``` + +Generated, byte-identical mirrors exist for platforms that discover other paths: + +```text +.agents/skills/ppgp/ cross-agent .agents/skills convention (Copilot, Windsurf, Devin) +plugins/ppgp/skills/ppgp/ Claude plugin package (Claude copies plugins into its cache) +``` + +Regenerate with `node scripts/sync-skill-mirror.js`; `npm test` fails on drift. Never edit a mirror directly. + +## Routes + +Universal Agent Skills route (preferred): + +```bash +npx skills add https://github.com/Fatboy-coder/ppgp/tree/main/skills/ppgp +``` + +Manual: open the latest GitHub Release, download its `ppgp-v.zip` (a SHA-256 file is published beside it), extract, and copy the `ppgp` directory into the client's skills location. + +CLI, zero-install: + +```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 +``` + +Global install keeps the short executable: `npm install -g @fatboy-coder/ppgp`, then `ppgp --version`. To pin an exact historical version for reproducibility, append `@` using a version listed on the npm page. + +Inside this repository the source CLI runs without installation: `node ./bin/ppgp.js --version`. + +### Windows note + +Some npm versions route `npm` through the `npm.ps1` wrapper and mis-forward arguments, so `npm exec … -- ppgp --version` can print the npm version. Use `npm.cmd exec --yes --package=@fatboy-coder/ppgp -- ppgp --version` to bypass the wrapper. CI packs and installs the package on Windows and verifies the generated `ppgp.cmd` shim. + +### Platform adapters + +| Platform | Adapter | Notes | +| --- | --- | --- | +| Claude / Claude Code | `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `plugins/ppgp/` | Plugins → Add marketplace → `Fatboy-coder/ppgp` → install `ppgp`. Invocation is `/ppgp`, or `/ppgp:ppgp` where Claude Code namespaces plugin skills. `/reload-plugins` exists only on some surfaces. The plugin manifest pins no version; refresh follows repository revisions. | +| OpenAI Codex / ChatGPT | `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json` | Skill-only plugin pointing at canonical `skills/`. Public Plugin Directory listing is a vendor-side step. | +| Google Gemini CLI | `gemini-extension.json` | `gemini extensions install https://github.com/Fatboy-coder/ppgp --auto-update`. | +| Cursor | `plugin.json` (Agent Plugins format) | Schema-safe manifest over canonical `skills/`. Marketplace publication is external. | +| GitHub Copilot, Windsurf, Devin | `.agents/skills/ppgp/` mirror | Repository-native discovery. | +| Kiro, Cline, JetBrains Junie | canonical `skills/ppgp/` | Import or copy the public skill; no platform-specific copy is maintained. | +| Roo Code, Amazon Q Developer | none | An adapter is added only once the platform exposes a stable, testable mechanism. | + +Repository packaging never implies vendor endorsement, submission, approval or public listing. Verification state and dates per platform are in [`COMPATIBILITY.md`](./COMPATIBILITY.md). + +Adapter manifests are repository distribution surfaces; they are excluded from the npm package, which bundles the CLI, the canonical skill, the specification, the evaluation guide and the benchmark protocol and tooling. + +## Publishing + +One guarded manual workflow starts a release; the package workflows chain from it: + +```text +Publish PPGP release (.github/workflows/publish-release.yml) + ↓ workflow_run +Publish PPGP to npm (.github/workflows/publish-npm.yml, npm Trusted Publishing / OIDC) + ↓ workflow_run +Publish PPGP to GitHub Packages +``` + +The release workflow refuses a version that does not equal the committed `package.json` version, then creates the immutable tag, the GitHub Release and the `ppgp-v.zip` asset. Downstream workflows re-check the release and package version before publishing. Manual dispatches exist to republish a package after an infrastructure failure. No long-lived npm token is required. + +The tree tagged by the release is immutable, so it never embeds publication state: `CHANGELOG.md` uses a bare `## ` heading and `CITATION.cff` carries no `date-released`. If a publish step fails after the merge, nothing in `main` is wrong; GitHub Releases and npm simply do not list the version yet. `npm test` enforces that rule. + +## What `npm test` validates about distribution + +- canonical skill and compact reference exist and mirrors are byte-identical; +- Claude, Codex, Agent Plugin and Gemini manifests parse and name `ppgp`; +- versioned adapters, skill metadata, specification title, citation, benchmark protocol and CLI headers agree on the `package.json` version; +- no active document hard-codes an unpublished release asset or package version; +- adapter directories do not enter the npm payload; +- packing and installing the package produces a working `ppgp` binary on Linux and Windows. diff --git a/EVALUATION.md b/docs/EVALUATION.md similarity index 91% rename from EVALUATION.md rename to docs/EVALUATION.md index 4fd1bdb..c1a633f 100644 --- a/EVALUATION.md +++ b/docs/EVALUATION.md @@ -1,10 +1,10 @@ # Evaluating PPGP -PPGP v0.1.2 is experimental. Independent tests, failures, replications and comparative evaluations are welcome. +PPGP 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. -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, use [`benchmarks/PROTOCOL.md`](../benchmarks/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`. ## Minimum evaluation record @@ -197,7 +197,7 @@ Apply the same interruption point where practical, then compare: - final verified outcome; - state-maintenance overhead. -For a reproducible paired design, metric definitions, exclusion rules, randomization guidance and reporting format, follow [`BENCHMARK_PROTOCOL.md`](./BENCHMARK_PROTOCOL.md). +For a reproducible paired design, metric definitions, exclusion rules, randomization guidance and reporting format, follow [`benchmarks/PROTOCOL.md`](../benchmarks/PROTOCOL.md). Report meaningful differences in setup. Avoid presenting a single repository or model as universal evidence. @@ -207,6 +207,6 @@ For a focused failure or reproducible observation, open an issue using the relev 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. -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. +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. Negative results are welcome. A simpler approach that preserves recovery quality is a useful contribution. diff --git a/gemini-extension.json b/gemini-extension.json index 2675836..618804e 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,5 +1,5 @@ { "name": "ppgp", - "version": "0.1.2", + "version": "0.1.3", "description": "Portable continuity protocol for long-running coding agents." } diff --git a/package.json b/package.json index 6a25dea..47f37ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fatboy-coder/ppgp", - "version": "0.1.2", + "version": "0.1.3", "description": "Portable Persistent Goal Protocol CLI and Agent Skill for long-running coding-agent continuity.", "license": "MIT", "author": "Hervey (Fatboy-coder)", @@ -33,8 +33,7 @@ "scripts/prepare-pilot-01.js", "benchmarks/", "SPEC.md", - "EVALUATION.md", - "BENCHMARK_PROTOCOL.md", + "docs/EVALUATION.md", "CITATION.cff", "LICENSE", "README.md" @@ -43,7 +42,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/hardening.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..7874d44 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "ppgp", - "version": "0.1.2", + "version": "0.1.3", "description": "Portable continuity protocol for long-running coding agents.", "author": { "name": "Hervey (Fatboy-coder)" diff --git a/plugins/ppgp/skills/ppgp/SKILL.md b/plugins/ppgp/skills/ppgp/SKILL.md index 258dd94..9f0ba7f 100644 --- a/plugins/ppgp/skills/ppgp/SKILL.md +++ b/plugins/ppgp/skills/ppgp/SKILL.md @@ -5,7 +5,7 @@ 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." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.1.3" protocol: PPGP --- @@ -21,13 +21,13 @@ When asked what PPGP is, who developed it, where it lives, or whether it is empi - Canonical repository: `https://github.com/Fatboy-coder/ppgp` - Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository +- Evaluation guide: `docs/EVALUATION.md` in the canonical repository - Citation metadata: `CITATION.cff` in the canonical repository - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` +- Source protocol version of this skill: `0.1.3` (published releases: https://github.com/Fatboy-coder/ppgp/releases) -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. +PPGP v0.1.3 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. 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. @@ -139,7 +139,7 @@ Before another agent or session takes over: Prefer: ```text -PPGP/0.1.2 +PPGP/0.1.3 G= P= F: @@ -151,6 +151,8 @@ N: Do not dump the conversation transcript. +The packet supplements ACTIVE_GOAL; it never replaces it. The receiving agent needs the repository, the current ACTIVE_GOAL and the packet. Do not expand the packet into a context dump; update ACTIVE_GOAL instead. + ### `ppgp distill` At the end of a goal or after major state accumulation: @@ -186,6 +188,22 @@ Close only when the synchronous Definition of Done is verified. Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +## Writing ACTIVE_GOAL so tools can read it + +Name fields as `## GOAL` style headers (any level), `**GOAL**` bold lines, or `GOAL:` upper-case key lines. Case, spacing and hyphens do not matter; extra sections are kept and reported. A file is conformant only with all thirteen fields; anything less is partial and the CLI names what is missing (exit 2). Prefer one substantial end-to-end goal over a trivial task; put sub-steps in DEFINITION_OF_DONE, COMPLETED and REMAINING. + +## Parking deferred work + +Keep one ACTIVE_GOAL. To defer a goal without closing it: bring ACTIVE_GOAL to verified truth, record the goal under ROADMAP as deferred with a resume condition and a pointer to its preserved state (last commit or a dated doc), then replace ACTIVE_GOAL. On resume, re-instantiate and re-verify. + +## Blocker scope + +State the smallest true scope in BLOCKERS ("Step A only: ...; steps B and C are not blocked") and keep NEXT_EXECUTABLE_ACTION on safe work. Continue independent work before escalating. + +## Goal state across branches + +ACTIVE_GOAL is read from the checked-out ref. If it lives on a topic branch, name that branch in ROADMAP on the integration branch. `ppgp doctor` lists other refs carrying an ACTIVE_GOAL but never switches or merges. + ## Human escalation Solve reversible technical decisions autonomously. diff --git a/plugins/ppgp/skills/ppgp/references/PPGP.md b/plugins/ppgp/skills/ppgp/references/PPGP.md index e4a6df2..a3dfbdb 100644 --- a/plugins/ppgp/skills/ppgp/references/PPGP.md +++ b/plugins/ppgp/skills/ppgp/references/PPGP.md @@ -1,4 +1,4 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.1.3 Compact Reference ## Objective @@ -52,6 +52,8 @@ NEXT_EXECUTABLE_ACTION Write current state, not a diary. +All thirteen fields present = conformant. Fewer = partial; tools name the missing fields. Fields may be headers, bold lines or `KEY:` lines. + ## Recovery Load the smallest sufficient boot packet: @@ -74,6 +76,16 @@ C authority boundary -> escalate minimally D hard dependency -> escalate if no safe autonomous path ``` +Scope blockers to the smallest true step in prose; continue unblocked work. + +## Parking + +One ACTIVE_GOAL. Defer a goal by recording it under ROADMAP with a resume condition and a pointer to its preserved state, then replace ACTIVE_GOAL. Re-verify on resume. + +## Refs + +ACTIVE_GOAL is read from the checked-out ref. Name a topic-branch goal in ROADMAP on the integration branch. + ## Evidence Default technical precedence: @@ -94,6 +106,8 @@ runtime/production Prefer deltas and compact structured state over transcript replay. +The packet supplements ACTIVE_GOAL; it never replaces it. Receiver needs repository + ACTIVE_GOAL + packet. + Keep the handoff human-auditable and cross-model readable. Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. diff --git a/ADVERSARIAL_VALIDATION.md b/research/2026-09-23-adversarial-validation.md similarity index 100% rename from ADVERSARIAL_VALIDATION.md rename to research/2026-09-23-adversarial-validation.md diff --git a/research/README.md b/research/README.md new file mode 100644 index 0000000..44623fe --- /dev/null +++ b/research/README.md @@ -0,0 +1,16 @@ +# Research and evidence records + +Dated, non-normative records behind PPGP's claims. Nothing here changes the protocol; `SPEC.md` is normative. Each record names the exact version it examined, its method, its limitations and what it did not show. Records are never edited to match later truth; a newer record supersedes an older one by date. + +| Date | Record | Examined | Kind | +| --- | --- | --- | --- | +| 2026-09-23 | [`2026-09-23-adversarial-validation.md`](./2026-09-23-adversarial-validation.md) | `@fatboy-coder/ppgp@0.1.2` | maintainer-run adversarial validation, 18 scenarios | + +Reproduction material: + +- `test/fixtures/` — ACTIVE_GOAL variants derived from the 2026-09-23 campaign, exercised by `test/hardening.test.js` on every `npm test`. +- `benchmarks/` — paired A/B recovery protocol, result schema, deterministic pilot fixture. +- `docs/EVALUATION.md` — what to record when evaluating PPGP. +- Branch `research/v0.2-concurrency-experiment` — a richer coordination model implemented in 2026-08 and preserved as research; not a release path. + +Vocabulary used in these records, in increasing strength: maintainer-tested, adversarially exercised under documented scenarios, reproducible methodology, independent validation, peer review, formal verification. Only the first three apply to PPGP today. diff --git a/skills/ppgp/SKILL.md b/skills/ppgp/SKILL.md index 258dd94..9f0ba7f 100644 --- a/skills/ppgp/SKILL.md +++ b/skills/ppgp/SKILL.md @@ -5,7 +5,7 @@ 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." metadata: author: Fatboy-coder - version: "0.1.2" + version: "0.1.3" protocol: PPGP --- @@ -21,13 +21,13 @@ When asked what PPGP is, who developed it, where it lives, or whether it is empi - Canonical repository: `https://github.com/Fatboy-coder/ppgp` - Public specification: `SPEC.md` in the canonical repository -- Evaluation guide: `EVALUATION.md` in the canonical repository +- Evaluation guide: `docs/EVALUATION.md` in the canonical repository - Citation metadata: `CITATION.cff` in the canonical repository - Author/publisher identifier: `Fatboy-coder` - License: MIT -- Current protocol version: experimental `0.1.2` +- Source protocol version of this skill: `0.1.3` (published releases: https://github.com/Fatboy-coder/ppgp/releases) -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. +PPGP v0.1.3 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. 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. @@ -139,7 +139,7 @@ Before another agent or session takes over: Prefer: ```text -PPGP/0.1.2 +PPGP/0.1.3 G= P= F: @@ -151,6 +151,8 @@ N: Do not dump the conversation transcript. +The packet supplements ACTIVE_GOAL; it never replaces it. The receiving agent needs the repository, the current ACTIVE_GOAL and the packet. Do not expand the packet into a context dump; update ACTIVE_GOAL instead. + ### `ppgp distill` At the end of a goal or after major state accumulation: @@ -186,6 +188,22 @@ Close only when the synchronous Definition of Done is verified. Do not wait for asynchronous external observations unless Definition of Done explicitly requires them. +## Writing ACTIVE_GOAL so tools can read it + +Name fields as `## GOAL` style headers (any level), `**GOAL**` bold lines, or `GOAL:` upper-case key lines. Case, spacing and hyphens do not matter; extra sections are kept and reported. A file is conformant only with all thirteen fields; anything less is partial and the CLI names what is missing (exit 2). Prefer one substantial end-to-end goal over a trivial task; put sub-steps in DEFINITION_OF_DONE, COMPLETED and REMAINING. + +## Parking deferred work + +Keep one ACTIVE_GOAL. To defer a goal without closing it: bring ACTIVE_GOAL to verified truth, record the goal under ROADMAP as deferred with a resume condition and a pointer to its preserved state (last commit or a dated doc), then replace ACTIVE_GOAL. On resume, re-instantiate and re-verify. + +## Blocker scope + +State the smallest true scope in BLOCKERS ("Step A only: ...; steps B and C are not blocked") and keep NEXT_EXECUTABLE_ACTION on safe work. Continue independent work before escalating. + +## Goal state across branches + +ACTIVE_GOAL is read from the checked-out ref. If it lives on a topic branch, name that branch in ROADMAP on the integration branch. `ppgp doctor` lists other refs carrying an ACTIVE_GOAL but never switches or merges. + ## Human escalation Solve reversible technical decisions autonomously. diff --git a/skills/ppgp/references/PPGP.md b/skills/ppgp/references/PPGP.md index e4a6df2..a3dfbdb 100644 --- a/skills/ppgp/references/PPGP.md +++ b/skills/ppgp/references/PPGP.md @@ -1,4 +1,4 @@ -# PPGP v0.1.2 Compact Reference +# PPGP v0.1.3 Compact Reference ## Objective @@ -52,6 +52,8 @@ NEXT_EXECUTABLE_ACTION Write current state, not a diary. +All thirteen fields present = conformant. Fewer = partial; tools name the missing fields. Fields may be headers, bold lines or `KEY:` lines. + ## Recovery Load the smallest sufficient boot packet: @@ -74,6 +76,16 @@ C authority boundary -> escalate minimally D hard dependency -> escalate if no safe autonomous path ``` +Scope blockers to the smallest true step in prose; continue unblocked work. + +## Parking + +One ACTIVE_GOAL. Defer a goal by recording it under ROADMAP with a resume condition and a pointer to its preserved state, then replace ACTIVE_GOAL. Re-verify on resume. + +## Refs + +ACTIVE_GOAL is read from the checked-out ref. Name a topic-branch goal in ROADMAP on the integration branch. + ## Evidence Default technical precedence: @@ -94,6 +106,8 @@ runtime/production Prefer deltas and compact structured state over transcript replay. +The packet supplements ACTIVE_GOAL; it never replaces it. Receiver needs repository + ACTIVE_GOAL + packet. + Keep the handoff human-auditable and cross-model readable. Do not require gibberish, hidden-state communication, embeddings, MCP or a particular vendor. diff --git a/test/cli.test.js b/test/cli.test.js index b231fc6..4070404 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -85,34 +85,41 @@ try { 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 currentVersionChecks = [ - ['README.md', `**Status:** Experimental v${version}`], - ['README.md', `ppgp-v${version}.zip`], + // Source-version artifacts must agree on the package.json version. This says nothing about publication. + const sourceVersionChecks = [ + ['README.md', `**Source version:** v${version}`], ['SPEC.md', `# PPGP Specification v${version}`], - ['EVALUATION.md', `PPGP v${version} is experimental.`], - ['CONTRIBUTING.md', `PPGP v${version} is intentionally provisional.`], - ['COMPATIBILITY.md', `PPGP v${version} remains experimental`], - ['DISTRIBUTION.md', `PPGP specification ${version}`], - ['DISTRIBUTION.md', `@fatboy-coder/ppgp@${version}`], + ['CHANGELOG.md', `## ${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/PROTOCOL.md', `Protocol under test: PPGP v${version}`], ['benchmarks/examples/pair-001-ppgp.json', `"ppgpVersion": "${version}"`], ]; - for (const [file, expected] of currentVersionChecks) { - assert(readText(file).includes(expected), `${file} is not aligned with current version ${version}: missing ${expected}`); + for (const [file, expected] of sourceVersionChecks) { + assert(readText(file).includes(expected), `${file} is not aligned with source version ${version}: missing ${expected}`); } - const readme = readText('README.md'); - const distribution = readText('DISTRIBUTION.md'); + // Publication state is never source truth. The source tree carries only its version; whether and when that + // version was published is answered by GitHub Releases and npm. A tagged tree is immutable, so it must not + // embed a publication date or a candidate marker that would go stale after the release. + assert(new RegExp(`^## ${version.replace(/\./g, '\\.')}\\s*$`, 'm').test(readText('CHANGELOG.md')), `CHANGELOG.md entry for ${version} must be a bare "## ${version}" heading; publication dates live on GitHub Releases`); + assert(!/^date-released:/m.test(readText('CITATION.cff')), 'CITATION.cff must not carry date-released; the release date is canonical on GitHub Releases'); + for (const file of ['README.md', 'docs/DISTRIBUTION.md', 'docs/COMPATIBILITY.md', 'docs/EVALUATION.md', 'CONTRIBUTING.md', 'ROADMAP.md', 'SPEC.md', 'skills/ppgp/SKILL.md', 'benchmarks/PROTOCOL.md']) { + const text = readText(file); + assert(!/releases\/latest\/download\/ppgp-v/.test(text), `${file} hard-codes a versioned latest-download URL; link to releases/latest instead`); + assert(!text.includes(`ppgp-v${version}.zip`), `${file} names a release asset for the source version; link to releases/latest instead`); + assert(!text.includes(`@fatboy-coder/ppgp@${version}`), `${file} pins the source version as an npm install target; link to the npm page instead`); + } + + const distribution = readText('docs/DISTRIBUTION.md'); const releaseWorkflow = readText('.github/workflows/publish-release.yml'); - assert(!readme.includes('releases/latest/download/ppgp-v0.1.zip'), 'README still points at stale ppgp-v0.1.zip alias'); 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'); + assert(!fs.existsSync(path.join(repo, 'dist')), 'generated release archives must not be tracked under dist/'); 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'); diff --git a/test/fixtures/blocker-scope.md b/test/fixtures/blocker-scope.md new file mode 100644 index 0000000..05d5236 --- /dev/null +++ b/test/fixtures/blocker-scope.md @@ -0,0 +1,44 @@ +# ACTIVE_GOAL + +## GOAL +Migrate outbound email from provider X to provider Y. + +## WHY +Provider X is being discontinued 2026-11-01. + +## PHASE +EXECUTE + +## DEFINITION_OF_DONE +- Step A: production sends via Y (requires Y API key in the prod env). +- Step B: mail templates rendered by the new adapter, snapshot tests pass. +- Step C: bounce webhook from Y handled and tested. + +## FROZEN_DECISIONS +- Adapter pattern; provider chosen by MAIL_PROVIDER env. + +## INVARIANTS +- No customer email may be sent from a non-production environment. + +## VERIFIED_CURRENT_STATE +- main at 77aa00b; adapter skeleton exists; 0 templates ported. + +## COMPLETED +- Adapter interface + X adapter extracted. + +## REMAINING +- Step A (blocked, see BLOCKERS). +- Step B (independent of A; safe to do now). +- Step C (independent of A; safe to do now). + +## BLOCKERS +- Step A only: Type C authority. Owner must create the Y account and place the API key in prod env. Steps B and C are NOT blocked. + +## HUMAN_AUTHORITY_REQUIRED +- Owner: create provider Y account, provision key (Step A). + +## VERIFICATION_EVIDENCE +- None yet. + +## NEXT_EXECUTABLE_ACTION +- Step B: port templates/welcome.html to the adapter and add snapshot test. diff --git a/test/fixtures/contradictory.md b/test/fixtures/contradictory.md new file mode 100644 index 0000000..50315fa --- /dev/null +++ b/test/fixtures/contradictory.md @@ -0,0 +1,41 @@ +# ACTIVE_GOAL + +## GOAL +Rename the service from Foo to Bar everywhere. + +## WHY +Brand change. + +## PHASE +CLOSED + +## DEFINITION_OF_DONE +- No occurrence of "Foo" remains in src/, docs/ or the deployed site. + +## FROZEN_DECISIONS +- None. + +## INVARIANTS +- None. + +## VERIFIED_CURRENT_STATE +- Deployed site still shows "Foo" in the footer (observed 2026-09-22). + +## COMPLETED +- All occurrences renamed and deployed. + +## REMAINING +- Footer still says Foo. + +## BLOCKERS +- None. + +## HUMAN_AUTHORITY_REQUIRED +- None. + +## VERIFICATION_EVIDENCE +- grep -r Foo src/ returns nothing. + +## NEXT_EXECUTABLE_ACTION +- Goal complete; nothing to do. +- Fix the footer. diff --git a/test/fixtures/false-completion.md b/test/fixtures/false-completion.md new file mode 100644 index 0000000..6cf5664 --- /dev/null +++ b/test/fixtures/false-completion.md @@ -0,0 +1,44 @@ +# ACTIVE_GOAL + +## GOAL +Add CSV export to the reports page. + +## WHY +Customers asked for it. + +## PHASE +CLOSED + +## DEFINITION_OF_DONE +- Export button downloads a CSV with all visible rows. +- CSV opens correctly in Excel (UTF-8 BOM, CRLF). +- Works for a report with 50,000 rows without timing out. + +## FROZEN_DECISIONS +- Stream the response; no temp file. + +## INVARIANTS +- No new dependency. + +## VERIFIED_CURRENT_STATE +- Branch feat/csv merged to main at 9c0ffee. + +## COMPLETED +- Button, streaming endpoint, BOM/CRLF handling. +- Unit tests for encoder. + +## REMAINING +- None. Done. + +## BLOCKERS +- None. + +## HUMAN_AUTHORITY_REQUIRED +- None. + +## VERIFICATION_EVIDENCE +- pytest reports/tests: 41 passed. +- Manually opened a 20-row export in Excel: OK. + +## NEXT_EXECUTABLE_ACTION +- None. Goal complete. diff --git a/test/fixtures/filled-goal.md b/test/fixtures/filled-goal.md new file mode 100644 index 0000000..2e7eb0c --- /dev/null +++ b/test/fixtures/filled-goal.md @@ -0,0 +1,49 @@ +# ACTIVE_GOAL + +## GOAL +Add rate limiting to /login (5 attempts per minute per IP, HTTP 429 with Retry-After). + +## WHY +Credential-stuffing traffic observed in access logs 2026-09-20; no throttle exists. + +## PHASE +EXECUTE + +## DEFINITION_OF_DONE +- POST /login returns 429 with Retry-After on the 6th attempt within 60 s from one IP (integration test). +- Existing login tests still pass. +- Limit is configurable via LOGIN_RATE_LIMIT env (default 5). +- Change merged to main. + +## FROZEN_DECISIONS +- Use the existing in-memory token bucket helper `ratelimit.bucket()`; no Redis. +- Key by client IP from X-Forwarded-For only when TRUST_PROXY=1. + +## INVARIANTS +- No change to session/cookie semantics. +- No logging of passwords or full request bodies. + +## VERIFIED_CURRENT_STATE +- main at 4f2c1e9, tests 118/118 pass (2026-09-22 14:05 UTC). +- `ratelimit.bucket()` exists and is unit-tested. + +## COMPLETED +- Middleware `login_limiter` added in auth/limiter.py (uncommitted on branch feat/login-rl). +- Unit test for bucket keying by IP added. + +## REMAINING +- Integration test for 429 + Retry-After. +- Env config LOGIN_RATE_LIMIT. +- Commit, open PR, merge. + +## BLOCKERS +- None. + +## HUMAN_AUTHORITY_REQUIRED +- None. + +## VERIFICATION_EVIDENCE +- pytest auth/tests/test_limiter_unit.py: 3 passed (2026-09-22 16:40 UTC, local). + +## NEXT_EXECUTABLE_ACTION +- Write auth/tests/test_login_429.py asserting 429 + Retry-After on 6th request; run full suite. diff --git a/test/fixtures/implemented-not-deployed.md b/test/fixtures/implemented-not-deployed.md new file mode 100644 index 0000000..85bb18a --- /dev/null +++ b/test/fixtures/implemented-not-deployed.md @@ -0,0 +1,46 @@ +# ACTIVE_GOAL + +## GOAL +Ship account-bound artifact chaining for the MCP tools to production. + +## WHY +Intermediate base64 round-trips make multi-step agent workflows slow and expensive. + +## PHASE +SHIP + +## DEFINITION_OF_DONE +- All six MCP tools accept artifact_ref and delivery="artifact" (source + tests). +- Backend + MCP regression suite green. +- Change merged to main. +- Deployed to production and a live 5-step chained workflow succeeds against api.example.com. + +## FROZEN_DECISIONS +- Base64 ingress stays supported. +- No arbitrary URL ingestion. + +## INVARIANTS +- Production is NOT to be changed before the 2026-09-24 release window (owner decision, freeze on the search control plane). + +## VERIFIED_CURRENT_STATE +- main at 5a64ed9 contains the full change; 1731 passed / 54 skipped / 0 failed (2026-09-18). +- Production still runs image c447695 (pre-change). Verified by reading the live container tag 2026-09-18, not from this file. + +## COMPLETED +- DoD items 1-3: source, tests, merge. + +## REMAINING +- DoD item 4: deploy + live chained-workflow acceptance. Not started. + +## BLOCKERS +- Type B (external, dated): release window opens 2026-09-24; search-control-plane freeze must be lifted first. + +## HUMAN_AUTHORITY_REQUIRED +- Production deployment. Do NOT deploy before the window; owner runs or explicitly delegates the runbook. + +## VERIFICATION_EVIDENCE +- Test run 1731/54/0 at 5a64ed9 (2026-09-18 CI log #412). +- 12-call chained MCP dispatch against a local candidate container, zero intermediate base64 (docs/MCP_ARTIFACT_CHAINING.md). + +## NEXT_EXECUTABLE_ACTION +- Nothing executable before 2026-09-24. On/after that date: run docs/SEPTEMBER_24_RUNBOOK.md from section 1. diff --git a/test/fixtures/malformed-headers.md b/test/fixtures/malformed-headers.md new file mode 100644 index 0000000..89dc5d7 --- /dev/null +++ b/test/fixtures/malformed-headers.md @@ -0,0 +1,16 @@ +# ACTIVE_GOAL + +## Goal +Lowercase header variant. + +### PHASE +EXECUTE + +## Definition of Done +- Something. + +## NEXT_EXECUTABLE_ACTION: +- Trailing colon on header. + +**NEXT_EXECUTABLE_ACTION** +- Bold instead of header. diff --git a/test/fixtures/malformed-missing-sections.md b/test/fixtures/malformed-missing-sections.md new file mode 100644 index 0000000..70969fd --- /dev/null +++ b/test/fixtures/malformed-missing-sections.md @@ -0,0 +1,10 @@ +# ACTIVE_GOAL + +## GOAL +Something half-written. + +## PHASE +EXECUTE + +## COMPLETED +- A few things. diff --git a/test/fixtures/observation-window.md b/test/fixtures/observation-window.md new file mode 100644 index 0000000..8b7d9aa --- /dev/null +++ b/test/fixtures/observation-window.md @@ -0,0 +1,43 @@ +# ACTIVE_GOAL + +## GOAL +Growth telemetry produces at least one MEDIUM-sufficiency opportunity after a full 28-day window in production. + +## WHY +Opportunities read from less than 28 days of data are LOW sufficiency by definition; decisions need a mature window. + +## PHASE +SHIP + +## DEFINITION_OF_DONE +- Telemetry enabled in production (done 2026-09-25). +- 28 consecutive days of ingest with zero DEAD outbox rows. +- On or after 2026-10-23: `growth opportunities` lists >= 1 opportunity at MEDIUM or higher. + +## FROZEN_DECISIONS +- No cost-model edits during the window (would invalidate comparability). + +## INVARIANTS +- Do not read any opportunity as more than LOW before 2026-10-23. + +## VERIFIED_CURRENT_STATE +- Enabled 2026-09-25 10:00 UTC; /health shows enabled=true configured=true dead=0 (observed 2026-09-25). + +## COMPLETED +- DoD item 1. + +## REMAINING +- DoD items 2-3 (time-gated). + +## BLOCKERS +- Type B external asynchronous: elapsed time until 2026-10-23. Not agent-solvable. + +## HUMAN_AUTHORITY_REQUIRED +- None. + +## VERIFICATION_EVIDENCE +- /health snapshot 2026-09-25 (ops/evidence/health-20260925.json). + +## NEXT_EXECUTABLE_ACTION +- Before 2026-10-23: nothing for this goal. Weekly: check /health dead==0 and record it here. Unrelated work may proceed on other branches; this file stays as is. +- On/after 2026-10-23: run `growth opportunities`, record result, close if DoD 3 holds. diff --git a/test/fixtures/ocpdf-style.md b/test/fixtures/ocpdf-style.md new file mode 100644 index 0000000..fa3a5a9 --- /dev/null +++ b/test/fixtures/ocpdf-style.md @@ -0,0 +1,26 @@ +# Product Active Goal + +Last reconciled: 2026-09-18 + +This file is a thin execution pointer. Durable/mutable project truth lives in CURRENT_STATE.md; do not turn this file back into a historical diary. + +## Current gate — September 24 production acceptance: WAITING ON RELEASE WINDOW + +The pre-release source cycle is **CLOSED / SOURCE READY**. Production remains intentionally unchanged while the external freeze is active. + +Latest source regression evidence: `1731 passed, 54 skipped, 0 failed`. + +## Next executable sequence + +On or after **2026-09-24**, and only after the freeze genuinely ends: + +`release-SHA preflight → build/test → release gate → deploy → live acceptance` + +## Known non-blocking follow-up + +- document-layout intelligence for complex forms; +- customer-facing packaging/pricing. + +## No other product GOAL implicitly active + +Historical work does not become active merely because it exists in Git/docs. diff --git a/test/fixtures/scp-style.md b/test/fixtures/scp-style.md new file mode 100644 index 0000000..635eb30 --- /dev/null +++ b/test/fixtures/scp-style.md @@ -0,0 +1,27 @@ +# ACTIVE GOAL — Growth Intelligence V1 + +STATUS (2026-09-22): **SOURCE-COMPLETE ON A BRANCH, NOT DEPLOYED, NOT MERGED.** + +GOAL: Make the control plane able to connect search demand to product value for its managed sites, as a bounded subsystem beside the frozen engine, with one client product first. + +PRODUCT_DIRECTION: measures and explains; never mutates pricing, content or spend. + +CURRENT_PHASE: **OWNER REVIEW → MERGE → DEPLOY → OBSERVE.** + +FROZEN_DECISIONS: +- Engine stays frozen; the new subsystem writes nothing into it. +- Generic multi-product model; nothing named after the first client. + +DEFINITION_OF_DONE (source, tests and local dogfood — deployment is a separate owner gate): + +1. **DONE — contracts.** Event families, vocabularies, bounds. +2. **PARTIAL — product intelligence.** Backend-observed truth complete; frontend events deferred. +3. **DONE — tests.** 423 passed. + +OWNER-GATED — NOT PERFORMED (stop conditions): +- **Merge** of the branch. +- **Production deployment.** + +RESIDUAL_LIMITATIONS: see the subsystem document "Known limitations". + +NEXT_EXECUTABLE_ACTION: owner reviews the PR; on merge, deploy with the added worker container, then let one full 28-day window accumulate before reading any opportunity as more than LOW sufficiency. diff --git a/test/hardening.test.js b/test/hardening.test.js new file mode 100644 index 0000000..d981f13 --- /dev/null +++ b/test/hardening.test.js @@ -0,0 +1,210 @@ +'use strict'; +// v0.1.3 hardening regression suite. Fixtures under test/fixtures/ were derived from the +// 2026-09-23 adversarial validation (research/2026-09-23-adversarial-validation.md). +// Every case runs in a disposable temp directory; nothing depends on the developer's repository. + +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'); +const fixtures = path.join(__dirname, 'fixtures'); +const pkg = JSON.parse(fs.readFileSync(path.join(repo, 'package.json'), 'utf8')); +const temps = []; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function tmp(name) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), `ppgp-hardening-${name}-`)); + temps.push(dir); + return dir; +} + +function ppgp(args, options = {}) { + const result = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', ...options }); + return { code: result.status, out: result.stdout || '', err: result.stderr || '' }; +} + +function git(dir, args) { + const result = spawnSync('git', ['-C', dir, '-c', 'user.name=t', '-c', 'user.email=t@example.test', ...args], { encoding: 'utf8' }); + assert(result.status === 0, `git ${args.join(' ')} failed: ${result.stderr}`); + return result.stdout.trim(); +} + +function repoWith(name, fixture) { + const dir = tmp(name); + git(dir, ['init', '-q', '-b', 'main']); + if (fixture) { + fs.mkdirSync(path.join(dir, 'docs')); + fs.writeFileSync(path.join(dir, 'docs', 'ACTIVE_GOAL.md'), fs.readFileSync(path.join(fixtures, fixture), 'utf8')); + } + return dir; +} + +function fields(out) { + return Object.fromEntries(out.split(/\r?\n/).filter((l) => /^[a-z]+: /.test(l)).map((l) => { + const i = l.indexOf(': '); + return [l.slice(0, i), l.slice(i + 2)]; + })); +} + +try { + // 1. canonical happy path: scaffold (all thirteen fields) -> status -> handoff; conformant, TODO warnings only + { + const dir = repoWith('happy'); + assert(ppgp(['init', '--root', dir]).code === 0, 'init failed'); + assert(ppgp(['goal', 'Ship the test', '--root', dir]).code === 0, 'goal failed'); + const status = ppgp(['status', '--root', dir]); + assert(status.code === 0, 'scaffold status must exit 0'); + assert(status.out.includes(`PPGP/${pkg.version} status from docs/ACTIVE_GOAL.md`), 'status header'); + assert(fields(status.out).goal === 'Ship the test', 'goal not recovered'); + assert(status.out.includes('state: conformant'), 'scaffold with all thirteen fields must be conformant'); + assert(/TODO/.test(status.err) && !/missing/.test(status.err), 'scaffold warns about TODOs only'); + const handoff = ppgp(['handoff', '--root', dir]); + assert(handoff.code === 0 && handoff.out.startsWith(`PPGP/${pkg.version}\nG=Ship the test\nP=THINK\n`), 'handoff packet shape changed'); + assert(ppgp(['goal', 'Second', '--root', dir]).code === 1, 'second goal must be refused without --force'); + } + + // 2. realistic parsing: filled canonical file, SCP-style key lines, OCPDF-style free headers + { + const filled = ppgp(['status', '--root', repoWith('filled', 'filled-goal.md')]); + assert(filled.code === 0 && filled.out.includes('state: conformant') && filled.err === '', `filled fixture must be conformant with no warnings: ${filled.err}`); + assert(fields(filled.out).next.startsWith('Write auth/tests/test_login_429.py'), 'filled next'); + + const scp = ppgp(['status', '--root', repoWith('scp', 'scp-style.md')]); + assert(scp.code === 2 && scp.out.includes('state: partial'), `SCP-style file lacks canonical fields and must be partial (exit ${scp.code})`); + assert(/canonical field\(s\) missing \(8 of 13\): WHY, INVARIANTS, VERIFIED_CURRENT_STATE, COMPLETED, REMAINING, BLOCKERS, HUMAN_AUTHORITY_REQUIRED, VERIFICATION_EVIDENCE/.test(scp.err), `missing fields must be named: ${scp.err}`); + const f = fields(scp.out); + assert(f.goal.startsWith('Make the control plane able'), 'SCP GOAL: line not parsed'); + assert(f.phase.includes('OWNER REVIEW'), 'SCP CURRENT_PHASE alias not parsed'); + assert(f.frozen.includes('Engine stays frozen'), 'SCP FROZEN_DECISIONS: block not parsed'); + assert(f.next.startsWith('owner reviews the PR'), 'SCP NEXT_EXECUTABLE_ACTION: not parsed'); + assert(!f.goal.includes('PRODUCT_DIRECTION'), 'unknown upper-case key must start its own section'); + assert(scp.out.includes('unrecognized: PRODUCT_DIRECTION | RESIDUAL_LIMITATIONS'), 'unknown keys must be listed, not dropped'); + assert(/not a lifecycle phase/.test(scp.err), 'non-lifecycle phase must warn'); + const scpHandoff = ppgp(['handoff', '--root', repoWith('scp2', 'scp-style.md')]); + assert(scpHandoff.code === 2 && scpHandoff.out.includes('N:owner reviews the PR'), 'SCP handoff recovers NEXT and stays partial'); + + const ocpdf = ppgp(['status', '--root', repoWith('ocpdf', 'ocpdf-style.md')]); + assert(ocpdf.code === 2, `OCPDF-style thin pointer must be partial (exit 2), got ${ocpdf.code}`); + assert(ocpdf.out.includes('goal: (no GOAL section) title: Product Active Goal'), 'title fallback'); + assert(fields(ocpdf.out).next.includes('On or after'), '"Next executable sequence" header must map to NEXT'); + assert(ocpdf.out.includes('unrecognized: Current gate'), 'unrecognized headers must be listed'); + assert(/canonical field\(s\) missing \(12 of 13\): GOAL, WHY, PHASE/.test(ocpdf.err), `missing fields must be named: ${ocpdf.err}`); + assert(!ocpdf.out.includes('goal: (not set)'), 'the silent (not set) failure mode must be gone'); + } + + // 3. state representation cases: implemented-not-deployed, observation window, scoped blocker are ok, not partial + for (const [fixture, expect] of [ + ['implemented-not-deployed.md', { phase: 'SHIP', remaining: 'DoD item 4', authority: 'Do NOT deploy' }], + ['observation-window.md', { phase: 'SHIP', blockers: 'elapsed time until 2026-10-23' }], + ['blocker-scope.md', { blockers: 'Steps B and C are NOT blocked', next: 'Step B' }], + ]) { + const r = ppgp(['status', '--root', repoWith('repr', fixture)]); + assert(r.code === 0 && r.out.includes('state: conformant') && r.err === '', `${fixture} must be conformant with no warnings: exit ${r.code} ${r.err}`); + const f = fields(r.out); + for (const [k, v] of Object.entries(expect)) assert(f[k].includes(v), `${fixture}: ${k} should contain "${v}", got "${f[k]}"`); + } + + // 4. malformed / contradictory diagnostics + { + const missing = ppgp(['status', '--root', repoWith('missing', 'malformed-missing-sections.md')]); + assert(missing.code === 2 && /canonical field\(s\) missing \(10 of 13\): WHY, DEFINITION_OF_DONE.*NEXT_EXECUTABLE_ACTION/.test(missing.err), `missing fields must be partial and named: ${missing.err}`); + + const headers = ppgp(['status', '--root', repoWith('headers', 'malformed-headers.md')]); + assert(headers.code === 2, `casing/level/bold/colon variants parse, but the file is partial: ${headers.err}`); + const h = fields(headers.out); + assert(h.goal === 'Lowercase header variant.' && h.phase === 'EXECUTE', 'lowercase and ### headers'); + assert(h.next === 'Trailing colon on header.', 'trailing-colon header'); + assert(/duplicate section NEXT_EXECUTABLE_ACTION/.test(headers.err), 'bold duplicate must warn, first kept'); + + const closed = ppgp(['status', '--root', repoWith('closed', 'false-completion.md')]); + assert(closed.code === 0 && /PHASE is CLOSED but ACTIVE_GOAL still exists/.test(closed.err), 'CLOSED-with-file must warn'); + const contra = ppgp(['status', '--root', repoWith('contra', 'contradictory.md')]); + assert(/contradiction: PHASE is CLOSED while REMAINING is not empty/.test(contra.err), 'CLOSED + REMAINING must warn'); + + const emptyDir = repoWith('empty', 'filled-goal.md'); + fs.writeFileSync(path.join(emptyDir, 'docs', 'ACTIVE_GOAL.md'), ''); + const empty = ppgp(['status', '--root', emptyDir]); + assert(empty.code === 1 && /malformed: file is empty/.test(empty.err), 'empty file must fail'); + fs.writeFileSync(path.join(emptyDir, 'docs', 'ACTIVE_GOAL.md'), Buffer.from([0, 1, 2, 255, 254, 0, 7, 8, 0, 1])); + const garbage = ppgp(['status', '--root', emptyDir]); + assert(garbage.code === 1 && /not readable text/.test(garbage.err), 'binary garbage must fail'); + fs.writeFileSync(path.join(emptyDir, 'docs', 'ACTIVE_GOAL.md'), 'Just a paragraph of prose with no sections.\n'); + const prose = ppgp(['status', '--root', emptyDir]); + assert(prose.code === 1 && /no recognizable ACTIVE_GOAL sections/.test(prose.err), 'sectionless prose must fail'); + const doctor = ppgp(['doctor', '--root', emptyDir]); + assert(doctor.code === 1 && /malformed/.test(doctor.out), 'doctor must surface malformed state'); + } + + // 5. nested-directory invocation resolves the Git top-level; outside Git, cwd is the root + { + const dir = repoWith('nested', 'filled-goal.md'); + fs.mkdirSync(path.join(dir, 'src', 'components'), { recursive: true }); + const nested = ppgp(['status'], { cwd: path.join(dir, 'src', 'components') }); + assert(nested.code === 0 && nested.out.includes('status from docs/ACTIVE_GOAL.md'), `nested cwd must find repo state: ${nested.err}`); + const plain = tmp('nogit'); + fs.mkdirSync(path.join(plain, 'docs')); + fs.copyFileSync(path.join(fixtures, 'filled-goal.md'), path.join(plain, 'docs', 'ACTIVE_GOAL.md')); + const noGit = ppgp(['status'], { cwd: plain }); + assert(noGit.code === 0, 'non-Git directory must still work from cwd'); + const noGitDoctor = ppgp(['doctor'], { cwd: plain }); + assert(noGitDoctor.code === 0 && /Git forensic history was not detected/.test(noGitDoctor.out), 'doctor without Git'); + } + + // 6. branch visibility: goal committed only on a feature branch is reported by doctor from main + { + const dir = repoWith('branch'); + git(dir, ['commit', '-q', '--allow-empty', '-m', 'init']); + git(dir, ['checkout', '-q', '-b', 'feat/x']); + ppgp(['goal', 'Goal on branch', '--root', dir]); + git(dir, ['add', '-A']); + git(dir, ['commit', '-q', '-m', 'goal on branch']); + git(dir, ['checkout', '-q', 'main']); + const doctor = ppgp(['doctor', '--root', dir]); + assert(doctor.code === 0, 'doctor exit'); + assert(/Git: branch main @ [0-9a-f]+, working tree clean/.test(doctor.out), `doctor must print branch summary: ${doctor.out}`); + assert(/other refs carry one: feat\/x:docs\/ACTIVE_GOAL\.md/.test(doctor.out), `doctor must list feat/x: ${doctor.out}`); + assert(/Not switching branches/.test(doctor.out), 'doctor must say it does not switch'); + assert(git(dir, ['rev-parse', '--abbrev-ref', 'HEAD']) === 'main', 'doctor must not change the checkout'); + fs.writeFileSync(path.join(dir, 'dirty.txt'), 'x'); + assert(/working tree 1 changed path/.test(ppgp(['doctor', '--root', dir]).out), 'dirty count'); + } + + // 7. --force preserves the previous ACTIVE_GOAL + { + const dir = repoWith('force', 'filled-goal.md'); + const forced = ppgp(['goal', 'Replacement', '--root', dir, '--force']); + assert(forced.code === 0 && /Previous ACTIVE_GOAL preserved at docs[\\/]ACTIVE_GOAL\.md\.\d{8}T\d{6}Z\.bak/.test(forced.out), `backup message: ${forced.out}`); + const backups = fs.readdirSync(path.join(dir, 'docs')).filter((f) => f.endsWith('.bak')); + assert(backups.length === 1, 'exactly one backup'); + assert(fs.readFileSync(path.join(dir, 'docs', backups[0]), 'utf8').includes('Add rate limiting to /login'), 'backup keeps old content'); + assert(fields(ppgp(['status', '--root', dir]).out).goal === 'Replacement', 'new goal active'); + } + + // 8. minimal repository: one command, one file, thirteen sections; no other files created + { + const dir = repoWith('minimal'); + ppgp(['goal', 'Fix typo in README', '--root', dir]); + const created = fs.readdirSync(dir).filter((f) => f !== '.git'); + assert(created.length === 1 && created[0] === 'docs', `goal must create only docs/: ${created}`); + const content = fs.readFileSync(path.join(dir, 'docs', 'ACTIVE_GOAL.md'), 'utf8'); + assert((content.match(/^## /gm) || []).length === 13, 'thirteen sections'); + } + + // 9. skill-path and install-skill unchanged + { + assert(ppgp(['skill-path']).out.trim().endsWith(path.join('skills', 'ppgp')), 'skill-path'); + const dest = tmp('skill'); + const installed = ppgp(['install-skill', dest]); + assert(installed.code === 0 && fs.existsSync(path.join(dest, 'ppgp', 'SKILL.md')), 'install-skill'); + } + + console.log('PPGP hardening regression tests passed.'); +} finally { + for (const dir of temps) fs.rmSync(dir, { recursive: true, force: true }); +} diff --git a/test/package.test.js b/test/package.test.js index 2b4034f..783fe08 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -39,9 +39,9 @@ for (const required of [ 'benchmarks/result.schema.json', 'benchmarks/pilot-01/RUNBOOK.md', 'benchmarks/pilot-01/TASK.md', - 'BENCHMARK_PROTOCOL.md', + 'benchmarks/PROTOCOL.md', 'SPEC.md', - 'EVALUATION.md', + 'docs/EVALUATION.md', 'CITATION.cff', ]) { assert(files.has(required), `published npm package is missing ${required}`);