@drawbridge/drawbridge-agents 0.0.11 → 0.1.1
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/README.md +64 -14
- package/bin/graph.js +61 -0
- package/bin/preflight-graphify.js +54 -0
- package/bin/sync-claude.js +75 -14
- package/claude/CLAUDE.md +4 -0
- package/conventions/cross-repo-contracts.md +46 -0
- package/conventions/docs-linkage.md +51 -0
- package/conventions/ecosystem.md +46 -0
- package/conventions/graphify.md +42 -0
- package/graph/README.md +10 -0
- package/hooks/drift-check.js +114 -0
- package/package.json +8 -2
- package/scripts/check-doc-links.js +110 -0
- package/skills/drawbridge-asana-ship-handoff/SKILL.md +43 -0
- package/skills/drawbridge-explore-ecosystem/SKILL.md +30 -0
- package/skills/drawbridge-record-contract/SKILL.md +36 -0
- package/skills/drawbridge-ship-feature/SKILL.md +31 -0
package/README.md
CHANGED
|
@@ -9,37 +9,50 @@ Before this package, every drawbridge-* repo carried a byte-identical copy of th
|
|
|
9
9
|
## Layout
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
conventions/ ← canonical, agent-neutral content
|
|
13
|
-
rules.md
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
property-shorthand.md
|
|
12
|
+
conventions/ ← canonical, agent-neutral content (@-imported into CLAUDE.md)
|
|
13
|
+
rules.md, javascript-formatting.md, ...
|
|
14
|
+
ecosystem.md ← always-loaded architecture map of the 16 repos
|
|
15
|
+
cross-repo-contracts.md ← load-bearing invariants that break across repos
|
|
16
|
+
docs-linkage.md ← consult the spec + @story/@doc code anchors
|
|
17
|
+
graphify.md ← query the family knowledge graph before grepping
|
|
19
18
|
|
|
20
19
|
claude/
|
|
21
20
|
CLAUDE.md ← aggregator — @-imports every conventions/*.md
|
|
22
21
|
|
|
23
22
|
.claude-template/ ← mirrored into each consumer repo's .claude/
|
|
24
23
|
settings.json ← shared permissions/env
|
|
25
|
-
hooks/
|
|
26
|
-
agents/
|
|
27
|
-
commands/
|
|
24
|
+
hooks/ agents/ commands/
|
|
28
25
|
|
|
29
26
|
.root-template/ ← mirrored into each consumer repo's root
|
|
30
|
-
.mcp.json ← project-level MCP servers (e.g. Sentry)
|
|
27
|
+
.mcp.json ← project-level MCP servers (e.g. Sentry) — gitignored in consumers
|
|
31
28
|
.editorconfig ← family-wide editor defaults (tabs, lf, utf-8)
|
|
32
29
|
|
|
33
30
|
bin/
|
|
34
|
-
sync-claude.js ← drawbridge-agents-sync — mirrors templates
|
|
31
|
+
sync-claude.js ← drawbridge-agents-sync — mirrors templates + installs hook/skills + graphify preflight
|
|
32
|
+
graph.js ← drawbridge-agents-graph — builds the merged family knowledge graph
|
|
33
|
+
preflight-graphify.js ← ensures the graphify runtime is installed (called by sync)
|
|
34
|
+
|
|
35
|
+
scripts/
|
|
36
|
+
check-doc-links.js ← drawbridge-agents-check-docs — validates @story/@doc tags resolve
|
|
37
|
+
|
|
38
|
+
hooks/
|
|
39
|
+
drift-check.js ← Stop hook — blocks on unresolved doc tags, nudges on stale graph
|
|
40
|
+
|
|
41
|
+
skills/ ← shared skills, installed to user-level ~/.claude/skills on sync
|
|
42
|
+
drawbridge-asana-ship-handoff/ drawbridge-ship-feature/
|
|
43
|
+
drawbridge-explore-ecosystem/ drawbridge-record-contract/
|
|
44
|
+
|
|
45
|
+
graph/ ← generated (gitignored) merged.json + manifest.json; see graph/README.md
|
|
35
46
|
```
|
|
36
47
|
|
|
37
48
|
## Consuming from a drawbridge-* repo
|
|
38
49
|
|
|
39
|
-
1. Install
|
|
50
|
+
1. Install as a **devDependency** (this package is agent-tooling only — no app-runtime imports —
|
|
51
|
+
so it must not ship to production / DigitalOcean):
|
|
40
52
|
```bash
|
|
41
|
-
npm install --save-exact @drawbridge/drawbridge-agents
|
|
53
|
+
npm install --save-exact --save-dev @drawbridge/drawbridge-agents
|
|
42
54
|
```
|
|
55
|
+
Ensure the app's deploy install step omits devDependencies (`npm ci --omit=dev`).
|
|
43
56
|
|
|
44
57
|
2. Replace the repo's `CLAUDE.md` with a thin import file:
|
|
45
58
|
```markdown
|
|
@@ -67,6 +80,43 @@ bin/
|
|
|
67
80
|
|
|
68
81
|
Claude Code's `@` imports cascade — one import line resolves the whole tree under `claude/CLAUDE.md`.
|
|
69
82
|
|
|
83
|
+
## Ecosystem knowledge, graph, and drift checks
|
|
84
|
+
|
|
85
|
+
Beyond the conventions, this package gives agents cross-repo visibility and keeps code, docs,
|
|
86
|
+
and the knowledge graph from drifting. Everything below is committed **only in this repo** and
|
|
87
|
+
reaches consumers without committing anything to them (see "Isolation").
|
|
88
|
+
|
|
89
|
+
- **Family knowledge graph (Graphify).** `npx drawbridge-agents-graph` runs code-only AST
|
|
90
|
+
extraction over every `drawbridge-*` repo and merges them into the cross-repo graph at
|
|
91
|
+
`~/.graphify/global-graph.json`. Agents query it with the `graphify` CLI (e.g.
|
|
92
|
+
`graphify query "…" --graph ~/.graphify/global-graph.json`) instead of grepping. The runtime is
|
|
93
|
+
a per-machine Python tool — `npm run sync` installs it (`bin/preflight-graphify.js`); set
|
|
94
|
+
`DRAWBRIDGE_SKIP_GRAPHIFY=1` to skip in CI/headless. We deliberately do **not** run
|
|
95
|
+
`graphify install` (it would rewrite `CLAUDE.md`); the optional `graphify-mcp` server is
|
|
96
|
+
available if you prefer MCP.
|
|
97
|
+
- **Docs linkage + validator.** Feature code carries `@story <domain>/<slug>` / `@doc
|
|
98
|
+
reference/<file>#<anchor>` anchors (see `conventions/docs-linkage.md`).
|
|
99
|
+
`npx drawbridge-agents-check-docs` scans every repo and fails if any anchor no longer resolves
|
|
100
|
+
against `drawbridge-docs`.
|
|
101
|
+
- **Drift-check Stop hook.** `hooks/drift-check.js` runs at each task-turn end (installed into the
|
|
102
|
+
consumer's gitignored `.claude/settings.local.json`). It **blocks** on an unresolved doc
|
|
103
|
+
anchor, and **nudges** (non-blocking) when the graph looks stale or docs may need updating. It
|
|
104
|
+
skips plan-mode and turns with no source changes.
|
|
105
|
+
- **Shared skills.** Installed to user-level `~/.claude/skills/` on sync:
|
|
106
|
+
`drawbridge-ship-feature` (verify → docs → graph → handoff), `drawbridge-asana-ship-handoff`,
|
|
107
|
+
`drawbridge-explore-ecosystem`, `drawbridge-record-contract`.
|
|
108
|
+
|
|
109
|
+
### Isolation (nothing ships to DigitalOcean)
|
|
110
|
+
|
|
111
|
+
- The package is a **devDependency** → not installed in production.
|
|
112
|
+
- The graph and CLI are per-machine (`~/.graphify/`); nothing graph-related is written into the
|
|
113
|
+
consumer repo, and `graphify install` (which edits `CLAUDE.md`) is never run.
|
|
114
|
+
- The Stop hook is merged into `.claude/settings.local.json` (gitignored).
|
|
115
|
+
- Skills install to `~/.claude/skills/` (per-machine, outside any repo).
|
|
116
|
+
- The only in-repo footprint in consumers is inert `@story`/`@doc` source comments.
|
|
117
|
+
|
|
118
|
+
After `npm run sync`, `git status` in a consumer should show **no new tracked files**.
|
|
119
|
+
|
|
70
120
|
## Updating a rule or shared `.claude/` file
|
|
71
121
|
|
|
72
122
|
1. Edit the relevant `conventions/<file>.md` or `.claude-template/<path>`.
|
package/bin/graph.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Builds the family knowledge graph: runs graphify's headless AST extraction over every
|
|
3
|
+
// drawbridge-* repo (plus drawbridge-docs and the shared @drawbridge/* packages) and merges
|
|
4
|
+
// each into the global cross-repo graph at ~/.graphify/global-graph.json — which is what agents
|
|
5
|
+
// query. Code-only (local AST, no API key). See conventions/graphify.md.
|
|
6
|
+
//
|
|
7
|
+
// A semantic build (adds doc/paper content + INFERRED edges) needs an LLM backend: drop
|
|
8
|
+
// --code-only and set an API key (e.g. ANTHROPIC_API_KEY) — slower and costs LLM calls.
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const os = require('os')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const { execFileSync } = require('child_process')
|
|
13
|
+
|
|
14
|
+
const packageRoot = path.resolve(__dirname, '..')
|
|
15
|
+
const familyRoot = path.resolve(packageRoot, '..')
|
|
16
|
+
const buildDir = path.join(os.homedir(), '.graphify', 'build')
|
|
17
|
+
|
|
18
|
+
// The canonical family list lives in the shared settings template — single source of truth.
|
|
19
|
+
const settings = JSON.parse(fs.readFileSync(path.join(packageRoot, '.claude-template', 'settings.json'), 'utf8'))
|
|
20
|
+
const repos = settings.permissions.additionalDirectories
|
|
21
|
+
.map((entry) => path.basename(entry))
|
|
22
|
+
.filter((name) => name !== 'drawbridge-agents')
|
|
23
|
+
|
|
24
|
+
const hasGraphify = () => {
|
|
25
|
+
try {
|
|
26
|
+
execFileSync('graphify', [ '--version' ], { stdio: 'ignore' })
|
|
27
|
+
return true
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!hasGraphify()) {
|
|
34
|
+
console.error('drawbridge-agents-graph: graphify runtime not found. Install it first: uv tool install graphifyy')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fs.mkdirSync(buildDir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
const built = []
|
|
41
|
+
for (const name of repos) {
|
|
42
|
+
const repoPath = path.join(familyRoot, name)
|
|
43
|
+
if (!fs.existsSync(repoPath)) {
|
|
44
|
+
console.warn(` skip ${ name } (not found at ${ repoPath })`)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
console.log(` extract ${ name }`)
|
|
48
|
+
// --out redirects the per-repo graphify-out/ away from the repo (keeps it clean);
|
|
49
|
+
// --global merges the result into ~/.graphify/global-graph.json under the --as tag.
|
|
50
|
+
execFileSync(
|
|
51
|
+
'graphify',
|
|
52
|
+
[ 'extract', repoPath, '--out', path.join(buildDir, name), '--global', '--as', name, '--code-only' ],
|
|
53
|
+
{ stdio: 'inherit' }
|
|
54
|
+
)
|
|
55
|
+
built.push(name)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const globalGraph = path.join(os.homedir(), '.graphify', 'global-graph.json')
|
|
59
|
+
console.log(`drawbridge-agents-graph: built cross-repo graph from ${ built.length } repo(s)`)
|
|
60
|
+
console.log(` global graph: ${ globalGraph }`)
|
|
61
|
+
console.log(' query it: graphify query "<question>" --graph ' + globalGraph)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Ensures the graphify runtime is installed (required for the family knowledge graph) and
|
|
3
|
+
// registers its MCP server with the local assistant. Called by drawbridge-agents-sync.
|
|
4
|
+
// Set DRAWBRIDGE_SKIP_GRAPHIFY=1 to skip in CI / headless environments.
|
|
5
|
+
const { execFileSync } = require('child_process')
|
|
6
|
+
|
|
7
|
+
if (process.env.DRAWBRIDGE_SKIP_GRAPHIFY === '1') {
|
|
8
|
+
console.log('drawbridge-agents-sync: DRAWBRIDGE_SKIP_GRAPHIFY set — skipping graphify preflight')
|
|
9
|
+
process.exit(0)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const has = (cmd) => {
|
|
13
|
+
try {
|
|
14
|
+
execFileSync(cmd, [ '--version' ], { stdio: 'ignore' })
|
|
15
|
+
return true
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!has('graphify')) {
|
|
22
|
+
console.log('drawbridge-agents-sync: graphify runtime not found, attempting install...')
|
|
23
|
+
const attempts = [
|
|
24
|
+
[ 'uv', [ 'tool', 'install', 'graphifyy' ] ],
|
|
25
|
+
[ 'pipx', [ 'install', 'graphifyy' ] ],
|
|
26
|
+
[ 'pip', [ 'install', '--user', 'graphifyy' ] ],
|
|
27
|
+
[ 'pip3', [ 'install', '--user', 'graphifyy' ] ]
|
|
28
|
+
]
|
|
29
|
+
for (const [ cmd, args ] of attempts) {
|
|
30
|
+
if (!has(cmd)) continue
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(cmd, args, { stdio: 'inherit' })
|
|
33
|
+
break
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// try the next installer
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!has('graphify')) {
|
|
39
|
+
console.error([
|
|
40
|
+
'drawbridge-agents-sync: could not install the graphify runtime automatically.',
|
|
41
|
+
'Install it, then re-run npm run sync:',
|
|
42
|
+
' uv tool install graphifyy (recommended)',
|
|
43
|
+
' pipx install graphifyy',
|
|
44
|
+
' pip install --user graphifyy',
|
|
45
|
+
'Or set DRAWBRIDGE_SKIP_GRAPHIFY=1 to skip (CI / headless only).'
|
|
46
|
+
].join('\n'))
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Note: we deliberately do NOT run `graphify install` — it writes a graphify section into the
|
|
52
|
+
// repo's CLAUDE.md and a PreToolUse hook, which would mutate a committed file and clash with the
|
|
53
|
+
// shared CLAUDE.md. Agents use the graphify CLI directly (see conventions/graphify.md).
|
|
54
|
+
console.log('drawbridge-agents-sync: graphify runtime ready (run `npx drawbridge-agents-graph` to build the graph)')
|
package/bin/sync-claude.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs')
|
|
3
|
+
const os = require('os')
|
|
3
4
|
const path = require('path')
|
|
5
|
+
const { execFileSync } = require('child_process')
|
|
4
6
|
|
|
5
7
|
const consumerRoot = process.env.INIT_CWD || process.cwd()
|
|
6
8
|
const packageRoot = path.resolve(__dirname, '..')
|
|
7
9
|
|
|
8
|
-
const targets = [
|
|
9
|
-
{ src: path.join(packageRoot, '.claude-template'), dst: path.join(consumerRoot, '.claude'), required: true },
|
|
10
|
-
{ src: path.join(packageRoot, '.root-template'), dst: consumerRoot, required: false },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
10
|
const copied = []
|
|
14
11
|
|
|
15
12
|
const mirror = (src, dst) => {
|
|
@@ -26,16 +23,80 @@ const mirror = (src, dst) => {
|
|
|
26
23
|
}
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// 1. Mirror the committed template dirs into the consumer (existing behaviour).
|
|
27
|
+
const mirrorTemplates = () => {
|
|
28
|
+
const targets = [
|
|
29
|
+
{ src: path.join(packageRoot, '.claude-template'), dst: path.join(consumerRoot, '.claude'), required: true },
|
|
30
|
+
{ src: path.join(packageRoot, '.root-template'), dst: consumerRoot, required: false }
|
|
31
|
+
]
|
|
32
|
+
for (const { src, dst, required } of targets) {
|
|
33
|
+
if (!fs.existsSync(src)) {
|
|
34
|
+
if (required) {
|
|
35
|
+
console.error(`drawbridge-agents-sync: template not found at ${ src }`)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
mirror(src, dst)
|
|
41
|
+
}
|
|
42
|
+
console.log(`drawbridge-agents-sync: ${ copied.length } file(s) mirrored`)
|
|
43
|
+
for (const p of copied) console.log(` ${ p }`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Merge the drift-check Stop hook into the consumer's gitignored .claude/settings.local.json,
|
|
47
|
+
// so nothing is committed to the consumer repo (see conventions/docs-linkage.md).
|
|
48
|
+
const installStopHook = () => {
|
|
49
|
+
const command = 'node ./node_modules/@drawbridge/drawbridge-agents/hooks/drift-check.js'
|
|
50
|
+
const settingsPath = path.join(consumerRoot, '.claude', 'settings.local.json')
|
|
51
|
+
|
|
52
|
+
let settings = {}
|
|
53
|
+
if (fs.existsSync(settingsPath)) {
|
|
54
|
+
try {
|
|
55
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn('drawbridge-agents-sync: could not parse .claude/settings.local.json — leaving Stop hook uninstalled')
|
|
58
|
+
return
|
|
34
59
|
}
|
|
35
|
-
continue
|
|
36
60
|
}
|
|
37
|
-
|
|
61
|
+
|
|
62
|
+
settings.hooks = settings.hooks || {}
|
|
63
|
+
settings.hooks.Stop = settings.hooks.Stop || []
|
|
64
|
+
|
|
65
|
+
const already = settings.hooks.Stop.some((entry) =>
|
|
66
|
+
(entry.hooks || []).some((hook) => typeof hook.command === 'string' && hook.command.includes('drawbridge-agents/hooks/drift-check.js'))
|
|
67
|
+
)
|
|
68
|
+
if (already) return
|
|
69
|
+
|
|
70
|
+
settings.hooks.Stop.push({
|
|
71
|
+
matcher: '',
|
|
72
|
+
hooks: [ { type: 'command', command } ]
|
|
73
|
+
})
|
|
74
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
75
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n')
|
|
76
|
+
console.log('drawbridge-agents-sync: drift-check Stop hook installed in .claude/settings.local.json')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Install the shared skills to the user-level ~/.claude/skills/ (per-machine, so nothing is
|
|
80
|
+
// committed to any consumer repo). Only drawbridge-* skills are managed.
|
|
81
|
+
const installSkills = () => {
|
|
82
|
+
const src = path.join(packageRoot, 'skills')
|
|
83
|
+
if (!fs.existsSync(src)) return
|
|
84
|
+
const dst = path.join(os.homedir(), '.claude', 'skills')
|
|
85
|
+
let count = 0
|
|
86
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
87
|
+
if (!entry.isDirectory() || !entry.name.startsWith('drawbridge-')) continue
|
|
88
|
+
mirror(path.join(src, entry.name), path.join(dst, entry.name))
|
|
89
|
+
count += 1
|
|
90
|
+
}
|
|
91
|
+
if (count > 0) console.log(`drawbridge-agents-sync: ${ count } shared skill(s) installed to ~/.claude/skills/`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Ensure the graphify runtime is present + MCP registered (hard-fails if it can't be).
|
|
95
|
+
const graphifyPreflight = () => {
|
|
96
|
+
execFileSync('node', [ path.join(packageRoot, 'bin', 'preflight-graphify.js') ], { stdio: 'inherit' })
|
|
38
97
|
}
|
|
39
98
|
|
|
40
|
-
|
|
41
|
-
|
|
99
|
+
mirrorTemplates()
|
|
100
|
+
installStopHook()
|
|
101
|
+
installSkills()
|
|
102
|
+
graphifyPreflight()
|
package/claude/CLAUDE.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@../conventions/ecosystem.md
|
|
2
|
+
@../conventions/cross-repo-contracts.md
|
|
1
3
|
@../conventions/karpathy-guidelines.md
|
|
2
4
|
@../conventions/rules.md
|
|
3
5
|
@../conventions/javascript-formatting.md
|
|
@@ -10,3 +12,5 @@
|
|
|
10
12
|
@../conventions/drawbridge-packages.md
|
|
11
13
|
@../conventions/sentry-sdk.md
|
|
12
14
|
@../conventions/app-web-forms.md
|
|
15
|
+
@../conventions/docs-linkage.md
|
|
16
|
+
@../conventions/graphify.md
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Cross-repo contracts
|
|
2
|
+
|
|
3
|
+
Invariants that only break when you touch two repos at once. Know these **before** a
|
|
4
|
+
cross-repo change. Each is a one-liner; fuller detail lives in `drawbridge-docs/reference/`
|
|
5
|
+
and the graph. When you discover a new one, record it here (see the `drawbridge-record-contract`
|
|
6
|
+
skill) so it stops living only in tribal memory.
|
|
7
|
+
|
|
8
|
+
## Shared secrets must match across services
|
|
9
|
+
|
|
10
|
+
- `OAUTH_TOKEN_HMAC_KEY` must be identical in **drawbridge-api** and **drawbridge-sync** — both
|
|
11
|
+
`assertEnv` it at boot. A mismatch silently breaks realtime token verification.
|
|
12
|
+
|
|
13
|
+
## Sync owns reactive side effects
|
|
14
|
+
|
|
15
|
+
- All socket emits from sync go through `drawbridge-sync/lib/emit.js` — never call
|
|
16
|
+
`io.to().emit()` directly from a worker.
|
|
17
|
+
- Cascade cleanup of dependent rows belongs in a sync change-stream listener, not inline in the
|
|
18
|
+
API route that triggered the delete (see `cascade-cleanup.md`).
|
|
19
|
+
- A collection with no `queue/<collection>.js` handler is watched but its events are **silently
|
|
20
|
+
dropped**. Adding a reactive collection means wiring `stream/index.js`, `lib/queue.js`, and
|
|
21
|
+
`queue/index.js` together.
|
|
22
|
+
- BullMQ jobs are keyed on the change-stream resume token as `jobId` for HA dedup across sync
|
|
23
|
+
replicas. Reusing a static `jobId` with `removeOnComplete` makes re-enqueues no-op silently.
|
|
24
|
+
|
|
25
|
+
## Billing / metering
|
|
26
|
+
|
|
27
|
+
- Meter events are **single-writer**: true-ups are `billing.trueup` ledger rows, never meter
|
|
28
|
+
events. A true-up written as a meter event in-window corrupts the reflection poll and strands
|
|
29
|
+
units unbillable.
|
|
30
|
+
- Every `$inc` on a `totals.*` counter needs a matching entry in `drawbridge-sync/lib/totals.js`
|
|
31
|
+
`SOURCES` — that map is the only drift backstop.
|
|
32
|
+
|
|
33
|
+
## `@drawbridge/*` package coordination
|
|
34
|
+
|
|
35
|
+
- Pins are version-exact across the whole family; bumping one package means auditing every
|
|
36
|
+
sibling's `peerDependencies` for a stale pin (see `drawbridge-packages.md`).
|
|
37
|
+
- `stripe`/`shopify` externalise `utils`; a pin mismatch spawns a nested duplicate copy of utils
|
|
38
|
+
in the tree. Keep the pins aligned.
|
|
39
|
+
- Shared packages that wrap a stateful SDK (Sentry, redis, mongodb) must direct-dep it at an
|
|
40
|
+
exact pin, never a loose peer range (see `sentry-sdk.md`).
|
|
41
|
+
|
|
42
|
+
## Import surfaces
|
|
43
|
+
|
|
44
|
+
- Import names against a package's **actual exports** — a missing export resolves to `undefined`
|
|
45
|
+
and throws at call time, not at import. Check `drawbridge-docs/reference/packages.md` (the
|
|
46
|
+
generated export surface) when unsure.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Docs linkage — keep code and specs in sync
|
|
2
|
+
|
|
3
|
+
`drawbridge-docs` is the product spec. Every user-facing behaviour has a user story with a
|
|
4
|
+
**stable ID** (`<domain>/<slug>`, e.g. `auth/email-sign-up`, `campaigns/create-draw`), and
|
|
5
|
+
developer lookups live in `reference/<file>.md` keyed by stable filename + heading anchor.
|
|
6
|
+
Story IDs never change; only the `NN-`/`01-` file-name numbers shift, so never cite those.
|
|
7
|
+
|
|
8
|
+
## Before you change a documented behaviour
|
|
9
|
+
|
|
10
|
+
1. Find the matching story by its **ID** (the story-index table in each
|
|
11
|
+
`user-stories/NN-<domain>/README.md` maps ID → file) or the relevant `reference/` doc, and
|
|
12
|
+
read it first.
|
|
13
|
+
2. Implement **to match the spec**.
|
|
14
|
+
3. If your change would diverge from the spec, **STOP and flag the drift** — do not silently
|
|
15
|
+
diverge. Offer to update the doc in the same change set (use the `write-user-stories` skill
|
|
16
|
+
for a new/edited story), or state explicitly that the doc needs updating.
|
|
17
|
+
4. Keep docs and code linked in one change: a behaviour change edits the story / reference doc
|
|
18
|
+
alongside the code.
|
|
19
|
+
|
|
20
|
+
## Back-reference tags in code
|
|
21
|
+
|
|
22
|
+
Feature-implementing code carries a stable back-reference to the doc it upholds, so a reader
|
|
23
|
+
(and the link validator) can trace code → spec:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// @story campaigns/create-draw
|
|
27
|
+
// Draw close is idempotent: re-entry after payout must not re-run winner selection.
|
|
28
|
+
const closeDraw = async ( draw ) => {
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Use `// @story <domain>/<slug>` for user-story behaviour, or
|
|
32
|
+
`// @doc reference/<file>.md#<anchor>` for a developer-reference invariant.
|
|
33
|
+
- Follow the tag with a brief line stating the invariant or spec it upholds. The tag is the
|
|
34
|
+
machine anchor; the sentence is the meaning.
|
|
35
|
+
- Anchor **where behaviour maps to a documented story** — not on every function. Match the
|
|
36
|
+
surrounding comment density (see `karpathy-guidelines.md`); this is not a licence for
|
|
37
|
+
blanket verbose comments.
|
|
38
|
+
|
|
39
|
+
## When a doc is renamed or an anchor changes
|
|
40
|
+
|
|
41
|
+
Story IDs are stable by design, so the fragile case is a `reference/` file rename or a heading
|
|
42
|
+
(anchor) change. When one happens, grep every sibling repo for the old `@doc`/`@story` tag and
|
|
43
|
+
update every hit in the same change:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
grep -rn "@doc reference/<old-file>" /Users/darrenshea/Projects/drawbridge-* --include="*.js"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The `drawbridge-agents-check-docs` validator scans all repos and fails when any `@story`/`@doc`
|
|
50
|
+
tag no longer resolves to a real story ID or reference anchor — run it (or let the drift hook
|
|
51
|
+
run it) before finishing a change.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Ecosystem map
|
|
2
|
+
|
|
3
|
+
Drawbridge is ~16 `drawbridge-*` repos that communicate **through MongoDB**, not by calling
|
|
4
|
+
each other. This is the baseline; query the graphify MCP (`graphify query`, see
|
|
5
|
+
`graphify.md`) for anything deeper, and see `drawbridge-docs/diagrams/01-system-map.md` for
|
|
6
|
+
the full diagram.
|
|
7
|
+
|
|
8
|
+
## The core rule
|
|
9
|
+
|
|
10
|
+
The API mutates documents; **sync reacts**. Reactive side effects (cache busts, notifications,
|
|
11
|
+
downstream enqueues, counter maintenance beyond the request's own totals) live in
|
|
12
|
+
`drawbridge-sync/queue/<collection>.js`, **not** in API route handlers. All inter-service
|
|
13
|
+
communication is mediated through MongoDB via change streams.
|
|
14
|
+
|
|
15
|
+
## Services (deployed)
|
|
16
|
+
|
|
17
|
+
- **drawbridge-api** — Express HTTP API + Socket.io. Reads/writes MongoDB. Owns route handlers,
|
|
18
|
+
not background workers.
|
|
19
|
+
- **drawbridge-webhooks** — separate Express service; ingests third-party webhooks (Shopify,
|
|
20
|
+
Stripe) and writes `buffer` documents to MongoDB.
|
|
21
|
+
- **drawbridge-sync** — subscribes to MongoDB change streams for ~40 collections
|
|
22
|
+
(`stream/index.js`), routes events to per-collection BullMQ queues (Redis), and runs the
|
|
23
|
+
workers that do all reactive work. Also owns the centralised socket emitter (`lib/emit.js`).
|
|
24
|
+
- **drawbridge-app-web / share / website** — Next.js apps that call the API over HTTPS and
|
|
25
|
+
listen on Socket.io for live updates. `share` serves the public campaign pages.
|
|
26
|
+
- **drawbridge-shopify-app** — hosted embedded Shopify admin app.
|
|
27
|
+
|
|
28
|
+
## Shared libraries (`@drawbridge/*` packages)
|
|
29
|
+
|
|
30
|
+
Consumed by the services; published to npm. **utils** (`@drawbridge/drawbridge-utils`),
|
|
31
|
+
**components** (shared UI + icons), **mongodb**, **redis**, **stripe**, **shopify**,
|
|
32
|
+
**telemetry**, **emails**. Pins are version-exact and must be aligned family-wide when bumped
|
|
33
|
+
(see `drawbridge-packages.md`).
|
|
34
|
+
|
|
35
|
+
## Data + infra
|
|
36
|
+
|
|
37
|
+
- **MongoDB** — single source of truth (~50 collections). Change streams drive sync.
|
|
38
|
+
- **Redis** — backs BullMQ queues (`@drawbridge/drawbridge-redis`, connection `sync:shared`).
|
|
39
|
+
- **drawbridge-docs** — the product spec (user stories + reference). Not deployed.
|
|
40
|
+
- **drawbridge-agents** — this package: the single source of shared agent instructions.
|
|
41
|
+
|
|
42
|
+
## Where to go deeper
|
|
43
|
+
|
|
44
|
+
The load-bearing invariants that break *across* repos are in `cross-repo-contracts.md`. The
|
|
45
|
+
generated `drawbridge-docs/reference/collections.md`, `queues.md`, `packages.md`, and
|
|
46
|
+
`versions.md` are the freshest structural facts. Trace anything else with the graph.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# The family knowledge graph (Graphify)
|
|
2
|
+
|
|
3
|
+
There is a queryable knowledge graph of the whole `drawbridge-*` family — code, docs, and the
|
|
4
|
+
shared packages — built by [Graphify](https://github.com/Graphify-Labs/graphify).
|
|
5
|
+
|
|
6
|
+
## Query the graph before grepping across repos
|
|
7
|
+
|
|
8
|
+
When you need to trace behaviour that spans repos (api ↔ sync ↔ stripe ↔ app-web, who emits an
|
|
9
|
+
event, what depends on a package), **query the graph instead of grepping 16 directories**. The
|
|
10
|
+
cross-repo graph lives at `~/.graphify/global-graph.json`; point the CLI at it with `--graph`:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
graphify query "what connects sync change streams to the billing meter?" --graph ~/.graphify/global-graph.json
|
|
14
|
+
graphify path "drawbridge-app-web" "drawbridge-stripe" --graph ~/.graphify/global-graph.json
|
|
15
|
+
graphify explain "closeDraw" --graph ~/.graphify/global-graph.json
|
|
16
|
+
graphify affected "closeDraw" --graph ~/.graphify/global-graph.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The graph spans all sibling repos (code, via local AST). A semantic build additionally pulls in
|
|
20
|
+
`drawbridge-docs` and richer INFERRED edges — see "Refreshing it".
|
|
21
|
+
|
|
22
|
+
## Refreshing it
|
|
23
|
+
|
|
24
|
+
The graph is as fresh as the last build. After a change that alters cross-repo structure,
|
|
25
|
+
rebuild it:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npx drawbridge-agents-graph
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This re-extracts every repo (code-only, no API key) and merges them into the global graph. The
|
|
32
|
+
drift-check hook nudges when tracked source has changed since the last build. For a semantic
|
|
33
|
+
build that also indexes docs, run `graphify extract <repo> --global --as <repo>` with an LLM
|
|
34
|
+
backend / API key set (slower, costs LLM calls).
|
|
35
|
+
|
|
36
|
+
## Runtime
|
|
37
|
+
|
|
38
|
+
Graphify is a per-machine Python tool; `npm run sync` installs it automatically (see the sync
|
|
39
|
+
preflight — `uv tool install graphifyy`). Set `DRAWBRIDGE_SKIP_GRAPHIFY=1` to skip in CI/headless
|
|
40
|
+
environments. The MCP server (`graphify-mcp <graph.json>`) is available if you prefer it, but is
|
|
41
|
+
**not** auto-registered — `graphify install` would rewrite CLAUDE.md, which we keep managed by
|
|
42
|
+
this package.
|
package/graph/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Family knowledge graph
|
|
2
|
+
|
|
3
|
+
The cross-repo knowledge graph is built by `npx drawbridge-agents-graph` (`bin/graph.js`), which
|
|
4
|
+
runs [Graphify](https://github.com/Graphify-Labs/graphify) headless AST extraction over every
|
|
5
|
+
`drawbridge-*` repo and merges each into the **global** graph at
|
|
6
|
+
`~/.graphify/global-graph.json` (per machine, not committed).
|
|
7
|
+
|
|
8
|
+
That global graph is what agents query — see `conventions/graphify.md`. There are no committed
|
|
9
|
+
artifacts in this directory; it holds this README only. Per-repo extraction output is redirected
|
|
10
|
+
to `~/.graphify/build/` so it never litters the source repos.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop hook: at the end of a task turn, check that docs and the knowledge graph haven't drifted.
|
|
3
|
+
// Blocks (must fix) on unresolved @story/@doc tags; nudges (non-blocking) on a stale graph or
|
|
4
|
+
// likely doc drift. Gated so it never fires on trivial turns. See conventions/docs-linkage.md.
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const { execFileSync } = require('child_process')
|
|
9
|
+
|
|
10
|
+
const SOURCE_EXTENSIONS = new Set([ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs' ])
|
|
11
|
+
const packageRoot = path.resolve(__dirname, '..')
|
|
12
|
+
|
|
13
|
+
const block = (reason) => {
|
|
14
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }))
|
|
15
|
+
process.exit(0)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const nudge = (additionalContext) => {
|
|
19
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext } }))
|
|
20
|
+
process.exit(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const readStdin = () => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Files changed in the working tree (staged, unstaged, untracked), relative to cwd.
|
|
32
|
+
const changedFiles = (cwd) => {
|
|
33
|
+
try {
|
|
34
|
+
const out = execFileSync('git', [ 'status', '--porcelain', '-uall' ], { cwd, encoding: 'utf8' })
|
|
35
|
+
return out.split('\n')
|
|
36
|
+
.map((line) => line.slice(3).trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((rel) => path.join(cwd, rel))
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const run = () => {
|
|
45
|
+
const input = readStdin()
|
|
46
|
+
|
|
47
|
+
// Final loop-safety escape: if we've already blocked repeatedly, let the turn end.
|
|
48
|
+
if (input.stop_hook_active) process.exit(0)
|
|
49
|
+
|
|
50
|
+
// Nothing to enforce on planning turns.
|
|
51
|
+
if (input.permission_mode === 'plan') process.exit(0)
|
|
52
|
+
|
|
53
|
+
const cwd = input.cwd || process.cwd()
|
|
54
|
+
const changed = changedFiles(cwd)
|
|
55
|
+
const changedSource = changed.filter((file) => SOURCE_EXTENSIONS.has(path.extname(file)))
|
|
56
|
+
|
|
57
|
+
// Pure Q&A / non-source turns: nothing to check.
|
|
58
|
+
if (changedSource.length === 0) process.exit(0)
|
|
59
|
+
|
|
60
|
+
// 1. Doc-link validity (blocking) — cheap, precise, quick to fix.
|
|
61
|
+
try {
|
|
62
|
+
execFileSync('node', [ path.join(packageRoot, 'scripts', 'check-doc-links.js') ], { cwd, encoding: 'utf8', stdio: 'pipe' })
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const detail = ((error.stdout || '') + (error.stderr || '')).trim()
|
|
65
|
+
block(`Documentation drift: one or more @story / @doc tags no longer resolve.\n${ detail }\nFix the tag(s) to point at a real drawbridge-docs story ID / reference anchor before finishing.`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const skipGraph = process.env.DRAWBRIDGE_SKIP_GRAPHIFY === '1'
|
|
69
|
+
|
|
70
|
+
// 2. Graph freshness (non-blocking nudge) — a full re-extract is expensive, so alert rather
|
|
71
|
+
// than block on every edit turn. Flip to block() if you want it enforced hard.
|
|
72
|
+
if (!skipGraph) {
|
|
73
|
+
const globalGraph = path.join(os.homedir(), '.graphify', 'global-graph.json')
|
|
74
|
+
let builtAt = 0
|
|
75
|
+
try {
|
|
76
|
+
builtAt = fs.statSync(globalGraph).mtimeMs
|
|
77
|
+
} catch (error) {
|
|
78
|
+
builtAt = 0
|
|
79
|
+
}
|
|
80
|
+
const stale = changedSource.some((file) => {
|
|
81
|
+
try {
|
|
82
|
+
return fs.statSync(file).mtimeMs > builtAt
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
if (stale) {
|
|
88
|
+
nudge('Source changed since the family knowledge graph was last built. If this change alters cross-repo structure, regenerate it: npx drawbridge-agents-graph')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Docs staleness (non-blocking heuristic) — code under a @story-anchored area changed but no
|
|
93
|
+
// drawbridge-docs change is in the working tree.
|
|
94
|
+
const touchedAnchoredCode = changedSource.some((file) => {
|
|
95
|
+
try {
|
|
96
|
+
return /@story\s+[a-z0-9]/.test(fs.readFileSync(file, 'utf8'))
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
const touchedDocs = changed.some((file) => file.includes(`${ path.sep }drawbridge-docs${ path.sep }`))
|
|
102
|
+
if (touchedAnchoredCode && !touchedDocs) {
|
|
103
|
+
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.')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
process.exit(0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
run()
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// Never fail a user's turn because of a hook bug.
|
|
113
|
+
process.exit(0)
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawbridge/drawbridge-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"build": "npm publish"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"drawbridge-agents-sync": "bin/sync-claude.js"
|
|
14
|
+
"drawbridge-agents-sync": "bin/sync-claude.js",
|
|
15
|
+
"drawbridge-agents-graph": "bin/graph.js",
|
|
16
|
+
"drawbridge-agents-check-docs": "scripts/check-doc-links.js"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
17
19
|
"claude",
|
|
@@ -19,6 +21,10 @@
|
|
|
19
21
|
".claude-template",
|
|
20
22
|
".root-template",
|
|
21
23
|
"bin",
|
|
24
|
+
"hooks",
|
|
25
|
+
"scripts",
|
|
26
|
+
"skills",
|
|
27
|
+
"graph",
|
|
22
28
|
"README.md"
|
|
23
29
|
],
|
|
24
30
|
"repository": {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Validates @story / @doc back-reference tags in the drawbridge-* repos against drawbridge-docs.
|
|
3
|
+
// @story tags must resolve to a stable user-story ID; @doc tags to a reference file (+ anchor).
|
|
4
|
+
// Exits non-zero (naming file:line) when any tag no longer resolves. See conventions/docs-linkage.md.
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set([ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs' ])
|
|
9
|
+
const SKIP_DIRS = new Set([ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.turbo', 'graphify-out' ])
|
|
10
|
+
|
|
11
|
+
// The family root is the directory that contains drawbridge-docs. Walk up from cwd to find it.
|
|
12
|
+
const findFamilyRoot = () => {
|
|
13
|
+
let dir = process.env.INIT_CWD || process.cwd()
|
|
14
|
+
for (let i = 0; i < 8; i++) {
|
|
15
|
+
if (fs.existsSync(path.join(dir, 'drawbridge-docs', 'user-stories'))) return dir
|
|
16
|
+
const parent = path.dirname(dir)
|
|
17
|
+
if (parent === dir) break
|
|
18
|
+
dir = parent
|
|
19
|
+
}
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const walk = (dir, onFile) => {
|
|
24
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
27
|
+
walk(path.join(dir, entry.name), onFile)
|
|
28
|
+
} else {
|
|
29
|
+
onFile(path.join(dir, entry.name))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// GitHub-style heading anchor.
|
|
35
|
+
const slug = (text) => text.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-')
|
|
36
|
+
|
|
37
|
+
const buildDocIndex = (docsRoot) => {
|
|
38
|
+
const storyIds = new Set()
|
|
39
|
+
const docTargets = new Set()
|
|
40
|
+
|
|
41
|
+
const storiesRoot = path.join(docsRoot, 'user-stories')
|
|
42
|
+
walk(storiesRoot, (file) => {
|
|
43
|
+
if (path.extname(file) !== '.md') return
|
|
44
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
45
|
+
const re = /\*\*ID\*\*\s*`([^`]+)`/g
|
|
46
|
+
let match
|
|
47
|
+
while ((match = re.exec(text)) !== null) storyIds.add(match[1].trim())
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const referenceRoot = path.join(docsRoot, 'reference')
|
|
51
|
+
if (fs.existsSync(referenceRoot)) {
|
|
52
|
+
for (const name of fs.readdirSync(referenceRoot)) {
|
|
53
|
+
if (path.extname(name) !== '.md') continue
|
|
54
|
+
const rel = `reference/${ name }`
|
|
55
|
+
docTargets.add(rel)
|
|
56
|
+
const text = fs.readFileSync(path.join(referenceRoot, name), 'utf8')
|
|
57
|
+
for (const line of text.split('\n')) {
|
|
58
|
+
const heading = line.match(/^#+\s+(.*)$/)
|
|
59
|
+
if (heading) docTargets.add(`${ rel }#${ slug(heading[1]) }`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { storyIds, docTargets }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const familyRoot = findFamilyRoot()
|
|
68
|
+
if (!familyRoot) {
|
|
69
|
+
console.error('drawbridge-agents-check-docs: could not locate drawbridge-docs; run from within the drawbridge-* family')
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { storyIds, docTargets } = buildDocIndex(path.join(familyRoot, 'drawbridge-docs'))
|
|
74
|
+
const unresolved = []
|
|
75
|
+
|
|
76
|
+
for (const name of fs.readdirSync(familyRoot, { withFileTypes: true })) {
|
|
77
|
+
if (!name.isDirectory() || !name.name.startsWith('drawbridge-')) continue
|
|
78
|
+
// drawbridge-agents is the meta-tooling repo (this script, the hook, the convention docs all
|
|
79
|
+
// contain the literal tag strings) — tags live in the app repos, so skip it.
|
|
80
|
+
if (name.name === 'drawbridge-agents') continue
|
|
81
|
+
const repoRoot = path.join(familyRoot, name.name)
|
|
82
|
+
walk(repoRoot, (file) => {
|
|
83
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(file))) return
|
|
84
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n')
|
|
85
|
+
lines.forEach((line, index) => {
|
|
86
|
+
// @story <domain>/<slug> (stable kebab-case id)
|
|
87
|
+
const story = line.match(/@story\s+([a-z0-9][a-z0-9/-]*)/)
|
|
88
|
+
if (story && !storyIds.has(story[1])) {
|
|
89
|
+
unresolved.push({ file, line: index + 1, tag: `@story ${ story[1] }` })
|
|
90
|
+
}
|
|
91
|
+
// @doc reference/<file>.md#<anchor>
|
|
92
|
+
const doc = line.match(/@doc\s+(reference\/\S+)/)
|
|
93
|
+
if (doc) {
|
|
94
|
+
const target = doc[1].replace(/[.,;)]+$/, '')
|
|
95
|
+
if (!docTargets.has(target)) unresolved.push({ file, line: index + 1, tag: `@doc ${ target }` })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (unresolved.length === 0) {
|
|
102
|
+
console.log(`drawbridge-agents-check-docs: all @story / @doc tags resolve (${ storyIds.size } story IDs, ${ docTargets.size } doc targets indexed)`)
|
|
103
|
+
process.exit(0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.error(`drawbridge-agents-check-docs: ${ unresolved.length } unresolved doc reference(s):`)
|
|
107
|
+
for (const item of unresolved) {
|
|
108
|
+
console.error(` ${ path.relative(familyRoot, item.file) }:${ item.line } ${ item.tag }`)
|
|
109
|
+
}
|
|
110
|
+
process.exit(1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drawbridge-asana-ship-handoff
|
|
3
|
+
description: Use when a feature has shipped (merged to develop) against an Asana task — reassigns the task back to its last commenter, posts a concise summary of what changed plus retest/verify steps, and sets the due date to now.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Asana ship handoff
|
|
7
|
+
|
|
8
|
+
Hand a shipped feature back to whoever last commented on its Asana task, with everything they
|
|
9
|
+
need to retest. Use this right after merging the work to `develop` (which deploys to dev).
|
|
10
|
+
|
|
11
|
+
## Inputs
|
|
12
|
+
|
|
13
|
+
- The Asana task URL or ID for the shipped work. Ask the user for it if it isn't obvious from
|
|
14
|
+
the branch, PR, or conversation — do not guess.
|
|
15
|
+
- The change you shipped (from the diff / PR).
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Load the task.** `asana_get_task` with the task gid (extract it from the URL).
|
|
20
|
+
2. **Find the last commenter.** `asana_get_stories_for_task`, filter to comment stories
|
|
21
|
+
(`type` / `resource_subtype` is a comment, not a system story), take the **most recent**
|
|
22
|
+
one's `created_by`. This is the reassignee — do not hardcode a person. If there are no
|
|
23
|
+
comments, ask the user who to assign to.
|
|
24
|
+
3. **Draft the comment.** A concise summary of what changed (not a changelog dump), then an
|
|
25
|
+
explicit **Retest & verify** section: the exact steps and expected results to confirm the
|
|
26
|
+
fix, and any data/setup needed. Ground the retest steps in the actual behaviour that
|
|
27
|
+
changed.
|
|
28
|
+
4. **Confirm before writing.** This posts to an external system. Show the user the chosen
|
|
29
|
+
assignee and the drafted comment and get a go-ahead, unless they've told you to proceed
|
|
30
|
+
without asking.
|
|
31
|
+
5. **Apply** (on approval):
|
|
32
|
+
- `asana_update_task` — set `assignee` to the last commenter's gid and `due_at` to the
|
|
33
|
+
current date/time (ISO 8601).
|
|
34
|
+
- `asana_create_task_story` — post the comment.
|
|
35
|
+
6. **Report** the task URL, who it was assigned to, and confirm the due date was updated.
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- If the Asana MCP isn't connected, stop and tell the user to authorize it — don't fabricate a
|
|
40
|
+
handoff.
|
|
41
|
+
- Keep the comment user/QA-facing: what to test and expect, not internal implementation detail.
|
|
42
|
+
- Consider running `drawbridge-ship-feature` instead if docs/graph updates for this ship
|
|
43
|
+
haven't been done yet — this skill is the final step of that flow.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drawbridge-explore-ecosystem
|
|
3
|
+
description: Use when tracing a behaviour that spans multiple drawbridge-* repos, or onboarding to how the ecosystem connects — drives the Graphify knowledge graph to answer cross-repo questions instead of grepping 16 directories.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Explore the ecosystem
|
|
7
|
+
|
|
8
|
+
Answer "how does X connect across the repos?" using the family knowledge graph rather than
|
|
9
|
+
grepping every sibling directory. Use it to trace a cross-repo flow (api ↔ sync ↔ stripe ↔
|
|
10
|
+
app-web), find what depends on a package, or orient someone new to the system.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Frame the question** as an entry point or a pair of endpoints: a function, collection,
|
|
15
|
+
event, package, or repo.
|
|
16
|
+
2. **Query the graph** (graphify MCP, or the CLI):
|
|
17
|
+
- `graphify query "<natural-language question>"` — semantic subgraph.
|
|
18
|
+
- `graphify path "<A>" "<B>"` — how two things connect.
|
|
19
|
+
- `graphify explain "<name>"` — what a symbol is and what touches it.
|
|
20
|
+
3. **Verify against source.** The graph tags edges `EXTRACTED` vs `INFERRED`; confirm the
|
|
21
|
+
load-bearing hops in the actual files before relying on them.
|
|
22
|
+
4. **Summarise** the flow for the user — the repos and hops involved, and where the detail
|
|
23
|
+
lives. Cross-check `conventions/cross-repo-contracts.md` for any invariant on the path.
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
- If the graph is stale (the drift hook nudged, or it's been a while), rebuild first:
|
|
28
|
+
`npx drawbridge-agents-graph`.
|
|
29
|
+
- Start broad (`query`) to locate, then narrow (`path` / `explain`) to trace. The graph spans
|
|
30
|
+
code, `drawbridge-docs`, and the `@drawbridge/*` packages.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drawbridge-record-contract
|
|
3
|
+
description: Use when you discover a load-bearing invariant that spans two or more drawbridge-* repos (a shared env key, a centralized side-effect path, a coordination rule) — records it in the canonical cross-repo-contracts convention so it stops living only in memory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Record a cross-repo contract
|
|
7
|
+
|
|
8
|
+
When you find (or get burned by) an invariant that only holds if two repos agree, capture it in
|
|
9
|
+
the canonical place so the next agent inherits it. This is how tribal knowledge becomes a rule
|
|
10
|
+
every agent loads.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
A rule where changing one repo silently breaks another: a shared secret both services read, a
|
|
15
|
+
centralized emitter/handler that must be the only writer, a version-pin or peer-dep coordination
|
|
16
|
+
requirement, a schema/enum shared by copy across repos.
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
1. **Confirm it's real and cross-repo.** Verify the invariant in the actual source of both
|
|
21
|
+
sides (not just a symptom). If it's single-repo, it belongs in that repo's docs, not here.
|
|
22
|
+
2. **Write it into the convention.** Edit `conventions/cross-repo-contracts.md` in
|
|
23
|
+
`drawbridge-agents`: add a one-liner under the right heading — what must hold, which repos,
|
|
24
|
+
and the failure mode if it's violated. Point to fuller detail in `drawbridge-docs/reference/`
|
|
25
|
+
if it warrants a longer write-up (create/extend a reference doc there).
|
|
26
|
+
3. **Flag the siblings.** If the invariant implies other repos need checking or updating, list
|
|
27
|
+
them for the user.
|
|
28
|
+
4. **Ship the convention change** through the normal `drawbridge-agents` publish flow (bump
|
|
29
|
+
version, build, hand the `npm publish --otp` to the user, then bump the pin + `npm run sync`
|
|
30
|
+
in consumers).
|
|
31
|
+
|
|
32
|
+
## Notes
|
|
33
|
+
|
|
34
|
+
- Keep entries terse — this file is always loaded into every agent's context. One line + a
|
|
35
|
+
pointer, not an essay.
|
|
36
|
+
- Don't duplicate an existing entry; extend it if the nuance is new.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drawbridge-ship-feature
|
|
3
|
+
description: Use when finishing and shipping a Drawbridge feature (merging to develop) — the closing checklist that verifies the change, keeps drawbridge-docs and the knowledge graph in sync, and hands the Asana task back for retest.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ship a feature
|
|
7
|
+
|
|
8
|
+
The closing workflow for a Drawbridge change. Run it once the implementation is complete and
|
|
9
|
+
you're about to (or just did) merge to `develop`. It ties together verification, docs, the
|
|
10
|
+
knowledge graph, and the Asana handoff so nothing drifts at ship time.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Verify it works.** Run the project's verification (the `verify` skill / build / lint /
|
|
15
|
+
manual smoke — there's no unit-test framework in api/sync/app-web). Confirm the behaviour
|
|
16
|
+
end-to-end, not just that it compiles.
|
|
17
|
+
2. **Reconcile the spec.** Find the matching `drawbridge-docs` user story by its
|
|
18
|
+
`<domain>/<slug>` ID (or the relevant `reference/` doc). If behaviour changed, update the
|
|
19
|
+
story in the same change (use `write-user-stories` for a new/edited story). If you can't,
|
|
20
|
+
flag the drift explicitly. See `conventions/docs-linkage.md`.
|
|
21
|
+
3. **Confirm doc anchors resolve.** Run `npx drawbridge-agents-check-docs`; fix any unresolved
|
|
22
|
+
`@story` / `@doc` tag. Add an anchor to newly-added feature code where it maps to a story.
|
|
23
|
+
4. **Refresh the graph** if the change altered cross-repo structure: `npx drawbridge-agents-graph`.
|
|
24
|
+
5. **Hand off the task.** Invoke `drawbridge-asana-ship-handoff` with the Asana task to reassign
|
|
25
|
+
to the last commenter, post the change summary + retest steps, and set the due date.
|
|
26
|
+
|
|
27
|
+
## Notes
|
|
28
|
+
|
|
29
|
+
- Steps 2–4 are what the drift-check Stop hook enforces; running them here means you won't get
|
|
30
|
+
blocked at turn end.
|
|
31
|
+
- "Shipped" in this family means merged to `develop` (that's the deployed dev environment).
|