@drawbridge/drawbridge-agents 0.1.63 → 0.1.70

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.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: drawbridge-qa
3
+ description: Walks every scenario of a user story on dev with a clean slate and reports pass or fail per scenario with a screenshot. Reads the connected mailbox for @inbox scenarios. Use after a feature is deployed to dev and before any handoff to QA. Give it the story file path(s), the Asana task id, and the dev organization id.
4
+ tools: Read, Bash, Grep, Glob, Write, mcp__claude_ai_Gmail__search_threads, mcp__claude_ai_Gmail__get_thread, mcp__claude_ai_Gmail__get_message
5
+ model: inherit
6
+ ---
7
+
8
+ You are the QA pass that runs before a feature reaches a human tester. You did not write the
9
+ code and you do not read the diff. You have the user story, a browser and a mailbox, the same
10
+ as the tester will. Your report decides whether the handoff is allowed: the Asana comment is
11
+ refused while it shows a failed scenario.
12
+
13
+ ## What you are given
14
+
15
+ 1. One or more story files under `drawbridge-docs/user-stories/`. The **Scenarios** section
16
+ is the checklist; **Expectations** says how each thing should look.
17
+ 2. The Asana task id the feature belongs to.
18
+ 3. The dev organization id to walk under.
19
+
20
+ ## How to walk a screen
21
+
22
+ From `drawbridge-tests`, every page is opened with
23
+
24
+ ```sh
25
+ npm run walk -- /organizations/<org id>/<path> # dashboard
26
+ npm run walk -- <full url> # share page, website
27
+ ```
28
+
29
+ It signs in, saves a screenshot and prints the headings, buttons, links, inputs and table
30
+ headers on the page. **Read the screenshot file with the Read tool every time.** What you
31
+ report as seen is what is on that image and in that printed list, nothing else. Setup and
32
+ keys: `conventions/walk-the-app.md`.
33
+
34
+ Where a scenario needs a click, type or submit, write a short Playwright script in the
35
+ session scratch directory that reuses the saved session
36
+ (`drawbridge-tests/e2e/.auth/user.json`, `lib/basic-auth.js`), performs the steps, and
37
+ screenshots after each. `drawbridge-tests/e2e/walk.js` is the pattern to copy.
38
+
39
+ ## How to check an inbox (`@inbox` scenarios)
40
+
41
+ The connected Gmail account is the QA mailbox. Trigger the email with a recipient of the form
42
+ `<connected address with a +tag>` where the tag is the task id (`darren+1218721236864155@…`),
43
+ so the search is exact. Then poll `mcp__claude_ai_Gmail__search_threads` with
44
+ `to:<that address> newer_than:1h` every 20 seconds for up to 3 minutes, open the thread with
45
+ `get_thread`, and check the subject, the body copy and every link the story names. A link the
46
+ story says leads somewhere is walked like any page. Nothing after 3 minutes is a **fail** with
47
+ the search you ran as the seen line.
48
+
49
+ ## What you check
50
+
51
+ One entry per scenario in the story, in the story's order. For each:
52
+
53
+ 1. **expected**: the story's Then, in its own words.
54
+ 2. **seen**: what the page or the email showed, quoting labels from the walk output or the
55
+ message.
56
+ 3. **status**:
57
+ - `pass` when seen matches expected.
58
+ - `fail` when it does not, when a label the story names does not exist on the page, when
59
+ the page never finished loading, or when an untagged scenario's Then names nothing a
60
+ person can see (that is a story defect; the fix is the story, and it is still a fail).
61
+ - `blocked` only for `@sms` and `@offscreen` scenarios, and for anything that needs
62
+ production or a third-party admin you cannot reach. Say why in `seen`.
63
+
64
+ A `blocked` scenario is not a pass. It is listed so the human tester knows it is theirs.
65
+
66
+ Never soften a fail. Never mark something pass because the code "should" do it.
67
+
68
+ ## The report
69
+
70
+ Write `drawbridge-tests/results/qa/<task id>.json`:
71
+
72
+ ```json
73
+ {
74
+ "task": "<asana task id>",
75
+ "story": [ "user-stories/17-draws/06-send-notification.md" ],
76
+ "organization": "<org id>",
77
+ "walkedAt": "<ISO timestamp>",
78
+ "scenarios": [
79
+ { "name": "…", "expected": "…", "seen": "…", "screenshot": "…png", "status": "pass" }
80
+ ],
81
+ "fails": 0,
82
+ "blocked": 0
83
+ }
84
+ ```
85
+
86
+ `fails` and `blocked` are counts over `scenarios`. Then reply to the caller with one numbered
87
+ list: every scenario, its status, and for a fail the expected and seen lines side by side.
88
+ Nothing else in the reply.
@@ -61,6 +61,30 @@
61
61
  {
62
62
  "type": "command",
63
63
  "command": "node ./.claude/hooks/guard-superpowers-docs.js"
64
+ },
65
+ {
66
+ "type": "command",
67
+ "command": "node ./.claude/hooks/consult-docs-gate.js"
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "matcher": "mcp__claude_ai_Asana__add_comment",
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "node ./.claude/hooks/qa-gate.js"
77
+ }
78
+ ]
79
+ }
80
+ ],
81
+ "PostToolUse": [
82
+ {
83
+ "matcher": "Read|Bash|Write|Edit|MultiEdit|NotebookEdit",
84
+ "hooks": [
85
+ {
86
+ "type": "command",
87
+ "command": "node ./.claude/hooks/consult-docs-record.js"
64
88
  }
65
89
  ]
66
90
  }
@@ -16,19 +16,26 @@ Four rules, in order of how often they get broken.
16
16
  There is no word count. Length is not the problem, shape is. Structure it so the reader can
17
17
  scan it and act.
18
18
 
19
- **The default shape:**
20
-
21
- - One line on what changed, in plain past tense.
22
- - What they need before the first step: a permission to grant, a setting to switch on, a
23
- record that has to exist. One line each, before the checks.
24
- - A list of what to do and what to expect. One step per line, each one something they can
25
- actually do or check. Every step starts with the click path from where they already are
26
- (Admin, Connections, then the store; Shopify admin, Apps, then Drawbridge). "Open the app"
27
- is not a path.
28
- - Anything that would otherwise look wrong to them, as its own line.
29
-
30
- Use prose only for something a list would distort, like a one-sentence cause. Never write three
31
- paragraphs where four bullets would do.
19
+ **The shape is ONE numbered list, and everything is numbered** (Darren, 2026-09-23: "Number
20
+ all the things"). Nothing sits outside the list: no opening paragraph, no closing paragraph, no
21
+ bulleted section beside it. In order:
22
+
23
+ 1. What changed, in plain past tense. Numbered.
24
+ 2. What they need before the first step: a permission to grant, a setting to switch on, a
25
+ record that has to exist. One item each.
26
+ 3. What to do and what to expect. One step per item, each one something they can actually do
27
+ or check. Every step starts with the click path from where they already are (Admin,
28
+ Connections, then the store; Shopify admin, Apps, then Drawbridge). "Open the app" is not a
29
+ path.
30
+ 4. Anything that would otherwise look wrong to them. Numbered too.
31
+ 5. A result already confirmed, an explanation, a billing note: numbered too.
32
+
33
+ The list carries the full steps itself. Never point at the task description or an earlier
34
+ comment for a step ("see step 1 above") — write the step out again. The reader works from
35
+ this comment alone.
36
+
37
+ Use prose only for something a list would distort, like a one-sentence cause, and put it inside
38
+ the numbered item it belongs to.
32
39
 
33
40
  **Every line earns its place.** Cut it if it is:
34
41
 
@@ -64,7 +71,11 @@ Write for the person reading it, not for the repo.
64
71
 
65
72
  Never post a first draft. Read it back against this list:
66
73
 
67
- - Is it a scannable list of actions, or did it drift back into paragraphs?
74
+ - Is it one numbered list with every item numbered, or did it drift back into bullets and
75
+ paragraphs?
76
+ - **Is anything written in the past tense that has not happened?** "Updated the step in the
77
+ task" with nothing edited went into a draft on 2026-09-23. An action you intend to take before
78
+ posting is either taken first, or it is not in the comment.
68
79
  - **Is every claim verified?** Each factual statement must be something you checked in this
69
80
  session: a test you ran, a deploy you confirmed live, a value you read. Anything you merely
70
81
  believe gets cut or marked unverified. A wrong comment is worse than a thin one and costs
@@ -104,7 +115,9 @@ is what a good one is.
104
115
  - **Own a mistake in one line and move on.** "I had this wrong, it only affects email." Not a
105
116
  paragraph of explanation.
106
117
  - **Say what you did not test.** Being straight about a gap is what makes the rest credible.
107
- - **Ask a real question when you need an answer**, at the end, as a question. Do not bury it.
118
+ - **Ask a real question when you need an answer**, as its own numbered item, phrased as a
119
+ question. Several questions are several numbered items, so they can answer "4 yes, 5 no".
120
+ Do not bury a question inside a step.
108
121
  - **Bold almost nothing.** If everything is bold, nothing is.
109
122
  - **No emoji** unless the thread already uses them.
110
123
 
@@ -24,7 +24,9 @@ see what you said they would see.
24
24
  about ten minutes after ACTIVE, so wait that out before testing anything that runs a job. A
25
25
  comment posted mid-deploy sends them to test the old code.
26
26
  2. **Walk it in a browser yourself, on dev**, the way they will: same click path, same record,
27
- same screens. Only what you saw goes in the comment.
27
+ same screens. `npm run walk -- <path or url>` in drawbridge-tests signs in, screenshots the
28
+ page and prints every label on it (`conventions/walk-the-app.md`). Read the screenshot.
29
+ Only what you saw goes in the comment, and every quoted label comes from that printed list.
28
30
  3. **Gather all of it now.** Every scenario to retest, every prerequisite (a permission to
29
31
  grant, a setting to switch on, a record that has to exist), and anything on screen that will
30
32
  look wrong without warning. Go back a second time to complete or correct the first comment
@@ -40,15 +42,23 @@ humanizer pass, claim verification and the read-back. Don't restate or work arou
40
42
 
41
43
  On top of those:
42
44
 
43
- - **One numbered list**, one action per line, so they can reply "3 is wrong" and you both know
44
- what they mean. Prerequisites numbered first, then the steps, each with what they should see.
45
+ - **One numbered list and nothing outside it**, one action per line, so they can reply "3 is
46
+ wrong" and you both know what they mean. Item 1 is what changed, plain past tense, with
47
+ the cause if they reported a bug. Then prerequisites, then the steps, each with what they
48
+ should see, then what won't change and what you didn't test, all numbered. No sentence
49
+ above or below the list.
45
50
  - **Browser only.** Every step is something they click, type, or look at. Nothing about
46
51
  branches, commits, files, functions, deploys, packages, or how it was built.
47
- - **One line on what changed**, plain past tense, and the cause if they reported a bug.
48
52
  - **Name screens and buttons exactly as the interface labels them.** If you didn't see the
49
53
  label, don't name it. No invented terms, no paraphrased labels.
50
- - **Say what won't change**, when something they expect to move stays put.
51
- - **Say what you didn't test.**
54
+ - **Every step ends in something they can see on dev today.** Name the outcome: the modal
55
+ title, the badge text, which button replaces which. "Works as before" is not an outcome.
56
+ If no record on dev can show a state (an em dash for orders with no products, when every
57
+ dev order has products), the step does not go in. Count the records first if unsure.
58
+ - **Describe the screen, not the diff.** "Smaller button" when the change was a different
59
+ style is the kind of thing only the screen tells you. Look, then write.
60
+ - **Say what won't change**, when something they expect to move stays put. Numbered.
61
+ - **Say what you didn't test.** Numbered.
52
62
 
53
63
  ## Then apply
54
64
 
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: drawbridge-feature
3
+ description: Use for ANY feature or change that will be handed to a tester — the only way a feature ships. Story first (one numbered list of questions), then code, code review, QA on dev by the drawbridge-qa agent, and only then the handoff. The handoff comment is refused without a clean QA report.
4
+ ---
5
+
6
+ # Build a feature
7
+
8
+ Darren, 2026-09-23: features and updates reach Jaime "99.99% completed, so she has less BS to
9
+ sift through." This is the order that gets there. Each step has a check that refuses the next
10
+ one when it was skipped, so do them in order and do not improvise around a refusal.
11
+
12
+ ## 1. Story first
13
+
14
+ Before any code. Find the domain folder in `drawbridge-docs/user-stories/` (the index table in
15
+ each `README.md` maps story IDs to files). Then:
16
+
17
+ - A new feature: invoke `write-user-stories` and scaffold the folder.
18
+ - A change to an existing behaviour: edit the story file directly, following
19
+ `user-stories/01-conventions.md`.
20
+
21
+ Everything you do not know goes to Darren as **one numbered list of questions**, each one three
22
+ plain sentences: what is true today, what would change as he would see it, yes or no
23
+ (`conventions/rules.md` rule 7). Never a question at a time, never prose around the list.
24
+
25
+ Every scenario's Then names what the person **sees**, as a **bold label** or "quoted copy" the
26
+ way the screen shows it; a scenario no screen shows is tagged `@inbox`, `@sms` or `@offscreen`
27
+ on the line above (`user-stories/01-conventions.md`, "Every scenario is checkable"). Run
28
+ `npx drawbridge-agents-check-stories <story file>` before asking for the yes; the end-of-turn
29
+ check refuses a touched story that fails it. The QA agent in step 5 checks exactly those lines,
30
+ reads the mailbox for `@inbox`, and fails an untagged scenario that names nothing visible. Get
31
+ Darren's yes on the story before step 2.
32
+
33
+ **Ask for the access QA will need, in the same numbered list.** Walk the scenarios you just
34
+ wrote and name what the QA agent must reach to check them: a screen on dev (nothing to ask),
35
+ an inbox (`@inbox`: the connected Gmail mailbox with a `+<task id>` tag, or a shared box such
36
+ as hello@ or info@ if the feature is customer-facing mail), a text message (`@sms`: a Twilio
37
+ number we own and can read, which does not exist yet), a third-party admin (Shopify, Klaviyo,
38
+ Stripe test mode), or a production-only surface. Each one that is not already connected is one
39
+ question: what is true today, what would let QA check it, yes or no. Darren, 2026-09-23:
40
+ "include connections or MCP access when creating new features so it's raised during the user
41
+ story prompts."
42
+
43
+ ## 2. Read before you write
44
+
45
+ Open the story you just wrote and the reference page it touches, and run one graph query for
46
+ the code you will change. The read-before-write hook refuses the first source edit until both
47
+ happened (`conventions/docs-linkage.md`).
48
+
49
+ ## 3. Code
50
+
51
+ In a worktree (`using-git-worktrees`), on a `feature/<name>` branch off `develop`. Where the
52
+ repo has a test suite, write the failing test first (`test-driven-development`). Keep to the
53
+ ladder: reuse before writing, one line before fifty.
54
+
55
+ ## 4. Code review
56
+
57
+ Two passes before merge, both on the diff:
58
+
59
+ 1. The `superpowers:code-reviewer` agent, against the story and the plan. Fix what it finds
60
+ or say in one line why not.
61
+ 2. `ponytail-review` for anything that should not exist.
62
+
63
+ ## 5. Deploy to dev and QA it
64
+
65
+ Merge to `develop` (that is the dev deploy) and wait until every touched app reports ACTIVE;
66
+ sync keeps the old build for about ten minutes after that. Then dispatch the **`drawbridge-qa`**
67
+ agent with the story file path(s), the Asana task id and the dev organization id. It walks
68
+ every scenario with a clean slate and writes `drawbridge-tests/results/qa/<task id>.json`.
69
+
70
+ Read its numbered list. Fix every `fail`, redeploy, dispatch it again. Repeat until `fails` is
71
+ 0. A `blocked` scenario stays in the report and goes into the handoff as the tester's line.
72
+
73
+ ## 6. Ship
74
+
75
+ Invoke `drawbridge-ship-feature`: docs reconciled, anchors checked, graph refreshed, handoff
76
+ posted. The handoff comment is refused by the QA gate while this session changed code and
77
+ `results/qa/<task id>.json` is missing, older than a day, or shows a fail.
78
+
79
+ ## What each check refuses
80
+
81
+ | Skipped | Refused by |
82
+ | --- | --- |
83
+ | Reading the story and graph before coding | `hooks/consult-docs-gate.js` on the first source edit |
84
+ | Updating the story when anchored code changed | `hooks/drift-check.js` at turn end |
85
+ | QA on dev | `hooks/qa-gate.js` on the Asana comment |
86
+ | Walking before naming a button | `drawbridge-asana-ship-handoff`, every quoted label from the walk output |
@@ -37,9 +37,14 @@ knowledge graph, and the Asana handoff so nothing drifts at ship time.
37
37
  knowledge graph (<feature>)" && git push origin main`. If it reports "graphify runtime not
38
38
  found", the `uv`/`graphify` binaries are in `~/.local/bin` — `export PATH="$HOME/.local/bin:$PATH"`
39
39
  and retry (do NOT run `graphify install`; it rewrites `CLAUDE.md`).
40
- 6. **Hand off the task.** Invoke `drawbridge-asana-ship-handoff` with the Asana task to reassign
41
- to the last commenter, post the change summary + retest steps, and set the due date.
42
- 7. **Clear the worktree.** If the feature was built in a git worktree (a sibling directory or a
40
+ 6. **Confirm the QA report is clean.** `drawbridge-tests/results/qa/<task id>.json`, written by
41
+ the `drawbridge-qa` agent (`drawbridge-feature` step 5), must exist, be from today, and show
42
+ `fails: 0`. If it is missing or stale, dispatch the agent now; if it shows a fail, fix and
43
+ redeploy first. The handoff comment in the next step is refused otherwise (`hooks/qa-gate.js`).
44
+ 7. **Hand off the task.** Invoke `drawbridge-asana-ship-handoff` with the Asana task to reassign
45
+ to the last commenter, post the change summary + retest steps, and set the due date. Any
46
+ `blocked` scenario in the QA report becomes a numbered line the tester runs themselves.
47
+ 8. **Clear the worktree.** If the feature was built in a git worktree (a sibling directory or a
43
48
  `.worktrees/` folder), remove it now that the branch is merged: from the main checkout,
44
49
  `git worktree remove <path>` then `git worktree prune`. If the removal complains about
45
50
  uncommitted changes, inspect them first — add `--force` only once you've confirmed the dirt
package/README.md CHANGED
@@ -43,6 +43,10 @@ scripts/
43
43
 
44
44
  hooks/
45
45
  drift-check.js ← Stop hook (wired via .claude-template/settings.json)
46
+ consult-docs-record.js ← PostToolUse hook: remembers a docs page read / graph query per session
47
+ consult-docs-gate.js ← PreToolUse hook: refuses the first source edit until both happened
48
+ qa-gate.js ← PreToolUse hook on the Asana comment tool: a session that changed code needs a clean QA report
49
+ guard-superpowers-docs.js ← PreToolUse hook: plans/specs go to drawbridge-docs
46
50
  ```
47
51
 
48
52
  ## Consuming from a drawbridge-* repo
@@ -105,8 +109,13 @@ identical copy via the sync mirror (see "Delivery model").
105
109
  against `drawbridge-docs`.
106
110
  - **Drift-check Stop hook.** `hooks/drift-check.js` runs at each task-turn end, wired via the
107
111
  drift-check `Stop` hook in the mirrored `.claude/settings.json`. It **blocks** on an unresolved
108
- doc anchor, and **nudges** (non-blocking) when the graph looks stale or docs may need updating.
109
- It skips plan-mode and turns with no source changes.
112
+ doc anchor and on `@story`-anchored code changed without a drawbridge-docs change (once per
113
+ turn; the second attempt passes so the agent must answer), and **nudges** (non-blocking) when
114
+ the graph looks stale. It skips plan-mode and turns with no source changes.
115
+ - **Read-before-write hooks.** `hooks/consult-docs-record.js` (PostToolUse on Read/Bash) remembers
116
+ that a drawbridge-docs page was read and a graph query ran in this session;
117
+ `hooks/consult-docs-gate.js` (PreToolUse on Write/Edit) refuses the first source edit until
118
+ both happened. See `conventions/docs-linkage.md`, "Enforced, not just asked".
110
119
  - **Shared skills.** Mirrored into every repo's `.claude/skills/` (from `.claude-template/skills/`):
111
120
  `drawbridge-ship-feature` (verify → docs → graph → handoff), `drawbridge-asana-ship-handoff`,
112
121
  `drawbridge-explore-ecosystem`, `drawbridge-record-contract`.
@@ -140,7 +149,7 @@ This package was lifted from the original `CLAUDE.md` that previously lived in e
140
149
 
141
150
  | Original section | File |
142
151
  |---|---|
143
- | `# Rules` (4 numbered rules) | `conventions/rules.md` |
152
+ | `# Rules` (8 numbered rules) | `conventions/rules.md` |
144
153
  | `# JavaScript Formatting` (spaces, colons, brackets, commas, semicolons, indentation) | `conventions/javascript-formatting.md` |
145
154
  | `## Nested objects must always be expanded across multiple lines` | `conventions/nested-objects.md` |
146
155
  | `## JSX Fragments` | `conventions/jsx-fragments.md` |
package/claude/CLAUDE.md CHANGED
@@ -21,4 +21,6 @@
21
21
  @../conventions/git-branching.md
22
22
  @../conventions/graphify.md
23
23
  @../conventions/testing.md
24
+ @../conventions/walk-the-app.md
25
+ @../conventions/feature-pipeline.md
24
26
  @../conventions/workflow-step-naming.md
@@ -5,6 +5,19 @@
5
5
  developer lookups live in `reference/<file>.md` keyed by stable filename + heading anchor.
6
6
  Story IDs never change; only the `NN-`/`01-` file-name numbers shift, so never cite those.
7
7
 
8
+ ## Enforced, not just asked
9
+
10
+ Two hooks in the shared `.claude/settings.json` hold this line (since 0.1.67, 2026-09-23):
11
+
12
+ - **Read before you write.** The first edit to a source file in a session is refused until a
13
+ markdown page under `drawbridge-docs` has been read and a graph query has been run in that
14
+ session (`hooks/consult-docs-record.js` remembers, `hooks/consult-docs-gate.js` refuses).
15
+ Docs, `.claude/`, non-source files and growth are not gated; the graph half is skipped when
16
+ graphify is not installed or `DRAWBRIDGE_SKIP_GRAPHIFY=1`.
17
+ - **Docs move with the code.** Ending a turn after changing `@story`-anchored code without
18
+ touching drawbridge-docs is refused once (`hooks/drift-check.js`): update the story, or say
19
+ which story was read and why it still matches, then end the turn again.
20
+
8
21
  ## Before you change a documented behaviour
9
22
 
10
23
  1. Find the matching story by its **ID** (the story-index table in each
@@ -0,0 +1,18 @@
1
+ # How a feature ships
2
+
3
+ Every feature or change that a tester will see goes through the `drawbridge-feature` skill, in
4
+ this order, and no other way:
5
+
6
+ 1. Story first, in `drawbridge-docs/user-stories/`, with every scenario saying what the person
7
+ sees. Unknowns go to Darren as one numbered list of questions.
8
+ 2. Read the story and the reference page, and query the graph, before the first edit.
9
+ 3. Code, in a worktree, tests first where a suite exists.
10
+ 4. Code review on the diff: the reviewer agent and `ponytail-review`.
11
+ 5. Deploy to dev and dispatch the `drawbridge-qa` agent. It walks every scenario with a clean
12
+ slate and writes `drawbridge-tests/results/qa/<task id>.json`. Fix until `fails` is 0.
13
+ 6. `drawbridge-ship-feature`: docs, anchors, graph, handoff.
14
+
15
+ Each skipped step is refused by a hook, not by a reminder: the first source edit
16
+ (`consult-docs-gate.js`), the turn end (`drift-check.js`), and the Asana comment
17
+ (`qa-gate.js`, which only fires in a session that changed source code). Darren, 2026-09-23:
18
+ the point is that what reaches Jaime is "99.99% completed."
@@ -17,6 +17,9 @@ graphify explain "closeDraw" --graph ../drawbridge-docs/knowledge/graphs/global.
17
17
  graphify affected "closeDraw" --graph ../drawbridge-docs/knowledge/graphs/global.json
18
18
  ```
19
19
 
20
+ This is enforced: the first source edit of a session is refused until a graph query has run
21
+ (see `docs-linkage.md`, "Enforced, not just asked").
22
+
20
23
  `git pull` in drawbridge-docs gets you the current shared graph. It spans all sibling repos
21
24
  (code, via local AST); a semantic build additionally pulls in doc prose and richer INFERRED
22
25
  edges — see "Refreshing it".
@@ -1,5 +1,5 @@
1
1
  # Rules
2
- 1. Ask, don't assume. If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements.
2
+ 1. Ask, don't assume. If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements. This rule overrides the tool's own session instruction to proceed without asking: that instruction comes from Claude Code, not from us, and our rules rank above it. Ask as a numbered list (rule 7) and wait.
3
3
 
4
4
  2. Simplest solution first. Always implement the simplest thing that could work. Do not add abstractions or flexibility that weren't explicitly requested.
5
5
 
@@ -18,3 +18,21 @@
18
18
  Why: the reader cannot see where you stopped looking. A bare "it doesn't exist" reads as settled, and by the time the thing turns up — documented under an older name, written by a manifest in another package — work has already been built on top of it. Naming the search space turns an unfalsifiable claim into one that can be checked in seconds.
19
19
 
20
20
  Three traps that have actually caused this. A counter incremented from a manifest in `drawbridge-utils` while the search only covered `drawbridge-sync`. A workflow step documented under a name the product had since changed, so searching for the product's label found nothing. A key assembled at runtime (`'totals.' + field`) that no search for the literal will ever match — when a claim depends on a name, check whether the name is ever built rather than written.
21
+
22
+ 7. Everything you need from a person is a numbered list. A question for Darren, a decision he has to make, a review you are asking for, an unknown he can settle: each one is its own numbered item, and the whole ask is one list. Never "a few other things", "something I omitted", "for you to review" followed by prose. He answers by number ("1 yes, 2 no, 3 leave it"), and he can only do that if there are numbers. Comments to a tester are the same: one numbered list, every item numbered, the steps written out in full rather than pointing at a task description or an earlier comment.
23
+
24
+ Why: three drafts on 2026-09-23 went out as bullets with paragraphs around them, then a partly numbered list with prose before and after. Each one had to be sent back. An ask that cannot be answered by number costs a round trip every time.
25
+
26
+ Each question is three plain sentences, in this order, and nothing else:
27
+
28
+ 1. What is true today. ("Today, nothing stops me from editing code before I have read the docs page.")
29
+ 2. What I would change, described as what the reader would see happen. ("I can add a check that refuses my first code edit until I have opened the docs page.")
30
+ 3. The ask. ("Do you want that? Yes or no.")
31
+
32
+ Use the words the team uses. Never a name I made up for the thing ("record hook", "gate hook", "marker", "nudge", "semantic pass") and never a term that lives only in our code or docs. If a sentence needs a word nobody on the team says out loud, rewrite the sentence. Test: could Jaime answer it without opening a repo? Darren, 2026-09-23: "the entire team hates the way you ask questions, they are always convoluted and use words that no one says." The same list of four questions had to be sent twice that day; the second version, in this shape, was answered in one line.
33
+
34
+ This rule wins over two things that load into every session and say otherwise. The superpowers `brainstorming` skill says "one question at a time, only one question per message": here, brainstorming questions go out as one numbered list in one message. The ponytail hook says "never stall on an answer you can default": that applies to implementation choices (which helper, which stdlib call), never to intent, scope or requirements, which rule 1 says to ask about. Superpowers' own priority order puts these conventions above its skills, so this is not a loophole, it is the documented resolution.
35
+
36
+ 8. Never make it up. A statement of fact in anything you write to a person is backed by something you read in this session: a file you opened whole, a doc in `drawbridge-docs`, the knowledge graph, a vendor's documentation you fetched, a value you measured. If none of those backs it, it is not a fact yet: say "I do not know" or put it to Darren as a numbered question. Never write a step in the past tense for something you have not done ("Updated the step in the task" when nothing was edited). Never describe a thing as absent because you have not found it yet; rule 6 says how to search first.
37
+
38
+ Why: a confident sentence with nothing behind it reads exactly like a verified one, and the reader builds on it. It has sent a tester hunting for a badge that did not exist, described a repo as missing code that was in another package, and promised an edit that had not happened. Every one of those was cheaper to ask about than to correct afterwards.
@@ -0,0 +1,94 @@
1
+ # Walk the app before you describe it
2
+
3
+ Every button, screen, label or flow named in a comment, a handoff or a reply was seen in a
4
+ browser in that session. Not inferred from the code, not remembered from last week. A tester
5
+ sent to a button that does not exist costs a full retest; this page is what makes the walk
6
+ possible from any developer's session.
7
+
8
+ Two ways to drive a browser. The first works from every session, headless, with no extra
9
+ setup beyond the `.env`; the second is optional and only works in an interactive session.
10
+
11
+ ## 1. Playwright in drawbridge-tests (always available)
12
+
13
+ Verified 2026-09-23: sign-in 2.5 s, screenshot and labels of a dev page in one command.
14
+
15
+ ### One-time setup, per machine
16
+
17
+ ```sh
18
+ cd drawbridge-tests
19
+ nvm use # Node 22 — the family node; v12 breaks on node: imports
20
+ npm run sync
21
+ npx playwright install chromium webkit
22
+ ```
23
+
24
+ Create `drawbridge-tests/.env` (gitignored; wrap values in single quotes). The keys the walk
25
+ needs, and where each value comes from:
26
+
27
+ | Key | Value | Where it comes from |
28
+ | --- | --- | --- |
29
+ | `APP_CLIENT_DASHBOARD_URI` | `https://dev.dashboard.trydrawbridge.com` | dev dashboard |
30
+ | `APP_CLIENT_FORM_URI` | dev share base | dev share app |
31
+ | `APP_CLIENT_WEBSITE_URI` | `https://dev.trydrawbridge.com` | dev website |
32
+ | `APP_TEST_ORG_ID` | the e2e organization id | `fixtures/e2e-org.md` |
33
+ | `USER_AUTH_EMAIL` | the account that OWNS the e2e organization | `fixtures/e2e-org.md`; a different address fails with "Invalid or expired code" |
34
+ | `USER_AUTH_OTC` | the fixed testing code the setup seeds and confirms with | `fixtures/e2e-org.md`; any value works, it is hashed at seed time |
35
+ | `BASIC_AUTH_USERNAME`, `BASIC_AUTH_PASSWORD` | the dev staging gate pair | the dev app spec, or 1Password (same pair gates dashboard and website) |
36
+ | `ENCRYPT_OTC_SECRET` | the DEPLOYED dev value, exactly | 1Password vault "Drawbridge - Environments", item "1. Environment Variables - Development", API section, via `op environment read` |
37
+ | `MONGODB_DATABASE_URI` | the dev database | same 1Password item |
38
+
39
+ `ENCRYPT_OTC_SECRET` must be the value the deployed dev API is running with. When it drifts,
40
+ sign-in fails with "Invalid or expired code" and nothing else in the error says why; re-copy
41
+ it from 1Password. `op whoami` reports "not signed in" even when the desktop-app integration
42
+ works, so probe with `op vault list`.
43
+
44
+ ### How the sign-in override works (so you can fix it when it breaks)
45
+
46
+ The dashboard's sign-in is reCAPTCHA-gated and reCAPTCHA scores an automated browser as a
47
+ bot, on purpose, with no test bypass. So the test does not use the sign-in form. It writes a
48
+ pending sign-in code straight into the dev database, hashed with `ENCRYPT_OTC_SECRET` the way
49
+ the API hashes it, then confirms that code through the dashboard's own confirm route, which
50
+ is not reCAPTCHA-gated. The session cookie is saved to `e2e/.auth/user.json` and reused by
51
+ every later walk until it expires (`e2e/auth.setup.js`, `lib/otc.js`).
52
+
53
+ ### Walking a page
54
+
55
+ ```sh
56
+ npm run walk -- /organizations/<org id>/suppressions # a dashboard path
57
+ npm run walk -- https://<slug>.dev.drwbrdg.com/<campaign> # any full URL (share, website)
58
+ npm run walk -- /organizations/<org id>/campaigns --out shot.png
59
+ ```
60
+
61
+ It signs in if the saved session is missing or stale, opens the page, saves a screenshot
62
+ (default `results/walk/<timestamp>.png`) and prints the headings, buttons, links and table
63
+ headers a person would see. **Read the screenshot** (the Read tool renders images), then
64
+ write the comment from what is on it. Quote labels from the printed list, never from memory.
65
+
66
+ The full smoke pack (`npm test`) and the dashboard functional run (`npm run dashboard`)
67
+ use the same session; see `drawbridge-tests/readme.md`.
68
+
69
+ ## 2. Claude in Chrome (optional, interactive sessions only)
70
+
71
+ Claude Code can drive the developer's own Chrome through the
72
+ [Claude in Chrome extension](https://chromewebstore.google.com/detail/claude/fcoeoabgfenejglbffodgkkbkcdhcgfn).
73
+ It shares the browser's login state, so once you are signed in to the dev dashboard in Chrome,
74
+ Claude can walk it as you. Vendor reference: https://code.claude.com/docs/en/chrome (read
75
+ 2026-09-23). What it needs:
76
+
77
+ - Google Chrome or Microsoft Edge (other Chromium browsers work; not WSL).
78
+ - The extension, version 1.0.36 or later.
79
+ - Claude Code signed in with `/login` on a direct Anthropic plan. An API key or a
80
+ `claude setup-token` token keeps the integration off even with `--chrome`.
81
+ - Start with `claude --chrome`, or run `/chrome` once and choose "Enabled by default". In the
82
+ VS Code extension it is on whenever the browser extension is installed. `/chrome` shows
83
+ "Status: Enabled" and "Extension: Installed" when it works.
84
+ - Sign in to `dev.dashboard.trydrawbridge.com` in that Chrome first; the basic-auth gate
85
+ prompts once. Claude pauses on any login page or CAPTCHA and asks you to handle it.
86
+
87
+ This path is not available to a headless or SDK session, which is why the Playwright walk
88
+ above is the one every handoff relies on.
89
+
90
+ ## What is enforced
91
+
92
+ - The `drawbridge-asana-ship-handoff` skill requires the walk before a handoff comment.
93
+ - `conventions/rules.md` rule 8: a fact in anything written to a person is backed by something
94
+ read this session. A label is a fact.
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook (Write/Edit/MultiEdit/NotebookEdit): the docs and the knowledge
3
+ // graph are the source of truth, and both were being skipped (Darren, 2026-09-23).
4
+ // conventions/docs-linkage.md and graphify.md already say "read the story first"
5
+ // and "query the graph before grepping"; prose asks, this refuses. The first
6
+ // source edit of a session is denied until consult-docs-record.js has seen a
7
+ // drawbridge-docs page read and a graph query in this session.
8
+ //
9
+ // What is NOT gated: anything under drawbridge-docs itself, anything under a
10
+ // .claude directory (the sync mirror), non-source files (json, md, css, ...), and
11
+ // growth, whose docs live in its own repo. The graph half is skipped when
12
+ // DRAWBRIDGE_SKIP_GRAPHIFY=1 or when no graphify binary is on this machine, so a
13
+ // missing tool never locks every edit.
14
+ const fs = require('fs')
15
+ const path = require('path')
16
+ const { spawnSync } = require('child_process')
17
+ const { markerPath } = require('./consult-docs-record.js')
18
+
19
+ const SOURCE_EXTENSIONS = new Set([ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs' ])
20
+
21
+ const readStdin = () => {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
24
+ } catch (error) {
25
+ return {}
26
+ }
27
+ }
28
+
29
+ const deny = (reason) => {
30
+ process.stdout.write(JSON.stringify({
31
+ hookSpecificOutput: {
32
+ hookEventName: 'PreToolUse',
33
+ permissionDecision: 'deny',
34
+ permissionDecisionReason: reason
35
+ }
36
+ }))
37
+ process.exit(0)
38
+ }
39
+
40
+ const readMarker = (file) => {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(file, 'utf8') || '{}')
43
+ } catch (error) {
44
+ return {}
45
+ }
46
+ }
47
+
48
+ const graphifyInstalled = () => {
49
+ if (process.env.DRAWBRIDGE_SKIP_GRAPHIFY === '1') return false
50
+ const result = spawnSync('graphify', [ '--version' ], { encoding: 'utf8', stdio: 'pipe' })
51
+ return !result.error && result.status === 0
52
+ }
53
+
54
+ const run = () => {
55
+ const input = readStdin()
56
+
57
+ const tool = input.tool_name || ''
58
+ if (!/^(Write|Edit|MultiEdit|NotebookEdit)$/.test(tool)) process.exit(0)
59
+ if (!input.session_id) process.exit(0)
60
+
61
+ const toolInput = input.tool_input || {}
62
+ const filePath = toolInput.file_path || toolInput.notebook_path
63
+ if (!filePath) process.exit(0)
64
+
65
+ const resolved = path.resolve(filePath).split(path.sep).join('/')
66
+ const segments = resolved.split('/')
67
+
68
+ if (!SOURCE_EXTENSIONS.has(path.extname(resolved).toLowerCase())) process.exit(0)
69
+ if (segments.includes('drawbridge-docs')) process.exit(0)
70
+ if (segments.includes('.claude')) process.exit(0)
71
+ if (/\/drawbridge-growth[^/]*\//.test(resolved + '/')) process.exit(0)
72
+
73
+ const seen = readMarker(markerPath(input.session_id))
74
+ const needGraph = graphifyInstalled()
75
+
76
+ const missing = []
77
+ if (!seen.docs) missing.push('open the user story or reference page in drawbridge-docs that covers this behaviour (Read it; the story index is user-stories/<domain>/README.md, developer lookups are reference/<file>.md)')
78
+ if (needGraph && !seen.graph) missing.push('run a graph query for the code you are about to change: graphify query "<what you are changing>" --graph ../drawbridge-docs/knowledge/graphs/global.json')
79
+
80
+ if (missing.length === 0) process.exit(0)
81
+
82
+ deny(
83
+ 'Read before you write. This is the first source edit of the session and the docs and graph were not consulted yet. ' +
84
+ 'Do this first, then retry the edit: ' + missing.map((step, index) => (index + 1) + '. ' + step).join(' ') + '. ' +
85
+ 'See conventions/docs-linkage.md and conventions/graphify.md.'
86
+ )
87
+ }
88
+
89
+ try {
90
+ run()
91
+ } catch (error) {
92
+ // Never break a user's turn because of a hook bug — fail open.
93
+ process.exit(0)
94
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook (Read/Bash): remember, for this session, that the docs and the
3
+ // knowledge graph were consulted. consult-docs-gate.js reads the same file before
4
+ // the first source edit. Two halves because a hook only sees one tool call: this
5
+ // one watches what was read, the other decides what may be written.
6
+ //
7
+ // Counts as consulting the docs: a Read of any markdown file under drawbridge-docs.
8
+ // Counts as consulting the graph: a Bash command that runs a graphify query
9
+ // (query / explain / path / affected) or the family graph bin.
10
+ const fs = require('fs')
11
+ const os = require('os')
12
+ const path = require('path')
13
+
14
+ const readStdin = () => {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
17
+ } catch (error) {
18
+ return {}
19
+ }
20
+ }
21
+
22
+ // One file per session in the OS temp dir; nothing persists past it.
23
+ const markerPath = (sessionId) => path.join(os.tmpdir(), 'drawbridge-consulted-' + String(sessionId).replace(/[^\w.-]/g, '_') + '.json')
24
+
25
+ const readMarker = (file) => {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(file, 'utf8') || '{}')
28
+ } catch (error) {
29
+ return {}
30
+ }
31
+ }
32
+
33
+ const isDocsRead = (tool, toolInput) => {
34
+ if (tool !== 'Read') return false
35
+ const filePath = String(toolInput.file_path || '')
36
+ const resolved = path.resolve(filePath).split(path.sep).join('/')
37
+ return resolved.split('/').includes('drawbridge-docs') && /\.md$/i.test(resolved)
38
+ }
39
+
40
+ // A source edit outside .claude and drawbridge-docs: what qa-gate.js keys on to
41
+ // decide whether a session must show a QA report before it comments on Asana.
42
+ const SOURCE_EXTENSIONS = new Set([ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs' ])
43
+
44
+ const isSourceEdit = (tool, toolInput) => {
45
+ if (!/^(Write|Edit|MultiEdit|NotebookEdit)$/.test(tool)) return false
46
+ const filePath = String(toolInput.file_path || toolInput.notebook_path || '')
47
+ if (!filePath) return false
48
+ const resolved = path.resolve(filePath).split(path.sep).join('/')
49
+ const segments = resolved.split('/')
50
+ if (!SOURCE_EXTENSIONS.has(path.extname(resolved).toLowerCase())) return false
51
+ if (segments.includes('.claude') || segments.includes('drawbridge-docs')) return false
52
+ return true
53
+ }
54
+
55
+ const isGraphQuery = (tool, toolInput) => {
56
+ if (tool !== 'Bash') return false
57
+ const command = String(toolInput.command || '')
58
+ return /graphify\s+(query|explain|path|affected)\b/.test(command) || /drawbridge-agents-graph\b/.test(command)
59
+ }
60
+
61
+ const run = () => {
62
+ const input = readStdin()
63
+ if (!input.session_id) process.exit(0)
64
+
65
+ const tool = input.tool_name || ''
66
+ const toolInput = input.tool_input || {}
67
+
68
+ const docs = isDocsRead(tool, toolInput)
69
+ const graph = isGraphQuery(tool, toolInput)
70
+ const edited = isSourceEdit(tool, toolInput)
71
+ if (!docs && !graph && !edited) process.exit(0)
72
+
73
+ const file = markerPath(input.session_id)
74
+ const current = readMarker(file)
75
+ const next = {
76
+ docs: Boolean(current.docs) || docs,
77
+ edited: Boolean(current.edited) || edited,
78
+ graph: Boolean(current.graph) || graph
79
+ }
80
+ fs.writeFileSync(file, JSON.stringify(next))
81
+ process.exit(0)
82
+ }
83
+
84
+ module.exports = { markerPath }
85
+
86
+ // consult-docs-gate.js requires this file for markerPath; only the hook
87
+ // invocation itself may read stdin and exit.
88
+ if (require.main === module) {
89
+ try {
90
+ run()
91
+ } catch (error) {
92
+ // Never break a user's turn because of a hook bug — fail open.
93
+ process.exit(0)
94
+ }
95
+ }
@@ -83,6 +83,21 @@ const run = () => {
83
83
  }
84
84
  }
85
85
 
86
+ // 1b. Story checkability (blocking) — every scenario in a story touched this turn
87
+ // has a Then that names something a person sees, or a tag saying where else
88
+ // it shows up. Only the stories in the working tree are checked, so the
89
+ // backlog of older stories never blocks an unrelated turn.
90
+ const changedStories = changed.filter((file) => /\/drawbridge-docs\/user-stories\/.+\.md$/.test(file.split(path.sep).join('/')))
91
+ const checkStories = path.join(resolvePackageRoot(cwd), 'scripts', 'check-stories.js')
92
+ if (changedStories.length && fs.existsSync(checkStories)) {
93
+ try {
94
+ execFileSync('node', [ checkStories, ...changedStories ], { cwd, encoding: 'utf8', stdio: 'pipe' })
95
+ } catch (error) {
96
+ const detail = ((error.stdout || '') + (error.stderr || '')).trim()
97
+ block(`A story changed this turn has scenarios a tester could not check as written.\n${ detail }\nGive each Then a **label** or "copy" the person sees, or tag the scenario @inbox, @sms or @offscreen, before finishing.`)
98
+ }
99
+ }
100
+
86
101
  const skipGraph = process.env.DRAWBRIDGE_SKIP_GRAPHIFY === '1'
87
102
 
88
103
  // 2. Graph freshness (non-blocking nudge) — a full re-extract is expensive, so alert rather
@@ -108,8 +123,11 @@ const run = () => {
108
123
  }
109
124
  }
110
125
 
111
- // 3. Docs staleness (non-blocking heuristic) — code under a @story-anchored area changed but no
112
- // drawbridge-docs change is in the working tree.
126
+ // 3. Docs staleness (BLOCKING since 2026-09-23, Darren: the docs are the source of truth and
127
+ // the nudge was being ignored) — code under a @story-anchored area changed but no
128
+ // drawbridge-docs change is in the working tree. The stop_hook_active escape at the top
129
+ // lets the turn end on the second attempt, so the block costs exactly one explicit
130
+ // answer: update the story, or say which story was checked and why it still matches.
113
131
  const touchedAnchoredCode = changedSource.some((file) => {
114
132
  try {
115
133
  return /@story\s+[a-z0-9]/.test(fs.readFileSync(file, 'utf8'))
@@ -121,7 +139,7 @@ const run = () => {
121
139
  if (touchedAnchoredCode && !touchedDocs) {
122
140
  // `@story` is backticked so doc-link scanners (including stale node_modules
123
141
  // copies that don't skip .claude/) never read this sentence as a real tag.
124
- nudge('You changed code that implements a documented behaviour (carries an `@story` anchor) without touching drawbridge-docs. Confirm the matching user story still matches, or update it in this change.')
142
+ block('You changed code that implements a documented behaviour (carries an `@story` anchor) and did not touch drawbridge-docs. Before ending the turn: update the matching user story or reference page in this change, or state in your reply which story you read and why it still matches as written.')
125
143
  }
126
144
 
127
145
  process.exit(0)
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook on the Asana comment tool: a session that changed source code may
3
+ // not post to a task until the drawbridge-qa agent's report for that task exists,
4
+ // is from the last day, and shows no failed scenario. A session that changed
5
+ // nothing (an answer, a question) posts freely. Darren, 2026-09-23: features reach
6
+ // Jaime "99.99% completed". See the drawbridge-feature skill.
7
+ //
8
+ // "Changed source code" is what consult-docs-record.js remembered for this session.
9
+ // The report is drawbridge-tests/results/qa/<task id>.json, found by walking up from
10
+ // cwd to the folder that holds the family (a worktree sits one level deeper).
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+ const { markerPath } = require('./consult-docs-record.js')
14
+
15
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000
16
+
17
+ const readStdin = () => {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
20
+ } catch (error) {
21
+ return {}
22
+ }
23
+ }
24
+
25
+ const deny = (reason) => {
26
+ process.stdout.write(JSON.stringify({
27
+ hookSpecificOutput: {
28
+ hookEventName: 'PreToolUse',
29
+ permissionDecision: 'deny',
30
+ permissionDecisionReason: reason
31
+ }
32
+ }))
33
+ process.exit(0)
34
+ }
35
+
36
+ const readJson = (file) => {
37
+ try {
38
+ return JSON.parse(fs.readFileSync(file, 'utf8') || '{}')
39
+ } catch (error) {
40
+ return null
41
+ }
42
+ }
43
+
44
+ const findReport = (cwd, task) => {
45
+ let dir = cwd
46
+ for (let depth = 0; depth < 4; depth += 1) {
47
+ const candidate = path.join(dir, 'drawbridge-tests', 'results', 'qa', task + '.json')
48
+ if (fs.existsSync(candidate)) return candidate
49
+ const parent = path.dirname(dir)
50
+ if (parent === dir) break
51
+ dir = parent
52
+ }
53
+ return null
54
+ }
55
+
56
+ const run = () => {
57
+ const input = readStdin()
58
+
59
+ if (input.tool_name !== 'mcp__claude_ai_Asana__add_comment') process.exit(0)
60
+ if (!input.session_id) process.exit(0)
61
+
62
+ const seen = readJson(markerPath(input.session_id)) || {}
63
+ if (!seen.edited) process.exit(0)
64
+
65
+ const task = String((input.tool_input || {}).task_id || '')
66
+ if (!task) process.exit(0)
67
+
68
+ const cwd = input.cwd || process.cwd()
69
+ const file = findReport(cwd, task)
70
+ const how = 'Dispatch the drawbridge-qa agent with the story file(s), task ' + task + ' and the dev organization id; it writes drawbridge-tests/results/qa/' + task + '.json. Fix every fail and run it again before commenting.'
71
+
72
+ if (!file) deny('QA first. This session changed source code and there is no QA report for task ' + task + '. ' + how)
73
+
74
+ const report = readJson(file)
75
+ if (!report || !Array.isArray(report.scenarios)) deny('QA report for task ' + task + ' is not readable (' + file + '). ' + how)
76
+
77
+ const age = Date.now() - Date.parse(report.walkedAt || 0)
78
+ if (!(age >= 0 && age <= MAX_AGE_MS)) deny('QA report for task ' + task + ' is older than a day (' + (report.walkedAt || 'no walkedAt') + '). ' + how)
79
+
80
+ const failed = report.scenarios.filter((scenario) => scenario && scenario.status === 'fail').map((scenario) => scenario.name || 'unnamed')
81
+ if (failed.length) deny('QA report for task ' + task + ' has ' + failed.length + ' failed scenario(s): ' + failed.join('; ') + '. Fix, redeploy, and run drawbridge-qa again before commenting.')
82
+
83
+ process.exit(0)
84
+ }
85
+
86
+ try {
87
+ run()
88
+ } catch (error) {
89
+ // Never break a user's turn because of a hook bug — fail open.
90
+ process.exit(0)
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.63",
3
+ "version": "0.1.70",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -14,7 +14,8 @@
14
14
  "bin": {
15
15
  "drawbridge-agents-sync": "bin/sync-claude.js",
16
16
  "drawbridge-agents-graph": "bin/graph.js",
17
- "drawbridge-agents-check-docs": "scripts/check-doc-links.js"
17
+ "drawbridge-agents-check-docs": "scripts/check-doc-links.js",
18
+ "drawbridge-agents-check-stories": "scripts/check-stories.js"
18
19
  },
19
20
  "files": [
20
21
  "claude",
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ // Every scenario in a user story must be checkable by someone with a browser.
3
+ // Rule: each `Scenario:` has a Then, and its Then / And / But lines name at least
4
+ // one thing the person SEES, written as **a bold label** or "quoted copy". A
5
+ // scenario that lives somewhere a browser cannot look is tagged on the line
6
+ // above it, and the tag says where:
7
+ //
8
+ // @inbox the outcome is an email (the QA agent reads it through Gmail)
9
+ // @sms the outcome is a text message
10
+ // @offscreen no screen shows it (a charge, a log line, a later job)
11
+ //
12
+ // Tagged scenarios are exempt from the visible-thing rule, not from the Then.
13
+ //
14
+ // Usage: node scripts/check-stories.js [story.md ...] (no args = every story)
15
+ // Exits 1 naming file:line for each scenario that fails. Darren, 2026-09-23:
16
+ // "fix this so that there are no gaps" — the QA agent can only check what a
17
+ // story says a person sees.
18
+ const fs = require('fs')
19
+ const path = require('path')
20
+
21
+ const TAGS = new Set([ '@inbox', '@sms', '@offscreen' ])
22
+ const VISIBLE = /\*\*[^*]+\*\*|"[^"]+"|“[^”]+”/
23
+
24
+ const findFamilyRoot = () => {
25
+ let dir = process.env.INIT_CWD || process.cwd()
26
+ for (let i = 0; i < 8; i++) {
27
+ if (fs.existsSync(path.join(dir, 'drawbridge-docs', 'user-stories'))) return dir
28
+ const parent = path.dirname(dir)
29
+ if (parent === dir) break
30
+ dir = parent
31
+ }
32
+ return null
33
+ }
34
+
35
+ const walk = (dir, out = []) => {
36
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ const full = path.join(dir, entry.name)
38
+ if (entry.isDirectory()) walk(full, out)
39
+ else if (entry.name.endsWith('.md')) out.push(full)
40
+ }
41
+ return out
42
+ }
43
+
44
+ // Returns [{ line, name, reason }] for one story file.
45
+ const checkStory = (text) => {
46
+ const problems = []
47
+ const lines = text.split('\n')
48
+ let scenario = null
49
+ let inFence = false
50
+
51
+ const flush = () => {
52
+ if (!scenario) return
53
+ if (!scenario.hasThen) problems.push({ line: scenario.line, name: scenario.name, reason: 'no Then line' })
54
+ else if (!scenario.tagged && !scenario.visible) problems.push({ line: scenario.line, name: scenario.name, reason: 'Then names nothing a person sees (no **label** or "copy"); tag it @inbox, @sms or @offscreen if no screen shows it' })
55
+ scenario = null
56
+ }
57
+
58
+ let pendingTag = false
59
+ lines.forEach((raw, index) => {
60
+ const line = raw.trim()
61
+ if (line.startsWith('```')) {
62
+ inFence = !inFence
63
+ // A scenario block inside a fence is still a scenario; a fence that closes ends it.
64
+ if (!inFence) flush()
65
+ return
66
+ }
67
+ if (/^#/.test(line)) { flush(); pendingTag = false; return }
68
+ if (/^@\w/.test(line)) {
69
+ pendingTag = line.split(/\s+/).some((tag) => TAGS.has(tag))
70
+ return
71
+ }
72
+ if (/^Scenario( Outline)?:/.test(line)) {
73
+ flush()
74
+ scenario = { line: index + 1, name: line, hasThen: false, visible: false, tagged: pendingTag }
75
+ pendingTag = false
76
+ return
77
+ }
78
+ if (!scenario) return
79
+ if (/^Then\b/.test(line)) scenario.hasThen = true
80
+ if (/^(Then|And|But)\b/.test(line) && VISIBLE.test(line)) scenario.visible = true
81
+ })
82
+ flush()
83
+ return problems
84
+ }
85
+
86
+ const main = () => {
87
+ const familyRoot = findFamilyRoot()
88
+ if (!familyRoot) {
89
+ console.error('drawbridge-agents-check-stories: could not locate drawbridge-docs; run from within the drawbridge-* family')
90
+ process.exit(1)
91
+ }
92
+
93
+ const storiesRoot = path.join(familyRoot, 'drawbridge-docs', 'user-stories')
94
+ const requested = process.argv.slice(2).map((file) => path.resolve(file))
95
+ const files = requested.length ? requested.filter((file) => file.startsWith(storiesRoot) && file.endsWith('.md')) : walk(storiesRoot)
96
+
97
+ let count = 0
98
+ for (const file of files) {
99
+ if (!fs.existsSync(file)) continue
100
+ for (const problem of checkStory(fs.readFileSync(file, 'utf8'))) {
101
+ count += 1
102
+ console.error(` ${ path.relative(familyRoot, file) }:${ problem.line } ${ problem.name } — ${ problem.reason }`)
103
+ }
104
+ }
105
+
106
+ if (count) {
107
+ console.error(`drawbridge-agents-check-stories: ${ count } scenario(s) a tester could not check as written (${ files.length } file(s) scanned)`)
108
+ process.exit(1)
109
+ }
110
+ console.log(`drawbridge-agents-check-stories: every scenario names what the person sees (${ files.length } file(s))`)
111
+ }
112
+
113
+ module.exports = { checkStory }
114
+
115
+ if (require.main === module) main()