@openora/create 0.4.1-canary.35 → 0.4.1-canary.351

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 +5 -0
  17. package/template/__dot__rulesync/commands/check.md +11 -10
  18. package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
  19. package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
  20. package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
  21. package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
  22. package/template/__dot__rulesync/commands/start.md +2 -2
  23. package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
  24. package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
  25. package/template/__dot__rulesync/hooks/guard-generated.mjs +15 -13
  26. package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
  27. package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
  28. package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
  29. package/template/__dot__rulesync/hooks.json +27 -4
  30. package/template/__dot__rulesync/rules/conventions.md +99 -119
  31. package/template/__dot__rulesync/rules/db-conventions.md +6 -72
  32. package/template/__dot__rulesync/rules/e2e-conventions.md +33 -7
  33. package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
  34. package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
  35. package/template/__dot__rulesync/rules/overview.md +18 -82
  36. package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +12 -23
  37. package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
  38. package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
  39. package/template/__dot__rulesync/skills/create-ui-module/{SKILL.md → SKILL.md.tpl} +12 -10
  40. package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
  41. package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
  42. package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
  43. package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
  44. package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
  45. package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
  46. package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
  47. package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
  48. package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
  49. package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
  50. package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
  51. package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
  52. package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
  53. package/template/__dot__rulesync/subagents/expert.md +28 -34
  54. package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
  55. package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
  56. package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
  57. package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
  58. package/template/__dot__rulesync/sync.json.tpl +26 -0
  59. package/template/apps/api/package.json.tpl +2 -2
  60. package/template/apps/api/src/extensions.config.ts.tpl +12 -0
  61. package/template/apps/api/src/main.ts.tpl +1 -4
  62. package/template/apps/api/src/seed.ts.tpl +8 -1
  63. package/template/docker-compose.yml +13 -0
  64. package/template/docs/agents/cross-repo.md.tpl +37 -0
  65. package/template/docs/agents/forge.md.tpl +54 -0
  66. package/template/docs/agents/issue-tracker.md.tpl +42 -0
  67. package/template/docs/standards/comments.md +16 -0
  68. package/template/docs/standards/database.md +77 -0
  69. package/template/docs/standards/enforcement.md +12 -0
  70. package/template/docs/standards/errors.md +30 -0
  71. package/template/docs/standards/frontend.md +60 -0
  72. package/template/docs/standards/functions.md +38 -0
  73. package/template/docs/standards/git-delivery.md +20 -0
  74. package/template/docs/standards/module-structure.md +37 -0
  75. package/template/docs/standards/testing.md +88 -0
  76. package/template/docs/standards/types.md +31 -0
  77. package/template/package.json.tpl +15 -9
  78. package/template/rulesync.jsonc +3 -3
  79. package/template/tools/oss-worktree.mjs.tpl +105 -0
  80. package/template/tools/review-precheck.mjs +531 -0
  81. package/template/tools/sync-agents.mjs +257 -0
  82. package/dist/generated/core-version.d.ts +0 -2
  83. package/dist/generated/core-version.d.ts.map +0 -1
  84. package/dist/generated/core-version.js +0 -2
  85. package/dist/generated/core-version.js.map +0 -1
  86. package/template/__dot__rulesync/commands/doctor.md +0 -16
  87. package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
  88. package/template/__dot__rulesync/rules/oss-boundaries.md +0 -31
  89. package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
  90. package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
  91. package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
  92. package/template/__dot__rulesync/skills/review/SKILL.md +0 -110
  93. package/template/__dot__rulesync/subagents/builder.md +0 -93
  94. package/template/__dot__rulesync/subagents/debugger.md +0 -82
  95. package/template/__dot__rulesync/subagents/deployer.md +0 -66
  96. package/template/__dot__rulesync/subagents/qa.md +0 -88
  97. package/template/__dot__rulesync/subagents/quality-reviewer.md +0 -67
  98. package/template/__dot__rulesync/subagents/security-reviewer.md +0 -54
@@ -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).
2
+ // PreToolUse guard (Claude / Copilot CLI / Codex CLI / Gemini CLI).
3
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 gen:agents\`). ` +
30
- 'Edit the source under .rulesync/ instead (rules/, subagents/, commands/, mcp.json), then run `pnpm gen: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
  }