@openora/create 0.4.1-canary.34 → 0.4.1-canary.340

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 (98) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/generated/versions.d.ts +11 -0
  3. package/dist/generated/versions.d.ts.map +1 -0
  4. package/dist/generated/versions.js +11 -0
  5. package/dist/generated/versions.js.map +1 -0
  6. package/dist/index.js +48 -93
  7. package/dist/index.js.map +1 -1
  8. package/dist/render.d.ts +15 -0
  9. package/dist/render.d.ts.map +1 -0
  10. package/dist/render.js +125 -0
  11. package/dist/render.js.map +1 -0
  12. package/package.json +8 -6
  13. package/template/README.md.tpl +12 -7
  14. package/template/__dot__claude/settings.json.tpl +22 -12
  15. package/template/__dot__env.example +10 -0
  16. package/template/__dot__gitignore +6 -1
  17. package/template/__dot__rulesync/commands/adr.md +32 -0
  18. package/template/__dot__rulesync/commands/check.md +11 -10
  19. package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
  20. package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
  21. package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
  22. package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
  23. package/template/__dot__rulesync/commands/start.md +2 -2
  24. package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
  25. package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
  26. package/template/__dot__rulesync/hooks/guard-generated.mjs +16 -14
  27. package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
  28. package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
  29. package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
  30. package/template/__dot__rulesync/hooks.json +27 -4
  31. package/template/__dot__rulesync/rules/conventions.md +114 -0
  32. package/template/__dot__rulesync/rules/db-conventions.md +6 -72
  33. package/template/__dot__rulesync/rules/e2e-conventions.md +52 -0
  34. package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
  35. package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
  36. package/template/__dot__rulesync/rules/overview.md +18 -73
  37. package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +13 -24
  38. package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
  39. package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
  40. package/template/__dot__rulesync/skills/create-ui-module/SKILL.md.tpl +60 -0
  41. package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
  42. package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
  43. package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
  44. package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
  45. package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
  46. package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
  47. package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
  48. package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
  49. package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
  50. package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
  51. package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
  52. package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
  53. package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
  54. package/template/__dot__rulesync/subagents/expert.md +28 -34
  55. package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
  56. package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
  57. package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
  58. package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
  59. package/template/__dot__rulesync/sync.json.tpl +26 -0
  60. package/template/apps/api/package.json.tpl +3 -4
  61. package/template/apps/api/src/extensions.config.ts.tpl +12 -0
  62. package/template/apps/api/src/main.ts.tpl +1 -4
  63. package/template/apps/api/src/seed.ts.tpl +8 -1
  64. package/template/docker-compose.yml +13 -0
  65. package/template/docs/agents/cross-repo.md.tpl +37 -0
  66. package/template/docs/agents/forge.md.tpl +54 -0
  67. package/template/docs/agents/issue-tracker.md.tpl +42 -0
  68. package/template/docs/standards/comments.md +16 -0
  69. package/template/docs/standards/database.md +77 -0
  70. package/template/docs/standards/enforcement.md +12 -0
  71. package/template/docs/standards/errors.md +30 -0
  72. package/template/docs/standards/frontend.md +60 -0
  73. package/template/docs/standards/functions.md +38 -0
  74. package/template/docs/standards/git-delivery.md +20 -0
  75. package/template/docs/standards/module-structure.md +37 -0
  76. package/template/docs/standards/testing.md +88 -0
  77. package/template/docs/standards/types.md +31 -0
  78. package/template/package.json.tpl +19 -13
  79. package/template/rulesync.jsonc +4 -4
  80. package/template/tools/oss-worktree.mjs.tpl +105 -0
  81. package/template/tools/review-precheck.mjs +531 -0
  82. package/template/tools/sync-agents.mjs +257 -0
  83. package/template/turbo/generators/config.ts +1 -1
  84. package/template/turbo.json +14 -2
  85. package/dist/generated/core-version.d.ts +0 -2
  86. package/dist/generated/core-version.d.ts.map +0 -1
  87. package/dist/generated/core-version.js +0 -2
  88. package/dist/generated/core-version.js.map +0 -1
  89. package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
  90. package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
  91. package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
  92. package/template/__dot__rulesync/skills/code-review/SKILL.md +0 -111
  93. package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
  94. package/template/__dot__rulesync/subagents/builder.md +0 -93
  95. package/template/__dot__rulesync/subagents/debugger.md +0 -83
  96. package/template/__dot__rulesync/subagents/deployer.md +0 -66
  97. package/template/__dot__rulesync/subagents/qa.md +0 -88
  98. package/template/apps/api/src/migrate.ts.tpl +0 -46
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: ship
3
+ targets: ['*']
4
+ description: >
5
+ Take one {{trackerKey}} ticket from plan to a green, reviewed MR with nobody in the loop: planner
6
+ plans it with test cases, builder builds it, tester attacks it, the reviewers review it, then it
7
+ pushes the ticket branch, opens the MR, moves the ticket and watches the MR until it is ready for a
8
+ person to review. Resumable: every run reads the ticket,
9
+ branch, MR and CI and does the next step, including fixing a red pipeline or new review comments.
10
+ Stops only on a question or a blocker. Use on "ship {{trackerKey}}-XXX", "add feature", "deliver
11
+ {{trackerKey}}-XXX", or /ship {{trackerKey}}-XXX [--ask].
12
+ ---
13
+
14
+ # ship ({{name}})
15
+
16
+ The orchestrator: one ticket key in, a green MR in Code Review out. You own the ticket's whole delivery, plan to review: you are done when the MR is ready for a person to review it (step 7), not when it is opened. You run in the main session and are the only one who delegates. You hand each step to its subagent, read its verdict, and pick the next step yourself - never wait for a person between steps. Every subagent you start is told it starts no subagents of its own; a domain question one of them returns comes back to you, you ask `expert`, and you pass the answer on.
17
+
18
+ ## Coordinates
19
+
20
+ - Jira: per `docs/agents/issue-tracker.md`, project `{{trackerKey}}`. Code forge: `{{gitRemotePath}}`, target `{{mrTarget}}`, commands in `docs/agents/forge.md`.
21
+ - `{{ossDir}}` (the linked OSS checkout) is read-only. Core-only work goes through `handoff.md`.
22
+
23
+ ## Arguments
24
+
25
+ - `{{trackerKey}}-XXX` from `$ARGUMENTS`. None: ask for it, that is the one question this skill always asks.
26
+ - `--ask`: stop for approval after the plan and before the push. Without it, invoking `/ship` is the authorization for every write below - and only those.
27
+
28
+ ## What ship may write without asking
29
+
30
+ - Jira, on this ticket only: the Plan comment (and its test cases where the repo's `test-plan` skill keeps them), a questions comment, a blocker comment, the MR link comment, and the transitions to In Progress and to Code Review. Nothing else: no field edits, no other transitions, no other tickets.
31
+ - Git: commits on the ticket branch `<type>/{{trackerKey}}-XXX/<slug>` and pushing it. Never a push to `{{mrTarget}}`, `stage`, `prod` or any shared branch.
32
+ - Force-push: only `git push --force-with-lease` on the ticket branch, and only while every commit on the remote branch past `{{mrTarget}}` is your own (`git log --format=%ae origin/{{mrTarget}}..origin/<branch>` shows only your `git config user.email`). Someone else's commit there: merge, never push over it. Never a plain `--force`.
33
+ - Merging: into a feature branch only (`{{mrTarget}}` merged into the ticket branch to clear conflicts, a stacked MR into its parent ticket branch). Never into `{{mrTarget}}`, `dev`, `develop`, `stage`, `staging`, `prod`, `production`, `main` or `master`.
34
+ - The forge: opening the MR for that branch and replying on its review threads. Every comment you leave starts with `Agent:`, so `watch-pr.mjs` and other tools never take it for one to answer.
35
+ - Slack: nothing. Announcing the MR is the forge integration's job, so ship never drafts a notice.
36
+
37
+ ## Do what the work needs, inside the worktree
38
+
39
+ Never stop to ask for something local that deleting the worktree undoes: do it, and say what you did in the reply and the MR description.
40
+
41
+ - Env: a variable the change needs goes into the worktree's `.env` with a local dev value, and into every place the repo declares one (env schema, `.env.example`, deploy docs) in the same commit. A value only a person has (a real API key, a prod URL): a dev placeholder or a mock locally, and a "Needs before deploy" line in the MR description. If no test can run without the real one, that is a blocker.
42
+ - Dependencies: add what the plan needs with the repo's package manager and commit the lockfile. A repo rule that asks for a step first (unlink a locally linked package, then link it back) is followed, so the lockfile carries no local links; it is never a reason to stop.
43
+ - Data: the worktree's own database and Redis db - create, migrate, seed and reset them freely. Migrations come from the repo's generator, generated files from its regen command.
44
+ - Processes and tools: start what the tests need on the worktree's own ports and stop it before you report; install a local tool the repo's docs name (browsers for Playwright and the like).
45
+
46
+ ## Access outside the repo
47
+
48
+ The plan's Access list names every account or session outside the repo the work needs (a cloud CLI profile, a provider's dashboard, a sandbox API), each with what it is for, a read-only check that proves the session works and the login a person runs. Before 2, every run runs each check:
49
+
50
+ - One fails -> `Asked:` on its own line, then each missing login as the command a person runs (`! aws sso login --profile <p>`) or the page to log in to in the browser. Report it, stop; no ticket comment. `/ship {{trackerKey}}-XXX` again picks up from here.
51
+ - All pass -> read what it names freely. A passing check is not consent to change anything there, and neither is an Access list built from the ticket. Before the first change outside the repo (create, configure, delete, issue a key): `Asked:`, then one line per change - account, resource, what changes - report it, stop. A person's yes in this session covers that exact list; anything not on it, or a new run, asks again. Then make the changes and put a dev key it issues in the worktree's `.env`.
52
+ - Every change outside the repo goes in the reply and the MR description under "Outside the repo", one line each: where, what, how to undo it. A change to something that already existed records its old value.
53
+
54
+ ## Never, however blocked
55
+
56
+ Anything shared, remote, or not undone by deleting the worktree, unless a person approved that exact change (see Access outside the repo):
57
+
58
+ - Servers, ssh, cloud accounts, dashboards, deploys, stage and prod, and any database that is not the worktree's own.
59
+ - CI/CD settings and variables, repo settings, other people's branches and MRs.
60
+ - Anything this run did not create, in the repo or outside it: no drop, flush, kill or delete.
61
+ - Secrets: never commit, print or invent a real-looking one; a `.env` with real values stays uncommitted.
62
+ - Gates: no `--no-verify`, no skipped or loosened test, lint rule or guard hook, no hand edit to a generated file or `node_modules`.
63
+
64
+ Blocked by one of these: `Blocked:` with what is needed and who can do it. Every subagent you start gets both lists.
65
+
66
+ ## Read the state, do the next step
67
+
68
+ Every run starts here, so a run that died midway resumes where it stopped. Read the ticket per `docs/agents/issue-tracker.md`, then `git branch -a --list '*{{trackerKey}}-XXX*'`, then the MR for that branch and its CI per `docs/agents/forge.md`.
69
+
70
+ | State | Next step |
71
+ | ------------------------------------------- | ------------------------------------------- |
72
+ | No Plan comment on the ticket | 1. Plan |
73
+ | Plan, no branch | 2. Build |
74
+ | Branch, no MR | 3. Test, 4. Review, 4b. Evidence, 5. Open the MR |
75
+ | MR open, CI red or conflicts | 6. `fix-pipeline` skill |
76
+ | MR open, review threads with no reply | 6. `fix-review-comments` skill |
77
+ | MR open, CI running, or green and answered | 7. Ready for review |
78
+ | MR merged or closed | Report it, do nothing |
79
+
80
+ A comment added to the ticket after the Plan comment that changes scope sends you back to 1.
81
+
82
+ ### 1. Plan
83
+
84
+ Start `planner` with the ticket key. It returns either `Asked:` with questions, or the plan.
85
+
86
+ - `Asked:` -> post the questions as one comment on the ticket, report them, stop.
87
+ - A plan -> post it, without its Core-only section, as one comment titled "Plan" (test cases where the repo's `test-plan` skill keeps them, if it has one), move the ticket to In Progress. `--ask`: show it and stop for approval.
88
+ - A Core-only section -> write the work order per `handoff.md` and drop that slice; ship the rest.
89
+
90
+ ### 2. Build
91
+
92
+ Add a worktree for the ticket branch off the freshly fetched `{{mrTarget}}`, per the repo's worktree rules (its own database and Redis db). Start `builder` in it with the plan. Run the **create-plugin** skill's steps through it for each overlay, adapter or page slice.
93
+
94
+ - `Blocked:` -> post the reason as one comment on the ticket, report it, stop.
95
+ - Done -> check its reply holds the gate output from HEAD and `git status` is clean. Missing either: send it back once to run the gate, then treat it as blocked.
96
+
97
+ ### 3. Test
98
+
99
+ Start `tester` in the worktree with the plan's test cases, for its verify pass: no videos yet. It returns an AC table.
100
+
101
+ - Every row PASS -> 4.
102
+ - Any FAIL -> back to `builder` with the failing rows only, then `tester` again.
103
+ - UNCHECKED rows ride along to the MR description; they do not block, except a case or a mutation row on a money, KYC, responsible-gambling or authz path: back to `tester` once to run it, still UNCHECKED -> blocker comment, report it, stop.
104
+ - A SURVIVED mutation row is a FAIL: back to `builder` with the row (the test that catches it, or the code that makes it testable), then `tester` again. A money, KYC, responsible-gambling or authz change with no mutation rows goes back to `tester` once to add them.
105
+ - A failing spec called pre-existing without its run on the base commit -> back to `tester` once to run it there. Still unrun: it is a FAIL.
106
+
107
+ ### 4. Review
108
+
109
+ Run the **review** skill on the branch against `{{mrTarget}}`, without `--post`: its precheck, risk tier, request trace and evidence gate run before any reviewer reads the diff. Hand its reviewers the tester's AC table, mutation rows included, so they dig where testing could not reach. This is the self-review the MR is opened on: people reviewing the MR later read it by hand and do not run an AI review again, so nothing goes out that this step has not passed.
110
+
111
+ Every finding with a fix gets fixed, whatever its severity, but only once it holds. Dedupe across reviewers, then check each yourself before anything changes: open its `file:line` and confirm the evidence on this branch. One that doesn't hold (wrong line, the code already handles it, a claim the code doesn't show) is dropped and goes to the MR description with why. A product call the ticket doesn't settle goes to `expert`, then is fixed or rides along as an open question. Send the rest to `builder` in one go, then 3 for the cases they touch and 4 again. A later round of 4 reviews only the commits since the last one and whether each sent finding is fixed. Clean means the review skill says **APPROVED** and no finding is left that holds; only then 4b.
112
+
113
+ Rounds of 3 and 4 share one budget: three. After the third with anything still failing, post a blocker comment naming what is left, report it, stop.
114
+
115
+ ### 4b. Record evidence
116
+
117
+ The code is final now, so the videos are recorded once, here, never in an earlier round. Start `tester` for its evidence pass on HEAD: every browser spec that drives an AC, new or existing, runs once with video on, `playwright-cli` only for an AC no spec drives. It returns the clip paths per AC.
118
+
119
+ - A spec that fails here -> back to `builder` with it, then 3 for the cases it touches, 4 on the new commits, 4b again. That round counts against the same budget of three.
120
+ - Every AC that needs a clip has one -> 5.
121
+
122
+ ### 5. Open the MR
123
+
124
+ `--ask`: report the SHA and stop for "yes push". Then run **create-pr** with the tester's AC table, the 4b videos and its Tests list. Comment the MR link on the ticket and move it to Code Review in the same turn. Then 7.
125
+
126
+ ### 6. Red CI and review comments
127
+
128
+ Run the repo's `fix-pipeline` skill on a red pipeline or conflicts, `fix-review-comments` on unanswered threads from a person or a bot. No such skill in this repo: do the same per `docs/agents/forge.md`. Under `/ship` they run like this:
129
+
130
+ - Each thread is checked against the code on this branch first: correct | already handled | wrong on this branch | out of scope. Only a correct one changes code; the rest get an answer.
131
+ - A red job is reproduced locally first; `builder` fixes its cause. Never skip or loosen a test.
132
+ - Every fix goes to `builder`. Then `tester` re-runs the cases the fix touches and leaves a test that fails without it, and 4 runs on the new commits. Then 4b records again only the ACs the fix touched, and their clips replace the old ones in the MR description. Nothing is pushed untested.
133
+ - Push the ticket branch without asking (a force-push only as above). Reply per thread, in the thread itself, with what changed and the test that proves it, or why nothing changed. Resolve only a thread the fix addressed; a question or a disagreement stays open for the reviewer.
134
+
135
+ Then 7.
136
+
137
+ ### 7. Ready for review
138
+
139
+ Start `node <this skill's folder>/watch-pr.mjs <branch> --until-green` as a background command, never a sleep loop in the session. It prints one line and exits when there is work:
140
+
141
+ - `failed` or `conflicts` -> 6, then watch again. Still red after three fixes -> blocker comment, report, stop.
142
+ - `threads <ids>` (unanswered for two checks in a row, from a person or a bot) -> 6 for those, then watch again. A thread the forge's AI review opens is a blocker it found on this head: it counts like a person's.
143
+ - `green` -> check the MR is ready (below), report "Ready for review:" and the MR URL, then watch again without `--until-green`.
144
+ - `merged` or `closed` -> report it, drop the worktree's database and Redis db, stop. `error ...` -> report it, stop.
145
+
146
+ Ready means: the pipeline on the last commit is green, no conflicts with `{{mrTarget}}`, every thread has an answer, no money, KYC, responsible-gambling or authz case or mutation row is UNCHECKED or SURVIVED, and the description holds the AC table, the screenshots and the UNCHECKED rows.
147
+
148
+ Keep watching until the MR merges or closes, for as long as the session lives. You never merge it: who approves and who merges is in `docs/agents/forge.md`. A caller that says it watches the MR itself (a headless run) gets the report at `green`, and you stop there; it brings the ticket back through `/ship {{trackerKey}}-XXX`, which lands on 6.
149
+
150
+ ## Rules
151
+
152
+ - One ticket, one branch, one MR.
153
+ - You edit no code, commit nothing and run no gate yourself, however small the fix: every fix goes to `builder`. Code changed after 3 means 3 runs again for the cases it touches, and 4 for the new commits.
154
+ - A subagent that stops short (no gate output, uncommitted work) goes back to it once, never finished by you.
155
+ - Never claim a step passed without its output: the gate from HEAD, the AC table, the review findings.
156
+ - A run you resume with a branch but no MR re-runs 3 and 4: verdicts from a dead run are gone.
157
+ - Clean up what the run started: worktree databases and Redis dbs stay for the next round until the MR merges; processes you started stop before you report.
@@ -0,0 +1,48 @@
1
+ # OSS core handoff
2
+
3
+ When a `ship` work item can only be fixed in `@openora/*` core, this consumer repo cannot edit it (the `guard-core.mjs` hook and `permissions.deny` block `{{ossDir}}/**`). Hand off to the platform-core twin skill instead of fighting the guard.
4
+
5
+ ## When to hand off
6
+
7
+ - The fix requires changing a contract, schema, service, router, or platform seam in `@openora/*`.
8
+ - An overlay plugin / adapter swap / UI provider override / config change genuinely cannot express the behavior.
9
+ - Default bias: extend from the outside first; hand off only when you've confirmed the outside-in path doesn't exist. If unsure, the planner says so in the plan; without a clear core-only reason, build it from the outside.
10
+
11
+ ## Work-order template
12
+
13
+ Write to `~/.claude/plans/<ticket-key>-oss.md` so both repos and future sessions can read it:
14
+
15
+ ```markdown
16
+ # OSS work order - <ticket-key>: <title>
17
+
18
+ ## Goal
19
+
20
+ <what core must do, 1-2 lines>
21
+
22
+ ## Unblocks
23
+
24
+ {{name}} feature <ticket-key> - <which downstream slice depends on this>
25
+
26
+ ## Core surface to change
27
+
28
+ - <package / module / contract / adapter token, as specific as possible>
29
+
30
+ ## Contract / schema impact
31
+
32
+ - <new or changed oRPC contracts, Drizzle tables, events; or "none">
33
+
34
+ ## Acceptance
35
+
36
+ - <observable outcomes that prove the core change is correct>
37
+
38
+ ## Dependency
39
+
40
+ {{name}} MR <url-or-TBD> stays in draft until this merges and a new @openora/core/\* version is consumed.
41
+ ```
42
+
43
+ ## Protocol
44
+
45
+ 1. This consumer repo writes the work-order and **stops** that slice (keep delivering independent downstream slices meanwhile).
46
+ 2. The user runs `/ship` inside the platform OSS repo (`{{ossDir}}`, separate cwd/session). It reads the work-order, implements, and opens its own MR there.
47
+ 3. The consumer MR stays draft/blocked until the OSS MR merges and the consumer bumps the consumed `@openora/*` version. Link the two by the work-order path and the ticket key.
48
+ 4. Note the handoff in the Jira ticket and the consumer MR description so the dependency is visible.
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // Watches the MR or PR of a branch and prints one line, then exits, when ship has work on it:
3
+ // failed | conflicts | threads <ids> | green (only with --until-green) | merged | closed | error <message>
4
+ // A thread waits for an answer until its last comment is an "Agent:" reply from the account this runs as
5
+ // (anyone can type the prefix); it is reported once it stayed the same for two checks, so a review with
6
+ // ten comments is one line. Run it in the background:
7
+ // node watch-pr.mjs <branch> [--until-green] WATCH_DEBUG=1 prints every check to stderr
8
+ // --once checks one time, for a caller that polls on its own (a scheduler): threads are reported at
9
+ // once, and `none` means nothing to do.
10
+ import { execFileSync } from 'node:child_process';
11
+
12
+ const EVERY_MS = 120_000;
13
+ // ponytail: a repo with no CI on the last commit reads green after this; a head-SHA match on checks if it matters
14
+ const NO_CI_MS = 10 * 60_000;
15
+ const MAX_ERRORS = 5;
16
+ const AGENT_REPLY = 'Agent:';
17
+ const FAILED = [
18
+ 'FAILURE',
19
+ 'ERROR',
20
+ 'CANCELLED',
21
+ 'TIMED_OUT',
22
+ 'ACTION_REQUIRED',
23
+ 'STARTUP_FAILURE',
24
+ 'failed',
25
+ 'canceled',
26
+ ];
27
+
28
+ const [branch, ...flags] = process.argv.slice(2);
29
+ if (!branch) {
30
+ console.error('usage: watch-pr.mjs <branch> [--until-green | --once]');
31
+ process.exit(2);
32
+ }
33
+ const untilGreen = flags.includes('--until-green');
34
+ const once = flags.includes('--once');
35
+
36
+ const run = (cmd, args) =>
37
+ execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
38
+ const json = (cmd, args) => JSON.parse(run(cmd, args));
39
+ const unansweredIds = (lastComments, me) =>
40
+ lastComments
41
+ .filter(
42
+ (comment) => !(comment.author === me && comment.body.trimStart().startsWith(AGENT_REPLY)),
43
+ )
44
+ .map((comment) => String(comment.id))
45
+ .sort();
46
+
47
+ const THREADS = `query($owner: String!, $name: String!, $number: Int!, $endCursor: String) {
48
+ viewer { login }
49
+ repository(owner: $owner, name: $name) { pullRequest(number: $number) {
50
+ reviewThreads(first: 100, after: $endCursor) {
51
+ nodes { isResolved comments(last: 1) { nodes { id body author { login } } } }
52
+ pageInfo { hasNextPage endCursor }
53
+ }
54
+ comments(last: 1) { nodes { id body author { login } } }
55
+ } }
56
+ }`;
57
+
58
+ function github() {
59
+ const pr = json('gh', [
60
+ 'pr',
61
+ 'view',
62
+ branch,
63
+ '--json',
64
+ 'number,url,state,mergeable,statusCheckRollup',
65
+ ]);
66
+ const [, owner, name] = new URL(pr.url).pathname.split('/');
67
+ const args = [
68
+ 'api',
69
+ 'graphql',
70
+ '--paginate',
71
+ '--slurp',
72
+ '-f',
73
+ `query=${THREADS}`,
74
+ '-f',
75
+ `owner=${owner}`,
76
+ '-f',
77
+ `name=${name}`,
78
+ '-F',
79
+ `number=${pr.number}`,
80
+ ];
81
+ const pages = json('gh', args).map((page) => page.data);
82
+ const open = pages
83
+ .flatMap(({ repository }) => repository.pullRequest.reviewThreads.nodes)
84
+ .filter((thread) => !thread.isResolved)
85
+ .flatMap((thread) => thread.comments.nodes)
86
+ .concat(pages[0].repository.pullRequest.comments.nodes)
87
+ .map((comment) => ({ ...comment, author: comment.author?.login }));
88
+ const checks = pr.statusCheckRollup ?? [];
89
+ const ci = !checks.length
90
+ ? null
91
+ : checks.some((check) => FAILED.includes(check.conclusion || check.state))
92
+ ? 'failed'
93
+ : checks.some((check) =>
94
+ check.status
95
+ ? check.status !== 'COMPLETED'
96
+ : check.state === 'PENDING' || check.state === 'EXPECTED',
97
+ )
98
+ ? 'running'
99
+ : 'passed';
100
+ return {
101
+ state: pr.state.toLowerCase(),
102
+ conflicts: pr.mergeable === 'CONFLICTING',
103
+ ci,
104
+ unanswered: unansweredIds(open, pages[0].viewer.login),
105
+ };
106
+ }
107
+
108
+ function gitlab() {
109
+ const mr = json('glab', ['mr', 'view', branch, '-F', 'json']);
110
+ const discussions = run('glab', [
111
+ 'api',
112
+ '--paginate',
113
+ '--output',
114
+ 'ndjson',
115
+ `projects/${mr.project_id}/merge_requests/${mr.iid}/discussions?per_page=100`,
116
+ ])
117
+ .split('\n')
118
+ .filter(Boolean)
119
+ .map((line) => JSON.parse(line));
120
+ const open = discussions
121
+ .map((discussion) => discussion.notes.filter((note) => !note.system))
122
+ .filter(
123
+ (notes) =>
124
+ notes.length &&
125
+ !(
126
+ notes.some((note) => note.resolvable) &&
127
+ notes.every((note) => !note.resolvable || note.resolved)
128
+ ),
129
+ )
130
+ .map((notes) => notes.at(-1))
131
+ .map((note) => ({ ...note, author: note.author?.username }));
132
+ const pipeline = mr.head_pipeline?.sha === mr.sha ? mr.head_pipeline.status : null;
133
+ const ci = !pipeline
134
+ ? null
135
+ : FAILED.includes(pipeline)
136
+ ? 'failed'
137
+ : ['success', 'skipped', 'manual'].includes(pipeline)
138
+ ? 'passed'
139
+ : 'running';
140
+ const state = mr.state === 'opened' ? 'open' : mr.state === 'merged' ? 'merged' : 'closed';
141
+ const me = json('glab', ['api', 'user']).username;
142
+ return { state, conflicts: mr.has_conflicts === true, ci, unanswered: unansweredIds(open, me) };
143
+ }
144
+
145
+ const say = (line) => {
146
+ console.log(line);
147
+ process.exit(0);
148
+ };
149
+ const status = /github\.com/.test(run('git', ['remote', 'get-url', 'origin'])) ? github : gitlab;
150
+ const started = Date.now();
151
+ let seen = null;
152
+ let errors = 0;
153
+ for (;;) {
154
+ try {
155
+ const now = status();
156
+ errors = 0;
157
+ if (process.env.WATCH_DEBUG) console.error(JSON.stringify(now));
158
+ if (now.state !== 'open') say(now.state);
159
+ if (now.conflicts) say('conflicts');
160
+ if (now.ci === 'failed') say('failed');
161
+ const ids = now.unanswered.join(' ');
162
+ if (ids && (once || ids === seen)) say(`threads ${ids}`);
163
+ seen = ids;
164
+ const green = now.ci === 'passed' || (now.ci === null && Date.now() - started >= NO_CI_MS);
165
+ if (untilGreen && green && !ids) say('green');
166
+ if (once) say('none');
167
+ } catch (reason) {
168
+ if (once || ++errors >= MAX_ERRORS)
169
+ say(`error ${reason instanceof Error ? reason.message.split('\n')[0] : reason}`);
170
+ }
171
+ await new Promise((resolve) => setTimeout(resolve, EVERY_MS));
172
+ }
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: ui-verify
3
+ targets: ['*']
4
+ description: >
5
+ Verify a UI change in a real browser with the global `playwright-cli`: cheap checks first,
6
+ then four screenshots per changed screen read for spacing, overlap, cut text and contrast,
7
+ then the per-AC videos for the PR. Use after any change to a page, component, style or
8
+ player/operator flow, on "check it looks right", "verify the UI", "does it render", "take a
9
+ screenshot", "record a video", and to reproduce a UI bug before fixing it.
10
+ claudecode:
11
+ context: fork
12
+ model: sonnet
13
+ effort: medium
14
+ ---
15
+
16
+ # ui-verify ({{name}})
17
+
18
+ Global `playwright-cli` (`npm i -g @playwright/cli` if missing), never a browser MCP: it costs a
19
+ fraction of the tokens. Do not spawn subagents. Return text only. A subagent (`tester`, `builder`) does not
20
+ invoke this skill, which forks one of its own: it reads this file and follows it itself.
21
+
22
+ ## Stack
23
+
24
+ - Player app :3000, backoffice :3002, API :3001, unless the repo's rules name other ports (a
25
+ worktree slot, say). Ports held by another checkout: never kill it, ask.
26
+ - Accounts: the seed's (`admin@oss.dev` / `password1234` after `pnpm db:seed`). Save a login once
27
+ with `state-save /tmp/ui-verify/<app>.json`, `state-load` it after.
28
+ - Theme: switch with the app's own toggle in a video; for a still, set what the toggle sets
29
+ (a cookie or `localStorage`) with `eval`, then reload.
30
+
31
+ ## Cost ladder
32
+
33
+ Deterministic checks first, reading pixels last. Write each check as an assertion with an
34
+ expected value (`[data-testid=balance]` text matches `/\d+(\.\d+)? USDT/`) and stop at the
35
+ first rung that answers it.
36
+
37
+ 1. **Shell.** The route serves: `curl -s -o /dev/null -w '%{http_code}' <url>`.
38
+ 2. **Browser, one value.** `eval` for existence, text, count, computed style; `console error`
39
+ and `requests` grepped for `4\d\d|5\d\d`; `find "<text>"` for one node.
40
+ 3. **Structure.** `snapshot <target>` scoped to the changed component; a full-page `snapshot`
41
+ only with `--depth` or `--filename` + `grep`.
42
+ 4. **Pixels.** The screenshot review below.
43
+
44
+ Batch commands in one bash call. Output over ~50 lines goes to `/tmp/ui-verify/` and gets
45
+ `grep`, not read whole. One named session per run (`-s=<name>`), `close` at the end.
46
+
47
+ ## Screenshot review
48
+
49
+ Screenshots are for you, videos for the reviewer: nobody reads a video back, so layout defects
50
+ are caught here. Every changed screen, after the checks pass and before the videos. Four shots:
51
+ mobile and desktop, light and dark (backoffice: desktop only).
52
+
53
+ - Sizes: never shrink a shot, halving it halves every padding you judge. Mobile 390x844 at 2x
54
+ device pixels (780x1688), desktop 1440x900 at 1x: both under the 2000 px per-image cap a
55
+ session with over 20 images gets, about 1.7k tokens each.
56
+ - Tall screens: one shot per viewport height (`eval "() => scrollTo(0, <y>)"`, then shoot),
57
+ never `--full-page`, which downscales a long page until its text is unreadable.
58
+ - Before reading, run `layout-check.js` in each of the four. It lists overlapping, clipped,
59
+ truncated and off-screen text and a page that scrolls sideways; every line it returns is a
60
+ check to settle on the shot. `sh .claude/skills/ui-verify/layout-check.check.sh` is its self-test.
61
+ - Read each shot for: spacing consistent with its neighbours, alignment, overlapping or cut text,
62
+ anything off-screen or out of its container, contrast in dark, and the ticket's designs. Every
63
+ shot gets a report line, a clean one too. A defect is a FAIL with the png path; it gets fixed
64
+ and shot again.
65
+
66
+ ```bash
67
+ mkdir -p /tmp/ui-verify
68
+ echo '{"browser":{"contextOptions":{"deviceScaleFactor":2}}}' > /tmp/ui-verify/2x.json
69
+ playwright-cli -s=m open <url> --config=/tmp/ui-verify/2x.json && playwright-cli -s=m resize 390 844
70
+ playwright-cli -s=m eval "$(cat .claude/skills/ui-verify/layout-check.js)"
71
+ playwright-cli -s=m screenshot --hires --filename=/tmp/ui-verify/<screen>-mobile-dark.png
72
+ # switch theme, reload, repeat as -light; desktop in its own session:
73
+ # no --config, resize 1440 900, no --hires
74
+ ```
75
+
76
+ In the PR, after the Videos section, collapsed per screen (upload as in `docs/agents/forge.md`):
77
+
78
+ ```markdown
79
+ <details><summary>Screenshots: <screen></summary>
80
+
81
+ | | Light | Dark |
82
+ | ------- | ------------------------ | ----------------------- |
83
+ | Mobile | ![mobile light](upload) | ![mobile dark](upload) |
84
+ | Desktop | ![desktop light](upload) | ![desktop dark](upload) |
85
+
86
+ </details>
87
+ ```
88
+
89
+ ## Video evidence
90
+
91
+ Last, once, on the final commit after the tests are green and review is clean (`ship` step 4b):
92
+ a clip taken earlier is thrown away by the next fix round. One clip per acceptance criterion
93
+ (`docs/standards/testing.md`), recorded on the local stack. The one early clip is a fix's
94
+ "before", taken when the bug is reproduced; its "after" twin comes here. Never read a video back.
95
+
96
+ ACs a spec drives come first: every spec that drives one, new or existing, in one run with video
97
+ on (the repo's record switch, or `test.use({ video: 'on' })`); copy each
98
+ `test-results/<test>/video.webm` to `<ac>-<slug>-{desktop,mobile}.webm`. Only an AC no spec
99
+ drives gets `playwright-cli`, one at mobile size (`resize 390 844`) and one at desktop
100
+ (`resize 1440 900`), each switching theme with the app's toggle partway through:
101
+
102
+ ```bash
103
+ playwright-cli -s=v video-start /tmp/ui-verify/ac1-<slug>-mobile.webm
104
+ playwright-cli -s=v video-show-actions
105
+ playwright-cli -s=v video-chapter "AC1: <criterion in a few words>" --duration=1500
106
+ # drive the AC ... toggle the theme ... until its outcome is on screen
107
+ playwright-cli -s=v video-stop
108
+ ```
109
+
110
+ ## Report
111
+
112
+ One line per check: `PASS|FAIL - <check> - rung <n> - expected <x>, got <y> - <artifact path>`,
113
+ plus the failure detail for each FAIL. A pixel claim neither a measurement nor the shot can
114
+ settle is `UNSURE - <check> - <what is ambiguous> - <png path>`, never a guess. Return every shot
115
+ and clip path.
116
+
117
+ ## Bugs
118
+
119
+ Reproduce here first, as close to real use as possible, then fix, then rerun the same checks.
120
+ A fix without a before and after run is unverified.
@@ -0,0 +1,58 @@
1
+ #!/bin/sh
2
+ # Self-check for layout-check.js: the page below plants 5 defects next to 5 look-alikes that are fine.
3
+ set -e
4
+ dir=$(dirname "$0")
5
+ fixture=$(cat <<'HTML'
6
+ <!doctype html>
7
+ <html>
8
+ <body style="margin: 0; font: 16px sans-serif; width: 390px">
9
+ <p>
10
+ Clean wrapped paragraph that wraps across lines with a <a href="#">link inside</a> and more
11
+ text after it.
12
+ </p>
13
+ <div data-testid="overlap-card" style="position: relative; height: 40px">
14
+ <span>Balance</span><span style="position: absolute; left: 20px; top: 0">USDT 12.50</span>
15
+ </div>
16
+ <div data-testid="clip-box" style="height: 18px; overflow: hidden">
17
+ <p style="margin: 0">Line one<br />Line two cut off</p>
18
+ </div>
19
+ <div
20
+ data-testid="ellipsis"
21
+ style="width: 80px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
22
+ >
23
+ A very long game title
24
+ </div>
25
+ <span style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0)"
26
+ >Screen reader only</span
27
+ >
28
+ <div style="overflow: hidden; width: 200px; white-space: nowrap">
29
+ <div style="display: inline-block; width: 200px">Slide one</div>
30
+ <div style="display: inline-block; width: 200px">Slide two parked</div>
31
+ </div>
32
+ <div data-testid="wide" style="white-space: nowrap">
33
+ This label is far too wide for a mobile viewport and runs out
34
+ </div>
35
+ <div style="overflow-x: auto; white-space: nowrap; width: 200px">
36
+ Scrollable chips row that is wider than its box on purpose
37
+ </div>
38
+ <p style="position: relative">Under the modal text</p>
39
+ <div style="position: fixed; top: 0; left: 0; width: 390px; height: 60px; background: #fff">
40
+ <p style="margin: 4px">Modal title over page text</p>
41
+ </div>
42
+ </body>
43
+ </html>
44
+ HTML
45
+ )
46
+ url="data:text/html;base64,$(printf '%s' "$fixture" | base64 | tr -d '\n')"
47
+ playwright-cli -s=layout-check open "$url" >/dev/null 2>&1
48
+ playwright-cli -s=layout-check resize 390 844 >/dev/null
49
+ out=$(playwright-cli -s=layout-check eval "$(cat "$dir/layout-check.js")")
50
+ playwright-cli -s=layout-check close >/dev/null
51
+ for expected in PAGE-SCROLLS-X 'CLIPPED.*clip-box' 'TRUNCATED.*ellipsis' 'OFFSCREEN.*wide' 'OVERLAP.*overlap-card.*with.*overlap-card'; do
52
+ echo "$out" | grep -q "$expected" || { echo "FAIL: missing $expected"; echo "$out"; exit 1; }
53
+ done
54
+ # Findings never carry page text: a real screen can show player data.
55
+ ! echo "$out" | grep -qE 'Balance|USDT|Line two|game title|too wide' || { echo "FAIL: page text in findings"; echo "$out"; exit 1; }
56
+ count=$(echo "$out" | grep -cE '^ "(PAGE|CLIPPED|TRUNCATED|OFFSCREEN|OVERLAP)')
57
+ [ "$count" -eq 5 ] || { echo "FAIL: $count findings, expected 5"; echo "$out"; exit 1; }
58
+ echo PASS