super-backlog 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cli.js +19 -1
- package/dist/commands/dashboard.js +18 -3
- package/dist/commands/doctor.js +82 -3
- package/dist/commands/phase.js +86 -0
- package/dist/dashboard/data.js +6 -0
- package/dist/dashboard/hub.js +3 -1
- package/dist/lib/build-fingerprint.js +50 -0
- package/dist/lib/hub-state.js +8 -2
- package/dist/lib/phase.js +30 -0
- package/dist/templates/dashboard.html +36 -0
- package/dist/templates/skill-spec-to-backlog.md +4 -2
- package/dist/templates/skill-task-review-gate.md +24 -10
- package/dist/templates/workflow-block.md +12 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,10 @@ When enabled:
|
|
|
97
97
|
|
|
98
98
|
The router is fully owned by super-backlog and removed by `sbl uninstall`. See the [model router design](docs/superpowers/specs/2026-08-26-sbl-model-router-design.md) for details.
|
|
99
99
|
|
|
100
|
+
## Pipeline phases
|
|
101
|
+
|
|
102
|
+
Tasks carry their pipeline phase as a label (`phase/spec` → `phase/plan` → `phase/impl` → `phase/verify`), managed by `sbl phase <task-id> [phase|done]` and checked by `sbl doctor`. The dashboard stepper, task table, and task modal render the live phase. Details: [docs/guide/pipeline-phases.md](docs/guide/pipeline-phases.md).
|
|
103
|
+
|
|
100
104
|
## Project Dashboard
|
|
101
105
|
|
|
102
106
|
`sbl dashboard` starts a local hub that serves an HTS-style cockpit (light/dark theme toggle) rendered from your Backlog data in eight sections — Board & Quick Actions, Status (KPI tiles, donut, and an aging strip for open tasks), Feature Cycle (pipeline stepper plus an Up Next / Blocked flow view from task dependencies), Milestones, Drafts (click a card for details), Tasks (sortable/filterable table; click a row to open a modal detail view with acceptance criteria and dependencies), Activity (a 26-week calendar heatmap; click a day for its tasks), and Decisions & Docs. Glossary tooltips explain domain terms inline; extend or override them project-wide via `backlog/docs/glossary.md` (`## Term` heading plus the text below it). Typefaces load from Google Fonts with full system fallbacks — the one external resource; everything else is inline, and the dashboard still renders offline. Bookmark `http://127.0.0.1:6428/p/<project_name>/`. The hub watches `backlog/`, regenerates on change, and serves on port `6428`; connected browser tabs reload automatically via Server-Sent Events. A second repo's `sbl dashboard` attaches to the same hub. `Ctrl+C` in the hub terminal stops all projects.
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runDashboard } from './commands/dashboard.js';
|
|
|
8
8
|
import { runDoctor } from './commands/doctor.js';
|
|
9
9
|
import { runInit } from './commands/init.js';
|
|
10
10
|
import { runModels } from './commands/models.js';
|
|
11
|
+
import { runPhase } from './commands/phase.js';
|
|
11
12
|
import { runUninstall } from './commands/uninstall.js';
|
|
12
13
|
import { runUpdate } from './commands/update.js';
|
|
13
14
|
import { KIT_VERSION } from './lib/version.js';
|
|
@@ -22,6 +23,7 @@ Commands:
|
|
|
22
23
|
update Refresh kit-managed files and report upstream versions
|
|
23
24
|
dashboard Start the project dashboard server (live-reload) (alias: db)
|
|
24
25
|
models Manage the model router (show, enable, disable, discover)
|
|
26
|
+
phase Show or advance a task's pipeline phase (spec|plan|impl|verify|done)
|
|
25
27
|
doctor Check the environment (node, PowerShell policy, backlog CLI)
|
|
26
28
|
|
|
27
29
|
init options:
|
|
@@ -45,7 +47,10 @@ dashboard options:
|
|
|
45
47
|
--no-open Do not open the dashboard browser automatically
|
|
46
48
|
|
|
47
49
|
doctor options:
|
|
48
|
-
(none) Prints one [ok]/[warn]/[skip] line per check; exit 4 on any warn
|
|
50
|
+
(none) Prints one [ok]/[warn]/[skip]/[fail] line per check; exit 4 on any warn, 1 on any fail
|
|
51
|
+
|
|
52
|
+
phase options:
|
|
53
|
+
--json Print the query result as JSON (phase + labels)
|
|
49
54
|
|
|
50
55
|
Global options:
|
|
51
56
|
--version Print the super-backlog version and exit
|
|
@@ -141,6 +146,19 @@ export async function runCli(argv) {
|
|
|
141
146
|
}
|
|
142
147
|
case 'doctor':
|
|
143
148
|
return runDoctor(process.cwd());
|
|
149
|
+
case 'phase': {
|
|
150
|
+
const parsed = parseArgs({
|
|
151
|
+
args: rest,
|
|
152
|
+
allowPositionals: true,
|
|
153
|
+
options: {
|
|
154
|
+
json: { type: 'boolean' },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return runPhase(process.cwd(), {
|
|
158
|
+
values: parsed.values,
|
|
159
|
+
positionals: parsed.positionals,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
144
162
|
default:
|
|
145
163
|
console.error(`Unknown command "${command}".\n`);
|
|
146
164
|
console.error(HELP);
|
|
@@ -8,6 +8,7 @@ import { startHubServer } from '../dashboard/hub.js';
|
|
|
8
8
|
import { renderDashboard } from '../dashboard/render.js';
|
|
9
9
|
import { DASHBOARD_PORT } from '../dashboard/server.js';
|
|
10
10
|
import { atomicWrite } from '../lib/atomic.js';
|
|
11
|
+
import { defaultBuildFingerprint } from '../lib/build-fingerprint.js';
|
|
11
12
|
import { clearHubState, isPidAlive, newHubToken, readHubState, writeHubState } from '../lib/hub-state.js';
|
|
12
13
|
import { projectSlug } from '../lib/slug.js';
|
|
13
14
|
import { KIT_VERSION } from '../lib/version.js';
|
|
@@ -157,6 +158,8 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
157
158
|
const isAlive = deps.isAlive ?? isPidAlive;
|
|
158
159
|
const killPid = deps.killPid ?? ((p) => { process.kill(p); });
|
|
159
160
|
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
161
|
+
const buildFingerprint = deps.buildFingerprint ?? defaultBuildFingerprint;
|
|
162
|
+
const myFingerprint = buildFingerprint();
|
|
160
163
|
let slugResult;
|
|
161
164
|
try {
|
|
162
165
|
slugResult = projectSlug(cwd);
|
|
@@ -180,7 +183,14 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
180
183
|
: undefined;
|
|
181
184
|
const liveVersion = typeof statusJson?.version === 'string' ? statusJson.version : undefined;
|
|
182
185
|
const effectiveVersion = liveVersion ?? state.version;
|
|
183
|
-
|
|
186
|
+
const liveFingerprint = typeof statusJson?.fingerprint === 'string' ? statusJson.fingerprint : undefined;
|
|
187
|
+
// Same version AND (proven identical build OR build cannot be
|
|
188
|
+
// assessed locally) -> attach. A missing live fingerprint from a
|
|
189
|
+
// same-version hub means it was started by an older CLI and cannot
|
|
190
|
+
// see the current build, so it is restarted exactly once.
|
|
191
|
+
const buildMatches = myFingerprint !== null && liveFingerprint === myFingerprint;
|
|
192
|
+
const buildUnassessable = myFingerprint === null;
|
|
193
|
+
if (effectiveVersion === KIT_VERSION && (buildMatches || buildUnassessable)) {
|
|
184
194
|
if (values['port'] !== undefined && port !== state.port) {
|
|
185
195
|
console.error(`error: a hub is already running on ${state.port}`);
|
|
186
196
|
return 1;
|
|
@@ -194,7 +204,12 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
194
204
|
noOpen,
|
|
195
205
|
});
|
|
196
206
|
}
|
|
197
|
-
|
|
207
|
+
if (effectiveVersion === KIT_VERSION) {
|
|
208
|
+
console.error(`hub build changed (fingerprint mismatch, same version v${KIT_VERSION}) — restarting it`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.error(`hub v${effectiveVersion ?? 'unknown'} does not match this CLI (v${KIT_VERSION}) — restarting it`);
|
|
212
|
+
}
|
|
198
213
|
killPid(state.pid);
|
|
199
214
|
let dead = !isAlive(state.pid);
|
|
200
215
|
for (let attempt = 0; !dead && attempt < STOP_POLL_MAX_ATTEMPTS; attempt++) {
|
|
@@ -234,7 +249,7 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
234
249
|
console.error(`error: dashboard serve failed (${err instanceof Error ? err.message : String(err)})`);
|
|
235
250
|
return 1;
|
|
236
251
|
}
|
|
237
|
-
writeHubState(home, { pid, port: hub.port, token, version: KIT_VERSION });
|
|
252
|
+
writeHubState(home, { pid, port: hub.port, token, version: KIT_VERSION, fingerprint: myFingerprint ?? undefined });
|
|
238
253
|
const result = hub.register({ cwd, file: outPath, regenerate });
|
|
239
254
|
if (!result.ok) {
|
|
240
255
|
await hub.close();
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
// src/commands/doctor.ts
|
|
2
2
|
import process from 'node:process';
|
|
3
|
+
import { extractPhaseLabels, PHASES } from '../lib/phase.js';
|
|
3
4
|
import { getEffectiveExecutionPolicy, isBlockingExecutionPolicy, } from '../lib/powershell.js';
|
|
4
|
-
import { resolveBacklogBin } from '../lib/run.js';
|
|
5
|
-
const MARK = {
|
|
5
|
+
import { resolveBacklogBin, runCapture } from '../lib/run.js';
|
|
6
|
+
const MARK = {
|
|
7
|
+
ok: '[ok] ',
|
|
8
|
+
warn: '[warn]',
|
|
9
|
+
skip: '[skip]',
|
|
10
|
+
fail: '[fail]',
|
|
11
|
+
};
|
|
12
|
+
function defaultReadTaskLabels(cwd, resolveBacklog) {
|
|
13
|
+
const bin = resolveBacklog(cwd);
|
|
14
|
+
if (!bin)
|
|
15
|
+
return null;
|
|
16
|
+
const res = runCapture(bin, ['task', 'list', '--json'], cwd);
|
|
17
|
+
if (res.status !== 0)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(res.stdout);
|
|
21
|
+
if (!Array.isArray(parsed.tasks))
|
|
22
|
+
return null;
|
|
23
|
+
const rows = [];
|
|
24
|
+
for (const t of parsed.tasks) {
|
|
25
|
+
if (typeof t !== 'object' || t === null)
|
|
26
|
+
continue;
|
|
27
|
+
const id = t.id;
|
|
28
|
+
const status = t.status;
|
|
29
|
+
const labels = t.labels;
|
|
30
|
+
if (typeof id !== 'string' || typeof status !== 'string')
|
|
31
|
+
continue;
|
|
32
|
+
rows.push({
|
|
33
|
+
id,
|
|
34
|
+
status,
|
|
35
|
+
labels: Array.isArray(labels) ? labels.filter((l) => typeof l === 'string') : [],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return rows;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
6
44
|
export function runDoctor(cwd, deps = {}) {
|
|
7
45
|
const platform = deps.platform ?? process.platform;
|
|
8
46
|
const nodeVersion = deps.nodeVersion ?? process.versions.node;
|
|
9
47
|
const resolveBacklog = deps.resolveBacklog ?? resolveBacklogBin;
|
|
48
|
+
const readTaskLabels = deps.readTaskLabels ?? ((c) => defaultReadTaskLabels(c, resolveBacklog));
|
|
10
49
|
const log = deps.log ?? ((line) => console.log(line));
|
|
11
50
|
let okCount = 0;
|
|
12
51
|
let warnCount = 0;
|
|
13
52
|
let skipCount = 0;
|
|
53
|
+
let failCount = 0;
|
|
14
54
|
const emit = (status, line, extra = []) => {
|
|
15
55
|
if (status === 'ok')
|
|
16
56
|
okCount += 1;
|
|
17
57
|
else if (status === 'warn')
|
|
18
58
|
warnCount += 1;
|
|
59
|
+
else if (status === 'fail')
|
|
60
|
+
failCount += 1;
|
|
19
61
|
else
|
|
20
62
|
skipCount += 1;
|
|
21
63
|
log(`${MARK[status]} ${line}`);
|
|
@@ -60,6 +102,43 @@ export function runDoctor(cwd, deps = {}) {
|
|
|
60
102
|
'fix: npx.cmd super-backlog init',
|
|
61
103
|
]);
|
|
62
104
|
}
|
|
63
|
-
|
|
105
|
+
// check 4: phase label hygiene
|
|
106
|
+
const taskRows = readTaskLabels(cwd);
|
|
107
|
+
if (taskRows === null) {
|
|
108
|
+
emit('skip', 'phase label hygiene (task labels unreadable - backlog CLI unavailable)');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const known = new Set(PHASES.map((p) => `phase/${p}`));
|
|
112
|
+
let problems = 0;
|
|
113
|
+
let legacy = 0;
|
|
114
|
+
for (const row of taskRows) {
|
|
115
|
+
const phaseLabels = extractPhaseLabels(row.labels);
|
|
116
|
+
const unknown = phaseLabels.filter((l) => !known.has(l));
|
|
117
|
+
if (phaseLabels.length > 1) {
|
|
118
|
+
problems += 1;
|
|
119
|
+
emit('fail', `${row.id}: multiple phase labels (${phaseLabels.join(', ')})`, [
|
|
120
|
+
`fix: sbl phase ${row.id} <phase> after removing the stale label`,
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
for (const l of unknown) {
|
|
124
|
+
problems += 1;
|
|
125
|
+
emit('fail', `${row.id}: unknown phase label ${l}`, [
|
|
126
|
+
`fix: backlog task edit ${row.id} --remove-label ${l}`,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
if (unknown.length === 0 && phaseLabels.length === 0 && row.status === 'In Progress') {
|
|
130
|
+
legacy += 1;
|
|
131
|
+
emit('warn', `${row.id}: In Progress without phase label (legacy inventory)`, [
|
|
132
|
+
`fix: sbl phase ${row.id} spec`,
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (problems === 0 && legacy === 0) {
|
|
137
|
+
emit('ok', `phase label hygiene clean (${taskRows.length} tasks)`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
log(`doctor summary: ${okCount} ok, ${warnCount} warn, ${skipCount} skip, ${failCount} fail`);
|
|
141
|
+
if (failCount > 0)
|
|
142
|
+
return 1;
|
|
64
143
|
return warnCount > 0 ? 4 : 0;
|
|
65
144
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/commands/phase.ts
|
|
2
|
+
import { derivePhase, extractPhaseLabels, isPhaseTarget, planTransition } from '../lib/phase.js';
|
|
3
|
+
import { resolveBacklogBin, runCapture } from '../lib/run.js';
|
|
4
|
+
function readLabels(raw) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = JSON.parse(raw);
|
|
7
|
+
const labels = parsed?.task?.labels;
|
|
8
|
+
if (!Array.isArray(labels))
|
|
9
|
+
return null;
|
|
10
|
+
return labels.filter((l) => typeof l === 'string');
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function runPhase(cwd, args, deps = {}) {
|
|
17
|
+
const resolveBacklog = deps.resolveBacklog ?? resolveBacklogBin;
|
|
18
|
+
const run = deps.run ?? runCapture;
|
|
19
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
20
|
+
const id = args.positionals[0];
|
|
21
|
+
const target = args.positionals[1];
|
|
22
|
+
const json = args.values['json'] === true;
|
|
23
|
+
if (!id || (target !== undefined && !isPhaseTarget(target))) {
|
|
24
|
+
log(`usage: sbl phase <task-id> [${['spec', 'plan', 'impl', 'verify', 'done'].join('|')}] [--json]`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
const bin = resolveBacklog(cwd);
|
|
28
|
+
if (!bin) {
|
|
29
|
+
log('error: backlog CLI not found - run sbl init or npm install first');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
const view = run(bin, ['task', 'view', id, '--json'], cwd);
|
|
33
|
+
if (view.status !== 0) {
|
|
34
|
+
log(`error: backlog task view ${id} failed (exit ${view.status})`);
|
|
35
|
+
if (view.stderr.trim())
|
|
36
|
+
log(view.stderr.trim());
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
const labels = readLabels(view.stdout);
|
|
40
|
+
if (labels === null) {
|
|
41
|
+
log(`error: unreadable task view JSON for ${id}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const phase = derivePhase(labels);
|
|
45
|
+
if (target === undefined) {
|
|
46
|
+
if (json) {
|
|
47
|
+
log(JSON.stringify({ id, phase, labels: extractPhaseLabels(labels) }));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
log(phase ? `${id}: phase/${phase}` : `${id}: none`);
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const result = planTransition(labels, target);
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
if (result.reason === 'multiple-phases') {
|
|
57
|
+
log(`error: ${id} carries multiple phase labels (${extractPhaseLabels(labels).join(', ')}) - run sbl doctor`);
|
|
58
|
+
}
|
|
59
|
+
else if (result.reason === 'no-phase') {
|
|
60
|
+
log(`error: ${id} has no phase label - start with: sbl phase ${id} spec`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
log(`error: unknown phase "${String(target)}"`);
|
|
64
|
+
}
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const { remove, add } = result.plan;
|
|
68
|
+
if (remove === null && add === null) {
|
|
69
|
+
log(`${id}: no phase label present, nothing to do`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const editArgs = ['task', 'edit', id];
|
|
73
|
+
if (remove)
|
|
74
|
+
editArgs.push('--remove-label', remove);
|
|
75
|
+
if (add)
|
|
76
|
+
editArgs.push('--add-label', add);
|
|
77
|
+
const edit = run(bin, editArgs, cwd);
|
|
78
|
+
if (edit.status !== 0) {
|
|
79
|
+
log(`error: backlog task edit failed (exit ${edit.status})`);
|
|
80
|
+
if (edit.stderr.trim())
|
|
81
|
+
log(edit.stderr.trim());
|
|
82
|
+
return 3;
|
|
83
|
+
}
|
|
84
|
+
log(add ? `${id}: ${add}` : `${id}: phase label removed (done)`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
package/dist/dashboard/data.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { resolveBacklogBin, runCapture } from '../lib/run.js';
|
|
6
|
+
import { derivePhase } from '../lib/phase.js';
|
|
6
7
|
import { isNewerVersion } from '../lib/version-check.js';
|
|
7
8
|
import { readSimpleKeys } from '../lib/yamlmini.js';
|
|
8
9
|
import { computeKpis } from './metrics.js';
|
|
@@ -16,6 +17,9 @@ function asString(v) {
|
|
|
16
17
|
return String(v);
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
20
|
+
function asStrings(v) {
|
|
21
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
|
|
22
|
+
}
|
|
19
23
|
/**
|
|
20
24
|
* Parse the stdout of `backlog task list --json` defensively.
|
|
21
25
|
* Accepts `{tasks:[...]}` and bare-array shapes; anything else throws
|
|
@@ -70,6 +74,8 @@ export function normalizeTasks(rawTasks) {
|
|
|
70
74
|
title: asString(t['title']) ?? '(untitled)',
|
|
71
75
|
status: asString(t['status']) ?? 'Unknown',
|
|
72
76
|
priority: asString(t['priority']),
|
|
77
|
+
labels: asStrings(t['labels']),
|
|
78
|
+
phase: derivePhase(asStrings(t['labels'])),
|
|
73
79
|
assignee: firstAssignee(t),
|
|
74
80
|
created: asString(t['createdAt']) ?? asString(t['created_at']) ?? asString(t['created']),
|
|
75
81
|
updated: asString(t['updatedAt']) ?? asString(t['updated_at']) ?? asString(t['updated']),
|
package/dist/dashboard/hub.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectDashboardData } from './data.js';
|
|
|
9
9
|
import { renderDashboard } from './render.js';
|
|
10
10
|
import { createDebouncedReloader, createReloadBroker, DASHBOARD_PORT, recursiveWatchSupported, } from './server.js';
|
|
11
11
|
import { atomicWrite } from '../lib/atomic.js';
|
|
12
|
+
import { defaultBuildFingerprint } from '../lib/build-fingerprint.js';
|
|
12
13
|
import { projectSlug, realpathKey } from '../lib/slug.js';
|
|
13
14
|
import { KIT_VERSION } from '../lib/version.js';
|
|
14
15
|
import { createModelApiHandler } from '../models/dashboard-api.js';
|
|
@@ -63,6 +64,7 @@ function generateDashboard(cwd, file) {
|
|
|
63
64
|
export async function startHubServer(opts) {
|
|
64
65
|
const projects = new Map();
|
|
65
66
|
const token = opts.token;
|
|
67
|
+
const fingerprint = defaultBuildFingerprint();
|
|
66
68
|
let port = 0;
|
|
67
69
|
let watchWarned = false;
|
|
68
70
|
function watchBacklog(cwd, reloader) {
|
|
@@ -166,7 +168,7 @@ export async function startHubServer(opts) {
|
|
|
166
168
|
sendText(res, 401, 'unauthorized');
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
|
-
sendJson(res, 200, { pid: process.pid, port, version: KIT_VERSION });
|
|
171
|
+
sendJson(res, 200, { pid: process.pid, port, version: KIT_VERSION, fingerprint: fingerprint ?? undefined });
|
|
170
172
|
return;
|
|
171
173
|
}
|
|
172
174
|
if (pathname === '/api/hub/register' && method === 'POST') {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/lib/build-fingerprint.ts
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
function distRoot() {
|
|
8
|
+
// Resolves from src/lib (dev/tests) and dist/lib (runtime) alike: both sit
|
|
9
|
+
// two levels below the package root that owns dist/.
|
|
10
|
+
const pkgPath = require.resolve('../../package.json');
|
|
11
|
+
return join(dirname(pkgPath), 'dist');
|
|
12
|
+
}
|
|
13
|
+
function walk(dir, out) {
|
|
14
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory())
|
|
17
|
+
walk(full, out);
|
|
18
|
+
else
|
|
19
|
+
out.push(full);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Stable content hash over the built CLI. Two builds of the same source
|
|
24
|
+
* produce the same fingerprint; any changed or added file changes it.
|
|
25
|
+
* Returns null when the build directory is missing.
|
|
26
|
+
*/
|
|
27
|
+
export function computeBuildFingerprint(root = distRoot()) {
|
|
28
|
+
if (!existsSync(root))
|
|
29
|
+
return null;
|
|
30
|
+
const files = [];
|
|
31
|
+
walk(root, files);
|
|
32
|
+
if (files.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
files.sort();
|
|
35
|
+
const hash = createHash('sha256');
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
hash.update(file.slice(root.length + 1).replaceAll('\\', '/'));
|
|
38
|
+
hash.update('\0');
|
|
39
|
+
hash.update(readFileSync(file));
|
|
40
|
+
hash.update('\0');
|
|
41
|
+
}
|
|
42
|
+
return hash.digest('hex').slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
let cachedDefault;
|
|
45
|
+
/** Process-lifetime cached fingerprint of the running build. */
|
|
46
|
+
export function defaultBuildFingerprint() {
|
|
47
|
+
if (cachedDefault === undefined)
|
|
48
|
+
cachedDefault = computeBuildFingerprint();
|
|
49
|
+
return cachedDefault;
|
|
50
|
+
}
|
package/dist/lib/hub-state.js
CHANGED
|
@@ -16,8 +16,14 @@ export function readHubState(home) {
|
|
|
16
16
|
typeof parsed.token !== 'string') {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
const { pid, port, token, version } = parsed;
|
|
20
|
-
return
|
|
19
|
+
const { pid, port, token, version, fingerprint } = parsed;
|
|
20
|
+
return {
|
|
21
|
+
pid,
|
|
22
|
+
port,
|
|
23
|
+
token,
|
|
24
|
+
...(typeof version === 'string' ? { version } : {}),
|
|
25
|
+
...(typeof fingerprint === 'string' ? { fingerprint } : {}),
|
|
26
|
+
};
|
|
21
27
|
}
|
|
22
28
|
catch {
|
|
23
29
|
return null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/lib/phase.ts
|
|
2
|
+
export const PHASES = ['spec', 'plan', 'impl', 'verify'];
|
|
3
|
+
export const PHASE_LABEL_PREFIX = 'phase/';
|
|
4
|
+
const PHASE_LABELS = PHASES.map((p) => PHASE_LABEL_PREFIX + p);
|
|
5
|
+
export function phaseLabel(p) {
|
|
6
|
+
return PHASE_LABEL_PREFIX + p;
|
|
7
|
+
}
|
|
8
|
+
export function isPhaseTarget(v) {
|
|
9
|
+
return v === 'done' || PHASES.includes(v);
|
|
10
|
+
}
|
|
11
|
+
export function extractPhaseLabels(labels) {
|
|
12
|
+
return labels.filter((l) => l.startsWith(PHASE_LABEL_PREFIX));
|
|
13
|
+
}
|
|
14
|
+
export function derivePhase(labels) {
|
|
15
|
+
const found = labels.find((l) => PHASE_LABELS.includes(l));
|
|
16
|
+
return found ? found.slice(PHASE_LABEL_PREFIX.length) : null;
|
|
17
|
+
}
|
|
18
|
+
export function planTransition(labels, target) {
|
|
19
|
+
if (!isPhaseTarget(target))
|
|
20
|
+
return { ok: false, reason: 'unknown-phase' };
|
|
21
|
+
const phaseLabels = extractPhaseLabels(labels);
|
|
22
|
+
if (phaseLabels.length > 1)
|
|
23
|
+
return { ok: false, reason: 'multiple-phases' };
|
|
24
|
+
const current = phaseLabels[0] ?? null;
|
|
25
|
+
if (current === null && target !== 'spec')
|
|
26
|
+
return { ok: false, reason: 'no-phase' };
|
|
27
|
+
const remove = current;
|
|
28
|
+
const add = target === 'done' ? null : phaseLabel(target);
|
|
29
|
+
return { ok: true, plan: { remove, add } };
|
|
30
|
+
}
|
|
@@ -395,6 +395,12 @@
|
|
|
395
395
|
overflow-wrap: break-word;
|
|
396
396
|
}
|
|
397
397
|
.step.gate .step-label { color: var(--warn); }
|
|
398
|
+
.step .step-count { position: relative; z-index: 1; display: inline-block; margin-top: 6px;
|
|
399
|
+
padding: 0 .45rem; border-radius: 999px; font: 600 .68rem/1.5 var(--mono);
|
|
400
|
+
background: color-mix(in oklab, var(--accent) 18%, transparent); color: var(--accent); }
|
|
401
|
+
|
|
402
|
+
/* ---------- Phase chip in task rows ---------- */
|
|
403
|
+
.cell-id .status-chip { margin-left: .45rem; }
|
|
398
404
|
|
|
399
405
|
/* ---------- Phase detail panel ---------- */
|
|
400
406
|
#phase-detail {
|
|
@@ -766,6 +772,13 @@
|
|
|
766
772
|
var chip = el('span', 'status-chip', field(task, k));
|
|
767
773
|
chip.setAttribute('data-tone', toneOf(task.status));
|
|
768
774
|
td.appendChild(chip);
|
|
775
|
+
} else if (k === 'id') {
|
|
776
|
+
td.textContent = field(task, k);
|
|
777
|
+
if (task.phase) {
|
|
778
|
+
var pc = el('span', 'status-chip', 'phase/' + task.phase);
|
|
779
|
+
pc.setAttribute('data-tone', 'accent');
|
|
780
|
+
td.appendChild(pc);
|
|
781
|
+
}
|
|
769
782
|
} else if (k === 'updated') {
|
|
770
783
|
td.textContent = formatRelative(field(task, k));
|
|
771
784
|
var exact = formatExact(field(task, k));
|
|
@@ -1342,6 +1355,10 @@
|
|
|
1342
1355
|
step.setAttribute('aria-controls', 'phase-detail');
|
|
1343
1356
|
step.appendChild(el('div', 'step-num', String(p.n)));
|
|
1344
1357
|
step.appendChild(el('span', 'step-label', p.name));
|
|
1358
|
+
var phaseKey = PHASE_STEP[p.n];
|
|
1359
|
+
if (phaseKey && phaseCounts[phaseKey] > 0) {
|
|
1360
|
+
step.appendChild(el('span', 'step-count', String(phaseCounts[phaseKey])));
|
|
1361
|
+
}
|
|
1345
1362
|
step.addEventListener('click', function () {
|
|
1346
1363
|
if (!panel) return;
|
|
1347
1364
|
var wasOpen = current === step;
|
|
@@ -1368,6 +1385,15 @@
|
|
|
1368
1385
|
} catch (e) {
|
|
1369
1386
|
phases = [];
|
|
1370
1387
|
}
|
|
1388
|
+
|
|
1389
|
+
/* ---------- Pipeline phase state (labels on tasks) ---------- */
|
|
1390
|
+
var PHASE_STEP = { 5: 'spec', 6: 'plan', 7: 'impl', 8: 'verify' };
|
|
1391
|
+
var NEXT_PHASE = { spec: 'plan', plan: 'impl', impl: 'verify', verify: 'done' };
|
|
1392
|
+
var phaseCounts = { spec: 0, plan: 0, impl: 0, verify: 0 };
|
|
1393
|
+
data.tasks.forEach(function (t) {
|
|
1394
|
+
if (t.phase && phaseCounts[t.phase] !== undefined) phaseCounts[t.phase] += 1;
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1371
1397
|
if ($('#donut')) renderDonut($('#donut'), data.statuses);
|
|
1372
1398
|
if ($('#kpis')) renderKpis($('#kpis'), data.kpis);
|
|
1373
1399
|
if ($('#aging')) renderAging($('#aging'), data.tasks);
|
|
@@ -1505,6 +1531,7 @@
|
|
|
1505
1531
|
}
|
|
1506
1532
|
grid.appendChild(cell('Milestone', task.milestone));
|
|
1507
1533
|
grid.appendChild(cell('Priority', task.priority, priorityTone(task.priority)));
|
|
1534
|
+
grid.appendChild(cell('Phase', task.phase ? 'phase/' + task.phase : ''));
|
|
1508
1535
|
grid.appendChild(cell('Assignee', task.assignee));
|
|
1509
1536
|
grid.appendChild(cell('Updated', task.updated));
|
|
1510
1537
|
return grid;
|
|
@@ -1570,6 +1597,15 @@
|
|
|
1570
1597
|
cmd.appendChild(el('span', 'cmd-title', 'copy'));
|
|
1571
1598
|
cmd.addEventListener('click', function () { copyCommand(cmd, cmdLine); });
|
|
1572
1599
|
content.appendChild(cmd);
|
|
1600
|
+
if (t.phase && NEXT_PHASE[t.phase]) {
|
|
1601
|
+
var advanceCmd = 'sbl phase ' + t.id.replace(/^task-/i, '') + ' ' + NEXT_PHASE[t.phase];
|
|
1602
|
+
var adv = el('button', 'phase-cmd detail-cmd');
|
|
1603
|
+
adv.type = 'button';
|
|
1604
|
+
adv.appendChild(el('span', 'cmd-line', advanceCmd));
|
|
1605
|
+
adv.appendChild(el('span', 'cmd-title', 'copy: advance'));
|
|
1606
|
+
adv.addEventListener('click', function () { copyCommand(adv, advanceCmd); });
|
|
1607
|
+
content.appendChild(adv);
|
|
1608
|
+
}
|
|
1573
1609
|
dialog.textContent = '';
|
|
1574
1610
|
dialog.appendChild(content);
|
|
1575
1611
|
dialog.showModal();
|
|
@@ -18,15 +18,17 @@ Bridge between Superpowers (brainstorming, writing-plans) and Backlog.md.
|
|
|
18
18
|
1. Read `backlog instructions overview` and `backlog instructions task-creation` first.
|
|
19
19
|
2. Decompose: every plan unit becomes ONE task, small enough for one session/PR.
|
|
20
20
|
3. Create per task:
|
|
21
|
-
backlog task create "Title" -d "<goal/context>" --ac "<criterion 1>" --ac "<criterion 2>" --type feature --
|
|
21
|
+
backlog task create "Title" -d "<goal/context>" --ac "<criterion 1>" --ac "<criterion 2>" --type feature --labels feature,phase/spec --ref "<path/to/plan-doc>"
|
|
22
22
|
- Dependencies: --dep TASK-y (order follows the plan).
|
|
23
23
|
- Larger efforts: backlog milestone add `"<Name>"`, attach via -m.
|
|
24
24
|
- Reference the plan doc via --ref; NEVER copy it into the task.
|
|
25
25
|
4. Never set --plan or --notes at create time — those belong to the "task started" checkpoint after codebase research.
|
|
26
|
-
5.
|
|
26
|
+
5. Every created task starts at `phase/spec`. Later phase changes happen only via `sbl phase <id> <phase>` at gate passages — never by editing labels manually.
|
|
27
|
+
6. STOP at the review gate: the human reviews specs and acceptance criteria (backlog board / backlog browser / project dashboard) before any code exists.
|
|
27
28
|
|
|
28
29
|
## Boundaries
|
|
29
30
|
|
|
30
31
|
- Never hand-edit task markdown; use the backlog CLI exclusively.
|
|
31
32
|
- No code, no worktrees, no status changes inside this skill.
|
|
33
|
+
- Phase labels are set with creation and advanced only via `sbl phase`.
|
|
32
34
|
- Project-specific human-gate topics get their own tasks with an explicit review gate.
|
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-review-gate
|
|
3
|
-
description: Enforce the human review checkpoint before implementation starts. Use
|
|
3
|
+
description: Enforce the human review checkpoint before implementation starts. Use at session start on an existing task, right after spec-to-backlog created tasks, or when the user asks to implement a specific task: present the task, its pipeline phase and acceptance criteria, and wait for explicit approval before any code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Task Review Gate:
|
|
6
|
+
# Task Review Gate: session entry, review gate, resume
|
|
7
7
|
|
|
8
|
-
Human checkpoint between reviewed specs and the first line of code
|
|
8
|
+
Human checkpoint between reviewed specs and the first line of code — and the
|
|
9
|
+
re-entry point for every session that continues a running task.
|
|
9
10
|
|
|
10
11
|
## When this skill runs
|
|
11
12
|
|
|
12
|
-
1.
|
|
13
|
-
2.
|
|
13
|
+
1. At the start of a session that works on an existing task (resume).
|
|
14
|
+
2. Right after spec-to-backlog created tasks (review specs + acceptance criteria).
|
|
15
|
+
3. When the user asks to implement a specific task (restate scope before starting).
|
|
14
16
|
|
|
15
17
|
## Procedure
|
|
16
18
|
|
|
17
19
|
1. Load the task: `backlog task view <ID> --plain`.
|
|
18
|
-
2.
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
2. Load the phase: `sbl phase <ID>` (prints `phase/spec|plan|impl|verify` or `none`).
|
|
21
|
+
3. Present compactly: goal, every acceptance criterion, dependencies, the
|
|
22
|
+
recorded plan if one exists, and the current phase.
|
|
23
|
+
4. STOP and wait for the user's explicit approval. Silence or a topic change
|
|
21
24
|
is NOT approval.
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
5. After approval, resume per phase:
|
|
26
|
+
- `phase/spec` — review gate passed: advance with `sbl phase <ID> plan`,
|
|
27
|
+
then start the plan-before-code pass.
|
|
28
|
+
- `phase/plan` — plan recorded: advance with `sbl phase <ID> impl` once the
|
|
29
|
+
human approves the plan, then TDD.
|
|
30
|
+
- `phase/impl` — refresh context from the recorded plan and implementation
|
|
31
|
+
notes, then continue TDD where it stopped.
|
|
32
|
+
- `phase/verify` — collect verification evidence (tests/lint/typecheck),
|
|
33
|
+
write the final summary, then `sbl phase <ID> done` at archival.
|
|
34
|
+
- `none` — a task without a phase label is either legacy (offer
|
|
35
|
+
`sbl phase <ID> spec`) or not yet started (walk the review gate first).
|
|
36
|
+
6. Set the task In Progress via the backlog CLI when work starts.
|
|
24
37
|
|
|
25
38
|
## Boundaries
|
|
26
39
|
|
|
27
40
|
- Never approve the gate yourself; vague consent is not approval.
|
|
41
|
+
- Phase changes only via `sbl phase` — never edit labels by hand.
|
|
28
42
|
- Trivial edits stay exempt only on explicit user instruction.
|
|
29
43
|
- If acceptance criteria look wrong or incomplete, send the user back to task
|
|
30
44
|
editing instead of starting.
|
|
@@ -8,17 +8,17 @@ methodology skills that decide how the work is done.
|
|
|
8
8
|
|
|
9
9
|
### Pipeline (follow in order)
|
|
10
10
|
|
|
11
|
-
| # | Phase | Gate to pass |
|
|
12
|
-
|
|
13
|
-
| 1 | Idea | User states a need; capture it before doing anything else |
|
|
14
|
-
| 2 | Brainstorming | Explore intent, requirements and design before any creative work |
|
|
15
|
-
| 3 | Design gate | Human approves the design document |
|
|
16
|
-
| 4 | Spec-to-backlog | Decompose the approved design into reviewed tasks with acceptance criteria |
|
|
17
|
-
| 5 | Review gate | Human reviews specs and acceptance criteria before any code exists |
|
|
18
|
-
| 6 | Plan-before-code | A written implementation plan is approved by the human |
|
|
19
|
-
| 7 | TDD implementation | Failing test first, then code; one task per session/PR |
|
|
20
|
-
| 8 | Verification & final summary | Run tests/lint/typecheck; verification evidence before success claims |
|
|
21
|
-
| 9 | Merge & archive | Merge the branch, then close/archive the task via the backlog CLI |
|
|
11
|
+
| # | Phase | Phase label | Gate to pass |
|
|
12
|
+
|---|-------|-------------|--------------|
|
|
13
|
+
| 1 | Idea | — | User states a need; capture it before doing anything else |
|
|
14
|
+
| 2 | Brainstorming | — | Explore intent, requirements and design before any creative work |
|
|
15
|
+
| 3 | Design gate | — | Human approves the design document |
|
|
16
|
+
| 4 | Spec-to-backlog | `phase/spec` set at creation | Decompose the approved design into reviewed tasks with acceptance criteria |
|
|
17
|
+
| 5 | Review gate | `phase/spec` | Human reviews specs and acceptance criteria before any code exists |
|
|
18
|
+
| 6 | Plan-before-code | `phase/plan` | A written implementation plan is approved by the human |
|
|
19
|
+
| 7 | TDD implementation | `phase/impl` | Failing test first, then code; one task per session/PR |
|
|
20
|
+
| 8 | Verification & final summary | `phase/verify` | Run tests/lint/typecheck; verification evidence before success claims |
|
|
21
|
+
| 9 | Merge & archive | label removed (`done`) | Merge the branch, then close/archive the task via the backlog CLI |
|
|
22
22
|
|
|
23
23
|
### Binding rules
|
|
24
24
|
|
|
@@ -26,6 +26,7 @@ methodology skills that decide how the work is done.
|
|
|
26
26
|
2. Plan before code — implementation starts only after an approved written plan.
|
|
27
27
|
3. Task status changes always go through the CLI backed by verification evidence, never from memory.
|
|
28
28
|
4. Skills take precedence over habit whenever a matching skill exists.
|
|
29
|
+
5. Phase transitions only via `sbl phase <id> <phase>`, always at a gate passage — never edit phase labels by hand.
|
|
29
30
|
|
|
30
31
|
Project-specific human gates are intentionally out of scope for this block.
|
|
31
32
|
Add project-specific human gates below the block.
|