From 5430885759453c85e41f83d01e51c7f81353693e Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:26:41 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(check-fork):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=A1=BB=E4=BF=9D=E7=95=99=E7=9A=84=E6=9C=AC=E5=9C=B0=E5=86=B3?= =?UTF-8?q?=E5=AE=9A=E6=94=B9=E7=94=B1=E6=AD=A3=E5=90=91=E5=AE=88=E5=8D=AB?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADAPTATION 记录的本地决定此前只靠阅读守护:守卫只拦 Cursor 黑名单, 上游整文件覆盖会静默抹掉 replay、显式授权、fast mode、--once 等本地措辞。 新增与 DENYLIST 平行的 REQUIRED 表:每条规则给出文件、可选锚点句和 ADAPTATION 小节,缺文件或锚点即报错,引用的小节不存在也报错。 validateT3Contract 的四个锚点并入该表。植入测试逐条删掉锚点 (有上游原文时换成上游措辞),确认只有对应规则报告;表中新增规则 而无植入用例时测试失败。 Co-Authored-By: Claude Opus 5.5 --- ADAPTATION.md | 6 +- docs/ci.md | 6 +- scripts/check-fork.mjs | 183 ++++++++++++++++++++++++++++++++---- scripts/check-fork.test.mjs | 66 ++++++++++--- 4 files changed, 227 insertions(+), 34 deletions(-) diff --git a/ADAPTATION.md b/ADAPTATION.md index 70a21a7..df61ce2 100644 --- a/ADAPTATION.md +++ b/ADAPTATION.md @@ -21,7 +21,7 @@ Benny remains dormant. Its copied runtime entry discovers this shared contract i - A file this fork never changed: apply the change, then check it against the decisions below. The 0.15.25 sync rewrote upstream's new fast-mode sentence in two otherwise untouched forensics playbooks. - A file this fork deleted, such as `.cursor-plugin/plugin.json`: keep it deleted unless the change removes the recorded reason. -The fork guards catch only the denylisted primitives, so read every change against the decisions in this guide. When `comparisonMayBeTruncated` is true, the file list is incomplete; sync through an intermediate upstream commit. Finish a sync by moving `UPSTREAM.json` and the first line of this guide to the new commit, adding a paragraph below that names what was taken, kept, and skipped, running the [verification](#verification) checks, and refreshing the installations as described under Antigravity integration. +The [fork guards](./docs/ci.md#fork-guards) reject the denylisted Cursor primitives and require each local decision listed in `REQUIRED` in `scripts/check-fork.mjs`, by file and anchor sentence. A decision this guide records without such a rule is guarded only by reading, so read every change against the guide. When a sync changes a guarded decision on purpose, update its section here and its rule in the same commit. When `comparisonMayBeTruncated` is true, the file list is incomplete; sync through an intermediate upstream commit. Finish a sync by moving `UPSTREAM.json` and the first line of this guide to the new commit, adding a paragraph below that names what was taken, kept, and skipped, running the [verification](#verification) checks, and refreshing the installations with the commands under [T3 Code overlay](#t3-code-overlay). ## Upstream behavior retained @@ -57,7 +57,7 @@ T3 Code runs Codex or Claude Code underneath, so it is an overlay on the selecte After maintained-source changes, refresh the existing installations with `agy plugin install `, `codex plugin add pstack-codex@personal` for this machine's existing Codex ID, and `claude plugin update pstack@pstack-local`. Do not flatten pstack skills into shared standalone names or restore retired paseo aliases. -The `0.15.25-shared.3` T3 update keeps the same shared configuration and adds a read-only `config.mjs resolve --host t3` preview for one role or panel. `bin/t3-targets.mjs` maps exact targets and effort against the current thread's capability catalog, preserves xagent entries and repeated/required panel members, and rejects unavailable explicit targets without model substitution. Missing effort controls remain visible warnings with no claim of delivery. T3 children now use application-owned `isolation: "worktree"`; briefs and acceptance evidence must distinguish the parent commit from uncommitted or separately prepared input. The catalog no longer assumes particular vendors are absent. `scripts/t3-targets.test.mjs` and the T3 contract guard protect these boundaries, and [T3 live verification](./docs/t3-verification.md) separates selection, task metadata, provider observations and artifact acceptance. chhoumann/t3-pstack's declared adaptation checks and creedants/pstack-t3's catalog-backed role resolution informed these boundaries; their code, model defaults, automatic fallback, separate configuration and standing coordinators are not imported. +The `0.15.25-shared.3` T3 update keeps the same shared configuration and adds a read-only `config.mjs resolve --host t3` preview for one role or panel. `bin/t3-targets.mjs` maps exact targets and effort against the current thread's capability catalog, preserves xagent entries and repeated/required panel members, and rejects unavailable explicit targets without model substitution. Missing effort controls remain visible warnings with no claim of delivery. T3 children now use application-owned `isolation: "worktree"`; briefs and acceptance evidence must distinguish the parent commit from uncommitted or separately prepared input. The catalog no longer assumes particular vendors are absent. `scripts/t3-targets.test.mjs` and the fork guards' required T3 anchors protect these boundaries, and [T3 live verification](./docs/t3-verification.md) separates selection, task metadata, provider observations and artifact acceptance. chhoumann/t3-pstack's declared adaptation checks and creedants/pstack-t3's catalog-backed role resolution informed these boundaries; their code, model defaults, automatic fallback, separate configuration and standing coordinators are not imported. The `0.15.25-shared.4` follow-up adds opt-in T3 candidate roles and same-family `@latest` selectors. A candidate role chooses one provider, preferring the current parent or the first configured candidate; an explicit `--provider` can select another configured candidate. Failures never walk the list. Family selection uses canonical numeric releases in the task's live catalog, freezes concrete IDs for that task and preserves exact pins. It does not add a second registry or change xagent defaults. Each new delegate is classified by its own phase and difficulty; ordinary implementation returns to its ordinary role after a hard design. The optional `hardest architect runners` panel supports Fable/Astra designs without rewriting other panels. Native roles and their inheritance remain separate. Both installed helpers must be upgraded before storing the new candidate-object shape, and existing sessions must reload the new runtime. [Routing examples](./docs/t3-model-routing.md) cover the contract and compatibility boundary. @@ -81,7 +81,7 @@ The CLI tests use real Git checkouts and subprocesses with vendor stand-ins, inc Run `scripts/xagent-e2e.sh` after changing `bin/xagent*` or upgrading an agent CLI; it calls every vendor. Run `python3 scripts/pstack-host-e2e.py ` for actual Claude and Codex hosts executing swarm then interrogate through their current shared policy on synthetic fixtures. It records source hashes, policy, worker receipts and independent tests, and checks that the parent verified the final source and that each configured reviewer saw it in a separate worktree. It checks `xagent:` entries through their receipts, counting a `meta.json` as an xagent attempt when either marker `bin/xagent` creates first (`prompt.md`, `agent.log`) sits beside it, so a broken one still fails, keyed by mode, agent and model, and listing any other `meta.json` under the evidence directory as ignored, and native entries through the host's own log, parsed by `scripts/e2e-native-evidence.py`: Claude's stream-json Agent calls and their resolved model, or Codex's persisted session rollouts (the Codex host therefore runs without `--ephemeral`). A native dispatch's checkout comes from its subagent's own tool calls: the Claude events whose `parent_tool_use_id` is its Agent call, or the Codex child rollout (whose task payload is encrypted). Those calls must use exactly one checkout under the run other than the parent, and it must be a linked worktree at the fixture base. The dispatch writes when it edited that checkout with its host's file-edit tool and the edit succeeded (Claude `Write`/`Edit` with a successful tool result, a completed Codex `FileChange`), and reviews when it edited nothing there and the checkout holds the final `stats.py`. A native review must also bracket its work, as xagent's `snapshot_before` and `snapshot_after` do: its subagent's own first and last shell commands in that worktree run the absolute path of `scripts/e2e-snapshot.py` with a Python interpreter (optionally after a `cd`, with nothing else in the same command), every other shell command there falls between them, and every snapshot the tool prints for that worktree (leaving ignored test receipts and caches out) matches the worktree's state when the run ends. The snapshot lines come from the subagent's own tool results or Codex `aggregated_output`, which the host cannot write, and an echoed line does not count because it never ran the tool; forging one would take rewriting the tool or shadowing `python3` on PATH, which is outside the threat model of an acceptance test for cooperating agents. A shell edit during a review, before it, or after it therefore stops it counting. Shell edits are not writer evidence, so a writer that edits only through the shell cannot fill the writer slot. When the effective policy sets `reasoning_effort`, a worker fills a role only if its xagent receipt, Claude Agent call or Codex `spawn_agent` records an effort level; the level itself may differ, since poteto-mode adjusts it per task. Each checkout belongs to one owner, a worker (kind, agent and model) in one mode, so an xagent retry or a native writer's follow-up fix may reuse its worktree; and each configured writer or reviewer slot needs an attempt in a checkout no other slot used, so two reviews in one worktree count once. A missing log or a dispatch that used no path is reported as a gap and fails the check. It never changes preferences. Each worker attempt is limited to xagent's default 900 seconds with one retry, not doubled. Both live harnesses use `timeout` with a kill-after limit for the outer host process; host workflows default to 6 attempts × (900-second limit + xagent's termination grace + 30 seconds of start-up and snapshots) + 600 seconds of host work + both final checks, so the writer, review and post-fix re-review rounds can each retry, configurable with `--host-timeout`. Verification commands use `scripts/e2e-process.py` with a 30-second default deadline, process-group termination and bounded output collection; the host harness exposes `--check-timeout`. A verification timeout remains a failed check with logs and exit code 124. -The runtime checks start with `scripts/check-fork.mjs`, which validates the manifests, all SKILL.md and agent frontmatter, each skill's link to the shared runtime contract, local links, and a denylist of the Cursor-only wording this guide replaces; see [Fork guards](./docs/ci.md#fork-guards). Use a fresh Codex app-server skills/list and Claude `plugin details` to verify inventory; a static validator alone is not a discovery or behavioral test. +The runtime checks start with `scripts/check-fork.mjs`, which validates the manifests, all SKILL.md and agent frontmatter, each skill's link to the shared runtime contract, local links, a denylist of the Cursor-only wording this guide replaces, and the files and anchor sentences of the local decisions it requires; see [Fork guards](./docs/ci.md#fork-guards). Use a fresh Codex app-server skills/list and Claude `plugin details` to verify inventory; a static validator alone is not a discovery or behavioral test. Rebuild the `orch` and `watch-pr` bundles with `python3 scripts/build-runtime.py` when their sources, dependencies or builder change. Use the Bun version in `.bun-version` and locked development dependencies. `python3 scripts/build-runtime.py --check` compares a temporary build with the working tree without writing artifacts. Node runtime helpers never download dependencies. diff --git a/docs/ci.md b/docs/ci.md index 7458cdb..5b8d404 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -37,9 +37,11 @@ bash scripts/check.sh build | Skills | a skill directory lacks `SKILL.md`, a frontmatter `name` differs from its directory or agent file name, a `description` is empty, or a `SKILL.md` has no link to `references/runtime.md` | | Links | a Markdown link outside code points at a missing file or leaves the repository | | Manifests | a manifest is not valid JSON or loses its name (`pstack`, or `pstack-codex` for Codex), or the root `plugin.json` references `agent-plugins.org`, whose `$schema` makes Codex truncate each skill to 8,000 bytes | -| T3 contract | the T3 runtime loses the catalog-backed resolver, application-owned worktree isolation, completed status or available-result requirement | +| Required | a local decision in the `REQUIRED` table of `scripts/check-fork.mjs` loses its file or anchor sentence, such as the verification-skill replays, xagent, explicit PR, push and merge authorization, fast mode only on request, watch-pr `--once`, or the T3 dispatch contract; or a rule cites an adaptation-guide section that no longer exists | -When the denylist fires after a sync, rewrite the sentence the way the adaptation guide records for that sync instead of swapping tokens. A file that names a pattern on purpose, such as an adapter listing the fields it forbids, goes in that rule's `allow` list in `scripts/check-fork.mjs`; `skills/poteto-help/SKILL.md` is the one allowed `disable-model-invocation`. An allowed file that stops matching fails as stale, so an allowance cannot outlive the sentence it excuses. `scripts/check-fork.test.mjs` plants each violation in a copy of the tree and checks that it is reported. +When the denylist fires after a sync, rewrite the sentence the way the adaptation guide records for that sync instead of swapping tokens. A file that names a pattern on purpose, such as an adapter listing the fields it forbids, goes in that rule's `allow` list in `scripts/check-fork.mjs`; `skills/poteto-help/SKILL.md` is the one allowed `disable-model-invocation`. An allowed file that stops matching fails as stale, so an allowance cannot outlive the sentence it excuses. + +When a required rule fires after a sync, restore the local decision in the synced file; each problem names the rule, its anchor, and the guide section that records the decision. If the decision itself changes, update that section and the rule in the same commit. `scripts/check-fork.test.mjs` plants each violation and each lost decision in a copy of the tree and checks that it is reported, and fails when a required rule has no planted loss. ## Update the distributed bundles diff --git a/scripts/check-fork.mjs b/scripts/check-fork.mjs index 97f84a9..21e1073 100644 --- a/scripts/check-fork.mjs +++ b/scripts/check-fork.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node // Static guards for upstream syncs. Shipped content must not bring back the -// Cursor-only instructions ADAPTATION.md replaced, every skill keeps its shared -// runtime contract, local links resolve, and the manifests keep their fork names. +// Cursor-only instructions ADAPTATION.md replaced and must keep the local +// decisions it records, every skill keeps its shared runtime contract, local +// links resolve, and the manifests keep their fork names. import { existsSync, readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -72,6 +73,150 @@ export const DENYLIST = [ }, ]; +// The local decisions an upstream sync must keep: each names a file and, where +// wording carries the decision, an anchor in it. `section` is the ADAPTATION.md +// heading that records the decision; a rule whose heading is gone is reported, +// so the table cannot point at a decision the guide no longer records. +export const REQUIRED = [ + { + id: "replay-after-pass", + file: "skills/create-verification-skill/SKILL.md", + pattern: /Write a replay only after a drive's end-state check passed/, + hint: "a replay is written only from a passing drive", + section: "maintained-source", + }, + { + id: "replay-stale-line", + file: "skills/create-verification-skill/SKILL.md", + pattern: /prints `stale: at `/, + hint: "a stale replay reports itself instead of being redone", + section: "maintained-source", + }, + { + id: "replay-runs-first", + file: "skills/maintain-verification-skill/SKILL.md", + pattern: /Run a feature's replay first when it has one/, + hint: "a replay runs before any manual drive", + section: "maintained-source", + }, + { + id: "replay-stale-is-finding", + file: "skills/maintain-verification-skill/SKILL.md", + pattern: /Never let a passing drive quietly stand in for a stale replay/, + hint: "a stale replay is a finding, never covered by a passing drive", + section: "maintained-source", + }, + { + id: "xagent-skill", + file: "skills/xagent/SKILL.md", + hint: "the xagent skill is a local addition with no upstream counterpart", + section: "maintained-source", + }, + { + id: "xagent-allowlist", + file: "bin/xagent-allowed-commands", + hint: "xagent writers and the pi guard read their command allowlist here", + section: "maintained-source", + }, + { + id: "pr-authorization", + file: "skills/poteto-mode/playbooks/opening-a-pr.md", + pattern: /Use when PR delivery is requested or otherwise explicitly authorized\./, + hint: "a PR is opened only on request or explicit authorization", + section: "upstream-behavior-retained", + }, + { + id: "babysit-write-authorization", + file: "skills/poteto-mode/playbooks/babysit.md", + pattern: /change local code or remote state only to the extent explicitly authorized/, + hint: "babysit pushes and replies only within explicit authorization", + section: "upstream-behavior-retained", + }, + { + id: "babysit-merge-authorization", + file: "skills/poteto-mode/playbooks/babysit.md", + pattern: /Babysitting never authorizes merging\. Only an explicit request to merge, land, ship, or merge when ready does\./, + hint: "only an explicit request authorizes a merge", + section: "upstream-behavior-retained", + }, + { + id: "auto-merge-on-request", + file: "skills/poteto-mode/playbooks/shipping.md", + pattern: /Arm `--auto` [^.]*only when the user asked for merge-when-ready/, + hint: "`--auto` is armed only when the user asked for merge-when-ready", + section: "upstream-behavior-retained", + }, + { + id: "autopilot-push-authorization", + file: "skills/poteto-mode/playbooks/autopilot-full.md", + pattern: /Within the program's push authorization, force-push only your own branch/, + hint: "owners push only within the program's push authorization", + section: "upstream-behavior-retained", + }, + { + id: "autopilot-merge-authorization", + file: "skills/poteto-mode/playbooks/autopilot-full.md", + pattern: /Only the operator's explicit authorization to merge this queue, together with your clean verdict, permits the merge\./, + hint: "owners merge only under the operator's explicit merge authorization", + section: "upstream-behavior-retained", + }, + { + id: "fast-mode-on-request", + file: "skills/poteto-mode/SKILL.md", + pattern: /Fast mode and priority service tiers cost more, so use them only when the user asked\./, + hint: "fast mode stays off unless the user asks; urgency does not turn it on", + section: "upstream-behavior-retained", + }, + { + id: "t3-no-service-tier", + file: "references/t3-runtime.md", + pattern: /The preview adds no speed or service-tier option; do not add one unless the user asked\./, + hint: "T3 dispatch adds no speed or service tier unless the user asks", + section: "upstream-behavior-retained", + }, + { + id: "t3-catalog-preview", + file: "references/t3-runtime.md", + pattern: /config\.mjs resolve --host t3/, + hint: "T3 targets come from the catalog-backed preview", + section: "t3-code-overlay", + }, + { + id: "t3-worktree-isolation", + file: "references/t3-runtime.md", + pattern: /isolation: "worktree"/, + hint: "T3 children run in application-owned worktrees", + section: "t3-code-overlay", + }, + { + id: "t3-completed-status", + file: "references/t3-runtime.md", + pattern: /status: completed/, + hint: "a T3 task counts only at terminal status completed", + section: "t3-code-overlay", + }, + { + id: "t3-result-available", + file: "references/t3-runtime.md", + pattern: /workState: result_available/, + hint: "a T3 result counts only once it is available", + section: "t3-code-overlay", + }, + { + id: "watch-pr-once", + file: "skills/poteto-mode/scripts/watch-pr/cli.ts", + pattern: /"--once"/, + hint: "T3 babysit and shipping rounds classify once with --once instead of polling", + section: "t3-code-overlay", + }, + { + id: "watch-pr-once-test", + file: "skills/poteto-mode/scripts/watch-pr/once.test.ts", + hint: "--once keeps its own test", + section: "t3-code-overlay", + }, +]; + function walk(dir) { if (!existsSync(dir)) return []; return readdirSync(dir, { withFileTypes: true }) @@ -279,23 +424,27 @@ export function validateManifests(root) { return problems; } -// These are the dispatch/evidence boundaries an upstream prose sync must retain. -export function validateT3Contract(root) { - const rel = "references/t3-runtime.md"; - const file = path.join(root, rel); - if (!existsSync(file)) return [`${rel}: missing T3 runtime contract`]; - const text = readFileSync(file, "utf8"); - const required = [ - [/config\.mjs resolve --host t3/, "catalog-backed target preview"], - [/isolation: "worktree"/, "application-owned worktree isolation"], - [/status: completed/, "terminal task status"], - [/workState: result_available/, "available terminal result"], - ]; - return required.filter(([pattern]) => !pattern.test(text)).map(([, boundary]) => `${rel}: missing ${boundary}`); +// GitHub's heading anchors: lowercase, punctuation other than `-` and `_` dropped, spaces to `-`. +function headingSlugs(text) { + return new Set([...text.matchAll(/^#{1,6}[ \t]+(.+?)[ \t]*$/gm)].map((m) => m[1].toLowerCase().replace(/[^\w\- ]/g, "").replaceAll(" ", "-"))); +} + +export function validateRequired(root) { + const guide = path.join(root, "ADAPTATION.md"); + const sections = existsSync(guide) ? headingSlugs(readFileSync(guide, "utf8")) : new Set(); + const problems = []; + for (const rule of REQUIRED) { + if (!sections.has(rule.section)) problems.push(`ADAPTATION.md: ${rule.id}: no section #${rule.section} records this decision`); + const file = path.join(root, rule.file); + const why = `${rule.hint} (ADAPTATION.md#${rule.section})`; + if (!existsSync(file)) problems.push(`${rule.file}: ${rule.id}: missing; ${why}`); + else if (rule.pattern && !rule.pattern.test(readFileSync(file, "utf8"))) problems.push(`${rule.file}: ${rule.id}: no match for ${rule.pattern}; ${why}`); + } + return problems; } export function checkFork(root) { - return [...validateManifests(root), ...validateSkills(root), ...validateLinks(root), ...scanDenylist(root), ...validateT3Contract(root)]; + return [...validateManifests(root), ...validateSkills(root), ...validateLinks(root), ...scanDenylist(root), ...validateRequired(root)]; } if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { @@ -305,5 +454,5 @@ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.ar console.error(`Fork guards failed:\n${problems.join("\n")}`); process.exit(1); } - console.log(`Fork guards passed: ${DENYLIST.length} denylist rules, skill frontmatter and contract links, local Markdown links, manifests, T3 dispatch contract.`); + console.log(`Fork guards passed: ${DENYLIST.length} denylist rules, ${REQUIRED.length} required local decisions, skill frontmatter and contract links, local Markdown links, manifests.`); } diff --git a/scripts/check-fork.test.mjs b/scripts/check-fork.test.mjs index 67eb325..61835e6 100644 --- a/scripts/check-fork.test.mjs +++ b/scripts/check-fork.test.mjs @@ -4,7 +4,7 @@ import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } f import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { checkFork, scanDenylist, validateLinks, validateManifests, validateSkills, validateT3Contract } from "./check-fork.mjs"; +import { checkFork, REQUIRED, scanDenylist, validateLinks, validateManifests, validateRequired, validateSkills } from "./check-fork.mjs"; const root = path.resolve(import.meta.dirname, ".."); const cli = path.join(root, "scripts/check-fork.mjs"); @@ -150,17 +150,59 @@ test("Given a clean copy, When the CLI runs on it, Then it exits 0", (t) => { assert.match(passed.stdout, /^Fork guards passed/); }); -test("Given a prose sync that loses a T3 dispatch boundary, When guarded, Then the missing boundary is named", (t) => { +// What a sync loses for each required decision: [rule, file, local text, what replaces it]. +// Replacements quote upstream's wording at the pin where upstream has its own; null text deletes the file. +const DROPPED = [ + ["replay-after-pass", "skills/create-verification-skill/SKILL.md", "Write a replay only after a drive's end-state check passed", ""], + ["replay-stale-line", "skills/create-verification-skill/SKILL.md", "`stale: at `", "a failure"], + ["replay-runs-first", "skills/maintain-verification-skill/SKILL.md", "Run a feature's replay first when it has one", ""], + ["replay-stale-is-finding", "skills/maintain-verification-skill/SKILL.md", "Never let a passing drive quietly stand in for a stale replay", ""], + ["xagent-skill", "skills/xagent/SKILL.md", null], + ["xagent-allowlist", "bin/xagent-allowed-commands", null], + ["pr-authorization", "skills/poteto-mode/playbooks/opening-a-pr.md", "Use when PR delivery is requested or otherwise explicitly authorized.", "Invoked at the end of every other playbook."], + ["babysit-write-authorization", "skills/poteto-mode/playbooks/babysit.md", "only to the extent explicitly authorized", ""], + ["babysit-merge-authorization", "skills/poteto-mode/playbooks/babysit.md", "Babysitting never authorizes merging. ", ""], + ["auto-merge-on-request", "skills/poteto-mode/playbooks/shipping.md", "only when the user asked for merge-when-ready, and only on a PR", "only on a PR"], + ["autopilot-push-authorization", "skills/poteto-mode/playbooks/autopilot-full.md", "Within the program's push authorization, force-push", "Force-push"], + ["autopilot-merge-authorization", "skills/poteto-mode/playbooks/autopilot-full.md", "Only the operator's explicit authorization to merge this queue, together with your clean verdict, permits the merge.", "Green CI alone never authorizes a merge."], + ["fast-mode-on-request", "skills/poteto-mode/SKILL.md", "Fast mode and priority service tiers cost more, so use them only when the user asked.", "Use fast mode only when the job is urgent, meaning CI or trunk is red or a person is blocked on the result right now."], + ["t3-no-service-tier", "references/t3-runtime.md", "The preview adds no speed or service-tier option; do not add one unless the user asked.", ""], + ["t3-catalog-preview", "references/t3-runtime.md", "config.mjs resolve --host t3", "removed-by-sync"], + ["t3-worktree-isolation", "references/t3-runtime.md", 'isolation: "worktree"', "removed-by-sync"], + ["t3-completed-status", "references/t3-runtime.md", "status: completed", "removed-by-sync"], + ["t3-result-available", "references/t3-runtime.md", "workState: result_available", "removed-by-sync"], + ["watch-pr-once", "skills/poteto-mode/scripts/watch-pr/cli.ts", '"--once"', ""], + ["watch-pr-once-test", "skills/poteto-mode/scripts/watch-pr/once.test.ts", null], +]; + +test("Given a sync that drops one recorded local decision, When guarded, Then only that decision's rule fails and cites the guide", (t) => { + assert.deepEqual(DROPPED.map(([rule]) => rule).sort(), REQUIRED.map((rule) => rule.id).sort(), "each required rule needs one planted loss here"); const dir = copyRepo(t); - const rel = "references/t3-runtime.md"; - const original = readFileSync(path.join(dir, rel), "utf8"); - for (const [token, boundary] of [ - ["config.mjs resolve --host t3", "catalog-backed target preview"], - ['isolation: "worktree"', "application-owned worktree isolation"], - ["status: completed", "terminal task status"], - ["workState: result_available", "available terminal result"], - ]) { - writeFileSync(path.join(dir, rel), original.split(token).join("removed-by-sync")); - assert.deepEqual(validateT3Contract(dir), [`${rel}: missing ${boundary}`]); + for (const [rule, rel, text, replacement] of DROPPED) { + const file = path.join(dir, rel); + const original = readFileSync(file, "utf8"); + if (text === null) rmSync(file); + else edit(dir, rel, (body) => body.split(text).join(replacement)); + + const problems = validateRequired(dir); + + assert.equal(problems.length, 1, `${rule}:\n${problems.join("\n")}`); + assert.ok(problems[0].startsWith(`${rel}: ${rule}: ${text === null ? "missing" : "no match for "}`), problems[0]); + assert.match(problems[0], /\(ADAPTATION\.md#[\w-]+\)$/); + writeFileSync(file, original); } }); + +test("Given the guide renames a section that rules cite, When guarded, Then each citing rule is reported", (t) => { + const dir = copyRepo(t); + edit(dir, "ADAPTATION.md", (text) => text.replace("## T3 Code overlay", "## T3 overlay")); + + assert.deepEqual(validateRequired(dir), [ + "t3-catalog-preview", + "t3-worktree-isolation", + "t3-completed-status", + "t3-result-available", + "watch-pr-once", + "watch-pr-once-test", + ].map((rule) => `ADAPTATION.md: ${rule}: no section #t3-code-overlay records this decision`)); +}); From 06b6baf48db85c896dd69efa9a5bd024c11a81e6 Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:26:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(e2e):=20=E5=AE=BF=E4=B8=BB=20e2e=20?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E7=BB=86=E5=88=99=E7=A7=BB=E5=85=A5=20pstack?= =?UTF-8?q?-host-e2e.py=20=E6=A8=A1=E5=9D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADAPTATION 中约 700 词的验收细则与脚本及其测试重复,每次 fix(e2e) 都要双写。细则移到脚本 docstring,与执行它的代码同处;ADAPTATION 只保留运行时机、判定原则与超时约定。--help 只显示首行摘要,并在 epilog 指向 docstring。 Co-Authored-By: Claude Opus 5.5 --- ADAPTATION.md | 2 +- scripts/pstack-host-e2e.py | 69 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/ADAPTATION.md b/ADAPTATION.md index df61ce2..49d3fd8 100644 --- a/ADAPTATION.md +++ b/ADAPTATION.md @@ -79,7 +79,7 @@ For all CI checks, including strict TypeScript checks, Bun tests and reproducibl The CLI tests use real Git checkouts and subprocesses with vendor stand-ins, including staged/committed read-only violations, misleading verdicts, timeout and cancellation. Vendor acceptance pins the fixture's test contents and checks the worker's successful test receipt against its final source before independently rerunning it. The shared verification runner gives each command a fresh Python bytecode-cache prefix and disables cache writes; `-B` alone can still load stale bytecode after an equal-size edit in the same second. A worker receipt is not sufficient without that independent run. Importing the test, rewriting it, or reusing a receipt from older code cannot satisfy the full acceptance gate. -Run `scripts/xagent-e2e.sh` after changing `bin/xagent*` or upgrading an agent CLI; it calls every vendor. Run `python3 scripts/pstack-host-e2e.py ` for actual Claude and Codex hosts executing swarm then interrogate through their current shared policy on synthetic fixtures. It records source hashes, policy, worker receipts and independent tests, and checks that the parent verified the final source and that each configured reviewer saw it in a separate worktree. It checks `xagent:` entries through their receipts, counting a `meta.json` as an xagent attempt when either marker `bin/xagent` creates first (`prompt.md`, `agent.log`) sits beside it, so a broken one still fails, keyed by mode, agent and model, and listing any other `meta.json` under the evidence directory as ignored, and native entries through the host's own log, parsed by `scripts/e2e-native-evidence.py`: Claude's stream-json Agent calls and their resolved model, or Codex's persisted session rollouts (the Codex host therefore runs without `--ephemeral`). A native dispatch's checkout comes from its subagent's own tool calls: the Claude events whose `parent_tool_use_id` is its Agent call, or the Codex child rollout (whose task payload is encrypted). Those calls must use exactly one checkout under the run other than the parent, and it must be a linked worktree at the fixture base. The dispatch writes when it edited that checkout with its host's file-edit tool and the edit succeeded (Claude `Write`/`Edit` with a successful tool result, a completed Codex `FileChange`), and reviews when it edited nothing there and the checkout holds the final `stats.py`. A native review must also bracket its work, as xagent's `snapshot_before` and `snapshot_after` do: its subagent's own first and last shell commands in that worktree run the absolute path of `scripts/e2e-snapshot.py` with a Python interpreter (optionally after a `cd`, with nothing else in the same command), every other shell command there falls between them, and every snapshot the tool prints for that worktree (leaving ignored test receipts and caches out) matches the worktree's state when the run ends. The snapshot lines come from the subagent's own tool results or Codex `aggregated_output`, which the host cannot write, and an echoed line does not count because it never ran the tool; forging one would take rewriting the tool or shadowing `python3` on PATH, which is outside the threat model of an acceptance test for cooperating agents. A shell edit during a review, before it, or after it therefore stops it counting. Shell edits are not writer evidence, so a writer that edits only through the shell cannot fill the writer slot. When the effective policy sets `reasoning_effort`, a worker fills a role only if its xagent receipt, Claude Agent call or Codex `spawn_agent` records an effort level; the level itself may differ, since poteto-mode adjusts it per task. Each checkout belongs to one owner, a worker (kind, agent and model) in one mode, so an xagent retry or a native writer's follow-up fix may reuse its worktree; and each configured writer or reviewer slot needs an attempt in a checkout no other slot used, so two reviews in one worktree count once. A missing log or a dispatch that used no path is reported as a gap and fails the check. It never changes preferences. Each worker attempt is limited to xagent's default 900 seconds with one retry, not doubled. Both live harnesses use `timeout` with a kill-after limit for the outer host process; host workflows default to 6 attempts × (900-second limit + xagent's termination grace + 30 seconds of start-up and snapshots) + 600 seconds of host work + both final checks, so the writer, review and post-fix re-review rounds can each retry, configurable with `--host-timeout`. Verification commands use `scripts/e2e-process.py` with a 30-second default deadline, process-group termination and bounded output collection; the host harness exposes `--check-timeout`. A verification timeout remains a failed check with logs and exit code 124. +Run `scripts/xagent-e2e.sh` after changing `bin/xagent*` or upgrading an agent CLI; it calls every vendor. Run `python3 scripts/pstack-host-e2e.py ` after changing cross-agent routing: actual Claude and Codex hosts run swarm, then interrogate, through their current shared policy on synthetic fixtures. The host harness accepts a run only on evidence the host cannot write, such as xagent receipts, the host's own logs, snapshots the snapshot tool printed, and its own independent tests, because the host's success report is the thing under test. Its acceptance rules live in the script's module docstring, beside the code and tests that enforce them, so change them there. Neither harness changes preferences. Both bound the outer host process with `timeout` and a kill-after limit, and a verification timeout is a failed check with exit code 124. The runtime checks start with `scripts/check-fork.mjs`, which validates the manifests, all SKILL.md and agent frontmatter, each skill's link to the shared runtime contract, local links, a denylist of the Cursor-only wording this guide replaces, and the files and anchor sentences of the local decisions it requires; see [Fork guards](./docs/ci.md#fork-guards). Use a fresh Codex app-server skills/list and Claude `plugin details` to verify inventory; a static validator alone is not a discovery or behavioral test. diff --git a/scripts/pstack-host-e2e.py b/scripts/pstack-host-e2e.py index 73f7081..ef179fd 100644 --- a/scripts/pstack-host-e2e.py +++ b/scripts/pstack-host-e2e.py @@ -1,5 +1,70 @@ #!/usr/bin/env python3 -"""Exercise real swarm -> interrogate policy routing from Claude and Codex on a synthetic repo.""" +"""Exercise real swarm -> interrogate policy routing from Claude and Codex on a synthetic repo. + +Actual Claude and Codex hosts run swarm, then interrogate, through their current +shared policy on synthetic fixtures. The run records source hashes, policy, +worker receipts and independent tests. It never changes preferences. A run +passes only on the evidence below; the host's own report never counts. + +Parent and reviewers. The parent verified the final source, and each configured +reviewer saw it in a separate worktree. + +xagent entries are checked through their receipts. A meta.json counts as an +xagent attempt when either marker bin/xagent creates first (prompt.md, +agent.log) sits beside it, so a broken attempt still fails. Attempts are keyed +by mode, agent and model; any other meta.json under the evidence directory is +listed as ignored. + +Native entries are checked through the host's own log, parsed by +scripts/e2e-native-evidence.py: Claude's stream-json Agent calls and their +resolved model, or Codex's persisted session rollouts. The Codex host therefore +runs without --ephemeral. + +- Checkout. A native dispatch's checkout comes from its subagent's own tool + calls: the Claude events whose parent_tool_use_id is its Agent call, or the + Codex child rollout, whose task payload is encrypted. Those calls must use + exactly one checkout under the run other than the parent, and it must be a + linked worktree at the fixture base. +- Writer or reviewer. The dispatch writes when it edited that checkout with its + host's file-edit tool and the edit succeeded (Claude Write/Edit with a + successful tool result, a completed Codex FileChange). It reviews when it + edited nothing there and the checkout holds the final stats.py. Shell edits + are not writer evidence, so a writer that edits only through the shell cannot + fill the writer slot. +- Review bracket. A native review brackets its work, as xagent's + snapshot_before and snapshot_after do. Its subagent's own first and last + shell commands in that worktree run the absolute path of + scripts/e2e-snapshot.py with a Python interpreter, optionally after a cd and + with nothing else in the same command. Every other shell command there falls + between them, and every snapshot the tool prints for that worktree (leaving + ignored test receipts and caches out) matches the worktree's state when the + run ends. A shell edit during a review, before it, or after it therefore stops + it counting. +- Snapshot provenance. The snapshot lines come from the subagent's own tool + results or Codex aggregated_output, which the host cannot write. An echoed + line does not count, because it never ran the tool. Forging one would take + rewriting the tool or shadowing python3 on PATH, which is outside the threat + model of an acceptance test for cooperating agents. + +Effort. When the effective policy sets reasoning_effort, a worker fills a role +only if its xagent receipt, Claude Agent call or Codex spawn_agent records an +effort level. The level itself may differ, since poteto-mode adjusts it per +task. + +Ownership. Each checkout belongs to one owner, a worker (kind, agent and model) +in one mode, so an xagent retry or a native writer's follow-up fix may reuse its +worktree. Each configured writer or reviewer slot needs an attempt in a checkout +no other slot used, so two reviews in one worktree count once. A missing log or +a dispatch that used no path is reported as a gap and fails the check. + +Time limits. Each worker attempt gets xagent's default limit with one retry, +not doubled. The outer host process runs under timeout with a kill-after limit; +the default --host-timeout lets the writer, review and post-fix re-review rounds +each retry, and --help prints its arithmetic. Verification commands run through +scripts/e2e-process.py with a default deadline (--check-timeout), +process-group termination and bounded output collection. A verification +timeout remains a failed check with logs and exit code 124. +""" from __future__ import annotations import argparse @@ -617,7 +682,7 @@ def exercise(host, run, host_timeout, check_timeout=30): def main(): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(description=__doc__.partition("\n")[0], epilog="Acceptance rules: the module docstring of scripts/pstack-host-e2e.py.") parser.add_argument("run_dir", type=Path) parser.add_argument("--host", choices=tuple(HOSTS), action="append") parser.add_argument("--host-timeout", type=int, help=( From a22413f7aa7170e872c7156d7eadfaa53d42bcef Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:26:41 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20AGENTS.md?= =?UTF-8?q?=EF=BC=8C=E5=86=99=E6=98=8E=E9=97=A8=E7=A6=81=E3=80=81=E4=B8=8A?= =?UTF-8?q?=E6=B8=B8=E5=90=8C=E6=AD=A5=E4=B8=8E=E5=AE=89=E8=A3=85=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..84edd31 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Agent guide + +This repository is the Codex and Claude Code fork of cursor/plugins `pstack`, pinned in `UPSTREAM.json`. [ADAPTATION.md](ADAPTATION.md) records each local decision and its reason. + +- Gate: `bash scripts/check.sh` (`runtime` or `build` for one half). On macOS, put coreutils' `gnubin` first in `PATH` as [docs/ci.md](docs/ci.md) shows. +- Upstream sync: run `node scripts/check-upstream.mjs`, then merge each changed file by the rules in [Upstream sync](ADAPTATION.md#upstream-sync). Never copy an upstream directory over this tree. +- A [fork guard](docs/ci.md#fork-guards) failure means a sync dropped a recorded decision or brought back Cursor-only wording. Fix the text, not the guard. +- Host caches and installed copies are generated installation artifacts, never edit targets. Edit this checkout. +- After maintained-source changes, refresh the installations with the commands in the "After maintained-source changes" paragraph under [T3 Code overlay](ADAPTATION.md#t3-code-overlay). From 09f2ac3c1a1946dc771bf47f1f5629f0b6ceb9c3 Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:33:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(ci):=20=E5=86=99=E6=98=8E=20main=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=BF=9D=E6=8A=A4=E5=B7=B2=E5=BC=80=E5=90=AF?= =?UTF-8?q?=E5=8F=8A=E6=A3=80=E6=9F=A5=E5=90=8D=E4=B8=8E=20matrix=20?= =?UTF-8?q?=E7=9A=84=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 分支保护已对 main 生效(含管理员),三个必需检查只认 GitHub Actions。 检查名由 job name 和 matrix 的 os/node/python 拼成,改版本或 job 名的 PR 须同时更新保护规则,否则一直等待旧检查名。补充 PUT 更新(整体替换, 须带全部设置)、回读与 DELETE 撤销命令。 Co-Authored-By: Claude Opus 5.5 --- docs/ci.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/ci.md b/docs/ci.md index 5b8d404..c9a71da 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -74,7 +74,43 @@ Each job has a ten-minute limit. A newer run on the same PR or ref cancels the o The macOS runtime job installs coreutils. Ubuntu already supplies GNU `timeout`. -After the first successful GitHub run, select all three check names in branch protection if they must block merges. Adding the workflow does not configure branch protection. +### Branch protection + +`main` is protected, and the protection also applies to administrators (`enforce_admins`). GitHub rejects direct pushes to `main`, so every change lands through a pull request. A merge needs these three checks, reported by GitHub Actions (app ID 15368): + +- `Runtime (ubuntu-24.04, Node 22.18.0, Python 3.9)` +- `Runtime (macos-15, Node 24, Python 3.14)` +- `Build and types` + +A branch does not need to be up to date with `main` before it merges (`strict` is off), no review approval is required, and force pushes to `main` and its deletion are blocked. + +Each check name is the job's `name` in the [workflow](../.github/workflows/ci.yml), with the matrix `os`, `node`, and `python` values filled in. A pull request that changes a matrix value or a job name reports its checks under new names. It never reports the old required names, so it waits for them indefinitely, and `enforce_admins` leaves no bypass. Update the protection when that pull request is ready to merge. Open pull requests that ran before the change keep the old names until CI runs on them again. `PUT` replaces the whole protection, so send every setting, not only the new check list: + +```sh +gh api -X PUT repos/mtmtian/pstack-remix/branches/main/protection --input - <<'EOF' +{ + "required_status_checks": { + "strict": false, + "checks": [ + { "context": "Runtime (ubuntu-24.04, Node 22.18.0, Python 3.9)", "app_id": 15368 }, + { "context": "Runtime (macos-15, Node 24, Python 3.14)", "app_id": 15368 }, + { "context": "Build and types", "app_id": 15368 } + ] + }, + "enforce_admins": true, + "required_pull_request_reviews": null, + "restrictions": null, + "allow_force_pushes": false, + "allow_deletions": false +} +EOF +``` + +Read the result back with `gh api repos/mtmtian/pstack-remix/branches/main/protection`. To remove the protection entirely: + +```sh +gh api -X DELETE repos/mtmtian/pstack-remix/branches/main/protection +``` ## Checks that still require a host