agentic-team 0.0.0-stage → 6.1.0

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -2
  3. package/bin/build-template.js +208 -0
  4. package/bin/index.js +1139 -0
  5. package/package.json +41 -4
  6. package/template/.claude-plugin/plugin.json +10 -0
  7. package/template/.orchestra/README.md +401 -0
  8. package/template/.orchestra/blueprints/README.md +87 -0
  9. package/template/.orchestra/blueprints/api-only.md +46 -0
  10. package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
  11. package/template/.orchestra/blueprints/saas-starter.md +69 -0
  12. package/template/.orchestra/config.yml +277 -0
  13. package/template/.orchestra/roles/orchestrator.md +105 -0
  14. package/template/.orchestra/roles/product-manager.md +438 -0
  15. package/template/.orchestra/scripts/compact-diff.awk +45 -0
  16. package/template/.orchestra/stacks/README.md +81 -0
  17. package/template/.orchestra/stacks/_lib/common.sh +3095 -0
  18. package/template/.orchestra/stacks/_repo/setup.sh +1109 -0
  19. package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
  20. package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
  21. package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
  22. package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
  23. package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
  24. package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
  25. package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
  26. package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
  27. package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
  28. package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
  29. package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
  30. package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
  31. package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
  32. package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
  33. package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
  34. package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
  35. package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
  36. package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
  37. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
  38. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
  39. package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
  40. package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
  41. package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
  42. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
  43. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
  44. package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
  45. package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
  46. package/template/.orchestra/trackers/github.sh +467 -0
  47. package/template/CLAUDE.md +54 -0
  48. package/template/agents/architect.md +83 -0
  49. package/template/agents/engineer-high.md +17 -0
  50. package/template/agents/engineer-low.md +17 -0
  51. package/template/agents/engineer-max.md +17 -0
  52. package/template/agents/engineer-medium.md +17 -0
  53. package/template/agents/engineer-xhigh.md +17 -0
  54. package/template/agents/lead.md +870 -0
  55. package/template/agents/milestone-reviewer.md +63 -0
  56. package/template/agents/orchestrator.md +105 -0
  57. package/template/agents/product-manager.md +438 -0
  58. package/template/agents/reviewer.md +301 -0
  59. package/template/agents/scaffolder.md +180 -0
  60. package/template/agents/tracker.md +202 -0
  61. package/template/commands/blueprint.md +23 -0
  62. package/template/commands/create-role.md +64 -0
  63. package/template/commands/export.md +45 -0
  64. package/template/commands/help.md +55 -0
  65. package/template/commands/hotfix.md +12 -0
  66. package/template/commands/orchestrator.md +12 -0
  67. package/template/commands/pm.md +18 -0
  68. package/template/commands/rewind.md +61 -0
  69. package/template/commands/setup.md +14 -0
  70. package/template/commands/start.md +29 -0
  71. package/template/commands/status.md +11 -0
  72. package/template/commands/sync.md +73 -0
  73. package/template/commands/verifier.md +53 -0
  74. package/template/hooks/audit-context.sh +153 -0
  75. package/template/hooks/enforce-role.sh +175 -0
  76. package/template/hooks/protect-system-files.sh +123 -0
  77. package/template/hooks/review-rounds.sh +172 -0
  78. package/template/rules/acceptance-check.orchestra.md +11 -0
  79. package/template/rules/analytics-kpi.orchestra.md +56 -0
  80. package/template/rules/code-standards.orchestra.md +34 -0
  81. package/template/rules/comments.orchestra.md +77 -0
  82. package/template/rules/commit-format.orchestra.md +20 -0
  83. package/template/rules/dependency-upgrade.orchestra.md +112 -0
  84. package/template/rules/design-parity.orchestra.md +170 -0
  85. package/template/rules/observability.orchestra.md +118 -0
  86. package/template/rules/performance.orchestra.md +186 -0
  87. package/template/rules/phase-limits.orchestra.md +3 -0
  88. package/template/rules/pull-request.orchestra.md +213 -0
  89. package/template/rules/role-boundaries.orchestra.md +117 -0
  90. package/template/rules/security.orchestra.md +106 -0
  91. package/template/rules/stuck-detection.orchestra.md +25 -0
  92. package/template/rules/testing-standards.orchestra.md +214 -0
  93. package/template/rules/tracker-authority.orchestra.md +190 -0
  94. package/template/rules/verification-gate.orchestra.md +56 -0
  95. package/template/skills/accessibility/SKILL.md +31 -0
  96. package/template/skills/android-testing/SKILL.md +112 -0
  97. package/template/skills/architecture/SKILL.md +175 -0
  98. package/template/skills/auth-setup/SKILL.md +105 -0
  99. package/template/skills/best-practices/SKILL.md +29 -0
  100. package/template/skills/code-optimizer/SKILL.md +27 -0
  101. package/template/skills/core-web-vitals/SKILL.md +32 -0
  102. package/template/skills/crud-api/SKILL.md +32 -0
  103. package/template/skills/debug/SKILL.md +28 -0
  104. package/template/skills/deployment/SKILL.md +104 -0
  105. package/template/skills/error-handling/SKILL.md +154 -0
  106. package/template/skills/frontend-design/SKILL.md +30 -0
  107. package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
  108. package/template/skills/ios-testing/SKILL.md +100 -0
  109. package/template/skills/mobile-testing/SKILL.md +102 -0
  110. package/template/skills/observability/SKILL.md +91 -0
  111. package/template/skills/project-structure/SKILL.md +114 -0
  112. package/template/skills/push-notifications/SKILL.md +146 -0
  113. package/template/skills/react-best-practices/SKILL.md +31 -0
  114. package/template/skills/realtime/SKILL.md +146 -0
  115. package/template/skills/review/SKILL.md +82 -0
  116. package/template/skills/testing/SKILL.md +44 -0
  117. package/template/skills/web-quality-audit/SKILL.md +47 -0
  118. package/template/skills/web-testing/SKILL.md +120 -0
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard: a session that declared a role stays inside it.
3
+ #
4
+ # `.claude/rules/role-boundaries.orchestra.md` says the PM writes only under
5
+ # `.orchestra/milestones/` and never opens a branch or a pull request. That
6
+ # was prose, and a PM opened a pull request anyway. A rule nothing checks is
7
+ # advice.
8
+ #
9
+ # The hook could not know the role — a slash command is a markdown prompt and
10
+ # exports nothing. So the role command has the agent run one Bash line,
11
+ # `true # orchestra-role=product-manager`, and this hook, which sees every
12
+ # Bash call together with the session id Claude Code hands it, records the
13
+ # pair under `.orchestra/.sessions/`. Every later call in that session is
14
+ # judged against the recorded role. Two terminals are two sessions, so the
15
+ # PM's marker never applies to lead.
16
+ #
17
+ # No marker means no role — discussion mode — and nothing is blocked.
18
+ #
19
+ # Reads the tool call on stdin. Exit 0 allows, exit 2 blocks.
20
+ set -euo pipefail
21
+
22
+ input="$(cat)"
23
+
24
+ verdict="$(printf '%s' "$input" | CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" node -e '
25
+ const fs = require("node:fs");
26
+ const path = require("node:path");
27
+ const { execFileSync } = require("node:child_process");
28
+
29
+ let s = "";
30
+ process.stdin.on("data", (d) => (s += d)).on("end", () => {
31
+ let call;
32
+ try {
33
+ call = JSON.parse(s);
34
+ } catch {
35
+ // Optional by design: an unparseable call is not ours to judge.
36
+ return;
37
+ }
38
+ const session = String(call.session_id || "").replace(/[^A-Za-z0-9._-]/g, "");
39
+ if (!session) return;
40
+ const tool = call.tool_name || "";
41
+ const input = call.tool_input || {};
42
+
43
+ let root;
44
+ try {
45
+ root = execFileSync("git", ["-C", process.env.CLAUDE_PROJECT_DIR, "rev-parse", "--show-toplevel"], {
46
+ encoding: "utf8",
47
+ stdio: ["ignore", "pipe", "ignore"],
48
+ }).trim();
49
+ } catch {
50
+ return;
51
+ }
52
+ if (!root) return;
53
+ // Both sides through realpath: on macOS a temp dir is /var/… while git
54
+ // answers /private/var/…, and path.relative between them says "outside".
55
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return p; } };
56
+ root = real(root);
57
+ const projectDir = real(process.env.CLAUDE_PROJECT_DIR);
58
+ // A path that does not exist yet — a file about to be written — is
59
+ // resolved through its nearest existing ancestor, so an absolute path
60
+ // spelled with the unresolved prefix still lands inside the repo.
61
+ const relTo = (p) => {
62
+ let abs = path.resolve(projectDir, p);
63
+ let head = abs;
64
+ let tail = "";
65
+ while (!fs.existsSync(head) && path.dirname(head) !== head) {
66
+ tail = path.join(path.basename(head), tail);
67
+ head = path.dirname(head);
68
+ }
69
+ abs = path.join(real(head), tail);
70
+ return path.relative(root, abs);
71
+ };
72
+
73
+ const dir = path.join(root, ".orchestra", ".sessions");
74
+ const marker = path.join(dir, session);
75
+
76
+ // The declaration. Recorded, then allowed through.
77
+ if (tool === "Bash") {
78
+ const m = /orchestra-role=(product-manager|orchestrator|lead)\b/.exec(input.command || "");
79
+ if (m) {
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ fs.writeFileSync(marker, m[1]);
82
+ return;
83
+ }
84
+ }
85
+
86
+ let role;
87
+ try {
88
+ role = fs.readFileSync(marker, "utf8").trim();
89
+ } catch {
90
+ return;
91
+ }
92
+ if (role !== "product-manager") return;
93
+
94
+ // ── The PM ───────────────────────────────────────────────────────────
95
+ // Writes milestones. Commits them to main and pushes. Nothing else.
96
+ const deny = (why) => {
97
+ process.stdout.write(`Outside the PM'"'"'s scope: ${why}\n`);
98
+ };
99
+
100
+ if (tool === "Write" || tool === "Edit") {
101
+ const file = input.file_path || input.path || "";
102
+ if (!file) return;
103
+ const rel = relTo(file);
104
+ if (rel.startsWith("..")) return deny(`${file} is outside the repository`);
105
+ if (!rel.startsWith(".orchestra/milestones/")) {
106
+ return deny(`the PM writes only under .orchestra/milestones/ — ${rel} is not there. Say what needs changing and which command can do it.`);
107
+ }
108
+ return;
109
+ }
110
+
111
+ if (tool === "Workflow") {
112
+ return deny("the PM never runs a workflow. A code review is lead'"'"'s, on the pull request; a plan review is the milestone-reviewer agent.");
113
+ }
114
+ if (tool === "Agent") {
115
+ const type = input.subagent_type || "";
116
+ if (type !== "milestone-reviewer" && type !== "tracker") {
117
+ return deny(`the PM launches only milestone-reviewer and tracker — not ${type || "a general-purpose agent"}. Reviewing code, building, and scouting are lead'"'"'s.`);
118
+ }
119
+ return;
120
+ }
121
+ if (/clickup_(create|update)_task$/.test(tool)) {
122
+ const status = String(input.status ?? "").trim();
123
+ if (!status) return;
124
+ // The tracker agent runs inside the PM'"'"'s session and lands a new task
125
+ // in backlog — the one status write the PM side is allowed. Read the
126
+ // board'"'"'s own name for it from config, compared case-insensitively.
127
+ let backlog = "backlog";
128
+ try {
129
+ const cfg = fs.readFileSync(path.join(root, ".orchestra", "config.yml"), "utf8");
130
+ const m = /^\s*backlog:\s*["'"'"']?([^"'"'"'#\n]+)/m.exec(cfg);
131
+ if (m) backlog = m[1].trim();
132
+ } catch {
133
+ // No config: the default name is the only name we know.
134
+ }
135
+ if (status.toLowerCase() === backlog.toLowerCase()) return;
136
+ return deny(`the PM never sets a task'"'"'s status — export lands it in backlog, lead advances it, the human moves it. Drop status: ${JSON.stringify(status)}.`);
137
+ }
138
+
139
+ if (tool !== "Bash") return;
140
+ const cmd = input.command || "";
141
+
142
+ if (/\bgh\s+pr\s+(create|merge|edit|close|ready|checkout)\b/.test(cmd)) {
143
+ return deny("the PM never opens, edits or merges a pull request. Milestone files go to main directly; code is lead'"'"'s.");
144
+ }
145
+ if (/\bgit\s+(checkout|switch)\s+(-b|-c|--create|-B)\b/.test(cmd)) {
146
+ return deny("the PM never creates a branch. Milestone files are committed to main.");
147
+ }
148
+ if (/\bgit\s+(merge|rebase|cherry-pick|reset\s+--hard)\b/.test(cmd)) {
149
+ return deny("the PM never rewrites history or merges.");
150
+ }
151
+ if (/\bgit\s+push\b.*(\s--force\b|\s-f\b|\s--force-with-lease\b)/.test(cmd)) {
152
+ return deny("the PM never force-pushes.");
153
+ }
154
+ if (/\bgit\s+commit\b[^&|;]*\s(?:-[a-zA-Z]*a[a-zA-Z]*|--all)\b/.test(cmd)) {
155
+ return deny("`git commit -a` stages everything. Add .orchestra/milestones/ explicitly, then commit.");
156
+ }
157
+ const add = /\bgit\s+add\s+(.+?)(?:\s*(?:&&|\|\||;|$))/.exec(cmd);
158
+ if (add) {
159
+ const args = add[1].split(/\s+/).filter((a) => a && !a.startsWith("-"));
160
+ const outside = args.filter((a) => {
161
+ const rel = relTo(a.replace(/^["'"'"']|["'"'"']$/g, ""));
162
+ return !rel.startsWith(".orchestra/milestones");
163
+ });
164
+ if (args.length === 0 || outside.length) {
165
+ return deny(`the PM stages only .orchestra/milestones/ — not ${outside.join(", ") || "-A / ."}.`);
166
+ }
167
+ }
168
+ });
169
+ ')"
170
+
171
+ if [ -n "$verdict" ]; then
172
+ printf '%s\n' "$verdict" >&2
173
+ exit 2
174
+ fi
175
+ exit 0
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard: never edit installed Orchestra system files in place.
3
+ #
4
+ # `.claude/rules/role-boundaries.orchestra.md` lists files no project role
5
+ # owns. The check is not "which role is running": a slash command is a
6
+ # markdown prompt and cannot export anything a hook could read, so any guard
7
+ # built on that premise would never fire. The reliable question is *which repo
8
+ # this is*, and then *did Orchestra put this file here*.
9
+ #
10
+ # The second half used to be a list of globs, which was wrong in both
11
+ # directions. It blocked `.orchestra/config.yml`, which the installer merges
12
+ # rather than overwrites and which `/orchestra:setup` is documented to write.
13
+ # It blocked every `.claude/skills/*/SKILL.md`, including the ones
14
+ # docs/skills.md tells the user to create, and including a stack's own
15
+ # `apps/<name>/.claude/` pack. And a relative file_path matched none of the
16
+ # patterns, so it sailed straight through.
17
+ #
18
+ # `.orchestra/.installed.json` records what was installed, so the question has
19
+ # an answer instead of an approximation.
20
+ #
21
+ # Reads the tool call on stdin. Exit 0 allows, exit 2 blocks.
22
+ set -euo pipefail
23
+
24
+ input="$(cat)"
25
+
26
+ blocked="$(printf '%s' "$input" | CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" node -e '
27
+ const fs = require("node:fs");
28
+ const path = require("node:path");
29
+ const { execFileSync } = require("node:child_process");
30
+
31
+ let s = "";
32
+ process.stdin.on("data", (d) => (s += d)).on("end", () => {
33
+ let file;
34
+ try {
35
+ const i = JSON.parse(s).tool_input || {};
36
+ file = i.file_path || i.path || "";
37
+ } catch {
38
+ // Optional by design: an unparseable call is not ours to judge.
39
+ return;
40
+ }
41
+ if (!file) return;
42
+
43
+ // A relative path used to match no pattern at all, which made the guard
44
+ // depend on how the caller happened to spell it.
45
+ const abs = path.resolve(process.env.CLAUDE_PROJECT_DIR, file);
46
+
47
+ let root;
48
+ try {
49
+ root = execFileSync("git", ["-C", path.dirname(abs), "rev-parse", "--show-toplevel"], {
50
+ encoding: "utf8",
51
+ stdio: ["ignore", "pipe", "ignore"],
52
+ }).trim();
53
+ } catch {
54
+ // Not in a repository: nothing here is an installed copy.
55
+ return;
56
+ }
57
+ if (!root) return;
58
+
59
+ // The orchestrator repo is where these files are source. That is a fact
60
+ // about the tree, not about who is asking.
61
+ if (fs.existsSync(path.join(root, "bin/build-template.js"))) return;
62
+
63
+ // git reports the real path and path.resolve does not, so on any system
64
+ // where the tree sits under a symlink — /var on macOS, a linked worktree
65
+ // — the two disagree and every comparison below silently misses.
66
+ let existing = path.dirname(abs);
67
+ while (!fs.existsSync(existing) && existing !== path.dirname(existing)) {
68
+ existing = path.dirname(existing);
69
+ }
70
+ const real = path.join(fs.realpathSync(existing), path.relative(existing, abs));
71
+ const rel = path.relative(fs.realpathSync(root), real).split(path.sep).join("/");
72
+ if (rel.startsWith("..")) return;
73
+
74
+ // config.yml is merged on upgrade, so an edit survives; setup writes its
75
+ // verification keys, which .orchestra/README.md documents.
76
+ if (rel === ".orchestra/config.yml") return;
77
+
78
+ const manifestPath = path.join(root, ".orchestra/.installed.json");
79
+ if (fs.existsSync(manifestPath)) {
80
+ let files = null;
81
+ try {
82
+ files = JSON.parse(fs.readFileSync(manifestPath, "utf8")).files;
83
+ } catch {
84
+ // Fall through to the patterns below rather than allowing everything.
85
+ }
86
+ if (Array.isArray(files)) {
87
+ if (files.includes(rel)) process.stdout.write(abs);
88
+ return;
89
+ }
90
+ }
91
+
92
+ // Installed before the manifest existed. Approximate, and deliberately
93
+ // narrower than the old list: skills and roles are left out because
94
+ // without a manifest a users own cannot be told from a shipped one, and
95
+ // blocking theirs is the worse mistake.
96
+ const legacy = [
97
+ /^\.orchestra\/stacks\//,
98
+ /^\.orchestra\/README\.md$/,
99
+ /^\.claude\/agents\//,
100
+ /^\.claude\/rules\/.*\.orchestra\.md$/,
101
+ /^\.claude\/commands\/orchestra\//,
102
+ ];
103
+ if (legacy.some((re) => re.test(rel))) process.stdout.write(abs);
104
+ });
105
+ ' 2>/dev/null || true)"
106
+
107
+ [ -n "$blocked" ] || exit 0
108
+
109
+ cat >&2 <<MSG
110
+ Blocked: ${blocked}
111
+
112
+ This is an installed Orchestra system file — .orchestra/.installed.json says
113
+ Orchestra put it there. Editing it here is lost work: the next upgrade
114
+ overwrites it, and the next project meets the same problem you were about to
115
+ fix.
116
+
117
+ Change it in the orchestrator repo and reinstall. If it is genuinely
118
+ project-specific, it belongs in the app's own CLAUDE.md, a project skill of
119
+ your own, or .orchestra/config.yml — none of which is overwritten.
120
+
121
+ See .claude/rules/role-boundaries.orchestra.md.
122
+ MSG
123
+ exit 2
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard: a phase gets at most `thresholds.stuck_retry_limit`
3
+ # review rounds, then a person decides.
4
+ #
5
+ # The cap was prose in lead.md, and a run went nine rounds on one phase
6
+ # because the user's "loop until no high or medium remains" read as
7
+ # overriding it. Each round found a new hole in the previous round's fix, so
8
+ # nothing inside the loop could end it. Counting is the one part a machine
9
+ # can do, so it is done here.
10
+ #
11
+ # A reviewer launch names its phase on a line
12
+ # `Phase: <milestone directory>/<phase file name>`, and that file must exist
13
+ # under .orchestra/milestones/ — a second spelling of the same phase would
14
+ # start a fresh count. Lead must name it; any other session may leave it
15
+ # out, uncounted, so a one-off review in discussion mode is never refused.
16
+ #
17
+ # The count lives in .orchestra/.sessions/review-rounds.json, keyed by phase
18
+ # rather than by session, so a restarted lead does not start from zero, and
19
+ # the installer keeps it across upgrades. Past the cap the launch is refused
20
+ # until the prompt quotes the user's approval on a line
21
+ # `Extra round approved by the user: …` — each launch past it carries its own
22
+ # line, so one approval buys one round. Nothing
23
+ # can check that the quote is real; it is honest by convention, like the
24
+ # escape-hatch comments the policy check reads.
25
+ #
26
+ # Reads the tool call on stdin. Exit 0 allows, exit 2 blocks.
27
+ set -euo pipefail
28
+
29
+ input="$(cat)"
30
+
31
+ verdict="$(printf '%s' "$input" | CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" node -e '
32
+ const fs = require("node:fs");
33
+ const path = require("node:path");
34
+ const { execFileSync } = require("node:child_process");
35
+
36
+ let s = "";
37
+ process.stdin.on("data", (d) => (s += d)).on("end", () => {
38
+ let call;
39
+ try {
40
+ call = JSON.parse(s);
41
+ } catch {
42
+ // Optional by design: an unparseable call is not ours to judge.
43
+ return;
44
+ }
45
+ if (call.tool_name !== "Agent") return;
46
+ const input = call.tool_input || {};
47
+ if (input.subagent_type !== "reviewer") return;
48
+ const prompt = String(input.prompt || "");
49
+
50
+ // The installer writes into the directory it ran in, which can be an app
51
+ // inside a larger repository, so the project directory comes first.
52
+ let root = process.env.CLAUDE_PROJECT_DIR;
53
+ if (!fs.existsSync(path.join(root, ".orchestra"))) {
54
+ try {
55
+ root = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], {
56
+ encoding: "utf8",
57
+ stdio: ["ignore", "pipe", "ignore"],
58
+ }).trim();
59
+ } catch {
60
+ return;
61
+ }
62
+ }
63
+ const milestones = path.join(root, ".orchestra", "milestones");
64
+ // Not an Orchestra project here: nothing to count against.
65
+ if (!root || !fs.existsSync(milestones)) return;
66
+ const dir = path.join(root, ".orchestra", ".sessions");
67
+
68
+ const phase = /^[ \t]*Phase:[ \t]*(\S[^\n]*?)[ \t]*$/m.exec(prompt);
69
+ if (!phase) {
70
+ const session = String(call.session_id || "").replace(/[^A-Za-z0-9._-]/g, "");
71
+ let role = "";
72
+ try {
73
+ role = session ? fs.readFileSync(path.join(dir, session), "utf8").trim() : "";
74
+ } catch {
75
+ // No marker: not a lead session, so an uncounted review is allowed.
76
+ }
77
+ if (role === "lead") {
78
+ process.stdout.write("A reviewer launch from lead names its phase on a line `Phase: <milestone directory>/<phase file name>`, so its review rounds can be counted.\n");
79
+ }
80
+ return;
81
+ }
82
+ const key = phase[1];
83
+ // Exactly two segments, matched by listing rather than by existsSync: a
84
+ // trailing slash, a third segment or a change of case on a
85
+ // case-insensitive disk would each name the same file under a new key.
86
+ const parts = key.split("/");
87
+ const listed = (d, name) => {
88
+ try {
89
+ return fs.readdirSync(d).includes(name);
90
+ } catch {
91
+ return false;
92
+ }
93
+ };
94
+ if (parts.length !== 2 || !listed(milestones, parts[0]) || !listed(path.join(milestones, parts[0], "phases"), `${parts[1]}.md`)) {
95
+ process.stdout.write(`Phase: ${key} names no file .orchestra/milestones/<milestone>/phases/<phase>.md. Spell it as the directory and the file name, e.g. Phase: M2-design-system/phase-3 — another spelling would start a fresh count.\n`);
96
+ return;
97
+ }
98
+
99
+ let cap = 3;
100
+ try {
101
+ const cfg = fs.readFileSync(path.join(root, ".orchestra", "config.yml"), "utf8");
102
+ const m = /^\s*stuck_retry_limit:\s*(\d+)/m.exec(cfg);
103
+ if (m) cap = Number(m[1]);
104
+ } catch {
105
+ // No config: the documented default is the only cap we know.
106
+ }
107
+
108
+ // Parallel phases launch reviewers at once. Without the lock two reads
109
+ // see the same count and one increment is lost; without the rename a
110
+ // reader can meet a half-written file and start every phase from zero.
111
+ fs.mkdirSync(dir, { recursive: true });
112
+ const lock = path.join(dir, "review-rounds.lock");
113
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
114
+ const deadline = Date.now() + 5000;
115
+ for (;;) {
116
+ try {
117
+ fs.mkdirSync(lock);
118
+ break;
119
+ } catch (e) {
120
+ if (e.code !== "EEXIST") throw e;
121
+ try {
122
+ // A launch holds the lock for milliseconds, so one older than this
123
+ // was left by a crash; shorter than the wait, so it clears in time.
124
+ if (Date.now() - fs.statSync(lock).mtimeMs > 3000) fs.rmdirSync(lock);
125
+ } catch {
126
+ // Another launch released or cleared it first; try again.
127
+ }
128
+ if (Date.now() > deadline) {
129
+ process.stdout.write("The review-round count is locked by another launch. Launch the reviewer again.\n");
130
+ return;
131
+ }
132
+ sleep(25);
133
+ }
134
+ }
135
+ try {
136
+ const file = path.join(dir, "review-rounds.json");
137
+ let counts = {};
138
+ try {
139
+ counts = JSON.parse(fs.readFileSync(file, "utf8"));
140
+ } catch {
141
+ // First reviewer launch in this project, or a file someone deleted:
142
+ // both mean no rounds recorded yet.
143
+ }
144
+ const done = counts[key] || 0;
145
+ const approved = /^[ \t]*Extra round approved by the user:[ \t]*\S/m.test(prompt);
146
+ if (done >= cap && !approved) {
147
+ process.stdout.write(
148
+ `Review round ${done + 1} for ${key} is past thresholds.stuck_retry_limit (${cap}). ` +
149
+ "Stop and give the user the open Blocking and Important findings. Under --auto, set the task blocked with a comment naming them. " +
150
+ "If the user approves another round, quote them on a line `Extra round approved by the user: …`.\n",
151
+ );
152
+ return;
153
+ }
154
+ counts[key] = done + 1;
155
+ const tmp = `${file}.${process.pid}`;
156
+ fs.writeFileSync(tmp, `${JSON.stringify(counts, null, 2)}\n`);
157
+ fs.renameSync(tmp, file);
158
+ } finally {
159
+ try {
160
+ fs.rmdirSync(lock);
161
+ } catch {
162
+ // Already cleared as stale by a waiter; there is nothing left to release.
163
+ }
164
+ }
165
+ });
166
+ ')"
167
+
168
+ if [ -n "$verdict" ]; then
169
+ printf '%s\n' "$verdict" >&2
170
+ exit 2
171
+ fi
172
+ exit 0
@@ -0,0 +1,11 @@
1
+ # Acceptance Check
2
+
3
+ After implementation, check whether you built **the right thing** — not just whether it works.
4
+
5
+ 1. Re-read the phase file's acceptance criteria
6
+ 2. For EACH criterion: "Does my implementation satisfy this?"
7
+ 3. If YES → proceed
8
+ 4. If NO → fix it
9
+ 5. If UNCERTAIN → flag: "Unverified: {criterion} — {reason}"
10
+
11
+ This catches "code works but doesn't do what was asked."
@@ -0,0 +1,56 @@
1
+ # Analytics & KPIs
2
+
3
+ **Mandatory.** A milestone that cannot state how its success is measured is
4
+ not ready to build.
5
+
6
+ ## KPIs come before code
7
+
8
+ Every milestone declares, in `milestone.md`, before implementation starts:
9
+
10
+ - **The metric** — one primary number this work is meant to move
11
+ - **The baseline** — what it is today, or explicitly "unknown, this
12
+ establishes it"
13
+ - **The target** — the number that would make this worth having built
14
+ - **How it is read** — the query, dashboard, or event that produces it
15
+
16
+ If you cannot fill these in, the milestone is a guess. Say so to the user
17
+ rather than building it and finding out later.
18
+
19
+ A metric that cannot go down is not a KPI. "Number of features shipped"
20
+ measures activity, not outcome.
21
+
22
+ ## Events
23
+
24
+ Instrument the user's action, at the point it happens. An event fired from
25
+ a wrapper three layers away eventually fires for the wrong reason.
26
+
27
+ - **Naming** is consistent and boring: `object_action` — `checkout_started`,
28
+ `signup_completed`. Pick the convention once, never mix styles.
29
+ - **Typed and centralised.** Event names and payload shapes live in one
30
+ schema module per app, and instrumentation imports from it. A string
31
+ literal at a call site is how analytics quietly rots.
32
+ - **No PII in properties.** No email, no name, no free-text the user typed.
33
+ Identify with a stable anonymous id.
34
+ - **Respect consent.** Where consent is required, no event leaves the device
35
+ until it is given. This is not a preference.
36
+
37
+ ## Every screen and every failure
38
+
39
+ - Each screen or route reports a view event.
40
+ - Each critical flow reports start, success, **and failure** — with a reason
41
+ code. A funnel that only records success cannot tell you why people left,
42
+ which is the only thing you actually wanted to know.
43
+
44
+ ## Verifying
45
+
46
+ Instrumentation is code, so it is tested like code: assert the event fires
47
+ once, with the right name and shape. An analytics call is exactly the kind
48
+ of third-party seam the testing rule exists to cover.
49
+
50
+ Before a milestone closes, confirm the events actually arrived. Untested
51
+ instrumentation is usually broken instrumentation.
52
+
53
+ ## Defaults
54
+
55
+ PostHog. A default, not a mandate — substitute freely, but the capability
56
+ is required. Configure under `observability` in `.orchestra/config.yml`.
@@ -0,0 +1,34 @@
1
+ # Code Standards
2
+
3
+ **SOLID** — Single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion. Open/closed and dependency inversion earn their place once a second real consumer needs the seam — see DRY below. Built before that consumer exists, they are over-engineering wearing a principle's name.
4
+
5
+ **KISS** — Keep it simple. Don't over-engineer.
6
+
7
+ **YAGNI** — Don't build what you don't need right now.
8
+
9
+ **DRY** — Don't repeat yourself. Extract after second duplication, not before. The same rule covers an interface, a seam, or an injection point built for open/closed or dependency inversion: add it when a second real caller needs it, never for a hypothetical one.
10
+
11
+ **Before writing code, stop at the first rung that holds:**
12
+ 1. Does this need to exist? → no: skip it (YAGNI)
13
+ 2. Already in this codebase? → reuse it
14
+ 3. Stdlib does it? → use it
15
+ 4. Native platform feature? → use it
16
+ 5. Installed dependency? → use it directly
17
+ 6. One line? → one line
18
+ 7. Only then: the minimum that works
19
+
20
+ This governs volume, never correctness. Validation, error handling, security
21
+ and accessibility are never on this ladder — they apply at every rung,
22
+ covered by their own rules (`security.orchestra.md`,
23
+ `observability.orchestra.md`, the `accessibility` skill), regardless of how
24
+ small the change looks. Rung 5 does not skip the wrapper
25
+ `testing-standards.orchestra.md` requires for anything calling a third-party
26
+ library (`src/integrations/<provider>/`) — that wrapper exists for the seam
27
+ test, not for swapability, and the ladder governs what sits on top of it,
28
+ never whether it exists.
29
+
30
+ **Zero-tolerance rules:**
31
+ - No workarounds — if the right solution is hard, do it right. Flag effort in report or context.md.
32
+ - No unused code — delete dead imports, functions, files. Don't comment out.
33
+ - No breaking changes without migration — update every consumer.
34
+ - Current library versions only — verify before using, don't assume from memory.
@@ -0,0 +1,77 @@
1
+ # Comments
2
+
3
+ Default to no comment.
4
+
5
+ A comment explains *why*, never *what*. The "what" is the job of naming: if
6
+ a comment is needed to say what the code does, rename or extract instead.
7
+ The test — would a reviewer ask *why* here? Then comment. Would they only
8
+ ask *what*? Then rename.
9
+
10
+ Write one only for:
11
+
12
+ - a workaround or constraint imposed from outside — bundler, driver, API
13
+ quirk — linking the upstream issue when one exists
14
+ - a non-obvious invariant the reader cannot derive locally
15
+ - a deliberate trade-off, including "do not simplify this to X" where X
16
+ looks tempting and is wrong
17
+
18
+ When a comment is warranted, write the single most accurate sentence you
19
+ can, and stop. It has to close the question, not open new ones: name the
20
+ consequence or the rejected alternative, not the restated rule. If a reader
21
+ would still ask *why* after reading it, the sentence is not finished.
22
+
23
+ Never: restating the next line, section banners, commented-out code, a
24
+ comment duplicating a test name. Put it above the line it guards, not
25
+ trailing a long line.
26
+
27
+ ## Some comments are read by the gate
28
+
29
+ Two checks in `scripts/policy-check.mjs` clear on a comment — `await-in-loop`
30
+ on one above the loop, `swallowed-error` on one inside the catch. Those are
31
+ not prose for a reviewer; they are the escape hatch, and deleting one as
32
+ noise turns the gate red.
33
+
34
+ They are also exactly the *why* this rule asks for: which of the sequential
35
+ cases this loop is, why failure on this path is acceptable. `// ok` clears
36
+ the check and tells the next reader nothing — that is a review finding, not
37
+ a pass.
38
+
39
+ `.claude/rules/security.orchestra.md` asks for the same shape beside a
40
+ security setting: the value taken and where it came from.
41
+
42
+ ## A TODO is a tech debt that skipped the tracker
43
+
44
+ `.claude/rules/tracker-authority.orchestra.md` gives a workaround a home: a
45
+ `tech_debt` task naming what should have been done instead. A TODO is that
46
+ task written where nobody will find it.
47
+
48
+ So a TODO carries the task — `TODO(#123)`, `TODO(CU-86abc)`, or a link — and
49
+ the task carries the reason. With `tracker.provider: none` there is no task,
50
+ so there is no TODO: do the work, or report it in the phase result as left
51
+ undone.
52
+
53
+ ## Doc comments
54
+
55
+ JSDoc, KDoc, Swift `///` — one rule for all three. Not mandatory. Add one
56
+ only when it says something the signature cannot.
57
+
58
+ Never restate the signature: no `@param value - the value`, no `@returns
59
+ the result`, no "this function does X" above a function named X.
60
+ `@deprecated` always earns its place — name the replacement.
61
+
62
+ ## While editing
63
+
64
+ Delete a restating comment on a line you are already changing. Leave the
65
+ rest — `.claude/rules/phase-limits.orchestra.md` applies, and a comment
66
+ sweep is not the phase.
67
+
68
+ ## What is enforced
69
+
70
+ Only what can be decided precisely. A restating comment cannot be — a
71
+ machine cannot tell "increment the counter" from a reason — so it stays a
72
+ review item above.
73
+
74
+ | Check | Fails on |
75
+ |---|---|
76
+ | `commented-out-code` | a `//` line that parses as a statement |
77
+ | `todo-without-task` | `TODO`, `FIXME`, `HACK`, `XXX` with no task id or link |
@@ -0,0 +1,20 @@
1
+ # Commit Format
2
+
3
+ All commits MUST use conventional commit format.
4
+
5
+ **Format:** `<type>(<scope>): <description>`
6
+
7
+ **Types:** feat, fix, refactor, perf, test, chore, docs, style, ci, build, revert
8
+
9
+ **Rules:**
10
+ - Each phase produces exactly ONE commit
11
+ - Subject line max 72 characters
12
+ - Body explains WHY, not WHAT
13
+ - Breaking changes add `!` after type: `feat!(scope): description`
14
+ - No `Co-Authored-By` trailers — never add co-author lines
15
+
16
+ **The one exception is a squashed task pull request.** Its subject is
17
+ `#<task-id> - <subject>`, because that is where the tracker's link lives and a
18
+ task PR collapses several conventional commits into one. The `commit-msg` hook
19
+ accepts that shape and nothing else outside the convention — see
20
+ `.claude/rules/pull-request.orchestra.md`.