spectoflow 0.12.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 +133 -0
- package/bin/spectoflow.js +170 -0
- package/lib/adapters.js +120 -0
- package/lib/detect.js +34 -0
- package/lib/manifest.js +36 -0
- package/lib/ownership.js +34 -0
- package/lib/update.js +78 -0
- package/package.json +30 -0
- package/templates/AGENTS.md +66 -0
- package/templates/agents/architect.md +54 -0
- package/templates/agents/business-analyst.md +53 -0
- package/templates/agents/code-reviewer.md +58 -0
- package/templates/agents/developer.md +73 -0
- package/templates/agents/devops.md +59 -0
- package/templates/agents/product-manager.md +53 -0
- package/templates/agents/qa-engineer.md +63 -0
- package/templates/agents/security-engineer.md +59 -0
- package/templates/agents/tech-lead.md +52 -0
- package/templates/agents/ux-designer.md +50 -0
- package/templates/capabilities.md +15 -0
- package/templates/config.json +10 -0
- package/templates/dashboard/orchestrator.js +117 -0
- package/templates/dashboard/public/app.js +747 -0
- package/templates/dashboard/public/charts.js +192 -0
- package/templates/dashboard/public/icons.js +28 -0
- package/templates/dashboard/public/index.html +226 -0
- package/templates/dashboard/public/stats.js +32 -0
- package/templates/dashboard/public/styles.css +426 -0
- package/templates/dashboard/runner.js +77 -0
- package/templates/dashboard/server.js +115 -0
- package/templates/lib/store.js +289 -0
- package/templates/policy.md +11 -0
- package/templates/skills/analyze-requirements/SKILL.md +67 -0
- package/templates/skills/brainstorm/SKILL.md +58 -0
- package/templates/skills/code-review/SKILL.md +67 -0
- package/templates/skills/implement/SKILL.md +80 -0
- package/templates/skills/security-review/SKILL.md +70 -0
- package/templates/skills/write-adr/SKILL.md +61 -0
- package/templates/skills/write-e2e-tests/SKILL.md +99 -0
- package/templates/skills/write-plan/SKILL.md +66 -0
- package/templates/skills/write-spec/SKILL.md +66 -0
- package/templates/skills/write-tests/SKILL.md +80 -0
- package/templates/workflow.md +15 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* spectoflow storage engine (zero dependency).
|
|
4
|
+
*
|
|
5
|
+
* Artifacts are MARKDOWN (human source of truth, versioned):
|
|
6
|
+
* plans/<name>.md — tasks as checkbox lines:
|
|
7
|
+
* ## Phase title
|
|
8
|
+
* - [ ] T-012 Add login form @dev ~standard %in_progress
|
|
9
|
+
* - note: some comment (indented sub-bullets = comments)
|
|
10
|
+
* specs/<name>.md — free-form spec markdown (listed, not parsed into tasks)
|
|
11
|
+
*
|
|
12
|
+
* Volatile execution state is JSON (gitignored, never read by humans):
|
|
13
|
+
* .spectoflow/runtime.json — running agents, heartbeats, test results
|
|
14
|
+
*
|
|
15
|
+
* Writes are GRANULAR: we locate a task's line by id and rewrite only that line (or insert a
|
|
16
|
+
* sub-bullet), leaving the rest of the file byte-for-byte intact.
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
// ---- task line parsing -------------------------------------------------------
|
|
22
|
+
// - [ ] T-012 Title here @owner ~level %status
|
|
23
|
+
const LINE_RE = /^(\s*)- \[( |x|X)\]\s+(\S+)\s*(.*)$/;
|
|
24
|
+
|
|
25
|
+
function parseTaskLine(line) {
|
|
26
|
+
const m = line.match(LINE_RE);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
const [, indent, check, id, rest0] = m;
|
|
29
|
+
let rest = rest0;
|
|
30
|
+
const owner = (rest.match(/(?:^|\s)@([\w-]+)/) || [])[1] || null;
|
|
31
|
+
const level = (rest.match(/(?:^|\s)~([\w-]+)/) || [])[1] || null;
|
|
32
|
+
const statusTag = (rest.match(/(?:^|\s)%([\w-]+)/) || [])[1] || null;
|
|
33
|
+
const title = rest.replace(/(?:^|\s)[@~%][\w-]+/g, '').replace(/\s+/g, ' ').trim();
|
|
34
|
+
const done = check.toLowerCase() === 'x';
|
|
35
|
+
const status = done ? 'done' : (statusTag || 'todo');
|
|
36
|
+
return { indent, id, title, owner, level: level || 'standard', status, done };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildTaskLine(t) {
|
|
40
|
+
const parts = [`${t.indent || ''}- [${t.status === 'done' ? 'x' : ' '}] ${t.id} ${t.title}`];
|
|
41
|
+
if (t.owner) parts.push(`@${t.owner}`);
|
|
42
|
+
if (t.level && t.level !== 'standard') parts.push(`~${t.level}`);
|
|
43
|
+
if (t.status !== 'done' && t.status !== 'todo') parts.push(`%${t.status}`);
|
|
44
|
+
return parts.join(' ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- plan file model ---------------------------------------------------------
|
|
48
|
+
function parsePlan(text) {
|
|
49
|
+
const lines = text.split('\n');
|
|
50
|
+
const phases = [];
|
|
51
|
+
let cur = { id: 'P0', title: 'Tasks', goal: '', tasks: [] };
|
|
52
|
+
let lastTask = null;
|
|
53
|
+
const pushPhase = () => { if (cur.tasks.length || cur.title !== 'Tasks') phases.push(cur); };
|
|
54
|
+
lines.forEach((line) => {
|
|
55
|
+
const h = line.match(/^##\s+(.*)$/);
|
|
56
|
+
if (h) { pushPhase(); cur = { id: 'P' + phases.length, title: h[1].trim(), goal: '', tasks: [] }; lastTask = null; return; }
|
|
57
|
+
const t = parseTaskLine(line);
|
|
58
|
+
if (t) { t.comments = []; cur.tasks.push(t); lastTask = t; return; }
|
|
59
|
+
const c = line.match(/^\s+- (?:note:|comment:)?\s*(.*)$/);
|
|
60
|
+
if (c && lastTask && c[1].trim()) lastTask.comments.push(c[1].trim());
|
|
61
|
+
});
|
|
62
|
+
pushPhase();
|
|
63
|
+
return phases;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readPlans(projectRoot) {
|
|
67
|
+
const dir = path.join(projectRoot, 'plans');
|
|
68
|
+
if (!fs.existsSync(dir)) return [];
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.md')).sort()) {
|
|
71
|
+
const text = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
72
|
+
out.push({ file: f, phases: parsePlan(text) });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- granular writes ---------------------------------------------------------
|
|
78
|
+
// Rewrite only the line whose task id matches, preserving everything else.
|
|
79
|
+
function updateTaskLine(projectRoot, file, id, patch) {
|
|
80
|
+
const fp = path.join(projectRoot, 'plans', file);
|
|
81
|
+
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
82
|
+
let changed = false;
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
const t = parseTaskLine(lines[i]);
|
|
85
|
+
if (t && t.id === id) {
|
|
86
|
+
const next = Object.assign(t, patch);
|
|
87
|
+
lines[i] = buildTaskLine(next);
|
|
88
|
+
changed = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (changed) writeAtomic(fp, lines.join('\n'));
|
|
93
|
+
return changed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addTaskComment(projectRoot, file, id, text, author) {
|
|
97
|
+
const fp = path.join(projectRoot, 'plans', file);
|
|
98
|
+
const lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const t = parseTaskLine(lines[i]);
|
|
101
|
+
if (t && t.id === id) {
|
|
102
|
+
const indent = (t.indent || '') + ' ';
|
|
103
|
+
const who = author ? `@${author}: ` : '';
|
|
104
|
+
// insert after any existing comment sub-bullets
|
|
105
|
+
let j = i + 1;
|
|
106
|
+
while (j < lines.length && /^\s+- /.test(lines[j])) j++;
|
|
107
|
+
lines.splice(j, 0, `${indent}- note: ${who}${text}`);
|
|
108
|
+
writeAtomic(fp, lines.join('\n'));
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeAtomic(fp, content) {
|
|
116
|
+
const tmp = fp + '.tmp';
|
|
117
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
118
|
+
fs.renameSync(tmp, fp);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- runtime sidecar (volatile) ---------------------------------------------
|
|
122
|
+
function runtimePath(projectRoot) { return path.join(projectRoot, '.spectoflow', 'runtime.json'); }
|
|
123
|
+
function readRuntime(projectRoot) {
|
|
124
|
+
try { return JSON.parse(fs.readFileSync(runtimePath(projectRoot), 'utf8')); }
|
|
125
|
+
catch { return { agents: [], tests: {}, messages: [], updatedAt: null }; }
|
|
126
|
+
}
|
|
127
|
+
function writeRuntime(projectRoot, rt) {
|
|
128
|
+
rt.updatedAt = new Date().toISOString();
|
|
129
|
+
writeAtomic(runtimePath(projectRoot), JSON.stringify(rt, null, 2) + '\n');
|
|
130
|
+
return rt;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- progress history (volatile, kept inside runtime.json) ------------------
|
|
134
|
+
// Pure helper: mutates+returns `runtime.history` — one {date,total,done} point per calendar day,
|
|
135
|
+
// newest last, capped to 60 points. Same-day calls UPDATE the existing point rather than appending
|
|
136
|
+
// (so re-recording the same day never grows history). No filesystem access here; the caller
|
|
137
|
+
// decides whether the result is worth persisting (see readProject's write-guard).
|
|
138
|
+
function recordSnapshot(runtime, counts, date) {
|
|
139
|
+
runtime.history = runtime.history || [];
|
|
140
|
+
const d = date || new Date().toISOString().slice(0, 10);
|
|
141
|
+
const last = runtime.history[runtime.history.length - 1];
|
|
142
|
+
const snap = { date: d, total: counts.total | 0, done: counts.done | 0 };
|
|
143
|
+
if (last && last.date === d) runtime.history[runtime.history.length - 1] = snap;
|
|
144
|
+
else runtime.history.push(snap);
|
|
145
|
+
if (runtime.history.length > 60) runtime.history = runtime.history.slice(-60);
|
|
146
|
+
return runtime;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- group-chat message log (volatile) --------------------------------------
|
|
150
|
+
// A running agent identifies itself by printing sentinel lines on stdout:
|
|
151
|
+
// ::spectoflow role=developer kind=status msg=finished T-023
|
|
152
|
+
// Everything after `msg=` is free text to end of line. Non-sentinel lines return null.
|
|
153
|
+
function parseAgentLine(line) {
|
|
154
|
+
const s = String(line);
|
|
155
|
+
if (!/^\s*::spectoflow\b/.test(s)) return null;
|
|
156
|
+
return {
|
|
157
|
+
role: (s.match(/\brole=(\S+)/) || [])[1] || 'agent',
|
|
158
|
+
kind: (s.match(/\bkind=(\S+)/) || [])[1] || 'message',
|
|
159
|
+
text: ((s.match(/\bmsg=([\s\S]*)$/) || [])[1] || '').trim(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Append one message to runtime.messages (id + at stamped here; caller can't override them).
|
|
164
|
+
function appendMessage(projectRoot, msg) {
|
|
165
|
+
const rt = readRuntime(projectRoot);
|
|
166
|
+
rt.messages = rt.messages || [];
|
|
167
|
+
const full = { kind: 'message', ...msg,
|
|
168
|
+
id: 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
169
|
+
at: new Date().toISOString() };
|
|
170
|
+
rt.messages.push(full);
|
|
171
|
+
writeRuntime(projectRoot, rt);
|
|
172
|
+
return full;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- config & workflow -------------------------------------------------------
|
|
176
|
+
function readConfig(projectRoot) {
|
|
177
|
+
try { return JSON.parse(fs.readFileSync(path.join(projectRoot, '.spectoflow', 'config.json'), 'utf8')); }
|
|
178
|
+
catch { return { mode: 'semi', language: 'en', agent: 'claude' }; }
|
|
179
|
+
}
|
|
180
|
+
function readWorkflow(projectRoot) {
|
|
181
|
+
try {
|
|
182
|
+
const text = fs.readFileSync(path.join(projectRoot, '.spectoflow', 'workflow.md'), 'utf8').replace(/\r\n?/g, '\n');
|
|
183
|
+
const steps = [];
|
|
184
|
+
text.split('\n').forEach((l) => {
|
|
185
|
+
const m = l.match(/^\s*- \[( |x|X)\]\s+(.*?)\s*$/);
|
|
186
|
+
if (!m) return;
|
|
187
|
+
let rest = m[2], cap = null, skill = null, policy = false;
|
|
188
|
+
const ann = rest.match(/\{([^}]*)\}\s*$/);
|
|
189
|
+
if (ann) {
|
|
190
|
+
rest = rest.slice(0, ann.index).trim();
|
|
191
|
+
cap = (ann[1].match(/\bcap:(\S+)/) || [])[1] || null;
|
|
192
|
+
skill = (ann[1].match(/\bskill:(\S+)/) || [])[1] || null;
|
|
193
|
+
policy = /\bpolicy\b/.test(ann[1]);
|
|
194
|
+
}
|
|
195
|
+
const optional = /\(optional\)/i.test(rest);
|
|
196
|
+
const name = rest.replace(/\s*\(optional\)\s*$/i, '').trim();
|
|
197
|
+
steps.push({ name, enabled: m[1].toLowerCase() === 'x', optional, cap, skill, policy });
|
|
198
|
+
});
|
|
199
|
+
return steps;
|
|
200
|
+
} catch { return []; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- unified read for the dashboard -----------------------------------------
|
|
204
|
+
function readProject(projectRoot) {
|
|
205
|
+
const config = readConfig(projectRoot);
|
|
206
|
+
const plans = readPlans(projectRoot);
|
|
207
|
+
let runtime = readRuntime(projectRoot);
|
|
208
|
+
const workflow = readWorkflow(projectRoot);
|
|
209
|
+
const specs = (() => {
|
|
210
|
+
const d = path.join(projectRoot, 'specs');
|
|
211
|
+
return fs.existsSync(d) ? fs.readdirSync(d).filter((x) => x.endsWith('.md')) : [];
|
|
212
|
+
})();
|
|
213
|
+
const agents = listMd(path.join(projectRoot, '.spectoflow', 'agents'));
|
|
214
|
+
const skills = listSkills(path.join(projectRoot, '.spectoflow', 'skills'));
|
|
215
|
+
|
|
216
|
+
// Write-guarded snapshot: readProject is polled continuously by the dashboard (and reacts to
|
|
217
|
+
// fs.watch on .spectoflow). Recording unconditionally on every read would rewrite runtime.json
|
|
218
|
+
// on every poll → fs.watch fires → SSE 'change' → client re-reads → infinite loop / SSE storm.
|
|
219
|
+
// So we only persist when today's {total,done} actually differs from the last history entry
|
|
220
|
+
// (or history is empty and needs seeding) — a no-op read never touches the filesystem.
|
|
221
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
222
|
+
let total = 0, done = 0;
|
|
223
|
+
for (const pl of plans) for (const ph of pl.phases) for (const t of ph.tasks) { total++; if (t.status === 'done') done++; }
|
|
224
|
+
const history = runtime.history || [];
|
|
225
|
+
const last = history[history.length - 1];
|
|
226
|
+
const changed = !last || last.date !== today || last.total !== total || last.done !== done;
|
|
227
|
+
if (changed) {
|
|
228
|
+
// Concurrency guard: readProject's own initial `readRuntime` above can be stale by the time
|
|
229
|
+
// we're ready to write — a concurrent writer (e.g. appendMessage, from a running agent) may
|
|
230
|
+
// have written runtime.json in between. Writing back our stale in-memory copy would silently
|
|
231
|
+
// clobber whatever that concurrent writer just persisted (messages, agent status, etc).
|
|
232
|
+
// So we re-read the freshest runtime immediately before writing and mutate ONLY its history
|
|
233
|
+
// in place; every other field (messages/agents/tests) comes from this fresh read, not from
|
|
234
|
+
// the possibly-stale `runtime` captured earlier in this function.
|
|
235
|
+
const cur = readRuntime(projectRoot);
|
|
236
|
+
recordSnapshot(cur, { total, done }, today);
|
|
237
|
+
runtime = writeRuntime(projectRoot, cur);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { config, plans, specs, workflow, agents, skills, runtime };
|
|
241
|
+
}
|
|
242
|
+
function frontmatter(text) {
|
|
243
|
+
const m = String(text).replace(/\r\n?/g, '\n').match(/^---\n([\s\S]*?)\n---/);
|
|
244
|
+
const out = {};
|
|
245
|
+
if (m) m[1].split('\n').forEach((l) => { const kv = l.match(/^([\w-]+):\s*(.*)$/); if (kv) out[kv[1]] = kv[2].trim(); });
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
// Parse a flat inline-list front-matter value, e.g. "[analyze-requirements, write-spec]" →
|
|
249
|
+
// ['analyze-requirements', 'write-spec']. Returns [] for an absent/empty value.
|
|
250
|
+
function parseFlatList(raw) {
|
|
251
|
+
if (!raw) return [];
|
|
252
|
+
return String(raw).replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
253
|
+
}
|
|
254
|
+
function listMd(dir) {
|
|
255
|
+
if (!fs.existsSync(dir)) return [];
|
|
256
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => {
|
|
257
|
+
const fm = frontmatter(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
258
|
+
return { file: f, name: fm.name || f.replace(/\.md$/, ''), title: fm.title || fm.name || f, capability: fm.capability || '', description: fm.description || '',
|
|
259
|
+
standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses) };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function listSkills(dir) {
|
|
263
|
+
if (!fs.existsSync(dir)) return [];
|
|
264
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
265
|
+
const sk = path.join(dir, e.name, 'SKILL.md');
|
|
266
|
+
const fm = fs.existsSync(sk) ? frontmatter(fs.readFileSync(sk, 'utf8')) : {};
|
|
267
|
+
return { name: fm.name || e.name, description: fm.description || '', capability: fm.capability || '',
|
|
268
|
+
inputs: fm.inputs || '', outputs: fm.outputs || '', standard: fm.standard || '' };
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function readAgents(projectRoot) {
|
|
272
|
+
const dir = path.join(projectRoot, '.spectoflow', 'agents');
|
|
273
|
+
if (!fs.existsSync(dir)) return [];
|
|
274
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => {
|
|
275
|
+
const fm = frontmatter(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
276
|
+
return { name: fm.name || f.replace(/\.md$/, ''), capability: fm.capability || null,
|
|
277
|
+
title: fm.title || '', description: fm.description || '',
|
|
278
|
+
standards: parseFlatList(fm.standards), uses: parseFlatList(fm.uses) };
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function readSkills(projectRoot) {
|
|
282
|
+
return listSkills(path.join(projectRoot, '.spectoflow', 'skills'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
parseTaskLine, buildTaskLine, parsePlan, readPlans, updateTaskLine, addTaskComment,
|
|
287
|
+
readRuntime, writeRuntime, parseAgentLine, appendMessage, readConfig, readWorkflow, readProject,
|
|
288
|
+
readAgents, readSkills, recordSnapshot,
|
|
289
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Policy — non-negotiable gates
|
|
2
|
+
|
|
3
|
+
Orthogonal to mode. Even in autopilot, these require **explicit human approval** before execution.
|
|
4
|
+
|
|
5
|
+
- **Production deployment** (build/release/deploy to prod).
|
|
6
|
+
- **Destructive migration** (irreversible drop/alter, data deletion, purge).
|
|
7
|
+
- **Security change** (auth, permissions, secrets, network exposure, session lifetime).
|
|
8
|
+
- **Committing spend / external side effect** (payment, purchase, mass send).
|
|
9
|
+
|
|
10
|
+
When a step hits a gate: stop, explain the act and its risk in one line, ask [Approve / Cancel /
|
|
11
|
+
Modify], and record the decision in the runtime log. Overridable per project (add or relax gates).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: analyze-requirements
|
|
3
|
+
description: Turn a need into testable acceptance criteria in Given/When/Then, with edge cases enumerated.
|
|
4
|
+
capability: analysis
|
|
5
|
+
inputs: The raw need (a request, ticket, or user story) and any known constraints or existing spec.
|
|
6
|
+
outputs: A list of testable acceptance criteria (Given/When/Then) plus an edge-case checklist.
|
|
7
|
+
standard: BDD / acceptance criteria
|
|
8
|
+
---
|
|
9
|
+
# Analyze requirements
|
|
10
|
+
|
|
11
|
+
Turn an ambiguous need into a set of unambiguous, testable acceptance criteria before design or code
|
|
12
|
+
starts.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
Whenever a need arrives that isn't yet expressed as testable criteria — a new feature, a change
|
|
16
|
+
request, a bug that implies a missing behavior — or whenever the workflow reaches the Analysis step.
|
|
17
|
+
|
|
18
|
+
## Method
|
|
19
|
+
Apply Behavior-Driven Development's Given/When/Then structure (Cucumber/Gherkin) to state each
|
|
20
|
+
criterion as a concrete example, then stress it with a standard edge-case taxonomy
|
|
21
|
+
(ISTQB equivalence partitioning + boundary value analysis):
|
|
22
|
+
|
|
23
|
+
1. **Restate the need in one sentence.** If it takes more than one sentence, split it into multiple
|
|
24
|
+
needs — each gets its own criteria.
|
|
25
|
+
2. **Write acceptance criteria as Given/When/Then.** For each distinct behavior:
|
|
26
|
+
- `Given` the initial context/state (3-5 steps max per scenario — more and it stops reading as a
|
|
27
|
+
spec and becomes an implementation).
|
|
28
|
+
- `When` the triggering action.
|
|
29
|
+
- `Then` the expected, observable outcome (behavior/contract level — no implementation detail).
|
|
30
|
+
One criterion = one behavior. If it needs "and" to describe, it is probably two criteria.
|
|
31
|
+
3. **Enumerate edge cases per input**, using equivalence partitioning + boundary value analysis:
|
|
32
|
+
- Valid equivalence class(es) — one representative example, not every value in the class.
|
|
33
|
+
- Invalid equivalence class(es) — what must be rejected and how.
|
|
34
|
+
- Boundaries — min, max, min-1, max+1, empty, zero, null/missing.
|
|
35
|
+
- Error paths — what happens on failure (timeout, denial, malformed input), not just success.
|
|
36
|
+
4. **Flag gaps as `need`s, don't fill them in.** Any requirement gap that depends on a third party, a
|
|
37
|
+
business decision, or information you don't have is raised as a `need` per `policy.md` — never
|
|
38
|
+
guessed at to keep moving.
|
|
39
|
+
5. **Hand off** the criteria list to `write-spec` to be shaped into a reviewable spec document.
|
|
40
|
+
|
|
41
|
+
## Output contract
|
|
42
|
+
Acceptance criteria (Given/When/Then) and the edge-case checklist are recorded in `specs/<feature>.md`
|
|
43
|
+
(or the plan, if no spec exists yet), written with granular, one-line-at-a-time updates. Report progress
|
|
44
|
+
with the `::spectoflow` sentinel:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
::spectoflow role=analysis kind=progress msg=<N> acceptance criteria drafted for <feature>
|
|
48
|
+
::spectoflow role=analysis kind=need msg=<what is missing and who must resolve it>
|
|
49
|
+
::spectoflow role=analysis kind=report msg=<N> criteria, <M> edge cases enumerated, ready for write-spec
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quality bar
|
|
53
|
+
- [ ] Every acceptance criterion is a concrete Given/When/Then example, not a vague statement.
|
|
54
|
+
- [ ] Each criterion covers exactly one behavior (splittable by "and" is a sign it's really two).
|
|
55
|
+
- [ ] Edge cases cover valid + invalid equivalence classes, boundaries, and error paths — not just the
|
|
56
|
+
happy path.
|
|
57
|
+
- [ ] No criterion states implementation detail (class names, frameworks, algorithms).
|
|
58
|
+
- [ ] Every gap that isn't this role's to answer is raised as a `need`, not silently assumed.
|
|
59
|
+
|
|
60
|
+
## References
|
|
61
|
+
- Cucumber, "Gherkin Syntax" — https://cucumber.netlify.app/docs/gherkin/
|
|
62
|
+
- SmartBear, "Writing scenarios with Gherkin syntax" —
|
|
63
|
+
https://support.smartbear.com/cucumberstudio/docs/bdd/write-gherkin-scenarios.html
|
|
64
|
+
- ISTQB Foundation Level — Boundary Value Analysis white paper —
|
|
65
|
+
https://istqb.org/wp-content/uploads/2025/10/Boundary-Value-Analysis-white-paper.pdf
|
|
66
|
+
- SoftwareTestingHelp, "Boundary Value Analysis & Equivalence Partitioning Examples" —
|
|
67
|
+
https://www.softwaretestinghelp.com/what-is-boundary-value-analysis-and-equivalence-partitioning/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorm
|
|
3
|
+
description: Frame a need — problem, users, scope, risks, success metric — before committing to a spec.
|
|
4
|
+
capability: intake
|
|
5
|
+
inputs: The raw ask/request from the user or requester.
|
|
6
|
+
outputs: A framed brief (problem, users, scope, risks, success metric) ready for analysis.
|
|
7
|
+
standard: product discovery
|
|
8
|
+
---
|
|
9
|
+
# Brainstorm
|
|
10
|
+
|
|
11
|
+
Frame a raw need into an agreed problem statement before any solution gets designed.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
When a new ask arrives — a feature request, a bug report reframed as a need, or any item without an
|
|
15
|
+
agreed problem/scope yet — or whenever the workflow reaches an intake step.
|
|
16
|
+
|
|
17
|
+
## Method
|
|
18
|
+
Frame the need *before* reaching for solutions, in this order:
|
|
19
|
+
|
|
20
|
+
1. **Problem** — what user-facing or business problem is this, stated as an outcome, not a feature
|
|
21
|
+
("users can't X" not "add a button").
|
|
22
|
+
2. **Users** — who specifically is affected; which segment, not "everyone".
|
|
23
|
+
3. **Scope** — what's in scope for a first useful version.
|
|
24
|
+
4. **Out of scope** — what is explicitly excluded, so nobody assumes it's included later.
|
|
25
|
+
5. **Risks** — name the risk(s) most likely to kill or derail this: value (will users want it),
|
|
26
|
+
usability (can they use it), feasibility (can it be built with what we have), business viability
|
|
27
|
+
(does it fit constraints/compliance/cost) — per SVPG's four big risks.
|
|
28
|
+
6. **Success metric** — one metric that will tell us the outcome was achieved.
|
|
29
|
+
|
|
30
|
+
Offer 2-3 directions with trade-offs once the problem is framed; let the user react and converge on a
|
|
31
|
+
shared understanding. Do not write code or a full spec at this stage — that belongs to the next
|
|
32
|
+
capability.
|
|
33
|
+
|
|
34
|
+
## Output contract
|
|
35
|
+
Write the framed brief as a granular note/task comment (one line at a time): problem statement,
|
|
36
|
+
users, scope, out-of-scope, top risk(s), success metric. Report to the orchestrator and group chat
|
|
37
|
+
with:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
::spectoflow role=intake kind=brief msg=<one-line problem + scope summary>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The brief feeds the next analysis/spec-writing step; it is not itself a spec or an implementation
|
|
44
|
+
plan.
|
|
45
|
+
|
|
46
|
+
## Quality bar
|
|
47
|
+
- [ ] Problem is stated as an outcome/user pain, not pre-decided as a solution/feature.
|
|
48
|
+
- [ ] Users are named specifically, not "everyone" or left implicit.
|
|
49
|
+
- [ ] Scope and out-of-scope are both stated explicitly.
|
|
50
|
+
- [ ] At least one risk (value/usability/feasibility/business viability) is named.
|
|
51
|
+
- [ ] Exactly one success metric is stated and agreed with the requester.
|
|
52
|
+
- [ ] No code or full spec was written during this step.
|
|
53
|
+
|
|
54
|
+
## References
|
|
55
|
+
- Teresa Torres, *Continuous Discovery Habits* (Product Talk, 2021) —
|
|
56
|
+
https://www.producttalk.org/continuous-discovery-habits-book/
|
|
57
|
+
- Marty Cagan, "The Four Big Risks" — Silicon Valley Product Group —
|
|
58
|
+
https://www.svpg.com/four-big-risks/
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Review a deliverable against its requirements, findings graded by severity.
|
|
4
|
+
capability: quality
|
|
5
|
+
inputs: The deliverable under review and its acceptance criteria/spec.
|
|
6
|
+
outputs: A severity-graded findings report with a ready / rework verdict.
|
|
7
|
+
standard: Google code-review guide
|
|
8
|
+
---
|
|
9
|
+
# Code review
|
|
10
|
+
|
|
11
|
+
Scoped review of a deliverable against its requirements to catch defects before it is marked done.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
When a `plans/*.md` task or deliverable is reported complete and needs an independent check before
|
|
15
|
+
its status flips to done — or whenever the workflow reaches a quality step.
|
|
16
|
+
|
|
17
|
+
## Method
|
|
18
|
+
Read the deliverable and its acceptance criteria/spec first; review against them, not against
|
|
19
|
+
personal preference. Following Google's "How to do a code review", walk each category and read every
|
|
20
|
+
line the author expects reviewed:
|
|
21
|
+
|
|
22
|
+
1. **Correctness / Functionality** — does the code behave as the spec and the author intended; are
|
|
23
|
+
edge cases and error paths handled.
|
|
24
|
+
2. **Tests** — does the change have correct, well-designed automated tests covering the new behavior
|
|
25
|
+
(not just the happy path)?
|
|
26
|
+
3. **Readability / Naming / Comments** — clear names, comments that explain *why* not *what*, no
|
|
27
|
+
dead code or leftover debug output.
|
|
28
|
+
4. **Design / Complexity** — is the change well-designed for the system it lands in; could it be
|
|
29
|
+
simpler; would another developer understand and reuse it later?
|
|
30
|
+
5. **Security** — obvious injection, auth/authz, secrets, or input-validation issues on the touched
|
|
31
|
+
surface (defer a full pass to the `security-review` skill when the change is security-sensitive).
|
|
32
|
+
6. **Consistency / Documentation** — matches existing style/conventions; docs updated if behavior or
|
|
33
|
+
interface changed.
|
|
34
|
+
|
|
35
|
+
Grade each finding by severity: **Critical** (breaks correctness/security, blocks), **Important**
|
|
36
|
+
(real defect or gap, should block), **Minor** (worth fixing, not blocking), **Nit** (polish, author's
|
|
37
|
+
choice — the guide's own "Nit:" convention for non-blocking points). Favor approving once the change
|
|
38
|
+
demonstrably improves the codebase, even if imperfect; don't hold it to a standard of perfection.
|
|
39
|
+
|
|
40
|
+
## Output contract
|
|
41
|
+
Write findings as a report / task comment alongside the deliverable (granular, one line at a time),
|
|
42
|
+
each carrying: severity, file:line, and the issue. End with an explicit verdict: **ready** or
|
|
43
|
+
**rework**. Report to the orchestrator and group chat with:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
::spectoflow role=quality kind=review msg=<verdict + counts by severity>
|
|
47
|
+
::spectoflow role=quality kind=finding msg=<severity> <file:line> — <issue>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Do not modify the deliverable — report only. A discrepancy with the spec is raised as a `need`, not
|
|
51
|
+
silently patched.
|
|
52
|
+
|
|
53
|
+
## Quality bar
|
|
54
|
+
- [ ] Reviewed against the deliverable's stated acceptance criteria/spec, not personal taste.
|
|
55
|
+
- [ ] Every finding has a severity (Critical/Important/Minor/Nit) and a file:line.
|
|
56
|
+
- [ ] Tests checked for real coverage of new behavior, not just the presence of a test file.
|
|
57
|
+
- [ ] Design/complexity and naming/readability both considered, not just correctness.
|
|
58
|
+
- [ ] Obvious security issues on the touched surface flagged (or routed to security-review).
|
|
59
|
+
- [ ] A clear verdict (ready/rework) is stated; no open Critical/Important left unacknowledged.
|
|
60
|
+
|
|
61
|
+
## References
|
|
62
|
+
- Google Engineering Practices, "How to do a code review" —
|
|
63
|
+
https://google.github.io/eng-practices/review/reviewer/
|
|
64
|
+
- Google Engineering Practices, "What to look for in a code review" —
|
|
65
|
+
https://google.github.io/eng-practices/review/reviewer/looking-for.html
|
|
66
|
+
- Google Engineering Practices, "The Standard of Code Review" —
|
|
67
|
+
https://google.github.io/eng-practices/review/reviewer/standard.html
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implement
|
|
3
|
+
description: Implement a plan task as small, conventional commits — red-green when a test exists, boy-scout cleanup on touched code, no scope creep.
|
|
4
|
+
capability: implementation
|
|
5
|
+
inputs: A `plans/*.md` task (checkbox item) with its linked spec section, and any failing test the testing capability already wrote for it.
|
|
6
|
+
outputs: Working code for the task, committed as one or more Conventional Commits, plus the task's checkbox and status flipped in `plans/*.md`.
|
|
7
|
+
standard: Conventional Commits + YAGNI/DRY
|
|
8
|
+
---
|
|
9
|
+
# Implement
|
|
10
|
+
|
|
11
|
+
Turn one plan task into shipped code through small, well-described commits — nothing more than the
|
|
12
|
+
task asks for, nothing left messier than it was found.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
When the workflow reaches the `implementation` capability for a task in `plans/*.md` that is not yet
|
|
16
|
+
checked off, or when the group chat routes work to the developer persona.
|
|
17
|
+
|
|
18
|
+
## Method
|
|
19
|
+
1. **Re-read the task's contract first.** Open the linked spec section and the plan task's acceptance
|
|
20
|
+
criteria before writing any code. If a test already exists for this task (unit/integration/e2e),
|
|
21
|
+
run it — it should fail (**red**). If no test exists, note that in the report; do not silently skip
|
|
22
|
+
testing, escalate to the `testing` capability if the task needs one.
|
|
23
|
+
2. **Build the smallest change that satisfies the criteria (YAGNI).** You Aren't Gonna Need It: implement
|
|
24
|
+
only what the current task requires — no speculative config, no unused abstraction, no extra endpoint
|
|
25
|
+
"while we're in here". Extra scope is a separate task, not a freebie.
|
|
26
|
+
3. **Make it pass, then clean it up (green → refactor).** Get the existing/linked test green with the
|
|
27
|
+
simplest correct code, then refactor for clarity and to remove duplication (DRY — Don't Repeat
|
|
28
|
+
Yourself: extract only when a *third* real occurrence appears, not on the first hint of similarity).
|
|
29
|
+
4. **Apply the boy-scout rule to code you touch.** Leave the lines you had to open cleaner than you found
|
|
30
|
+
them (naming, dead code, obvious lint issues) — but do not refactor unrelated files or modules just
|
|
31
|
+
because you passed through the repo; that belongs to its own task.
|
|
32
|
+
5. **Commit in small, working, Conventional Commits.** Each commit:
|
|
33
|
+
- Builds and (if a test exists) passes on its own — no "WIP" or broken intermediate commits on the
|
|
34
|
+
shared branch (trunk-based hygiene: keep the branch always releasable).
|
|
35
|
+
- Follows the Conventional Commits grammar:
|
|
36
|
+
`<type>[optional scope]: <description>` header, optional body one blank line after, optional
|
|
37
|
+
footer(s) one blank line after that.
|
|
38
|
+
- Uses `feat:` for new capability, `fix:` for a bug fix, and other conventional types (`refactor:`,
|
|
39
|
+
`test:`, `docs:`, `chore:`, …) for everything else — pick the type that matches what the commit
|
|
40
|
+
actually does, not what the task was called.
|
|
41
|
+
- Marks a breaking change with `!` before the colon (e.g. `feat(api)!: ...`) or a `BREAKING CHANGE:`
|
|
42
|
+
footer — only when the task's contract says the change is breaking.
|
|
43
|
+
- Stays scoped to one logical change; split a task into several commits rather than bundling unrelated
|
|
44
|
+
edits into one.
|
|
45
|
+
6. **Flip the task's status as you go**, not in one batch at the end — granular, one line at a time, so
|
|
46
|
+
the dashboard and other agents see live progress rather than a silent gap.
|
|
47
|
+
|
|
48
|
+
## Output contract
|
|
49
|
+
- Code changes committed with Conventional Commits messages as described above.
|
|
50
|
+
- The corresponding checkbox/status line in `plans/*.md` updated via a granular, one-line write
|
|
51
|
+
(`- [ ]` → `- [x]`, or the task's status field) — never a full-file rewrite of the plan.
|
|
52
|
+
- Progress and completion reported to the orchestrator and group chat with:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
::spectoflow role=implementation kind=progress msg=<task id> <what changed, one line>
|
|
56
|
+
::spectoflow role=implementation kind=commit msg=<commit type>(<scope>): <description>
|
|
57
|
+
::spectoflow role=implementation kind=done msg=<task id> done — <tests status: red→green | no test>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quality bar
|
|
61
|
+
- [ ] Task's acceptance criteria (from the spec/plan) are met — nothing more, nothing less.
|
|
62
|
+
- [ ] If a test existed for this task, it went red → green; if none existed, that is stated explicitly.
|
|
63
|
+
- [ ] No speculative code, config, or abstraction beyond what the task requires (YAGNI).
|
|
64
|
+
- [ ] No duplicated logic left behind that a third occurrence should have collapsed (DRY).
|
|
65
|
+
- [ ] Every commit builds/passes standalone and follows `<type>[scope]: <description>` grammar.
|
|
66
|
+
- [ ] Breaking changes are marked with `!` or a `BREAKING CHANGE:` footer, and only when real.
|
|
67
|
+
- [ ] Code the task touched is left cleaner (boy-scout), with no drive-by edits outside the task's scope.
|
|
68
|
+
- [ ] Plan status updated via a granular write, not a full-file rewrite.
|
|
69
|
+
|
|
70
|
+
## References
|
|
71
|
+
- Conventional Commits v1.0.0 — https://www.conventionalcommits.org/en/v1.0.0/ (message grammar: type,
|
|
72
|
+
optional scope, description, body, footer; `feat`/`fix` as the baseline types; `!` and
|
|
73
|
+
`BREAKING CHANGE:` footer for breaking changes).
|
|
74
|
+
- Trunk-Based Development — https://trunkbaseddevelopment.com/ (small, short-lived changes committed
|
|
75
|
+
frequently to a shared branch that always stays releasable).
|
|
76
|
+
- Martin Fowler, "YAGNI" — https://martinfowler.com/bliki/Yagni.html (build capability only when it is
|
|
77
|
+
actually needed, not because it might be useful later).
|
|
78
|
+
- "Don't repeat yourself" — https://en.wikipedia.org/wiki/Don%27t_repeat_yourself (every piece of
|
|
79
|
+
knowledge should have a single, unambiguous representation; the classic "rule of three" for when to
|
|
80
|
+
extract).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-review
|
|
3
|
+
description: Review a change for secrets, authorization and attack surface against OWASP ASVS + Top 10.
|
|
4
|
+
capability: security
|
|
5
|
+
inputs: The change under review (diff/branch), the affected code, config, and dependency manifests.
|
|
6
|
+
outputs: A severity-ranked findings report with a Pass / Pass-with-follow-ups / Block verdict.
|
|
7
|
+
standard: OWASP ASVS + Top 10
|
|
8
|
+
---
|
|
9
|
+
# Security review
|
|
10
|
+
|
|
11
|
+
Scoped, standards-based review of a change to catch security defects before merge or deploy.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
When a change touches authentication, authorization, secrets, input handling, injection surfaces,
|
|
15
|
+
sensitive-data flows, outbound network calls, or dependencies — or whenever the workflow reaches a
|
|
16
|
+
security step or a `policy.md` security gate.
|
|
17
|
+
|
|
18
|
+
## Method
|
|
19
|
+
Scope to the diff and its trust boundaries (new inputs, new auth/authz checks, new secrets, new outbound
|
|
20
|
+
calls, new dependencies). Walk each area, mapping findings to OWASP Top 10 (2021) and ASVS 5.0:
|
|
21
|
+
|
|
22
|
+
1. **Authentication & session** (Top-10 A07; ASVS Authentication) — credential handling, MFA where
|
|
23
|
+
required, session lifetime/rotation/invalidation, no auth bypass introduced.
|
|
24
|
+
2. **Authorization / access control** (A01) — every new endpoint/action enforces least privilege and
|
|
25
|
+
object-level checks; no IDOR, no missing server-side authz, no privilege escalation.
|
|
26
|
+
3. **Secrets & cryptography** (A02) — no secrets in cleartext, source, logs, or fixtures; `.gitignore`
|
|
27
|
+
covers them and a `*.example` is provided; strong algorithms, no hard-coded keys, TLS in transit.
|
|
28
|
+
4. **Input validation & injection** (A03) — untrusted input is validated/encoded/parameterised; check
|
|
29
|
+
SQL/NoSQL/command/LDAP injection and XSS on every new sink; no string-built queries.
|
|
30
|
+
5. **Sensitive-data exposure & misconfiguration** (A02, A05) — PII minimised and protected, safe error
|
|
31
|
+
messages, secure defaults, no debug/verbose leakage, correct security headers/CORS.
|
|
32
|
+
6. **Vulnerable & outdated dependencies** (A06) — new/updated packages checked for known CVEs and
|
|
33
|
+
maintenance; pin and justify additions.
|
|
34
|
+
7. **Design, integrity & SSRF** (A04, A08, A10) — threat-model the change for insecure design, unsafe
|
|
35
|
+
deserialization / unsigned update or CI paths, and outbound requests reachable by user-controlled
|
|
36
|
+
URLs (SSRF).
|
|
37
|
+
8. **Logging & monitoring** (A09) — security-relevant events are logged without leaking secrets/PII.
|
|
38
|
+
|
|
39
|
+
Assign each finding a severity (Critical / High / Medium / Low / Info) calibrated to the project's target
|
|
40
|
+
ASVS level, and a concrete remediation.
|
|
41
|
+
|
|
42
|
+
## Output contract
|
|
43
|
+
Write findings as a report / task comments alongside the change (granular, one line at a time), each
|
|
44
|
+
finding carrying: severity, the Top-10 category, the ASVS requirement id where applicable, location
|
|
45
|
+
(file:line), and remediation. End with an explicit verdict: **Pass**, **Pass with follow-ups**, or
|
|
46
|
+
**Block**. Report to the orchestrator and group chat with:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
::spectoflow role=security kind=review msg=<verdict + counts by severity>
|
|
50
|
+
::spectoflow role=security kind=finding msg=<severity> <Top-10 cat> <file:line> — <issue>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A security-sensitive change routes to the `policy.md` human-approval gate; the skill never self-approves.
|
|
54
|
+
|
|
55
|
+
## Quality bar
|
|
56
|
+
- [ ] Every OWASP Top 10 (2021) category A01–A10 is explicitly marked **Considered** or **N/A**.
|
|
57
|
+
- [ ] Each finding has severity + Top-10 category + remediation (+ ASVS id where applicable).
|
|
58
|
+
- [ ] Secrets checked: none in cleartext/source/logs; gitignored; `*.example` present.
|
|
59
|
+
- [ ] Authz checked on every new endpoint/action (least privilege, object-level).
|
|
60
|
+
- [ ] Injection/XSS checked on every new sink; inputs validated/parameterised.
|
|
61
|
+
- [ ] New/updated dependencies screened for known vulnerabilities.
|
|
62
|
+
- [ ] A clear verdict is stated; no open Critical/High without a recorded human decision.
|
|
63
|
+
|
|
64
|
+
## References
|
|
65
|
+
- OWASP Top 10:2021 — https://owasp.org/Top10/2021/ (A01 Broken Access Control, A02 Cryptographic
|
|
66
|
+
Failures, A03 Injection, A04 Insecure Design, A05 Security Misconfiguration, A06 Vulnerable and
|
|
67
|
+
Outdated Components, A07 Identification and Authentication Failures, A08 Software and Data Integrity
|
|
68
|
+
Failures, A09 Security Logging and Monitoring Failures, A10 Server-Side Request Forgery).
|
|
69
|
+
- OWASP Application Security Verification Standard (ASVS) 5.0.0 (2025-05-30) —
|
|
70
|
+
https://owasp.org/www-project-application-security-verification-standard/
|