@justjoshn/pi-pstack 0.15.36-pi.1 → 0.15.36-pi.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -12
  3. package/agents/ci-watcher.md +27 -0
  4. package/agents/thermo-nuclear-code-quality-review.md +23 -0
  5. package/assets/logo.png +0 -0
  6. package/automations/benny/FOR_AGENTS.md +1 -1
  7. package/automations/benny/README.md +2 -2
  8. package/automations/benny/skills/setup-benny/SKILL.md +5 -5
  9. package/bin/pstack-routines-supervisor +9 -0
  10. package/docs/guide/07-overnight.md +1 -1
  11. package/docs/guide/09-make-it-yours.md +1 -1
  12. package/docs/guide/10-recipes-and-pitfalls.md +1 -1
  13. package/docs/native-gaps.md +11 -5
  14. package/docs/parity-reverse-engineering.md +94 -0
  15. package/package.json +41 -6
  16. package/packaging/install-supervisor-unit.sh +164 -0
  17. package/packaging/launchd/com.pstack.routines-supervisor.plist.example +24 -0
  18. package/packaging/systemd/pstack-routines-supervisor.service.example +15 -0
  19. package/skills/architect/SKILL.md +1 -1
  20. package/skills/architect/references/runner-prompt.md +1 -1
  21. package/skills/arena/SKILL.md +3 -3
  22. package/skills/check-compiler-errors/SKILL.md +23 -0
  23. package/skills/create-skill/SKILL.md +515 -29
  24. package/skills/fix-ci/SKILL.md +29 -0
  25. package/skills/fix-merge-conflicts/SKILL.md +32 -0
  26. package/skills/get-pr-comments/SKILL.md +23 -0
  27. package/skills/how/SKILL.md +1 -1
  28. package/skills/ketchup-video/SKILL.md +2 -2
  29. package/skills/loop-on-ci/SKILL.md +50 -0
  30. package/skills/make-bot-ui/SKILL.md +2 -2
  31. package/skills/make-pr-easy-to-review/SKILL.md +59 -0
  32. package/skills/new-branch-and-pr/SKILL.md +29 -0
  33. package/skills/no-inline-imports/SKILL.md +12 -0
  34. package/skills/poteto-help/SKILL.md +4 -4
  35. package/skills/poteto-mode/playbooks/multi-phase-plan.md +1 -1
  36. package/skills/poteto-mode/playbooks/orchestrate.md +1 -1
  37. package/skills/pr-review-canvas/SKILL.md +176 -0
  38. package/skills/pr-review-canvas/renderer.js +170 -0
  39. package/skills/pr-review-canvas/styles.css +96 -0
  40. package/skills/pr-review-canvas/template.html +18 -0
  41. package/skills/reflect/SKILL.md +1 -1
  42. package/skills/review-and-ship/SKILL.md +41 -0
  43. package/skills/run-smoke-tests/SKILL.md +42 -0
  44. package/skills/statechart/scripts/test/cli.test.ts +1 -1
  45. package/skills/swarm/SKILL.md +2 -2
  46. package/skills/thermo-nuclear-code-quality-review/SKILL.md +192 -0
  47. package/skills/typescript-exhaustive-switch/SKILL.md +10 -0
  48. package/skills/verify-this/SKILL.md +74 -0
  49. package/skills/weekly-review/SKILL.md +32 -0
  50. package/skills/what-did-i-get-done/SKILL.md +31 -0
  51. package/skills/why/SKILL.md +1 -1
  52. package/skills/workflow-from-chats/SKILL.md +50 -0
  53. package/src/mode.ts +3 -3
  54. package/src/paths.ts +13 -0
  55. package/src/pi-command.ts +12 -0
  56. package/src/rule.ts +4 -4
  57. package/src/sections.ts +2 -1
  58. package/src/subscriptions/durable-store.ts +482 -0
  59. package/src/subscriptions/durable-types.ts +41 -0
  60. package/src/subscriptions/ensure-supervisor.ts +67 -0
  61. package/src/subscriptions/index.ts +231 -271
  62. package/src/subscriptions/supervisor-main.ts +88 -0
  63. package/src/subscriptions/supervisor.ts +271 -0
  64. package/src/subscriptions/webhook.ts +22 -9
  65. package/src/task/card.ts +29 -0
  66. package/src/task/records.ts +3 -8
  67. package/src/task/runner.ts +9 -14
  68. package/src/task/tool.ts +18 -21
  69. package/src/todo.ts +1 -2
  70. package/scripts/live-e2e.mjs +0 -257
  71. package/test/aliases.test.ts +0 -57
  72. package/test/ask.test.ts +0 -85
  73. package/test/autocomplete.test.ts +0 -39
  74. package/test/e2e/aliases.e2e.test.ts +0 -81
  75. package/test/e2e/ask.e2e.test.ts +0 -135
  76. package/test/e2e/harness.ts +0 -179
  77. package/test/e2e/host.e2e.test.ts +0 -193
  78. package/test/e2e/lifecycle.e2e.test.ts +0 -99
  79. package/test/e2e/mode.e2e.test.ts +0 -160
  80. package/test/e2e/package.e2e.test.ts +0 -67
  81. package/test/e2e/task.e2e.test.ts +0 -734
  82. package/test/e2e/todo.e2e.test.ts +0 -59
  83. package/test/fixtures/cursor-askquestion-calls.json +0 -806
  84. package/test/fixtures/fake-pi.mjs +0 -96
  85. package/test/fixtures/image-model.ts +0 -16
  86. package/test/fixtures/list-models.ts +0 -15
  87. package/test/fixtures/readonly-shell-exit.ts +0 -17
  88. package/test/fixtures/scripted-model.ts +0 -113
  89. package/test/loop.test.ts +0 -209
  90. package/test/mode.test.ts +0 -143
  91. package/test/plan.test.ts +0 -126
  92. package/test/readonly-shell-security.test.ts +0 -84
  93. package/test/readonly-shell.test.ts +0 -44
  94. package/test/records.test.ts +0 -143
  95. package/test/registry.test.ts +0 -18
  96. package/test/resources.test.ts +0 -162
  97. package/test/rule.test.ts +0 -92
  98. package/test/scripts.test.ts +0 -182
  99. package/test/sections.test.ts +0 -56
  100. package/test/subscriptions-cron.test.ts +0 -60
  101. package/test/subscriptions.test.ts +0 -598
  102. package/test/task-agents.test.ts +0 -49
  103. package/test/task-card.test.ts +0 -34
  104. package/test/task-fork.test.ts +0 -233
  105. package/test/task-format.test.ts +0 -65
  106. package/test/task-model.test.ts +0 -141
  107. package/test/task-runner.test.ts +0 -259
  108. package/test/task-schema.test.ts +0 -50
  109. package/test/task-status.test.ts +0 -155
  110. package/test/task-worktree.test.ts +0 -106
  111. package/test/todo.test.ts +0 -74
  112. package/test/visualize.test.ts +0 -152
  113. package/tsconfig.json +0 -5
  114. package/vitest.config.ts +0 -16
@@ -6,7 +6,7 @@ disable-model-invocation: true
6
6
 
7
7
  # Ketchup video
8
8
 
9
- Follow [`/ketchup`](../ketchup/SKILL.md), but show what happened in a short subtitled video instead of its summary and visuals. Its window starts at the last message the user wrote before `/ketchup-video`, not before any earlier `/ketchup`. Writing and rendering the video, outside the repo, is the one exception to its read-only rule. The reply is ketchup's opening sentence, the embedded video, one line naming any work left that you can do yourself, and the action items. When the video shows a call's options, call each option by the name the video gives it. If the render fails, say so in one line and give only the calls.
9
+ Follow [`/ketchup`](../ketchup/SKILL.md), but show what happened in a short subtitled video instead of its summary and visuals. Its window starts at the last message the user wrote before `/ketchup-video`, not before any earlier `/ketchup`. Writing and rendering the video, outside the repo, is the one exception to its read-only rule. After render and frame QC, open the mp4 with the OS player (`open` on macOS, `xdg-open` on Linux), read one frame per slide with the read tool for terminals that support images, and name the video path in the reply. Pi chat does not play video inside a reply, so do not assume Markdown embedding plays it. The reply is ketchup's opening sentence, that video delivery (open, path, and frames), one line naming any work left that you can do yourself, and the action items. When the video shows a call's options, call each option by the name the video gives it. If the render fails, say so in one line and give only the calls.
10
10
 
11
11
  ## Slides
12
12
 
@@ -26,4 +26,4 @@ Give each slide one short subtitle line, about ten words, that says its point th
26
26
 
27
27
  Render one silent landscape video, such as an mp4, with motion between slides. Every frame is the slide itself, edge to edge, above its subtitle strip. Make the frames from the slides, never by recording a screen, and burn each subtitle into its slide's frames so it stays on screen for the slide's whole hold. The hold is the time a slide is fully on screen, not counting the motion into or out of it. Hold each slide long enough to read the words it adds, at least one second for each three words. A slide adds its subtitle and each word that was not on the slide before it, including a label it changes. The first slide adds all its words. Words inside a screenshot do not count.
28
28
 
29
- Before you embed the video, check the rendered file. Look at one frame from each slide at chat size, and check that its subtitle is readable and covers nothing. Measure each hold by matching the file's frames to the slide, not with a still-frame detector, which counts the faint ends of a fade as still. Count the words each slide adds against its measured hold. Fix any slide you cannot read in that time, render again, and check again.
29
+ Before you deliver the video, check the rendered file. Look at one frame from each slide at chat size, and check that its subtitle is readable and covers nothing. Measure each hold by matching the file's frames to the slide, not with a still-frame detector, which counts the faint ends of a fade as still. Count the words each slide adds against its measured hold. Fix any slide you cannot read in that time, render again, and check again. Then open the file in the system player, read one frame per slide into the reply, and name the path.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: loop-on-ci
3
+ description: Monitor PR checks and fix failures until green. Uses gh pr checks as the source of truth for PR-attached checks.
4
+ ---
5
+
6
+ # Loop on CI
7
+
8
+ ## Trigger
9
+
10
+ Need to watch a branch or pull request and iterate on CI failures until all required checks are green.
11
+
12
+ Use `gh pr checks` as the source of truth. It includes all PR-attached checks, while `gh run list` only covers GitHub Actions.
13
+
14
+ ## Workflow
15
+
16
+ 1. Resolve the PR for the current branch.
17
+ 2. Inspect current PR checks before waiting.
18
+ 3. If checks already failed, diagnose those failures first.
19
+ 4. If checks are pending, watch with `gh pr checks --watch --fail-fast`.
20
+ 5. After each push, re-check the full PR check set and repeat until green.
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ # Resolve the active PR
26
+ gh pr view --json number,url,headRefName
27
+
28
+ # Inspect all attached checks
29
+ gh pr checks --json name,bucket,state,workflow,link
30
+
31
+ # Watch pending checks and fail fast
32
+ gh pr checks --watch --fail-fast
33
+
34
+ # GitHub Actions logs, when the failing check links to a GHA run
35
+ gh run view <run-id> --log-failed
36
+ ```
37
+
38
+ ## Guardrails
39
+
40
+ - Keep each fix scoped to a single failure cause when possible.
41
+ - Do not bypass hooks (`--no-verify`) to force progress.
42
+ - If the failure is clearly unrelated to the PR and appears fixed on main, merge latest main instead of bloating the PR with unrelated fixes.
43
+ - If failures are flaky, retry once and report flake evidence.
44
+ - Re-run `gh pr checks --json name,bucket,state,workflow,link` after every push; the check set can change.
45
+
46
+ ## Output
47
+
48
+ - Current CI status
49
+ - Failure summary and fixes applied
50
+ - PR URL once checks are green
@@ -20,7 +20,7 @@ Call `subscribe_webhook`. Set these fields:
20
20
  The routine slug is the kebab-case form of the name.
21
21
  The result gives the routine URL, `http://127.0.0.1:<port>/routines/<slug>`, and the credential file path. Use the path the result names, such as `~/.pi/agent/pstack/routines/<slug>/credentials.json` (`PI_CODING_AGENT_DIR` moves it).
22
22
  The result does not include the sender key.
23
- The routine lives in this Pi process: it stops when Pi exits and expires at the `expiresAt` the result names. Keep this Pi session running while the UI is in use, and check it with `list_subscriptions`.
23
+ The routine targets the owning Pi session and expires at the `expiresAt` the result names. The durable supervisor keeps the listener available after the chat exits. Wakes received while the owner is offline park until that session is restored. Keep the owner running for immediate UI actions, and check the routine with `list_subscriptions`. A timer with `sessionStrategy: "new_session"` can start detached work while the owner is offline. A webhook does not use that strategy.
24
24
 
25
25
  ## Copy the URL and the sender key
26
26
 
@@ -55,7 +55,7 @@ The server POSTs to the routine URL with:
55
55
  - timeout: 8 seconds
56
56
  - one try, no retry
57
57
 
58
- The POST returns HTTP 200 when the routine wakes.
58
+ The POST returns HTTP 200 when the routine accepts the event. This does not prove that the owning session has handled it.
59
59
  Before you tell the user that the UI is live, probe once with a harmless payload.
60
60
  Use an action that the prompt ignores.
61
61
 
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: make-pr-easy-to-review
3
+ description: Prepare PRs for review by cleaning noisy history, improving PR descriptions, and adding reviewer guidance without changing code behavior. Use for "make this easy to review", "tidy this PR", "clean up commits", or "annotate the diff".
4
+ ---
5
+
6
+ # Make PR Easy to Review
7
+
8
+ Prepare a PR so a reviewer can quickly understand the intent, important files, and risk. The default goal is reviewability without behavior changes.
9
+
10
+ ## Workflow
11
+
12
+ 1. Resolve the target PR from the user-provided URL or current branch.
13
+ 2. Inspect commits, diff size, changed paths, generated files, and PR description.
14
+ 3. Identify reviewability issues: noisy commits, stale description, unrelated changes, mixed mechanical and logic changes, missing tests, or unclear reviewer entry points.
15
+ 4. Propose a plan before rewriting history or force-pushing.
16
+ 5. Apply safe improvements, then verify the tree or diff still matches the intended code.
17
+
18
+ ## History Cleanup
19
+
20
+ Only rewrite history when the user asks for it or agrees to the plan. Before rewriting:
21
+
22
+ ```bash
23
+ gh pr view <PR> --json title,headRefName,baseRefName,state,commits
24
+ git fetch origin <headRefName> <baseRefName>
25
+ ORIGINAL_TREE=$(git rev-parse origin/<headRefName>^{tree})
26
+ ```
27
+
28
+ Good commit groupings usually follow dependency order:
29
+
30
+ 1. Schema/storage or generated API definitions.
31
+ 2. Core logic.
32
+ 3. Wiring and integration.
33
+ 4. UI or surface behavior.
34
+ 5. Tests.
35
+
36
+ After rewriting, verify content identity:
37
+
38
+ ```bash
39
+ echo "Original tree: $ORIGINAL_TREE"
40
+ echo "Current tree: $(git rev-parse HEAD^{tree})"
41
+ git diff origin/<headRefName> --stat
42
+ ```
43
+
44
+ Do not push if the tree changed unintentionally.
45
+
46
+ ## Reviewer Guidance
47
+
48
+ When code behavior should stay untouched, prefer PR description and review notes:
49
+
50
+ - Add a TL;DR that matches the actual diff.
51
+ - Separate core files from generated or mechanical files.
52
+ - Call out risky behavior changes, migration order, rollout plan, and test coverage.
53
+ - Link issue trackers, dashboards, or design docs when they explain intent.
54
+
55
+ ## Guardrails
56
+
57
+ - Never hide meaningful behavior changes inside "cleanup".
58
+ - Do not bypass hooks unless the user explicitly asks.
59
+ - If the PR is too large to make reviewable with notes, recommend splitting instead of polishing around the problem.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: new-branch-and-pr
3
+ description: Create a fresh branch, complete work, and open a pull request
4
+ ---
5
+
6
+ # New branch and PR
7
+
8
+ ## Trigger
9
+
10
+ Starting work that should be shipped through a clean branch and pull request workflow.
11
+
12
+ ## Workflow
13
+
14
+ 1. Ensure the working tree is clean or explicitly handled.
15
+ 2. Create a descriptive branch from the latest main.
16
+ 3. Complete implementation and tests.
17
+ 4. Commit focused changes and push.
18
+ 5. Create a concise PR with summary and test notes.
19
+
20
+ ## Guardrails
21
+
22
+ - Keep branch scope focused on one change set.
23
+ - Include verification notes before requesting review.
24
+
25
+ ## Output
26
+
27
+ - New branch name
28
+ - PR summary and test notes
29
+ - PR URL
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: no-inline-imports
3
+ description: Keep imports at top of file and avoid inline imports
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # No Inline Imports
8
+
9
+ # No inline imports
10
+
11
+ Always place imports at the top of the module. Avoid inline imports in function bodies, type annotations, or interface fields unless there is a strict circular-dependency reason and it is documented.
12
+
@@ -24,7 +24,7 @@ Infer the need from the message and the conversation. A named situation, such as
24
24
 
25
25
  Check the state that changes the answer, and mention it only when it does:
26
26
 
27
- - No `~/.pi/agent/pstack/pstack-models.md` means `/setup-pstack` hasn't run for this user, so every role uses its default model.
27
+ - If the model rule is absent at the path the system prompt names, every role uses its default model. Do not infer absence from the default path when `PI_CODING_AGENT_DIR` relocates it.
28
28
  - No `verify-*` skill or other app harness in the project means agents have no scripted way to drive the app. Mention `/create-verification-skill` when the question is about proving a change works.
29
29
  - No formal verification skill in the project means no property is checked past what tests reach. Mention `/create-formal-verification-skill` when the question is about a race, an invariant, or another case tests rarely hit. When one exists and the code it covers changed after the commit it records as last in sync, mention `/maintain-formal-verification-skill` if the question touches that code.
30
30
 
@@ -39,7 +39,7 @@ When the model rule is missing and it matters, ask whether the user wants to pic
39
39
  2. Run [`/setup-pstack`](../setup-pstack/SKILL.md). It asks for a reasoning budget, maps a model to each role, and writes the model rule. The rule applies from the next request. Then it offers once each to generate a verification skill and a formal verification skill for the project, unless the project already has one.
40
40
  3. Start a real task with `/poteto-mode`, a goal, and a check that can pass or fail.
41
41
 
42
- Installing adds pi-pstack's tools and system-prompt sections, and no skill runs until the user invokes one. Only `/setup-pstack`, and the `/create-skill`, `/deslop`, `control-cli`, and `control-ui` skills pi-pstack ships outside pstack, load from the user's words. The [README](../../README.md) and [guide page 1](../../docs/guide/01-setup.md) have the details. Offer to word their first prompt with them, per [`references/prompting.md`](references/prompting.md).
42
+ Installing adds pi-pstack's tools and system-prompt sections. Pi advertises `/setup-pstack` and bundled skills whose frontmatter does not set `disable-model-invocation: true`. The model may read those skills when the user's request matches. Most pstack workflow skills stay hidden from automatic selection and load through an explicit command or another skill's instructions. The [README](../../README.md) and [guide page 1](../../docs/guide/01-setup.md) have the details. Offer to word their first prompt with them, per [`references/prompting.md`](references/prompting.md).
43
43
 
44
44
  If cost is the worry, say where the tokens go and how to spend fewer. pstack spends extra tokens on subagents and review panels. Rerun `/setup-pstack` and pick a smaller budget or cheaper models. A role set to `auto` or `inherit-parent` runs on the chat's model, which saves tokens when the chat runs on a cheaper model. A shorter panel list runs fewer subagents, one for each entry. Save `/poteto-mode` for work that needs rigor.
45
45
 
@@ -140,8 +140,8 @@ Principles are one-rule skills that `/poteto-mode` reads and cites in its replie
140
140
  | A question got treated as the next step of the last task | Say "new task", or say the turn doesn't need the mode. |
141
141
  | A new model choice had no effect | The rule from `/setup-pstack` applies from the next request. Send one. |
142
142
  | Runs cost more than expected | See the cost paragraph under Get set up. |
143
- | A skill didn't load on its own | Only `/setup-pstack`, and the `/create-skill`, `/deslop`, `control-cli`, and `control-ui` skills pi-pstack ships outside pstack, load from the user's words. The others load when the user types them or when `/poteto-mode` runs them, and it doesn't run every skill. |
144
- | Parallel agents overwrote each other | Give each agent its own worktree, or run them as isolated workers (`environment: "cloud"`), which each get their own git worktree. |
143
+ | A skill didn't load on its own | Check its `disable-model-invocation` frontmatter. Hidden skills load when the user types their command or another skill tells the model to read them. `/poteto-mode` does not run every skill. |
144
+ | Parallel agents overwrote each other | Give each agent its own git worktree with `environment: "cloud"`. They still share this machine. Assign distinct ports, output directories, and writable cache paths to concurrent runtime lanes. Stronger machine isolation is not provided. |
145
145
  | An overnight run moved but finished nothing | `/loop` needs a check that can pass or fail, not a duration. See [guide page 7](../../docs/guide/07-overnight.md). |
146
146
  | The reply claims success from a green build | Ask for the real command, flow, stored value, or profile. That's the prove-it-works principle. |
147
147
 
@@ -68,7 +68,7 @@ Tests alone are not sufficient verification. A PR is verified only when its unit
68
68
 
69
69
  ### Boot recipe, for every live lane
70
70
 
71
- Each live lane runs as its own isolated worker (`environment: "cloud"`) at the PR head. Drive through `control-ui` or `control-cli`, shipped with pi-pstack.
71
+ Each live lane runs in its own git worktree (`environment: "cloud"`) at the PR head on this machine. Assign each concurrent lane distinct ports, output directories, and writable cache paths. A worktree does not provide VM or credential isolation. If the lane needs stronger machine isolation, mark it blocked until that isolation is available. Drive through `control-ui` or `control-cli`, shipped with pi-pstack.
72
72
 
73
73
  - [ ] `git fetch origin <head-branch> && git checkout <head SHA>`.
74
74
  - [ ] <Start the backend and the surface. Wait for ready.>
@@ -7,7 +7,7 @@ Run it with your own tools. Spawn workers with `Task`. Keep the program's state
7
7
  #### Topology
8
8
 
9
9
  - **One owner per PR.** The owner is the only writer of its branch and carries the PR from build to merge. Only the owner pushes, rebases, or retargets it. Closing or retargeting a base PR orphans every PR above it, so the owner does that only on your call. You never author or edit code. Conflicted merges, chain rebases, and fixes go to owners.
10
- - **A fresh worker for each independent task.** Its kickoff carries everything it needs, including each upstream report it depends on, pasted in full. Workers can't see their siblings. Run workers as a new isolated worktree worker (`environment: "cloud"`) unless the work needs this machine: a `control-ui` or `control-cli` runtime check (shipped with pi-pstack), local transcripts, a simulator, or auth that exists only here. Pick each subagent's reasoning level (the `:<level>` suffix), and fast or standard speed when the model has a `-fast` variant, for the job, per the Subagents section.
10
+ - **A fresh worker for each independent task.** Its kickoff carries everything it needs, including each upstream report it depends on, pasted in full. Workers can't see their siblings. Run workers in a new git worktree (`environment: "cloud"`) unless the work needs the existing checkout or its uncommitted state. Both environments run on this machine and can access its local transcripts, simulators, and auth. Assign distinct ports, output directories, and writable cache paths to concurrent runtime workers, including `control-ui` and `control-cli` checks. A worktree is not a machine or credential boundary. Report stronger isolation as unavailable when the task requires it. Pick each subagent's reasoning level (the `:<level>` suffix), and fast or standard speed when the model has a `-fast` variant, for the job, per the Subagents section.
11
11
  - **A sub-coordinator only when one area has many parallel workers.** It owns that area's workers and kickoffs and sends you one rollup, never raw worker reports. Stop at three levels: you, sub-coordinators, and workers. Keep about ten workers in flight per coordinator, and start the next one as each finishes instead of waiting on a whole batch.
12
12
  - **Fork with Task `fork` when new work needs a worker's built state,** such as its checkout, uncommitted changes, or a running process. Message a worker with Task `resume` only for a follow-up on the same work. Never message one to check on it, because a message restarts an idle agent.
13
13
  - **Each PR gets a verifier that isn't its author,** on a different model family. The owner merges only on that verifier's pass at the current head.
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: pr-review-canvas
3
+ disable-model-invocation: true
4
+ description: Generate an interactive PR review walkthrough as an HTML page. Fetches PR data via gh API, categorizes files into core vs mechanical changes, adds reviewer annotations, and renders diffs with moved-code detection. Use when the user pastes a GitHub PR URL and asks for a review, walkthrough, or summary, or says "review this PR".
5
+ ---
6
+
7
+ # PR Review Canvas
8
+
9
+ Generate an interactive HTML review of a GitHub PR that reads like a peer walking you through what matters.
10
+
11
+ ## Workflow
12
+
13
+ ### 1. Fetch PR data
14
+
15
+ Run these `gh api` calls in parallel:
16
+
17
+ ```bash
18
+ gh api repos/{owner}/{repo}/pulls/{number} --jq '{title, body, user: .user.login, state, additions, deletions, changed_files, base: .base.ref, head: .head.ref}'
19
+ gh api repos/{owner}/{repo}/pulls/{number}/files --paginate --jq '.[] | {filename, status, additions, deletions, patch}'
20
+ gh api repos/{owner}/{repo}/pulls/{number}/comments --jq '.[] | {user: .user.login, body, path, line}'
21
+ ```
22
+
23
+ ### 2. Analyze the PR and write the body HTML
24
+
25
+ Read the diffs, understand the PR, and write the `<body>` content directly as HTML. You have full creative freedom -- the goal is to explain the PR clearly to a reviewer. Use whatever structure best fits the PR.
26
+
27
+ **Typical structure** (adapt as needed):
28
+ - Header with title, PR number, author, stats
29
+ - Summary box explaining what the PR does in plain English
30
+ - Core file sections with annotations and diffs
31
+ - Mechanical/boilerplate files collapsed by default
32
+ - Review checklist at the bottom
33
+
34
+ **But you can also add:**
35
+ - **Pseudocode summaries** for verbose code -- show the algorithm in plain English or short pseudocode, with the real diff collapsed below (use a `.bp-section` card labeled "Show full implementation"). Great when 150 lines of retry/backoff/error-handling code is really just "fetch with exponential backoff and circuit breaker."
36
+ - Diagrams (inline SVG, mermaid via CDN, ASCII art in `<pre>`)
37
+ - Flowcharts showing before/after control flow
38
+ - Tables comparing old vs new behavior
39
+ - Callout boxes for warnings, questions, or gotchas
40
+ - Interactive widgets if they help
41
+ - Anything else that makes the review clearer
42
+
43
+ **Pseudocode pattern example:**
44
+ ```html
45
+ <div class="file-card">
46
+ <div class="file-hdr" onclick="toggle(this)">
47
+ <span class="fname">retryClient.ts</span>
48
+ <div class="fstats"><span class="pill add">+173</span><span class="pill del">&minus;11</span><span class="chev open">&#9654;</span></div>
49
+ </div>
50
+ <div class="file-body open">
51
+ <div class="file-note">
52
+ <strong>What this does in plain English:</strong>
53
+ <pre style="margin-top:8px;color:var(--text);font-size:12px;line-height:1.6;">
54
+ fetch(url):
55
+ if circuit breaker is open → fail fast
56
+ retry up to N times:
57
+ try fetch with timeout
58
+ on success → close circuit breaker, return
59
+ on retryable error → wait (exponential backoff + jitter)
60
+ on non-retryable error → throw
61
+ circuit breaker records failure</pre>
62
+ </div>
63
+ <div class="bp-section" style="margin:0;border:0;border-radius:0;">
64
+ <div class="bp-hdr" onclick="toggleBP(this)">
65
+ <span>Show full implementation (+173 lines)</span><span class="chev">&#9654;</span>
66
+ </div>
67
+ <div class="bp-body"><div data-diff="retryClient"></div></div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ ```
72
+
73
+ ### 3. Available CSS classes and JS utilities
74
+
75
+ Read [styles.css](styles.css) and [renderer.js](renderer.js) from this skill directory. These give you a prebuilt dark-themed toolkit. Inject them into [template.html](template.html) verbatim.
76
+
77
+ **CSS classes you can use:**
78
+
79
+ | Class | Purpose |
80
+ |-------|---------|
81
+ | `.header`, `.header h1`, `.header-meta` | Page header |
82
+ | `.pill.add`, `.pill.del`, `.pill.files` | Stat badges (+N, -N, N files) |
83
+ | `.content` | Centered content wrapper (max 900px) |
84
+ | `.summary` | Summary/TL;DR box |
85
+ | `.section-title` | Section heading with bottom border |
86
+ | `.ic` | Inline code reference (mono, blue, dark bg) |
87
+ | `.file-card`, `.file-hdr`, `.file-body` | Collapsible file card (use `onclick="toggle(this)"` on `.file-hdr`) |
88
+ | `.file-note` | Sticky reviewer annotation inside a file card |
89
+ | `.bp-section`, `.bp-hdr`, `.bp-body` | Collapsed boilerplate card (use `onclick="toggleBP(this)"`) |
90
+ | `.bp-note` | Note inside a boilerplate card |
91
+ | `.verdict` | Review checklist box |
92
+
93
+ **JS functions available:**
94
+
95
+ | Function | Usage |
96
+ |----------|-------|
97
+ | `toggle(hdrElement)` | Toggle a `.file-body` open/closed |
98
+ | `toggleBP(hdrElement)` | Toggle a `.bp-body` open/closed |
99
+ | `renderDiff(target, diffInput)` | Render a unified diff. `target` can be a DOM element, string ID, or CSS selector. `diffInput` can be a raw patch string OR an array of lines -- both work. Automatically filters imports, collapses whitespace-only changes, detects moved code (blue/purple tint). |
100
+ | `esc(string)` | HTML-escape a string |
101
+
102
+ **Rendering diffs -- use `data-diff` attributes with auto-discovery.**
103
+ Put `<div data-diff="KEY"></div>` placeholders in your body HTML wherever you want a diff rendered. The renderer finds them automatically after DOM load and fills them from the `<script id="pr-diffs-json" type="application/json">` element in `template.html`.
104
+
105
+ **CRITICAL: Patch strings can contain `</script>` in addition to newlines, backslashes, and quotes.** Even `json.dumps(...)` is not enough if you paste raw output into executable `<script>` because HTML parsing can terminate the tag early. Never manually embed patch strings in JS/JSON. Instead, use this safe approach:
106
+
107
+ 1. During the fetch step, save patches to a JSON file using `jq` (which handles escaping correctly):
108
+ ```bash
109
+ gh api repos/{owner}/{repo}/pulls/{number}/files --paginate \
110
+ --jq '[.[] | {key: (.filename | gsub("[^a-zA-Z0-9]"; "_")), value: (.patch // "")}] | from_entries' \
111
+ > /tmp/pr-patches-{number}.json
112
+ ```
113
+
114
+ 2. During assembly, use Python to safely inject the JSON into `template.html`:
115
+ ```bash
116
+ python3 <<'PY'
117
+ import json
118
+ from pathlib import Path
119
+
120
+ patches = json.loads(Path('/tmp/pr-patches-{number}.json').read_text())
121
+ html = Path('/tmp/pr-review-{number}-body.html').read_text()
122
+ css = Path('styles.css').read_text()
123
+ js = Path('renderer.js').read_text()
124
+ tmpl = Path('template.html').read_text()
125
+
126
+ # Prevent literal </script> from terminating HTML script tags early.
127
+ safe_json = json.dumps(patches).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
128
+
129
+ out = (
130
+ tmpl.replace('/* INJECT_CSS */', css)
131
+ .replace('/* INJECT_JS */', js)
132
+ .replace('<!-- INJECT_BODY -->', html)
133
+ .replace('{"__PR_DIFFS_PLACEHOLDER__":true}', safe_json)
134
+ )
135
+
136
+ Path('/tmp/pr-review-{number}.html').write_text(out)
137
+ PY
138
+ ```
139
+
140
+ This guarantees valid JSON and script-safe HTML embedding. The agent writes body HTML to a temp file, then Python assembles everything safely.
141
+
142
+ The diff data keys should match the `data-diff` attribute values in the HTML:
143
+ ```html
144
+ <div data-diff="path_to_file_ts"></div>
145
+ ```
146
+
147
+ Since renderer.js loads in `<head>`, you can also call `renderDiff(target, lines)` directly from inline `<script>` tags if needed for custom use cases. The function accepts a DOM element, ID string, or CSS selector as `target`, and a string or array as `lines`.
148
+
149
+ **You're not limited to these.** Add your own inline `<style>` blocks, `<script>` blocks, SVGs, diagrams, or anything else. The prebuilt pieces save time but don't constrain you.
150
+
151
+ ### 4. Assemble and serve
152
+
153
+ 1. Write your body HTML (everything that goes inside `<body>`) to `/tmp/pr-review-{number}-body.html`
154
+ 2. Save patches to `/tmp/pr-patches-{number}.json` using the `jq` command from step 3 above
155
+ 3. Run the Python assembly script from step 3 above (reads styles.css, renderer.js, template.html from this skill directory, injects body + patches safely, writes final HTML)
156
+ 4. Start a local server on a fixed port:
157
+ ```bash
158
+ cd /tmp && python3 -m http.server 8432 --bind 127.0.0.1
159
+ ```
160
+ Run this backgrounded, then navigate the in-app browser to `http://127.0.0.1:8432/pr-review-{number}.html`.
161
+
162
+ **Why a fixed port and `cd /tmp`:** Background shells have no TTY, so Python buffers its startup message ("Serving HTTP on...") indefinitely — using port 0 means you can never read which port was chosen. And `--directory /tmp` works but `cd /tmp` is more robust across Python versions. If port 8432 is taken, try 8433, 8434, etc.
163
+
164
+ ### Diff features (handled automatically by renderer.js)
165
+
166
+ - Filters out import-only lines
167
+ - Collapses whitespace-only changes into context lines
168
+ - Detects moved code blocks (3+ consecutive lines deleted in one place and added identically elsewhere) -- renders in blue/purple instead of red/green
169
+ - Near-matches (moved + small edit) get a different purple tint
170
+
171
+ ### Style notes
172
+
173
+ - Dark theme: `#1a1a1a` background, Inter body font, IBM Plex Mono for code
174
+ - Use `var(--warning)` for orange, `var(--success)` for green, `var(--danger)` for red, `var(--accent)` for blue
175
+ - Sticky file headers (`position: sticky; top: 0`) and notes (`top: 35px`) pin while scrolling
176
+ - Core files expanded by default (`.file-body.open`), mechanical files collapsed
@@ -0,0 +1,170 @@
1
+ function toggle(hdr) {
2
+ var b = hdr.nextElementSibling, c = hdr.querySelector('.chev');
3
+ b.classList.toggle('open'); c.classList.toggle('open');
4
+ }
5
+ function toggleBP(hdr) {
6
+ var b = hdr.nextElementSibling, c = hdr.querySelector('.chev');
7
+ b.classList.toggle('open'); c.classList.toggle('open');
8
+ }
9
+
10
+ function isImport(line) {
11
+ var s = line.replace(/^[+ -]/, '').trim();
12
+ return s.startsWith('import ') || s.startsWith('import{') || s.startsWith('} from ');
13
+ }
14
+
15
+ function isWhitespaceOnly(del, add) {
16
+ return del.replace(/^-/, '').replace(/\s/g, '') === add.replace(/^\+/, '').replace(/\s/g, '');
17
+ }
18
+
19
+ function esc(s) {
20
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
21
+ }
22
+
23
+ function normWs(s) {
24
+ return s.replace(/\s+/g, ' ').trim();
25
+ }
26
+
27
+ function toLines(input) {
28
+ if (!input) return [];
29
+ if (Array.isArray(input)) return input;
30
+ if (typeof input === 'string') return input.split('\n');
31
+ return [];
32
+ }
33
+
34
+ function loadPrDiffs() {
35
+ var el = document.getElementById('pr-diffs-json');
36
+ if (!el) return null;
37
+ try {
38
+ return JSON.parse(el.textContent || '');
39
+ } catch (err) {
40
+ console.error('Failed to parse PR diff JSON payload', err);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function detectMoves(dels, adds) {
46
+ var TH = 3;
47
+ var md = {}, ma = {};
48
+ for (var di = 0; di < dels.length; di++) {
49
+ if (md[di]) continue;
50
+ var db = [di];
51
+ for (var d2 = di+1; d2 < dels.length && d2-di < 40; d2++) {
52
+ if (dels[d2].consecutive && !md[d2]) db.push(d2); else break;
53
+ }
54
+ if (db.length < TH) continue;
55
+ var dn = db.map(function(i){ return normWs(dels[i].code); });
56
+ for (var ai = 0; ai < adds.length; ai++) {
57
+ if (ma[ai]) continue;
58
+ var ab = [ai];
59
+ for (var a2 = ai+1; a2 < adds.length && a2-ai < 40; a2++) {
60
+ if (adds[a2].consecutive && !ma[a2]) ab.push(a2); else break;
61
+ }
62
+ if (ab.length < TH) continue;
63
+ var an = ab.map(function(i){ return normWs(adds[i].code); });
64
+ var ml = Math.min(dn.length, an.length), mc = 0;
65
+ for (var m = 0; m < ml; m++) { if (dn[m] === an[m]) mc++; }
66
+ if (mc >= TH && mc >= ml * 0.7) {
67
+ for (var k = 0; k < ml; k++) {
68
+ md[db[k]] = { exact: dn[k] === an[k] };
69
+ ma[ab[k]] = { exact: dn[k] === an[k] };
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ return { movedDels: md, movedAdds: ma };
76
+ }
77
+
78
+ /**
79
+ * renderDiff(target, diffInput)
80
+ * target: DOM element, string ID, or CSS selector
81
+ * diffInput: array of diff lines, OR a single string (will be split on \n)
82
+ */
83
+ function renderDiff(target, diffInput) {
84
+ var el;
85
+ if (typeof target === 'string') {
86
+ el = document.getElementById(target) || document.querySelector(target);
87
+ } else {
88
+ el = target;
89
+ }
90
+ if (!el) return;
91
+
92
+ var lines = toLines(diffInput);
93
+ if (!lines.length) { el.innerHTML = '<div style="padding:12px;color:#777;font-size:12px;">No diff data</div>'; return; }
94
+
95
+ var filtered = lines.filter(function(l) {
96
+ if (l.startsWith('--- ') || l.startsWith('+++ ') || l.startsWith('@@') || l.startsWith('diff ')) return true;
97
+ return !isImport(l);
98
+ });
99
+
100
+ var wsOut = [];
101
+ for (var wi = 0; wi < filtered.length; wi++) {
102
+ if (filtered[wi].startsWith('-')) {
103
+ var dr = [filtered[wi]], wj = wi+1;
104
+ while (wj < filtered.length && filtered[wj].startsWith('-')) { dr.push(filtered[wj]); wj++; }
105
+ var ar = [], wk = wj;
106
+ while (wk < filtered.length && filtered[wk].startsWith('+')) { ar.push(filtered[wk]); wk++; }
107
+ if (dr.length === ar.length && dr.length > 0) {
108
+ var allWs = true;
109
+ for (var wc = 0; wc < dr.length; wc++) { if (!isWhitespaceOnly(dr[wc], ar[wc])) { allWs = false; break; } }
110
+ if (allWs) { for (var wx = 0; wx < ar.length; wx++) wsOut.push(' ' + ar[wx].slice(1)); wi = wk-1; continue; }
111
+ }
112
+ }
113
+ wsOut.push(filtered[wi]);
114
+ }
115
+
116
+ var dels = [], adds = [], parsed = [];
117
+ var oL = 0, nL = 0, pD = false, pA = false;
118
+ for (var pi = 0; pi < wsOut.length; pi++) {
119
+ var line = wsOut[pi];
120
+ if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('diff ')) continue;
121
+ if (line.startsWith('@@')) {
122
+ var hm = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)/);
123
+ if (hm) { oL = parseInt(hm[1]); nL = parseInt(hm[2]); }
124
+ parsed.push({ type: 'hunk', text: line }); pD = false; pA = false; continue;
125
+ }
126
+ if (line.startsWith('+')) {
127
+ var ae = { type:'add', code:line.slice(1), newLine:nL, consecutive:pA, idx:parsed.length };
128
+ adds.push(ae); parsed.push(ae); nL++; pA = true; pD = false;
129
+ } else if (line.startsWith('-')) {
130
+ var de = { type:'del', code:line.slice(1), oldLine:oL, consecutive:pD, idx:parsed.length };
131
+ dels.push(de); parsed.push(de); oL++; pD = true; pA = false;
132
+ } else {
133
+ var c = line.startsWith(' ') ? line.slice(1) : line;
134
+ parsed.push({ type:'ctx', code:c, oldLine:oL, newLine:nL }); oL++; nL++; pD = false; pA = false;
135
+ }
136
+ }
137
+
138
+ var mv = detectMoves(dels, adds);
139
+ var rows = [];
140
+ for (var ri = 0; ri < parsed.length; ri++) {
141
+ var p = parsed[ri];
142
+ if (p.type === 'hunk') {
143
+ rows.push('<tr class="diff-hunk"><td class="diff-ln"></td><td class="diff-ln"></td><td class="diff-code">' + esc(p.text) + '</td></tr>');
144
+ } else if (p.type === 'add') {
145
+ var ai2 = -1; for (var fa=0;fa<adds.length;fa++) if(adds[fa].idx===p.idx){ai2=fa;break;}
146
+ var cls = (mv.movedAdds[ai2]) ? (mv.movedAdds[ai2].exact ? 'diff-moved-add' : 'diff-moved-add-edited') : 'diff-add';
147
+ rows.push('<tr class="'+cls+'"><td class="diff-ln"></td><td class="diff-ln">'+p.newLine+'</td><td class="diff-code">'+esc(p.code)+'</td></tr>');
148
+ } else if (p.type === 'del') {
149
+ var di2 = -1; for(var fd=0;fd<dels.length;fd++) if(dels[fd].idx===p.idx){di2=fd;break;}
150
+ var cls2 = (mv.movedDels[di2]) ? (mv.movedDels[di2].exact ? 'diff-moved-del' : 'diff-moved-del-edited') : 'diff-del';
151
+ rows.push('<tr class="'+cls2+'"><td class="diff-ln">'+p.oldLine+'</td><td class="diff-ln"></td><td class="diff-code">'+esc(p.code)+'</td></tr>');
152
+ } else {
153
+ rows.push('<tr class="diff-ctx"><td class="diff-ln">'+p.oldLine+'</td><td class="diff-ln">'+p.newLine+'</td><td class="diff-code">'+esc(p.code)+'</td></tr>');
154
+ }
155
+ }
156
+ el.innerHTML = '<table class="diff-table"><tbody>' + rows.join('') + '</tbody></table>';
157
+ }
158
+
159
+ /* Auto-discovery: after DOM loads, find all [data-diff] elements and render diffs from pr-diffs-json. */
160
+ document.addEventListener('DOMContentLoaded', function() {
161
+ var prDiffs = loadPrDiffs();
162
+ if (!prDiffs) return;
163
+ var els = document.querySelectorAll('[data-diff]');
164
+ for (var i = 0; i < els.length; i++) {
165
+ var key = els[i].getAttribute('data-diff');
166
+ if (key && Object.prototype.hasOwnProperty.call(prDiffs, key)) {
167
+ renderDiff(els[i], prDiffs[key]);
168
+ }
169
+ }
170
+ });