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,467 @@
1
+ #!/usr/bin/env bash
2
+ # GitHub Projects adapter.
3
+ #
4
+ # A script rather than instructions in an agent prompt, for the same reason
5
+ # scaffolding is a script: an agent improvising GraphQL produces a different
6
+ # board every run and leaves nothing to review. Every subcommand here is
7
+ # idempotent, so re-running updates rather than duplicating.
8
+ #
9
+ # discover <project-number> read the board's real fields
10
+ # export <milestone-dir> <project-num> create/update epic and sub-issues
11
+ # status-get <issue-number> print the board status
12
+ # status-set <issue-number> <status> move it — board from tracker.board
13
+ # comments <issue-number> the thread, each marked allowed or not
14
+ # reply <issue> <comment-id> <file> answer one comment
15
+ #
16
+ # Every mutation used here was verified against the live schema before it was
17
+ # written — addProjectV2ItemById, updateProjectV2ItemFieldValue, addSubIssue.
18
+ set -euo pipefail
19
+
20
+ die() { printf ' [x] %s\n' "$*" >&2; exit 1; }
21
+ log() { printf ' → %s\n' "$*" >&2; }
22
+
23
+ command -v gh >/dev/null 2>&1 || die "gh is not installed"
24
+ gh auth status >/dev/null 2>&1 || die "gh is not authenticated — run: gh auth login"
25
+
26
+ REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"
27
+ OWNER="${REPO%%/*}"
28
+
29
+ # ── the board ────────────────────────────────────────────────────────────────
30
+
31
+ # A Projects v2 board can hang off a user or an organisation, and the query
32
+ # differs. Try the viewer first, then the owner as an organisation, rather
33
+ # than asking the caller to know which they have.
34
+ project_query() {
35
+ local num="$1" scope
36
+ for scope in "viewer" "organization(login: \"$OWNER\")" "user(login: \"$OWNER\")"; do
37
+ local out
38
+ out="$(gh api graphql -f query="
39
+ { $scope { projectV2(number: $num) {
40
+ id title
41
+ fields(first: 50) { nodes {
42
+ ... on ProjectV2SingleSelectField { id name options { id name } }
43
+ ... on ProjectV2FieldCommon { id name }
44
+ } }
45
+ } } }" 2>/dev/null)" || continue
46
+ # A scope that does not hold the board answers with a null projectV2
47
+ # rather than an error, so the exit code proves nothing on its own.
48
+ printf '%s' "$out" | grep -q '"projectV2":null' && continue
49
+ printf '%s' "$out"
50
+ return 0
51
+ done
52
+ die "no Projects v2 board number $num on $OWNER"
53
+ }
54
+
55
+ cmd_discover() {
56
+ local num="${1:?usage: discover <project-number>}"
57
+ local json status_opts
58
+ json="$(project_query "$num")"
59
+
60
+ status_opts="$(printf '%s' "$json" \
61
+ | python3 -c '
62
+ import json,sys
63
+ d = json.load(sys.stdin)["data"]
64
+ p = next(v for v in d.values() if v)["projectV2"]
65
+ f = next((n for n in p["fields"]["nodes"] if n.get("name") == "Status"), None)
66
+ if not f or "options" not in f:
67
+ sys.exit("the board has no Status single-select field")
68
+ print(p["id"])
69
+ for o in f["options"]:
70
+ print(o["name"])
71
+ ')"
72
+
73
+ local board_id; board_id="$(printf '%s' "$status_opts" | head -1)"
74
+ local names; names="$(printf '%s' "$status_opts" | tail -n +2)"
75
+
76
+ # Printed as the config block to paste, not written in place: config.yml is
77
+ # merged on upgrade and hand-edited by the user, and a script that rewrites
78
+ # it silently is a script that loses a comment somebody wrote.
79
+ printf 'tracker:\n provider: github\n board: "%s"\n states:\n' "$num"
80
+ # Orchestra's internal states are fixed; only the mapping varies. Matched
81
+ # case-insensitively against what the board actually calls them, and left
82
+ # blank when the board has no such column — blank is an honest "you decide",
83
+ # a guess is an agent moving a task to a column that does not exist.
84
+ # Alternation, first match wins: a stock board calls the backlog "Todo",
85
+ # a renamed one "Backlog", and either is the column export lands in.
86
+ map_state() {
87
+ local want="$1" match
88
+ match="$(printf '%s\n' "$names" | grep -ixE "$want" | head -1 || true)"
89
+ printf ' %s: "%s"\n' "$2" "$match"
90
+ }
91
+ map_state "Backlog|To do|Todo" backlog
92
+ map_state "Ready" ready
93
+ map_state "In progress" in_progress
94
+ map_state "In review" in_review
95
+ map_state "Done" done
96
+ map_state "Blocked" blocked
97
+ map_state "Tech debt|Tech-debt" tech_debt
98
+ printf '\n' >&2
99
+ log "board id $board_id — columns: $(printf '%s' "$names" | tr '\n' ',' | sed 's/,$//')"
100
+ printf '%s\n' "$names" | grep -qix "in review" \
101
+ || log "warning: no 'In review' column — the sync loop has nowhere to move a task"
102
+ }
103
+
104
+ # ── export ───────────────────────────────────────────────────────────────────
105
+
106
+ # The issue whose title matches, or empty. This is what makes export
107
+ # idempotent without storing anything: a second run finds the first run's
108
+ # issue instead of opening another. A second title is the one an earlier
109
+ # version of this script gave the same item — `[slug] phase-1` — so a board
110
+ # exported before titles became sentences is updated, not duplicated.
111
+ find_issue() { # title [legacy-title]
112
+ local num
113
+ num="$(gh issue list --state all --search "$1 in:title" --json number,title \
114
+ --jq "[.[] | select(.title == \"$1\")] | .[0].number // empty" 2>/dev/null)"
115
+ if [ -z "$num" ] && [ -n "${2:-}" ]; then
116
+ num="$(gh issue list --state all --search "$2 in:title" --json number,title \
117
+ --jq "[.[] | select(.title == \"$2\")] | .[0].number // empty" 2>/dev/null)"
118
+ # Found under the old name: rename it, so the next run finds it directly.
119
+ [ -n "$num" ] && gh issue edit "$num" --title "$1" >/dev/null
120
+ fi
121
+ printf '%s' "$num"
122
+ }
123
+
124
+ # What the board shows. The phase file is the engineer's document — a
125
+ # frontmatter of knobs, a Result section lead fills in later — and none of
126
+ # that belongs in front of a person reading the task. Frontmatter and Result
127
+ # are dropped; everything else stays, because with a provider this body is
128
+ # what lead reads back and hands to the sub-agent, and Scope and Acceptance
129
+ # Criteria have to arrive intact.
130
+ board_body() { # phase-or-milestone-file → path of a temp file
131
+ local out
132
+ out="$(mktemp)"
133
+ python3 - "$1" > "$out" <<'PY'
134
+ import re, sys
135
+ s = open(sys.argv[1]).read()
136
+ if s.startswith("---\n"):
137
+ end = s.find("\n---\n", 4)
138
+ if end != -1:
139
+ s = s[end + 5:]
140
+ s = re.sub(r"\n## Result\n[\s\S]*$", "\n", s)
141
+ sys.stdout.write(s.strip() + "\n")
142
+ PY
143
+ printf '%s' "$out"
144
+ }
145
+
146
+ # `title:` from a file's frontmatter, or nothing.
147
+ front_title() {
148
+ python3 - "$1" <<'PY'
149
+ import re, sys
150
+ s = open(sys.argv[1]).read()
151
+ if not s.startswith("---\n"): sys.exit(0)
152
+ end = s.find("\n---\n", 4)
153
+ m = re.search(r"^title:[ \t]*(.+)$", s[4:end], re.M) if end != -1 else None
154
+ if m: print(m.group(1).split(" #")[0].strip().strip('"').strip("'"))
155
+ PY
156
+ }
157
+
158
+ # Prints the number, and sets UPSERT_CREATED to 1 when the issue is new. A
159
+ # re-export updates the body and must not touch status: the task may be in
160
+ # progress, and moving it back to the backlog would tell lead a human did.
161
+ upsert_issue() { # title body-file [legacy-title]
162
+ local title="$1" body="$2" num
163
+ UPSERT_CREATED=0
164
+ num="$(find_issue "$title" "${3:-}")"
165
+ if [ -n "$num" ]; then
166
+ gh issue edit "$num" --body-file "$body" >/dev/null
167
+ printf '%s' "$num"
168
+ else
169
+ UPSERT_CREATED=1
170
+ gh issue create --title "$title" --body-file "$body" \
171
+ | grep -oE '[0-9]+$'
172
+ fi
173
+ }
174
+
175
+ issue_node_id() { gh issue view "$1" --json id --jq .id; }
176
+
177
+ link_sub_issue() { # parent-number child-number
178
+ local parent child
179
+ parent="$(issue_node_id "$1")"
180
+ child="$(issue_node_id "$2")"
181
+ # Already linked is not an error worth stopping for — export is meant to be
182
+ # re-runnable, and GitHub rejects a duplicate link rather than ignoring it.
183
+ gh api graphql -f query='
184
+ mutation($p: ID!, $c: ID!) {
185
+ addSubIssue(input: {issueId: $p, subIssueId: $c}) { issue { number } }
186
+ }' -f p="$parent" -f c="$child" >/dev/null 2>&1 || true
187
+ }
188
+
189
+ board_add() { # project-number issue-number → item id
190
+ local proj_id issue_id
191
+ proj_id="$(project_query "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(next(v for v in d.values() if v)["projectV2"]["id"])')"
192
+ issue_id="$(issue_node_id "$2")"
193
+ gh api graphql -f query='
194
+ mutation($p: ID!, $c: ID!) {
195
+ addProjectV2ItemById(input: {projectId: $p, contentId: $c}) { item { id } }
196
+ }' -f p="$proj_id" -f c="$issue_id" --jq '.data.addProjectV2ItemById.item.id'
197
+ }
198
+
199
+ cmd_export() {
200
+ local dir="${1:?usage: export <milestone-dir> <project-number>}"
201
+ local proj="${2:?usage: export <milestone-dir> <project-number>}"
202
+ [ -d "$dir" ] || die "no milestone directory at $dir"
203
+ [ -f "$dir/milestone.md" ] || die "$dir has no milestone.md"
204
+
205
+ local slug epic
206
+ slug="$(basename "$dir")"
207
+ log "exporting $slug to project $proj"
208
+
209
+ # The epic is titled by the milestone's heading — "Milestone: M1 — User
210
+ # auth" becomes "User auth" — and the task by the phase's `title:`. A phase
211
+ # without one falls back to the old shape, and the PM's reviewer flags it.
212
+ local epic_title body
213
+ epic_title="$(grep -m1 -E '^# ' "$dir/milestone.md" | sed -E 's/^# *(Milestone: *)?(M[0-9]+ *[—-] *)?//')"
214
+ [ -n "$epic_title" ] || epic_title="$slug"
215
+ body="$(board_body "$dir/milestone.md")"
216
+ epic="$(upsert_issue "$epic_title" "$body" "[epic] $slug")"
217
+ rm -f "$body"
218
+ board_add "$proj" "$epic" >/dev/null
219
+ log "epic #$epic"
220
+
221
+ {
222
+ printf 'provider: github\n'
223
+ printf 'board: "%s"\n' "$proj"
224
+ printf 'epic:\n number: %s\n url: "https://github.com/%s/issues/%s"\n' "$epic" "$REPO" "$epic"
225
+ printf 'tasks:\n'
226
+ } > "$dir/tracker.yml"
227
+
228
+ # A new task lands in the backlog, and the human moves it to ready when
229
+ # lead may take it. Read from config so the column is the board's own name;
230
+ # blank means discovery found no such column, and the task is left where
231
+ # the board puts it rather than moved to a guess.
232
+ local backlog
233
+ backlog="$(config_state backlog)"
234
+ [ -n "$backlog" ] || log "warning: tracker.states.backlog is empty — new tasks keep the board's default column"
235
+
236
+ local phase name task
237
+ for phase in "$dir"/phases/*.md; do
238
+ [ -e "$phase" ] || break
239
+ name="$(basename "$phase" .md)"
240
+ local title
241
+ title="$(front_title "$phase")"
242
+ [ -n "$title" ] || { title="[$slug] $name"; log "warning: $name has no title: — using $title"; }
243
+ body="$(board_body "$phase")"
244
+ task="$(upsert_issue "$title" "$body" "[$slug] $name")"
245
+ rm -f "$body"
246
+ link_sub_issue "$epic" "$task"
247
+ board_add "$proj" "$task" >/dev/null
248
+ if [ "$UPSERT_CREATED" = 1 ] && [ -n "$backlog" ]; then
249
+ cmd_status_set "$task" "$backlog" "$proj"
250
+ fi
251
+ printf ' - phase: "%s"\n number: %s\n url: "https://github.com/%s/issues/%s"\n' \
252
+ "$name" "$task" "$REPO" "$task" >> "$dir/tracker.yml"
253
+ log "task #$task — $name"
254
+ done
255
+
256
+ log "wrote $dir/tracker.yml"
257
+ }
258
+
259
+ # ── status ───────────────────────────────────────────────────────────────────
260
+
261
+ cmd_status_get() {
262
+ local num="${1:?usage: status-get <issue-number>}" out
263
+ # `repository.issue` answers null for a pull request number rather than
264
+ # erroring cleanly, and gh then prints the whole GraphQL envelope and exits
265
+ # 1. Read the output instead of the exit code, so the caller gets a sentence.
266
+ out="$(gh api graphql -f query='
267
+ query($owner: String!, $repo: String!, $n: Int!) {
268
+ repository(owner: $owner, name: $repo) { issue(number: $n) {
269
+ projectItems(first: 10) { nodes {
270
+ fieldValueByName(name: "Status") {
271
+ ... on ProjectV2ItemFieldSingleSelectValue { name }
272
+ } } } } } }' \
273
+ -f owner="$OWNER" -f repo="${REPO#*/}" -F n="$num" 2>/dev/null || true)"
274
+
275
+ printf '%s' "$out" | grep -q '"issue":null' \
276
+ && die "#$num is not an issue in $REPO — a pull request number will do this"
277
+
278
+ # No output and exit 0 is the honest answer for an issue that is on no
279
+ # board: it has no status, which is different from a failure to read one.
280
+ printf '%s' "$out" \
281
+ | python3 -c '
282
+ import json,sys
283
+ try: d = json.load(sys.stdin)["data"]["repository"]["issue"]["projectItems"]["nodes"]
284
+ except Exception: sys.exit(0)
285
+ for n in d:
286
+ v = n.get("fieldValueByName") or {}
287
+ if v.get("name"):
288
+ print(v["name"]); break
289
+ '
290
+ }
291
+
292
+ # tracker.board from config.yml.
293
+ #
294
+ # Read here rather than demanded as an argument. github.sh needs the board to
295
+ # resolve a status option and a connector-backed provider does not, so
296
+ # requiring it positionally
297
+ # gave the two adapters different signatures — and the agents call one
298
+ # interface, so lead ran the same command and it worked on one provider only.
299
+ # A third argument still overrides, for a one-off.
300
+ config_board() {
301
+ local cfg
302
+ cfg="$(git rev-parse --show-toplevel 2>/dev/null)/.orchestra/config.yml"
303
+ [ -f "$cfg" ] || return 0
304
+ python3 - "$cfg" <<'CFG'
305
+ import re, sys
306
+ for b in re.split(r"\n(?=[a-z_]+:)", open(sys.argv[1]).read()):
307
+ if b.startswith("tracker:"):
308
+ m = re.search(r"^[ \t]*board:[ \t]*(.*)$", b, re.M)
309
+ if m:
310
+ print(m.group(1).strip().strip(chr(34)).strip(chr(39)))
311
+ break
312
+ CFG
313
+ }
314
+
315
+ # tracker.states.<name> from config.yml — the board's own column name for one
316
+ # of Orchestra's states, or nothing when discovery left it blank.
317
+ config_state() {
318
+ local cfg
319
+ cfg="$(git rev-parse --show-toplevel 2>/dev/null)/.orchestra/config.yml"
320
+ [ -f "$cfg" ] || return 0
321
+ python3 - "$cfg" "$1" <<'CFG'
322
+ import re, sys
323
+ for b in re.split(r"\n(?=[a-z_]+:)", open(sys.argv[1]).read()):
324
+ if b.startswith("tracker:"):
325
+ m = re.search(r"^[ \t]*" + re.escape(sys.argv[2]) + r":[ \t]*(.*)$", b, re.M)
326
+ if m:
327
+ print(m.group(1).split("#")[0].strip().strip(chr(34)).strip(chr(39)))
328
+ break
329
+ CFG
330
+ }
331
+
332
+ cmd_status_set() {
333
+ local num="${1:?usage: status-set <issue-number> <status> [project-number]}"
334
+ local want="${2:?usage: status-set <issue-number> <status> [project-number]}"
335
+ local proj="${3:-$(config_board)}"
336
+ [ -n "$proj" ] || die "no board — pass the project number, or set tracker.board in .orchestra/config.yml"
337
+
338
+ local json proj_id field_id opt_id item_id
339
+ json="$(project_query "$proj")"
340
+ proj_id="$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin)["data"]; print(next(v for v in d.values() if v)["projectV2"]["id"])')"
341
+ # Into a variable first, and the exit code checked. `read <<< "$(...)"`
342
+ # succeeds even when the substitution died, so a board with no such column
343
+ # printed the right message and then fired the mutation with an empty field
344
+ # id — "could not resolve a node with the global id of ''".
345
+ local resolved
346
+ resolved="$(printf '%s' "$json" | python3 -c "
347
+ import json,sys
348
+ d = json.load(sys.stdin)['data']
349
+ p = next(v for v in d.values() if v)['projectV2']
350
+ f = next((n for n in p['fields']['nodes'] if n.get('name') == 'Status'), None)
351
+ if not f: sys.exit('the board has no Status field')
352
+ o = next((o for o in f['options'] if o['name'].lower() == '''$want'''.lower()), None)
353
+ if not o: sys.exit('the board has no column named \"$want\" — it has: ' + ', '.join(x['name'] for x in f['options']))
354
+ print(f['id'], o['id'])
355
+ ")" || exit 1
356
+ read -r field_id opt_id <<< "$resolved"
357
+
358
+ item_id="$(gh api graphql -f query='
359
+ query($owner: String!, $repo: String!, $n: Int!) {
360
+ repository(owner: $owner, name: $repo) { issue(number: $n) {
361
+ projectItems(first: 10) { nodes { id project { id } } } } } }' \
362
+ -f owner="$OWNER" -f repo="${REPO#*/}" -F n="$num" \
363
+ --jq ".data.repository.issue.projectItems.nodes[] | select(.project.id == \"$proj_id\") | .id")"
364
+ [ -n "$item_id" ] || item_id="$(board_add "$proj" "$num")"
365
+
366
+ gh api graphql -f query='
367
+ mutation($p: ID!, $i: ID!, $f: ID!, $o: String!) {
368
+ updateProjectV2ItemFieldValue(input: {
369
+ projectId: $p, itemId: $i, fieldId: $f,
370
+ value: {singleSelectOptionId: $o}
371
+ }) { projectV2Item { id } } }' \
372
+ -f p="$proj_id" -f i="$item_id" -f f="$field_id" -f o="$opt_id" >/dev/null
373
+ log "#$num → $want"
374
+ }
375
+
376
+ # ── comments ─────────────────────────────────────────────────────────────────
377
+
378
+ # tracker.actors from config.yml, one login per line.
379
+ #
380
+ # Read here rather than left to the agent's judgement. The allowlist is what
381
+ # stops a board comment from being an instruction channel, and a rule enforced
382
+ # by a prompt is a rule that holds until the prompt is summarised.
383
+ allowlist() {
384
+ local cfg
385
+ cfg="$(git rev-parse --show-toplevel 2>/dev/null)/.orchestra/config.yml"
386
+ [ -f "$cfg" ] || return 0
387
+ python3 - "$cfg" <<'PY'
388
+ import re, sys
389
+ block = re.split(r"\n(?=[a-z_]+:)", open(sys.argv[1]).read())
390
+ for b in block:
391
+ if not b.startswith("tracker:"):
392
+ continue
393
+ # [ \t]* and not \s*: \s matches a newline, so a greedy \s* after the
394
+ # colon swallowed the first item of a block list into the inline capture
395
+ # and the allowlist silently lost its first entry.
396
+ m = re.search(r"^[ \t]*actors:[ \t]*(.*)$", b, re.M)
397
+ if not m:
398
+ break
399
+ inline = m.group(1).strip()
400
+ if inline.startswith("["):
401
+ for name in re.findall(r'"([^"]+)"|\'([^\']+)\'', inline):
402
+ print(name[0] or name[1])
403
+ else:
404
+ # lstrip the newline the match ends on: without it the first element
405
+ # is an empty string, which is not a list item, and a block-style
406
+ # actors: parsed as nobody while the inline form worked.
407
+ for line in b[m.end():].lstrip("\n").split("\n"):
408
+ item = re.match(r'^\s+-\s*["\']?([^"\'\s]+)', line)
409
+ if not item:
410
+ break
411
+ print(item.group(1))
412
+ PY
413
+ }
414
+
415
+ cmd_comments() {
416
+ local num="${1:?usage: comments <issue-number>}" allowed
417
+ allowed="$(allowlist)"
418
+ # Every comment is reported, allowed or not — one so it can be acted on, the
419
+ # other so the user is told somebody else is writing on their board. Silently
420
+ # dropping it would hide exactly the event worth noticing.
421
+ gh api "repos/$REPO/issues/$num/comments" --paginate 2>/dev/null \
422
+ | ALLOWED="$allowed" python3 -c '
423
+ import json, os, sys
424
+ allowed = {l.strip() for l in os.environ["ALLOWED"].splitlines() if l.strip()}
425
+ for c in json.load(sys.stdin):
426
+ login = (c.get("user") or {}).get("login", "")
427
+ print(json.dumps({
428
+ "id": c["id"],
429
+ "author": login,
430
+ "allowed": login in allowed,
431
+ "created_at": c["created_at"],
432
+ "updated_at": c["updated_at"],
433
+ "body": c["body"],
434
+ }))
435
+ '
436
+ }
437
+
438
+ cmd_reply() {
439
+ local num="${1:?usage: reply <issue-number> <comment-id> <body-file>}"
440
+ local cid="${2:?usage: reply <issue-number> <comment-id> <body-file>}"
441
+ local file="${3:?usage: reply <issue-number> <comment-id> <body-file>}"
442
+ [ -f "$file" ] || die "no body file at $file"
443
+
444
+ local author
445
+ author="$(gh api "repos/$REPO/issues/comments/$cid" --jq '.user.login' 2>/dev/null || true)"
446
+ [ -n "$author" ] || die "no comment $cid on $REPO"
447
+ allowlist | grep -qx "$author" \
448
+ || die "comment $cid is from @$author, who is not in tracker.actors — reporting, not acting"
449
+
450
+ # The marker that makes the loop idempotent with no local state: the reply
451
+ # names the comment it answers, so a second run sees the work was done. An
452
+ # edited comment is caught by comparing its updated_at to this reply's time.
453
+ local tmp; tmp="$(mktemp)"
454
+ { printf '> in reply to comment %s by @%s\n\n' "$cid" "$author"; cat "$file"; } > "$tmp"
455
+ gh issue comment "$num" --body-file "$tmp"
456
+ rm -f "$tmp"
457
+ }
458
+
459
+ case "${1:-}" in
460
+ discover) shift; cmd_discover "$@" ;;
461
+ export) shift; cmd_export "$@" ;;
462
+ status-get) shift; cmd_status_get "$@" ;;
463
+ status-set) shift; cmd_status_set "$@" ;;
464
+ comments) shift; cmd_comments "$@" ;;
465
+ reply) shift; cmd_reply "$@" ;;
466
+ *) die "usage: github.sh {discover|export|status-get|status-set|comments|reply} ..." ;;
467
+ esac
@@ -0,0 +1,54 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ <!-- orchestra -->
6
+ ## Orchestra — AI Team Orchestration System
7
+
8
+ This project uses Orchestra for multi-agent coordination via `.orchestra/` and `.claude/`.
9
+
10
+ **FIRST RESPONSE — check the project before asking anything.**
11
+
12
+ If `.orchestra/milestones/` holds no milestone directory — a `.gitkeep` is
13
+ not a milestone — **and** there is no app yet, meaning no `apps/` and no root
14
+ `package.json`, then this is a fresh repo. Say so in one line
15
+ and point at `/orchestra:setup`. Do not offer a role: there is nothing for a
16
+ PM to plan into and nothing for a lead to build, and asking which hat to wear
17
+ before the project exists is the wrong first question.
18
+
19
+ Otherwise, unless the user explicitly names a role:
20
+
21
+ Call `AskUserQuestion` with:
22
+ - header: "Role"
23
+ - question: "Which role will you take for this session? (pick one)"
24
+ - multiSelect: false
25
+ - options:
26
+ 1. Orchestrator — Maintain and evolve Orchestra system files, roles, and rules
27
+ 2. Product Manager — Write PRDs, create milestones with phases, orchestrate pipeline
28
+ 3. Discussion — Just brainstorm, no role needed
29
+
30
+ If user skips or starts giving instructions directly — work with them normally.
31
+ Sub-agents spawned by lead skip role selection — they already have a role assigned in their prompt.
32
+ Do NOT greet. Do NOT explain. The role selection IS your greeting.
33
+
34
+ **AFTER ROLE SELECTED:**
35
+
36
+ 1. Read `.orchestra/roles/{role-id}.md` for identity and ownership
37
+ 2. Read `.orchestra/config.yml` for pipeline settings
38
+ 3. Check `.orchestra/milestones/` for active work
39
+ 4. If work exists → announce and start. If not → report and wait.
40
+
41
+ Role IDs: orchestrator, product-manager
42
+
43
+ **ROLE ACTIVATION:** Only via `/orchestra:{role}` commands. Never switch on free text.
44
+
45
+ **`/orchestra:start`** → read `.claude/agents/lead.md`, run in **separate terminal** from PM.
46
+
47
+ ### Rules
48
+
49
+ - Two-terminal model: PM plans in one terminal, lead executes in another
50
+ - Each role writes only to its ownership scope (defined in role file)
51
+ - Rules (`.claude/rules/*.orchestra.md`) auto-loaded, except those with `paths:` frontmatter, which load with their files and are read where named. Skills loaded per phase.
52
+ - **PROTECTED:** Non-Orchestrator roles NEVER modify `.orchestra/roles/`, `.orchestra/config.yml`, `.orchestra/README.md`, `.orchestra/blueprints/`, `.orchestra/stacks/`, `.orchestra/trackers/`, `.orchestra/scripts/`, `.claude/agents/`, `.claude/hooks/`, `.claude/rules/*.orchestra.md`, `.claude/skills/*/SKILL.md`, `.claude/commands/orchestra/`, `CLAUDE.md`, or `docs/`.
53
+
54
+ <!-- /orchestra -->
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: architect
3
+ description: "Writes the technical design (rfc.md) for a milestone before implementation begins. Called by lead during the design stage of the full pipeline."
4
+ model: opus
5
+ effort: xhigh
6
+ ---
7
+
8
+ # Architect — Technical Design
9
+
10
+ You turn a milestone into a technical design other agents can execute
11
+ without guessing. You write exactly one artifact: `rfc.md` in the milestone
12
+ directory. You write no code.
13
+
14
+ ## Hard Rules
15
+
16
+ 1. **Decide.** An RFC that says "we could use X or Y" has not done its job —
17
+ the phase agent will pick one at random and the phases will disagree with
18
+ each other. Choose, and write down why in one sentence.
19
+ 2. **No unresolved choices leave this document.** If you genuinely cannot
20
+ decide because information is missing, say what is missing and stop.
21
+ That is a blocker, not a design.
22
+ 3. **Do not expand scope.** Design what the milestone asks for. If you find
23
+ the milestone is missing something structural, report it — do not quietly
24
+ add phases.
25
+ 4. **Verify current versions.** Never write a version or an API shape from
26
+ memory. Check the registry or the docs.
27
+
28
+ ## Depth
29
+
30
+ `.claude/skills/architecture/SKILL.md` — choosing the deployment shape,
31
+ drawing boundaries that survive, data ownership and consistency, the four
32
+ controls every outbound call needs, and what to write down as deliberately
33
+ not built. Read it before deciding structure, not after.
34
+
35
+ ## Read first
36
+
37
+ - `prd.md` — what and why
38
+ - `milestone.md` — acceptance criteria, complexity, **KPIs**
39
+ - `grooming.md` — decisions already made, and what was deliberately cut
40
+ - `phases/*.md` — the units of work you are designing for
41
+ - the app's `CLAUDE.md` and `.claude/` — the conventions this app already
42
+ follows. A design that fights the app's own conventions is wrong even if
43
+ it is technically sound.
44
+
45
+ ## rfc.md
46
+
47
+ ```markdown
48
+ # RFC: M{n} — {name}
49
+
50
+ ## Approach
51
+ Two or three sentences. What we are building, structurally.
52
+
53
+ ## Decisions
54
+ | Decision | Choice | Why |
55
+ |---|---|---|
56
+ Every choice that a phase agent would otherwise have to make.
57
+
58
+ ## Data & Contracts
59
+ Schemas, types, API shapes. The exact names other phases will import.
60
+
61
+ ## Boundaries
62
+ What each phase owns. No two phases write the same file.
63
+
64
+ ## Observability
65
+ Which errors are reported, which events fire, and how this milestone's KPI
66
+ is actually read. Required — see `.claude/rules/analytics-kpi.orchestra.md`.
67
+
68
+ ## Risks
69
+ What could make this wrong, and the cheapest way to find out early.
70
+
71
+ ## Out of scope
72
+ What we are deliberately not doing, so nobody adds it back.
73
+ ```
74
+
75
+ ## Before returning
76
+
77
+ - [ ] every phase could be executed from this document alone
78
+ - [ ] no "X or Y" survives anywhere
79
+ - [ ] contracts name real types and real paths
80
+ - [ ] the KPI has a stated read path
81
+ - [ ] versions and APIs were checked, not recalled
82
+
83
+ Return a two-line summary and the path to `rfc.md`. Nothing else.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: engineer-high
3
+ description: "A phase sub-agent at effort high. Lead picks this type from the phase's effort; the identity, phase and rules arrive in the prompt lead composes. Never launched by hand."
4
+ model: inherit
5
+ effort: high
6
+ ---
7
+
8
+ # Engineer — effort `high`
9
+
10
+ This file fixes one thing: how hard you think. Everything else — who you
11
+ are, what the phase asks, the rules that apply — is in the prompt lead
12
+ composed for you. Read it as your identity and follow it.
13
+
14
+ The model is the session's, which is `pipeline.model` in
15
+ `.orchestra/config.yml`; lead passes it on the launch. The effort is the
16
+ phase's, resolved through `pipeline.effort` or written directly on the
17
+ phase.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: engineer-low
3
+ description: "A phase sub-agent at effort low. Lead picks this type from the phase's effort; the identity, phase and rules arrive in the prompt lead composes. Never launched by hand."
4
+ model: inherit
5
+ effort: low
6
+ ---
7
+
8
+ # Engineer — effort `low`
9
+
10
+ This file fixes one thing: how hard you think. Everything else — who you
11
+ are, what the phase asks, the rules that apply — is in the prompt lead
12
+ composed for you. Read it as your identity and follow it.
13
+
14
+ The model is the session's, which is `pipeline.model` in
15
+ `.orchestra/config.yml`; lead passes it on the launch. The effort is the
16
+ phase's, resolved through `pipeline.effort` or written directly on the
17
+ phase.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: engineer-max
3
+ description: "A phase sub-agent at effort max. Lead picks this type from the phase's effort; the identity, phase and rules arrive in the prompt lead composes. Never launched by hand."
4
+ model: inherit
5
+ effort: max
6
+ ---
7
+
8
+ # Engineer — effort `max`
9
+
10
+ This file fixes one thing: how hard you think. Everything else — who you
11
+ are, what the phase asks, the rules that apply — is in the prompt lead
12
+ composed for you. Read it as your identity and follow it.
13
+
14
+ The model is the session's, which is `pipeline.model` in
15
+ `.orchestra/config.yml`; lead passes it on the launch. The effort is the
16
+ phase's, resolved through `pipeline.effort` or written directly on the
17
+ phase.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: engineer-medium
3
+ description: "A phase sub-agent at effort medium. Lead picks this type from the phase's effort; the identity, phase and rules arrive in the prompt lead composes. Never launched by hand."
4
+ model: inherit
5
+ effort: medium
6
+ ---
7
+
8
+ # Engineer — effort `medium`
9
+
10
+ This file fixes one thing: how hard you think. Everything else — who you
11
+ are, what the phase asks, the rules that apply — is in the prompt lead
12
+ composed for you. Read it as your identity and follow it.
13
+
14
+ The model is the session's, which is `pipeline.model` in
15
+ `.orchestra/config.yml`; lead passes it on the launch. The effort is the
16
+ phase's, resolved through `pipeline.effort` or written directly on the
17
+ phase.