agentic-team 0.0.0-stage → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -2
  3. package/bin/build-template.js +208 -0
  4. package/bin/index.js +1139 -0
  5. package/package.json +41 -4
  6. package/template/.claude-plugin/plugin.json +10 -0
  7. package/template/.orchestra/README.md +401 -0
  8. package/template/.orchestra/blueprints/README.md +87 -0
  9. package/template/.orchestra/blueprints/api-only.md +46 -0
  10. package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
  11. package/template/.orchestra/blueprints/saas-starter.md +69 -0
  12. package/template/.orchestra/config.yml +277 -0
  13. package/template/.orchestra/roles/orchestrator.md +105 -0
  14. package/template/.orchestra/roles/product-manager.md +438 -0
  15. package/template/.orchestra/scripts/compact-diff.awk +45 -0
  16. package/template/.orchestra/stacks/README.md +81 -0
  17. package/template/.orchestra/stacks/_lib/common.sh +3095 -0
  18. package/template/.orchestra/stacks/_repo/setup.sh +1109 -0
  19. package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
  20. package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
  21. package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
  22. package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
  23. package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
  24. package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
  25. package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
  26. package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
  27. package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
  28. package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
  29. package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
  30. package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
  31. package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
  32. package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
  33. package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
  34. package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
  35. package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
  36. package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
  37. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
  38. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
  39. package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
  40. package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
  41. package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
  42. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
  43. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
  44. package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
  45. package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
  46. package/template/.orchestra/trackers/github.sh +467 -0
  47. package/template/CLAUDE.md +54 -0
  48. package/template/agents/architect.md +83 -0
  49. package/template/agents/engineer-high.md +17 -0
  50. package/template/agents/engineer-low.md +17 -0
  51. package/template/agents/engineer-max.md +17 -0
  52. package/template/agents/engineer-medium.md +17 -0
  53. package/template/agents/engineer-xhigh.md +17 -0
  54. package/template/agents/lead.md +870 -0
  55. package/template/agents/milestone-reviewer.md +63 -0
  56. package/template/agents/orchestrator.md +105 -0
  57. package/template/agents/product-manager.md +438 -0
  58. package/template/agents/reviewer.md +301 -0
  59. package/template/agents/scaffolder.md +180 -0
  60. package/template/agents/tracker.md +202 -0
  61. package/template/commands/blueprint.md +23 -0
  62. package/template/commands/create-role.md +64 -0
  63. package/template/commands/export.md +45 -0
  64. package/template/commands/help.md +55 -0
  65. package/template/commands/hotfix.md +12 -0
  66. package/template/commands/orchestrator.md +12 -0
  67. package/template/commands/pm.md +18 -0
  68. package/template/commands/rewind.md +61 -0
  69. package/template/commands/setup.md +14 -0
  70. package/template/commands/start.md +29 -0
  71. package/template/commands/status.md +11 -0
  72. package/template/commands/sync.md +73 -0
  73. package/template/commands/verifier.md +53 -0
  74. package/template/hooks/audit-context.sh +153 -0
  75. package/template/hooks/enforce-role.sh +175 -0
  76. package/template/hooks/protect-system-files.sh +123 -0
  77. package/template/hooks/review-rounds.sh +172 -0
  78. package/template/rules/acceptance-check.orchestra.md +11 -0
  79. package/template/rules/analytics-kpi.orchestra.md +56 -0
  80. package/template/rules/code-standards.orchestra.md +34 -0
  81. package/template/rules/comments.orchestra.md +77 -0
  82. package/template/rules/commit-format.orchestra.md +20 -0
  83. package/template/rules/dependency-upgrade.orchestra.md +112 -0
  84. package/template/rules/design-parity.orchestra.md +170 -0
  85. package/template/rules/observability.orchestra.md +118 -0
  86. package/template/rules/performance.orchestra.md +186 -0
  87. package/template/rules/phase-limits.orchestra.md +3 -0
  88. package/template/rules/pull-request.orchestra.md +213 -0
  89. package/template/rules/role-boundaries.orchestra.md +117 -0
  90. package/template/rules/security.orchestra.md +106 -0
  91. package/template/rules/stuck-detection.orchestra.md +25 -0
  92. package/template/rules/testing-standards.orchestra.md +214 -0
  93. package/template/rules/tracker-authority.orchestra.md +190 -0
  94. package/template/rules/verification-gate.orchestra.md +56 -0
  95. package/template/skills/accessibility/SKILL.md +31 -0
  96. package/template/skills/android-testing/SKILL.md +112 -0
  97. package/template/skills/architecture/SKILL.md +175 -0
  98. package/template/skills/auth-setup/SKILL.md +105 -0
  99. package/template/skills/best-practices/SKILL.md +29 -0
  100. package/template/skills/code-optimizer/SKILL.md +27 -0
  101. package/template/skills/core-web-vitals/SKILL.md +32 -0
  102. package/template/skills/crud-api/SKILL.md +32 -0
  103. package/template/skills/debug/SKILL.md +28 -0
  104. package/template/skills/deployment/SKILL.md +104 -0
  105. package/template/skills/error-handling/SKILL.md +154 -0
  106. package/template/skills/frontend-design/SKILL.md +30 -0
  107. package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
  108. package/template/skills/ios-testing/SKILL.md +100 -0
  109. package/template/skills/mobile-testing/SKILL.md +102 -0
  110. package/template/skills/observability/SKILL.md +91 -0
  111. package/template/skills/project-structure/SKILL.md +114 -0
  112. package/template/skills/push-notifications/SKILL.md +146 -0
  113. package/template/skills/react-best-practices/SKILL.md +31 -0
  114. package/template/skills/realtime/SKILL.md +146 -0
  115. package/template/skills/review/SKILL.md +82 -0
  116. package/template/skills/testing/SKILL.md +44 -0
  117. package/template/skills/web-quality-audit/SKILL.md +47 -0
  118. package/template/skills/web-testing/SKILL.md +120 -0
@@ -0,0 +1,213 @@
1
+ # Pull Requests
2
+
3
+ **One task, one PR.** Always — there is no mode in which work reaches the
4
+ user any other way.
5
+
6
+ One per milestone is the failure this rule exists to prevent, and it happened
7
+ anyway: the diff spans every phase, no reviewer can hold it, and the design
8
+ table has one row while the milestone has ten screens — so the screenshots get
9
+ dropped and nobody notices. A PR is one task, one commit, one screen.
10
+
11
+ ## Body
12
+
13
+ Use the repo's PR template. Keep it short — the reader wants to know what
14
+ changed and whether it is safe, not how you got there.
15
+
16
+ - **What** — one or two sentences
17
+ - **Why** — link the milestone or task
18
+ - **Design** — required for any UI change: expected and implemented, side by
19
+ side, in the template's table. Any deliberate difference is stated with
20
+ its reason. "Close enough" is not a reason.
21
+
22
+ One PR is one task is one screen, so the table has one row and an empty
23
+ cell has no excuse. The implemented image is the task's `design_capture`,
24
+ committed under `designs/.captures/` **in this branch** — `gh` cannot
25
+ upload an image, so a screenshot outside the branch cannot appear here. If
26
+ it could not be captured, the cell carries the reason. An empty cell reads
27
+ as a pass nobody observed.
28
+ - **Verification** — the checklist, actually checked
29
+
30
+ Do not paste the diff into the description. Do not narrate your process.
31
+ Do not paste the acceptance criteria. Read the body back as the colleague
32
+ who will review it: a table of files, a `path:line`, a list of what was
33
+ checked — those came from a tool, and the reader wanted a person.
34
+
35
+ ## Mechanism
36
+
37
+ The `gh` CLI. Check it before relying on it:
38
+
39
+ ```bash
40
+ gh auth status # not authenticated → stop and tell the user, do not work around it
41
+ ```
42
+
43
+ Open with a body file, never an inline string — the body contains newlines,
44
+ a table, and backticks:
45
+
46
+ ```bash
47
+ gh pr create --title "#<task-id> - <subject>" --body-file .git/PR_BODY.md
48
+ ```
49
+
50
+ ## Naming
51
+
52
+ | | Shape | Example |
53
+ |---|---|---|
54
+ | Branch | `<git-user>/<task-id>-<slug>` | `sulhadin/123-add-login` |
55
+ | PR title | `#<task-id> - <subject>` | `#123 - Add login` |
56
+
57
+ The id goes in both so the board finds the work without anyone pasting a link,
58
+ and it is the tracker's own id — a GitHub issue number or a ClickUp task id. With `tracker.provider: none` there is no id, so the branch drops
59
+ that segment and the title is the subject alone.
60
+
61
+ **The title is not conventional-commit form**, and that is deliberate. A task
62
+ PR accumulates commits — the implementation, then whatever review asked for —
63
+ and each of those *is* conventional. The squash collapses them, and its
64
+ subject is the one place the board's id has to appear.
65
+
66
+ That squashed subject still passes the scaffolded `commit-msg` hook, which
67
+ accepts `#<task-id> - <subject>` alongside the conventional form for exactly
68
+ this case. Without that, the delivery flow would produce the one commit the
69
+ repo's own gate rejects.
70
+
71
+ Record the returned URL. Lead reports it and stores it in `context.md`, so a
72
+ later session can find the PR without searching.
73
+
74
+ Reading the change is `gh pr diff <n>`, and a diff read straight into
75
+ context spends four lines per file on the `diff --git`/`index`/`---`/`+++`
76
+ header. Pipe it — or any `git diff` / `git show` — through
77
+ `.orchestra/scripts/compact-diff.awk`; it replaces those four with one and
78
+ touches nothing else, so every hunk line comes through as git wrote it.
79
+
80
+ Reading review feedback is also `gh`, and it is two different calls — a
81
+ review body and inline line comments are separate objects, and reading only
82
+ one loses half the feedback:
83
+
84
+ ```bash
85
+ gh pr view <n> --json reviews,comments
86
+ gh api repos/{owner}/{repo}/pulls/<n>/comments
87
+ ```
88
+
89
+ Never poll in a loop. Fetch when the user says the review is ready.
90
+
91
+ ## Review cycle
92
+
93
+ The review happens **on the pull request**, not in a file beside it. Findings
94
+ land inline, on the line they are about, and the engineer answers them on the
95
+ thread. Nothing about the conversation lives only on somebody's disk.
96
+
97
+ The loop is: reviewer comments → a fresh engineer with the phase's identity
98
+ fixes, replies and resolves → re-review. Max rounds is `thresholds.stuck_retry_limit`,
99
+ counted by `.claude/hooks/review-rounds.sh`. On exhaustion the user decides
100
+ whether another round is worth it — say what is still open rather than going
101
+ round again.
102
+
103
+ **An engineer answers, not lead.** It reads the code, the diff and the thread
104
+ before it replies. A thread answered from a summary is a thread answered
105
+ wrong. It is a new engineer each round rather than the one that wrote the
106
+ code, because a resumed agent re-reads its whole history on every call.
107
+
108
+ ### Posting the review — one call, every finding
109
+
110
+ ```bash
111
+ cat > .git/REVIEW.json <<'JSON'
112
+ {
113
+ "event": "COMMENT",
114
+ "body": "changes-requested — two or three sentences",
115
+ "comments": [
116
+ { "path": "src/auth/session.ts", "line": 42, "side": "RIGHT",
117
+ "body": "[blocking] The session id is compared with ===, which is not constant time.\n\nUse `crypto.timingSafeEqual` over the two buffers." }
118
+ ]
119
+ }
120
+ JSON
121
+ gh api repos/{owner}/{repo}/pulls/<n>/reviews --input .git/REVIEW.json
122
+ ```
123
+
124
+ One review, not one call per finding — each separate call is a separate
125
+ notification.
126
+
127
+ **`COMMENT`, never `APPROVE` or `REQUEST_CHANGES`.** The same account authored
128
+ the pull request and GitHub refuses to let an author approve their own.
129
+
130
+ So the verdict travels as a **label**, which is the part GitHub will let the
131
+ author set and the part everything else can read — the board, a filter, a
132
+ human scanning the list, and the next agent.
133
+
134
+ | Label | Means |
135
+ |---|---|
136
+ | `review:changes-requested` | unresolved findings; the engineer is up |
137
+ | `review:approved` | nothing blocking; the task can move on |
138
+
139
+ ```bash
140
+ # once per repo, idempotent — the scaffold does this
141
+ gh label create review:changes-requested -c D93F0B -d "Reviewer found blocking issues" -f
142
+ gh label create review:approved -c 0E8A16 -d "Reviewer found nothing blocking" -f
143
+
144
+ # one state at a time, so the two can never both be on
145
+ gh pr edit <n> --add-label review:changes-requested --remove-label review:approved
146
+ ```
147
+
148
+ The label is the handshake between reviewer and engineer, and it is what lead
149
+ reads to decide the next move. The review body still carries the summary, but
150
+ a body has to be parsed and a label does not — and a run that dies between
151
+ rounds can pick up exactly where it left off by asking the pull request what
152
+ state it is in.
153
+
154
+ Every inline comment names the problem in a sentence and says what to do
155
+ instead. A comment that only objects makes the next agent guess.
156
+
157
+ ### Reading the threads
158
+
159
+ REST returns comments; only GraphQL carries the thread id and whether it is
160
+ resolved, and both are needed to answer one.
161
+
162
+ ```bash
163
+ gh api graphql -f query='
164
+ query($o:String!,$r:String!,$n:Int!) {
165
+ repository(owner:$o, name:$r) { pullRequest(number:$n) {
166
+ reviewThreads(first:50) { nodes {
167
+ id isResolved path line
168
+ comments(first:20) { nodes { body author { login } } }
169
+ } } } } }' -f o={owner} -f r={repo} -F n=<n>
170
+ ```
171
+
172
+ ### Answering and resolving
173
+
174
+ Reply on the thread, then resolve it. A few words on what was done and the
175
+ commit id — the diff says the rest.
176
+
177
+ ```bash
178
+ gh api graphql -f query='
179
+ mutation($t:ID!, $b:String!) {
180
+ addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$t, body:$b}) {
181
+ comment { id } } }' -f t=<thread-id> -f b="Fixed with timingSafeEqual — a1b2c3d"
182
+
183
+ gh api graphql -f query='
184
+ mutation($t:ID!) {
185
+ resolveReviewThread(input:{threadId:$t}) { thread { isResolved } } }' -f t=<thread-id>
186
+ ```
187
+
188
+ Resolve only what you actually addressed. A thread resolved without a fix is
189
+ worse than one left open, because the next reader believes it.
190
+
191
+ If you did something other than what was asked, say why in the reply. If you
192
+ disagree, say so and leave the thread open — resolving a disagreement is not
193
+ answering it.
194
+
195
+ Never poll in a loop. A human review is fetched when the user says it is
196
+ ready.
197
+
198
+ ## Who merges
199
+
200
+ Decided by the `--auto` flag on `/orchestra:start`, and by nothing else.
201
+
202
+ | | Who merges | What follows |
203
+ |---|---|---|
204
+ | normal | **the human**, squashing | lead waits; a task whose `depends_on` is unmerged does not start |
205
+ | `--auto` | lead, squash-merging | commit link posted on the tracker task, status moved, branch deleted |
206
+
207
+ Outside `--auto`, **never merge your own PR unless explicitly told to.**
208
+
209
+ **Never stack a branch on an unmerged branch** to avoid waiting. The human
210
+ squashes, so the parent's commits reappear in the child's diff until it is
211
+ rebased, and an agent doing that on every task will get it wrong. Waiting is
212
+ the honest cost of reviewing per task — say what is open and what is blocked
213
+ rather than stalling silently.
@@ -0,0 +1,117 @@
1
+ # Role Boundary Enforcement
2
+
3
+ Before writing ANY file, check your role's ownership scope.
4
+
5
+ ## Ownership Map
6
+
7
+ | Role | Can Write | Everything Else |
8
+ |------|-----------|----------------|
9
+ | Orchestrator | `.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`, `docs/` | Refuse |
10
+ | PM | `.orchestra/milestones/*` (prd, milestone, grooming, phases) | Refuse |
11
+ | Lead | `.orchestra/milestones/*/context.md` | Refuse |
12
+ | Scaffolder | `.orchestra/config.yml` (verification, tracker, observability keys only), scaffolded app files, repo config | Refuse |
13
+ | Tracker | Tracker id frontmatter in `.orchestra/milestones/*` | Refuse |
14
+ | Sub-agents | Only what phase `## Scope` defines | Refuse |
15
+ | Reviewer | Review verdict in phase file only | Refuse |
16
+
17
+ Some entries on the Orchestrator's list are **not** blocked by the hook below,
18
+ each for its own reason. `.orchestra/blueprints/` is where `/orchestra
19
+ blueprint` writes, `.orchestra/config.yml` is merged rather than overwritten,
20
+ and `CLAUDE.md` keeps everything outside the orchestra section for the user —
21
+ the installer preserves all three on upgrade, so an edit there is not lost
22
+ work. `docs/` is not installed into a project at all; it is this repo's own
23
+ documentation, so in an installed copy there is nothing under that name to
24
+ protect and blocking it would only block the user's own docs.
25
+
26
+ They stay on the ownership map because a role should still not reach into
27
+ them. They are simply not things a hook can decide.
28
+
29
+ ## Enforced, not asked for
30
+
31
+ Three `PreToolUse` hooks. The first blocks Write and Edit against installed
32
+ system files. The second holds a session inside the role it declared. The
33
+ third caps review rounds per phase.
34
+
35
+ ### The system-file hook
36
+
37
+ Two questions decide it, and neither is "which role is running": a slash
38
+ command is a markdown prompt and cannot export anything a hook could read on
39
+ its own, so a guard built only on the role would never fire.
40
+
41
+ **Which repo is this?** In the orchestrator repo these files are source, and
42
+ the hook allows everything. Elsewhere they are build output.
43
+
44
+ **Did Orchestra put this file here?** `.orchestra/.installed.json` records
45
+ every file the installer wrote, and the hook blocks exactly that list.
46
+
47
+ The manifest exists because the answer stopped being guessable. Rules carry a
48
+ `.orchestra.md` suffix, so a system rule can be told from one you wrote.
49
+ Skills lost that marker when they became directories, and a list of globs
50
+ cannot tell a shipped skill from yours — so it blocked both, including the
51
+ extension point `docs/skills.md` tells you to use, and including a stack's
52
+ own `apps/<name>/.claude/` pack. The upgrade had the same blindness from the
53
+ other side: a skill this project stops shipping looked like one you wrote, and
54
+ survived forever. The manifest answers both, and retires what a new version no
55
+ longer ships.
56
+
57
+ **`.orchestra/config.yml` is never blocked.** It is merged on upgrade rather
58
+ than overwritten, so an edit survives — and `/orchestra:setup` writes its
59
+ verification keys, which `.orchestra/README.md` has always documented as an
60
+ exception the hook did not implement.
61
+
62
+ Neither are `.orchestra/milestones/`, `blueprints/`, roles you create, or
63
+ skills and rules you write: none of them is in the manifest, because the
64
+ installer preserves them.
65
+
66
+ When it blocks: make the change in the orchestrator repo and reinstall, or —
67
+ if it is genuinely project-specific — put it in the app's own `CLAUDE.md`, a
68
+ project skill of your own, or `config.yml`, none of which is overwritten.
69
+
70
+ ### The role hook
71
+
72
+ The ownership map above was prose, and a PM opened a pull request. So the
73
+ role is now something a hook can read: each role command has the agent run
74
+ one Bash line — `true # orchestra-role=product-manager` — and the hook, which
75
+ sees every Bash call together with the session id Claude Code passes it,
76
+ records the pair under `.orchestra/.sessions/`. Every later call in that
77
+ session is judged against it. Two terminals are two sessions; the PM's marker
78
+ never touches lead.
79
+
80
+ What it refuses for the PM: a Write or Edit outside `.orchestra/milestones/`;
81
+ `gh pr` in any form; a new branch; merge, rebase, cherry-pick, a hard reset,
82
+ a force push; `git commit -a`; `git add` of anything but milestone files;
83
+ any `Agent` launch other than `milestone-reviewer` and `tracker`; any
84
+ `Workflow`; and a ClickUp task write carrying a status other than the
85
+ backlog one. The last three came from one afternoon: the word "ultracode" in
86
+ a message told the platform to use the Workflow tool, and a PM whose role
87
+ said "cannot review code" ran a multi-agent code review, recommended itself
88
+ for it, and opened the task straight into `in progress`. Lead and the
89
+ Orchestrator are recorded and, for now, not restricted — lead's sub-agents
90
+ write code in the same session, and a restriction there would block the
91
+ work it exists to do.
92
+
93
+ Two things are structural rather than hook-judged, because they do not
94
+ depend on the role. `Workflow` is denied in the project's `settings.json`;
95
+ no role runs one. The reviewer and the milestone reviewer carry
96
+ `disallowedTools: [Write, Edit, NotebookEdit]` in their frontmatter; they
97
+ review, and Claude Code removes the tools rather than asking them not to.
98
+
99
+ No marker means no role, and nothing is refused. That is discussion mode.
100
+
101
+ When it blocks, the message says why. Say so to the user and stop; the
102
+ answer is never to find a command the pattern does not match.
103
+
104
+ ### The review-round hook
105
+
106
+ `.claude/hooks/review-rounds.sh` counts reviewer launches per phase, from the
107
+ `Phase:` line lead puts in every reviewer prompt, in
108
+ `.orchestra/.sessions/review-rounds.json`. Past `thresholds.stuck_retry_limit`
109
+ it refuses the launch until the prompt quotes the user's approval. The cap
110
+ was prose, and a run read "loop until clean" as overriding it.
111
+
112
+ ## Rules
113
+
114
+ - Check what file you'd write to, not what the user said
115
+ - If user insists → still refuse: "Outside my scope as {role}. Use /orchestra:{command}."
116
+ - Discussion mode (no role) → no restrictions
117
+ - Reading → always allowed
@@ -0,0 +1,106 @@
1
+ # Security
2
+
3
+ Not a review checklist item — a property of the code as it is written.
4
+
5
+ ## OWASP is the reference
6
+
7
+ When a security question has an OWASP answer, that answer is the one — the
8
+ cheat sheets for how to do a specific thing, the ASVS for what a review
9
+ should ask.
10
+
11
+ It is named here rather than left implicit because "industry standard
12
+ practice" resolves to whatever the model remembers, and what it remembers is
13
+ usually a few years stale. Password storage is the clearest case: the
14
+ recommended algorithm and its parameters have changed more than once, and a
15
+ work factor written from memory is weaker than the one somebody thinks they
16
+ configured.
17
+
18
+ **So it is read at implementation time, not recalled**, the same as a library
19
+ version — see `.claude/rules/dependency-upgrade.orchestra.md`. Quote the
20
+ setting you took and where it came from, in a comment beside the code.
21
+
22
+ Where the guidance offers several acceptable options, choosing among them is
23
+ an engineering decision that gets written down with its reason. The scaffolded
24
+ backend hashes with the 16 MiB scrypt parameters rather than OWASP's primary
25
+ 128 MiB set, because a login route allocating 128 MiB per attempt is the
26
+ cheapest way to exhaust a small container — the comment says exactly that, and
27
+ the numbers behind it were measured.
28
+
29
+ Deviating without a reason is not an option this rule offers.
30
+
31
+ ## Secrets
32
+
33
+ - Never in source. Never in a commit. Never in a log line, an error
34
+ message, or an analytics property.
35
+ - Read from the environment. Fail loudly at boot when a required one is
36
+ missing — a service that starts without its credentials and fails on the
37
+ first request is harder to diagnose than one that refuses to start.
38
+ - `.env` is gitignored in every project, always. `.env.example` is committed,
39
+ and it is **generated from the validating schema**, never written beside it
40
+ — a hand-kept example is wrong the first time somebody adds a variable and
41
+ forgets, and a new engineer trusts it anyway.
42
+ - If a secret is ever committed, rotating it is the fix. Removing the commit
43
+ is not — assume it was scraped the moment it was pushed.
44
+
45
+ **Enforced, not asked for.** gitleaks runs in its own workflow over the full
46
+ history — a secret committed and later deleted is still leaked, so scanning
47
+ the tip would miss every one of those — and in the pre-commit gate over
48
+ staged changes, where it is still cheap to fix.
49
+
50
+ ## The rules a machine can check
51
+
52
+ `scripts/policy-check.mjs` runs in the gate. These are the checks it makes
53
+ that belong to this rule — it decides others too;
54
+ `.claude/rules/performance.orchestra.md` and
55
+ `.claude/rules/comments.orchestra.md` each list their own:
56
+
57
+ | Check | What fails |
58
+ |---|---|
59
+ | `swallowed-error` | a catch that neither handles nor reports |
60
+ | `query-by-testid` | `getByTestId` / `onNodeWithTag` in a test |
61
+ | `mocked-database` | `vi.mock` / `jest.mock` aimed at a db module |
62
+ | `interpolated-sql` | a template literal passed to query/execute/raw |
63
+
64
+ Only checks that can be decided precisely live there. **Authorization on a
65
+ route and PII in a log line cannot be**, so they stay review items below
66
+ rather than becoming a check that cries wolf — a gate people learn to
67
+ override is worse than no gate.
68
+
69
+ ## Input
70
+
71
+ - Validate at the boundary, before anything else touches it.
72
+ - Parameterised queries only. String-built SQL is a defect regardless of
73
+ where the input came from.
74
+ - Escape on output according to the sink — HTML, SQL, and a shell command
75
+ need different treatment, and "it was already validated" is not one of
76
+ them.
77
+ - Size limits on uploads and request bodies.
78
+
79
+ ## Authorisation
80
+
81
+ - Checked on the route, not in a helper a future route can forget to call.
82
+ - Deny by default. A new endpoint is closed until it is deliberately opened.
83
+ - Never trust a client-supplied id to imply ownership — verify it.
84
+
85
+ ## Dependencies
86
+
87
+ - Anything added is checked for maintenance, per
88
+ `.claude/rules/dependency-upgrade.orchestra.md`.
89
+ - Audit runs in CI. A known critical vulnerability blocks the merge.
90
+ - Prefer the platform over a package. The safest dependency is the one you
91
+ did not add.
92
+
93
+ ## Client-side
94
+
95
+ - No secret ever reaches a client bundle. Anything in the browser or the app
96
+ binary is public.
97
+ - Content Security Policy on web.
98
+ - Certificate pinning where the threat model calls for it, not by reflex.
99
+
100
+ ## Data
101
+
102
+ - Collect the minimum that answers the question.
103
+ - PII never lands in logs, errors, or analytics — see
104
+ `.claude/rules/observability.orchestra.md`.
105
+ - Encrypt in transit, always. At rest wherever the data would matter if the
106
+ disk were taken.
@@ -0,0 +1,25 @@
1
+ # Stuck Detection & Recovery
2
+
3
+ You are **stuck** when any of these happen:
4
+ - **Same error N times** (N = config.yml `thresholds.stuck_retry_limit`, default 3)
5
+ - **Circular fix** — fixing issue A creates issue B, fixing B recreates A
6
+ - **Verification loop** — verification gate fails max retries on the same check
7
+ - **No progress** — reading files and running commands but not making meaningful code changes
8
+
9
+ ## Recovery Protocol
10
+
11
+ 1. **STOP immediately.** Do not attempt another fix.
12
+ 2. **Log** in context.md: phase name, symptom, attempts, root cause hypothesis.
13
+ 3. **Try a different approach** (ONE attempt):
14
+ - Same code fix failed? → entirely different implementation strategy
15
+ - Dependency broken? → alternative library
16
+ - Tests fail due to environment? → isolate the test
17
+ 4. **If different approach also fails** → escalate to user with options:
18
+ - Try specific alternative
19
+ - Skip this phase and continue
20
+ - Stop execution
21
+
22
+ ## Auto-Recovery (--auto mode)
23
+
24
+ Try the different approach (step 3) automatically.
25
+ If that also fails → set phase to `failed`, log everything, move to next phase.
@@ -0,0 +1,214 @@
1
+ # Testing Standards
2
+
3
+ **Code without tests is not done.** Write them as part of implementation,
4
+ never as a follow-up phase.
5
+
6
+ ## The rule that decides what to test
7
+
8
+ > If one of our functions calls into a third-party library, that function
9
+ > has a test. When a dependency is bumped and its behaviour changes, the
10
+ > test must fail.
11
+
12
+ A green suite is the merge signal for dependency upgrades. That only works
13
+ if the suite covers the seams where our code meets someone else's.
14
+
15
+ Snapshot tests do not satisfy this. A snapshot passes while a handler
16
+ silently stops firing — assert behaviour, not markup.
17
+
18
+ ## Backend — the shape
19
+
20
+ The test pyramid is wrong for a backend. It comes from an era when starting
21
+ a database took minutes. A backend is mostly plumbing — parse, validate,
22
+ query, transform, respond — and the bugs live in the seams, not in pure
23
+ functions.
24
+
25
+ | Layer | How much | What |
26
+ |---|---|---|
27
+ | Unit | few | Pure logic only: date math, pricing, permissions, state machines. No I/O. |
28
+ | **Integration** | **most of the suite** | Route level, through the real app, against a real database. |
29
+ | External services | as needed | Faked at the HTTP boundary. |
30
+ | E2E | a handful | Smoke: it boots, health is honest, signup and login work. |
31
+
32
+ A route-level test covers routing, middleware, validation, auth, the
33
+ handler, the query and serialisation in one assertion path. That is where
34
+ the value is.
35
+
36
+ ### Never mock the database
37
+
38
+ Mock it and you test your own mock. A fake repository returns exactly what
39
+ you told it to; it cannot tell you the `WHERE` clause is wrong, the
40
+ constraint is missing, or the transaction does not roll back.
41
+
42
+ The database driver is a third-party library, so the rule above already
43
+ forbids this.
44
+
45
+ - Real database, in a container, **same major version as production**.
46
+ - Never SQLite standing in for Postgres — different dialect, different
47
+ types, different constraint behaviour. You would be testing a database
48
+ you do not ship.
49
+ - Run migrations against it. Migration testing comes free.
50
+ - Isolation: each test in a transaction that is rolled back, or truncate
51
+ between tests. Never rely on ordering.
52
+ - Each test creates the data it needs. A shared seed fixture is what makes
53
+ a suite unmaintainable somewhere around test three hundred.
54
+
55
+ ### Fake external services at the network boundary
56
+
57
+ Intercept the HTTP call — MSW or a stub server. Do **not** mock your own
58
+ `sendEmail()`.
59
+
60
+ Mocking your own wrapper means the wrapper is never tested, and the wrapper
61
+ is exactly where a provider's API change bites you.
62
+
63
+ ### Test data
64
+
65
+ Synthetic, built by factories, created per test. Never a copy of production
66
+ data — it carries PII and it drifts.
67
+
68
+ ## Coverage is a diagnostic, not a target
69
+
70
+ A single global percentage stops measuring test quality the moment it
71
+ becomes a gate. It measures test quantity instead, and it treats a design
72
+ token file as equal to the module wrapping your database driver.
73
+
74
+ Four gates replace it:
75
+
76
+ | Gate | Value |
77
+ |---|---|
78
+ | Global coverage | **ratchet** — may never fall below what the branch already had |
79
+ | Branch coverage of the lines this change touched | `testing.coverage.diff_branch_threshold` |
80
+ | Modules that wrap a third-party library | **100%** |
81
+ | Type-only files, tokens, generated code, entry points | excluded, not counted |
82
+
83
+ The diff gate is the one that changes behaviour. It asks about the code
84
+ being written now rather than the historical average.
85
+
86
+ The seam gate is live from the first commit, because the location is a
87
+ convention: anything wrapping a third party lives in
88
+ `src/integrations/<provider>/`, so the glob is known before the first wrapper
89
+ exists. It asks that the wrapper be **exercised**, not that a test file sit
90
+ beside it — a route test driving it through a faked HTTP boundary satisfies
91
+ it. Depth in `.claude/skills/project-structure/SKILL.md`.
92
+
93
+ Do not chase the global number with unit tests. If the integration tests
94
+ are real, coverage arrives on its own. If tests are being written to move a
95
+ percentage, the gate has started doing harm — say so rather than complying.
96
+
97
+ ## Mutation testing
98
+
99
+ Coverage says a line ran. Mutation says the line was **checked**.
100
+
101
+ Stryker rewrites the code one small change at a time — `>` becomes `>=`,
102
+ a return value is replaced — and runs the suite. If the tests still pass,
103
+ nothing was actually asserting that behaviour.
104
+
105
+ Runs on every pull request, over the files that pull request changed
106
+ (`--mutate` with the changed paths, plus `--incremental`). A full run over
107
+ everything is a scheduled job, not a gate — it takes hours.
108
+
109
+ A suite at 95% line coverage with a 40% mutation score is decorative.
110
+
111
+ Stryker reads the TypeScript compiler's JS API, which TypeScript 7 removed.
112
+ On an app using TS 7 the scaffolder skips the install and says so — do not
113
+ work around it by pinning TypeScript backwards. The gate turns on by itself
114
+ once Stryker supports the new compiler.
115
+
116
+ ## Web — jsdom is not a browser
117
+
118
+ It has no layout engine. `getBoundingClientRect` returns zeros, nothing
119
+ scrolls, CSS is never applied, focus order is approximated. A component can
120
+ be invisible, unclickable or overlapping another and every jsdom test still
121
+ passes.
122
+
123
+ Component tests therefore run in **real Chromium** via Vitest browser mode.
124
+ Do not swap in jsdom because a test is awkward — the awkwardness is usually
125
+ the bug.
126
+
127
+ Query by role, never by test id: Testing Library resolves roles from the
128
+ accessible tree, so a div-with-onClick fails the test and accessibility
129
+ becomes something the suite enforces rather than something review notices.
130
+
131
+ **axe runs as an assertion**, at two levels, because they see different
132
+ failures. In a component test it checks the mounted subtree; in
133
+ `e2e/a11y.spec.ts` it checks the built page, which is the only place contrast
134
+ against the real background, landmark structure and heading order exist at
135
+ all. Both are wired by the stack — accessibility is not a review checklist
136
+ item here.
137
+
138
+ Depth in `.claude/skills/web-testing/SKILL.md`.
139
+
140
+ ## Mobile is a different shape
141
+
142
+ Jest does not run React Native — `jest-expo` runs components in Node against
143
+ mocked native modules. The whole JS suite can be green while the app fails
144
+ to build, crashes on launch, or works on one platform and not the other.
145
+
146
+ So the device layer is not optional there, and two commands belong in the
147
+ gate because nothing else can see what they see:
148
+
149
+ ```
150
+ expo-doctor
151
+ expo install --check
152
+ ```
153
+
154
+ Depth in `.claude/skills/mobile-testing/SKILL.md`. Native iOS and Android
155
+ have their own shapes again — `.claude/skills/ios-testing/SKILL.md` and
156
+ `.claude/skills/android-testing/SKILL.md`.
157
+
158
+ ## Native has two more shapes
159
+
160
+ **iOS:** a green unit suite does not prove the app launches. A missing
161
+ Info.plist key, an entitlement the profile lacks, a `@main` that throws
162
+ before the first view — no XCTest sees any of them. The launch test is the
163
+ first test worth writing, not the last.
164
+
165
+ **Android:** a passing debug build proves little about the release build. R8
166
+ shrinks and obfuscates, a keep rule nobody wrote removes a class loaded by
167
+ name, and the release APK crashes on launch with every test green. So
168
+ `assembleRelease` is in the gate.
169
+
170
+ Both query the accessibility tree rather than an identifier — XCUITest
171
+ through accessible names, Compose through the semantics tree — for the same
172
+ reason web and React Native query by role: it makes the suite enforce
173
+ accessibility instead of review noticing it.
174
+
175
+ Depth in `.claude/skills/ios-testing/SKILL.md` and
176
+ `.claude/skills/android-testing/SKILL.md`.
177
+
178
+ ## The bundle has a budget, and it is a ratchet
179
+
180
+ Same shape as the coverage ratchet: the committed baseline is whatever the
181
+ build actually produced. It may fall and it may not rise.
182
+
183
+ Nobody has to defend "why 180kB", and the build still fails the day a stray
184
+ import pulls in a date library. Measured gzipped, because that is what a
185
+ browser downloads.
186
+
187
+ Deliberate growth is re-recorded on purpose, in its own commit, so it is a
188
+ decision somebody made rather than a number that drifted.
189
+
190
+ ## A build is part of the suite
191
+
192
+ `tsc --noEmit` accepts an app that fails to bundle, and neither tests nor
193
+ lint ever load the bundler. The gate runs `build` for that reason, and a
194
+ failure is fixed in the code — never by loosening the config.
195
+
196
+ ## Dependency upgrades
197
+
198
+ - **minor and patch** green → merge, no changelog needed. That is what the
199
+ seam rule buys.
200
+ - **major** → a human reads it. A major version is the maintainer stating
201
+ the contract changed, and some of what changed — default timeouts, retry
202
+ behaviour, performance — is not visible to any test.
203
+
204
+ Never loosen a test to make a dependency PR green. See
205
+ `.claude/rules/dependency-upgrade.orchestra.md`.
206
+
207
+ ## Always
208
+
209
+ - Cover happy path, edge cases, and failure paths — a funnel that only
210
+ records success cannot tell you why people left.
211
+ - Match the project's existing test style and naming.
212
+ - Run the tests, then break the code and confirm they fail. A test that has
213
+ never failed has not been tested.
214
+ - One assertion per concept. The test name states the scenario.