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.
- package/LICENSE +21 -0
- package/README.md +184 -2
- package/bin/build-template.js +208 -0
- package/bin/index.js +1139 -0
- package/package.json +41 -4
- package/template/.claude-plugin/plugin.json +10 -0
- package/template/.orchestra/README.md +401 -0
- package/template/.orchestra/blueprints/README.md +87 -0
- package/template/.orchestra/blueprints/api-only.md +46 -0
- package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
- package/template/.orchestra/blueprints/saas-starter.md +69 -0
- package/template/.orchestra/config.yml +277 -0
- package/template/.orchestra/roles/orchestrator.md +105 -0
- package/template/.orchestra/roles/product-manager.md +438 -0
- package/template/.orchestra/scripts/compact-diff.awk +45 -0
- package/template/.orchestra/stacks/README.md +81 -0
- package/template/.orchestra/stacks/_lib/common.sh +3095 -0
- package/template/.orchestra/stacks/_repo/setup.sh +1109 -0
- package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
- package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
- package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
- package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
- package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
- package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
- package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
- package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
- package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
- package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
- package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
- package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
- package/template/.orchestra/trackers/github.sh +467 -0
- package/template/CLAUDE.md +54 -0
- package/template/agents/architect.md +83 -0
- package/template/agents/engineer-high.md +17 -0
- package/template/agents/engineer-low.md +17 -0
- package/template/agents/engineer-max.md +17 -0
- package/template/agents/engineer-medium.md +17 -0
- package/template/agents/engineer-xhigh.md +17 -0
- package/template/agents/lead.md +870 -0
- package/template/agents/milestone-reviewer.md +63 -0
- package/template/agents/orchestrator.md +105 -0
- package/template/agents/product-manager.md +438 -0
- package/template/agents/reviewer.md +301 -0
- package/template/agents/scaffolder.md +180 -0
- package/template/agents/tracker.md +202 -0
- package/template/commands/blueprint.md +23 -0
- package/template/commands/create-role.md +64 -0
- package/template/commands/export.md +45 -0
- package/template/commands/help.md +55 -0
- package/template/commands/hotfix.md +12 -0
- package/template/commands/orchestrator.md +12 -0
- package/template/commands/pm.md +18 -0
- package/template/commands/rewind.md +61 -0
- package/template/commands/setup.md +14 -0
- package/template/commands/start.md +29 -0
- package/template/commands/status.md +11 -0
- package/template/commands/sync.md +73 -0
- package/template/commands/verifier.md +53 -0
- package/template/hooks/audit-context.sh +153 -0
- package/template/hooks/enforce-role.sh +175 -0
- package/template/hooks/protect-system-files.sh +123 -0
- package/template/hooks/review-rounds.sh +172 -0
- package/template/rules/acceptance-check.orchestra.md +11 -0
- package/template/rules/analytics-kpi.orchestra.md +56 -0
- package/template/rules/code-standards.orchestra.md +34 -0
- package/template/rules/comments.orchestra.md +77 -0
- package/template/rules/commit-format.orchestra.md +20 -0
- package/template/rules/dependency-upgrade.orchestra.md +112 -0
- package/template/rules/design-parity.orchestra.md +170 -0
- package/template/rules/observability.orchestra.md +118 -0
- package/template/rules/performance.orchestra.md +186 -0
- package/template/rules/phase-limits.orchestra.md +3 -0
- package/template/rules/pull-request.orchestra.md +213 -0
- package/template/rules/role-boundaries.orchestra.md +117 -0
- package/template/rules/security.orchestra.md +106 -0
- package/template/rules/stuck-detection.orchestra.md +25 -0
- package/template/rules/testing-standards.orchestra.md +214 -0
- package/template/rules/tracker-authority.orchestra.md +190 -0
- package/template/rules/verification-gate.orchestra.md +56 -0
- package/template/skills/accessibility/SKILL.md +31 -0
- package/template/skills/android-testing/SKILL.md +112 -0
- package/template/skills/architecture/SKILL.md +175 -0
- package/template/skills/auth-setup/SKILL.md +105 -0
- package/template/skills/best-practices/SKILL.md +29 -0
- package/template/skills/code-optimizer/SKILL.md +27 -0
- package/template/skills/core-web-vitals/SKILL.md +32 -0
- package/template/skills/crud-api/SKILL.md +32 -0
- package/template/skills/debug/SKILL.md +28 -0
- package/template/skills/deployment/SKILL.md +104 -0
- package/template/skills/error-handling/SKILL.md +154 -0
- package/template/skills/frontend-design/SKILL.md +30 -0
- package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
- package/template/skills/ios-testing/SKILL.md +100 -0
- package/template/skills/mobile-testing/SKILL.md +102 -0
- package/template/skills/observability/SKILL.md +91 -0
- package/template/skills/project-structure/SKILL.md +114 -0
- package/template/skills/push-notifications/SKILL.md +146 -0
- package/template/skills/react-best-practices/SKILL.md +31 -0
- package/template/skills/realtime/SKILL.md +146 -0
- package/template/skills/review/SKILL.md +82 -0
- package/template/skills/testing/SKILL.md +44 -0
- package/template/skills/web-quality-audit/SKILL.md +47 -0
- 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`.
|