spectoflow 0.13.4 → 0.14.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/README.md +5 -0
- package/package.json +1 -1
- package/templates/agents/spec-source-guardian.md +58 -0
- package/templates/capabilities.md +5 -1
- package/templates/dashboard/public/app.js +27 -8
- package/templates/dashboard/public/index.html +1 -0
- package/templates/dashboard/public/styles.css +6 -0
- package/templates/hooks/spec-drift.js +58 -0
- package/templates/lib/spec-drift.js +68 -0
- package/templates/policy.md +4 -0
- package/templates/skills/audit-source/SKILL.md +57 -0
package/README.md
CHANGED
|
@@ -157,6 +157,11 @@ runs a skill. Improve a skill without touching the agent.
|
|
|
157
157
|
Agents and skills follow real domain standards, cited in-file — TDD, OWASP ASVS/Top 10, C4/ADR,
|
|
158
158
|
INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liners.
|
|
159
159
|
|
|
160
|
+
A `governance` capability adds a **Spec Source Guardian** (skill `audit-source`): it keeps the spec
|
|
161
|
+
(intent) and the code/tests (reality) coherent — flagging drift in both directions, never auto-fixing,
|
|
162
|
+
surfacing findings to the Attention tab, and gating only at `done`/Major. It ships with a zero-dep
|
|
163
|
+
drift helper (`lib/spec-drift.js`) and an opt-in Claude Code `Stop` hook (`hooks/spec-drift.js`).
|
|
164
|
+
|
|
160
165
|
## Language
|
|
161
166
|
|
|
162
167
|
`.spectoflow/config.json` → `language` (default `en`, incl. code comments). Switchable from the CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-source-guardian
|
|
3
|
+
title: Spec Source Guardian
|
|
4
|
+
capability: governance
|
|
5
|
+
uses: [audit-source]
|
|
6
|
+
description: Keeps the spec (intent) and the code/tests (reality) in sync — flags drift, never silently fixes.
|
|
7
|
+
standards: [traceability matrix, EARS acceptance criteria, spec-anchored]
|
|
8
|
+
---
|
|
9
|
+
# Spec Source Guardian
|
|
10
|
+
|
|
11
|
+
Stable team persona (the "who") for the `governance` capability. The *how* lives in the `audit-source`
|
|
12
|
+
skill (see `uses`). Delegate here to keep the **source of truth coherent**: the spec is the intent of
|
|
13
|
+
record, the code and tests are the enforced reality, and this role makes sure they don't drift apart.
|
|
14
|
+
|
|
15
|
+
## Mandate
|
|
16
|
+
Audit the alignment **spec ↔ plan ↔ code ↔ tests** in both directions, and surface divergence as
|
|
17
|
+
advisory findings. It guards *coherence and traceability*, not spec authorship (that is the
|
|
18
|
+
business-analyst) and not code quality (that is the code-reviewer). It **never edits** the spec or the
|
|
19
|
+
code to "fix" drift — natural language is too ambiguous to auto-sync safely; it flags, and a human or
|
|
20
|
+
the owning capability resolves.
|
|
21
|
+
|
|
22
|
+
## Operating standards
|
|
23
|
+
- **Spec-anchored, not spec-as-source.** Treat the spec as intent + decisions + acceptance criteria;
|
|
24
|
+
keep the code the source of truth and the **tests the enforcer**. Why: regenerating a system from
|
|
25
|
+
prose is lossy and unpredictable (Thoughtworks); the durable value is the spec's decisions and the
|
|
26
|
+
tests that pin them down, not the prose replacing the code.
|
|
27
|
+
- **Traceability, both directions.** Flag *orphan work* (code/tasks with no spec/decision backing) and
|
|
28
|
+
*dead spec* (requirements with no implementation or test). Why: drift hides in whichever direction
|
|
29
|
+
is unwatched; a one-way check misses half of it.
|
|
30
|
+
- **Acceptance criteria as the contract (EARS).** Verify the new/changed behaviour is expressed as
|
|
31
|
+
testable acceptance criteria and that a test actually encodes them. Why: a decision no test enforces
|
|
32
|
+
will silently rot; the test is what keeps intent and reality bound.
|
|
33
|
+
- **Advisory by default; gate only at `done` / Major.** Findings post to the Attention tab; only a
|
|
34
|
+
Major or a `done` is gated on unresolved drift (see `policy.md`). Why: blocking every edit is noise
|
|
35
|
+
and pushes teams to bypass the guardian entirely.
|
|
36
|
+
|
|
37
|
+
## Definition of done
|
|
38
|
+
An audit exists that names each drift finding (direction + spec/plan/code/test involved + why), posts
|
|
39
|
+
it to Attention, and records a verdict (**aligned** or **drift**). No unresolved *warn*-level drift is
|
|
40
|
+
left unacknowledged when a Major or a `done` is declared.
|
|
41
|
+
|
|
42
|
+
## Handoff
|
|
43
|
+
Posts findings to the group chat and the Attention tab via the `::spectoflow` sentinels (exact syntax
|
|
44
|
+
owned by the `audit-source` skill's Output contract). Drift returns to the owning capability
|
|
45
|
+
(business-analyst to update the spec, developer/qa to add the missing test) — it is not fixed here.
|
|
46
|
+
|
|
47
|
+
## Guardrails
|
|
48
|
+
- Never edit a spec or code to reconcile drift — flag it; the owner decides how to resolve.
|
|
49
|
+
- Never auto-generate code from the spec, or rewrite the spec from the code — no lossy auto-sync.
|
|
50
|
+
- Never gate an ordinary edit; only a Major or a `done` may be gated, and only on unresolved drift.
|
|
51
|
+
- Prefer a `need` / Attention item over a hard block when intent is genuinely ambiguous.
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
- Thoughtworks — "Spec-driven development": code stays the source of truth, spec drives generation —
|
|
55
|
+
https://www.thoughtworks.com/en-us/insights/blog/agile-engineering-practices/spec-driven-development-unpacking-2025-new-engineering-practices
|
|
56
|
+
- Sean Grove (OpenAI), "The New Code" — specifications as the durable, versioned artifact —
|
|
57
|
+
https://lawwu.github.io/transcripts/8rABwKRsec4.html
|
|
58
|
+
- EARS (Easy Approach to Requirements Syntax) for testable acceptance criteria.
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
A capability is a role; an agent implements it; a skill is the procedure it runs. Workflows ask for a
|
|
4
4
|
capability, never a named agent — this keeps spectoflow agent-agnostic.
|
|
5
5
|
|
|
6
|
-
Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations.
|
|
6
|
+
Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance.
|
|
7
|
+
|
|
8
|
+
`governance` is the odd one out: it is **advisory, not a workflow step**. The `spec-source-guardian`
|
|
9
|
+
(skill `audit-source`) watches that the spec (intent) and the code/tests (reality) stay coherent, and
|
|
10
|
+
surfaces drift to the Attention tab; it gates only at `done`/Major (see `policy.md`), never mid-edit.
|
|
7
11
|
|
|
8
12
|
| Project type | Active capabilities |
|
|
9
13
|
|---|---|
|
|
@@ -328,6 +328,7 @@ function renderBoard(){
|
|
|
328
328
|
board.append(renderPhase(ph,pl.file,filtered));
|
|
329
329
|
}));
|
|
330
330
|
if(!shown) board.append(noMatchState());
|
|
331
|
+
updatePhaseToggleAll();
|
|
331
332
|
}
|
|
332
333
|
// Kanban view — one column per status, filtered by the text search (columns already are the statuses).
|
|
333
334
|
function renderKanban(board, tasks){
|
|
@@ -435,16 +436,26 @@ function backlogRow(r){
|
|
|
435
436
|
return tr;
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
// ----
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
// ---- phase expand state: we track which phases are EXPANDED (default = none), so on a big
|
|
440
|
+
// project the board opens compact — just phase headers with progress — and the user opens what
|
|
441
|
+
// they need. Persisted per phase title (guarded for private mode). ----
|
|
442
|
+
function loadExpanded(){
|
|
443
|
+
try{ const raw=localStorage.getItem('spf-expanded'); const arr=raw?JSON.parse(raw):[]; return new Set(Array.isArray(arr)?arr:[]); }
|
|
441
444
|
catch{ return new Set(); }
|
|
442
445
|
}
|
|
443
|
-
function
|
|
444
|
-
let
|
|
446
|
+
function saveExpanded(set){ try{ localStorage.setItem('spf-expanded', JSON.stringify([...set])); }catch{} }
|
|
447
|
+
let expandedPhases=loadExpanded();
|
|
448
|
+
function allPhaseTitles(){ const t=new Set(); (P.plans||[]).forEach(pl=> pl.phases.forEach(ph=> t.add(ph.title))); return [...t]; }
|
|
449
|
+
function updatePhaseToggleAll(){
|
|
450
|
+
const btn=$('#phaseToggleAll'); if(!btn) return;
|
|
451
|
+
const titles=allPhaseTitles();
|
|
452
|
+
const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
|
|
453
|
+
btn.textContent = allOpen ? 'Collapse all' : 'Expand all';
|
|
454
|
+
btn.dataset.state = allOpen ? 'open' : 'closed';
|
|
455
|
+
}
|
|
445
456
|
|
|
446
457
|
function renderPhase(ph,file,filteredTasks){
|
|
447
|
-
const isCollapsed
|
|
458
|
+
const isCollapsed=!expandedPhases.has(ph.title);
|
|
448
459
|
const sec=el('section','phase'+(isCollapsed?' is-collapsed':''));
|
|
449
460
|
const head=el('div','phase-head'); head.tabIndex=0; head.setAttribute('role','button'); head.setAttribute('aria-expanded',String(!isCollapsed));
|
|
450
461
|
head.append(el('span','chevron'));
|
|
@@ -458,8 +469,8 @@ function renderPhase(ph,file,filteredTasks){
|
|
|
458
469
|
const toggle=()=>{
|
|
459
470
|
const now=sec.classList.toggle('is-collapsed');
|
|
460
471
|
head.setAttribute('aria-expanded',String(!now));
|
|
461
|
-
if(now)
|
|
462
|
-
|
|
472
|
+
if(now) expandedPhases.delete(ph.title); else expandedPhases.add(ph.title);
|
|
473
|
+
saveExpanded(expandedPhases); updatePhaseToggleAll();
|
|
463
474
|
};
|
|
464
475
|
head.addEventListener('click',toggle);
|
|
465
476
|
head.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggle(); } });
|
|
@@ -953,6 +964,14 @@ $$('#statusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ filter.s
|
|
|
953
964
|
$('#search').addEventListener('input', e=>{ filter.q=e.target.value; renderBoard(); });
|
|
954
965
|
// board view switch — List (phase-grouped) vs Kanban (columns by status), persisted per viewer
|
|
955
966
|
$$('#boardViewToggle .vt-btn').forEach(b=> b.addEventListener('click', ()=>{ boardView=b.dataset.view; try{ localStorage.setItem('spf-board-view',boardView); }catch{} renderBoard(); }));
|
|
967
|
+
// expand / collapse all phases (List view) — keeps a big board compact by default
|
|
968
|
+
const phaseToggleAllBtn=$('#phaseToggleAll');
|
|
969
|
+
if(phaseToggleAllBtn) phaseToggleAllBtn.addEventListener('click', ()=>{
|
|
970
|
+
const titles=allPhaseTitles();
|
|
971
|
+
const allOpen = titles.length>0 && titles.every(t=> expandedPhases.has(t));
|
|
972
|
+
if(allOpen) expandedPhases.clear(); else titles.forEach(t=> expandedPhases.add(t));
|
|
973
|
+
saveExpanded(expandedPhases); renderBoard();
|
|
974
|
+
});
|
|
956
975
|
// backlog: independent filters + sortable column headers — client-side only (reset to page 1 on change)
|
|
957
976
|
$$('#backlogStatusChips .fchip').forEach(b=> b.addEventListener('click', ()=>{ backlogFilter.status=b.dataset.status; backlogPage=1; renderBacklog(); }));
|
|
958
977
|
$('#backlogSearch').addEventListener('input', e=>{ backlogFilter.q=e.target.value; backlogPage=1; renderBacklog(); });
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
<button class="vt-btn active" data-view="list" title="Grouped by phase"><span class="tab-ico" data-icon="backlog"></span><span>List</span></button>
|
|
66
66
|
<button class="vt-btn" data-view="kanban" title="Columns by status"><span class="tab-ico" data-icon="board"></span><span>Kanban</span></button>
|
|
67
67
|
</div>
|
|
68
|
+
<button class="mini-btn" id="phaseToggleAll" title="Expand or collapse all phases">Expand all</button>
|
|
68
69
|
<input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
|
|
69
70
|
</div>
|
|
70
71
|
<div class="board" id="board"></div>
|
|
@@ -815,3 +815,9 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
815
815
|
.kanban-col-body { display:flex; flex-direction:column; gap:8px; padding:10px; }
|
|
816
816
|
.kanban-empty { color:var(--faint); font-size:12px; text-align:center; padding:10px 0; }
|
|
817
817
|
.kanban .task { width:auto; }
|
|
818
|
+
|
|
819
|
+
/* ---- board compactness: expand/collapse-all button + kanban column scroll (v0.13.5) ---- */
|
|
820
|
+
.mini-btn { font:inherit; font-size:11.5px; padding:5px 12px; border:1px solid var(--line); border-radius:999px; background:var(--surface); color:var(--muted); cursor:pointer; flex-shrink:0; }
|
|
821
|
+
.mini-btn:hover { color:var(--ink); border-color:var(--cool); }
|
|
822
|
+
.main:has(.board.is-kanban) #phaseToggleAll { display:none; } /* phases are a List concept */
|
|
823
|
+
.kanban-col-body { max-height:64vh; overflow-y:auto; } /* keep columns compact on a big project */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Claude Code "Stop" hook (OPT-IN) — the spec-source-guardian's watchdog.
|
|
4
|
+
*
|
|
5
|
+
* At the end of an agent turn it runs the deterministic spec-drift check on the git working tree and
|
|
6
|
+
* appends any source-of-truth signals to the dashboard's Attention tab. Advisory only: it NEVER edits
|
|
7
|
+
* code or specs and NEVER fails the agent (always exits 0).
|
|
8
|
+
*
|
|
9
|
+
* Enable it by adding this to your project's .claude/settings.json (spectoflow does NOT wire it
|
|
10
|
+
* automatically, to avoid clobbering your settings):
|
|
11
|
+
*
|
|
12
|
+
* {
|
|
13
|
+
* "hooks": {
|
|
14
|
+
* "Stop": [
|
|
15
|
+
* { "hooks": [ { "type": "command", "command": "node .spectoflow/hooks/spec-drift.js" } ] }
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Deduped against still-open Attention items, so it never spams.
|
|
21
|
+
*/
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { execFileSync } = require('child_process');
|
|
25
|
+
const { classifyChange, coverageSignals } = require('../lib/spec-drift');
|
|
26
|
+
|
|
27
|
+
let input = '';
|
|
28
|
+
process.stdin.on('data', (c) => { input += c; });
|
|
29
|
+
process.stdin.on('end', () => { try { run(); } catch { /* never fail the agent */ } process.exit(0); });
|
|
30
|
+
|
|
31
|
+
function run() {
|
|
32
|
+
const root = process.cwd();
|
|
33
|
+
let changed = [];
|
|
34
|
+
try {
|
|
35
|
+
changed = execFileSync('git', ['status', '--porcelain', '-uall'], { cwd: root, encoding: 'utf8' })
|
|
36
|
+
.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
37
|
+
} catch {}
|
|
38
|
+
const ls = (d) => { try { return fs.readdirSync(path.join(root, d)).filter((f) => f.endsWith('.md')); } catch { return []; } };
|
|
39
|
+
const signals = [...classifyChange(changed), ...coverageSignals({ specs: ls('specs'), plans: ls('plans') })];
|
|
40
|
+
if (!signals.length) return;
|
|
41
|
+
|
|
42
|
+
const rp = path.join(root, '.spectoflow', 'runtime.json');
|
|
43
|
+
let rt = {};
|
|
44
|
+
try { rt = JSON.parse(fs.readFileSync(rp, 'utf8')); } catch {}
|
|
45
|
+
rt.attention = rt.attention || [];
|
|
46
|
+
const open = new Set(rt.attention.filter((a) => a.status !== 'resolved').map((a) => a.text));
|
|
47
|
+
let added = false;
|
|
48
|
+
for (const s of signals) {
|
|
49
|
+
const text = 'Source-of-truth: ' + s.msg;
|
|
50
|
+
if (open.has(text)) continue;
|
|
51
|
+
rt.attention.unshift({
|
|
52
|
+
id: 'att' + Date.now().toString(36) + Math.floor(Math.random() * 1e3).toString(36),
|
|
53
|
+
at: new Date().toISOString(), by: 'spec-source-guardian', source: 'agent', status: 'open', text,
|
|
54
|
+
});
|
|
55
|
+
added = true;
|
|
56
|
+
}
|
|
57
|
+
if (added) { try { fs.writeFileSync(rp, JSON.stringify(rt, null, 2) + '\n'); } catch {} }
|
|
58
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* spec-drift — a small, ZERO-DEPENDENCY helper for the spec-source-guardian.
|
|
4
|
+
*
|
|
5
|
+
* It computes deterministic, ADVISORY signals about source-of-truth drift between the spec (intent)
|
|
6
|
+
* and the code/tests (reality). It never edits anything and never blocks — natural-language specs
|
|
7
|
+
* can't be auto-synced safely, so this only *flags* divergence for a human (or the guardian agent)
|
|
8
|
+
* to judge. That is the honest, spec-anchored stance: the spec stays the intent of record, the code
|
|
9
|
+
* and tests stay the enforced reality, and a guardian keeps them from silently drifting apart.
|
|
10
|
+
*
|
|
11
|
+
* Pure, unit-tested functions: classifyChange(paths), coverageSignals({specs, plans}).
|
|
12
|
+
* CLI: `node spec-drift.js` — inspects the git working tree + specs/plans and prints signals, and
|
|
13
|
+
* emits `::spectoflow attention msg=…` lines that the dashboard turns into Attention items.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CODE_EXT = /\.(js|jsx|ts|tsx|py|go|rs|java|rb|php|c|cc|cpp|h|hpp|cs|kt|swift|scala|sql|sh)$/i;
|
|
17
|
+
const isSpec = (p) => /(^|\/)specs?\//.test(p);
|
|
18
|
+
const isPlan = (p) => /(^|\/)plans?\//.test(p);
|
|
19
|
+
const isFramework = (p) => /(^|\/)\.spectoflow\//.test(p);
|
|
20
|
+
const isTest = (p) => (/(^|[._-])(test|spec|e2e)s?([._-]|\/|$)/i.test(p) || /(^|\/)(tests?|__tests__)\//i.test(p)) && !isSpec(p) && !isPlan(p);
|
|
21
|
+
const isCode = (p) => CODE_EXT.test(p) && !isFramework(p) && !isTest(p);
|
|
22
|
+
|
|
23
|
+
// Advisory drift signals for the set of paths changed in one unit of work → [{level, msg}].
|
|
24
|
+
function classifyChange(paths) {
|
|
25
|
+
const list = (paths || []).map(String);
|
|
26
|
+
const has = (fn) => list.some(fn);
|
|
27
|
+
const codeOrTest = has((p) => (isCode(p) || isTest(p)) && !isSpec(p) && !isPlan(p) && !isFramework(p));
|
|
28
|
+
const specOrPlan = has((p) => isSpec(p) || isPlan(p));
|
|
29
|
+
const out = [];
|
|
30
|
+
if (codeOrTest && !specOrPlan)
|
|
31
|
+
out.push({ level: 'warn', msg: 'code/tests changed but no specs/ or plans/ file was updated — confirm the spec still matches, or update it.' });
|
|
32
|
+
if (has(isSpec) && !has(isCode) && !has(isTest))
|
|
33
|
+
out.push({ level: 'info', msg: 'a spec changed but no code/tests followed — make sure a plan task and tests carry the new intent.' });
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Coverage signals from the presence of spec/plan markdown files (not their content) → [{level, msg}].
|
|
38
|
+
function coverageSignals({ specs, plans } = {}) {
|
|
39
|
+
const s = (specs || []).length, p = (plans || []).length;
|
|
40
|
+
const out = [];
|
|
41
|
+
if (p > 0 && s === 0)
|
|
42
|
+
out.push({ level: 'warn', msg: 'plans/ has work but specs/ is empty — the plan has no spec to trace back to (source of truth missing).' });
|
|
43
|
+
if (s > 0 && p === 0)
|
|
44
|
+
out.push({ level: 'info', msg: 'specs/ exists but plans/ is empty — the spec is not yet broken into traceable, testable tasks.' });
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { classifyChange, coverageSignals, isCode, isTest, isSpec, isPlan };
|
|
49
|
+
|
|
50
|
+
// ---- CLI ------------------------------------------------------------------
|
|
51
|
+
if (require.main === module) {
|
|
52
|
+
const fs = require('fs'), path = require('path'), { execFileSync } = require('child_process');
|
|
53
|
+
const root = process.cwd();
|
|
54
|
+
let changed = [];
|
|
55
|
+
try {
|
|
56
|
+
changed = execFileSync('git', ['status', '--porcelain', '-uall'], { cwd: root, encoding: 'utf8' })
|
|
57
|
+
.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
58
|
+
} catch {}
|
|
59
|
+
const ls = (d) => { try { return fs.readdirSync(path.join(root, d)).filter((f) => f.endsWith('.md')); } catch { return []; } };
|
|
60
|
+
const signals = [...classifyChange(changed), ...coverageSignals({ specs: ls('specs'), plans: ls('plans') })];
|
|
61
|
+
if (!signals.length) { console.log('spec-drift: no source-of-truth signals — spec and reality look aligned.'); process.exit(0); }
|
|
62
|
+
console.log('spec-drift — advisory source-of-truth signals (judge, do not auto-apply):');
|
|
63
|
+
for (const s of signals) {
|
|
64
|
+
console.log(` [${s.level}] ${s.msg}`);
|
|
65
|
+
console.log(`::spectoflow attention msg=Source-of-truth: ${s.msg}`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
package/templates/policy.md
CHANGED
|
@@ -6,6 +6,10 @@ Orthogonal to mode. Even in autopilot, these require **explicit human approval**
|
|
|
6
6
|
- **Destructive migration** (irreversible drop/alter, data deletion, purge).
|
|
7
7
|
- **Security change** (auth, permissions, secrets, network exposure, session lifetime).
|
|
8
8
|
- **Committing spend / external side effect** (payment, purchase, mass send).
|
|
9
|
+
- **Source-of-truth drift at `done` / Major** (governance) — before a Major, or before a task flips to
|
|
10
|
+
`done`, the `spec-source-guardian`'s drift check must be clean (or the drift explicitly accepted):
|
|
11
|
+
the change is reflected in the spec **and** covered by a test. This gate *acknowledges*, it does not
|
|
12
|
+
auto-fix — unresolved drift blocks the "done" until resolved or accepted.
|
|
9
13
|
|
|
10
14
|
When a step hits a gate: stop, explain the act and its risk in one line, ask [Approve / Cancel /
|
|
11
15
|
Modify], and record the decision in the runtime log. Overridable per project (add or relax gates).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-source
|
|
3
|
+
description: Audit spec ↔ code ↔ tests alignment; surface drift as advisory findings, never auto-fix.
|
|
4
|
+
capability: governance
|
|
5
|
+
inputs: The specs/, plans/, code and tests; optionally the recent change set (git diff).
|
|
6
|
+
outputs: A drift report + Attention items; a verdict (aligned / drift).
|
|
7
|
+
standard: spec-anchored traceability
|
|
8
|
+
---
|
|
9
|
+
# Audit source-of-truth
|
|
10
|
+
|
|
11
|
+
Scoped audit that keeps the **spec (intent)** and the **code/tests (reality)** coherent by flagging
|
|
12
|
+
drift in both directions. Advisory — it reports, it never edits or auto-syncs.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
- Before a Major or a `done` (the governance gate in `policy.md`).
|
|
16
|
+
- After a batch of changes, to catch drift early (a `Stop` hook can trigger it — see `hooks/`).
|
|
17
|
+
- On demand: "is the spec still the source of truth for what's built?"
|
|
18
|
+
|
|
19
|
+
## Method
|
|
20
|
+
1. **Build the trace.** For each spec section/decision, find the plan task(s), the code, and the
|
|
21
|
+
test(s) that carry it; for each plan task and code change, find the spec section it serves.
|
|
22
|
+
2. **Flag both directions.**
|
|
23
|
+
- *Orphan work* — code or a task with no spec/decision backing → the decision is undocumented.
|
|
24
|
+
- *Dead spec* — a requirement with no implementation or no test → intent that will rot.
|
|
25
|
+
3. **Check the enforcer.** Confirm the new/changed behaviour has **acceptance criteria** (EARS /
|
|
26
|
+
Given-When-Then) and that a **test actually encodes them** — tests are what bind intent to reality.
|
|
27
|
+
4. **Run the deterministic helper** (optional, zero-dep): `node .spectoflow/lib/spec-drift.js` — it
|
|
28
|
+
inspects the git working tree + specs/plans and prints coupling/coverage signals (e.g. "code
|
|
29
|
+
changed but no specs/ or plans/ updated"). Treat its output as signals to judge, not verdicts.
|
|
30
|
+
5. **Judge, don't auto-apply.** Where intent is ambiguous, raise a `need` rather than guess. Never
|
|
31
|
+
regenerate code from the spec or rewrite the spec from the code.
|
|
32
|
+
|
|
33
|
+
## Output contract
|
|
34
|
+
Write findings as a report / task comment (granular, one line at a time), each carrying: the drift
|
|
35
|
+
direction, the spec/plan/code/test involved, and why it matters. End with a verdict: **aligned** or
|
|
36
|
+
**drift**. Surface to the orchestrator, group chat, and the **Attention tab** with:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
::spectoflow role=governance kind=review msg=<verdict + counts>
|
|
40
|
+
::spectoflow attention msg=Source-of-truth: <one drift finding, actionable>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `attention` line becomes an item in the dashboard's Attention tab (the user can edit, resolve, or
|
|
44
|
+
**validate → task**). Do not edit the spec or the code to reconcile drift — that is the owner's call.
|
|
45
|
+
|
|
46
|
+
## Quality bar
|
|
47
|
+
- [ ] Trace built **both directions** (orphan work *and* dead spec), not just one.
|
|
48
|
+
- [ ] Each new/changed behaviour has testable acceptance criteria **and** a test that encodes them.
|
|
49
|
+
- [ ] Findings are actionable (name the spec section / task / file, and the resolution owner).
|
|
50
|
+
- [ ] Nothing was auto-fixed, regenerated, or silently reconciled.
|
|
51
|
+
- [ ] A clear verdict (aligned / drift) is stated; unresolved *warn* drift is surfaced to Attention.
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
- Thoughtworks — spec-driven development (code as source of truth, tests as enforcer) —
|
|
55
|
+
https://www.thoughtworks.com/en-us/insights/blog/agile-engineering-practices/spec-driven-development-unpacking-2025-new-engineering-practices
|
|
56
|
+
- Augment Code — spec-anchored vs spec-as-source, drift & traceability —
|
|
57
|
+
https://www.augmentcode.com/guides/spec-as-source-of-truth-rebuildable-codebase
|