@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
@@ -1,44 +1,92 @@
1
1
  #!/usr/bin/env node
2
- // PreToolUse guard (Claude / Copilot CLI / Codex CLI / Gemini CLI).
3
- // Enforces the HARD RULE: never modify OSS core. Blocks shell write-primitives
4
- // and direct file edits that target the linked OSS checkout or node_modules.
5
- // Reads/blocks are allowed; only writes into protected paths are denied.
2
+ // PreToolUse guard (Claude / Copilot CLI / Codex CLI / Gemini CLI - rulesync renders the same
3
+ // script for every hook target, so the rule below holds identically in each tool).
4
+ // Enforces the HARD RULE: the linked OSS checkout and node_modules are read-only. The one
5
+ // exception is a git worktree under <oss>/.worktrees/ - the sanctioned place to change OSS
6
+ // code from this repo (oss-boundaries rule, `pnpm oss:worktree`). Reads are always allowed.
6
7
 
7
- import { extractCommand, extractFilePath, readPayload, deny } from './_shared.mjs';
8
+ import { readFileSync, realpathSync } from 'node:fs';
9
+ import { basename, dirname, join, resolve, sep } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { extractCommand, extractFilePaths, readPayload, deny } from './_shared.mjs';
8
12
 
9
13
  const payload = readPayload();
10
14
 
11
- // The linked OSS core checkout (baked in at generation time) plus any node_modules.
12
- const ossPath = String.raw`{{ossFromRoot}}`;
13
- const escapedOss = ossPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
- const PROTECTED = `(?:${escapedOss}|node_modules\\b)`;
15
+ // Paths resolve from the repo root (this file lives in <root>/.rulesync/hooks/), so an
16
+ // absolute path - what most tools send - is caught as well as the relative form.
17
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
18
+ const OSS_RELATIVE = String.raw`{{ossFromRoot}}`;
19
+ // This repo is often checked out as a git worktree, whose `.git` is a file pointing at
20
+ // <main>/.git/worktrees/<name>. The OSS checkout is a sibling of the MAIN checkout, so without
21
+ // this the guard would resolve it inside the worktree, match nothing, and let core writes pass.
22
+ const mainCheckoutOf = (dir) => {
23
+ try {
24
+ const pointer = readFileSync(join(dir, '.git'), 'utf8');
25
+ const gitdir = pointer.match(/^gitdir:\s*(.*[/\\]worktrees[/\\][^/\\]+?)\s*$/m)?.[1];
26
+ return gitdir ? resolve(dir, gitdir, '..', '..', '..') : dir;
27
+ } catch {
28
+ return dir; // `.git` is a directory (the main checkout) or missing
29
+ }
30
+ };
31
+ const OSS = resolve(mainCheckoutOf(ROOT), OSS_RELATIVE);
32
+
33
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ const OSS_PATH = `(?:${escape(OSS)}|${escape(OSS_RELATIVE)})`;
35
+ const PROTECTED = `(?:${OSS_PATH}|node_modules\\b)`;
36
+ // A path into a worktree with no `..` hop and no node_modules segment is sanctioned; it is
37
+ // masked out before the write checks run, so the rest of the command is still judged.
38
+ const WORKTREE_TOKEN = new RegExp(
39
+ String.raw`${OSS_PATH}/\.worktrees/(?![^\s'"|;&]*(?:\.\.|node_modules))[^\s'"|;&]*`,
40
+ 'g',
41
+ );
42
+ // cp, install, and rsync write only to their last argument, so copying out of core stays a read.
43
+ const LAST_ARG = String.raw`\s['"]?[^'"\s|;&]*${PROTECTED}[^'"\s|;&]*['"]?(?:\s+\d?>\S*)*\s*(?:[|;&]|$)`;
44
+ const GIT_WRITES = 'apply|am|checkout|restore|reset|clean|stash|merge|rebase|pull|switch|cherry-pick|revert|rm|mv';
45
+
46
+ const HOW_TO_CHANGE_OSS =
47
+ 'To change OSS code, run `pnpm oss:worktree <branch>` and edit inside ' +
48
+ `${OSS_RELATIVE}/.worktrees/ (rule: oss-boundaries). Otherwise extend from the OUTSIDE ` +
49
+ '(overlay plugin, adapter rebinding, UI plugin, config).';
15
50
 
16
- // 1) Shell command writing into a protected path (sed -i, redirect, tee, rm, ...).
17
- const command = extractCommand(payload);
51
+ // 1) Shell command writing into a protected path (sed -i, redirect, tee, cp, rm, git, ...).
52
+ const command = extractCommand(payload).replace(WORKTREE_TOKEN, '<oss-worktree>');
18
53
  if (command) {
19
54
  const writeToCore = [
20
- [new RegExp(String.raw`\b(?:sed|perl)\b[^|;&]*\s-\w*i\w*\b[^|;&]*(?:${PROTECTED})`), 'in-place edit (sed/perl -i)'],
21
- [new RegExp(String.raw`(?:>>?|>\|)\s*['"]?[^'"\s|;&]*(?:${PROTECTED})`), 'shell redirection'],
22
- [new RegExp(String.raw`\btee\b\s+(?:-a\s+)?['"]?[^'"\s|;&]*(?:${PROTECTED})`), 'tee'],
23
- [new RegExp(String.raw`\b(?:rm|truncate|dd|chmod|chown|unlink|shred|mv)\b[^|;&]*(?:${PROTECTED})`), 'destructive file op'],
55
+ [new RegExp(String.raw`\b(?:sed|perl)\b[^|;&]*\s-\w*i\w*\b[^|;&]*${PROTECTED}`), 'in-place edit (sed/perl -i)'],
56
+ [new RegExp(String.raw`(?:>>?|>\|)\s*['"]?[^'"\s|;&]*${PROTECTED}`), 'shell redirection'],
57
+ [new RegExp(String.raw`\btee\b\s+(?:-a\s+)?['"]?[^'"\s|;&]*${PROTECTED}`), 'tee'],
58
+ [new RegExp(String.raw`(?<![\w-])(?:cp|install|rsync)\b[^|;&]*${LAST_ARG}`), 'copy'],
59
+ [new RegExp(String.raw`(?<![\w-])(?:rm|truncate|dd|chmod|chown|unlink|shred|mv|ln|patch)\b[^|;&]*${PROTECTED}`), 'destructive file op'],
60
+ [new RegExp(String.raw`\bgit\b[^|;&]*${PROTECTED}[^|;&]*\s(?:${GIT_WRITES})\b`), 'git command'],
24
61
  ];
25
- const hit = writeToCore.find(([re]) => re.test(command));
62
+ const hit = writeToCore.find(([pattern]) => pattern.test(command));
26
63
  if (hit) {
27
- deny(
28
- `Blocked: this ${hit[1]} writes into OSS core / node_modules, which is read-only here. ` +
29
- 'Extend the platform from the OUTSIDE (overlay plugin, adapter rebinding, UI plugin, config). ' +
30
- 'If it can only be fixed in core, STOP and report it as an upstream OSS issue.',
31
- );
64
+ deny(`Blocked: this ${hit[1]} writes into the OSS checkout or node_modules, which are read-only here. ${HOW_TO_CHANGE_OSS}`);
32
65
  }
33
66
  }
34
67
 
35
- // 2) Direct file edit/write whose target is inside a protected path.
36
- const filePath = extractFilePath(payload);
37
- if (filePath && new RegExp(PROTECTED).test(filePath)) {
38
- deny(
39
- `Blocked: ${filePath} is inside OSS core / node_modules, which is read-only here. ` +
40
- 'Extend from the OUTSIDE (overlay, adapter, UI plugin, config), or report an upstream OSS issue.',
41
- );
68
+ // The path on disk, symlinks followed, so a link in this repo that points into the checkout
69
+ // does not pass as a local path. A file not yet created resolves through its nearest existing
70
+ // ancestor.
71
+ const physical = (path) => {
72
+ try {
73
+ return realpathSync(path);
74
+ } catch {
75
+ const parent = dirname(path);
76
+ return parent === path ? path : join(physical(parent), basename(path));
77
+ }
78
+ };
79
+
80
+ // 2) Direct file edit/write whose target resolves inside a protected path.
81
+ const oss = physical(OSS);
82
+ for (const filePath of extractFilePaths(payload)) {
83
+ const written = resolve(payload.cwd ?? process.cwd(), filePath);
84
+ const target = physical(written);
85
+ const isInside = (dir) => target.startsWith(dir + sep);
86
+ const inNodeModules = [written, target].some((path) => /(?:^|[\\/])node_modules(?:[\\/]|$)/.test(path));
87
+ if (inNodeModules || (isInside(oss) && !isInside(join(oss, '.worktrees')))) {
88
+ deny(`Blocked: ${filePath} is inside the OSS checkout or node_modules, which are read-only here. ${HOW_TO_CHANGE_OSS}`);
89
+ }
42
90
  }
43
91
 
44
92
  process.exit(0);
@@ -1,34 +1,36 @@
1
1
  #!/usr/bin/env node
2
- // PreToolUse guard (Claude / Copilot CLI / Codex CLI).
3
- // Blocks edits to files generated by rulesync (`pnpm sync:agents`). The single
2
+ // PreToolUse guard (Claude / Copilot CLI / Codex CLI / Gemini CLI).
3
+ // Blocks edits to files generated by rulesync (`pnpm gen:agents`). The single
4
4
  // source of truth is .rulesync/; editing a generated mirror is lost on the next
5
5
  // sync. Fail-open: only a positive match denies.
6
6
 
7
- import { extractFilePath, readPayload, deny } from './_shared.mjs';
7
+ import { extractFilePaths, readPayload, deny } from './_shared.mjs';
8
8
  import { relative, isAbsolute } from 'node:path';
9
9
 
10
- const payload = readPayload();
11
- const filePath = extractFilePath(payload);
12
- if (!filePath) process.exit(0);
13
-
14
- const rel = isAbsolute(filePath) ? relative(process.cwd(), filePath) : filePath;
15
-
10
+ // Everything rulesync generates from .rulesync/.
16
11
  const generated = [
17
12
  /^AGENTS\.md$/,
18
13
  /^CLAUDE\.md$/,
14
+ /^GEMINI\.md$/,
19
15
  /^\.github\/copilot-instructions\.md$/,
20
16
  /^\.mcp\.json$/,
21
17
  /^\.vscode\/mcp\.json$/,
22
18
  /^\.codex\/config\.toml$/,
19
+ /^\.gemini\/settings\.json$/,
23
20
  /^\.claude\/(agents|commands)\//,
24
21
  /^\.github\/(agents|prompts)\//,
22
+ /^\.gemini\/(agents|commands)\//,
25
23
  ];
26
24
 
27
- if (generated.some((re) => re.test(rel))) {
28
- deny(
29
- `Blocked: ${rel} is generated by rulesync (\`pnpm sync:agents\`). ` +
30
- 'Edit the source under .rulesync/ instead (rules/, subagents/, commands/, mcp.json), then run `pnpm sync:agents`.',
31
- );
25
+ for (const filePath of extractFilePaths(readPayload())) {
26
+ // Repo-relative (hooks run with cwd = repo root) - name-agnostic.
27
+ const rel = isAbsolute(filePath) ? relative(process.cwd(), filePath) : filePath;
28
+ if (generated.some((re) => re.test(rel))) {
29
+ deny(
30
+ `Blocked: ${rel} is generated by rulesync (\`pnpm gen:agents\`). ` +
31
+ 'Edit the source under .rulesync/ instead (rules/, subagents/, commands/, mcp.json), then run `pnpm gen:agents`.',
32
+ );
33
+ }
32
34
  }
33
35
 
34
36
  process.exit(0);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse guard: no merge/pull request until `pnpm verify` passed on the commit being sent.
3
+ // `pnpm verify` ends with `node .rulesync/hooks/guard-mr.mjs --stamp`, which records HEAD in
4
+ // node_modules/.cache/verify-head when the tree is clean; a `--stamp` outside the verify script is
5
+ // refused. Opening an MR/PR (CLI, push option, REST or GraphQL) is denied unless that record matches
6
+ // the current HEAD of a clean tree. Git errors fail open, like every other guard.
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { deny, extractCommand, readPayload } from './_shared.mjs';
12
+
13
+ const OPENS_REVIEW =
14
+ /\b(?:glab\s+mr\s+create|gh\s+pr\s+create|createPullRequest|mergeRequestCreate)\b|merge_request\.create/;
15
+ const API_CALL = /\b(?:gh|glab)\s+api\b/;
16
+ const REVIEW_COLLECTION = /\/(?:pulls|merge_requests)(?=["'\s?]|$)/;
17
+ // `gh api` and `glab api` default to POST once a field or body is passed.
18
+ const WRITES = /\s(?:(?:-X\s*|--method[=\s]+)POST\b|(?:-f|-F|--field|--raw-field|--input)\b)/i;
19
+ const READS = /\s(?:-X\s*|--method[=\s]+)GET\b/i;
20
+
21
+ const opensReview = (command) =>
22
+ OPENS_REVIEW.test(command) ||
23
+ (API_CALL.test(command) &&
24
+ REVIEW_COLLECTION.test(command) &&
25
+ WRITES.test(command) &&
26
+ !READS.test(command));
27
+
28
+ const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
29
+ const stampFile = (root) => join(root, 'node_modules', '.cache', 'verify-head');
30
+
31
+ const checkout = (cwd) => {
32
+ try {
33
+ const root = git(cwd, 'rev-parse', '--show-toplevel');
34
+ return {
35
+ root,
36
+ head: git(root, 'rev-parse', 'HEAD'),
37
+ isClean: git(root, 'status', '--porcelain') === '',
38
+ };
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ };
43
+
44
+ if (process.argv.includes('--stamp')) {
45
+ if (process.env.npm_lifecycle_event !== 'verify') {
46
+ process.stderr.write('--stamp is recorded by `pnpm verify` only: run `pnpm verify`.\n');
47
+ process.exit(1);
48
+ }
49
+ const state = checkout(process.cwd());
50
+ if (state?.isClean) {
51
+ mkdirSync(dirname(stampFile(state.root)), { recursive: true });
52
+ writeFileSync(stampFile(state.root), state.head);
53
+ } else {
54
+ process.stderr.write(
55
+ 'verify passed on uncommitted changes: commit, then rerun `pnpm verify` before opening an MR/PR.\n',
56
+ );
57
+ }
58
+ process.exit(0);
59
+ }
60
+
61
+ const payload = readPayload();
62
+ if (!opensReview(extractCommand(payload))) {
63
+ process.exit(0);
64
+ }
65
+ const state = checkout(payload.cwd ?? process.cwd());
66
+ if (!state) {
67
+ process.exit(0);
68
+ }
69
+ const readStamp = () => {
70
+ try {
71
+ return readFileSync(stampFile(state.root), 'utf8').trim();
72
+ } catch {
73
+ return '';
74
+ }
75
+ };
76
+ if (!state.isClean || readStamp() !== state.head) {
77
+ deny(
78
+ 'Blocked: run `pnpm verify` on a clean, committed HEAD before opening a merge/pull request. It records the verified commit; this check compares it with HEAD.',
79
+ );
80
+ }
81
+ process.exit(0);
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // PreToolUse(Task) guard: keep the orchestrator from spawning a GENERIC subagent
3
3
  // (general-purpose / claude) for work a roster agent is purpose-built for. The
4
- // Agent roster in AGENTS.md is prose the model sometimes skips out of habit; this
5
- // is the deterministic backstop.
4
+ // Agent roster in the workflow rule is prose the model sometimes skips out of
5
+ // habit; this is the deterministic backstop.
6
6
  //
7
7
  // Conservative by design: it acts ONLY when (a) the chosen subagent is generic AND
8
8
  // (b) the task text contains a HIGH-SIGNAL phrase that maps unambiguously to one
@@ -16,14 +16,16 @@ const ti = payload.tool_input ?? {};
16
16
  const sub = String(ti.subagent_type ?? '').toLowerCase();
17
17
 
18
18
  const GENERIC = new Set(['general-purpose', 'claude', '']);
19
- if (!GENERIC.has(sub)) process.exit(0);
19
+ if (!GENERIC.has(sub)) {
20
+ process.exit(0);
21
+ }
20
22
 
21
23
  const text = `${ti.description ?? ''}\n${ti.prompt ?? ''}`.toLowerCase();
22
24
 
23
25
  // High-signal phrase -> the roster agent that owns it. Keep each pattern SPECIFIC;
24
26
  // err toward missing a case over false-blocking a generic task.
25
27
  const ROUTES = [
26
- { agent: 'qa', re: /\b(playwright|e2e test|end-to-end test|e2e coverage)\b/ },
28
+ { agent: 'tester', re: /\b(playwright|e2e test|end-to-end test|e2e coverage)\b/ },
27
29
  {
28
30
  agent: 'debugger',
29
31
  re: /\b(build (failure|error)|turbopack|tsc error|module resolution|runtime error|stack trace)\b/,
@@ -40,14 +42,28 @@ const ROUTES = [
40
42
  agent: 'deployer',
41
43
  re: /\b(dockerfile|containerize|deploy pipeline|deploy to (ecs|kubernetes|fly|railway|render)|ci\/cd deploy|helm chart)\b/,
42
44
  },
45
+ {
46
+ agent: 'compliance-reviewer',
47
+ re: /\b(compliance|responsible gambling|ledger|audit trail) review\b/,
48
+ },
49
+ {
50
+ agent: 'quality-reviewer',
51
+ re: /\b(quality review|code quality|over-engineer(ed|ing)?|simplification review|duplication review)\b|\breview (the )?(mr|pr|diff|changed files)\b/,
52
+ },
53
+ {
54
+ agent: 'security-reviewer',
55
+ re: /\bsecurity (review|audit)\b|\baudit\b[^.]*\bvulnerabilit|\b(authz|owasp)\b/,
56
+ },
43
57
  ];
44
58
 
45
59
  const hit = ROUTES.find((r) => r.re.test(text));
46
- if (!hit) process.exit(0);
60
+ if (!hit) {
61
+ process.exit(0);
62
+ }
47
63
 
48
64
  process.stderr.write(
49
65
  `Use the \`${hit.agent}\` subagent for this task, not \`${sub || 'general-purpose'}\`. ` +
50
- `It is pre-scoped for this work (tools + model + brief) - see the Agent roster in AGENTS.md. ` +
66
+ `It is pre-scoped for this work (tools + model + brief) - see the Agents list in the workflow rule. ` +
51
67
  `Re-issue the Task with subagent_type: "${hit.agent}". ` +
52
68
  `If it genuinely does not fit ${hit.agent}, rephrase the description to say why.\n`,
53
69
  );
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook (Claude / Copilot CLI / Codex CLI / Gemini CLI).
3
+ // Lint-fixes the edited files, then typechecks the apps that own them and reports
4
+ // (exit 2) only if the error is in a file just edited. Output is capped so a
5
+ // failure can't balloon the model context. Fail-open on anything unexpected.
6
+
7
+ import { execFileSync } from 'node:child_process';
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { basename, join, isAbsolute } from 'node:path';
10
+ import { extractFilePaths, readPayload } from './_shared.mjs';
11
+
12
+ const CAP_LINES = 40;
13
+ const CAP_CHARS = 2000;
14
+
15
+ const filePaths = extractFilePaths(readPayload()).filter(
16
+ (filePath) =>
17
+ /\.(ts|tsx)$/.test(filePath) &&
18
+ !filePath.endsWith('.d.ts') &&
19
+ !filePath.includes('/templates/') &&
20
+ !filePath.includes('/generated/') &&
21
+ existsSync(filePath),
22
+ );
23
+ if (filePaths.length === 0) {
24
+ process.exit(0);
25
+ }
26
+ const edited = filePaths.join(', ');
27
+
28
+ /**
29
+ * node_modules/.bin/<name> when it exists, else the bare name off PATH. `pnpm exec`
30
+ * costs ~2.2s of package-manager startup per call against ~0.12s for the binary, and
31
+ * this hook runs on every edit.
32
+ */
33
+ function bin(name) {
34
+ const local = join(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), 'node_modules/.bin', name);
35
+ return existsSync(local) ? local : name;
36
+ }
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
+ // Lint-fix (best effort - never block on the linter).
44
+ try {
45
+ execFileSync(bin('oxlint'), ['--fix', ...filePaths], { stdio: 'pipe' });
46
+ } catch {
47
+ /* oxlint unavailable or errored - fall through to the re-lint / typecheck */
48
+ }
49
+
50
+ // Re-lint WITHOUT --fix to surface UNFIXABLE errors the agent must resolve by
51
+ // hand - import/no-cycle and the no-restricted-imports boundary rules (deep @openora
52
+ // or {{scope}}/ui imports, app-to-app imports). oxlint exits non-zero on errors
53
+ // only (warnings don't fail), so this blocks on a real boundary/cycle violation
54
+ // in an edited file and feeds the message back. Fail-open on anything else.
55
+ try {
56
+ execFileSync(bin('oxlint'), filePaths, { stdio: 'pipe' });
57
+ } catch (e) {
58
+ const out = (e.stdout?.toString() ?? '') + (e.stderr?.toString() ?? '');
59
+ if (/\berror\b/.test(out)) {
60
+ process.stderr.write(`Boundary/lint error in ${edited} - fix before continuing:\n${cap(out)}`);
61
+ process.exit(2);
62
+ }
63
+ }
64
+
65
+ // Resolve the owning workspace app (apps/<x> or packages/<x>).
66
+ function packageNameFor(fp) {
67
+ const m = fp.match(/(.*?\/(?:apps|packages)\/[^/]+)\//);
68
+ if (!m) {
69
+ return null;
70
+ }
71
+ try {
72
+ const pkg = JSON.parse(readFileSync(join(m[1], 'package.json'), 'utf8'));
73
+ if (!pkg.name || !pkg.scripts?.['check:types']) {
74
+ return null;
75
+ }
76
+ return pkg.name;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ const owners = [
83
+ ...new Set(
84
+ filePaths
85
+ .map((filePath) => packageNameFor(isAbsolute(filePath) ? filePath : join(process.cwd(), filePath)))
86
+ .filter(Boolean),
87
+ ),
88
+ ];
89
+ if (owners.length === 0) {
90
+ process.exit(0);
91
+ }
92
+
93
+ try {
94
+ execFileSync(
95
+ bin('turbo'),
96
+ [
97
+ 'run',
98
+ 'check:types',
99
+ ...owners.flatMap((owner) => ['--filter', owner]),
100
+ '--output-logs=errors-only',
101
+ '--ui=stream',
102
+ ],
103
+ { stdio: 'pipe' },
104
+ );
105
+ process.exit(0);
106
+ } catch (e) {
107
+ const output = (e.stdout?.toString() ?? '') + (e.stderr?.toString() ?? '');
108
+ const failing = filePaths.map((filePath) => basename(filePath)).filter((name) => output.includes(name));
109
+ if (failing.length > 0) {
110
+ process.stderr.write(
111
+ `Typecheck failed for ${owners.join(', ')} after editing ${failing.join(', ')}:\n${cap(output)}`,
112
+ );
113
+ process.exit(2);
114
+ }
115
+ process.exit(0);
116
+ }
@@ -2,10 +2,33 @@
2
2
  "version": 1,
3
3
  "hooks": {
4
4
  "preToolUse": [
5
- { "type": "command", "command": "node .rulesync/hooks/guard-core.mjs" },
6
- { "type": "command", "command": "node .rulesync/hooks/guard-generated.mjs" },
7
- { "type": "command", "matcher": "Task", "command": "node .rulesync/hooks/guard-subagent.mjs" }
5
+ {
6
+ "type": "command",
7
+ "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
8
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.rulesync/hooks/guard-core.mjs\""
9
+ },
10
+ {
11
+ "type": "command",
12
+ "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
13
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.rulesync/hooks/guard-generated.mjs\""
14
+ },
15
+ {
16
+ "type": "command",
17
+ "matcher": "Bash",
18
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.rulesync/hooks/guard-mr.mjs\""
19
+ },
20
+ {
21
+ "type": "command",
22
+ "matcher": "Task|Agent",
23
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.rulesync/hooks/guard-subagent.mjs\""
24
+ }
8
25
  ],
9
- "postToolUse": [{ "type": "command", "command": "node .rulesync/hooks/post-edit.mjs" }]
26
+ "postToolUse": [
27
+ {
28
+ "type": "command",
29
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
30
+ "command": "node \"${CLAUDE_PROJECT_DIR:-.}/.rulesync/hooks/post-edit.mjs\""
31
+ }
32
+ ]
10
33
  }
11
34
  }
@@ -0,0 +1,114 @@
1
+ ---
2
+ root: false
3
+ targets:
4
+ - '*'
5
+ globs:
6
+ - 'apps/**'
7
+ - 'packages/**'
8
+ - 'tools/**'
9
+ - '*.config.*'
10
+ - 'package.json'
11
+ description: Engineering code conventions (TS, DB, frontend, testing, git) - the always-on core, with a routing table to full detail in docs/standards/.
12
+ ---
13
+
14
+ # Engineering conventions
15
+
16
+ The always-on core of the code standard: what you must obey while typing. Detail, examples, and
17
+ rationale live in `docs/standards/` - read the one file that matches the change instead of
18
+ carrying all of it. The enforced import graph lives in `oss-boundaries`; Playwright rules in
19
+ `e2e-conventions`; the always-on UI and SQL cores in `frontend-conventions` and `db-conventions`.
20
+
21
+ | Change you are making | Read first |
22
+ | ------------------------------------- | ------------------------------------ |
23
+ | schema, type, enum-like value set | `docs/standards/types.md` |
24
+ | function, service method, constructor | `docs/standards/functions.md` |
25
+ | new package, overlay, module layout | `docs/standards/module-structure.md` |
26
+ | a comment or JSDoc | `docs/standards/comments.md` |
27
+ | error class, catch, money path | `docs/standards/errors.md` |
28
+ | a test | `docs/standards/testing.md` |
29
+ | commit, PR | `docs/standards/git-delivery.md` |
30
+ | a failing gate, a new lint rule | `docs/standards/enforcement.md` |
31
+ | React/UI component, page, styling | `docs/standards/frontend.md` |
32
+ | SQL, Drizzle, migration, seed | `docs/standards/database.md` |
33
+
34
+ `docs/standards/frontend.md` doesn't apply to a headless/api-only repo - delete it (and its row
35
+ above) if this repo has no UI apps.
36
+
37
+ ## Philosophy
38
+
39
+ - Functional and declarative: pure functions, immutable data, composition. No stateful classes, no
40
+ imperative accumulation loops.
41
+ - Explicit over magic: no decorators, no auto-discovery, no reflection. Every wiring point is a
42
+ greppable typed call (`ctx.provide(TOKEN, factory)`).
43
+ - Self-documenting: clear names beat comments (`percentChange`, not `d` + a comment).
44
+ - YAGNI then DRY: don't build for imagined futures; abstract on the third occurrence, not the
45
+ first.
46
+ - Boring and consistent: match the surrounding code's idiom, naming, and density.
47
+
48
+ ## Never (lint-enforced unless noted)
49
+
50
+ - `any` outside tests, `!` non-null assertions, `as` casts to silence the compiler (`as const` is
51
+ fine). Two sanctioned casts: test doubles through one `mock<T>()` helper (never inline in a
52
+ test), and a third-party inference boundary a library gives no honest way to satisfy (one cast,
53
+ one-line `// Library boundary:` note).
54
+ - `interface`, TS `enum`, decorators, inheritance for reuse, default exports (exceptions:
55
+ `*.config.*`, `plugin.ts`, Next.js App Router files).
56
+ - Hand-written duplicates of an inferrable type, re-inferring an imported schema, re-typing
57
+ derived schema fields, ad-hoc/duplicated Zod schemas outside a module's `contract/`.
58
+ - Re-exporting types "to be nice" - import from where defined.
59
+ - Inline `fetch`/`axios` in module code - third-party access is a port + adapter bound at the
60
+ composition root.
61
+ - Deep (`../../`+) relative imports that leave your module/zone, imports of another module's
62
+ internals, import cycles, deep `dist/`/`src/` paths into another package.
63
+ - SQL anti-patterns (bare `timestamp()`, CamelCase identifiers, hand-edited migrations) - detail
64
+ in `docs/standards/database.md`.
65
+ - Silent catches - log with context and rethrow. `ORPCError.message` rendered to a player - UI copy
66
+ keys off `.code` + typed `.data` through `t()`.
67
+ - Comments, unless one states a fact the code cannot contain (an external system's behaviour, a
68
+ spec constraint); a reason goes in the commit or PR, never inline. Never in tests. Detail:
69
+ `docs/standards/comments.md`.
70
+ - An in-process test that mocks the database, a repository, or a sibling service - found one in
71
+ the diff, delete it and replace it with the API E2E; never add to it.
72
+
73
+ ## Always
74
+
75
+ - **Naming:** files `kebab-case.ts`, one concept per file, filename names the concept
76
+ (`wallet.service.ts`, never `helpers.ts`); types `PascalCase`; values/functions `camelCase`;
77
+ true global constants `SCREAMING_SNAKE_CASE`; Zod schemas `<Name>Schema` with inferred type
78
+ `<Name>`; booleans read as predicates (`isActive`, `canEdit`); money is a decimal string plus a
79
+ `currency` field alongside it, never `amountCents`; IO functions are verbs (`fetchInvoice`); names
80
+ carry units and intent (`delayMs`, `maxRetries`).
81
+ - **One source of truth per shape** - infer, never hand-write: `z.infer<typeof XSchema>`,
82
+ `typeof x.$inferSelect`. Full detail: `docs/standards/types.md`.
83
+ - **Literal config arrays/objects (option lists, key sets) use `as const`**, not an explicit union
84
+ type annotation - let TypeScript infer the literal types.
85
+ - **Schema-first at every boundary** (HTTP, config, env, events); validate once at the edge, trust
86
+ the type after.
87
+ - **Entity ids typed through their owning type** (`playerId: Player['id']`), never a bare
88
+ `string`.
89
+ - **Guard clauses first, main path last; more than 3 params -> one named object** (a leading
90
+ `tx` handle may stay positional). Always brace control statements, even one-liners (lint:
91
+ `curly`). Detail: `docs/standards/functions.md`.
92
+ - **Construct objects by spread + override**, never a hand-copied field list.
93
+ - **Side effects at the edges**; money paths are transactional AND idempotent (a DB guard inside
94
+ the transaction, not just an idempotency key). Detail: `docs/standards/errors.md`.
95
+ - **Typed, named error classes** from the shared factories, mapped to transport in the router's
96
+ `mapErrors`.
97
+ - **Pin exact dependency versions** (no `^`/`~`); add a dependency deliberately - std lib or a few
98
+ lines often beat a tree.
99
+ - **A feature or fix PR ships its own tests** - no separate test PR. It ships the change, the
100
+ manual verification pass that proves it (drive the running stack by hand, attach a short video
101
+ per acceptance criterion, or the request/response trace for an API-only change, to the PR
102
+ description), and the tests that pass exercised, listed in the description's "Tests" list.
103
+ Which tier each one belongs to, and how to run the manual pass: `docs/standards/testing.md`.
104
+ - **Green before review:** `/check` (typecheck + lint + unit tests) while iterating, `pnpm verify`
105
+ on the committed tree before the PR (the `guard-mr` hook refuses to open one until it passed on
106
+ HEAD). Conventional commits, lowercase subject (PR
107
+ title too - a squash merge turns it into the commit), one PR per concern. Never push without
108
+ explicit confirmation. The description carries what / why / acceptance criteria / bare ticket
109
+ key, the manual-verification evidence (a short video per acceptance criterion, before and after on
110
+ a fix) and the "Tests" list - but no CI checklist the pipeline already shows, no URLs,
111
+ hostnames, secrets, or PII. Detail: `docs/standards/git-delivery.md`.
112
+ - **Fix the import, never work around a lint or boundary violation.** Agent rules are generated
113
+ from `.rulesync/` via `pnpm gen:agents` - never hand-edit a generated file. Detail:
114
+ `docs/standards/enforcement.md`.