agentic-team 0.0.0-stage → 6.2.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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -2
  3. package/bin/build-template.js +208 -0
  4. package/bin/index.js +1334 -0
  5. package/bin/platforms.js +510 -0
  6. package/package.json +41 -4
  7. package/template/.claude-plugin/plugin.json +10 -0
  8. package/template/.orchestra/README.md +391 -0
  9. package/template/.orchestra/blueprints/README.md +87 -0
  10. package/template/.orchestra/blueprints/api-only.md +46 -0
  11. package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
  12. package/template/.orchestra/blueprints/saas-starter.md +69 -0
  13. package/template/.orchestra/config.yml +293 -0
  14. package/template/.orchestra/roles/orchestrator.md +105 -0
  15. package/template/.orchestra/roles/product-manager.md +439 -0
  16. package/template/.orchestra/scripts/compact-diff.awk +45 -0
  17. package/template/.orchestra/scripts/context.mjs +373 -0
  18. package/template/.orchestra/stacks/README.md +81 -0
  19. package/template/.orchestra/stacks/_lib/common.sh +3095 -0
  20. package/template/.orchestra/stacks/_repo/setup.sh +1113 -0
  21. package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
  22. package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
  23. package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
  24. package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
  25. package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
  26. package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
  27. package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
  28. package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
  29. package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
  30. package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
  31. package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
  32. package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
  33. package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
  34. package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
  35. package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
  36. package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
  37. package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
  38. package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
  39. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
  40. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
  41. package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
  42. package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
  43. package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
  44. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
  45. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
  46. package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
  47. package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
  48. package/template/.orchestra/trackers/github.sh +467 -0
  49. package/template/CLAUDE.md +54 -0
  50. package/template/agents/architect.md +83 -0
  51. package/template/agents/engineer-high.md +17 -0
  52. package/template/agents/engineer-low.md +17 -0
  53. package/template/agents/engineer-max.md +17 -0
  54. package/template/agents/engineer-medium.md +17 -0
  55. package/template/agents/engineer-xhigh.md +17 -0
  56. package/template/agents/lead.md +925 -0
  57. package/template/agents/milestone-reviewer.md +65 -0
  58. package/template/agents/orchestrator.md +105 -0
  59. package/template/agents/product-manager.md +439 -0
  60. package/template/agents/re-reviewer.md +25 -0
  61. package/template/agents/reviewer.md +303 -0
  62. package/template/agents/scaffolder.md +206 -0
  63. package/template/agents/tracker.md +202 -0
  64. package/template/commands/blueprint.md +23 -0
  65. package/template/commands/create-role.md +64 -0
  66. package/template/commands/export.md +45 -0
  67. package/template/commands/help.md +56 -0
  68. package/template/commands/hotfix.md +14 -0
  69. package/template/commands/orchestrator.md +12 -0
  70. package/template/commands/pm.md +18 -0
  71. package/template/commands/rewind.md +63 -0
  72. package/template/commands/setup.md +15 -0
  73. package/template/commands/start.md +29 -0
  74. package/template/commands/status.md +11 -0
  75. package/template/commands/sync.md +76 -0
  76. package/template/commands/verifier.md +53 -0
  77. package/template/hooks/audit-context.sh +166 -0
  78. package/template/hooks/enforce-role.sh +193 -0
  79. package/template/hooks/protect-system-files.sh +123 -0
  80. package/template/hooks/review-rounds.sh +172 -0
  81. package/template/rules/acceptance-check.orchestra.md +11 -0
  82. package/template/rules/analytics-kpi.orchestra.md +56 -0
  83. package/template/rules/code-standards.orchestra.md +35 -0
  84. package/template/rules/comments.orchestra.md +77 -0
  85. package/template/rules/commit-format.orchestra.md +20 -0
  86. package/template/rules/dependency-upgrade.orchestra.md +112 -0
  87. package/template/rules/design-parity.orchestra.md +181 -0
  88. package/template/rules/observability.orchestra.md +121 -0
  89. package/template/rules/performance.orchestra.md +190 -0
  90. package/template/rules/phase-limits.orchestra.md +3 -0
  91. package/template/rules/pull-request.orchestra.md +218 -0
  92. package/template/rules/role-boundaries.orchestra.md +132 -0
  93. package/template/rules/security.orchestra.md +106 -0
  94. package/template/rules/stuck-detection.orchestra.md +25 -0
  95. package/template/rules/testing-standards.orchestra.md +219 -0
  96. package/template/rules/tracker-authority.orchestra.md +198 -0
  97. package/template/rules/verification-gate.orchestra.md +56 -0
  98. package/template/skills/accessibility/SKILL.md +31 -0
  99. package/template/skills/android-testing/SKILL.md +112 -0
  100. package/template/skills/architecture/SKILL.md +175 -0
  101. package/template/skills/auth-setup/SKILL.md +105 -0
  102. package/template/skills/best-practices/SKILL.md +29 -0
  103. package/template/skills/code-optimizer/SKILL.md +27 -0
  104. package/template/skills/core-web-vitals/SKILL.md +32 -0
  105. package/template/skills/crud-api/SKILL.md +32 -0
  106. package/template/skills/debug/SKILL.md +28 -0
  107. package/template/skills/deployment/SKILL.md +104 -0
  108. package/template/skills/error-handling/SKILL.md +154 -0
  109. package/template/skills/frontend-design/SKILL.md +30 -0
  110. package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
  111. package/template/skills/ios-testing/SKILL.md +100 -0
  112. package/template/skills/mobile-testing/SKILL.md +102 -0
  113. package/template/skills/observability/SKILL.md +91 -0
  114. package/template/skills/project-structure/SKILL.md +114 -0
  115. package/template/skills/push-notifications/SKILL.md +146 -0
  116. package/template/skills/react-best-practices/SKILL.md +31 -0
  117. package/template/skills/realtime/SKILL.md +146 -0
  118. package/template/skills/review/SKILL.md +82 -0
  119. package/template/skills/testing/SKILL.md +45 -0
  120. package/template/skills/web-quality-audit/SKILL.md +47 -0
  121. package/template/skills/web-testing/SKILL.md +120 -0
@@ -0,0 +1,53 @@
1
+ Verify that implemented milestones match their requirements. PM role only.
2
+
3
+ **Usage:**
4
+ - `/orchestra:verifier` — verify all `done` milestones
5
+ - `/orchestra:verifier 1,2,3` — verify only specified milestone numbers
6
+
7
+ 1. Read `.orchestra/roles/product-manager.md` to activate PM.
8
+ 2. Scan `.orchestra/milestones/` using Glob + Read tools (NOT bash scripts) — collect milestones to verify:
9
+ - No arguments: all milestones with `status: done`
10
+ - With numbers: only milestones matching those numbers (e.g., `1` matches `M1-*`)
11
+ 3. For each milestone, read:
12
+ - `prd.md` — product requirements and acceptance criteria
13
+ - `rfc.md` — technical design decisions (if exists)
14
+ - `milestone.md` — summary and acceptance criteria
15
+ - `grooming.md` — scope decisions and phase breakdown
16
+ - All `phases/*.md` — phase acceptance criteria
17
+ 4. For each milestone, read execution context:
18
+ - `decisions.md` — why specific approaches were chosen, and which were later closed
19
+ - `journal.md` — rejected alternatives, useful when a requirement shows as "deviated"
20
+ - `context.md` — `## Phases` section (which phases completed, which failed)
21
+ 5. For each milestone, read the actual implementation:
22
+ - Run `git log --oneline` filtered to commits from that milestone's phases
23
+ - Run `git diff` for those commits, piped through `.orchestra/scripts/compact-diff.awk`, to see what changed
24
+ - Read the current state of modified files — diff shows changes, but current code shows completeness
25
+ 6. Compare requirements vs implementation. For each requirement/acceptance criterion:
26
+ - **met** — implementation satisfies the requirement
27
+ - **partial** — partially implemented, missing aspects noted
28
+ - **missed** — not implemented at all
29
+ - **deviated** — implemented differently than specified
30
+ 6. Report:
31
+
32
+ ```
33
+ ## Verification: M1-user-auth
34
+
35
+ ### Requirements Coverage
36
+ - ✅ met: JWT authentication endpoint (phase-1, commit abc123)
37
+ - ⚠️ partial: Rate limiting — implemented but no Redis backing (phase-2)
38
+ - ❌ missed: Password reset flow — not in any commit
39
+ - 🔀 deviated: Token refresh — RFC said rotating tokens, implemented static expiry
40
+
41
+ ### Summary
42
+ 4 requirements: 1 met, 1 partial, 1 missed, 1 deviated
43
+
44
+ ### Severity
45
+ - 🔴 critical: Password reset flow missing (core auth feature)
46
+ - 🟡 moderate: Rate limiting without Redis (works but won't scale)
47
+ - 🟡 moderate: Token refresh deviation (security concern)
48
+ ```
49
+
50
+ 8. After reporting all milestones, if there are critical or moderate gaps:
51
+ - List gaps grouped by severity
52
+ - Suggest: "Use `/orchestra:pm` to plan fix milestones for these gaps."
53
+ - Do NOT create milestones directly — PM decides scope and priority
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ # Two hook registrations share this one file — PreCompact writes, SessionStart
3
+ # reads — because the finding they pass between them has to live somewhere in
4
+ # between, and that somewhere is the only thing the two halves share: audit()
5
+ # itself runs on PreCompact alone. A mechanical check that a milestone's
6
+ # context.md is internally consistent, run right where a human stops watching
7
+ # and a lead session risks losing the thread.
8
+ #
9
+ # PreCompact cannot block compaction — it is not in Claude Code's exit-code-2
10
+ # table — and whether its own additionalContext reaches the model afterward
11
+ # is undocumented. So this hook never tries to stop or steer compaction: on
12
+ # PreCompact it only writes findings to a per-session scratch file.
13
+ # SessionStart's "compact" matcher is documented and fires right after
14
+ # compaction lands, so that is where the findings actually reach the model —
15
+ # this is advisory, never a block, and it never writes to context.md itself:
16
+ # status stays lead's to advance and the human's to move, per
17
+ # tracker-authority.orchestra.md.
18
+ #
19
+ # The scratch file is keyed by session_id, the same way enforce-role.sh keys
20
+ # its role marker: two terminals are two sessions, and a single shared file
21
+ # would let one session's compaction findings surface on the other's
22
+ # SessionStart — a real cross-session leak in the two-terminal model this
23
+ # project runs by design.
24
+ #
25
+ # Reads the hook call on stdin. Always exits 0.
26
+ set -euo pipefail
27
+
28
+ input="$(cat)"
29
+
30
+ printf '%s' "$input" | CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}" node -e '
31
+ const fs = require("node:fs");
32
+ const path = require("node:path");
33
+
34
+ // Advisory findings are stale past this; a run that never reached
35
+ // SessionStart (crash, a long gap before the next prompt) should not
36
+ // surface an old result as if it just happened. Not project-configurable
37
+ // like thresholds.stuck_retry_limit: this is a display window for
38
+ // informational text, not a gate anything can fail.
39
+ const STALE_MS = 10 * 60 * 1000;
40
+
41
+ let s = "";
42
+ process.stdin.on("data", (d) => (s += d)).on("end", () => {
43
+ let call;
44
+ try {
45
+ call = JSON.parse(s);
46
+ } catch {
47
+ // Optional by design: an unparseable call is not ours to judge.
48
+ return;
49
+ }
50
+
51
+ const session = String(call.session_id || "").replace(/[^A-Za-z0-9._-]/g, "");
52
+ if (!session) return;
53
+
54
+ const root = process.env.CLAUDE_PROJECT_DIR;
55
+ const auditPath = path.join(root, ".orchestra", ".sessions", `${session}.context-audit.json`);
56
+ const milestonesDir = path.join(root, ".orchestra", "milestones");
57
+
58
+ function audit() {
59
+ const findings = [];
60
+ const open = [];
61
+ let entries = [];
62
+ try {
63
+ entries = fs.readdirSync(milestonesDir, { withFileTypes: true });
64
+ } catch {
65
+ return { findings, open };
66
+ }
67
+ for (const entry of entries) {
68
+ if (!entry.isDirectory()) continue;
69
+ let text;
70
+ try {
71
+ text = fs.readFileSync(path.join(milestonesDir, entry.name, "context.md"), "utf8");
72
+ } catch {
73
+ continue;
74
+ }
75
+ // Anchored to a line start (not a stray "### Phases" or a mention
76
+ // inside prose) via lookaround rather than the "m" flag, which would
77
+ // also redefine "$" to mean end-of-line and cut the capture short at
78
+ // the very first line break instead of the next real section.
79
+ const section = /(?:^|\r?\n)## Phases\r?\n([\s\S]*?)(?=\r?\n## |$)/.exec(text);
80
+ const body = section ? section[1] : "";
81
+ for (const line of body.split(/\r?\n/)) {
82
+ const m = /^-\s*(\S+):\s*(done|in-progress|failed|pending|blocked)\b/i.exec(line.trim());
83
+ if (!m) continue;
84
+ const [, phase, statusRaw] = m;
85
+ const status = statusRaw.toLowerCase();
86
+ if (status === "done" && !/commit:\s*\S+/.test(line)) {
87
+ findings.push(`${entry.name}/${phase}: marked done with no commit hash`);
88
+ } else if (status !== "done") {
89
+ open.push(`${entry.name}/${phase}: ${status}`);
90
+ }
91
+ }
92
+ }
93
+ // decisions.md: one source for what a valid line is — the script that
94
+ // writes it — rather than a second parser here that drifts from it.
95
+ const script = path.join(root, ".orchestra", "scripts", "context.mjs");
96
+ if (fs.existsSync(script)) {
97
+ try {
98
+ require("node:child_process").execFileSync(process.execPath, [script, "check"], {
99
+ cwd: root,
100
+ stdio: ["ignore", "pipe", "ignore"],
101
+ });
102
+ } catch (e) {
103
+ for (const f of String(e.stdout || "").split("\n").filter(Boolean)) findings.push(f);
104
+ }
105
+ }
106
+ return { findings, open };
107
+ }
108
+
109
+ if (call.hook_event_name === "PreCompact") {
110
+ const result = audit();
111
+ result.at = new Date().toISOString();
112
+ try {
113
+ const sessionsDir = path.dirname(auditPath);
114
+ fs.mkdirSync(sessionsDir, { recursive: true });
115
+ // The installer writes this once at setup time, but this hook must
116
+ // not depend on that having happened — the directory it just
117
+ // created above needs its own ignore rule, not an assumption that
118
+ // setup already put one there.
119
+ const ignorePath = path.join(sessionsDir, ".gitignore");
120
+ if (!fs.existsSync(ignorePath)) fs.writeFileSync(ignorePath, "*\n!.gitignore\n");
121
+ fs.writeFileSync(auditPath, JSON.stringify(result));
122
+ } catch {
123
+ // Advisory only: a write failure here is not worth surfacing.
124
+ }
125
+ return;
126
+ }
127
+
128
+ // The "compact" filter lives in settings.json'"'"'s matcher for this
129
+ // registration, not here: the exact stdin field naming that trigger is
130
+ // undocumented, so this branch trusts the registration to have already
131
+ // scoped it rather than re-checking a field name that might be wrong.
132
+ if (call.hook_event_name === "SessionStart") {
133
+ let result;
134
+ try {
135
+ result = JSON.parse(fs.readFileSync(auditPath, "utf8"));
136
+ } catch {
137
+ return;
138
+ }
139
+ // A missing or unparseable timestamp must read as stale, not as
140
+ // "not old enough to discard" — Date.parse(undefined) is NaN, and
141
+ // NaN > STALE_MS is false, which would otherwise treat a corrupted
142
+ // write as always-fresh.
143
+ const at = result.at ? Date.parse(result.at) : Number.NaN;
144
+ if (Number.isNaN(at) || Date.now() - at > STALE_MS) return;
145
+
146
+ const lines = [];
147
+ if (result.findings && result.findings.length) {
148
+ lines.push("Context audit after compaction — the milestone record has unresolved defects:");
149
+ for (const f of result.findings) lines.push(` - ${f}`);
150
+ }
151
+ if (result.open && result.open.length) {
152
+ lines.push("Open phases across milestones:");
153
+ for (const o of result.open) lines.push(` - ${o}`);
154
+ }
155
+ if (!lines.length) return;
156
+
157
+ // Native Claude Code reads SessionStart'"'"'s stdout as plain text, not the
158
+ // hookSpecificOutput JSON form other hosts (Codex, Qoder) need for it —
159
+ // confirmed against a production implementation (ponytail'"'"'s
160
+ // hooks/ponytail-runtime.js), since the official docs never showed a
161
+ // literal example either way.
162
+ process.stdout.write(lines.join("\n"));
163
+ }
164
+ });
165
+ '
166
+ exit 0
@@ -0,0 +1,193 @@
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
+ # Lead is held to one rule: phase work starts inside a milestone agent.
18
+ #
19
+ # No marker means no role — discussion mode — and nothing is blocked.
20
+ #
21
+ # Reads the tool call on stdin. Exit 0 allows, exit 2 blocks.
22
+ set -euo pipefail
23
+
24
+ input="$(cat)"
25
+
26
+ verdict="$(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 call;
34
+ try {
35
+ call = JSON.parse(s);
36
+ } catch {
37
+ // Optional by design: an unparseable call is not ours to judge.
38
+ return;
39
+ }
40
+ const session = String(call.session_id || "").replace(/[^A-Za-z0-9._-]/g, "");
41
+ if (!session) return;
42
+ const tool = call.tool_name || "";
43
+ const input = call.tool_input || {};
44
+
45
+ let root;
46
+ try {
47
+ root = execFileSync("git", ["-C", process.env.CLAUDE_PROJECT_DIR, "rev-parse", "--show-toplevel"], {
48
+ encoding: "utf8",
49
+ stdio: ["ignore", "pipe", "ignore"],
50
+ }).trim();
51
+ } catch {
52
+ return;
53
+ }
54
+ if (!root) return;
55
+ // Both sides through realpath: on macOS a temp dir is /var/… while git
56
+ // answers /private/var/…, and path.relative between them says "outside".
57
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return p; } };
58
+ root = real(root);
59
+ const projectDir = real(process.env.CLAUDE_PROJECT_DIR);
60
+ // A path that does not exist yet — a file about to be written — is
61
+ // resolved through its nearest existing ancestor, so an absolute path
62
+ // spelled with the unresolved prefix still lands inside the repo.
63
+ const relTo = (p) => {
64
+ let abs = path.resolve(projectDir, p);
65
+ let head = abs;
66
+ let tail = "";
67
+ while (!fs.existsSync(head) && path.dirname(head) !== head) {
68
+ tail = path.join(path.basename(head), tail);
69
+ head = path.dirname(head);
70
+ }
71
+ abs = path.join(real(head), tail);
72
+ return path.relative(root, abs);
73
+ };
74
+
75
+ const dir = path.join(root, ".orchestra", ".sessions");
76
+ const marker = path.join(dir, session);
77
+
78
+ // The declaration. Recorded, then allowed through.
79
+ if (tool === "Bash") {
80
+ const m = /orchestra-role=(product-manager|orchestrator|lead)\b/.exec(input.command || "");
81
+ if (m) {
82
+ fs.mkdirSync(dir, { recursive: true });
83
+ fs.writeFileSync(marker, m[1]);
84
+ return;
85
+ }
86
+ }
87
+
88
+ let role;
89
+ try {
90
+ role = fs.readFileSync(marker, "utf8").trim();
91
+ } catch {
92
+ return;
93
+ }
94
+ // ── Lead ─────────────────────────────────────────────────────────────
95
+ // Phase work runs inside a milestone agent, so each milestone'"'"'s context is
96
+ // dropped when it ends. One run kept two milestones in the lead session
97
+ // instead: 738 calls, context up to 925k tokens. A call from a sub-agent
98
+ // carries agent_id; one from the lead session itself does not.
99
+ if (role === "lead") {
100
+ if (tool !== "Agent" || call.agent_id) return;
101
+ const type = input.subagent_type || "";
102
+ if (!/^(engineer-\w+|reviewer|re-reviewer)$/.test(type)) return;
103
+ if (/^engineer-/.test(type) && /^\s*Hotfix:/.test(String(input.prompt || ""))) return;
104
+ process.stdout.write(
105
+ `Lead runs phase work inside a milestone agent, not in its own session — launch a general-purpose agent with the Milestone Agent or Single-Phase Agent prompt in .claude/agents/lead.md, and let it start ${type}. A hotfix engineer'"'"'s prompt starts with a line "Hotfix: <description>".\n`,
106
+ );
107
+ return;
108
+ }
109
+
110
+ if (role !== "product-manager") return;
111
+
112
+ // ── The PM ───────────────────────────────────────────────────────────
113
+ // Writes milestones. Commits them to delivery.base_branch and pushes. Nothing else.
114
+ const deny = (why) => {
115
+ process.stdout.write(`Outside the PM'"'"'s scope: ${why}\n`);
116
+ };
117
+
118
+ if (tool === "Write" || tool === "Edit") {
119
+ const file = input.file_path || input.path || "";
120
+ if (!file) return;
121
+ const rel = relTo(file);
122
+ if (rel.startsWith("..")) return deny(`${file} is outside the repository`);
123
+ if (!rel.startsWith(".orchestra/milestones/")) {
124
+ return deny(`the PM writes only under .orchestra/milestones/ — ${rel} is not there. Say what needs changing and which command can do it.`);
125
+ }
126
+ return;
127
+ }
128
+
129
+ if (tool === "Workflow") {
130
+ 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.");
131
+ }
132
+ if (tool === "Agent") {
133
+ const type = input.subagent_type || "";
134
+ if (type !== "milestone-reviewer" && type !== "tracker") {
135
+ return deny(`the PM launches only milestone-reviewer and tracker — not ${type || "a general-purpose agent"}. Reviewing code, building, and scouting are lead'"'"'s.`);
136
+ }
137
+ return;
138
+ }
139
+ if (/clickup_(create|update)_task$/.test(tool)) {
140
+ const status = String(input.status ?? "").trim();
141
+ if (!status) return;
142
+ // The tracker agent runs inside the PM'"'"'s session and lands a new task
143
+ // in backlog — the one status write the PM side is allowed. Read the
144
+ // board'"'"'s own name for it from config, compared case-insensitively.
145
+ let backlog = "backlog";
146
+ try {
147
+ const cfg = fs.readFileSync(path.join(root, ".orchestra", "config.yml"), "utf8");
148
+ const m = /^\s*backlog:\s*["'"'"']?([^"'"'"'#\n]+)/m.exec(cfg);
149
+ if (m) backlog = m[1].trim();
150
+ } catch {
151
+ // No config: the default name is the only name we know.
152
+ }
153
+ if (status.toLowerCase() === backlog.toLowerCase()) return;
154
+ 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)}.`);
155
+ }
156
+
157
+ if (tool !== "Bash") return;
158
+ const cmd = input.command || "";
159
+
160
+ if (/\bgh\s+pr\s+(create|merge|edit|close|ready|checkout)\b/.test(cmd)) {
161
+ return deny("the PM never opens, edits or merges a pull request. Milestone files go to delivery.base_branch directly; code is lead'"'"'s.");
162
+ }
163
+ if (/\bgit\s+(checkout|switch)\s+(-b|-c|--create|-B)\b/.test(cmd)) {
164
+ return deny("the PM never creates a branch. Milestone files are committed to delivery.base_branch.");
165
+ }
166
+ if (/\bgit\s+(merge|rebase|cherry-pick|reset\s+--hard)\b/.test(cmd)) {
167
+ return deny("the PM never rewrites history or merges.");
168
+ }
169
+ if (/\bgit\s+push\b.*(\s--force\b|\s-f\b|\s--force-with-lease\b)/.test(cmd)) {
170
+ return deny("the PM never force-pushes.");
171
+ }
172
+ if (/\bgit\s+commit\b[^&|;]*\s(?:-[a-zA-Z]*a[a-zA-Z]*|--all)\b/.test(cmd)) {
173
+ return deny("`git commit -a` stages everything. Add .orchestra/milestones/ explicitly, then commit.");
174
+ }
175
+ const add = /\bgit\s+add\s+(.+?)(?:\s*(?:&&|\|\||;|$))/.exec(cmd);
176
+ if (add) {
177
+ const args = add[1].split(/\s+/).filter((a) => a && !a.startsWith("-"));
178
+ const outside = args.filter((a) => {
179
+ const rel = relTo(a.replace(/^["'"'"']|["'"'"']$/g, ""));
180
+ return !rel.startsWith(".orchestra/milestones");
181
+ });
182
+ if (args.length === 0 || outside.length) {
183
+ return deny(`the PM stages only .orchestra/milestones/ — not ${outside.join(", ") || "-A / ."}.`);
184
+ }
185
+ }
186
+ });
187
+ ')"
188
+
189
+ if [ -n "$verdict" ]; then
190
+ printf '%s\n' "$verdict" >&2
191
+ exit 2
192
+ fi
193
+ 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 agentic-team 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 agentic-team 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