@justjoshn/pi-pstack 0.15.36-pi.1 → 0.15.36-pi.8
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.
- package/CHANGELOG.md +43 -0
- package/README.md +15 -12
- package/agents/ci-watcher.md +27 -0
- package/agents/thermo-nuclear-code-quality-review.md +23 -0
- package/assets/logo.png +0 -0
- package/automations/benny/FOR_AGENTS.md +1 -1
- package/automations/benny/README.md +2 -2
- package/automations/benny/skills/setup-benny/SKILL.md +5 -5
- package/bin/pstack-routines-supervisor +9 -0
- package/docs/guide/07-overnight.md +1 -1
- package/docs/guide/09-make-it-yours.md +1 -1
- package/docs/guide/10-recipes-and-pitfalls.md +1 -1
- package/docs/native-gaps.md +11 -5
- package/docs/parity-reverse-engineering.md +94 -0
- package/package.json +41 -6
- package/packaging/install-supervisor-unit.sh +164 -0
- package/packaging/launchd/com.pstack.routines-supervisor.plist.example +24 -0
- package/packaging/systemd/pstack-routines-supervisor.service.example +15 -0
- package/skills/architect/SKILL.md +1 -1
- package/skills/architect/references/runner-prompt.md +1 -1
- package/skills/arena/SKILL.md +3 -3
- package/skills/check-compiler-errors/SKILL.md +23 -0
- package/skills/create-skill/SKILL.md +515 -29
- package/skills/fix-ci/SKILL.md +29 -0
- package/skills/fix-merge-conflicts/SKILL.md +32 -0
- package/skills/get-pr-comments/SKILL.md +23 -0
- package/skills/how/SKILL.md +1 -1
- package/skills/ketchup-video/SKILL.md +2 -2
- package/skills/loop-on-ci/SKILL.md +50 -0
- package/skills/make-bot-ui/SKILL.md +2 -2
- package/skills/make-pr-easy-to-review/SKILL.md +59 -0
- package/skills/new-branch-and-pr/SKILL.md +29 -0
- package/skills/no-inline-imports/SKILL.md +12 -0
- package/skills/poteto-help/SKILL.md +4 -4
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +1 -1
- package/skills/poteto-mode/playbooks/orchestrate.md +1 -1
- package/skills/pr-review-canvas/SKILL.md +176 -0
- package/skills/pr-review-canvas/renderer.js +170 -0
- package/skills/pr-review-canvas/styles.css +96 -0
- package/skills/pr-review-canvas/template.html +18 -0
- package/skills/reflect/SKILL.md +1 -1
- package/skills/review-and-ship/SKILL.md +41 -0
- package/skills/run-smoke-tests/SKILL.md +42 -0
- package/skills/statechart/scripts/test/cli.test.ts +1 -1
- package/skills/swarm/SKILL.md +2 -2
- package/skills/thermo-nuclear-code-quality-review/SKILL.md +192 -0
- package/skills/typescript-exhaustive-switch/SKILL.md +10 -0
- package/skills/verify-this/SKILL.md +74 -0
- package/skills/weekly-review/SKILL.md +32 -0
- package/skills/what-did-i-get-done/SKILL.md +31 -0
- package/skills/why/SKILL.md +1 -1
- package/skills/workflow-from-chats/SKILL.md +50 -0
- package/src/mode.ts +3 -3
- package/src/paths.ts +13 -0
- package/src/pi-command.ts +12 -0
- package/src/rule.ts +4 -4
- package/src/sections.ts +2 -1
- package/src/subscriptions/durable-store.ts +482 -0
- package/src/subscriptions/durable-types.ts +41 -0
- package/src/subscriptions/ensure-supervisor.ts +67 -0
- package/src/subscriptions/index.ts +231 -271
- package/src/subscriptions/supervisor-main.ts +88 -0
- package/src/subscriptions/supervisor.ts +271 -0
- package/src/subscriptions/webhook.ts +22 -9
- package/src/task/card.ts +29 -0
- package/src/task/records.ts +3 -8
- package/src/task/runner.ts +15 -15
- package/src/task/tool.ts +18 -21
- package/src/todo.ts +1 -2
- package/scripts/live-e2e.mjs +0 -257
- package/test/aliases.test.ts +0 -57
- package/test/ask.test.ts +0 -85
- package/test/autocomplete.test.ts +0 -39
- package/test/e2e/aliases.e2e.test.ts +0 -81
- package/test/e2e/ask.e2e.test.ts +0 -135
- package/test/e2e/harness.ts +0 -179
- package/test/e2e/host.e2e.test.ts +0 -193
- package/test/e2e/lifecycle.e2e.test.ts +0 -99
- package/test/e2e/mode.e2e.test.ts +0 -160
- package/test/e2e/package.e2e.test.ts +0 -67
- package/test/e2e/task.e2e.test.ts +0 -734
- package/test/e2e/todo.e2e.test.ts +0 -59
- package/test/fixtures/cursor-askquestion-calls.json +0 -806
- package/test/fixtures/fake-pi.mjs +0 -96
- package/test/fixtures/image-model.ts +0 -16
- package/test/fixtures/list-models.ts +0 -15
- package/test/fixtures/readonly-shell-exit.ts +0 -17
- package/test/fixtures/scripted-model.ts +0 -113
- package/test/loop.test.ts +0 -209
- package/test/mode.test.ts +0 -143
- package/test/plan.test.ts +0 -126
- package/test/readonly-shell-security.test.ts +0 -84
- package/test/readonly-shell.test.ts +0 -44
- package/test/records.test.ts +0 -143
- package/test/registry.test.ts +0 -18
- package/test/resources.test.ts +0 -162
- package/test/rule.test.ts +0 -92
- package/test/scripts.test.ts +0 -182
- package/test/sections.test.ts +0 -56
- package/test/subscriptions-cron.test.ts +0 -60
- package/test/subscriptions.test.ts +0 -598
- package/test/task-agents.test.ts +0 -49
- package/test/task-card.test.ts +0 -34
- package/test/task-fork.test.ts +0 -233
- package/test/task-format.test.ts +0 -65
- package/test/task-model.test.ts +0 -141
- package/test/task-runner.test.ts +0 -259
- package/test/task-schema.test.ts +0 -50
- package/test/task-status.test.ts +0 -155
- package/test/task-worktree.test.ts +0 -106
- package/test/todo.test.ts +0 -74
- package/test/visualize.test.ts +0 -152
- package/tsconfig.json +0 -5
- 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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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 |
|
|
144
|
-
| Parallel agents overwrote each other | Give each agent its own 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
|
|
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
|
|
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">−11</span><span class="chev open">▶</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">▶</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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
});
|