agent-templates 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/ADOPTING.md +57 -57
  2. package/CLAUDE.md +128 -125
  3. package/LICENSE +21 -21
  4. package/README.md +32 -32
  5. package/package.json +32 -32
  6. package/patterns/three-agent-architect-builder-reviewer/README.md +161 -153
  7. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/architect.md +31 -31
  8. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/builder.md +25 -25
  9. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/reviewer.md +33 -33
  10. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/agents/triage.md +31 -31
  11. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/breakdown-prd.md +18 -18
  12. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/nightly-issues.md +14 -14
  13. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/review-ticket.md +14 -14
  14. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-all.md +17 -0
  15. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/start-milestone.md +15 -15
  16. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/commands/verify-delivery.md +20 -20
  17. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/hooks/guard-main-session-writes.mjs +53 -53
  18. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/milestone-dag.mjs +94 -0
  19. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/scripts/publish-tickets.mjs +263 -263
  20. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/settings.json +15 -15
  21. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/nightly-issues.js +139 -139
  22. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/run-milestone.js +223 -223
  23. package/patterns/three-agent-architect-builder-reviewer/scaffold/.claude/workflows/start-all.js +118 -0
  24. package/patterns/three-agent-architect-builder-reviewer/scaffold/INSTALL.md +65 -65
  25. package/patterns/three-agent-architect-builder-reviewer/scaffold/claude-md-snippet.md +38 -37
  26. package/scripts/adopt.mjs +187 -177
  27. package/scripts/build-site.mjs +264 -264
  28. package/scripts/cli.mjs +52 -52
  29. package/templates/pattern-README.template.md +65 -65
  30. package/templates/ticket.template.md +104 -104
  31. package/templates/tracker/github/ISSUE_TEMPLATE/bug-report.md +28 -28
  32. package/templates/tracker/github/ISSUE_TEMPLATE/decision-record.md +25 -25
  33. package/templates/tracker/github/ISSUE_TEMPLATE/task.md +36 -36
  34. package/templates/tracker/github/PULL_REQUEST_TEMPLATE.md +54 -54
  35. package/templates/tracker/gitlab/issue_templates/bug-report.md +23 -23
  36. package/templates/tracker/gitlab/issue_templates/decision-record.md +20 -20
  37. package/templates/tracker/gitlab/issue_templates/task.md +31 -31
  38. package/templates/tracker/gitlab/merge_request_templates/default.md +55 -55
@@ -1,263 +1,263 @@
1
- #!/usr/bin/env node
2
- // publish-tickets.mjs — the ONLY sanctioned issue-creation path for the three-agent
3
- // pattern. Agents never hand-create issues (fabrication risk); this script is
4
- // deterministic and idempotent. Adapted from fx-eye-tracking scripts/create-issues.mjs
5
- // (read 2026-07-17), extended with gh support and a machine-readable summary.
6
- //
7
- // Usage:
8
- // node .claude/scripts/publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]
9
- //
10
- // <module-dir> e.g. docs/prd/01-foo — scans <module-dir>/tickets/*.md
11
- // --create actually create issues (default: dry-run preview)
12
- // --platform tracker CLI; default: autodetect from the origin remote host
13
- //
14
- // Mapping (one issue per ticket file):
15
- // title = "[<id>] <title>" <- the [<id>] prefix is the idempotency key
16
- // body = file content minus frontmatter (the ticket FILE stays the content source of truth)
17
- // labels = module:<module>, size:<size>, agent:<agent> (each only if present)
18
- //
19
- // Idempotency: the existing-issue list is fetched ONCE per run (list endpoints are
20
- // strongly consistent, unlike per-ticket search) and matched client-side by the
21
- // "[<id>]" title prefix. In --create mode a failed fetch aborts BEFORE creating
22
- // anything. Note: the gh path lists up to 1000 issues; beyond that, split modules.
23
- //
24
- // Last line of stdout is machine-readable for /start-milestone:
25
- // PUBLISH-SUMMARY-JSON: [{"id","path","title","issue","error"?}]
26
- // Exit codes: 0 = ok (invalid tickets are reported in the summary, not fatal);
27
- // 1 = bad invocation, missing CLI in --create mode, fetch failure in
28
- // --create mode, or any create failure (summary still printed).
29
-
30
- import { execFileSync } from 'node:child_process'
31
- import { readdirSync, readFileSync, statSync } from 'node:fs'
32
- import { join } from 'node:path'
33
-
34
- const argv = process.argv.slice(2)
35
- const CREATE = argv.includes('--create')
36
-
37
- const platformIx = argv.indexOf('--platform')
38
- let PLATFORM = ''
39
- if (platformIx !== -1) {
40
- PLATFORM = argv[platformIx + 1] || ''
41
- if (!PLATFORM || PLATFORM.startsWith('--')) {
42
- console.error('missing or invalid --platform value (expected gh or glab)')
43
- process.exit(1)
44
- }
45
- }
46
- const moduleDir = argv.find((a, i) => !a.startsWith('--') && (platformIx === -1 || i !== platformIx + 1))
47
-
48
- if (!moduleDir) {
49
- console.error('usage: node publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]')
50
- process.exit(1)
51
- }
52
- const ticketsDir = join(moduleDir, 'tickets')
53
- let ticketsDirOk = false
54
- try { ticketsDirOk = statSync(ticketsDir).isDirectory() } catch {}
55
- if (!ticketsDirOk) {
56
- console.error(`no tickets directory: ${ticketsDir}`)
57
- process.exit(1)
58
- }
59
-
60
- const run = (bin, args, opts = {}) => execFileSync(bin, args, { encoding: 'utf8', ...opts })
61
-
62
- // GH_BIN / GLAB_BIN env overrides (precedent: fx-eye-tracking's GLAB_BIN) for
63
- // non-PATH binaries and test doubles. The value may include leading args, e.g.
64
- // GH_BIN="node tools/fake-gh.mjs" (no spaces in the path itself).
65
- const cli = (platform, args, opts = {}) => {
66
- const raw = platform === 'gh' ? process.env.GH_BIN || 'gh' : process.env.GLAB_BIN || 'glab'
67
- const parts = raw.split(' ')
68
- return run(parts[0], [...parts.slice(1), ...args], opts)
69
- }
70
-
71
- let detectedFrom = ''
72
- if (!PLATFORM) {
73
- try {
74
- const origin = run('git', ['remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] }).trim()
75
- const host = (origin.match(/(?:@|:\/\/)([^/:]+)[/:]/) || [])[1] || ''
76
- PLATFORM = /(^|\.)gitlab\./.test(host) || host.includes('gitlab') ? 'glab' : 'gh'
77
- detectedFrom = host || origin
78
- } catch {
79
- PLATFORM = 'gh'
80
- detectedFrom = 'no origin remote'
81
- }
82
- }
83
- if (PLATFORM !== 'gh' && PLATFORM !== 'glab') {
84
- console.error(`unknown platform: ${PLATFORM} (expected gh or glab)`)
85
- process.exit(1)
86
- }
87
- console.log(`platform: ${PLATFORM}${detectedFrom ? ` (autodetected from ${detectedFrom}; override with --platform)` : ''}`)
88
-
89
- let cliOk = false
90
- try {
91
- cli(PLATFORM, ['auth', 'status'], { stdio: ['ignore', 'ignore', 'ignore'] })
92
- cliOk = true
93
- } catch {}
94
- if (CREATE && !cliOk) {
95
- console.error(`x ${PLATFORM} not found or not authenticated — install it and run \`${PLATFORM} auth login\`.`)
96
- process.exit(1)
97
- }
98
- if (!cliOk) {
99
- console.log(`(note) ${PLATFORM} unavailable — dry-run previews without checking which issues already exist.`)
100
- }
101
-
102
- // Fetch the existing-issue list ONCE; match "[<id>]" prefixes client-side.
103
- // Returns [{number, title}] or null when unavailable.
104
- const fetchExistingIssues = () => {
105
- if (!cliOk) return null
106
- try {
107
- if (PLATFORM === 'gh') {
108
- const out = cli('gh', ['issue', 'list', '--state', 'all', '--limit', '1000', '--json', 'number,title'])
109
- return JSON.parse(out).map((i) => ({ number: i.number, title: i.title }))
110
- }
111
- try {
112
- const out = cli('glab', ['issue', 'list', '--all', '--output', 'json'])
113
- return JSON.parse(out).map((i) => ({ number: i.iid ?? i.id, title: i.title }))
114
- } catch {
115
- // older glab without --output json: parse per LINE so the number always
116
- // belongs to the line whose title matches (never "first #N in the blob")
117
- const out = cli('glab', ['issue', 'list', '--all'])
118
- return out
119
- .split('\n')
120
- .map((l) => l.match(/^#(\d+)\s+(.*)$/))
121
- .filter(Boolean)
122
- .map((m) => ({ number: Number(m[1]), title: m[2] }))
123
- }
124
- } catch {
125
- return null
126
- }
127
- }
128
-
129
- const existingIssues = fetchExistingIssues()
130
- if (CREATE && existingIssues === null) {
131
- console.error('x could not fetch the existing-issue list — refusing to create without a reliable existence check.')
132
- process.exit(1)
133
- }
134
- // Returns an issue number, null (not found), or 'ambiguous' (mentions of "[<id>]"
135
- // exist but none is a clean title prefix — creating would risk a duplicate, guessing
136
- // would risk closing the wrong issue later, so the ticket is skipped with an error).
137
- const findExisting = (id) => {
138
- if (!existingIssues) return null
139
- const marker = `[${id}]`
140
- const hits = existingIssues.filter((i) => String(i.title).includes(marker))
141
- const exact = hits.find((i) => String(i.title).trim().startsWith(marker))
142
- if (exact) return exact.number
143
- if (hits.length > 0) return 'ambiguous'
144
- return null
145
- }
146
-
147
- const field = (fm, name) => {
148
- const m = fm.match(new RegExp(`^${name}\\s*:\\s*(.+)$`, 'm'))
149
- if (!m) return ''
150
- // strip one pair of surrounding YAML quotes (and unescape \" inside them) so
151
- // quoted titles don't leak quote characters into issue titles
152
- const v = m[1].trim()
153
- const stripped = v.replace(/^(['"])(.*)\1$/s, '$2')
154
- return stripped === v ? v : stripped.replace(/\\"/g, '"')
155
- }
156
-
157
- const createIssue = (issueTitle, body, labels) => {
158
- const attempt = (withLabels) => {
159
- if (PLATFORM === 'gh') {
160
- const args = ['issue', 'create', '--title', issueTitle, '--body-file', '-']
161
- if (withLabels) for (const l of labels) args.push('--label', l)
162
- return cli('gh', args, { input: body }).trim()
163
- }
164
- const args = ['issue', 'create', '--title', issueTitle, '--description', body]
165
- if (withLabels && labels.length) args.push('--label', labels.join(','))
166
- return cli('glab', args).trim()
167
- }
168
- try {
169
- return attempt(true)
170
- } catch (e) {
171
- if (!labels.length) throw e
172
- console.error(` (warn) create with labels failed — retrying without labels (create them in the tracker to keep labeling)`)
173
- return attempt(false)
174
- }
175
- }
176
-
177
- const summary = []
178
- const seenIds = new Set()
179
- let created = 0
180
- let skipped = 0
181
- let planned = 0
182
- let invalid = 0
183
- let createFailed = 0
184
-
185
- for (const f of readdirSync(ticketsDir).filter((n) => n.endsWith('.md')).sort()) {
186
- const path = join(ticketsDir, f).replaceAll('\\', '/')
187
- const text = readFileSync(path, 'utf8').replace(/^/, '') // strip BOM (PowerShell 5.1 utf8 writes one)
188
- const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
189
- if (!fmMatch) {
190
- console.log(` skip (no frontmatter): ${path}`)
191
- summary.push({ id: null, path, title: null, issue: null, error: 'no-frontmatter' })
192
- invalid++
193
- continue
194
- }
195
- const fm = fmMatch[1]
196
- const id = field(fm, 'id')
197
- const title = field(fm, 'title')
198
- if (!id || !title) {
199
- console.log(` skip (missing id/title): ${path}`)
200
- summary.push({ id: id || null, path, title: null, issue: null, error: 'missing-id-title' })
201
- invalid++
202
- continue
203
- }
204
- if (seenIds.has(id)) {
205
- console.log(` skip (duplicate id ${id}): ${path}`)
206
- summary.push({ id, path, title: null, issue: null, error: 'duplicate-id' })
207
- invalid++
208
- continue
209
- }
210
- seenIds.add(id)
211
-
212
- const issueTitle = `[${id}] ${title}`
213
- const body = text.slice(fmMatch[0].length).trimStart()
214
- const labels = [
215
- field(fm, 'module') && `module:${field(fm, 'module')}`,
216
- field(fm, 'size') && `size:${field(fm, 'size')}`,
217
- field(fm, 'agent') && `agent:${field(fm, 'agent')}`,
218
- ].filter(Boolean)
219
-
220
- const existing = findExisting(id)
221
- if (existing === 'ambiguous') {
222
- console.error(`x skip ${id}: issues mention "[${id}]" but none has it as a clean title prefix — resolve by hand`)
223
- summary.push({ id, path, title: issueTitle, issue: null, error: 'ambiguous-existing' })
224
- invalid++
225
- continue
226
- }
227
- if (existing) {
228
- console.log(`= skip ${id}: issue #${existing} already exists`)
229
- summary.push({ id, path, title: issueTitle, issue: existing })
230
- skipped++
231
- continue
232
- }
233
-
234
- if (!CREATE) {
235
- console.log(`+ would create ${id}: "${issueTitle}" labels=[${labels.join(',')}]`)
236
- summary.push({ id, path, title: issueTitle, issue: null })
237
- planned++
238
- continue
239
- }
240
-
241
- try {
242
- const out = createIssue(issueTitle, body, labels)
243
- const lastLine = out.split('\n').filter(Boolean).pop() || ''
244
- const num = (lastLine.match(/\/issues\/(\d+)\s*$/) || out.match(/#(\d+)/) || [])[1]
245
- console.log(`+ created ${id}: ${lastLine}`)
246
- summary.push({ id, path, title: issueTitle, issue: num ? Number(num) : null })
247
- created++
248
- } catch (e) {
249
- const msg = String(e && e.message ? e.message : e).split('\n')[0]
250
- console.error(`x create failed for ${id}: ${msg}`)
251
- summary.push({ id, path, title: issueTitle, issue: null, error: `create-failed: ${msg}` })
252
- createFailed++
253
- }
254
- }
255
-
256
- const invalidNote = invalid ? `, invalid: ${invalid}` : ''
257
- console.log(
258
- CREATE
259
- ? `CREATED: ${created}, already existed: ${skipped}, failed: ${createFailed}${invalidNote}.`
260
- : `DRY-RUN: ${planned} would be created, ${skipped} already exist${invalidNote}. Re-run with --create after Gate 1 sign-off.`
261
- )
262
- console.log('PUBLISH-SUMMARY-JSON: ' + JSON.stringify(summary))
263
- process.exit(createFailed ? 1 : 0)
1
+ #!/usr/bin/env node
2
+ // publish-tickets.mjs — the ONLY sanctioned issue-creation path for the three-agent
3
+ // pattern. Agents never hand-create issues (fabrication risk); this script is
4
+ // deterministic and idempotent. Adapted from fx-eye-tracking scripts/create-issues.mjs
5
+ // (read 2026-07-17), extended with gh support and a machine-readable summary.
6
+ //
7
+ // Usage:
8
+ // node .claude/scripts/publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]
9
+ //
10
+ // <module-dir> e.g. docs/prd/01-foo — scans <module-dir>/tickets/*.md
11
+ // --create actually create issues (default: dry-run preview)
12
+ // --platform tracker CLI; default: autodetect from the origin remote host
13
+ //
14
+ // Mapping (one issue per ticket file):
15
+ // title = "[<id>] <title>" <- the [<id>] prefix is the idempotency key
16
+ // body = file content minus frontmatter (the ticket FILE stays the content source of truth)
17
+ // labels = module:<module>, size:<size>, agent:<agent> (each only if present)
18
+ //
19
+ // Idempotency: the existing-issue list is fetched ONCE per run (list endpoints are
20
+ // strongly consistent, unlike per-ticket search) and matched client-side by the
21
+ // "[<id>]" title prefix. In --create mode a failed fetch aborts BEFORE creating
22
+ // anything. Note: the gh path lists up to 1000 issues; beyond that, split modules.
23
+ //
24
+ // Last line of stdout is machine-readable for /start-milestone:
25
+ // PUBLISH-SUMMARY-JSON: [{"id","path","title","issue","error"?}]
26
+ // Exit codes: 0 = ok (invalid tickets are reported in the summary, not fatal);
27
+ // 1 = bad invocation, missing CLI in --create mode, fetch failure in
28
+ // --create mode, or any create failure (summary still printed).
29
+
30
+ import { execFileSync } from 'node:child_process'
31
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
32
+ import { join } from 'node:path'
33
+
34
+ const argv = process.argv.slice(2)
35
+ const CREATE = argv.includes('--create')
36
+
37
+ const platformIx = argv.indexOf('--platform')
38
+ let PLATFORM = ''
39
+ if (platformIx !== -1) {
40
+ PLATFORM = argv[platformIx + 1] || ''
41
+ if (!PLATFORM || PLATFORM.startsWith('--')) {
42
+ console.error('missing or invalid --platform value (expected gh or glab)')
43
+ process.exit(1)
44
+ }
45
+ }
46
+ const moduleDir = argv.find((a, i) => !a.startsWith('--') && (platformIx === -1 || i !== platformIx + 1))
47
+
48
+ if (!moduleDir) {
49
+ console.error('usage: node publish-tickets.mjs <module-dir> [--create] [--platform gh|glab]')
50
+ process.exit(1)
51
+ }
52
+ const ticketsDir = join(moduleDir, 'tickets')
53
+ let ticketsDirOk = false
54
+ try { ticketsDirOk = statSync(ticketsDir).isDirectory() } catch {}
55
+ if (!ticketsDirOk) {
56
+ console.error(`no tickets directory: ${ticketsDir}`)
57
+ process.exit(1)
58
+ }
59
+
60
+ const run = (bin, args, opts = {}) => execFileSync(bin, args, { encoding: 'utf8', ...opts })
61
+
62
+ // GH_BIN / GLAB_BIN env overrides (precedent: fx-eye-tracking's GLAB_BIN) for
63
+ // non-PATH binaries and test doubles. The value may include leading args, e.g.
64
+ // GH_BIN="node tools/fake-gh.mjs" (no spaces in the path itself).
65
+ const cli = (platform, args, opts = {}) => {
66
+ const raw = platform === 'gh' ? process.env.GH_BIN || 'gh' : process.env.GLAB_BIN || 'glab'
67
+ const parts = raw.split(' ')
68
+ return run(parts[0], [...parts.slice(1), ...args], opts)
69
+ }
70
+
71
+ let detectedFrom = ''
72
+ if (!PLATFORM) {
73
+ try {
74
+ const origin = run('git', ['remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] }).trim()
75
+ const host = (origin.match(/(?:@|:\/\/)([^/:]+)[/:]/) || [])[1] || ''
76
+ PLATFORM = /(^|\.)gitlab\./.test(host) || host.includes('gitlab') ? 'glab' : 'gh'
77
+ detectedFrom = host || origin
78
+ } catch {
79
+ PLATFORM = 'gh'
80
+ detectedFrom = 'no origin remote'
81
+ }
82
+ }
83
+ if (PLATFORM !== 'gh' && PLATFORM !== 'glab') {
84
+ console.error(`unknown platform: ${PLATFORM} (expected gh or glab)`)
85
+ process.exit(1)
86
+ }
87
+ console.log(`platform: ${PLATFORM}${detectedFrom ? ` (autodetected from ${detectedFrom}; override with --platform)` : ''}`)
88
+
89
+ let cliOk = false
90
+ try {
91
+ cli(PLATFORM, ['auth', 'status'], { stdio: ['ignore', 'ignore', 'ignore'] })
92
+ cliOk = true
93
+ } catch {}
94
+ if (CREATE && !cliOk) {
95
+ console.error(`x ${PLATFORM} not found or not authenticated — install it and run \`${PLATFORM} auth login\`.`)
96
+ process.exit(1)
97
+ }
98
+ if (!cliOk) {
99
+ console.log(`(note) ${PLATFORM} unavailable — dry-run previews without checking which issues already exist.`)
100
+ }
101
+
102
+ // Fetch the existing-issue list ONCE; match "[<id>]" prefixes client-side.
103
+ // Returns [{number, title}] or null when unavailable.
104
+ const fetchExistingIssues = () => {
105
+ if (!cliOk) return null
106
+ try {
107
+ if (PLATFORM === 'gh') {
108
+ const out = cli('gh', ['issue', 'list', '--state', 'all', '--limit', '1000', '--json', 'number,title'])
109
+ return JSON.parse(out).map((i) => ({ number: i.number, title: i.title }))
110
+ }
111
+ try {
112
+ const out = cli('glab', ['issue', 'list', '--all', '--output', 'json'])
113
+ return JSON.parse(out).map((i) => ({ number: i.iid ?? i.id, title: i.title }))
114
+ } catch {
115
+ // older glab without --output json: parse per LINE so the number always
116
+ // belongs to the line whose title matches (never "first #N in the blob")
117
+ const out = cli('glab', ['issue', 'list', '--all'])
118
+ return out
119
+ .split('\n')
120
+ .map((l) => l.match(/^#(\d+)\s+(.*)$/))
121
+ .filter(Boolean)
122
+ .map((m) => ({ number: Number(m[1]), title: m[2] }))
123
+ }
124
+ } catch {
125
+ return null
126
+ }
127
+ }
128
+
129
+ const existingIssues = fetchExistingIssues()
130
+ if (CREATE && existingIssues === null) {
131
+ console.error('x could not fetch the existing-issue list — refusing to create without a reliable existence check.')
132
+ process.exit(1)
133
+ }
134
+ // Returns an issue number, null (not found), or 'ambiguous' (mentions of "[<id>]"
135
+ // exist but none is a clean title prefix — creating would risk a duplicate, guessing
136
+ // would risk closing the wrong issue later, so the ticket is skipped with an error).
137
+ const findExisting = (id) => {
138
+ if (!existingIssues) return null
139
+ const marker = `[${id}]`
140
+ const hits = existingIssues.filter((i) => String(i.title).includes(marker))
141
+ const exact = hits.find((i) => String(i.title).trim().startsWith(marker))
142
+ if (exact) return exact.number
143
+ if (hits.length > 0) return 'ambiguous'
144
+ return null
145
+ }
146
+
147
+ const field = (fm, name) => {
148
+ const m = fm.match(new RegExp(`^${name}\\s*:\\s*(.+)$`, 'm'))
149
+ if (!m) return ''
150
+ // strip one pair of surrounding YAML quotes (and unescape \" inside them) so
151
+ // quoted titles don't leak quote characters into issue titles
152
+ const v = m[1].trim()
153
+ const stripped = v.replace(/^(['"])(.*)\1$/s, '$2')
154
+ return stripped === v ? v : stripped.replace(/\\"/g, '"')
155
+ }
156
+
157
+ const createIssue = (issueTitle, body, labels) => {
158
+ const attempt = (withLabels) => {
159
+ if (PLATFORM === 'gh') {
160
+ const args = ['issue', 'create', '--title', issueTitle, '--body-file', '-']
161
+ if (withLabels) for (const l of labels) args.push('--label', l)
162
+ return cli('gh', args, { input: body }).trim()
163
+ }
164
+ const args = ['issue', 'create', '--title', issueTitle, '--description', body]
165
+ if (withLabels && labels.length) args.push('--label', labels.join(','))
166
+ return cli('glab', args).trim()
167
+ }
168
+ try {
169
+ return attempt(true)
170
+ } catch (e) {
171
+ if (!labels.length) throw e
172
+ console.error(` (warn) create with labels failed — retrying without labels (create them in the tracker to keep labeling)`)
173
+ return attempt(false)
174
+ }
175
+ }
176
+
177
+ const summary = []
178
+ const seenIds = new Set()
179
+ let created = 0
180
+ let skipped = 0
181
+ let planned = 0
182
+ let invalid = 0
183
+ let createFailed = 0
184
+
185
+ for (const f of readdirSync(ticketsDir).filter((n) => n.endsWith('.md')).sort()) {
186
+ const path = join(ticketsDir, f).replaceAll('\\', '/')
187
+ const text = readFileSync(path, 'utf8').replace(/^/, '') // strip BOM (PowerShell 5.1 utf8 writes one)
188
+ const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
189
+ if (!fmMatch) {
190
+ console.log(` skip (no frontmatter): ${path}`)
191
+ summary.push({ id: null, path, title: null, issue: null, error: 'no-frontmatter' })
192
+ invalid++
193
+ continue
194
+ }
195
+ const fm = fmMatch[1]
196
+ const id = field(fm, 'id')
197
+ const title = field(fm, 'title')
198
+ if (!id || !title) {
199
+ console.log(` skip (missing id/title): ${path}`)
200
+ summary.push({ id: id || null, path, title: null, issue: null, error: 'missing-id-title' })
201
+ invalid++
202
+ continue
203
+ }
204
+ if (seenIds.has(id)) {
205
+ console.log(` skip (duplicate id ${id}): ${path}`)
206
+ summary.push({ id, path, title: null, issue: null, error: 'duplicate-id' })
207
+ invalid++
208
+ continue
209
+ }
210
+ seenIds.add(id)
211
+
212
+ const issueTitle = `[${id}] ${title}`
213
+ const body = text.slice(fmMatch[0].length).trimStart()
214
+ const labels = [
215
+ field(fm, 'module') && `module:${field(fm, 'module')}`,
216
+ field(fm, 'size') && `size:${field(fm, 'size')}`,
217
+ field(fm, 'agent') && `agent:${field(fm, 'agent')}`,
218
+ ].filter(Boolean)
219
+
220
+ const existing = findExisting(id)
221
+ if (existing === 'ambiguous') {
222
+ console.error(`x skip ${id}: issues mention "[${id}]" but none has it as a clean title prefix — resolve by hand`)
223
+ summary.push({ id, path, title: issueTitle, issue: null, error: 'ambiguous-existing' })
224
+ invalid++
225
+ continue
226
+ }
227
+ if (existing) {
228
+ console.log(`= skip ${id}: issue #${existing} already exists`)
229
+ summary.push({ id, path, title: issueTitle, issue: existing })
230
+ skipped++
231
+ continue
232
+ }
233
+
234
+ if (!CREATE) {
235
+ console.log(`+ would create ${id}: "${issueTitle}" labels=[${labels.join(',')}]`)
236
+ summary.push({ id, path, title: issueTitle, issue: null })
237
+ planned++
238
+ continue
239
+ }
240
+
241
+ try {
242
+ const out = createIssue(issueTitle, body, labels)
243
+ const lastLine = out.split('\n').filter(Boolean).pop() || ''
244
+ const num = (lastLine.match(/\/issues\/(\d+)\s*$/) || out.match(/#(\d+)/) || [])[1]
245
+ console.log(`+ created ${id}: ${lastLine}`)
246
+ summary.push({ id, path, title: issueTitle, issue: num ? Number(num) : null })
247
+ created++
248
+ } catch (e) {
249
+ const msg = String(e && e.message ? e.message : e).split('\n')[0]
250
+ console.error(`x create failed for ${id}: ${msg}`)
251
+ summary.push({ id, path, title: issueTitle, issue: null, error: `create-failed: ${msg}` })
252
+ createFailed++
253
+ }
254
+ }
255
+
256
+ const invalidNote = invalid ? `, invalid: ${invalid}` : ''
257
+ console.log(
258
+ CREATE
259
+ ? `CREATED: ${created}, already existed: ${skipped}, failed: ${createFailed}${invalidNote}.`
260
+ : `DRY-RUN: ${planned} would be created, ${skipped} already exist${invalidNote}. Re-run with --create after Gate 1 sign-off.`
261
+ )
262
+ console.log('PUBLISH-SUMMARY-JSON: ' + JSON.stringify(summary))
263
+ process.exit(createFailed ? 1 : 0)
@@ -1,15 +1,15 @@
1
- {
2
- "hooks": {
3
- "PreToolUse": [
4
- {
5
- "matcher": "Edit|Write|MultiEdit|NotebookEdit",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "node .claude/hooks/guard-main-session-writes.mjs"
10
- }
11
- ]
12
- }
13
- ]
14
- }
15
- }
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node .claude/hooks/guard-main-session-writes.mjs"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }