@drawbridge/drawbridge-agents 0.1.70 → 0.1.71

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.
@@ -64,6 +64,10 @@ Write for the person reading it, not for the repo.
64
64
  the interface labels them. Not your paraphrase, not the variable name. If the button says
65
65
  Send, write Send.
66
66
  - **Quote user-facing copy exactly**, so they can match it against what is on screen.
67
+ Double quotes mean "one of our labels", and the comment tool refuses the post if a
68
+ double-quoted label was not seen by a walk in the last day (`npm run walk` in
69
+ drawbridge-tests) and exists nowhere in the family's source. A third-party label, a mail
70
+ client's button, a Shopify admin page, goes in 'single quotes' and is not checked.
67
71
  - Use their words for their bug. If they called it a prize notification, it is a prize
68
72
  notification, not a lead-facing transactional send.
69
73
 
@@ -83,4 +83,4 @@ posted. The handoff comment is refused by the QA gate while this session changed
83
83
  | Reading the story and graph before coding | `hooks/consult-docs-gate.js` on the first source edit |
84
84
  | Updating the story when anchored code changed | `hooks/drift-check.js` at turn end |
85
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 |
86
+ | Walking before naming a button | `hooks/qa-gate.js` on every Asana comment: a "double-quoted" label must appear in a walk record from the last day or in family source |
package/README.md CHANGED
@@ -45,7 +45,7 @@ hooks/
45
45
  drift-check.js ← Stop hook (wired via .claude-template/settings.json)
46
46
  consult-docs-record.js ← PostToolUse hook: remembers a docs page read / graph query per session
47
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
48
+ qa-gate.js ← PreToolUse hook on the Asana comment tool: every "quoted label" must have been walked or exist in source; a session that changed code needs a clean QA report
49
49
  guard-superpowers-docs.js ← PreToolUse hook: plans/specs go to drawbridge-docs
50
50
  ```
51
51
 
package/hooks/qa-gate.js CHANGED
@@ -41,11 +41,10 @@ const readJson = (file) => {
41
41
  }
42
42
  }
43
43
 
44
- const findReport = (cwd, task) => {
44
+ const findFamilyRoot = (cwd) => {
45
45
  let dir = cwd
46
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
47
+ if (fs.existsSync(path.join(dir, 'drawbridge-tests'))) return dir
49
48
  const parent = path.dirname(dir)
50
49
  if (parent === dir) break
51
50
  dir = parent
@@ -53,19 +52,97 @@ const findReport = (cwd, task) => {
53
52
  return null
54
53
  }
55
54
 
55
+ const findReport = (cwd, task) => {
56
+ const root = findFamilyRoot(cwd)
57
+ if (!root) return null
58
+ const candidate = path.join(root, 'drawbridge-tests', 'results', 'qa', task + '.json')
59
+ return fs.existsSync(candidate) ? candidate : null
60
+ }
61
+
62
+ // --- Quoted labels must have been seen -------------------------------------
63
+ //
64
+ // A "double-quoted" string in a comment is a label on OUR screens and is checked:
65
+ // it must appear in a walk record from the last day (drawbridge-tests/results/walk/
66
+ // *.json, written by npm run walk) or, failing that, in the family's source. A
67
+ // 'single-quoted' string is a third-party label (Outlook's buttons, a Shopify
68
+ // admin page) or spoken words, and is not checked. Darren, 2026-09-23: no more
69
+ // "buttons or functions that don't exist".
70
+
71
+ const SOURCE_REPOS = [ 'drawbridge-app-web', 'drawbridge-components', 'drawbridge-share', 'drawbridge-api', 'drawbridge-utils', 'drawbridge-shopify-app', 'drawbridge-website', 'drawbridge-emails' ]
72
+
73
+ const quotedLabels = (text) => {
74
+ const found = new Set()
75
+ const re = /"([^"\n]{1,120})"|“([^”\n]{1,120})”/g
76
+ let match
77
+ while ((match = re.exec(text)) !== null) found.add((match[1] || match[2]).trim())
78
+ return Array.from(found).filter(Boolean)
79
+ }
80
+
81
+ const walkedLabels = (root) => {
82
+ const dir = path.join(root, 'drawbridge-tests', 'results', 'walk')
83
+ const seen = []
84
+ if (!fs.existsSync(dir)) return seen
85
+ for (const name of fs.readdirSync(dir)) {
86
+ if (!name.endsWith('.json')) continue
87
+ const file = path.join(dir, name)
88
+ try {
89
+ if (Date.now() - fs.statSync(file).mtimeMs > MAX_AGE_MS) continue
90
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'))
91
+ for (const key of [ 'buttons', 'headings', 'inputs', 'links', 'tableHeaders', 'title' ]) {
92
+ const value = record[key]
93
+ if (Array.isArray(value)) seen.push(...value.map(String))
94
+ else if (typeof value === 'string') seen.push(value)
95
+ }
96
+ } catch (error) {
97
+ // an unreadable record proves nothing
98
+ }
99
+ }
100
+ return seen
101
+ }
102
+
103
+ const inSource = (root, label) => {
104
+ const { spawnSync } = require('child_process')
105
+ const dirs = SOURCE_REPOS.map((name) => path.join(root, name)).filter((dir) => fs.existsSync(dir))
106
+ if (!dirs.length) return false
107
+ const result = spawnSync('grep', [
108
+ '-rFq', '--include=*.js', '--include=*.jsx', '--include=*.ts', '--include=*.tsx', '--include=*.json', '--include=*.hbs', '--include=*.md',
109
+ '--exclude-dir=node_modules', '--exclude-dir=.next', '--exclude-dir=dist', '--exclude-dir=.git', '--exclude-dir=.claude',
110
+ '--', label, ...dirs
111
+ ], { encoding: 'utf8' })
112
+ return result.status === 0
113
+ }
114
+
115
+ const unseenLabels = (cwd, text) => {
116
+ const root = findFamilyRoot(cwd)
117
+ if (!root) return []
118
+ const labels = quotedLabels(text)
119
+ if (!labels.length) return []
120
+ const walked = walkedLabels(root).map((value) => value.toLowerCase())
121
+ return labels.filter((label) => {
122
+ const needle = label.toLowerCase()
123
+ if (walked.some((value) => value.includes(needle))) return false
124
+ return !inSource(root, label)
125
+ })
126
+ }
127
+
56
128
  const run = () => {
57
129
  const input = readStdin()
58
130
 
59
131
  if (input.tool_name !== 'mcp__claude_ai_Asana__add_comment') process.exit(0)
60
132
  if (!input.session_id) process.exit(0)
61
133
 
134
+ const cwd = input.cwd || process.cwd()
135
+ const toolInput = input.tool_input || {}
136
+
137
+ // Every comment, code changed or not: a quoted label has to exist.
138
+ const unseen = unseenLabels(cwd, String(toolInput.html_text || toolInput.text || ''))
139
+ if (unseen.length) deny('A label in this comment was never seen: ' + unseen.map((label) => '"' + label + '"').join(', ') + '. Walk the page (npm run walk in drawbridge-tests) and quote the label as it prints, or use \'single quotes\' for a third-party label such as a mail client\'s button.')
140
+
62
141
  const seen = readJson(markerPath(input.session_id)) || {}
63
142
  if (!seen.edited) process.exit(0)
64
143
 
65
- const task = String((input.tool_input || {}).task_id || '')
144
+ const task = String(toolInput.task_id || '')
66
145
  if (!task) process.exit(0)
67
-
68
- const cwd = input.cwd || process.cwd()
69
146
  const file = findReport(cwd, task)
70
147
  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
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {