buildline 0.0.0-stage → 0.3.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 +214 -2
- package/bin/buildline.js +131 -0
- package/package.json +44 -4
- package/src/cli.js +537 -0
- package/src/detect.js +228 -0
- package/src/features.js +157 -0
- package/src/files.js +107 -0
- package/src/install.js +280 -0
- package/src/models.js +66 -0
- package/src/render.js +9 -0
- package/src/server.js +143 -0
- package/src/ui/index.html +333 -0
- package/templates/AGENTS.block.md +48 -0
- package/templates/CLAUDE.block.md +20 -0
- package/templates/GEMINI.block.md +3 -0
- package/templates/agents/buildline-design-reviewer.md +21 -0
- package/templates/agents/buildline-mapper.md +23 -0
- package/templates/agents/buildline-reviewer.md +24 -0
- package/templates/agents/buildline-spec-critic.md +19 -0
- package/templates/agents/buildline-verifier.md +30 -0
- package/templates/bin/gates.mjs +65 -0
- package/templates/bin/hook.mjs +90 -0
- package/templates/buildline-readme.md +55 -0
- package/templates/ci/buildline.yml +71 -0
- package/templates/context/architecture.md +21 -0
- package/templates/context/design.md +22 -0
- package/templates/context/product.md +22 -0
- package/templates/cursor.mdc +11 -0
- package/templates/feature/progress.md +6 -0
- package/templates/feature/spec.md +34 -0
- package/templates/principles.md +23 -0
- package/templates/stations/autopilot.md +52 -0
- package/templates/stations/build.md +52 -0
- package/templates/stations/buildline.md +48 -0
- package/templates/stations/design.md +39 -0
- package/templates/stations/new.md +39 -0
- package/templates/stations/onboard.md +57 -0
- package/templates/stations/plan.md +44 -0
- package/templates/stations/review.md +37 -0
- package/templates/stations/ship.md +32 -0
- package/templates/stations/spec.md +46 -0
- package/templates/stations/verify.md +44 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Buildline Dashboard</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Cpath d=%22M8 1L15 14H1z%22 fill=%22%23e5a400%22/%3E%3C/svg%3E">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #f6f5f2; --panel: #ffffff; --panel-2: #f0eee9; --ink: #1d1c1a; --ink-2: #5d5a54; --ink-3: #8d8980;
|
|
11
|
+
--line: #e3e0d9; --accent: #e5a400; --accent-ink: #1d1c1a; --accent-soft: #fdf3d4;
|
|
12
|
+
--green: #2f8a4c; --green-soft: #e3f3e7; --red: #c23b31; --red-soft: #fbe6e4; --blue: #2c6bd1; --blue-soft: #e4edfb;
|
|
13
|
+
--violet: #7b4cc2; --violet-soft: #efe7fa; --shadow: 0 1px 2px rgba(0,0,0,.05), 0 4px 16px rgba(0,0,0,.04);
|
|
14
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
15
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
|
|
16
|
+
}
|
|
17
|
+
@media (prefers-color-scheme: dark) {
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #151514; --panel: #1e1e1c; --panel-2: #262624; --ink: #ecebe7; --ink-2: #b2afa8; --ink-3: #807d76;
|
|
20
|
+
--line: #33322f; --accent: #f2b81c; --accent-soft: #3a2f10; --green: #5cc27c; --green-soft: #183223;
|
|
21
|
+
--red: #ef6b61; --red-soft: #3b1b19; --blue: #6fa0f0; --blue-soft: #18263d; --violet: #b18ef0; --violet-soft: #2a2140;
|
|
22
|
+
--shadow: 0 1px 2px rgba(0,0,0,.3);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
html, body { margin: 0; height: 100%; }
|
|
27
|
+
body { background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); }
|
|
28
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
29
|
+
button { cursor: pointer; }
|
|
30
|
+
a { color: var(--blue); }
|
|
31
|
+
|
|
32
|
+
header { display: flex; align-items: center; gap: 12px; padding: 12px 20px; border-bottom: 1px solid var(--line); background: var(--panel); position: sticky; top: 0; z-index: 5; flex-wrap: wrap; }
|
|
33
|
+
.brand { font-weight: 700; letter-spacing: .02em; display: flex; align-items: center; gap: 8px; }
|
|
34
|
+
.brand .mark { color: var(--accent); font-size: 18px; }
|
|
35
|
+
.project { color: var(--ink-2); }
|
|
36
|
+
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; border-radius: 999px; font-size: 12px; background: var(--panel-2); color: var(--ink-2); border: 1px solid var(--line); white-space: nowrap; }
|
|
37
|
+
.chip.accent { background: var(--accent-soft); color: var(--ink); border-color: transparent; }
|
|
38
|
+
.spacer { flex: 1; }
|
|
39
|
+
nav.tabs { display: flex; gap: 2px; background: var(--panel-2); padding: 3px; border-radius: 8px; }
|
|
40
|
+
nav.tabs button { border: 0; background: transparent; padding: 4px 12px; border-radius: 6px; color: var(--ink-2); }
|
|
41
|
+
nav.tabs button[aria-selected="true"] { background: var(--panel); color: var(--ink); box-shadow: var(--shadow); }
|
|
42
|
+
.btn { border: 1px solid var(--line); background: var(--panel); padding: 6px 12px; border-radius: 8px; }
|
|
43
|
+
.btn:hover { background: var(--panel-2); }
|
|
44
|
+
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600; }
|
|
45
|
+
.btn.primary:hover { filter: brightness(1.05); }
|
|
46
|
+
.btn.small { padding: 3px 9px; font-size: 13px; }
|
|
47
|
+
.btn:focus-visible, nav.tabs button:focus-visible, .card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
48
|
+
|
|
49
|
+
main { padding: 16px 20px 40px; }
|
|
50
|
+
.board { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(165px, 1fr); gap: 10px; overflow-x: auto; padding-bottom: 8px; }
|
|
51
|
+
.col { background: var(--panel-2); border-radius: 12px; padding: 10px; min-height: 200px; }
|
|
52
|
+
.col h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-2); margin: 2px 4px 10px; display: flex; justify-content: space-between; }
|
|
53
|
+
.col h2 small { color: var(--ink-3); font-weight: 500; }
|
|
54
|
+
.col .model { font-family: var(--mono); color: var(--ink-2); }
|
|
55
|
+
.col .owner { display: block; font-size: 11px; font-weight: 500; color: var(--ink-3); text-transform: none; letter-spacing: 0; margin-top: 1px; }
|
|
56
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; box-shadow: var(--shadow); cursor: pointer; display: block; width: 100%; text-align: left; }
|
|
57
|
+
.card:hover { border-color: var(--accent); }
|
|
58
|
+
.card .top { display: flex; gap: 6px; align-items: center; margin-bottom: 4px; }
|
|
59
|
+
.card .id { font: 12px var(--mono); color: var(--ink-3); }
|
|
60
|
+
.card .title { font-weight: 600; line-height: 1.3; }
|
|
61
|
+
.card .next { font-size: 12px; line-height: 1.35; color: var(--ink-2); margin-top: 6px; display: flex; gap: 6px; align-items: baseline; }
|
|
62
|
+
.card .next .who { flex-shrink: 0; }
|
|
63
|
+
.type-fix { background: var(--red-soft); color: var(--red); border-color: transparent; }
|
|
64
|
+
.type-feature { background: var(--blue-soft); color: var(--blue); border-color: transparent; }
|
|
65
|
+
.type-epic { background: var(--violet-soft); color: var(--violet); border-color: transparent; }
|
|
66
|
+
.who { font-size: 11px; font-weight: 600; padding: 0 6px; border-radius: 4px; }
|
|
67
|
+
.who-pm { background: var(--violet-soft); color: var(--violet); }
|
|
68
|
+
.who-designer { background: var(--blue-soft); color: var(--blue); }
|
|
69
|
+
.who-eng, .who-agent { background: var(--green-soft); color: var(--green); }
|
|
70
|
+
.who-human { background: var(--red-soft); color: var(--red); }
|
|
71
|
+
.progress { height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; margin-top: 8px; }
|
|
72
|
+
.progress > i { display: block; height: 100%; background: var(--green); }
|
|
73
|
+
.approvals { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
|
74
|
+
.empty { color: var(--ink-3); font-size: 13px; padding: 8px 4px; }
|
|
75
|
+
.hero { max-width: 560px; margin: 60px auto; text-align: center; color: var(--ink-2); }
|
|
76
|
+
.hero h1 { color: var(--ink); font-size: 22px; }
|
|
77
|
+
|
|
78
|
+
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.25); z-index: 10; display: none; }
|
|
79
|
+
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(760px, 100%); background: var(--panel); z-index: 11; border-left: 1px solid var(--line); display: none; flex-direction: column; }
|
|
80
|
+
.drawer.open, .drawer-backdrop.open { display: flex; }
|
|
81
|
+
.drawer header { position: static; }
|
|
82
|
+
.drawer .body { overflow: auto; padding: 16px 20px 40px; flex: 1; }
|
|
83
|
+
.drawer h1 { font-size: 18px; margin: 0; }
|
|
84
|
+
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 10px 20px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
|
85
|
+
.actions label { font-size: 12px; color: var(--ink-2); }
|
|
86
|
+
select { border: 1px solid var(--line); background: var(--panel); border-radius: 8px; padding: 5px 8px; }
|
|
87
|
+
.cmd { font: 12px var(--mono); background: var(--panel); border: 1px dashed var(--line); padding: 4px 8px; border-radius: 6px; }
|
|
88
|
+
|
|
89
|
+
.md h1 { font-size: 20px; } .md h2 { font-size: 16px; margin-top: 24px; padding-bottom: 4px; border-bottom: 1px solid var(--line); } .md h3 { font-size: 14px; }
|
|
90
|
+
.md code { font: 12.5px var(--mono); background: var(--panel-2); padding: 1px 4px; border-radius: 4px; }
|
|
91
|
+
.md pre { background: var(--panel-2); padding: 12px; border-radius: 8px; overflow: auto; } .md pre code { background: none; padding: 0; }
|
|
92
|
+
.md table { border-collapse: collapse; width: 100%; font-size: 13px; } .md th, .md td { border: 1px solid var(--line); padding: 5px 8px; text-align: left; vertical-align: top; }
|
|
93
|
+
.md blockquote { margin: 0; padding: 2px 12px; border-left: 3px solid var(--accent); color: var(--ink-2); background: var(--accent-soft); border-radius: 0 6px 6px 0; }
|
|
94
|
+
.md em { color: var(--ink-2); }
|
|
95
|
+
textarea.editor { width: 100%; min-height: 60vh; font: 13px/1.55 var(--mono); padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); resize: vertical; }
|
|
96
|
+
.tasks { list-style: none; padding: 0; margin: 0; }
|
|
97
|
+
.tasks li { display: grid; grid-template-columns: 22px 1fr; gap: 8px; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
|
98
|
+
.tasks .ok { color: var(--green); font-weight: 700; } .tasks .todo { color: var(--ink-3); }
|
|
99
|
+
.tasks small { color: var(--ink-2); display: block; }
|
|
100
|
+
.history { list-style: none; padding: 0; font-size: 13px; } .history li { padding: 6px 0; border-bottom: 1px solid var(--line); } .history time { color: var(--ink-3); font: 12px var(--mono); margin-right: 8px; }
|
|
101
|
+
|
|
102
|
+
.context { display: grid; grid-template-columns: 200px 1fr; gap: 16px; }
|
|
103
|
+
.context .list button { display: flex; justify-content: space-between; width: 100%; border: 0; background: transparent; text-align: left; padding: 8px 10px; border-radius: 8px; color: var(--ink-2); }
|
|
104
|
+
.context .list button[aria-selected="true"] { background: var(--panel); color: var(--ink); box-shadow: var(--shadow); }
|
|
105
|
+
.context .doc { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px 24px; min-width: 0; }
|
|
106
|
+
.row { display: flex; gap: 8px; align-items: center; justify-content: space-between; margin-bottom: 8px; flex-wrap: wrap; }
|
|
107
|
+
|
|
108
|
+
dialog { border: 1px solid var(--line); border-radius: 14px; background: var(--panel); color: var(--ink); padding: 20px; width: min(460px, calc(100% - 32px)); }
|
|
109
|
+
dialog::backdrop { background: rgba(0,0,0,.35); }
|
|
110
|
+
dialog h2 { margin: 0 0 12px; font-size: 17px; }
|
|
111
|
+
.field { display: grid; gap: 4px; margin-bottom: 12px; } .field span { font-size: 12px; color: var(--ink-2); }
|
|
112
|
+
.field input, .field select { border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; background: var(--panel); }
|
|
113
|
+
.seg { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
|
|
114
|
+
.seg label { border: 1px solid var(--line); border-radius: 8px; padding: 8px; cursor: pointer; font-size: 13px; }
|
|
115
|
+
.seg label small { display: block; color: var(--ink-3); font-size: 11px; }
|
|
116
|
+
.seg input { position: absolute; opacity: 0; }
|
|
117
|
+
.seg input:checked + label, .seg label:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
|
|
118
|
+
.toast { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); background: var(--ink); color: var(--bg); padding: 8px 14px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .2s; pointer-events: none; z-index: 20; }
|
|
119
|
+
.toast.show { opacity: 1; }
|
|
120
|
+
@media (max-width: 720px) {
|
|
121
|
+
header, main { padding-left: 16px; padding-right: 16px; }
|
|
122
|
+
.context { grid-template-columns: 1fr; }
|
|
123
|
+
.context .list { display: flex; overflow-x: auto; gap: 4px; }
|
|
124
|
+
.context .list button { white-space: nowrap; width: auto; }
|
|
125
|
+
.board { grid-auto-columns: 82%; }
|
|
126
|
+
header .spacer { display: none; }
|
|
127
|
+
#whoami { margin-left: auto; }
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
130
|
+
</head>
|
|
131
|
+
<body>
|
|
132
|
+
<header>
|
|
133
|
+
<div class="brand"><span class="mark">▲</span> buildline</div>
|
|
134
|
+
<span class="project" id="project"></span>
|
|
135
|
+
<span class="chip accent" id="autonomy" title="Autonomy level: set in .buildline/buildline.json"></span>
|
|
136
|
+
<nav class="tabs" role="tablist">
|
|
137
|
+
<button role="tab" aria-selected="true" data-view="board">Board</button>
|
|
138
|
+
<button role="tab" aria-selected="false" data-view="context">Context</button>
|
|
139
|
+
</nav>
|
|
140
|
+
<span class="spacer"></span>
|
|
141
|
+
<button class="btn" id="whoami" title="Name used for approvals"></button>
|
|
142
|
+
<button class="btn primary" id="newBtn">+ New</button>
|
|
143
|
+
</header>
|
|
144
|
+
<main id="main"></main>
|
|
145
|
+
|
|
146
|
+
<div class="drawer-backdrop" id="backdrop"></div>
|
|
147
|
+
<aside class="drawer" id="drawer" aria-label="Feature details"></aside>
|
|
148
|
+
|
|
149
|
+
<dialog id="newDialog">
|
|
150
|
+
<form method="dialog" id="newForm">
|
|
151
|
+
<h2>New feature or fix</h2>
|
|
152
|
+
<div class="field"><span>What should change?</span><input name="title" required placeholder="e.g. Let users save carts for later" autofocus></div>
|
|
153
|
+
<div class="field"><span>Size</span>
|
|
154
|
+
<div class="seg">
|
|
155
|
+
<label><input type="radio" name="type" value="fix">Fix<small>bug / small change</small></label>
|
|
156
|
+
<label><input type="radio" name="type" value="feature" checked>Feature<small>full pipeline</small></label>
|
|
157
|
+
<label><input type="radio" name="type" value="epic">Epic<small>split later</small></label>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="field"><span>Owner (optional)</span><input name="owner" placeholder="Who drives this?"></div>
|
|
161
|
+
<div class="row" style="justify-content:flex-end"><button class="btn" value="cancel" formnovalidate>Cancel</button><button class="btn primary" value="ok">Create</button></div>
|
|
162
|
+
</form>
|
|
163
|
+
</dialog>
|
|
164
|
+
<div class="toast" id="toast" role="status"></div>
|
|
165
|
+
|
|
166
|
+
<script>
|
|
167
|
+
const TOKEN = '__BUILDLINE_TOKEN__';
|
|
168
|
+
const COLUMNS = [
|
|
169
|
+
['spec', 'Spec', 'PM'], ['design', 'Design', 'designer · eng'], ['plan', 'Plan', 'eng'], ['build', 'Build', 'agent'],
|
|
170
|
+
['verify', 'Verify', 'agent'], ['review', 'Review', 'agent + human'], ['ship', 'Ship', 'eng'], ['done', 'Done', ''],
|
|
171
|
+
];
|
|
172
|
+
let S = null, view = 'board', openId = null, drawerTab = 'spec', editing = false, ctxPath = null, ctxEditing = false;
|
|
173
|
+
const $ = (s, el = document) => el.querySelector(s);
|
|
174
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
175
|
+
const me = () => { try { return localStorage.getItem('buildline.me') || ''; } catch { return ''; } };
|
|
176
|
+
const setMe = (v) => { try { localStorage.setItem('buildline.me', v); } catch {} };
|
|
177
|
+
|
|
178
|
+
async function api(path, opts = {}) {
|
|
179
|
+
const res = await fetch(path, { ...opts, headers: { 'content-type': 'application/json', 'x-buildline-token': TOKEN, ...(opts.headers || {}) } });
|
|
180
|
+
const data = await res.json();
|
|
181
|
+
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toast.t); toast.t = setTimeout(() => t.classList.remove('show'), 2200); }
|
|
185
|
+
async function copy(text) { try { await navigator.clipboard.writeText(text); toast('Copied: paste it into Claude Code'); } catch { toast(text); } }
|
|
186
|
+
|
|
187
|
+
// ---- tiny markdown renderer (headings, lists, code, tables, quotes, inline) ----
|
|
188
|
+
function inline(s) {
|
|
189
|
+
return esc(s)
|
|
190
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
191
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
192
|
+
.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>')
|
|
193
|
+
.replace(/(^|\W)_([^_]+)_(?=\W|$)/g, '$1<em>$2</em>')
|
|
194
|
+
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
195
|
+
}
|
|
196
|
+
function md(src) {
|
|
197
|
+
const lines = src.replace(/<!--[\s\S]*?-->/g, '').split('\n');
|
|
198
|
+
let html = '', i = 0;
|
|
199
|
+
while (i < lines.length) {
|
|
200
|
+
const l = lines[i];
|
|
201
|
+
if (/^```/.test(l)) { let code = []; i++; while (i < lines.length && !/^```/.test(lines[i])) code.push(lines[i++]); i++; html += `<pre><code>${esc(code.join('\n'))}</code></pre>`; continue; }
|
|
202
|
+
const h = /^(#{1,4})\s+(.*)/.exec(l); if (h) { html += `<h${h[1].length}>${inline(h[2])}</h${h[1].length}>`; i++; continue; }
|
|
203
|
+
if (/^\s*[-*]{3,}\s*$/.test(l)) { html += '<hr>'; i++; continue; }
|
|
204
|
+
if (/^\|/.test(l)) { const rows = []; while (i < lines.length && /^\|/.test(lines[i])) rows.push(lines[i++]); const cells = (r) => r.replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
205
|
+
const body = rows.filter((r, k) => k !== 1 || !/^[|\s:-]+$/.test(r)); html += '<table>' + body.map((r, k) => `<tr>${cells(r).map((c) => k ? `<td>${inline(c)}</td>` : `<th>${inline(c)}</th>`).join('')}</tr>`).join('') + '</table>'; continue; }
|
|
206
|
+
if (/^>\s?/.test(l)) { const q = []; while (i < lines.length && /^>\s?/.test(lines[i])) q.push(lines[i++].replace(/^>\s?/, '')); html += `<blockquote>${md(q.join('\n'))}</blockquote>`; continue; }
|
|
207
|
+
if (/^\s*([-*]|\d+\.)\s+/.test(l)) { const ordered = /^\s*\d+\./.test(l); const items = []; while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i])) items.push(lines[i++].replace(/^\s*([-*]|\d+\.)\s+/, ''));
|
|
208
|
+
html += `<${ordered ? 'ol' : 'ul'}>${items.map((t) => `<li>${inline(t.replace(/^\[( |x)\]\s*/i, (m, c) => c === ' ' ? '☐ ' : '☑ '))}</li>`).join('')}</${ordered ? 'ol' : 'ul'}>`; continue; }
|
|
209
|
+
if (!l.trim()) { i++; continue; }
|
|
210
|
+
const para = []; while (i < lines.length && lines[i].trim() && !/^(#|```|\||>|\s*([-*]|\d+\.)\s)/.test(lines[i])) para.push(lines[i++]);
|
|
211
|
+
html += `<p>${para.map(inline).join('<br>')}</p>`;
|
|
212
|
+
}
|
|
213
|
+
return html;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- rendering ----
|
|
217
|
+
function card(f) {
|
|
218
|
+
const tasks = f.tasks || [], done = tasks.filter((t) => t.passes).length;
|
|
219
|
+
const ap = Object.keys(f.approvals || {});
|
|
220
|
+
return `<button class="card" data-id="${esc(f.id)}">
|
|
221
|
+
<div class="top"><span class="id">${esc(f.id)}</span><span class="chip type-${esc(f.type)}">${esc(f.type)}</span>${f.blocked ? '<span class="chip type-fix">blocked</span>' : ''}</div>
|
|
222
|
+
<div class="title">${esc(f.title)}</div>
|
|
223
|
+
${tasks.length ? `<div class="progress" title="${done}/${tasks.length} tasks passing"><i style="width:${(done / tasks.length) * 100}%"></i></div>` : ''}
|
|
224
|
+
${f.next?.text && f.status !== 'done' ? `<div class="next">${f.next.who ? `<span class="who who-${esc(f.next.who)}">${esc(f.next.who)}</span>` : ''}${esc(f.next.text)}</div>` : ''}
|
|
225
|
+
${ap.length ? `<div class="approvals">${ap.map((a) => `<span class="chip" title="by ${esc(f.approvals[a].by)}">✓ ${esc(a)}</span>`).join('')}</div>` : ''}
|
|
226
|
+
</button>`;
|
|
227
|
+
}
|
|
228
|
+
function renderBoard() {
|
|
229
|
+
if (!S.features.length) {
|
|
230
|
+
return `<div class="hero"><h1>No features yet</h1><p>Every change in this project flows through the factory: spec, design, plan, build, verify, review, ship.</p>
|
|
231
|
+
<p><button class="btn primary" onclick="newFeature()">+ Start the first feature</button></p>
|
|
232
|
+
<p style="font-size:13px">Developers can also type <code>/buildline-new <idea></code> in Claude Code.</p></div>`;
|
|
233
|
+
}
|
|
234
|
+
return `<div class="board">${COLUMNS.map(([key, label, owner]) => {
|
|
235
|
+
const items = S.features.filter((f) => f.status === key);
|
|
236
|
+
const model = (S.config.models || {})[key] ?? S.config.models?.default;
|
|
237
|
+
const modelTag = model && model !== 'inherit' ? ` · <span class="model" title="Model for this station (buildline models)">${esc(model)}</span>` : '';
|
|
238
|
+
return `<section class="col" aria-label="${label}"><h2><span>${label} <span class="owner">${owner}${modelTag}</span></span><small>${items.length}</small></h2>${items.map(card).join('') || '<div class="empty">—</div>'}</section>`;
|
|
239
|
+
}).join('')}</div>`;
|
|
240
|
+
}
|
|
241
|
+
async function renderContext() {
|
|
242
|
+
ctxPath ??= S.context[0]?.path;
|
|
243
|
+
const list = S.context.map((c) => `<button aria-selected="${c.path === ctxPath}" data-ctx="${esc(c.path)}"><span>${esc(c.name)}</span>${c.todo ? '<span class="chip accent">todo</span>' : ''}</button>`).join('');
|
|
244
|
+
let doc = '';
|
|
245
|
+
if (ctxPath) {
|
|
246
|
+
const { content } = await api(`/api/file?path=${encodeURIComponent(ctxPath)}`);
|
|
247
|
+
doc = ctxEditing
|
|
248
|
+
? `<div class="row"><code>${esc(ctxPath)}</code><span><button class="btn small" onclick="ctxEditing=false;render()">Cancel</button> <button class="btn small primary" onclick="saveCtx()">Save</button></span></div><textarea class="editor" id="ctxEditor">${esc(content)}</textarea>`
|
|
249
|
+
: `<div class="row"><code>${esc(ctxPath)}</code><button class="btn small" onclick="ctxEditing=true;render()">Edit</button></div><div class="md">${md(content)}</div>`;
|
|
250
|
+
}
|
|
251
|
+
return `<div class="context"><div class="list">${list}</div><div class="doc">${doc}</div></div>`;
|
|
252
|
+
}
|
|
253
|
+
async function render() {
|
|
254
|
+
$('#project').textContent = S.config.project;
|
|
255
|
+
$('#autonomy').textContent = S.autonomyLabel || `L${S.config.autonomy}`;
|
|
256
|
+
$('#whoami').textContent = me() ? `👤 ${me()}` : 'Set your name';
|
|
257
|
+
document.querySelectorAll('nav.tabs button').forEach((b) => b.setAttribute('aria-selected', b.dataset.view === view));
|
|
258
|
+
if (!(view === 'context' && ctxEditing && $('#ctxEditor'))) $('#main').innerHTML = view === 'board' ? renderBoard() : await renderContext();
|
|
259
|
+
if (openId) await renderDrawer();
|
|
260
|
+
}
|
|
261
|
+
async function renderDrawer() {
|
|
262
|
+
const f = S.features.find((x) => x.id === openId);
|
|
263
|
+
if (!f) return closeDrawer();
|
|
264
|
+
const d = $('#drawer');
|
|
265
|
+
if (editing && d.classList.contains('open') && $('#specEditor')) return; // don't clobber an open editor
|
|
266
|
+
const dir = `.buildline/features/${f.dir}`;
|
|
267
|
+
let body = '';
|
|
268
|
+
if (drawerTab === 'spec' || drawerTab === 'progress') {
|
|
269
|
+
const { content } = await api(`/api/file?path=${encodeURIComponent(`${dir}/${drawerTab}.md`)}`).catch(() => ({ content: '_Missing file._' }));
|
|
270
|
+
body = drawerTab === 'spec' && editing
|
|
271
|
+
? `<textarea class="editor" id="specEditor">${esc(content)}</textarea><div class="row" style="justify-content:flex-end;margin-top:8px"><button class="btn" onclick="editing=false;renderDrawer()">Cancel</button><button class="btn primary" onclick="saveSpec('${esc(dir)}')">Save spec</button></div>`
|
|
272
|
+
: `${drawerTab === 'spec' ? '<div class="row" style="justify-content:flex-end"><button class="btn small" onclick="editing=true;renderDrawer()">Edit spec</button></div>' : ''}<div class="md">${md(content)}</div>`;
|
|
273
|
+
} else if (drawerTab === 'tasks') {
|
|
274
|
+
const t = f.tasks || [];
|
|
275
|
+
body = t.length ? `<ul class="tasks">${t.map((x) => `<li><span class="${x.passes ? 'ok' : 'todo'}">${x.passes ? '✓' : '○'}</span><div><strong>${esc(x.id)}</strong> ${esc(x.title)}<small>${x.covers ? 'covers ' + esc(x.covers.join(', ')) + ' · ' : ''}verify: ${esc(x.verify || '—')}</small>${x.notes ? `<small>${esc(x.notes)}</small>` : ''}</div></li>`).join('')}</ul>`
|
|
276
|
+
: '<p class="empty">No tasks yet. The plan station writes them.</p>';
|
|
277
|
+
} else {
|
|
278
|
+
body = `<ul class="history">${(f.history || []).slice().reverse().map((h) => `<li><time>${esc((h.at || '').slice(0, 16).replace('T', ' '))}</time><strong>${esc(h.station)}</strong> · ${esc(h.note)}</li>`).join('')}</ul>`;
|
|
279
|
+
}
|
|
280
|
+
const appr = ['spec', 'design', 'plan'].map((s) => f.approvals?.[s]
|
|
281
|
+
? `<span class="chip" title="${esc(f.approvals[s].at)}">✓ ${s} · ${esc(f.approvals[s].by)}</span>`
|
|
282
|
+
: `<button class="btn small" onclick="approveStage('${s}')">Approve ${s}</button>`).join('');
|
|
283
|
+
d.innerHTML = `
|
|
284
|
+
<header><div style="min-width:0;flex:1"><div class="top" style="display:flex;gap:6px;align-items:center"><span class="id" style="font:12px var(--mono);color:var(--ink-3)">${esc(f.id)}</span><span class="chip type-${esc(f.type)}">${esc(f.type)}</span><span class="chip">${esc(f.status)}</span>${f.owner ? `<span class="chip">👤 ${esc(f.owner)}</span>` : ''}</div><h1>${esc(f.title)}</h1></div><button class="btn" onclick="closeDrawer()" aria-label="Close">✕</button></header>
|
|
285
|
+
<div class="actions">${appr}<span class="spacer"></span>
|
|
286
|
+
<label>Station <select id="moveSel">${COLUMNS.map(([k, l]) => `<option value="${k}" ${k === f.status ? 'selected' : ''}>${l}</option>`).join('')}</select></label></div>
|
|
287
|
+
${f.next?.cmd ? `<div class="actions"><label>Next</label><span>${esc(f.next.text)}</span><code class="cmd">${esc(f.next.cmd)}</code><button class="btn small" onclick="copy(${esc(JSON.stringify(f.next.cmd))})">Copy</button>${S.config.autonomy >= 3 && ['plan','build','verify','review'].includes(f.status) ? `<button class="btn small" onclick="copy(${esc(JSON.stringify(f.goal))})">Copy /goal</button>` : ''}</div>` : ''}
|
|
288
|
+
<nav class="tabs" style="margin:12px 20px 0;align-self:flex-start">${['spec', 'tasks', 'progress', 'history'].map((t) => `<button aria-selected="${t === drawerTab}" data-dtab="${t}">${t[0].toUpperCase() + t.slice(1)}</button>`).join('')}</nav>
|
|
289
|
+
<div class="body">${body}</div>`;
|
|
290
|
+
d.classList.add('open'); $('#backdrop').classList.add('open');
|
|
291
|
+
$('#moveSel').onchange = async (e) => { await api(`/api/features/${f.id}/move`, { method: 'POST', body: JSON.stringify({ status: e.target.value, note: `Moved to ${e.target.value} from dashboard by ${me() || 'someone'}` }) }); toast(`Moved to ${e.target.value}`); refresh(); };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- actions ----
|
|
295
|
+
function openDrawer(id) { openId = id; drawerTab = 'spec'; editing = false; renderDrawer(); }
|
|
296
|
+
function closeDrawer() { openId = null; editing = false; $('#drawer').classList.remove('open'); $('#backdrop').classList.remove('open'); }
|
|
297
|
+
async function saveSpec(dir) { await api('/api/file', { method: 'PUT', body: JSON.stringify({ path: `${dir}/spec.md`, content: $('#specEditor').value }) }); editing = false; toast('Spec saved'); refresh(); }
|
|
298
|
+
async function saveCtx() { await api('/api/file', { method: 'PUT', body: JSON.stringify({ path: ctxPath, content: $('#ctxEditor').value }) }); ctxEditing = false; toast('Saved'); refresh(); }
|
|
299
|
+
function askName() { const n = prompt('Your name (used when you approve things):', me()); if (n !== null) { setMe(n.trim()); render(); } return me(); }
|
|
300
|
+
async function approveStage(stage) {
|
|
301
|
+
const by = me() || askName(); if (!by) return;
|
|
302
|
+
await api(`/api/features/${openId}/approve`, { method: 'POST', body: JSON.stringify({ stage, by }) }); toast(`${stage} approved`); refresh();
|
|
303
|
+
}
|
|
304
|
+
function newFeature() { $('#newForm').reset(); $('#newDialog').showModal(); }
|
|
305
|
+
$('#newDialog').addEventListener('close', async () => {
|
|
306
|
+
if ($('#newDialog').returnValue !== 'ok') return;
|
|
307
|
+
const fd = new FormData($('#newForm'));
|
|
308
|
+
try { const r = await api('/api/features', { method: 'POST', body: JSON.stringify(Object.fromEntries(fd)) }); toast(`Created ${r.id}`); await refresh(); openDrawer(r.id); }
|
|
309
|
+
catch (e) { toast(e.message); }
|
|
310
|
+
});
|
|
311
|
+
$('#newBtn').onclick = newFeature;
|
|
312
|
+
$('#whoami').onclick = askName;
|
|
313
|
+
$('#backdrop').onclick = closeDrawer;
|
|
314
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && openId && !editing) closeDrawer(); });
|
|
315
|
+
document.addEventListener('click', (e) => {
|
|
316
|
+
const c = e.target.closest('.card'); if (c) return openDrawer(c.dataset.id);
|
|
317
|
+
const v = e.target.closest('[data-view]'); if (v) { view = v.dataset.view; render(); }
|
|
318
|
+
const t = e.target.closest('[data-dtab]'); if (t) { drawerTab = t.dataset.dtab; editing = false; renderDrawer(); }
|
|
319
|
+
const x = e.target.closest('[data-ctx]'); if (x) { ctxPath = x.dataset.ctx; ctxEditing = false; render(); }
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
async function refresh() {
|
|
323
|
+
try {
|
|
324
|
+
const next = await api('/api/state');
|
|
325
|
+
const changed = JSON.stringify(next) !== JSON.stringify(S);
|
|
326
|
+
S = next; if (changed) await render();
|
|
327
|
+
} catch (e) { toast('Dashboard lost connection: is `buildline ui` still running?'); }
|
|
328
|
+
}
|
|
329
|
+
refresh();
|
|
330
|
+
setInterval(() => { if (!document.hidden && !editing && !ctxEditing) refresh(); }, 3000);
|
|
331
|
+
</script>
|
|
332
|
+
</body>
|
|
333
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
## Software factory
|
|
2
|
+
|
|
3
|
+
This project runs a **software factory**: every change moves through a short, spec-driven pipeline with real quality gates. Humans own the *what* and the *why*; agents do most of the *how*, and nothing is called done without evidence.
|
|
4
|
+
|
|
5
|
+
**Start here:** read `.buildline/buildline.json` (config), `.buildline/principles.md` (non-negotiables) and the `.buildline/context/` files before planning any change.
|
|
6
|
+
|
|
7
|
+
### Pipeline
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
intake ─▶ spec ─▶ design ─▶ plan ─▶ build ─▶ verify ─▶ review ─▶ ship
|
|
11
|
+
(PM) (design) (eng) (agent) (agent) (agent+human)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Route by size, never by habit:
|
|
15
|
+
|
|
16
|
+
| Type | When | Stations |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `fix` | Bug or small, well-understood change (≤ ~1 file, no new UX) | intake → build → verify → ship |
|
|
19
|
+
| `feature` | New behaviour, new UI, or touches several modules | all stations |
|
|
20
|
+
| `epic` | Too big for one PR | intake → spec → split into features |
|
|
21
|
+
|
|
22
|
+
Each station has a playbook in `.buildline/stations/<station>.md`. Read the playbook before doing that station's work.
|
|
23
|
+
|
|
24
|
+
### Where things live
|
|
25
|
+
|
|
26
|
+
- `.buildline/features/<id>-<slug>/feature.json` — state: status, approvals, tasks (each with `passes`), history
|
|
27
|
+
- `.buildline/features/<id>-<slug>/spec.md` — the single source of truth for that change
|
|
28
|
+
- `.buildline/features/<id>-<slug>/progress.md` — append-only work log so any session can resume
|
|
29
|
+
- `.buildline/context/` — living product, architecture and design knowledge (update it when you ship)
|
|
30
|
+
|
|
31
|
+
### Quality gates
|
|
32
|
+
|
|
33
|
+
Run `node .buildline/bin/gates.mjs` (or `npx buildline gates`). Configured gates:
|
|
34
|
+
|
|
35
|
+
{{gatesList}}
|
|
36
|
+
|
|
37
|
+
A task only gets `"passes": true` after the gates pass **and** its own verification step has been run and observed.
|
|
38
|
+
|
|
39
|
+
### Rules for agents
|
|
40
|
+
|
|
41
|
+
1. **One feature, one task at a time.** Pick the first task with `"passes": false`, finish it, record evidence, commit, then move on.
|
|
42
|
+
2. **Never weaken the checks.** Do not delete, skip, or loosen tests, acceptance scenarios or gates to make them pass. If a check is wrong, stop and say so.
|
|
43
|
+
3. **Never edit the `acceptance` section of a spec during build.** It belongs to the spec owner.
|
|
44
|
+
4. **Leave a trail.** Append to `progress.md` at the end of every work session: what changed, what was verified, what is next.
|
|
45
|
+
5. **Respect the autonomy level** (`autonomy` in `buildline.json`): pause for human approval where the station playbook says to.
|
|
46
|
+
6. **Small, reversible steps.** Prefer many small commits over one large one. Match the existing code style.
|
|
47
|
+
7. **Ask, don't guess,** when the spec is ambiguous about user-facing behaviour.
|
|
48
|
+
8. **Respect model routing.** `models` in `buildline.json` can send a station to another tool (e.g. `"review": "codex"`). If a station is routed to a different CLI than the one you are, hand it off with `npx buildline step <station> <id> --headless` and summarise the result instead of doing it yourself.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{{agentsImport}}
|
|
2
|
+
### Buildline skills (Claude Code)
|
|
3
|
+
|
|
4
|
+
Type `/buildline` to start anything. It looks at where things are and sends you to the right station.
|
|
5
|
+
|
|
6
|
+
| Skill | Who | What it does |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `/buildline` | anyone | Front door: status, what's next, routes to the right station |
|
|
9
|
+
| `/buildline-onboard` | lead | Set up or refresh the factory's context (new or existing project) |
|
|
10
|
+
| `/buildline-new <idea>` | anyone | Intake: size the change and open a feature |
|
|
11
|
+
| `/buildline-spec <id>` | PM | Write requirements and acceptance scenarios |
|
|
12
|
+
| `/buildline-design <id>` | designer / eng | UX flows, states and technical design |
|
|
13
|
+
| `/buildline-plan <id>` | eng | Break the spec into verifiable tasks |
|
|
14
|
+
| `/buildline-build <id>` | agent | Implement tasks one at a time behind the gates |
|
|
15
|
+
| `/buildline-verify <id>` | agent | Independent check against acceptance scenarios |
|
|
16
|
+
| `/buildline-review <id>` | agent + human | Review the diff against the spec |
|
|
17
|
+
| `/buildline-ship <id>` | eng | PR, changelog, fold learnings into context |
|
|
18
|
+
| `/buildline-autopilot <id>` | agent | Build → verify → review loop until done (uses `/goal`) |
|
|
19
|
+
|
|
20
|
+
Subagents in `.claude/agents/` (`buildline-spec-critic`, `buildline-verifier`, `buildline-reviewer`, `buildline-design-reviewer`, `buildline-mapper`) run checks in a clean context so the builder never grades its own work.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buildline-design-reviewer
|
|
3
|
+
description: Reviews UX design notes, prototypes or implemented UI (via screenshots) against the project's design system, the spec's acceptance scenarios and accessibility basics. Use at the design and review stations for any user-facing change.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior product designer reviewing a change for consistency, clarity and accessibility.
|
|
9
|
+
|
|
10
|
+
Read `.buildline/context/design.md` (tokens, components, patterns), the feature's `spec.md` (Acceptance and `## Design → ### Experience`) and any screenshots in the feature's `evidence/` folder or paths you're given.
|
|
11
|
+
|
|
12
|
+
Check:
|
|
13
|
+
|
|
14
|
+
1. **States:** every screen has loading, empty, error, success and disabled states where relevant.
|
|
15
|
+
2. **Design system:** existing components and tokens are reused; no raw colours, ad-hoc spacing or one-off components where a system one exists (search the codebase to confirm).
|
|
16
|
+
3. **Hierarchy and clarity:** the primary action is obvious; copy is plain and tells users what happened and what to do next.
|
|
17
|
+
4. **Accessibility:** keyboard path and focus order, visible focus, labels for icon-only controls, colour contrast (WCAG 2.2 AA), no information conveyed by colour alone, reduced-motion respect.
|
|
18
|
+
5. **Responsive:** layout holds at narrow widths; touch targets are big enough.
|
|
19
|
+
6. **Spec fit:** the flow satisfies the acceptance scenarios with no extra steps.
|
|
20
|
+
|
|
21
|
+
Output at most 10 findings, most important first: `[must|consider] where · issue → fix`. Mention one thing that works well, so it's kept.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buildline-mapper
|
|
3
|
+
description: Maps an existing codebase (product purpose, architecture, conventions, design system, commands, hazards) into a compact summary for the factory's context files. Use during onboarding of an existing project or when the context files are stale.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a staff engineer joining a new codebase. Produce the summary you'd want on your first day: dense, factual, with file paths as evidence. Read, don't modify anything.
|
|
9
|
+
|
|
10
|
+
Investigate in this order, sampling rather than reading everything:
|
|
11
|
+
|
|
12
|
+
1. Manifests and config: package/lock files, build config, CI workflows, Dockerfiles, env examples.
|
|
13
|
+
2. README, docs folder, ADRs.
|
|
14
|
+
3. Entry points and routing; trace one typical request or user action end to end.
|
|
15
|
+
4. Data layer: schemas, migrations, ORM models, external APIs.
|
|
16
|
+
5. UI layer, if any: component library, tokens/theme files, CSS variables, Storybook, icons.
|
|
17
|
+
6. Tests: frameworks, where they live, how they're named, rough coverage of critical paths.
|
|
18
|
+
7. Commands: install, dev, test, lint, type-check, build. Run the cheap ones (lint, type-check, unit tests) and note whether they pass and how long they take.
|
|
19
|
+
|
|
20
|
+
Return markdown with these sections, each a set of bullets with paths:
|
|
21
|
+
`Product` · `Stack` · `Module map` · `Request/data flow` · `Conventions` (one real example path each) · `Design system` · `Commands` (with observed pass/fail) · `Hazards` (generated code, fragile areas, secrets, things not to touch) · `Unknowns` (questions only a human can answer).
|
|
22
|
+
|
|
23
|
+
Keep it under 150 lines.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buildline-reviewer
|
|
3
|
+
description: Reviews a factory feature's diff against its spec and the project's principles for spec fit, correctness, security, maintainability and test quality. Use at the review station.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
model: opus
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior engineer doing a pre-merge review. Your goal is to find the problems that matter, not to show thoroughness.
|
|
9
|
+
|
|
10
|
+
Input: a feature folder and a git base ref. Read `spec.md`, `.buildline/principles.md`, and `.buildline/context/architecture.md`. Get the diff with `git diff <base>...HEAD` and read surrounding code where needed.
|
|
11
|
+
|
|
12
|
+
Look for:
|
|
13
|
+
|
|
14
|
+
- **Spec fit:** requirements missing or only partly implemented; behaviour nobody asked for.
|
|
15
|
+
- **Correctness:** edge cases, error handling, races, wrong assumptions about data, off-by-one, null handling.
|
|
16
|
+
- **Security:** injection, missing authorization on new endpoints or actions, secrets in code or logs, unsafe deserialization, new dependencies with known risk.
|
|
17
|
+
- **Tests:** tests that don't actually assert the behaviour; skipped, deleted or weakened tests; acceptance scenarios with no test.
|
|
18
|
+
- **Maintainability:** breaks conventions in architecture.md, duplicated logic that already exists elsewhere (name the existing function), dead code, debug leftovers.
|
|
19
|
+
|
|
20
|
+
Only report a finding if you can describe a concrete failure scenario or a concrete rule it breaks. Skip style preferences the linter doesn't enforce.
|
|
21
|
+
|
|
22
|
+
Output at most 12 findings, most severe first, each as:
|
|
23
|
+
`[blocker|should-fix|nit] path:line · what's wrong · failure scenario · suggested fix`
|
|
24
|
+
End with one line: `verdict: approve` or `verdict: changes requested`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buildline-spec-critic
|
|
3
|
+
description: Critiques a factory feature spec (spec.md) for untestable requirements, missing unhappy paths, contradictions and hidden scope. Use after writing or changing a spec, before design or planning.
|
|
4
|
+
tools: Read, Grep, Glob
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a sharp, constructive product reviewer. You review one `spec.md` from `.buildline/features/<id>-<slug>/`. Also read `.buildline/context/product.md` and `.buildline/principles.md` for context.
|
|
9
|
+
|
|
10
|
+
Check:
|
|
11
|
+
|
|
12
|
+
1. **Testability:** every requirement (`R*`) can be checked from outside the system. Flag vague words: "fast", "intuitive", "secure", "should handle", "etc.".
|
|
13
|
+
2. **Coverage:** every requirement is exercised by at least one acceptance scenario (`A*`). List orphans.
|
|
14
|
+
3. **Unhappy paths:** empty, error, timeout, offline, permission denied, invalid input, very large input, concurrent edits. Flag the ones that matter for this feature and are missing.
|
|
15
|
+
4. **Contradictions:** between requirements, between the spec and product context, between the spec and existing behaviour (search the code if needed).
|
|
16
|
+
5. **Scope creep:** requirements that don't serve the brief's outcome; suggest moving them to `## Out of scope`.
|
|
17
|
+
6. **Size:** if the spec covers several independent outcomes, recommend splitting it into an epic.
|
|
18
|
+
|
|
19
|
+
Output at most 10 findings, most important first, each as: `[severity] section · problem → suggested fix`. Severity is `must` or `consider`. If the spec is solid, say so in one line. Don't rewrite the spec yourself.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buildline-verifier
|
|
3
|
+
description: Independently verifies a factory feature against its acceptance scenarios by running gates, the app, tests and browser checks, then reports PASS/FAIL with evidence. Use at the verify station; never the same context that built the feature.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Write
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an independent QA engineer. You did not build this feature and you don't trust claims you haven't observed yourself.
|
|
9
|
+
|
|
10
|
+
Input: a feature folder `.buildline/features/<id>-<slug>/`. Read `spec.md` (Acceptance and Design sections) and `.buildline/buildline.json` (commands, gates).
|
|
11
|
+
|
|
12
|
+
Process:
|
|
13
|
+
|
|
14
|
+
1. Run `node .buildline/bin/gates.mjs`. Record the result.
|
|
15
|
+
2. Start the app or service if scenarios need it (`commands.dev`), in the background, and wait until it responds.
|
|
16
|
+
3. For every acceptance scenario `A*`: set up the Given, do the When, observe the Then the way a real user or client would (browser automation with Playwright or available browser tools for UI, real HTTP calls for APIs, real command runs for CLIs). Prefer running an existing end-to-end test that encodes the scenario, after confirming it really asserts the Then.
|
|
17
|
+
4. For UI, also check the loading/empty/error states listed in the design and that the main path works with the keyboard alone.
|
|
18
|
+
5. Save screenshots and logs to the feature's `evidence/` folder.
|
|
19
|
+
6. Stop any processes you started.
|
|
20
|
+
|
|
21
|
+
Rules: don't modify source code, tests or the spec. Only write inside `evidence/`. If a scenario can't be checked (missing data, credentials, unclear wording), mark it `BLOCKED` and say what's missing; don't guess.
|
|
22
|
+
|
|
23
|
+
Report in exactly this shape:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
gates: PASS|FAIL (summary)
|
|
27
|
+
A1 PASS|FAIL|BLOCKED: how it was checked → what was observed (evidence path)
|
|
28
|
+
A2 …
|
|
29
|
+
overall: PASS|FAIL
|
|
30
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Buildline quality gates. Runs every gate in .buildline/buildline.json in order and
|
|
3
|
+
// prints a summary. Exit code 0 only if all required gates pass.
|
|
4
|
+
//
|
|
5
|
+
// node .buildline/bin/gates.mjs run all gates
|
|
6
|
+
// node .buildline/bin/gates.mjs lint test run only the named gates
|
|
7
|
+
// node .buildline/bin/gates.mjs --json machine-readable summary
|
|
8
|
+
//
|
|
9
|
+
// Managed by buildline. Configure gates in .buildline/buildline.json, not here.
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const json = args.includes('--json');
|
|
18
|
+
const only = args.filter((a) => !a.startsWith('--'));
|
|
19
|
+
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = JSON.parse(fs.readFileSync(path.join(root, '.buildline', 'buildline.json'), 'utf8'));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`Cannot read .buildline/buildline.json: ${err.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const gates = (config.gates || []).filter((g) => g.command && (!only.length || only.includes(g.name)));
|
|
29
|
+
if (!gates.length) {
|
|
30
|
+
const msg = 'No gates configured. Add commands to "gates" in .buildline/buildline.json.';
|
|
31
|
+
if (json) console.log(JSON.stringify({ ok: false, results: [], message: msg }));
|
|
32
|
+
else console.log(msg);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const gate of gates) {
|
|
38
|
+
if (!json) console.log(`\n▶ ${gate.name}: ${gate.command}`);
|
|
39
|
+
const started = Date.now();
|
|
40
|
+
const res = spawnSync(gate.command, {
|
|
41
|
+
cwd: root,
|
|
42
|
+
shell: true,
|
|
43
|
+
stdio: json ? 'pipe' : 'inherit',
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout: (gate.timeoutSeconds || 900) * 1000,
|
|
46
|
+
});
|
|
47
|
+
const ok = res.status === 0;
|
|
48
|
+
const required = gate.required !== false;
|
|
49
|
+
const result = { name: gate.name, command: gate.command, ok, required, seconds: Math.round((Date.now() - started) / 100) / 10 };
|
|
50
|
+
if (json && !ok) result.output = `${res.stdout || ''}${res.stderr || ''}`.slice(-4000);
|
|
51
|
+
results.push(result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const failedRequired = results.filter((r) => !r.ok && r.required);
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({ ok: failedRequired.length === 0, results }, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
console.log('\n──── gates ────');
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
const mark = r.ok ? '✔' : r.required ? '✘' : '⚠';
|
|
61
|
+
console.log(`${mark} ${r.name.padEnd(12)} ${String(r.seconds).padStart(6)}s${r.ok || r.required ? '' : ' (optional)'}`);
|
|
62
|
+
}
|
|
63
|
+
console.log(failedRequired.length ? `\nGATES FAILED: ${failedRequired.map((r) => r.name).join(', ')}` : '\nALL GATES PASSED');
|
|
64
|
+
}
|
|
65
|
+
process.exit(failedRequired.length ? 1 : 0);
|