@openora/create 0.4.1-canary.33 → 0.4.1-canary.331
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/generated/versions.d.ts +11 -0
- package/dist/generated/versions.d.ts.map +1 -0
- package/dist/generated/versions.js +11 -0
- package/dist/generated/versions.js.map +1 -0
- package/dist/index.js +48 -93
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +15 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +125 -0
- package/dist/render.js.map +1 -0
- package/package.json +8 -6
- package/template/README.md.tpl +12 -7
- package/template/__dot__claude/settings.json.tpl +22 -12
- package/template/__dot__env.example +10 -0
- package/template/__dot__gitignore +6 -1
- package/template/__dot__rulesync/commands/adr.md +32 -0
- package/template/__dot__rulesync/commands/check.md +11 -10
- package/template/__dot__rulesync/commands/doctor.md.tpl +17 -0
- package/template/__dot__rulesync/commands/scaffold-module.md +3 -2
- package/template/__dot__rulesync/commands/scaffold-plugin.md +19 -3
- package/template/__dot__rulesync/commands/scaffold-route.md +1 -0
- package/template/__dot__rulesync/commands/start.md +2 -2
- package/template/__dot__rulesync/hooks/_shared.mjs +26 -13
- package/template/__dot__rulesync/hooks/guard-core.mjs.tpl +76 -28
- package/template/__dot__rulesync/hooks/guard-generated.mjs +16 -14
- package/template/__dot__rulesync/hooks/guard-mr.mjs +81 -0
- package/template/__dot__rulesync/hooks/guard-subagent.mjs +22 -6
- package/template/__dot__rulesync/hooks/post-edit.mjs.tpl +116 -0
- package/template/__dot__rulesync/hooks.json +27 -4
- package/template/__dot__rulesync/rules/conventions.md +114 -0
- package/template/__dot__rulesync/rules/db-conventions.md +6 -72
- package/template/__dot__rulesync/rules/e2e-conventions.md +52 -0
- package/template/__dot__rulesync/rules/frontend-conventions.md.tpl +35 -0
- package/template/__dot__rulesync/rules/oss-boundaries.md.tpl +78 -0
- package/template/__dot__rulesync/rules/overview.md +18 -73
- package/template/__dot__rulesync/skills/create-plugin/{SKILL.md → SKILL.md.tpl} +13 -24
- package/template/__dot__rulesync/skills/create-pr/SKILL.md.tpl +38 -0
- package/template/__dot__rulesync/skills/create-task/{SKILL.md → SKILL.md.tpl} +14 -22
- package/template/__dot__rulesync/skills/create-ui-module/SKILL.md.tpl +60 -0
- package/template/__dot__rulesync/skills/enhance-prompt/{SKILL.md → SKILL.md.tpl} +4 -4
- package/template/__dot__rulesync/skills/handoff/SKILL.md +95 -0
- package/template/__dot__rulesync/skills/review/SKILL.md.tpl +277 -0
- package/template/__dot__rulesync/skills/ship/SKILL.md.tpl +157 -0
- package/template/__dot__rulesync/skills/ship/handoff.md.tpl +48 -0
- package/template/__dot__rulesync/skills/ship/watch-pr.mjs +172 -0
- package/template/__dot__rulesync/skills/ui-verify/SKILL.md.tpl +120 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.check.sh +58 -0
- package/template/__dot__rulesync/skills/ui-verify/layout-check.js +163 -0
- package/template/__dot__rulesync/subagents/builder.md.tpl +115 -0
- package/template/__dot__rulesync/subagents/compliance-reviewer.md +78 -0
- package/template/__dot__rulesync/subagents/debugger.md.tpl +86 -0
- package/template/__dot__rulesync/subagents/deployer.md.tpl +54 -0
- package/template/__dot__rulesync/subagents/expert.md +28 -34
- package/template/__dot__rulesync/subagents/planner.md.tpl +39 -0
- package/template/__dot__rulesync/subagents/quality-reviewer.md.tpl +112 -0
- package/template/__dot__rulesync/subagents/security-reviewer.md.tpl +85 -0
- package/template/__dot__rulesync/subagents/tester.md.tpl +102 -0
- package/template/__dot__rulesync/sync.json.tpl +26 -0
- package/template/apps/api/package.json.tpl +3 -4
- package/template/apps/api/src/extensions.config.ts.tpl +12 -0
- package/template/apps/api/src/main.ts.tpl +1 -4
- package/template/apps/api/src/seed.ts.tpl +8 -1
- package/template/docker-compose.yml +13 -0
- package/template/docs/agents/cross-repo.md.tpl +37 -0
- package/template/docs/agents/forge.md.tpl +54 -0
- package/template/docs/agents/issue-tracker.md.tpl +42 -0
- package/template/docs/standards/comments.md +16 -0
- package/template/docs/standards/database.md +77 -0
- package/template/docs/standards/enforcement.md +12 -0
- package/template/docs/standards/errors.md +26 -0
- package/template/docs/standards/frontend.md +60 -0
- package/template/docs/standards/functions.md +38 -0
- package/template/docs/standards/git-delivery.md +20 -0
- package/template/docs/standards/module-structure.md +37 -0
- package/template/docs/standards/testing.md +88 -0
- package/template/docs/standards/types.md +31 -0
- package/template/package.json.tpl +19 -13
- package/template/rulesync.jsonc +4 -4
- package/template/tools/oss-worktree.mjs.tpl +105 -0
- package/template/tools/review-precheck.mjs +531 -0
- package/template/tools/sync-agents.mjs +257 -0
- package/template/turbo/generators/config.ts +1 -1
- package/template/turbo.json +14 -2
- package/dist/generated/core-version.d.ts +0 -2
- package/dist/generated/core-version.d.ts.map +0 -1
- package/dist/generated/core-version.js +0 -2
- package/dist/generated/core-version.js.map +0 -1
- package/template/__dot__rulesync/hooks/post-edit.mjs +0 -57
- package/template/__dot__rulesync/skills/add-feature/SKILL.md +0 -113
- package/template/__dot__rulesync/skills/add-feature/handoff.md +0 -58
- package/template/__dot__rulesync/skills/code-review/SKILL.md +0 -111
- package/template/__dot__rulesync/skills/create-pr/SKILL.md +0 -55
- package/template/__dot__rulesync/subagents/builder.md +0 -93
- package/template/__dot__rulesync/subagents/debugger.md +0 -83
- package/template/__dot__rulesync/subagents/deployer.md +0 -66
- package/template/__dot__rulesync/subagents/qa.md +0 -88
- 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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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 {
|
|
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
|
-
//
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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[^|;&]
|
|
21
|
-
[new RegExp(String.raw`(?:>>?|>\|)\s*['"]?[^'"\s|;&]
|
|
22
|
-
[new RegExp(String.raw`\btee\b\s+(?:-a\s+)?['"]?[^'"\s|;&]
|
|
23
|
-
[new RegExp(String.raw
|
|
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(([
|
|
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
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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 {
|
|
7
|
+
import { extractFilePaths, readPayload, deny } from './_shared.mjs';
|
|
8
8
|
import { relative, isAbsolute } from 'node:path';
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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))
|
|
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: '
|
|
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)
|
|
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
|
|
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
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
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": [
|
|
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`.
|