@drawbridge/drawbridge-agents 0.1.66 → 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.
- package/.claude-template/agents/drawbridge-qa.md +88 -0
- package/.claude-template/settings.json +24 -0
- package/.claude-template/skills/drawbridge-asana-ship-handoff/SKILL.md +3 -1
- package/.claude-template/skills/drawbridge-feature/SKILL.md +86 -0
- package/.claude-template/skills/drawbridge-ship-feature/SKILL.md +8 -3
- package/README.md +11 -2
- package/claude/CLAUDE.md +2 -0
- package/conventions/docs-linkage.md +13 -0
- package/conventions/feature-pipeline.md +18 -0
- package/conventions/graphify.md +3 -0
- package/conventions/rules.md +8 -0
- package/conventions/walk-the-app.md +94 -0
- package/hooks/consult-docs-gate.js +94 -0
- package/hooks/consult-docs-record.js +95 -0
- package/hooks/drift-check.js +21 -3
- package/hooks/qa-gate.js +91 -0
- package/package.json +3 -2
- package/scripts/check-stories.js +115 -0
|
@@ -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
|
}
|
|
@@ -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.
|
|
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
|
|
@@ -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. **
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
109
|
-
|
|
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`.
|
package/claude/CLAUDE.md
CHANGED
|
@@ -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."
|
package/conventions/graphify.md
CHANGED
|
@@ -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".
|
package/conventions/rules.md
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
|
|
24
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
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
|
+
|
|
26
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.
|
|
27
35
|
|
|
28
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.
|
|
@@ -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
|
+
}
|
package/hooks/drift-check.js
CHANGED
|
@@ -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 (
|
|
112
|
-
//
|
|
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
|
-
|
|
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)
|
package/hooks/qa-gate.js
ADDED
|
@@ -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.
|
|
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()
|