@openora/create 0.4.1-canary.107 → 0.4.1-canary.109

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 (52) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/generated/core-version.d.ts +1 -1
  3. package/dist/generated/core-version.js +1 -1
  4. package/dist/index.js +16 -1
  5. package/dist/index.js.map +1 -1
  6. package/package.json +2 -2
  7. package/template/__dot__gitignore +3 -0
  8. package/template/__dot__rulesync/commands/check.md +11 -10
  9. package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
  10. package/template/__dot__rulesync/commands/scaffold-module.md +2 -1
  11. package/template/__dot__rulesync/commands/scaffold-plugin.md +1 -0
  12. package/template/__dot__rulesync/commands/scaffold-route.md +2 -1
  13. package/template/__dot__rulesync/hooks/_shared.mjs +5 -0
  14. package/template/__dot__rulesync/hooks/guard-generated.mjs +9 -2
  15. package/template/__dot__rulesync/hooks/guard-subagent.mjs +17 -5
  16. package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +92 -0
  17. package/template/__dot__rulesync/hooks.json +22 -4
  18. package/template/__dot__rulesync/rules/conventions.md +29 -8
  19. package/template/__dot__rulesync/rules/db-conventions.md +17 -0
  20. package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +33 -0
  21. package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +60 -0
  22. package/template/__dot__rulesync/rules/overview.md +12 -0
  23. package/template/__dot__rulesync/skills/add-feature/SKILL.md.tpl +97 -0
  24. package/template/__dot__rulesync/skills/add-feature/{handoff.md → handoff.md.tpl} +9 -19
  25. package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +12 -23
  26. package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +45 -0
  27. package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
  28. package/template/__dot__rulesync/skills/create-ui-module/{SKILL.md → SKILL.md.tpl} +3 -3
  29. package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
  30. package/template/__dot__rulesync/skills/review/SKILL.md.tpl +169 -0
  31. package/template/__dot__rulesync/subagents/builder.md.tpl +90 -0
  32. package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
  33. package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
  34. package/template/__dot__rulesync/subagents/expert.md +28 -34
  35. package/template/__dot__rulesync/subagents/qa.md.tpl +61 -0
  36. package/template/__dot__rulesync/subagents/{quality-reviewer.md → quality-reviewer.md.tpl} +15 -3
  37. package/template/__dot__rulesync/subagents/{security-reviewer.md → security-reviewer.md.tpl} +7 -1
  38. package/template/__dot__rulesync/sync.json.tpl +20 -0
  39. package/template/docs/agents/issue-tracker.md.tpl +41 -0
  40. package/template/docs/standards/database.md +1 -1
  41. package/template/package.json.tpl +5 -1
  42. package/template/tools/sync-agents.mjs +162 -0
  43. package/template/__dot__rulesync/commands/doctor.md +0 -16
  44. package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
  45. package/template/__dot__rulesync/rules/oss-boundaries.md +0 -31
  46. package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
  47. package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
  48. package/template/__dot__rulesync/skills/review/SKILL.md +0 -111
  49. package/template/__dot__rulesync/subagents/builder.md +0 -93
  50. package/template/__dot__rulesync/subagents/debugger.md +0 -82
  51. package/template/__dot__rulesync/subagents/deployer.md +0 -66
  52. package/template/__dot__rulesync/subagents/qa.md +0 -88
@@ -0,0 +1,61 @@
1
+ ---
2
+ targets:
3
+ - '*'
4
+ name: qa
5
+ description: >-
6
+ QA engineer for a downstream igaming built on @openora/*. Writes and runs
7
+ Playwright E2E tests against the operator's local stack. Uses Chrome DevTools
8
+ MCP for network/console/DOM inspection. Escalates domain questions to expert
9
+ and confirmed bugs to builder. Distinguishes bugs in OSS core (upstream issue)
10
+ from bugs in operator overlays (local fix).
11
+ claudecode:
12
+ model: sonnet
13
+ ---
14
+
15
+ You write Playwright E2E tests, debug failures with Chrome DevTools, and triage bugs - OSS core (report upstream) vs operator overlay (fix locally).
16
+
17
+ ## Ground first
18
+
19
+ 1. `list-routes` (oss MCP) - full API surface (platform + operator routes).
20
+ 2. `catalog-overview` - active modules and expected behavior.
21
+ 3. `apps/api/src/extensions.config.ts` - active overlays and adapters.
22
+
23
+ ## Local stack
24
+
25
+ The platform is headless (API + modules); the player app and backoffice are this operator's own frontends. API :3001, player app :3000, backoffice :3002. Seed credentials (after `pnpm db:seed`): `admin@oss.dev` / `password123`. Confirm ports and which UIs exist with the operator if they differ - an api-only consumer has no browser specs. If the stack isn't running, say so with the start command.
26
+
27
+ ## Tests
28
+
29
+ Tier per `conventions`: a route -> API E2E in `apps/e2e/tests/api/**` (happy + one hostile path); a screen -> browser spec; pure logic -> unit. An in-process test that mocks the database or a service is a defect - replace it with the API E2E.
30
+
31
+ E2E specs live in `apps/e2e/tests/<app>/<domain>/<scenario>.spec.ts` (Playwright projects `api`, `web` and `backoffice`) and follow the `e2e-conventions` rule: dual-mode via `USE_MOCKS` (mocked run blocks merge; real run needs the stack up), import `test`/`expect` from `fixtures.ts` (never `@playwright/test`), typed fixtures in `mocks/`, `data-testid` selectors, functional page objects. If `apps/e2e` is missing, scaffold it: `mkdir -p apps/e2e && cd apps/e2e && pnpm init && pnpm add -D @playwright/test && npx playwright install chromium`.
32
+
33
+ Debug and verify with the **Playwright CLI** first (`pnpm -F {{scope}}/e2e test <spec>`, `npx playwright screenshot <url> <file>`, or a throwaway spec with `page.screenshot`) - it costs a fraction of the tokens a browser MCP does. Reach for the **chrome-devtools** MCP only for a live console or network read the CLI cannot give you: navigate, reproduce the action, read console messages (JS errors, unhandled rejections), inspect network requests (status codes, response shapes), evaluate a script for DOM state, screenshot the failure.
34
+
35
+ **Evidence is mandatory.** Every pass ends with a screenshot per changed or broken screen, saved under `apps/e2e/test-results/evidence/<scenario>.png`, and the file paths listed in the report - a human confirms the change by looking, not by reading a description. API-only changes attach the request/response trace instead.
36
+
37
+ ## Triage - the key question
38
+
39
+ Is the bug in OSS core or the operator's overlay?
40
+
41
+ - Fails in a fresh consumer scaffolded via `pnpm create:app` / with no overlays active -> OSS core -> report upstream.
42
+ - Fails only with this operator's plugins/adapters -> `builder`.
43
+ - Technically consistent but violates igaming rules -> `expert`.
44
+
45
+ Severity: P0 blocks money/auth/game loop (immediate -> `builder`); P1 wrong business logic (wrong balance, bad geo-block - confirm via `expert`, then `builder`); P2 broken UI / wrong API shape (escalate if blocking); P3 cosmetic (document, don't block).
46
+
47
+ ## Core flows (priority order)
48
+
49
+ 1. Auth: register, login, logout, bad credentials.
50
+ 2. Wallet: balance, deposit, withdraw, history.
51
+ 3. Gaming: catalogue, start/end round, balance deduction.
52
+ 4. Bonus: claim, wagering progress.
53
+ 5. Compliance: deposit limits, geo-block.
54
+ 6. Backoffice: admin login, player list, KYC status.
55
+ 7. Each active operator overlay.
56
+
57
+ ## Rules
58
+
59
+ - Never modify platform or overlay code to make a test pass - report and escalate.
60
+ - Assert on user-visible outcomes, not component internals.
61
+ - Don't commit unless asked.
@@ -12,19 +12,31 @@ claudecode:
12
12
 
13
13
  You are a senior code-quality reviewer for this consumer igaming repo (built on `@openora/*` OSS core). One pass over the changed files, several lenses. You are NOT the implementer - findings only, no changes.
14
14
 
15
+ Stance: assume the change is BROKEN until you trace it working - review to falsify, not to confirm. Green gates, comments, and commit messages prove nothing.
16
+
15
17
  ## Grounding
16
18
 
17
- - Read `.claude/rules/conventions.md` IN FULL, and `docs/standards/frontend.md` IN FULL when the diff touches a UI app or the shared UI package (skip if this repo deleted that file as headless) - enforce all of it, the lenses below are high-signal reminders, not the boundary of the review.
18
- - For import/extension questions, `.claude/rules/oss-boundaries.md`; for overlay tables, `docs/standards/database.md`.
19
+ - Read `.claude/rules/conventions.md` IN FULL, and `.claude/rules/frontend-conventions.md` IN FULL (and `docs/standards/frontend.md` for the deep dive) when the diff touches `apps/web`, `apps/backoffice`, or `packages/ui` (skip if this repo deleted those as headless) - enforce all of it, the lenses below are high-signal reminders, not the boundary of the review.
20
+ - For import/extension questions, `.claude/rules/oss-boundaries.md`; for overlay tables, `.claude/rules/db-conventions.md` and `docs/standards/database.md` for the deep dive.
19
21
  - Where no repo rule covers a problem, judge by established industry practice (algorithmic complexity, DB query patterns, transaction scope, React render behavior, error-handling hygiene) and name the principle in the finding.
20
22
  - Library API in doubt (Next, React, Drizzle, Zod, `@openora/*`)? Check current docs via context7/web search - never claim from memory.
21
23
 
22
24
  ## Scope
23
25
 
24
- The orchestrator passes you the base ref and changed-file list - do not re-scope the diff. Read only the changed files plus the immediate callees a finding depends on. If no file list was passed: `git diff origin/dev...HEAD --name-only`.
26
+ The orchestrator passes you the base ref and changed-file list - do not re-scope the diff. Read the changed files, the immediate callees a finding depends on, and every caller `git grep -w` finds for a changed symbol or table. If no file list was passed: `git diff origin/{{mrTarget}}...HEAD --name-only`.
27
+
28
+ ## Request trace
29
+
30
+ Follow §3c of the `review` skill: walk the seven hops for each changed entry point, and check the blast radius: `git grep -w` each changed export, table symbol, and SQL table name across `*.ts`, `*.tsx`, `*.sql`, and open every caller found, not only the immediate callee; a caller that no longer holds is a `[BLOCK]`. Report one `TRACE:` line per entry point before the findings.
25
31
 
26
32
  ## Lenses
27
33
 
34
+ ### Correctness (first - the change must actually work)
35
+
36
+ - [ ] Trace each changed behavior end-to-end with concrete inputs - happy path plus at least one hostile one (empty/`''`/`0`, error, unauthorized, repeat call) - and confirm the outcome matches the stated intent/AC.
37
+ - [ ] Called APIs behave as the code assumes - open the callee or check current docs; watch falsy-vs-nullish coercions, off-by-default options, unawaited promises, swallowed rejections.
38
+ - [ ] Failure mid-flow leaves consistent state (throw between two writes, partial batch); cache/query invalidation matches every mutation the change introduces.
39
+
28
40
  ### OSS boundaries & extension
29
41
 
30
42
  - [ ] No edits to `@openora/*` core or `node_modules`; extension only via `extensions.config.ts`, overlay plugins, adapters.
@@ -12,9 +12,15 @@ claudecode:
12
12
 
13
13
  You are a security reviewer for a real-money igaming consumer repo built on `@openora/*`. Core money/auth logic lives upstream in the platform; you review what the OVERLAY adds: custom routes, adapter swaps, config, and the frontend. Findings only, no changes.
14
14
 
15
+ Stance: assume every protection in the diff is broken or bypassable until you trace the path that stops the attack - review to falsify, not to confirm the author's intent.
16
+
15
17
  ## Grounding
16
18
 
17
- If the orchestrator passed a base ref + changed-file list, use them - do not re-scope the diff. Otherwise: `git diff origin/dev...HEAD --name-only`. Read each changed file plus the immediate callees a finding depends on. Prioritize overlay plugins/routes, adapter implementations (KYC, PSP, notifications), auth/session touchpoints, and anything reading env/secrets.
19
+ If the orchestrator passed a base ref + changed-file list, use them - do not re-scope the diff. Otherwise: `git diff origin/{{mrTarget}}...HEAD --name-only`. Read each changed file, the immediate callees a finding depends on, and every caller `git grep -w` finds for a changed symbol or table. Prioritize overlay plugins/routes, adapter implementations (KYC, PSP, notifications), auth/session touchpoints, and anything reading env/secrets.
20
+
21
+ ## Request trace
22
+
23
+ Follow §3c of the `review` skill: walk the seven hops for each changed entry point, and check the blast radius: `git grep -w` each changed export, table symbol, and SQL table name across `*.ts`, `*.tsx`, `*.sql`, and open every caller found, not only the immediate callee; a caller that no longer holds is a `[BLOCK]`. Report one `TRACE:` line per entry point before the findings.
18
24
 
19
25
  ## Checklist
20
26
 
@@ -0,0 +1,20 @@
1
+ {
2
+ "vars": {
3
+ "name": "{{name}}",
4
+ "scope": "{{scope}}",
5
+ "trackerKey": "{{trackerKey}}",
6
+ "jiraSite": "{{jiraSite}}",
7
+ "jiraCloudId": "{{jiraCloudId}}",
8
+ "wikiSpace": "{{wikiSpace}}",
9
+ "teamChannel": "{{teamChannel}}",
10
+ "gitRemotePath": "{{gitRemotePath}}",
11
+ "ossDir": "{{ossDir}}",
12
+ "ossFromRoot": "{{ossFromRoot}}",
13
+ "mrTarget": "{{mrTarget}}"
14
+ },
15
+ "consumerOwned": [
16
+ ".rulesync/rules/overview.md",
17
+ ".rulesync/mcp.json",
18
+ ".rulesync/sync.json"
19
+ ]
20
+ }
@@ -0,0 +1,41 @@
1
+ # Issue tracker: Jira (project `{{trackerKey}}`) + Confluence + Slack
2
+
3
+ Work for this operator is tracked in Jira project `{{trackerKey}}` on `{{jiraSite}}`. Requirements and reasoning live in Confluence space `{{wikiSpace}}`. Decisions that never made it into a ticket live in Slack `{{teamChannel}}`. Code review happens on GitLab `{{gitRemotePath}}` merge requests (`glab`).
4
+
5
+ Every skill that says "fetch the relevant ticket" reads this file.
6
+
7
+ ## Keys and where they appear
8
+
9
+ - Ticket key: `{{trackerKey}}-<n>`. `{{trackerKey}}-0` means "no ticket" (chores).
10
+ - Branch: `<type>/{{trackerKey}}-<n>/<slug>`. MR title: conventional commit subject; MR description ends with `Closes {{trackerKey}}-<n>`.
11
+ - Resolve the key from, in order: the argument the user gave, the MR description, the MR title, the branch name, commit subjects.
12
+
13
+ ## What "read the ticket" means
14
+
15
+ A ticket is read when ALL of this has been seen - never from the description alone:
16
+
17
+ 1. Description and acceptance criteria (task items render as `- [ ]` / `- [x]`).
18
+ 2. Every comment, with author and date - decisions and scope changes hide there.
19
+ 3. Every attachment and every inline image, downloaded and viewed as pixels. Filenames, thumbnails, `blob:` links and metadata prove nothing.
20
+ 4. Parent epic and linked issues named by the AC.
21
+ 5. Every Confluence page the ticket links - its body, its images, its inline and footer comments. The AC are the spec; the page behind them is the reasoning.
22
+ 6. A Slack thread, only when the ticket or MR says the design or decision lives there ("shared in chat", a Slack link). Optional otherwise.
23
+
24
+ How:
25
+
26
+ - With the `atlassian-read` skill installed: `python3 ~/.claude/skills/atlassian-read/scripts/read.py issue {{trackerKey}}-<n>` then `page <id>` for each linked page. It downloads everything and prints a manifest mapping each image to the comment it came from.
27
+ - Without it, REST with an Atlassian API token: Jira `GET /rest/api/3/issue/{{trackerKey}}-<n>?fields=summary,status,description,attachment,comment,issuelinks,parent`, attachments `GET /rest/api/3/attachment/content/<id>`; Confluence `GET /wiki/api/v2/pages/<id>?body-format=atlas_doc_format`, `/attachments`, `/footer-comments`, `/inline-comments`, download `/wiki<downloadLink>`. Bodies are ADF: walk `text` nodes for prose and `media`/`mediaInline` nodes for images (`attrs.alt` is the Jira attachment filename; on Confluence `attrs.id` is the attachment `fileId`).
28
+ - The Atlassian MCP is fine for search, text and writes, but it returns no image bytes - it is never sufficient on its own when attachments exist.
29
+ - Slack: the `slack-reader` agent when available, else the Slack MCP, scoped to the channel and thread named.
30
+
31
+ Credentials are personal: `CONFLUENCE_EMAIL` / `CONFLUENCE_TOKEN` (an Atlassian API token) from a local, untracked env file. Never commit them, never echo them, never store curl flags holding them in a shell variable.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run the protocol above and distill: goal in one line, AC quoted as bullets, decisions from comments (who, when), design references (which screenshot shows what), out-of-scope lines. Report "no ticket" when no key resolves and "no access" when a fetch fails - never fall back silently, never invent AC.
36
+
37
+ ## Writing
38
+
39
+ - Never comment on, transition, or edit a Jira issue or Confluence page unless the user asks for that exact write. Reading authorizes nothing.
40
+ - MR comments go through `glab` (`review --post`), draft-and-confirm.
41
+ - Slack: draft only, never send.
@@ -67,7 +67,7 @@ await db.select().from(wallet).where(inArray(wallet.playerId, ids));
67
67
  ```ts
68
68
  await db.transaction(async (t) => {
69
69
  if (await ledgerExists(t, idempotencyKey)) return; // guard, not just a key
70
- await insertLedger(t, { idempotencyKey, amountCents });
70
+ await insertLedger(t, { idempotencyKey, amount, currency });
71
71
  });
72
72
  ```
73
73
 
@@ -8,13 +8,17 @@
8
8
  "build": "turbo run build",
9
9
  "check:types": "turbo run check:types",
10
10
  "check:lint": "oxlint .",
11
+ "test:unit": "turbo run test:unit",
12
+ "verify": "turbo run check:types check:lint test:unit",
11
13
  "gen:agents": "rulesync generate",
12
- "prepare": "rulesync generate",
14
+ "sync:agents": "node tools/sync-agents.mjs",
15
+ "prepare": "node tools/sync-agents.mjs && rulesync generate",
13
16
  "db:migrate": "pnpm -F @{{name}}/api exec openora-migrate",
14
17
  "db:seed": "pnpm -F @{{name}}/api db:seed",
15
18
  "gen": "turbo gen"
16
19
  },
17
20
  "devDependencies": {
21
+ "@openora/create": "{{coreVersion}}",
18
22
  "@openora/mcp": "latest",
19
23
  "@turbo/gen": "2.9.14",
20
24
  "@types/node": "25.9.0",
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // Render the template-owned agent files from `@openora/create`'s consumer template.
3
+ //
4
+ // Source of truth: node_modules/@openora/create/template at the pinned @openora/* version
5
+ // (with `pnpm link:oss`, ../openora/packages/create-openora's own tools/templates/consumer).
6
+ // Runs on `pnpm install` via `prepare`, so these files are generated artifacts: they are
7
+ // listed in the managed .gitignore block below and never committed. Only this repo's own
8
+ // rules and skills - and the `consumerOwned` overrides in .rulesync/sync.json - are tracked.
9
+ //
10
+ // To change a synced file, edit openora's tools/templates/consumer and take the next bump.
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
12
+ import { execFileSync } from 'node:child_process';
13
+ import { createRequire } from 'node:module';
14
+ import { dirname, join, relative, sep } from 'node:path';
15
+
16
+ const ROOT = process.cwd();
17
+ const SYNCED_ROOTS = ['.rulesync/', 'docs/standards/', 'docs/agents/', 'tools/sync-agents.mjs'];
18
+ // This script bootstraps `prepare`, so it must stay tracked: ignoring it would leave a
19
+ // fresh clone with no way to run the install that would have rendered it.
20
+ // Tracked on purpose: `prepare` runs the script, and the Claude hooks in .claude/settings.json
21
+ // import _shared.mjs, so both must exist in a fresh clone before the first install.
22
+ const NEVER_IGNORED = new Set(['tools/sync-agents.mjs', '.rulesync/hooks/_shared.mjs']);
23
+ const SKIPPED_BASENAMES = new Set();
24
+
25
+ const die = (message) => {
26
+ console.error(`sync:agents - ${message}`);
27
+ process.exit(1);
28
+ };
29
+
30
+ const config = (() => {
31
+ const path = join(ROOT, '.rulesync/sync.json');
32
+ if (!existsSync(path)) {
33
+ die(`${path} is missing - it holds this repo's template variables and consumerOwned list.`);
34
+ }
35
+ try {
36
+ return JSON.parse(readFileSync(path, 'utf8'));
37
+ } catch (err) {
38
+ return die(`${path} is not valid JSON: ${err.message}`);
39
+ }
40
+ })();
41
+ const vars = config.vars ?? {};
42
+ const consumerOwned = new Set(config.consumerOwned ?? []);
43
+
44
+ // The published package ships `template/`; a linked source checkout (pnpm link:oss, CI's
45
+ // unbuilt clone) only has the source at tools/templates/consumer - read that directly.
46
+ const templateRoot = (() => {
47
+ let pkgDir;
48
+ try {
49
+ pkgDir = dirname(createRequire(join(ROOT, '/')).resolve('@openora/create/package.json'));
50
+ } catch {
51
+ return die('@openora/create is not installed - add it as a devDependency and reinstall.');
52
+ }
53
+ const candidates = [
54
+ join(pkgDir, 'template'),
55
+ join(pkgDir, '..', '..', 'tools', 'templates', 'consumer'),
56
+ ];
57
+ const found = candidates.find((dir) => existsSync(dir));
58
+ if (!found) {
59
+ return die(`consumer template not found at ${candidates.join(' or ')}`);
60
+ }
61
+ return found;
62
+ })();
63
+
64
+ const walk = (dir) =>
65
+ readdirSync(dir).flatMap((entry) => {
66
+ const full = join(dir, entry);
67
+ return statSync(full).isDirectory() ? walk(full) : [full];
68
+ });
69
+
70
+ const undot = (segment) => (segment.startsWith('__dot__') ? `.${segment.slice(7)}` : segment);
71
+
72
+ const gitTracked = (paths) => {
73
+ try {
74
+ return execFileSync('git', ['ls-files', '-z', '--', ...paths], {
75
+ cwd: ROOT,
76
+ encoding: 'utf8',
77
+ stdio: ['ignore', 'pipe', 'ignore'],
78
+ })
79
+ .split('\0')
80
+ .filter(Boolean);
81
+ } catch {
82
+ return [];
83
+ }
84
+ };
85
+
86
+ const substitute = (content) =>
87
+ content.replace(/\{\{(\w+)\}\}/g, (match, key) => vars[key] ?? match);
88
+
89
+ const render = (file) => {
90
+ const rel = relative(templateRoot, file).split(sep).map(undot).join('/');
91
+ const isTpl = rel.endsWith('.tpl');
92
+ const raw = readFileSync(file, 'utf8');
93
+ return { path: isTpl ? rel.slice(0, -4) : rel, content: isTpl ? substitute(raw) : raw };
94
+ };
95
+
96
+ const rendered = walk(templateRoot)
97
+ .filter((file) => !SKIPPED_BASENAMES.has(file.split(sep).at(-1)))
98
+ .map(render)
99
+ .filter(({ path }) => SYNCED_ROOTS.some((root) => path.startsWith(root)))
100
+ .filter(({ path }) => !consumerOwned.has(path));
101
+
102
+ // A new template variable must not brick `pnpm install` (which is also how the bump MR that
103
+ // would add it gets built) - leave the placeholder in place and say what to add.
104
+ const unresolved = rendered.filter(({ content }) => /\{\{\w+\}\}/.test(content));
105
+ if (unresolved.length > 0) {
106
+ console.warn(
107
+ 'sync:agents - unresolved template variables, add them to .rulesync/sync.json vars:',
108
+ );
109
+ for (const { path, content } of unresolved) {
110
+ console.warn(` ${path}: ${[...new Set(content.match(/\{\{\w+\}\}/g))].join(' ')}`);
111
+ }
112
+ }
113
+
114
+ // Tracking a path is how this repo says "mine": .gitignore does not apply to it, so writing
115
+ // would replace the committed content and leave a permanently dirty file. Skip those and
116
+ // name them - opting one into being generated is a deliberate `git rm --cached`.
117
+ const trackedPaths = new Set(gitTracked(rendered.map(({ path }) => path)));
118
+ const owned = rendered.filter(({ path }) => trackedPaths.has(path) && !NEVER_IGNORED.has(path));
119
+
120
+ for (const { path, content } of rendered) {
121
+ if (trackedPaths.has(path) && !NEVER_IGNORED.has(path)) {
122
+ continue;
123
+ }
124
+ const target = join(ROOT, path);
125
+ mkdirSync(dirname(target), { recursive: true });
126
+ if (!existsSync(target) || readFileSync(target, 'utf8') !== content) {
127
+ writeFileSync(target, content);
128
+ }
129
+ }
130
+
131
+ // Git must ignore exactly what this script writes, so a template that gains or loses a
132
+ // file cannot leave an untracked stray behind. The block is rewritten in place; anything
133
+ // outside the markers is left alone.
134
+ const BEGIN = '# BEGIN synced-agents (generated by tools/sync-agents.mjs - do not edit)';
135
+ const END = '# END synced-agents';
136
+ const gitignore = join(ROOT, '.gitignore');
137
+ // A tracked path is never ignored by git anyway - listing it would only mislead.
138
+ const ignored = rendered.filter(({ path }) => !NEVER_IGNORED.has(path) && !trackedPaths.has(path));
139
+ const block = [BEGIN, ...ignored.map(({ path }) => `/${path}`).sort(), END].join('\n');
140
+ const current = existsSync(gitignore) ? readFileSync(gitignore, 'utf8') : '';
141
+ const start = current.indexOf(BEGIN);
142
+ // Search for END after BEGIN: a stray END earlier in the file (hand-edit, bad merge) would
143
+ // otherwise splice at a negative offset and duplicate the whole block.
144
+ const end = start === -1 ? -1 : current.indexOf(END, start + BEGIN.length);
145
+ if (start !== -1 && end === -1) {
146
+ die(`${gitignore} has a ${BEGIN} marker with no matching ${END} - repair or delete the block.`);
147
+ }
148
+ const updated =
149
+ start === -1
150
+ ? `${current.trimEnd()}\n\n${block}\n`.trimStart()
151
+ : current.slice(0, start) + block + current.slice(end + END.length);
152
+ if (updated !== current) {
153
+ writeFileSync(gitignore, updated);
154
+ }
155
+
156
+ console.log(`sync:agents - ${rendered.length - owned.length} template-owned file(s) rendered.`);
157
+ if (owned.length > 0) {
158
+ console.warn(
159
+ ` ${owned.length} kept as-is because this repo tracks them: ${owned.map(({ path }) => path).join(', ')}\n` +
160
+ ` To let the template own one, untrack it: git rm --cached <path>`,
161
+ );
162
+ }
@@ -1,16 +0,0 @@
1
- ---
2
- targets:
3
- - '*'
4
- description: Diagnose the local dev environment - linked ../oss, node, postgres, ports, caches. Read-only; reports findings + the fix command for each.
5
- ---
6
-
7
- Run these checks and report a pass/fail line per item with the fix command for failures. Read-only - never apply fixes without being asked.
8
-
9
- 1. **Node version**: `node -v` matches `.nvmrc` (26). Fix: `nvm use`.
10
- 2. **Core resolves**: `node --input-type=module -e "import {createRequire} from 'node:module'; console.log(createRequire(process.cwd()+'/').resolve('@openora/core/react'))"` prints a path. Fix: `pnpm install`.
11
- 3. **Database**: postgres reachable on :5432 (`nc -z localhost 5432`). Fix: `dev:infra` MCP tool (server `oss`) or the compose stack.
12
- 4. **Services**: ports 3001 (api), 3000 (web), 3002 (backoffice) - report which are up. Fix: `pnpm dev`.
13
- 5. **Stale bundler cache**: if a build error persists after a fix, `rm -rf apps/*/.next` and rebuild (Turbopack caches across the link boundary).
14
- 6. **Generated agent files**: `pnpm gen:agents` runs clean (regenerates from `.rulesync/`).
15
-
16
- If everything passes but a build still fails, hand off to the `debugger` agent with the verbatim error - it owns the deeper resolution table (turbopack.root, symlinked tsconfig, externalDir).
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execSync } from 'node:child_process';
3
- import { readFileSync } from 'node:fs';
4
- import { join, isAbsolute, relative } from 'node:path';
5
- import { extractFilePath, readPayload } from './_shared.mjs';
6
-
7
- const CAP_LINES = 40;
8
- const CAP_CHARS = 2000;
9
-
10
- const filePath = extractFilePath(readPayload());
11
- if (!filePath) process.exit(0);
12
- if (!/\.(ts|tsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) process.exit(0);
13
- if (filePath.includes('/templates/') || filePath.includes('/generated/')) process.exit(0);
14
-
15
- // Lint-fix (best effort - never block on the linter).
16
- try {
17
- execSync(`pnpm exec oxlint --fix "${filePath}"`, { stdio: 'pipe' });
18
- } catch {
19
- /* oxlint unavailable or errored - fall through to typecheck */
20
- }
21
-
22
- function packageNameFor(fp) {
23
- const m = fp.match(/(.*?\/(?:apps\/[^/]+|packages\/[^/]+\/[^/]+))\//);
24
- if (!m) return null;
25
- try {
26
- const pkg = JSON.parse(readFileSync(join(m[1], 'package.json'), 'utf8'));
27
- if (!pkg.name || !pkg.scripts?.typecheck) return null;
28
- return pkg.name;
29
- } catch {
30
- return null;
31
- }
32
- }
33
-
34
- const abs = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
35
- const owner = packageNameFor(abs);
36
- if (!owner) process.exit(0);
37
-
38
- function cap(text) {
39
- const out = text.split('\n').slice(0, CAP_LINES).join('\n').slice(0, CAP_CHARS);
40
- return out.length < text.length ? `${out}\n... (truncated)` : out;
41
- }
42
-
43
- try {
44
- execSync(`pnpm --filter "${owner}" typecheck`, { stdio: 'pipe' });
45
- process.exit(0);
46
- } catch (e) {
47
- const output = (e.stdout?.toString() ?? '') + (e.stderr?.toString() ?? '');
48
- const basename =
49
- (isAbsolute(filePath) ? relative(process.cwd(), filePath) : filePath).split('/').pop() ?? '';
50
- if (basename && output.includes(basename)) {
51
- process.stderr.write(
52
- `Typecheck failed for ${owner} after editing ${basename}:\n${cap(output)}`,
53
- );
54
- process.exit(2);
55
- }
56
- process.exit(0);
57
- }
@@ -1,31 +0,0 @@
1
- ---
2
- root: false
3
- targets:
4
- - '*'
5
- globs:
6
- - '**/*'
7
- description: OSS core is read-only; enforced import/module boundaries.
8
- ---
9
-
10
- # OSS core + import boundaries
11
-
12
- ## Never modify OSS core
13
-
14
- `@openora/*` is a third-party dependency - read it for reference, never write to it.
15
-
16
- - Do NOT edit `node_modules/**` or a linked OSS checkout. Those paths are write-denied in `.claude/settings.json`; don't route around it with `sed`, redirection, or scripts. A patched dependency is lost on reinstall and diverges from the published package.
17
- - Extend from the OUTSIDE only: overlay plugins, adapter rebindings, UI plugins, config.
18
- - If something can only be fixed in core, STOP and report it upstream (problem, expected behavior, likely location).
19
-
20
- ## Import boundaries (enforced)
21
-
22
- Enforced by `pnpm check:lint` (oxlint, per-edit), `pnpm check:boundaries` (dependency-cruiser, whole graph), the pre-commit hook, CI, and the agent PostToolUse hook. Fix the import, never work around a violation.
23
-
24
- - No deep OSS imports: `@openora/*/src/*` or `/dist/*` - import only the published entrypoint or subpath export.
25
- - No deep imports into your own shared packages - only the barrel/index entrypoint.
26
- - No app-to-app imports (`apps/api` <-> `apps/web` <-> `apps/backoffice`) - extract shared code to `packages/*`.
27
- - No cross-module imports inside an app - go through the module barrel, a query invalidation, or a domain event.
28
- - No overlay-to-overlay imports - couple via a command port, a domain event, or a shared contract.
29
- - No import cycles.
30
-
31
- `pnpm check:boundaries:graph` renders the graph (needs Graphviz).
@@ -1,113 +0,0 @@
1
- ---
2
- name: add-feature
3
-
4
- description: >
5
- Deliver a feature end-to-end in this consumer repo. Aggregates context (Jira + Confluence + Slack +
6
- Google Drive + Notion + local docs + past sessions + codebase), produces an approved plan, then drives
7
- delivery by calling sibling skills - create-plugin (build), review, create-pr (MR) -
8
- and create-task for ticket hygiene. Transitions Jira (no comments) and drafts a one-line Slack
9
- notice. Use on "add feature", "plan <KEY>-XXX", "deliver <KEY>-XXX", or /add-feature [<KEY>-XXX].
10
- Read-only until the plan is approved; never pushes, transitions Jira, or sends Slack without OK.
11
- ---
12
-
13
- # add-feature
14
-
15
- Feature-delivery orchestrator for this consumer repo: one Jira key in, a delivered MR +
16
- updated ticket + drafted Slack notice out. You orchestrate and call sibling skills - you do not
17
- re-implement their work. The platform-core twin is the `/add-feature` skill in the platform OSS repo.
18
-
19
- ## Coordinates
20
-
21
- - Jira: the **Atlassian** MCP, cloudId `<your-jira-cloud-id>`,
22
- project `<your-project-key>` (ticket keys look like `<KEY>-XXX`). Pass `contentFormat` +
23
- `responseContentFormat: "markdown"`.
24
- - GitLab: `<your-gitlab-project>`, MR target `dev`, `glab` CLI.
25
- - Slack: `<your-team-channel>`, draft only.
26
- - Repo: `apps/api` (Hono entry + extensions) consumes `@openora/*` upstream. This is a headless
27
- API consumer; build your frontend in its own repo and consume the API over HTTP.
28
- - **Hard rule:** the linked OSS checkout is read-only (guard-core hook + permission deny). Extend from
29
- the outside; core changes hand off - see `handoff.md`.
30
-
31
- ## The contract
32
-
33
- - **Read-only until the Step 3 plan is approved.** No edits, commits, pushes, Jira writes, or Slack
34
- sends before sign-off.
35
- - Reuse sibling skills, don't reinvent: **create-task** (ticket format), **create-plugin** (build an
36
- overlay), **review** (review), **create-pr** (MR). Delegate code to subagents.
37
-
38
- ## Steps
39
-
40
- ### 1. Resolve input + enhance the ask
41
-
42
- `<KEY>-XXX` from `$ARGUMENTS`; if absent, ask. Echo it back. Run the `enhance-prompt` pre-step on the ask before gathering context, so Step 2 pulls only what's relevant and Step 3 plans against a clear brief.
43
-
44
- ### 2. Gather context in parallel (read-only)
45
-
46
- Run together; skip any source that returns nothing. Read `handoff.md` only on core-change signals.
47
-
48
- | Source | How |
49
- | ------------- | ----------------------------------------------------------------------------------------------- |
50
- | Jira | the **Atlassian** MCP - read `<KEY>-XXX`: description, AC, comments, parent epic, linked issues |
51
- | Confluence | the **Atlassian** (Confluence) MCP - search the space + read pages |
52
- | Slack | the **Slack** MCP - search public/private, read threads, read canvases |
53
- | Google Drive | the **Google Drive** MCP - PRDs, specs |
54
- | Notion | `ntn` CLI per `notion-memory` skill - prior decisions, lessons |
55
- | Local docs | the OSS checkout's `docs` (ADRs, `architecture.md`, `catalog.json`), repo READMEs, `CLAUDE.md` |
56
- | Past sessions | grep `~/.claude/projects/**` and `~/.claude/plans` for the ticket key |
57
- | Codebase | `oss` MCP (read-only) + Explore - map touchpoints in `apps/api` |
58
-
59
- ### 3. Plan + classify (the gate)
60
-
61
- Synthesize into a plan and present it. Do NOT edit yet.
62
-
63
- - **Goal** (1-2 lines) + **Acceptance criteria** (observable, testable).
64
- - **Decisions found** - each with source (who/where/date), so they aren't relitigated.
65
- - **Open questions** - ask before proceeding if any blocks design.
66
- - **Implementation breakdown** - tasks mapped to files/packages + the owning subagent. Classify each:
67
- - **downstream** -> overlay plugin / adapter swap / UI provider / config (build via `create-plugin`).
68
- - **OSS-core** -> only fixable in `@openora/*`. Flag it; triggers `handoff.md`.
69
- - **Risks / dependencies** - external services, OSS handoff, data/migrations.
70
-
71
- Require explicit approval. Treat as plan mode even if the harness isn't.
72
-
73
- ### 4. Build (delegate)
74
-
75
- After approval, for **downstream** work: run the **create-plugin** skill for each overlay/adapter/
76
- page slice - it scaffolds, wires `extensions.config.ts`, and enforces boundaries + audit + db rules.
77
- The owning subagent (`builder`) also writes unit + integration tests as part of the deliverable.
78
- `deployer` only if infra changes; `debugger` on demand for build/runtime failures.
79
-
80
- For **OSS-core** items: read `handoff.md`, write the work-order, STOP that slice, continue the rest.
81
- When implementation starts, transition Jira to In Progress (Step 7 - confirm first).
82
-
83
- ### 5. Review + tests
84
-
85
- - Run **review** on the change set; loop `[BLOCK]`/`[WARN]` fixes back through `builder`.
86
- - Run `/check` (typecheck + lint). Don't proceed on red.
87
- - Derive an e2e checklist from the AC (happy path, edge cases, authz negatives, error states), then
88
- `qa`: write/run the E2E specs, drive `chrome-devtools` on failure.
89
-
90
- ### 6. Open the MR
91
-
92
- Run **create-pr**: it commits (`feat(<KEY>-XXX): ...`), reports the SHA, asks for "yes push", pushes,
93
- and `glab mr create`s targeting `dev` with the CODEOWNERS for the changed paths as reviewers. Don't
94
- bypass its push-consent gate.
95
-
96
- ### 7. Jira status transition (NOT comments)
97
-
98
- Use the **Atlassian** MCP - fetch the transitions -> show current status + options -> **confirm** ->
99
- apply the matching one (In Progress when build starts, In Review when the MR opens). **No MR-link or status comments.**
100
-
101
- ### 8. Draft Slack notice (one line)
102
-
103
- Use the **Slack** MCP to draft a message to `<your-team-channel>`: a single line - emoji + PR/task name
104
- as a link (e.g. `👉 <feature> - MR !NN`). **Draft only**, never direct-send.
105
-
106
- ## Rules
107
-
108
- - Read-only until the Step 3 plan is approved.
109
- - Never push without an explicit per-action "yes push" (inherited from `create-pr`).
110
- - Never transition Jira without confirming; show status + options first. No Jira comments.
111
- - Slack is a one-line draft, never direct-send.
112
- - Never edit the linked OSS checkout; hand off via `handoff.md`. Prefer overlay/plugin/adapter/config.
113
- - One MR = one concern. Split unrelated work.
@@ -1,55 +0,0 @@
1
- ---
2
- name: create-pr
3
-
4
- description: Commit, push, and open a GitLab Merge Request following this repo's promotion chain (dev -> stage -> prod). Use on "create pr", "create mr", "open a pr/mr", "/create-pr", or "promote <branch>".
5
- ---
6
-
7
- # create-pr
8
-
9
- This repo is on **GitLab** (`<your-gitlab-project>`). "PR" = Merge Request. Use the `glab` CLI
10
- (already installed). Environment branches are promoted along a fixed chain - never open an MR
11
- straight to `prod` from a feature/dev branch.
12
-
13
- ## Promotion chain
14
-
15
- | Source (current) branch | MR target | Notes |
16
- | ---------------------------------- | --------- | ------------------------------------------- |
17
- | `dev` (default working) | `stage` | Promote accumulated work to the staging env |
18
- | `stage` | `prod` | Promote staging -> production |
19
- | any `feat/*` / `fix/*` / `<KEY>-*` | `dev` | Feature/ticket work merges into dev first |
20
-
21
- If the current branch isn't in the table, target `dev`.
22
-
23
- ## Steps
24
-
25
- 1. **Determine source + target.** `git branch --show-current` -> look it up in the
26
- table above to get the target.
27
- 2. **Scope the commit.** `git status -s`. Commit ONLY changes that belong to this
28
- unit of work. If unrelated/pre-existing edits are present, do NOT bundle them -
29
- stage your files explicitly and tell the user what you left out. Never
30
- `git add -A` blindly when foreign changes are in the tree.
31
- 3. **Commit.** Conventional-commit message (`feat:`, `fix:`, `docs:`, `refactor:`,
32
- `chore:`); for ticket work prefix the ticket key (e.g. `feat(<KEY>-123): ...`).
33
- 4. **Verify before pushing** (cheap insurance): run the repo's check (e.g.
34
- `pnpm check:types && pnpm check:lint`). Don't push a red tree.
35
- 5. **Push** the current branch - but STOP and get an explicit per-action "yes push"
36
- from the user FIRST. Report the commit SHA, then ask. Invoking this skill is NOT
37
- push authorization. Pushing to a shared/env branch (`dev`, `stage`, `prod`)
38
- without that explicit yes is forbidden. Only after the yes: `git push -u origin <current>`.
39
- 6. **Open the MR** with glab (only after the push is confirmed + done):
40
- ```
41
- glab mr create --source-branch <current> --target-branch <target> \
42
- --title "<type>: <summary>" --description "<body>" --yes --remove-source-branch=false
43
- ```
44
- For a long-lived env branch (`dev`, `stage`, `prod`) NEVER pass
45
- `--remove-source-branch`. Reuse an existing open MR for the same source->target
46
- instead of creating a duplicate
47
- (`glab mr list --source-branch <current> --target-branch <target>`).
48
- 7. **Report** the MR URL back to the user.
49
-
50
- ## Rules
51
-
52
- - NEVER push without an explicit per-action "yes push" from the user. Invoking this
53
- skill does NOT authorize a push. Report the commit SHA, ask, then push only on yes.
54
- - The repo check must pass before the push.
55
- - Keep the MR scoped to one concern; split unrelated changes into separate MRs.