@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.
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/dist/dashboard-artifact.json +21240 -0
- package/dist/index.js +22752 -0
- package/dist/run-wrapper.js +377 -0
- package/package.json +46 -0
- package/skill/dev-architect/SKILL.md +106 -0
- package/skill/dev-architect/agents/openai.yaml +4 -0
- package/skill/dev-architect/references/ai-agents.md +96 -0
- package/skill/dev-architect/references/conventions.md +113 -0
- package/skill/dev-architect/references/data.md +89 -0
- package/skill/dev-architect/references/infra.md +99 -0
- package/skill/dev-architect/references/mobile.md +75 -0
- package/skill/dev-architect/references/pinned-facts.md +133 -0
- package/skill/dev-architect/references/principles.md +117 -0
- package/skill/dev-architect/references/security.md +90 -0
- package/skill/dev-architect/references/stack.md +38 -0
- package/skill/dev-architect/references/web.md +102 -0
- package/skill/dev-architect/refresh/REFRESH.md +31 -0
- package/skill/dev-architect/refresh/sources.json +239 -0
- package/skill/dev-chronicle/SKILL.md +52 -0
- package/skill/dev-chronicle/agents/openai.yaml +4 -0
- package/skill/dev-chronicle/references/conventions.md +113 -0
- package/skill/dev-chronicle/references/styles.md +75 -0
- package/skill/dev-chronicle/refresh/REFRESH.md +3 -0
- package/skill/dev-chronicle/refresh/sources.json +6 -0
- package/skill/dev-debug/SKILL.md +43 -0
- package/skill/dev-debug/agents/openai.yaml +4 -0
- package/skill/dev-debug/references/conventions.md +113 -0
- package/skill/dev-debug/references/loop-ladder.md +20 -0
- package/skill/dev-debug/refresh/REFRESH.md +3 -0
- package/skill/dev-debug/refresh/sources.json +6 -0
- package/skill/dev-implement/SKILL.md +81 -0
- package/skill/dev-implement/agents/openai.yaml +4 -0
- package/skill/dev-implement/assets/workflows/implement-children.js +12 -0
- package/skill/dev-implement/references/ask-route.md +80 -0
- package/skill/dev-implement/references/changelog-and-chronicle.md +21 -0
- package/skill/dev-implement/references/conventions.md +113 -0
- package/skill/dev-implement/references/ledger-and-resume.md +52 -0
- package/skill/dev-implement/references/parallel-children.md +35 -0
- package/skill/dev-implement/references/worktrees.md +63 -0
- package/skill/dev-implement/refresh/REFRESH.md +3 -0
- package/skill/dev-implement/refresh/sources.json +6 -0
- package/skill/dev-implement/scripts/children.mjs +479 -0
- package/skill/dev-implement/scripts/effective-policy.mjs +694 -0
- package/skill/dev-implement/scripts/evidence-check.mjs +107 -0
- package/skill/dev-implement/scripts/evidence-upload.mjs +181 -0
- package/skill/dev-implement/scripts/learning.mjs +41 -0
- package/skill/dev-implement/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-implement/scripts/lib/gh.mjs +101 -0
- package/skill/dev-implement/scripts/preflight.mjs +153 -0
- package/skill/dev-implement/scripts/questions.mjs +381 -0
- package/skill/dev-implement/scripts/reclaim.mjs +116 -0
- package/skill/dev-implement/scripts/recovery.mjs +213 -0
- package/skill/dev-implement/scripts/worktree.mjs +991 -0
- package/skill/dev-intake/SKILL.md +80 -0
- package/skill/dev-intake/agents/openai.yaml +4 -0
- package/skill/dev-intake/references/ask-route.md +80 -0
- package/skill/dev-intake/references/brief-template.md +89 -0
- package/skill/dev-intake/references/conventions.md +113 -0
- package/skill/dev-intake/refresh/REFRESH.md +3 -0
- package/skill/dev-intake/refresh/sources.json +6 -0
- package/skill/dev-intake/scripts/brief-lint.mjs +87 -0
- package/skill/dev-intake/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-intake/scripts/questions.mjs +381 -0
- package/skill/dev-plan/SKILL.md +54 -0
- package/skill/dev-plan/agents/openai.yaml +4 -0
- package/skill/dev-plan/references/ask-route.md +80 -0
- package/skill/dev-plan/references/conventions.md +113 -0
- package/skill/dev-plan/references/plan-format.md +82 -0
- package/skill/dev-plan/refresh/REFRESH.md +3 -0
- package/skill/dev-plan/refresh/sources.json +6 -0
- package/skill/dev-plan/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-plan/scripts/plan-lint.mjs +319 -0
- package/skill/dev-plan/scripts/questions.mjs +381 -0
- package/skill/dev-review/SKILL.md +76 -0
- package/skill/dev-review/agents/openai.yaml +4 -0
- package/skill/dev-review/assets/review-known-patterns.md.template +32 -0
- package/skill/dev-review/references/conventions.md +113 -0
- package/skill/dev-review/references/cross-agent.md +39 -0
- package/skill/dev-review/references/dispatch-prompts.md +121 -0
- package/skill/dev-review/references/security-axis.md +45 -0
- package/skill/dev-review/refresh/REFRESH.md +5 -0
- package/skill/dev-review/refresh/sources.json +6 -0
- package/skill/dev-setup/SKILL.md +113 -0
- package/skill/dev-setup/agents/openai.yaml +4 -0
- package/skill/dev-setup/assets/agents-section.md.template +24 -0
- package/skill/dev-setup/assets/dev-profile.md.template +102 -0
- package/skill/dev-setup/assets/factory-board.yml.template +162 -0
- package/skill/dev-setup/assets/hooks/decision-nudge.mjs +30 -0
- package/skill/dev-setup/assets/hooks/prompt-skill-mention.mjs +29 -0
- package/skill/dev-setup/assets/hooks/session-end.mjs +14 -0
- package/skill/dev-setup/assets/hooks/session-start.mjs +178 -0
- package/skill/dev-setup/assets/hooks/ship-guard.mjs +738 -0
- package/skill/dev-setup/assets/hooks/skill-activated.mjs +47 -0
- package/skill/dev-setup/assets/hooks/stop-heartbeat.mjs +14 -0
- package/skill/dev-setup/assets/workflows/implement-children.js +12 -0
- package/skill/dev-setup/references/ask-route.md +80 -0
- package/skill/dev-setup/references/conventions.md +113 -0
- package/skill/dev-setup/references/github-app.md +286 -0
- package/skill/dev-setup/references/harness-facts.md +169 -0
- package/skill/dev-setup/references/stack-playbooks.md +124 -0
- package/skill/dev-setup/refresh/REFRESH.md +13 -0
- package/skill/dev-setup/refresh/sources.json +602 -0
- package/skill/dev-setup/scripts/effective-policy.mjs +694 -0
- package/skill/dev-setup/scripts/questions.mjs +381 -0
- package/skill/dev-setup/scripts/ship-policy.mjs +195 -0
- package/skill/dev-ship/SKILL.md +56 -0
- package/skill/dev-ship/agents/openai.yaml +4 -0
- package/skill/dev-ship/references/conventions.md +113 -0
- package/skill/dev-ship/references/runbook.md +72 -0
- package/skill/dev-ship/refresh/REFRESH.md +3 -0
- package/skill/dev-ship/refresh/sources.json +6 -0
- package/skill/dev-ship/scripts/lib/approval.mjs +1182 -0
- package/skill/dev-ship/scripts/ship-gate.mjs +485 -0
- package/skill/dev-status/SKILL.md +56 -0
- package/skill/dev-status/agents/openai.yaml +4 -0
- package/skill/dev-status/references/conventions.md +113 -0
- package/skill/dev-status/refresh/REFRESH.md +3 -0
- package/skill/dev-status/refresh/sources.json +6 -0
- package/skill/dev-status/scripts/effective-policy.mjs +694 -0
- package/skill/dev-status/scripts/status.mjs +311 -0
- package/skill/skill-maintainer/SKILL.md +82 -0
- package/skill/skill-maintainer/agents/openai.yaml +4 -0
- package/skill/skill-maintainer/references/release-ops.md +61 -0
- package/skill/skill-maintainer/references/standards.md +137 -0
- package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
- package/skill/skill-maintainer/refresh/sources.json +103 -0
- package/skill/skill-scan/SKILL.md +59 -0
- package/skill/skill-scan/agents/openai.yaml +4 -0
- package/skill/skill-scan/refresh/REFRESH.md +18 -0
- package/skill/skill-scan/refresh/sources.json +133 -0
- package/skill/skill-scan/scripts/lib/skillspector.mjs +226 -0
- package/skill/skill-scan/scripts/skill-scan.mjs +1002 -0
- package/skill/skillify/SKILL.md +108 -0
- package/skill/skillify/agents/openai.yaml +4 -0
- package/skill/skillify/assets/templates/README.md.template +26 -0
- package/skill/skillify/assets/templates/REFRESH.md.template +16 -0
- package/skill/skillify/assets/templates/SKILL.md.template +27 -0
- package/skill/skillify/assets/templates/evals.json.template +12 -0
- package/skill/skillify/assets/templates/openai.yaml.template +4 -0
- package/skill/skillify/assets/templates/skill.test.ts.template +27 -0
- package/skill/skillify/assets/templates/sources.json.template +6 -0
- package/skill/skillify/assets/templates/trigger-queries.json.template +1 -0
- package/skill/skillify/references/authoring.md +103 -0
- package/skill/skillify/references/eval-playbook.md +112 -0
- package/skill/skillify/refresh/REFRESH.md +23 -0
- package/skill/skillify/refresh/sources.json +6 -0
- package/skill/skillify/scripts/scaffold-skill.mjs +334 -0
- package/skill/skillify/scripts/trigger-check.mjs +209 -0
- package/skill/vegafactory-setup/SKILL.md +77 -0
- package/skill/vegafactory-setup/agents/openai.yaml +4 -0
- package/skill/vegafactory-setup/assets/control-room/boards.md.template +17 -0
- package/skill/vegafactory-setup/assets/control-room/decisions.md.template +5 -0
- package/skill/vegafactory-setup/assets/control-room/group.md.template +44 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/dispatcher-box.md.template +86 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/new-repo.md.template +13 -0
- package/skill/vegafactory-setup/assets/control-room/onboarding/new-teammate.md.template +12 -0
- package/skill/vegafactory-setup/assets/control-room/org.md.template +60 -0
- package/skill/vegafactory-setup/assets/control-room/people.csv.template +2 -0
- package/skill/vegafactory-setup/assets/control-room/repos.md.template +7 -0
- package/skill/vegafactory-setup/assets/control-room/rules/CODEOWNERS.template +6 -0
- package/skill/vegafactory-setup/assets/control-room/rules/README.md.template +9 -0
- package/skill/vegafactory-setup/assets/control-room/rules/stats-privacy.md.template +44 -0
- package/skill/vegafactory-setup/assets/control-room/templates/README.md.template +9 -0
- package/skill/vegafactory-setup/references/control-room.md +149 -0
- package/skill/vegafactory-setup/references/conventions.md +113 -0
- package/skill/vegafactory-setup/refresh/REFRESH.md +5 -0
- package/skill/vegafactory-setup/refresh/sources.json +6 -0
- 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,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev-review
|
|
3
|
+
description: Independent review of finished implementation work — a diff against its brief and plan. Use when dev-implement's review step runs, when asked to "review this branch/diff/issue", "give this a second pair of eyes", "check the finished work on issue N", when a cross-agent session (Claude or Codex) is handed a REVIEW REQUEST, or when review findings need a fix loop, re-review, or adjudication. Not for reviewing an unbuilt plan (dev-plan's approval gate), architecture review (dev-architect), shipping gates (dev-ship), scanning skills for vulnerabilities or vetting a skill you did not write (skill-scan), or generic PR review in repos outside this workflow.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dev-review
|
|
7
|
+
|
|
8
|
+
Advise: report every finding with its confidence and severity, or the verified absence of findings — the loop downstream is the filter.
|
|
9
|
+
|
|
10
|
+
Fresh reviewers report findings or their verified absence, with bounded fix loops and recorded dismissals. Briefs: [dispatch-prompts](references/dispatch-prompts.md).
|
|
11
|
+
|
|
12
|
+
Nearest neighbors: `dev-implement` invokes this per dev.md's `review:` knob and applies the findings; `dev-ship` consumes the verdict marker; `dev-plan`'s approval gate reviews plans, this skill reviews built work.
|
|
13
|
+
|
|
14
|
+
## Inputs — files, not pasted context
|
|
15
|
+
|
|
16
|
+
Build the review package first — `git log --oneline <base>..<head>` + `git diff --stat` + `git diff -U10` — at `.vegastack/.tmp/<issue>-<slug>/review-<base7>..<head7>.diff`. Reviewers get paths — the brief (issue body), the plan comment, the package file, the project's `.vegastack/review-known-patterns.md` — plus the binding constraints copied verbatim, ordered data first and ask last, as the dispatch prompts show. **Read the file before judging it** — the full files where the diff needs context, because a diff-only read misses invariants. Reviewers write full reports to `.tmp` files and return short status, so a dead reviewer's findings survive on disk.
|
|
17
|
+
|
|
18
|
+
When dev.md names a `skill-scan:` root, the security dispatch also gets the scan report, at `.vegastack/.tmp/<issue>-<slug>/skill-scan.json`, produced by `node <path-to-skill-scan>/scripts/skill-scan.mjs --json` — the `skill-scan` skill owns running it, its knobs, and its baseline. The same guard already ran at `dev-implement`'s Verify gate; this axis triages what sits below the blocking bar and judges whether anything above it was suppressed rather than fixed.
|
|
19
|
+
|
|
20
|
+
## The axes — parallel, fresh, reported separately
|
|
21
|
+
|
|
22
|
+
| Axis | Runs | Judges |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| **Spec** | always | the diff vs the current brief + plan: missing, scope creep, implemented-but-wrong — quoting the brief line per finding; includes the tests-are-real rubric |
|
|
25
|
+
| **Standards** | always | project rules (known-patterns file + repo docs, which override) + the fixed smell baseline pasted in full into its prompt |
|
|
26
|
+
| **Security** | on `risky`, when touch points hit auth, money, user data, or external input, or when the diff touches a skill under dev.md's `skill-scan:` root | data-flow traces, exploitability before severity, and triage of the skill scan's findings — method in [security-axis](references/security-axis.md) |
|
|
27
|
+
|
|
28
|
+
Each axis uses a fresh subagent without implementation memory and reports separately. Report every finding with confidence/severity; the loop filters findings.
|
|
29
|
+
|
|
30
|
+
**Dispatch without pre-judgement** — a brief saying what not to flag ("do not flag…", "don't treat X as a defect", "at most minor") hides a false positive that belongs in the open adjudication below.
|
|
31
|
+
|
|
32
|
+
## The review comment — one per cycle, rounds appended, marker always current
|
|
33
|
+
|
|
34
|
+
Use one cycle comment. Update its top marker each round; ship-gate reads it. Keep prior rounds as plain text without markers/bindings. Publish one fenced `reviewBinding` JSON per `references/conventions.md`: full commit/base IDs, plan digest, verdict, stable finding IDs/statuses. Marker/binding stay identical; renew legacy reviews. Consumers accept one SHA/base/scope match from a publisher in the accepted base commit's `operators:` roster; candidate edits cannot appoint a reviewer. The gate reports that roster's SHA/path/body digest as `operatorPolicy`; a roster change becomes eligible when its commit later serves as the accepted base. Zero or multiple matches refuse. `agent=` records the harness, not cryptographic model/publisher identity.
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+
<!-- vsk:v1 type=review round=<n> sha=<full-head-sha> agent=<claude|codex> verdict=<clean|needs-fixes> -->
|
|
38
|
+
## Review — round <n> @ <sha7>
|
|
39
|
+
|
|
40
|
+
**Verdict: <clean|needs-fixes>** — spec: <counts> · standards: <counts> · security: <counts | n/a (no surface)>
|
|
41
|
+
|
|
42
|
+
### <Axis> axis
|
|
43
|
+
**Finding [N]: <title>** — **[SEVERITY]** (confidence: high|medium|low) `path/file.ts:42`
|
|
44
|
+
<issue> / <why it matters> / <fix, fenced snippet> / <quoted brief line, spec axis>
|
|
45
|
+
|
|
46
|
+
<details><summary>Nitpicks and low-confidence (N) — non-blocking</summary>…</details>
|
|
47
|
+
|
|
48
|
+
Reviewed: <sha7> · axes: <list> · reviewer: <mode>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Severities: `[CRITICAL]` (security axis: exploitable now — blocks) > `[MUST-FIX]` (wrong, broken, or contradicts the brief — blocks) > `[SHOULD-FIX]` (convention or quality, does not block) > `[NIT]`. Finding IDs are `Finding [N]`, because `#N` auto-links to an issue. Low-confidence findings and nitpicks go in the collapsed block, because low-confidence items in the main list dilute it. Group one recurring defect across files into one finding with a location list. Each finding appears once with issue, impact and fix; collapse nitpicks.
|
|
52
|
+
|
|
53
|
+
## The loop — 3 rounds max, then open adjudication
|
|
54
|
+
|
|
55
|
+
`[CRITICAL]` and `[MUST-FIX]` findings enter the loop; `[SHOULD-FIX]`/`[NIT]` are fixed opportunistically or recorded as deferred minors, keeping the loop bounded.
|
|
56
|
+
|
|
57
|
+
- **Rounds 1–2:** resume (or redispatch) the implementer with the open findings verbatim and the report-file path. It fixes, re-runs the covering tests, appends its fix report to the same file.
|
|
58
|
+
- **Round 3:** a fresh implementer — "a prior implementer attempted this; read the report file for what was tried" — because a loop surviving two resumes means the implementer can't see its own problem.
|
|
59
|
+
- **Every round:** the re-review is scoped to the fix diff (`FIX_BASE..HEAD`, a new package file); the re-reviewer verdicts each finding **ADDRESSED / NOT ADDRESSED** ("attempted" is not addressed), new breakage in the fix diff joins the open list, and out-of-scope observations become deferred minors.
|
|
60
|
+
- **At the cap:** fix forward or bring unresolved findings to the operator. Only their explicit same-review decision can accept risk; record the typed `adjudication` in evidence and surface its rulings in the Review line and ledger. Negative prose or an agent’s parked ruling grants no exception. Early adjudication never shortens the loop.
|
|
61
|
+
|
|
62
|
+
Risky work requires tests and independent review. Every commit renews full-candidate review. After qualification, review all assembled child/preparation scopes; child reviews bind only their parent base. Transformations/delivery: dev-ship’s runbook.
|
|
63
|
+
|
|
64
|
+
## Noise controls — hard filters, not politeness
|
|
65
|
+
|
|
66
|
+
- Default quiet profile: spec, bugs, and security always; style only where a documented rule exists.
|
|
67
|
+
- `.vegastack/review-known-patterns.md` (seed: [template](assets/review-known-patterns.md.template)) holds the project's never-flag patterns — each entry requires a **"Still flag if:"** exception clause; a suppression without one is a blind spot. dev-implement's corrections loop appends operator dismissals there, so a dismissed pattern stays dismissed.
|
|
68
|
+
- The skill scan's suppressions follow the same discipline in the baseline the `skill-scan` skill owns, and its guard enforces the clause — a rule scoped `id:` with no `path:` is a repo-wide blind spot. A finding suppressed rather than fixed is a review finding, not a settled matter.
|
|
69
|
+
|
|
70
|
+
## A scan with no issue attached
|
|
71
|
+
|
|
72
|
+
A `skill-scan` run outside an issue — a standalone check, or the pre-publish guard in dev.md's `## Ship` — has no review comment to land in, so its findings go to intake as a `risky` issue, because a comment posted somewhere convenient is a finding nobody owns. Offer the operator one `risky` issue whose brief body carries the findings, their locations, and what is known about each cause; intake's questions, scope call and approval follow.
|
|
73
|
+
|
|
74
|
+
## Cross-agent — the independence upgrade
|
|
75
|
+
|
|
76
|
+
The dev.md `review:` knob maps to exactly three states — `subagent` (fresh-subagent axes always, no cross-agent), `cross-agent-risky` (subagent axes normally; the other agent on `risky` — the recommended default where the CLI exists), `cross-agent` (the other agent always). On the other agent, follow [cross-agent](references/cross-agent.md): announce the invocation to the operator at trigger time, send the `REVIEW REQUEST (vsk cross-agent v1)` handoff (`codex exec` from Claude; `claude -p` from Codex), and summarize the outcome at the end. The reviewing agent posts its own review comment (`agent=codex`), so independence is verifiable. CLI absent → the manual relay, noting that dev-setup recommends installing it. Which model and effort the other agent runs at is dev.md's `harness-policy:` `review` entry (`<stage> <agent> <model> <effort>`); with no such line, the reviewing harness's own default stands — never a guessed model id.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Review known patterns — {{owner/repo}}
|
|
2
|
+
|
|
3
|
+
The project's never-flag list. Every entry requires a "Still flag if:" exception clause — a suppression without one is a blind spot, not a calibration. dev-implement's corrections loop appends an entry whenever the operator dismisses a review finding, so dismissed patterns stay dismissed. Repo-documented standards override the reviewer's baseline; this file overrides neither — it only suppresses specific recurring false positives.
|
|
4
|
+
|
|
5
|
+
Format, one entry per pattern:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
## <short pattern name>
|
|
9
|
+
|
|
10
|
+
- **Pattern:** <what the reviewer keeps flagging, concretely — code shape, file area>
|
|
11
|
+
- **Why it's safe here:** <the project fact that makes it a false positive>
|
|
12
|
+
- **Still flag if:** <the exception that makes it a real finding after all>
|
|
13
|
+
- **Origin:** <date + issue where the dismissal happened — (<username>)>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The four fields are list items — this file is read in a repo's file view, where single newlines soft-wrap into one paragraph; bullets are what guarantee a line per field. Appended entries (dev-implement's corrections loop) keep the same shape.
|
|
17
|
+
|
|
18
|
+
## Worked examples (replace with real entries)
|
|
19
|
+
|
|
20
|
+
### Sequential awaits in setup scripts
|
|
21
|
+
|
|
22
|
+
- **Pattern:** consecutive `await` calls in bootstrap/setup code flagged as a parallelization miss.
|
|
23
|
+
- **Why it's safe here:** setup steps have ordering dependencies the reviewer can't see locally (labels before issues, scaffold before wiring).
|
|
24
|
+
- **Still flag if:** the awaits are inside a hot request path or a loop over user data.
|
|
25
|
+
- **Origin:** seeded example — delete when the first real entry lands.
|
|
26
|
+
|
|
27
|
+
### Direct `gh` CLI calls without a wrapper
|
|
28
|
+
|
|
29
|
+
- **Pattern:** scripts shelling to `gh` flagged as needing an API client abstraction.
|
|
30
|
+
- **Why it's safe here:** the workflow standardizes on `gh` for auth and pagination; a wrapper would duplicate it.
|
|
31
|
+
- **Still flag if:** arguments are interpolated into a shell string instead of an execFile arg array.
|
|
32
|
+
- **Origin:** seeded example — delete when the first real entry lands.
|
|
@@ -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.
|