@vegastack/vegafactory 0.19.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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +417 -0
  3. package/dist/dashboard-artifact.json +21240 -0
  4. package/dist/index.js +22752 -0
  5. package/dist/run-wrapper.js +377 -0
  6. package/package.json +46 -0
  7. package/skill/dev-architect/SKILL.md +106 -0
  8. package/skill/dev-architect/agents/openai.yaml +4 -0
  9. package/skill/dev-architect/references/ai-agents.md +96 -0
  10. package/skill/dev-architect/references/conventions.md +113 -0
  11. package/skill/dev-architect/references/data.md +89 -0
  12. package/skill/dev-architect/references/infra.md +99 -0
  13. package/skill/dev-architect/references/mobile.md +75 -0
  14. package/skill/dev-architect/references/pinned-facts.md +133 -0
  15. package/skill/dev-architect/references/principles.md +117 -0
  16. package/skill/dev-architect/references/security.md +90 -0
  17. package/skill/dev-architect/references/stack.md +38 -0
  18. package/skill/dev-architect/references/web.md +102 -0
  19. package/skill/dev-architect/refresh/REFRESH.md +31 -0
  20. package/skill/dev-architect/refresh/sources.json +239 -0
  21. package/skill/dev-chronicle/SKILL.md +52 -0
  22. package/skill/dev-chronicle/agents/openai.yaml +4 -0
  23. package/skill/dev-chronicle/references/conventions.md +113 -0
  24. package/skill/dev-chronicle/references/styles.md +75 -0
  25. package/skill/dev-chronicle/refresh/REFRESH.md +3 -0
  26. package/skill/dev-chronicle/refresh/sources.json +6 -0
  27. package/skill/dev-debug/SKILL.md +43 -0
  28. package/skill/dev-debug/agents/openai.yaml +4 -0
  29. package/skill/dev-debug/references/conventions.md +113 -0
  30. package/skill/dev-debug/references/loop-ladder.md +20 -0
  31. package/skill/dev-debug/refresh/REFRESH.md +3 -0
  32. package/skill/dev-debug/refresh/sources.json +6 -0
  33. package/skill/dev-implement/SKILL.md +81 -0
  34. package/skill/dev-implement/agents/openai.yaml +4 -0
  35. package/skill/dev-implement/assets/workflows/implement-children.js +12 -0
  36. package/skill/dev-implement/references/ask-route.md +80 -0
  37. package/skill/dev-implement/references/changelog-and-chronicle.md +21 -0
  38. package/skill/dev-implement/references/conventions.md +113 -0
  39. package/skill/dev-implement/references/ledger-and-resume.md +52 -0
  40. package/skill/dev-implement/references/parallel-children.md +35 -0
  41. package/skill/dev-implement/references/worktrees.md +63 -0
  42. package/skill/dev-implement/refresh/REFRESH.md +3 -0
  43. package/skill/dev-implement/refresh/sources.json +6 -0
  44. package/skill/dev-implement/scripts/children.mjs +479 -0
  45. package/skill/dev-implement/scripts/effective-policy.mjs +694 -0
  46. package/skill/dev-implement/scripts/evidence-check.mjs +107 -0
  47. package/skill/dev-implement/scripts/evidence-upload.mjs +181 -0
  48. package/skill/dev-implement/scripts/learning.mjs +41 -0
  49. package/skill/dev-implement/scripts/lib/approval.mjs +1182 -0
  50. package/skill/dev-implement/scripts/lib/gh.mjs +101 -0
  51. package/skill/dev-implement/scripts/preflight.mjs +153 -0
  52. package/skill/dev-implement/scripts/questions.mjs +381 -0
  53. package/skill/dev-implement/scripts/reclaim.mjs +116 -0
  54. package/skill/dev-implement/scripts/recovery.mjs +213 -0
  55. package/skill/dev-implement/scripts/worktree.mjs +991 -0
  56. package/skill/dev-intake/SKILL.md +80 -0
  57. package/skill/dev-intake/agents/openai.yaml +4 -0
  58. package/skill/dev-intake/references/ask-route.md +80 -0
  59. package/skill/dev-intake/references/brief-template.md +89 -0
  60. package/skill/dev-intake/references/conventions.md +113 -0
  61. package/skill/dev-intake/refresh/REFRESH.md +3 -0
  62. package/skill/dev-intake/refresh/sources.json +6 -0
  63. package/skill/dev-intake/scripts/brief-lint.mjs +87 -0
  64. package/skill/dev-intake/scripts/lib/approval.mjs +1182 -0
  65. package/skill/dev-intake/scripts/questions.mjs +381 -0
  66. package/skill/dev-plan/SKILL.md +54 -0
  67. package/skill/dev-plan/agents/openai.yaml +4 -0
  68. package/skill/dev-plan/references/ask-route.md +80 -0
  69. package/skill/dev-plan/references/conventions.md +113 -0
  70. package/skill/dev-plan/references/plan-format.md +82 -0
  71. package/skill/dev-plan/refresh/REFRESH.md +3 -0
  72. package/skill/dev-plan/refresh/sources.json +6 -0
  73. package/skill/dev-plan/scripts/lib/approval.mjs +1182 -0
  74. package/skill/dev-plan/scripts/plan-lint.mjs +319 -0
  75. package/skill/dev-plan/scripts/questions.mjs +381 -0
  76. package/skill/dev-review/SKILL.md +76 -0
  77. package/skill/dev-review/agents/openai.yaml +4 -0
  78. package/skill/dev-review/assets/review-known-patterns.md.template +32 -0
  79. package/skill/dev-review/references/conventions.md +113 -0
  80. package/skill/dev-review/references/cross-agent.md +39 -0
  81. package/skill/dev-review/references/dispatch-prompts.md +121 -0
  82. package/skill/dev-review/references/security-axis.md +45 -0
  83. package/skill/dev-review/refresh/REFRESH.md +5 -0
  84. package/skill/dev-review/refresh/sources.json +6 -0
  85. package/skill/dev-setup/SKILL.md +113 -0
  86. package/skill/dev-setup/agents/openai.yaml +4 -0
  87. package/skill/dev-setup/assets/agents-section.md.template +24 -0
  88. package/skill/dev-setup/assets/dev-profile.md.template +102 -0
  89. package/skill/dev-setup/assets/factory-board.yml.template +162 -0
  90. package/skill/dev-setup/assets/hooks/decision-nudge.mjs +30 -0
  91. package/skill/dev-setup/assets/hooks/prompt-skill-mention.mjs +29 -0
  92. package/skill/dev-setup/assets/hooks/session-end.mjs +14 -0
  93. package/skill/dev-setup/assets/hooks/session-start.mjs +178 -0
  94. package/skill/dev-setup/assets/hooks/ship-guard.mjs +738 -0
  95. package/skill/dev-setup/assets/hooks/skill-activated.mjs +47 -0
  96. package/skill/dev-setup/assets/hooks/stop-heartbeat.mjs +14 -0
  97. package/skill/dev-setup/assets/workflows/implement-children.js +12 -0
  98. package/skill/dev-setup/references/ask-route.md +80 -0
  99. package/skill/dev-setup/references/conventions.md +113 -0
  100. package/skill/dev-setup/references/github-app.md +286 -0
  101. package/skill/dev-setup/references/harness-facts.md +169 -0
  102. package/skill/dev-setup/references/stack-playbooks.md +124 -0
  103. package/skill/dev-setup/refresh/REFRESH.md +13 -0
  104. package/skill/dev-setup/refresh/sources.json +602 -0
  105. package/skill/dev-setup/scripts/effective-policy.mjs +694 -0
  106. package/skill/dev-setup/scripts/questions.mjs +381 -0
  107. package/skill/dev-setup/scripts/ship-policy.mjs +195 -0
  108. package/skill/dev-ship/SKILL.md +56 -0
  109. package/skill/dev-ship/agents/openai.yaml +4 -0
  110. package/skill/dev-ship/references/conventions.md +113 -0
  111. package/skill/dev-ship/references/runbook.md +72 -0
  112. package/skill/dev-ship/refresh/REFRESH.md +3 -0
  113. package/skill/dev-ship/refresh/sources.json +6 -0
  114. package/skill/dev-ship/scripts/lib/approval.mjs +1182 -0
  115. package/skill/dev-ship/scripts/ship-gate.mjs +485 -0
  116. package/skill/dev-status/SKILL.md +56 -0
  117. package/skill/dev-status/agents/openai.yaml +4 -0
  118. package/skill/dev-status/references/conventions.md +113 -0
  119. package/skill/dev-status/refresh/REFRESH.md +3 -0
  120. package/skill/dev-status/refresh/sources.json +6 -0
  121. package/skill/dev-status/scripts/effective-policy.mjs +694 -0
  122. package/skill/dev-status/scripts/status.mjs +311 -0
  123. package/skill/skill-maintainer/SKILL.md +82 -0
  124. package/skill/skill-maintainer/agents/openai.yaml +4 -0
  125. package/skill/skill-maintainer/references/release-ops.md +61 -0
  126. package/skill/skill-maintainer/references/standards.md +137 -0
  127. package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
  128. package/skill/skill-maintainer/refresh/sources.json +103 -0
  129. package/skill/skill-scan/SKILL.md +59 -0
  130. package/skill/skill-scan/agents/openai.yaml +4 -0
  131. package/skill/skill-scan/refresh/REFRESH.md +18 -0
  132. package/skill/skill-scan/refresh/sources.json +133 -0
  133. package/skill/skill-scan/scripts/lib/skillspector.mjs +226 -0
  134. package/skill/skill-scan/scripts/skill-scan.mjs +1002 -0
  135. package/skill/skillify/SKILL.md +108 -0
  136. package/skill/skillify/agents/openai.yaml +4 -0
  137. package/skill/skillify/assets/templates/README.md.template +26 -0
  138. package/skill/skillify/assets/templates/REFRESH.md.template +16 -0
  139. package/skill/skillify/assets/templates/SKILL.md.template +27 -0
  140. package/skill/skillify/assets/templates/evals.json.template +12 -0
  141. package/skill/skillify/assets/templates/openai.yaml.template +4 -0
  142. package/skill/skillify/assets/templates/skill.test.ts.template +27 -0
  143. package/skill/skillify/assets/templates/sources.json.template +6 -0
  144. package/skill/skillify/assets/templates/trigger-queries.json.template +1 -0
  145. package/skill/skillify/references/authoring.md +103 -0
  146. package/skill/skillify/references/eval-playbook.md +112 -0
  147. package/skill/skillify/refresh/REFRESH.md +23 -0
  148. package/skill/skillify/refresh/sources.json +6 -0
  149. package/skill/skillify/scripts/scaffold-skill.mjs +334 -0
  150. package/skill/skillify/scripts/trigger-check.mjs +209 -0
  151. package/skill/vegafactory-setup/SKILL.md +77 -0
  152. package/skill/vegafactory-setup/agents/openai.yaml +4 -0
  153. package/skill/vegafactory-setup/assets/control-room/boards.md.template +17 -0
  154. package/skill/vegafactory-setup/assets/control-room/decisions.md.template +5 -0
  155. package/skill/vegafactory-setup/assets/control-room/group.md.template +44 -0
  156. package/skill/vegafactory-setup/assets/control-room/onboarding/dispatcher-box.md.template +86 -0
  157. package/skill/vegafactory-setup/assets/control-room/onboarding/new-repo.md.template +13 -0
  158. package/skill/vegafactory-setup/assets/control-room/onboarding/new-teammate.md.template +12 -0
  159. package/skill/vegafactory-setup/assets/control-room/org.md.template +60 -0
  160. package/skill/vegafactory-setup/assets/control-room/people.csv.template +2 -0
  161. package/skill/vegafactory-setup/assets/control-room/repos.md.template +7 -0
  162. package/skill/vegafactory-setup/assets/control-room/rules/CODEOWNERS.template +6 -0
  163. package/skill/vegafactory-setup/assets/control-room/rules/README.md.template +9 -0
  164. package/skill/vegafactory-setup/assets/control-room/rules/stats-privacy.md.template +44 -0
  165. package/skill/vegafactory-setup/assets/control-room/templates/README.md.template +9 -0
  166. package/skill/vegafactory-setup/references/control-room.md +149 -0
  167. package/skill/vegafactory-setup/references/conventions.md +113 -0
  168. package/skill/vegafactory-setup/refresh/REFRESH.md +5 -0
  169. package/skill/vegafactory-setup/refresh/sources.json +6 -0
  170. package/skill-integrity.json +252 -0
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ // The ask round: render questions into a `questions` comment, parse whatever a
3
+ // teammate replies, re-ask only what is still open, and decide which route the
4
+ // round takes at all. Text in, text out — this script makes no `gh` call, no
5
+ // network call, and writes no file, so it needs no credentials and no dry run.
6
+ //
7
+ // The route, the comment shape and the reply grammar are documented once in
8
+ // dev-setup's references/ask-route.md; this file is the method behind them.
9
+ //
10
+ // node questions.mjs render --spec <file.json> [--rev <n>] [--json]
11
+ // node questions.mjs parse --comment <file> (--spec <file.json> | --round <file.md>) [--json]
12
+ // node questions.mjs re-ask (--spec <file.json> | --round <file.md>) --comment <file> --rev <n> [--json]
13
+ // node questions.mjs route --tool <name|none> --asker <login> --operator <login> [--json]
14
+ //
15
+ // Exit codes: 0 pass · 1 answers still open or malformed (nothing to re-ask, for
16
+ // `re-ask`) · 2 refusal or usage error.
17
+ import { lstatSync, readFileSync } from 'node:fs'
18
+ import { resolve } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ // The comment marker, written once: the renderer emits it and the round reader
22
+ // finds it by the same string, so the two can never drift apart.
23
+ const MARKER_OPEN = '<!-- vsk:v1 type=questions rev='
24
+ const MARKER_CLOSE = ' -->'
25
+
26
+ // Options run a through h: eight is already more than a person will weigh, and a
27
+ // bounded set is what makes the reply grammar a single character.
28
+ const LETTERS = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
29
+
30
+ // Every path this script reads goes through here. A symlink is refused rather
31
+ // than followed: the caller hands it a path from an issue comment's neighbourhood,
32
+ // and a guard that resolves links reads whatever the link points at.
33
+ export function readTextFile(path) {
34
+ const stats = lstatSync(path)
35
+ if (stats.isSymbolicLink()) throw new Error('refusing to read a symlink: ' + path)
36
+ if (!stats.isFile()) throw new Error('not a regular file: ' + path)
37
+ return readFileSync(path, 'utf8')
38
+ }
39
+
40
+ // The display number of a question: its `n` when the round is a re-ask carrying
41
+ // the original numbering, otherwise its position.
42
+ function numberOf(question, index) {
43
+ return typeof question.n === 'number' ? question.n : index + 1
44
+ }
45
+
46
+ // Text that would break out of the round it is rendered into. A round can be read
47
+ // back out of a comment anyone may have written, so this is checked on the way in
48
+ // as well as on the way out — rendered text is never allowed to carry a marker or
49
+ // close the block it sits in.
50
+ const UNSAFE_TEXT = ['<!--', '-->', '<questions>', '</questions>']
51
+
52
+ function checkText(label, value) {
53
+ if (typeof value !== 'string' || value.trim() === '') throw new Error(label + ' is empty')
54
+ for (const fragment of UNSAFE_TEXT) {
55
+ if (value.includes(fragment)) throw new Error(label + ' carries markup that would break the round: ' + fragment)
56
+ }
57
+ }
58
+
59
+ // A spec that cannot render is a bug in the caller, not a question for the user,
60
+ // so every check throws with the question's own number in the message.
61
+ export function checkSpec(spec) {
62
+ const questions = spec && Array.isArray(spec.questions) ? spec.questions : null
63
+ if (!questions) throw new Error('spec has no questions array')
64
+ if (questions.length === 0) throw new Error('a round needs at least one question')
65
+ questions.forEach((question, index) => {
66
+ const n = numberOf(question, index)
67
+ const label = 'question ' + n
68
+ checkText(label, question.text)
69
+ const options = Array.isArray(question.options) ? question.options : []
70
+ if (options.length < 2) throw new Error(label + ' has fewer than two options')
71
+ const seen = new Set()
72
+ for (const option of options) {
73
+ checkText(label, option.text)
74
+ if (!LETTERS.includes(option.letter)) {
75
+ throw new Error(label + ' uses the letter "' + option.letter + '" — options run a through h')
76
+ }
77
+ if (seen.has(option.letter)) throw new Error(label + ' repeats the letter "' + option.letter + '"')
78
+ seen.add(option.letter)
79
+ if (option.recommended) {
80
+ if (typeof option.reason !== 'string' || option.reason.trim() === '') {
81
+ throw new Error(label + ' recommends "' + option.letter + '" without a reason')
82
+ }
83
+ checkText(label, option.reason)
84
+ }
85
+ }
86
+ const recommended = options.filter((option) => option.recommended)
87
+ if (recommended.length === 0) throw new Error(label + ' has no recommended option')
88
+ if (recommended.length > 1) throw new Error(label + ' has ' + recommended.length + ' recommended options')
89
+ })
90
+ return questions
91
+ }
92
+
93
+ // The rendered comment. The `<questions>` wrapper is what lets the parser and the
94
+ // model both find the round inside a comment that may carry prose around it.
95
+ export function renderQuestions(spec, options) {
96
+ const rev = options && options.rev ? Number(options.rev) : 1
97
+ if (!Number.isInteger(rev) || rev < 1) throw new Error('rev must be a positive integer')
98
+ const questions = checkSpec(spec)
99
+ const lines = []
100
+ lines.push(MARKER_OPEN + rev + MARKER_CLOSE)
101
+ lines.push('## Questions (v' + rev + ')')
102
+ lines.push('')
103
+ lines.push('<questions>')
104
+ questions.forEach((question, index) => {
105
+ if (index > 0) lines.push('')
106
+ lines.push('**Q' + numberOf(question, index) + '.** ' + question.text)
107
+ for (const option of question.options) {
108
+ let line = '- ' + option.letter + ') ' + option.text
109
+ if (option.recommended) line += ' (recommended — ' + option.reason + ')'
110
+ lines.push(line)
111
+ }
112
+ })
113
+ // The example in the reply line is the round's own first question and its first
114
+ // letter, so a re-ask that starts at Q3 tells the reader to write `3: a`.
115
+ const first = questions[0]
116
+ const example = numberOf(first, 0) + ': ' + first.options[0].letter
117
+ lines.push('')
118
+ lines.push('Reply with `' + example + '` per question, or `all recommended`.')
119
+ lines.push('</questions>')
120
+ lines.push('')
121
+ return lines.join('\n')
122
+ }
123
+
124
+ // An answer line: an optional bullet, the question number, `:` or `.`, the option
125
+ // word, and optional trailing prose — only after a dash. The dash is what makes
126
+ // `1: a — too slow` a note and `1: a is too slow, go with b` a sentence: without
127
+ // it the letter is not an answer, and a numbered line that is not an answer is
128
+ // reported rather than read as the letter it happens to open with. Lines with no
129
+ // number are prose — a reply is a comment written by a person, not a form.
130
+ const ANSWER_LINE = /^\s*(?:[-*]\s+)?(\d+)\s*[:.]\s*([A-Za-z]+)[.!]?\s*(?:(?:—|–|--|-)\s*(.*))?$/
131
+ const NUMBERED_LINE = /^\s*(?:[-*]\s+)?(\d+)\s*[:.]\s*(\S.*)$/
132
+ const ALL_RECOMMENDED = /^\s*(?:[-*]\s+)?all\s+recommended\s*[.!]?\s*$/i
133
+
134
+ // Reads a reply comment against the round it answers. Never throws on the reply
135
+ // itself: an unusable reply comes back as `malformed`, which is what a re-ask is
136
+ // built from. It throws only on a spec that could not have been rendered.
137
+ export function parseAnswers(replyText, spec) {
138
+ const questions = checkSpec(spec)
139
+ const numbers = questions.map((question, index) => numberOf(question, index))
140
+ const answers = {}
141
+ const malformed = []
142
+ let sawAnswerLine = false
143
+ let allRecommended = false
144
+
145
+ for (const line of String(replyText).split('\n')) {
146
+ if (ALL_RECOMMENDED.test(line)) {
147
+ allRecommended = true
148
+ sawAnswerLine = true
149
+ continue
150
+ }
151
+ const match = ANSWER_LINE.exec(line)
152
+ if (!match) {
153
+ const numbered = NUMBERED_LINE.exec(line)
154
+ if (numbered) {
155
+ sawAnswerLine = true
156
+ malformed.push('question ' + Number(numbered[1]) + ': "' + numbered[2].trim() + '" is not a bare answer — write "' + Number(numbered[1]) + ': a" or "' + Number(numbered[1]) + ': a — <note>"')
157
+ }
158
+ continue
159
+ }
160
+ sawAnswerLine = true
161
+ const n = Number(match[1])
162
+ const token = match[2].toLowerCase()
163
+ const trailing = (match[3] || '').trim()
164
+ const position = numbers.indexOf(n)
165
+ if (position === -1) {
166
+ malformed.push('question ' + n + ' is not in this round')
167
+ continue
168
+ }
169
+ if (Object.prototype.hasOwnProperty.call(answers, n)) {
170
+ malformed.push('question ' + n + ' answered twice — the first answer stands')
171
+ continue
172
+ }
173
+ const known = questions[position].options.some((option) => option.letter === token)
174
+ if (token !== 'other' && !known) {
175
+ malformed.push('question ' + n + ': "' + token + '" is not an option')
176
+ continue
177
+ }
178
+ answers[n] = { option: token, text: trailing === '' ? null : trailing }
179
+ }
180
+
181
+ if (allRecommended) {
182
+ questions.forEach((question, index) => {
183
+ const n = numbers[index]
184
+ if (Object.prototype.hasOwnProperty.call(answers, n)) return
185
+ const recommended = question.options.find((option) => option.recommended)
186
+ answers[n] = { option: recommended.letter, text: null }
187
+ })
188
+ }
189
+
190
+ const missing = numbers.filter((n) => !Object.prototype.hasOwnProperty.call(answers, n))
191
+ if (!sawAnswerLine) {
192
+ malformed.push('no answer line found — expected "<number>: <letter>" per question, or "all recommended"')
193
+ }
194
+ return { answers, missing, malformed }
195
+ }
196
+
197
+ // The next round: the questions a reply left open, each carrying the number it
198
+ // had in the original round, so a re-ask re-uses `**Q3.**` rather than renumbering
199
+ // and inviting an answer to a question nobody asked twice.
200
+ export function openQuestions(spec, parsed) {
201
+ const questions = checkSpec(spec)
202
+ const numbers = questions.map((question, index) => numberOf(question, index))
203
+ const open = new Set(parsed.missing)
204
+ // A malformed entry names its question; reopen it unless an earlier line in the
205
+ // same reply already answered it (a repeat is malformed, but the first stands).
206
+ for (const problem of parsed.malformed) {
207
+ const named = /^question (\d+)\b/.exec(problem)
208
+ if (!named) continue
209
+ const n = Number(named[1])
210
+ if (!numbers.includes(n)) continue
211
+ if (Object.prototype.hasOwnProperty.call(parsed.answers, n)) continue
212
+ open.add(n)
213
+ }
214
+ const kept = questions
215
+ .map((question, index) => ({ ...question, n: numbers[index] }))
216
+ .filter((question) => open.has(question.n))
217
+ return { ...spec, questions: kept }
218
+ }
219
+
220
+ // The inverse of `renderQuestions`: a posted `questions` comment read back into
221
+ // the spec it was rendered from. This is what makes the wrapper earn its place —
222
+ // the run that reads the reply is a different, later session with no round.json
223
+ // on disk, so the comment itself has to be the record of what was asked.
224
+ const ROUND_QUESTION = /^\*\*Q(\d+)\.\*\*\s+(.+)$/
225
+ const ROUND_OPTION = /^-\s+([a-z])\)\s+(.+)$/
226
+ const ROUND_RECOMMENDED = /^(.*?)\s+\(recommended — (.+)\)$/
227
+
228
+ export function parseRound(commentText) {
229
+ const text = String(commentText)
230
+ const opened = text.indexOf('<questions>')
231
+ const closed = text.indexOf('</questions>', opened + 1)
232
+ if (opened === -1 || closed === -1) throw new Error('no <questions> block in this comment')
233
+ const markerAt = text.indexOf(MARKER_OPEN)
234
+ const revDigits = markerAt === -1 ? null : /^(\d+)/.exec(text.slice(markerAt + MARKER_OPEN.length))
235
+ const rev = revDigits ? Number(revDigits[1]) : 1
236
+ const questions = []
237
+ for (const line of text.slice(opened + '<questions>'.length, closed).split('\n')) {
238
+ const asked = ROUND_QUESTION.exec(line)
239
+ if (asked) {
240
+ questions.push({ n: Number(asked[1]), text: asked[2].trim(), options: [] })
241
+ continue
242
+ }
243
+ const offered = ROUND_OPTION.exec(line)
244
+ if (!offered) continue
245
+ if (questions.length === 0) throw new Error('an option appears before any question')
246
+ const recommended = ROUND_RECOMMENDED.exec(offered[2].trim())
247
+ if (recommended) {
248
+ questions[questions.length - 1].options.push({
249
+ letter: offered[1],
250
+ text: recommended[1].trim(),
251
+ recommended: true,
252
+ reason: recommended[2].trim(),
253
+ })
254
+ } else {
255
+ questions[questions.length - 1].options.push({ letter: offered[1], text: offered[2].trim() })
256
+ }
257
+ }
258
+ const round = { rev, questions }
259
+ checkSpec(round)
260
+ return round
261
+ }
262
+
263
+ // Which surface the round goes to, in one precedence a caller cannot reorder:
264
+ // the environment the dispatcher sets, then whether this harness and run have a
265
+ // question tool at all, then whether the person being asked is the person who
266
+ // owns the issue. Anything unresolved routes to the issue, because a round in a
267
+ // comment is always readable and a round put to the wrong person is not.
268
+ export function decideRoute(input) {
269
+ const env = (input && input.env) || {}
270
+ const forced = typeof env.VSK_ASK_ROUTE === 'string' ? env.VSK_ASK_ROUTE.trim() : ''
271
+ if (forced !== '') {
272
+ if (forced !== 'issue' && forced !== 'tool') throw new Error('VSK_ASK_ROUTE must be issue or tool, not "' + forced + '"')
273
+ return { route: forced, reason: 'VSK_ASK_ROUTE=' + forced }
274
+ }
275
+ const tool = input && typeof input.tool === 'string' ? input.tool.trim() : ''
276
+ if (tool === '' || tool === 'none') return { route: 'issue', reason: 'no question tool in this harness or run' }
277
+ const asker = input && typeof input.asker === 'string' ? input.asker.trim() : ''
278
+ const operator = input && typeof input.operator === 'string' ? input.operator.trim() : ''
279
+ if (asker === '' || operator === '') return { route: 'issue', reason: 'the asker or the issue operator is unknown' }
280
+ if (asker !== operator) return { route: 'issue', reason: 'asker ' + asker + ' is not the issue operator ' + operator }
281
+ return { route: 'tool', reason: 'tool ' + tool + ' is available and the asker is the issue operator' }
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // CLI
286
+ // ---------------------------------------------------------------------------
287
+
288
+ const USAGE = [
289
+ 'usage:',
290
+ ' questions.mjs render --spec <file.json> [--rev <n>] [--json]',
291
+ ' questions.mjs parse --comment <file> (--spec <file.json> | --round <file.md>) [--json]',
292
+ ' questions.mjs re-ask (--spec <file.json> | --round <file.md>) --comment <file> --rev <n> [--json]',
293
+ ' questions.mjs route --tool <name|none> --asker <login> --operator <login> [--json]',
294
+ ].join('\n')
295
+
296
+ // A caller supplies the round it is answering either as the JSON it rendered from
297
+ // (--spec) or as the posted comment itself (--round), which is what a later,
298
+ // fresh session actually has.
299
+ function readSpec(path) {
300
+ return JSON.parse(readTextFile(path))
301
+ }
302
+
303
+ function loadRound(specPath, roundPath) {
304
+ if (specPath && roundPath) throw new Error('pass --spec or --round, not both')
305
+ if (specPath) return readSpec(specPath)
306
+ return parseRound(readTextFile(roundPath))
307
+ }
308
+
309
+ function report(json, payload, exitCode, humanLines) {
310
+ if (json) console.log(JSON.stringify(payload, null, 2))
311
+ else for (const line of humanLines) console.log(line)
312
+ process.exit(exitCode)
313
+ }
314
+
315
+ const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
316
+ if (invokedDirectly) {
317
+ const argv = process.argv.slice(2)
318
+ const json = argv.includes('--json')
319
+ const get = (flag) => { const i = argv.indexOf(flag); return i === -1 ? undefined : argv[i + 1] }
320
+ const command = argv[0]
321
+ const refuse = (reason) => report(json, { command: command || null, ok: false, error: reason }, 2, ['questions: ' + reason])
322
+
323
+ if (command === 'render') {
324
+ const specPath = get('--spec')
325
+ if (!specPath) refuse(USAGE)
326
+ try {
327
+ const rev = get('--rev') ? Number(get('--rev')) : 1
328
+ const markdown = renderQuestions(readSpec(specPath), { rev })
329
+ report(json, { command: 'render', ok: true, rev, markdown }, 0, [markdown])
330
+ } catch (error) {
331
+ refuse(error.message)
332
+ }
333
+ } else if (command === 'parse') {
334
+ const specPath = get('--spec')
335
+ const roundPath = get('--round')
336
+ const commentPath = get('--comment')
337
+ if ((!specPath && !roundPath) || !commentPath) refuse(USAGE)
338
+ try {
339
+ const parsed = parseAnswers(readTextFile(commentPath), loadRound(specPath, roundPath))
340
+ const open = parsed.missing.length > 0 || parsed.malformed.length > 0
341
+ const human = ['questions: ' + Object.keys(parsed.answers).length + ' answered']
342
+ if (parsed.missing.length) human.push(' open: ' + parsed.missing.join(', '))
343
+ for (const problem of parsed.malformed) human.push(' malformed: ' + problem)
344
+ report(json, { command: 'parse', ok: !open, ...parsed }, open ? 1 : 0, human)
345
+ } catch (error) {
346
+ refuse(error.message)
347
+ }
348
+ } else if (command === 're-ask') {
349
+ const specPath = get('--spec')
350
+ const roundPath = get('--round')
351
+ const commentPath = get('--comment')
352
+ const revRaw = get('--rev')
353
+ if ((!specPath && !roundPath) || !commentPath || !revRaw) refuse(USAGE)
354
+ try {
355
+ const spec = loadRound(specPath, roundPath)
356
+ const parsed = parseAnswers(readTextFile(commentPath), spec)
357
+ const open = openQuestions(spec, parsed)
358
+ if (open.questions.length === 0) {
359
+ report(json, { command: 're-ask', ok: false, open: [], reason: 'nothing is open — every question is answered' }, 1, ['questions: nothing is open'])
360
+ }
361
+ const rev = Number(revRaw)
362
+ const markdown = renderQuestions(open, { rev })
363
+ report(json, { command: 're-ask', ok: true, rev, open: open.questions.map((q) => q.n), markdown }, 0, [markdown])
364
+ } catch (error) {
365
+ refuse(error.message)
366
+ }
367
+ } else if (command === 'route') {
368
+ const tool = get('--tool')
369
+ const asker = get('--asker')
370
+ const operator = get('--operator')
371
+ if (!tool || !asker || !operator) refuse(USAGE)
372
+ try {
373
+ const decision = decideRoute({ env: process.env, tool, asker, operator })
374
+ report(json, { command: 'route', ok: true, ...decision }, 0, ['questions: route ' + decision.route + ' — ' + decision.reason])
375
+ } catch (error) {
376
+ refuse(error.message)
377
+ }
378
+ } else {
379
+ refuse(USAGE)
380
+ }
381
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: dev-plan
3
+ description: Write the implementation plan for an approved issue before any code exists. Use when asked to "plan issue 12", "write the plan for" a feature or issue, "plan this before building", when picking up an issue labeled needs-plan, when an approved brief needs its technical approach worked out, or when dev-intake requests the inline plan for a quick-build issue. Not for writing or approving the brief itself (dev-intake), not for executing an approved plan (dev-implement), not for architecture stack advice (dev-architect — this skill consults it while planning).
4
+ ---
5
+
6
+ # dev-plan
7
+
8
+ Advise: turn the approved brief into the plan the operator approves and dev-implement executes, against the repo as it is now.
9
+
10
+ The planning stage: an approved brief goes in, an operator-approved plan comes out, and only then does code exist. A `full-plan` issue is planned in a separate session from intake, because code drifts between brief approval and build.
11
+
12
+ Nearest neighbors: `dev-intake` owns the brief and its approval mechanics — this skill owns the how; `dev-implement` executes what this produces, task by task.
13
+
14
+ ## Every run
15
+
16
+ 1. Read `.vegastack/dev.md` and the issue: brief (description), current schema-v2 brief approval (bundled `scripts/lib/approval.mjs`’s `evaluateApprovals`, complete history and current operator policy), scope label. Full-plan issues arrive labeled `needs-plan`; anything else at this door is either intake's inline request (below) or a misroute — say so.
17
+ 2. **Re-ground before planning** — open the brief's touch points in the current code and verify the flow, names and shapes the plan builds on, because code drifts between approval and build. A claim that no longer matches reality goes back to the operator (`handback` comment, `needs-operator`).
18
+ 3. Stack-bearing choices (schema, hosting, services, jobs, auth) check `dev-architect` — its verify protocol governs platform claims, and no plan re-proposes a recorded rejection.
19
+
20
+ ## The questionnaire
21
+
22
+ Numbered rounds over the full frontier (every open question whose prerequisites are settled), each question carrying a recommended answer so the operator can reply "all recommended":
23
+
24
+ 1. **Approaches** — two or three candidates with the trade-off that matters and one recommendation, because a single option is a decision dressed as a question.
25
+ 2. **System design** — schema, interfaces, migration shape, failure modes; what breaks at the edges.
26
+ 3. **Risk** — blast radius, what a rollback looks like, what should stop a dark run beyond the standing stop-list.
27
+ 4. **Brief challenge** — anything planning revealed the brief missed or got wrong goes back to the operator as a question, because a brief gap absorbed into the plan is a decision the operator did not make.
28
+
29
+ Rounds go out by the ask route (`references/ask-route.md`); an issue-routed round stops the session at `needs-operator` and the next run parses the reply before re-asking anything. A vague or self-contradicting answer gets pushback with concrete options — simple words, a mermaid or ASCII sketch in the issue when a picture beats prose (conventions' collaboration rule).
30
+
31
+ ## The plan
32
+
33
+ Give every task its stable `<issue>-T<n>` identity before approval. Post one comment per [plan-format](references/plan-format.md): Goal · Approach (alternatives named) · Constraints · ordered `- [ ]` tasks, each with exact Files, an Interfaces block (consumes/produces with exact signatures), and Steps that put the failing test before the code. Before posting:
34
+
35
+ - Walk the brief section by section per plan-format's self-review — every requirement points at a task, names match across tasks, no banned placeholder.
36
+ - Work that can run at the same time gets plan-format's independent-group block, because a declared file set is what lets dev-implement run children in parallel and prove afterwards that none of them wandered.
37
+ - Run this skill's plan-lint: `node <path-to-this-skill>/scripts/plan-lint.mjs --file <draft> --json`; exit 2 = fix before posting (placeholders and structural gaps block).
38
+
39
+ Checkboxes belong to the implement session and post empty, because dev-status reads a ticked box as progress.
40
+
41
+ ## Labels and approval
42
+
43
+ Post a new plan → `needs-operator`, assigned to the issue's operator (conventions' Labels table). Resolve fresh complete approval history against current canonical artifacts first: reuse valid intent covering the requested scope, including a combined brief+plan grant, preserving canonical `approvalBindings`. When new approval is needed, record the operator's actual scope per conventions: `scope=plan` for standalone full-plan approval, `scope=brief+plan` for quick-build inline approval, with exact source quotation and current canonical `ArtifactRef`s. A policy-operator publisher attests session words; another recorder may only relay a verified identical scoped grant.
44
+
45
+ A requested approval draft must match that disposition: the exact existing event body for reuse, or the applicable new event/verified relay, never a hypothetical substitute. Validate the complete body per conventions. Only after fresh evaluation confirms both current brief and plan approval, flip to `ready` and unassign; stop — building is dev-implement's.
46
+
47
+ ## The ratchet — one home, this file
48
+
49
+ - **Upgrade (any time, no permission needed to propose):** planning reveals the work is bigger than its scope label — a quick-build that needs real design, an issue that is actually several deliverables. Stop, post one `handback` comment proposing the reclassification or the epic split (parent map + sub-issues, each classified fresh), `needs-operator`, assigned to the operator. A plan runs about one screen per task — Files, Interfaces, Steps — and a plan approaching GitHub's ~65,536-character comment cap is the slicing telling you it wants to be an epic.
50
+ - **Downgrade (operator's yes only):** planning reveals the work is trivial — propose skipping to `ready` with the brief's inline approach, and wait for the yes.
51
+
52
+ ## Quick-build inline mode
53
+
54
+ Invoked from inside dev-intake's conversation, after the brief has consensus: same format, proportionally small (a four-item task list is a fine plan), posted as the plan comment alongside the brief. The operator's single approval covers both (`scope=brief+plan`) — no `needs-plan` stop, no second exchange. The re-grounding step collapses to what intake already read; the ratchet still applies.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "dev-plan"
3
+ short_description: "Plan an approved issue before any code exists"
4
+ default_prompt: "Use $dev-plan to write the implementation plan for this approved issue."
@@ -0,0 +1,80 @@
1
+ # The ask route
2
+
3
+ Where a round of questions goes when a skill needs the user. Two surfaces: the harness's question tool, or the issue itself. This file is the one home for the route, the comment shape and the reply grammar; `scripts/questions.mjs` is the deterministic method behind them, and the marker row lives in [conventions](conventions.md).
4
+
5
+ ## The route
6
+
7
+ Three steps, in this order, first match wins:
8
+
9
+ 1. `VSK_ASK_ROUTE` is set to `issue` or `tool` — the dispatcher and CI set it; any other non-empty value is refused rather than guessed.
10
+ 2. This harness and this run have no question tool → `issue`.
11
+ 3. The asker is not the issue's operator → `issue`. An unresolved asker or operator is also `issue`.
12
+
13
+ Otherwise the tool. The issue route needs an issue: where none exists yet — dev-intake's interview runs before its issue is created — the skill creates the issue first, at `needs-operator` with the operator assigned and the request as its body, and posts the round to it; the brief is written into that issue once the round is answered. On the tool route the issue is created after approval, as before. Settle it deterministically rather than by judgement:
14
+
15
+ ```sh
16
+ node <path-to-this-skill>/scripts/questions.mjs route --tool <name|none> --asker <login> --operator <login> --json
17
+ ```
18
+
19
+ Which tool each harness has — and the Codex Plan-mode gating on `request_user_input` — is recorded in dev-setup's `references/harness-facts.md` (a backticked path, not a link: this file ships into dev-intake, dev-plan and dev-implement, which do not carry that reference) and not restated here. The operator identity comes from the caller under dev.md's `operators:` knob; the script takes both logins as inputs and never resolves them itself.
20
+
21
+ The bias is deliberate: a round in a comment is always readable by whoever owns the issue, and a round put to the wrong person is not.
22
+
23
+ ## The comment
24
+
25
+ One comment per round, marker first, the whole round wrapped in `<questions>` tags so the parser and the model both find it inside a comment that may carry prose around it:
26
+
27
+ ```markdown
28
+ <!-- vsk:v1 type=questions rev=1 -->
29
+ ## Questions (v1)
30
+
31
+ <questions>
32
+ **Q1.** Where does the reminder queue live?
33
+ - a) A Postgres table
34
+ - b) A Redis list (recommended — Redis is already a dependency)
35
+
36
+ **Q2.** How late may a reminder fire?
37
+ - a) Within the hour (recommended — matches the existing cron cadence)
38
+ - b) Same day
39
+
40
+ Reply with `1: a` per question, or `all recommended`.
41
+ </questions>
42
+ ```
43
+
44
+ No question, option or reason may carry an HTML comment delimiter or a `questions` block tag: a round can be read back out of a comment anyone may have written, so text that could close the block or forge a marker is refused in both directions. Every question carries at least two options and exactly one recommendation, and every recommendation carries its one-line reason — a round that cannot say which way it leans is a round that has not been thought through, and the renderer refuses it. Options run `a` through `h`. After posting, the label moves to `needs-operator` and the session ends.
45
+
46
+ ## Replying
47
+
48
+ Anyone on the issue may answer, in an ordinary comment, one line per question:
49
+
50
+ - `1: b`
51
+ - `2: a — the cron already runs` (trailing prose after the dash is kept)
52
+ - `3: other — a third way` (`other` is accepted for any question)
53
+ - `all recommended` on its own line fills every question not answered explicitly
54
+
55
+ An explicit line always beats `all recommended`. Bulleted lines, a `.` instead of `:`, and upper-case letters all parse. Surrounding prose is ignored; the first answer to a question stands and a repeat is reported. A numbered line whose letter runs straight into prose with no dash (`1: a is wrong, go with b`) is malformed, not an answer of `a` — the parser reports it and the question is re-asked.
56
+
57
+ ## Re-asks
58
+
59
+ Parse before asking anything. A later session has no `round.json` on disk, so it reads the round back out of the posted comment — that is what the `<questions>` wrapper is for, and `--round <comment.md>` takes it in place of `--spec`. What comes back is answered questions, still-open questions, and malformed lines. If nothing is open, continue — never re-ask an answered question. If something is open, post a second comment at `rev=<n+1>` carrying only the open questions **at their original numbers**, so `**Q3.**` stays Q3. Earlier `questions` comments are left in place as record. A reply with no answer line at all is malformed, not empty: it gets one re-ask naming the expected shape.
60
+
61
+ ## What it is not
62
+
63
+ An answer is not an approval. Any teammate may answer a round; only the operator approves, and approval is still its own `approval` marker comment recording the operator's own words in the `(<username>)` format ([conventions](conventions.md)). A round that is fully answered unblocks the work; it does not move the issue to `ready`.
64
+
65
+ Assignment is not this route's job either — the label moving to `needs-operator` is what puts the issue on the operator.
66
+
67
+ ## Commands
68
+
69
+ Save the reply comment to `.vegastack/.tmp/<issue>-<slug>/reply.md` first; the parser reads a file, never a network.
70
+
71
+ ```sh
72
+ node <path-to-this-skill>/scripts/questions.mjs render --spec round.json --rev 1 --json
73
+ node <path-to-this-skill>/scripts/questions.mjs parse --comment reply.md --round asked.md --json
74
+ node <path-to-this-skill>/scripts/questions.mjs re-ask --round asked.md --comment reply.md --rev 2 --json
75
+ node <path-to-this-skill>/scripts/questions.mjs route --tool none --asker <login> --operator <login> --json
76
+ ```
77
+
78
+ `asked.md` is the posted `questions` comment; `--spec round.json` takes the same round as JSON instead, where the rendering session still has it. A spec is `{ "questions": [ { "text": "…", "options": [ { "letter": "a", "text": "…" }, { "letter": "b", "text": "…", "recommended": true, "reason": "…" } ] } ] }`.
79
+
80
+ Exit codes: 0 pass · 1 answers still open or malformed, and for `re-ask` nothing left to ask · 2 refusal or usage error.
@@ -0,0 +1,113 @@
1
+ # Workflow conventions
2
+
3
+ Artifact authority.
4
+
5
+ Defaults resolve repo, group, then org; locks require explicit org delegation. Repository dispatch/commands never inherit; registers concatenate. Policy/migration uses dev-setup's `scripts/effective-policy.mjs` and vegafactory-setup's control-room reference.
6
+
7
+ ## Comment metadata markers
8
+
9
+ Comments open:
10
+
11
+ ```markdown
12
+ <!-- vsk:v1 type=<type> rev=<n> [key=value ...] -->
13
+ ## <Human title> (v<n>)
14
+ ```
15
+
16
+ | type | required keys | instances |
17
+ |---|---|---|
18
+ | `approval` | `scope=<brief\|brief+plan\|plan\|consolidated\|none>` and schema-v2 JSON | one per approval event |
19
+ | `questions` | `rev` | one per ask round; earlier rounds stay as record (dev-setup's `references/ask-route.md`) |
20
+ | `plan` | `rev` | one, edited in place |
21
+ | `ledger` | `branch` | one, edited in place |
22
+ | `evidence` | `rev branch sha` | one, edited in place |
23
+ | `review` | `round sha agent=<claude\|codex> verdict=<clean\|needs-fixes>` | one per review cycle, rounds appended inside |
24
+ | `decision` | — | one per decision proposal |
25
+ | `handback` | — | one per stop event |
26
+
27
+ `rev=<n>` and `(v<n>)` start at 1 only on brief, plan, questions and evidence; other types have neither. Locate by marker, never heading/legacy fallback.
28
+
29
+ ## Operator identity
30
+
31
+ Use parenthesized GitHub usernames:
32
+
33
+ - Approval: `Approved by (<username>) on DD-MM-YYYY: "<their words>"`
34
+ - Register line: `- DD-MM-YYYY (<username>) — <decision>`
35
+
36
+ Approval.mjs’s publisher/relay contract lets current-policy provider-envelope publishers attest listed operators’ session words. Other recorders may only relay independently read identical operator-published scoped grants within complete authority history, without lifecycle mutations. Relays inherit source authority/lifecycle; account attestation cannot authenticate off-platform speech.
37
+
38
+ ## Scoped approval records
39
+
40
+ Use only dev-implement’s `scripts/lib/approval.mjs` and follow its contract. Refresh current policy and complete GitHub histories. `ArtifactRef={repo,issue,kind,artifactId,rev,digest}` binds brief issue-node or unique plan/protocol comment-node identity, revision and canonical SHA-256.
41
+
42
+ Post exactly one approval comment: matching scope marker, then one fenced JSON `ApprovalRecord={schemaVersion:2,id,operator,scope,source:{kind,ref,quote},artifacts,supersedes,revokes}`. Exclude outer Markdown fences, future alternatives and unresolved source locators; validate the whole body with approval.mjs's `parseApproval`. Source kind is `session` or `github-comment`, with inspectable words. Reuse valid current grants/relays; avoid counterfactual plan-only or redundant approvals. Scope is `brief`, `plan` or `brief+plan`; planning requires brief, implementation both, research execution also its protocol. Empty-artifact revocations remove exact earlier IDs. Conflicts explicitly supersede; newest never wins.
43
+
44
+ Preserve legacy comments. Without writes, inventory refusals/current digests and request reconfirmation. For duplicate canonical plans preserve both identities/bodies and request record-preserving reconciliation; never delete to clear ambiguity. Follow approval.mjs’s exact correction schema, operator-publisher and target checks. Only malformed or demonstrably invalid-source targets qualify, never valid authority or unavailable/inconsistent facts. Resolve source facts first; corrections grant no scope. Exact child checkpoint actions bind one selected code child's branch/ref/base/tasks/files; other actions never substitute.
45
+
46
+ Consolidated parent events bind frozen manifests, canonical artifacts and exact task/action subsets. Use inline UTF-8 or immutable repository/commit/path plus blob hash, never local paths. Canonical `approvalBindings` authorize; requested `recordBinding` only audits. Follow approval.mjs’s preparation/research/recovery provenance, receipts, adapters, counted attempts and fresh admission; retain immutable history and unverified legacy records. Keep checkpoint/private/live/shipping gates separate.
47
+
48
+ Canonicalization normalizes CRLF; its only exceptions are structural plan checkboxes and one validated JSON `{tasks:[{id,evidenceUrls}]}` block between `<!-- vsk:progress:start -->` / `<!-- vsk:progress:end -->`. IDs must exist; URLs are HTTP(S); unknown fields/duplicates refuse. Stable task IDs/order, interfaces, actions, revisions and all other bytes remain scope. Brief/protocol bodies have no mutable fields; fenced examples stay immutable and grant no authority.
49
+
50
+ ## Revision markers
51
+
52
+ Scope edits increment marker/heading revisions and append `Revisions: v2 — DD-MM-YYYY: <change>, per (<username>) correction`; preserve earlier lines and obtain fresh approval. Validated progress changes need neither.
53
+
54
+ ## Scope classes
55
+
56
+ Intake explains scope; operator overrides:
57
+
58
+ - **`research`** — inquiry; throwaway code allowed, never merged. No branch/PR/changelog; evidence comment contains findings and recommendation.
59
+ - **`quick-build`** — existing flow: draft brief+plan together, approve both, then `ready`.
60
+ - **`full-plan`** — new ground: approve brief, `needs-plan`, separate grounded planning session, `needs-operator`, approve plan, `ready`. Split multiple deliverables into independently classified epic children.
61
+
62
+ Scope ratchet: `dev-plan`.
63
+
64
+ ## Labels
65
+
66
+ One state; flips set assignees (colors: dev-setup):
67
+
68
+ | label | meaning | assignee |
69
+ |---|---|---|
70
+ | `needs-operator` | question, brief or plan approval, proposal | the operator |
71
+ | `needs-plan` | brief approved; awaiting planning (full-plan only) | the operator |
72
+ | `ready` | approved — an agent may start | nobody |
73
+ | `working` | claimed; ledger shows live progress | the runner |
74
+ | `for-operator` | done — evidence posted, awaiting operator review | the operator |
75
+
76
+ Modifiers coexist with state: `risky` · scope `research` / `quick-build` / `full-plan` · `epic` (map parents without a native Epic type). Boards mirror states one-way.
77
+
78
+ ## Titles, types, hierarchy
79
+
80
+ - **Title prefixes:** issues, branches and PRs use dev.md's `branch:` types plus `research:`; PR title = issue title.
81
+ - **Native issue types/fields:** Feature (feat) · Bug (fix) · Task (docs/chore/refactor/research) · Epic for parents (else label); intake sets Priority/Effort. Scope classes stay labels.
82
+ - **Hierarchy:** epic parent = map only (Destination · Decisions so far · Not clear yet · Out of scope), with native child sub-issues. Issues are work units (brief, approvals, branch, PR, evidence); tasks are checkboxes **only in the plan comment**. Blockers use dependencies; phases use milestones. Only non-epic issues get `ready`.
83
+
84
+ ## The ledger
85
+
86
+ One implementation ledger:
87
+
88
+ ```markdown
89
+ <!-- vsk:v1 type=ledger branch=<branch> -->
90
+ ## Ledger — <branch>
91
+ - <issue>-T<N>: complete (commits <base7>..<head7>[, review clean | K parked])
92
+ - <issue>-T<N>: fix round <R>/3 (<X> addressed, <Y> open — <one-liners>; commits <a>..<b>)
93
+ - Ruling: <what> — <why> — cost if wrong: <cost>
94
+ - <issue>-T<N>: parked — <finding> — Ruling: <why the code stands>
95
+ - Deferred minor: <one-liner>
96
+ ```
97
+
98
+
99
+ **Resume protocol:** brief → plan comment → ledger → `git log`; then reconcile task IDs, canonical approval history, edited authority, source/evidence, ownership and delivery effects. Preserve completed work/provenance; stale heartbeat is not stop proof. Preparation never implies issue completion. Dev-implement's ledger reference owns recovery detail.
100
+
101
+ ## `.vegastack/` workspaces
102
+
103
+ Drafts/reports: `.vegastack/.tmp/<issue-number>-<title-slug>/` (pre-issue: `intake-<slug>`), self-ignored by a `.gitignore` containing `*`. Branch checkouts: root-ignored `.vegastack/.worktrees/<issue-number>-<title-slug>/`; main stays on its default branch. Keep both outside `.git/`. Subagents save full reports and return short status. `<path-to-this-skill>` means SKILL.md’s directory.
104
+
105
+ ## Verification gate
106
+
107
+ Prove claims with fresh command output and exit codes; report failures and skips. Delegate only substantial independent parallel work, never your own verification; keep spawn counts low. Guards block machine-verifiable failures (exit 2); heuristics warn. Guards contain no AI inference; unverifiable state fails closed.
108
+
109
+ ## Review bindings
110
+
111
+ One fenced JSON each: `{"reviewBinding":{sha,baseSha,scopeDigest,verdict,findings:[{id,status}]}}` in review; `{"adjudication":{sha,reviewCommentId,operator,source:{kind,ref,quote},findings:[{id,disposition,reason}]}}` in evidence. Use full commit IDs and canonical-plan scopeDigest; status=open/resolved; disposition=accept-risk. Every open finding requires same-review operator acceptance. dev-ship’s README defines source checks. No prose exceptions.
112
+
113
+ Communicate starts/findings/direction plainly; self-contained outcomes include paths and remaining checks. Avoid invented labels/arrows; visualize usefully. Challenge ambiguity with options; never guess silently.