godpowers 2.1.1 → 2.2.1
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/CHANGELOG.md +54 -0
- package/README.md +14 -3
- package/RELEASE.md +36 -48
- package/SKILL.md +3 -3
- package/agents/god-architect.md +3 -0
- package/agents/god-executor.md +17 -0
- package/agents/god-greenfieldifier.md +10 -2
- package/agents/god-launch-strategist.md +3 -0
- package/agents/god-orchestrator.md +27 -1
- package/agents/god-planner.md +9 -1
- package/agents/god-pm.md +23 -1
- package/agents/god-quality-reviewer.md +8 -1
- package/agents/god-reconstructor.md +17 -0
- package/agents/god-roadmap-reconciler.md +7 -0
- package/agents/god-roadmap-updater.md +16 -1
- package/agents/god-roadmapper.md +18 -7
- package/agents/god-spec-reviewer.md +8 -0
- package/agents/god-storyteller.md +5 -0
- package/agents/god-updater.md +6 -0
- package/bin/install.js +1 -1
- package/hooks/session-start.sh +1 -0
- package/lib/dashboard.js +20 -0
- package/lib/feature-awareness.js +6 -0
- package/lib/requirements.js +556 -0
- package/lib/reverse-sync.js +28 -1
- package/lib/route-quality-sync.js +1 -0
- package/lib/state.js +5 -0
- package/package.json +2 -2
- package/routing/god-progress.yaml +27 -0
- package/routing/recipes/whats-done.yaml +28 -0
- package/schema/state.v1.json +25 -0
- package/skills/god-doctor.md +2 -2
- package/skills/god-locate.md +1 -0
- package/skills/god-progress.md +105 -0
- package/skills/god-status.md +11 -0
- package/skills/god-version.md +2 -2
- package/templates/PRD.md +12 -4
- package/templates/PROGRESS.md +2 -0
- package/templates/REQUIREMENTS.md +45 -0
- package/templates/ROADMAP.md +23 -5
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirements / Deliverable Tracking
|
|
3
|
+
*
|
|
4
|
+
* Answers the question "which requirements are done, in progress, or not
|
|
5
|
+
* started yet?" with a disk-derived, human-readable view. Nothing here is
|
|
6
|
+
* remembered: status is computed every time from
|
|
7
|
+
*
|
|
8
|
+
* .godpowers/prd/PRD.md declared requirements (P-MUST/SHOULD/COULD)
|
|
9
|
+
* .godpowers/roadmap/ROADMAP.md delivery increments and their member reqs
|
|
10
|
+
* .godpowers/links/ linkage forward map (requirement -> code)
|
|
11
|
+
* .godpowers/state.json build/increment completion
|
|
12
|
+
*
|
|
13
|
+
* Status rules (honest, evidence-based):
|
|
14
|
+
* untouched - no code is linked to the requirement yet
|
|
15
|
+
* in-progress - code is linked, but its increment (or the build) is not done
|
|
16
|
+
* done - code is linked AND its increment is done (or build complete)
|
|
17
|
+
*
|
|
18
|
+
* Public API:
|
|
19
|
+
* parsePrdRequirements(projectRoot) -> [{ id, priority, text, acceptance }]
|
|
20
|
+
* parseRoadmapIncrements(projectRoot) -> [{ id, name, horizon, requirements, declaredStatus }]
|
|
21
|
+
* derive(projectRoot, opts) -> { hasRequirements, requirements, increments, summary, gaps, updated }
|
|
22
|
+
* progressBar(done, total, width) -> "[####----] 4/10"
|
|
23
|
+
* renderLedger(derived) -> markdown for .godpowers/REQUIREMENTS.md
|
|
24
|
+
* renderProgressLines(derived) -> string[] compact block for the dashboard
|
|
25
|
+
* writeLedger(projectRoot, derived) -> writes the ledger, returns its path
|
|
26
|
+
* summarizeForState(derived) -> small object cached under state.deliverables
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const linkage = require('./linkage');
|
|
33
|
+
const state = require('./state');
|
|
34
|
+
|
|
35
|
+
const PRD_PATH = '.godpowers/prd/PRD.md';
|
|
36
|
+
const ROADMAP_PATH = '.godpowers/roadmap/ROADMAP.md';
|
|
37
|
+
const LEDGER_PATH = '.godpowers/REQUIREMENTS.md';
|
|
38
|
+
|
|
39
|
+
const PRIORITIES = ['MUST', 'SHOULD', 'COULD'];
|
|
40
|
+
const REQ_ID_RE = /\bP-(MUST|SHOULD|COULD)-(\d+)\b/;
|
|
41
|
+
const REQ_ID_RE_G = /\bP-(MUST|SHOULD|COULD)-\d+\b/g;
|
|
42
|
+
const MILESTONE_ID_RE = /\bM-[\w-]+\b/;
|
|
43
|
+
const LABEL_RE = /\[(?:DECISION|HYPOTHESIS|OPEN QUESTION)\]/g;
|
|
44
|
+
|
|
45
|
+
function readText(projectRoot, relPath) {
|
|
46
|
+
const file = path.join(projectRoot, relPath);
|
|
47
|
+
if (!fs.existsSync(file)) return null;
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(file, 'utf8');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pad2(n) {
|
|
56
|
+
return String(n).padStart(2, '0');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function slugify(text) {
|
|
60
|
+
return String(text)
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
63
|
+
.replace(/^-+|-+$/g, '')
|
|
64
|
+
.slice(0, 40) || 'increment';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// PRD parsing
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse declared requirements from the PRD. Each MUST/SHOULD/COULD bullet
|
|
73
|
+
* becomes a requirement. Explicit `P-MUST-01` ids are honored; bullets without
|
|
74
|
+
* an id are numbered by position within their priority so legacy PRDs still
|
|
75
|
+
* parse.
|
|
76
|
+
*/
|
|
77
|
+
function parsePrdRequirements(projectRoot) {
|
|
78
|
+
const content = readText(projectRoot, PRD_PATH);
|
|
79
|
+
if (!content) return [];
|
|
80
|
+
|
|
81
|
+
const lines = content.split(/\r?\n/);
|
|
82
|
+
const reqs = [];
|
|
83
|
+
const counters = { MUST: 0, SHOULD: 0, COULD: 0 };
|
|
84
|
+
let priority = null;
|
|
85
|
+
|
|
86
|
+
for (const raw of lines) {
|
|
87
|
+
const line = raw.replace(/\s+$/, '');
|
|
88
|
+
const heading = line.match(/^#{2,4}\s+(.*)$/);
|
|
89
|
+
if (heading) {
|
|
90
|
+
const title = heading[1].trim().toUpperCase();
|
|
91
|
+
const hit = PRIORITIES.find(p => title.startsWith(p));
|
|
92
|
+
priority = hit || null;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!priority) continue;
|
|
96
|
+
|
|
97
|
+
// Top-level list item only (requirement bullets are not indented).
|
|
98
|
+
const item = line.match(/^[-*]\s+(.*)$/);
|
|
99
|
+
if (!item) continue;
|
|
100
|
+
let body = item[1].trim();
|
|
101
|
+
if (!body) continue;
|
|
102
|
+
// Drop a leading checkbox if a ledger-style PRD ever feeds back in.
|
|
103
|
+
body = body.replace(/^\[[ xX~]\]\s*/, '');
|
|
104
|
+
|
|
105
|
+
const idMatch = body.match(REQ_ID_RE);
|
|
106
|
+
let id;
|
|
107
|
+
let prio;
|
|
108
|
+
if (idMatch) {
|
|
109
|
+
prio = idMatch[1];
|
|
110
|
+
id = idMatch[0];
|
|
111
|
+
counters[prio] = Math.max(counters[prio], Number(idMatch[2]));
|
|
112
|
+
} else {
|
|
113
|
+
prio = priority;
|
|
114
|
+
counters[prio] += 1;
|
|
115
|
+
id = `P-${prio}-${pad2(counters[prio])}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let text = body
|
|
119
|
+
.replace(REQ_ID_RE, '')
|
|
120
|
+
.replace(LABEL_RE, '')
|
|
121
|
+
.replace(/^[:\-\s]+/, '')
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
let acceptance = '';
|
|
125
|
+
const accSplit = text.split(/\s+--\s+(?:Acceptance|Validation):\s*/i);
|
|
126
|
+
if (accSplit.length > 1) {
|
|
127
|
+
text = accSplit[0].trim();
|
|
128
|
+
acceptance = accSplit.slice(1).join(' / ').trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
reqs.push({ id, priority: prio, text: text || '(unspecified)', acceptance });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// De-duplicate by id, keeping the first occurrence.
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
return reqs.filter(r => (seen.has(r.id) ? false : seen.add(r.id)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// ROADMAP parsing
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parse delivery increments and the requirements each one covers. Increments
|
|
145
|
+
* may carry an explicit `M-slug` id and a `Status:` field; both are optional.
|
|
146
|
+
*/
|
|
147
|
+
function parseRoadmapIncrements(projectRoot) {
|
|
148
|
+
const content = readText(projectRoot, ROADMAP_PATH);
|
|
149
|
+
if (!content) return [];
|
|
150
|
+
|
|
151
|
+
const lines = content.split(/\r?\n/);
|
|
152
|
+
const increments = [];
|
|
153
|
+
let horizon = null;
|
|
154
|
+
let current = null;
|
|
155
|
+
let inHaveNots = false;
|
|
156
|
+
|
|
157
|
+
const flush = () => {
|
|
158
|
+
if (current) increments.push(current);
|
|
159
|
+
current = null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const raw of lines) {
|
|
163
|
+
const line = raw.replace(/\s+$/, '');
|
|
164
|
+
|
|
165
|
+
const h2 = line.match(/^##\s+(.*)$/);
|
|
166
|
+
if (h2) {
|
|
167
|
+
flush();
|
|
168
|
+
const title = h2[1].trim().toLowerCase();
|
|
169
|
+
inHaveNots = title.includes('have-not');
|
|
170
|
+
if (title.startsWith('now')) horizon = 'now';
|
|
171
|
+
else if (title.startsWith('next')) horizon = 'next';
|
|
172
|
+
else if (title.startsWith('later')) horizon = 'later';
|
|
173
|
+
else horizon = null;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const h3 = line.match(/^###\s+(.*)$/);
|
|
178
|
+
if (h3 && horizon && !inHaveNots) {
|
|
179
|
+
flush();
|
|
180
|
+
let name = h3[1].trim();
|
|
181
|
+
name = name.replace(/^Delivery Increment\s*\d+\s*:\s*/i, '');
|
|
182
|
+
name = name.replace(/^Theme\s*:\s*/i, '');
|
|
183
|
+
const idMatch = h3[1].match(MILESTONE_ID_RE);
|
|
184
|
+
current = {
|
|
185
|
+
id: idMatch ? idMatch[0] : null,
|
|
186
|
+
name: name || h3[1].trim(),
|
|
187
|
+
horizon,
|
|
188
|
+
requirements: [],
|
|
189
|
+
declaredStatus: null
|
|
190
|
+
};
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!current) continue;
|
|
195
|
+
|
|
196
|
+
const idLine = line.match(/^\s*[-*]\s*\*\*ID\*\*\s*:\s*(.+)$/i);
|
|
197
|
+
if (idLine) {
|
|
198
|
+
const m = idLine[1].match(MILESTONE_ID_RE);
|
|
199
|
+
if (m) current.id = m[0];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const statusLine = line.match(/^\s*[-*]\s*\*\*Status\*\*\s*:\s*(.+)$/i);
|
|
204
|
+
if (statusLine) {
|
|
205
|
+
current.declaredStatus = normalizeStatus(statusLine[1].trim());
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let m;
|
|
210
|
+
while ((m = REQ_ID_RE_G.exec(line)) !== null) {
|
|
211
|
+
if (!current.requirements.includes(m[0])) current.requirements.push(m[0]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
flush();
|
|
215
|
+
|
|
216
|
+
for (const inc of increments) {
|
|
217
|
+
if (!inc.id) inc.id = `M-${slugify(inc.name)}`;
|
|
218
|
+
}
|
|
219
|
+
return increments;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeStatus(value) {
|
|
223
|
+
const v = String(value || '').trim().toLowerCase();
|
|
224
|
+
if (!v) return null;
|
|
225
|
+
if (['done', 'complete', 'completed', 'shipped', 'verified'].some(s => v.startsWith(s))) return 'done';
|
|
226
|
+
if (['building', 'in-progress', 'in progress', 'active', 'wip'].some(s => v.startsWith(s))) return 'building';
|
|
227
|
+
if (['pending', 'not started', 'not-started', 'todo', 'planned', 'queued'].some(s => v.startsWith(s))) return 'pending';
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Status derivation
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
function buildComplete(projectRoot) {
|
|
236
|
+
const s = state.read(projectRoot);
|
|
237
|
+
const build = s && s.tiers && s.tiers['tier-2'] && s.tiers['tier-2'].build;
|
|
238
|
+
return Boolean(build && state.isCompleteStatus(build.status));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compute the full deliverable picture from disk.
|
|
243
|
+
*/
|
|
244
|
+
function derive(projectRoot, opts = {}) {
|
|
245
|
+
const requirements = parsePrdRequirements(projectRoot);
|
|
246
|
+
const increments = parseRoadmapIncrements(projectRoot);
|
|
247
|
+
const forward = linkage.readForward(projectRoot);
|
|
248
|
+
const isBuilt = Object.prototype.hasOwnProperty.call(opts, 'buildComplete')
|
|
249
|
+
? Boolean(opts.buildComplete)
|
|
250
|
+
: buildComplete(projectRoot);
|
|
251
|
+
|
|
252
|
+
const isLinked = id => Array.isArray(forward[id]) && forward[id].length > 0;
|
|
253
|
+
const reqById = new Map(requirements.map(r => [r.id, r]));
|
|
254
|
+
|
|
255
|
+
// Requirement -> increment (first increment that lists it wins).
|
|
256
|
+
const incForReq = new Map();
|
|
257
|
+
for (const inc of increments) {
|
|
258
|
+
for (const id of inc.requirements) {
|
|
259
|
+
if (!incForReq.has(id)) incForReq.set(id, inc);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Derive increment status from member-requirement linkage + declared status.
|
|
264
|
+
for (const inc of increments) {
|
|
265
|
+
const members = inc.requirements.filter(id => reqById.has(id));
|
|
266
|
+
const mustMembers = members.filter(id => reqById.get(id).priority === 'MUST');
|
|
267
|
+
const anyLinked = members.some(isLinked);
|
|
268
|
+
const gateMembers = mustMembers.length > 0 ? mustMembers : members;
|
|
269
|
+
const gateLinked = gateMembers.length > 0 && gateMembers.every(isLinked);
|
|
270
|
+
|
|
271
|
+
let status;
|
|
272
|
+
if (inc.declaredStatus === 'done') status = 'done';
|
|
273
|
+
else if (gateLinked && isBuilt) status = 'done';
|
|
274
|
+
else if (anyLinked || inc.declaredStatus === 'building') status = 'building';
|
|
275
|
+
else status = inc.declaredStatus || 'pending';
|
|
276
|
+
|
|
277
|
+
inc.status = status;
|
|
278
|
+
inc.memberIds = members;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Derive each requirement's status.
|
|
282
|
+
const enriched = requirements.map(r => {
|
|
283
|
+
const inc = incForReq.get(r.id) || null;
|
|
284
|
+
const linked = isLinked(r.id);
|
|
285
|
+
const files = linked ? forward[r.id].slice() : [];
|
|
286
|
+
let status;
|
|
287
|
+
if (!linked) status = 'untouched';
|
|
288
|
+
else if (inc ? inc.status === 'done' : isBuilt) status = 'done';
|
|
289
|
+
else status = 'in-progress';
|
|
290
|
+
const gap = Boolean(inc && inc.status === 'done' && !linked);
|
|
291
|
+
return {
|
|
292
|
+
id: r.id,
|
|
293
|
+
priority: r.priority,
|
|
294
|
+
text: r.text,
|
|
295
|
+
acceptance: r.acceptance,
|
|
296
|
+
status,
|
|
297
|
+
gap,
|
|
298
|
+
files,
|
|
299
|
+
increment: inc ? inc.id : null,
|
|
300
|
+
incrementName: inc ? inc.name : null
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Increment done-counts based on final requirement statuses.
|
|
305
|
+
const statusById = new Map(enriched.map(r => [r.id, r.status]));
|
|
306
|
+
for (const inc of increments) {
|
|
307
|
+
inc.doneCount = inc.memberIds.filter(id => statusById.get(id) === 'done').length;
|
|
308
|
+
inc.totalCount = inc.memberIds.length;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const summary = summarize(enriched, increments);
|
|
312
|
+
const gaps = enriched.filter(r => r.gap);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
hasRequirements: enriched.length > 0,
|
|
316
|
+
requirements: enriched,
|
|
317
|
+
increments,
|
|
318
|
+
summary,
|
|
319
|
+
gaps,
|
|
320
|
+
updated: new Date().toISOString()
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function summarize(requirements, increments) {
|
|
325
|
+
const byPriority = {};
|
|
326
|
+
for (const p of PRIORITIES) {
|
|
327
|
+
byPriority[p] = { done: 0, inProgress: 0, untouched: 0, total: 0 };
|
|
328
|
+
}
|
|
329
|
+
let done = 0;
|
|
330
|
+
let inProgress = 0;
|
|
331
|
+
let untouched = 0;
|
|
332
|
+
for (const r of requirements) {
|
|
333
|
+
const bucket = byPriority[r.priority] || (byPriority[r.priority] = { done: 0, inProgress: 0, untouched: 0, total: 0 });
|
|
334
|
+
bucket.total += 1;
|
|
335
|
+
if (r.status === 'done') { done += 1; bucket.done += 1; }
|
|
336
|
+
else if (r.status === 'in-progress') { inProgress += 1; bucket.inProgress += 1; }
|
|
337
|
+
else { untouched += 1; bucket.untouched += 1; }
|
|
338
|
+
}
|
|
339
|
+
const total = requirements.length;
|
|
340
|
+
const incStatus = { done: 0, building: 0, pending: 0 };
|
|
341
|
+
for (const inc of increments) incStatus[inc.status] = (incStatus[inc.status] || 0) + 1;
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
total,
|
|
345
|
+
done,
|
|
346
|
+
inProgress,
|
|
347
|
+
untouched,
|
|
348
|
+
percent: total === 0 ? 0 : Math.round((done / total) * 100),
|
|
349
|
+
byPriority,
|
|
350
|
+
increments: { total: increments.length, ...incStatus }
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Rendering
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
function progressBar(done, total, width = 20) {
|
|
359
|
+
if (!total || total <= 0) return `[${'-'.repeat(width)}] 0/0`;
|
|
360
|
+
const filled = Math.max(0, Math.min(width, Math.round((done / total) * width)));
|
|
361
|
+
return `[${'#'.repeat(filled)}${'-'.repeat(width - filled)}] ${done}/${total}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const MARK = { done: '[x]', 'in-progress': '[~]', untouched: '[ ]' };
|
|
365
|
+
const INC_MARK = { done: '[x]', building: '[~]', pending: '[ ]' };
|
|
366
|
+
const UPDATED_LINE_RE = /^Updated: .+$/m;
|
|
367
|
+
|
|
368
|
+
function finishLedger(lines) {
|
|
369
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
370
|
+
return lines.join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function normalizeLedgerTimestamp(content) {
|
|
374
|
+
return String(content).replace(UPDATED_LINE_RE, 'Updated: <timestamp>');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function withoutUpdated(value) {
|
|
378
|
+
if (!value || typeof value !== 'object') return value;
|
|
379
|
+
const copy = JSON.parse(JSON.stringify(value));
|
|
380
|
+
delete copy.updated;
|
|
381
|
+
return copy;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function sameIgnoringUpdated(a, b) {
|
|
385
|
+
return JSON.stringify(withoutUpdated(a)) === JSON.stringify(withoutUpdated(b));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Compact lines for the dashboard "Deliverable progress" section.
|
|
390
|
+
*/
|
|
391
|
+
function renderProgressLines(derived) {
|
|
392
|
+
if (!derived || !derived.hasRequirements) {
|
|
393
|
+
return [' Requirements: none declared yet (no PRD requirements found)'];
|
|
394
|
+
}
|
|
395
|
+
const s = derived.summary;
|
|
396
|
+
const byPriority = PRIORITIES
|
|
397
|
+
.filter(p => s.byPriority[p] && s.byPriority[p].total > 0)
|
|
398
|
+
.map(p => `${p} ${s.byPriority[p].done}/${s.byPriority[p].total}`)
|
|
399
|
+
.join(', ');
|
|
400
|
+
const lines = [
|
|
401
|
+
` Requirements: ${progressBar(s.done, s.total)} done (${s.percent}%), ${s.inProgress} in progress, ${s.untouched} not started`
|
|
402
|
+
];
|
|
403
|
+
if (byPriority) lines.push(` By priority: ${byPriority}`);
|
|
404
|
+
if (s.increments.total > 0) {
|
|
405
|
+
lines.push(` Increments: ${s.increments.done} done, ${s.increments.building} building, ${s.increments.pending} pending`);
|
|
406
|
+
}
|
|
407
|
+
if (derived.gaps.length > 0) {
|
|
408
|
+
lines.push(` Gaps: ${derived.gaps.length} requirement(s) in a done increment with no linked code`);
|
|
409
|
+
}
|
|
410
|
+
lines.push(` Ledger: ${LEDGER_PATH}`);
|
|
411
|
+
return lines;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Full human-readable ledger written to .godpowers/REQUIREMENTS.md.
|
|
416
|
+
*/
|
|
417
|
+
function renderLedger(derived) {
|
|
418
|
+
const s = derived.summary;
|
|
419
|
+
const out = [];
|
|
420
|
+
out.push('# Requirements Ledger');
|
|
421
|
+
out.push('');
|
|
422
|
+
out.push('> Disk-derived. Status comes from the linkage map (code that implements');
|
|
423
|
+
out.push('> each requirement) plus build and roadmap-increment state. Regenerate with');
|
|
424
|
+
out.push('> `/god-progress`, `/god-status`, or `/god-sync`. Do not hand-edit statuses;');
|
|
425
|
+
out.push('> they are recomputed from disk.');
|
|
426
|
+
out.push('');
|
|
427
|
+
out.push(`Updated: ${derived.updated}`);
|
|
428
|
+
out.push('Source: PRD + ROADMAP + linkage forward map + build state');
|
|
429
|
+
|
|
430
|
+
if (!derived.hasRequirements) {
|
|
431
|
+
out.push('');
|
|
432
|
+
out.push('No requirements are declared yet. Once the PRD lists MUST/SHOULD/COULD');
|
|
433
|
+
out.push('requirements with stable ids (P-MUST-01, ...), they appear here.');
|
|
434
|
+
out.push('');
|
|
435
|
+
return finishLedger(out);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
out.push(`Progress: ${progressBar(s.done, s.total)} done (${s.percent}%) | ${s.inProgress} in progress | ${s.untouched} not started`);
|
|
439
|
+
out.push('');
|
|
440
|
+
out.push('## By priority');
|
|
441
|
+
out.push('');
|
|
442
|
+
out.push('| Priority | Done | In progress | Not started | Total |');
|
|
443
|
+
out.push('|----------|------|-------------|-------------|-------|');
|
|
444
|
+
for (const p of PRIORITIES) {
|
|
445
|
+
const b = s.byPriority[p];
|
|
446
|
+
if (!b || b.total === 0) continue;
|
|
447
|
+
out.push(`| ${p} | ${b.done} | ${b.inProgress} | ${b.untouched} | ${b.total} |`);
|
|
448
|
+
}
|
|
449
|
+
out.push('');
|
|
450
|
+
|
|
451
|
+
const groups = [
|
|
452
|
+
['Done', 'done'],
|
|
453
|
+
['In progress', 'in-progress'],
|
|
454
|
+
['Not started', 'untouched']
|
|
455
|
+
];
|
|
456
|
+
for (const [label, key] of groups) {
|
|
457
|
+
const items = derived.requirements.filter(r => r.status === key);
|
|
458
|
+
out.push(`## ${label} (${items.length})`);
|
|
459
|
+
out.push('');
|
|
460
|
+
if (items.length === 0) {
|
|
461
|
+
out.push('- (none)');
|
|
462
|
+
out.push('');
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
for (const r of items) {
|
|
466
|
+
const inc = r.increment ? ` _(increment: ${r.increment})_` : '';
|
|
467
|
+
const files = r.files.length > 0 ? ` - ${r.files.slice(0, 4).join(', ')}${r.files.length > 4 ? `, +${r.files.length - 4} more` : ''}` : '';
|
|
468
|
+
out.push(`- ${MARK[r.status]} **${r.id}** ${r.text}${inc}${files}`);
|
|
469
|
+
}
|
|
470
|
+
out.push('');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (derived.increments.length > 0) {
|
|
474
|
+
out.push('## Increments');
|
|
475
|
+
out.push('');
|
|
476
|
+
for (const inc of derived.increments) {
|
|
477
|
+
out.push(`- ${INC_MARK[inc.status]} **${inc.id}**: ${inc.name} _[${inc.horizon}]_ - ${inc.status} - ${inc.doneCount}/${inc.totalCount} requirements done`);
|
|
478
|
+
}
|
|
479
|
+
out.push('');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (derived.gaps.length > 0) {
|
|
483
|
+
out.push('## Gaps');
|
|
484
|
+
out.push('');
|
|
485
|
+
out.push('Requirements whose increment is marked done but have no implementing code linked:');
|
|
486
|
+
out.push('');
|
|
487
|
+
for (const r of derived.gaps) {
|
|
488
|
+
out.push(`- **${r.id}** ${r.text} _(increment: ${r.increment})_`);
|
|
489
|
+
}
|
|
490
|
+
out.push('');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return finishLedger(out);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function writeLedger(projectRoot, derived) {
|
|
497
|
+
const data = derived || derive(projectRoot);
|
|
498
|
+
const file = path.join(projectRoot, LEDGER_PATH);
|
|
499
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
500
|
+
const rendered = renderLedger(data) + '\n';
|
|
501
|
+
if (fs.existsSync(file)) {
|
|
502
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
503
|
+
if (normalizeLedgerTimestamp(current) === normalizeLedgerTimestamp(rendered)) {
|
|
504
|
+
return LEDGER_PATH;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
fs.writeFileSync(file, rendered);
|
|
508
|
+
return LEDGER_PATH;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Small cacheable summary for state.json (state.deliverables).
|
|
513
|
+
*/
|
|
514
|
+
function summarizeForState(derived, currentSummary = null) {
|
|
515
|
+
const s = derived.summary;
|
|
516
|
+
const next = {
|
|
517
|
+
updated: derived.updated,
|
|
518
|
+
source: 'PRD + ROADMAP + linkage + build state',
|
|
519
|
+
requirements: {
|
|
520
|
+
total: s.total,
|
|
521
|
+
done: s.done,
|
|
522
|
+
'in-progress': s.inProgress,
|
|
523
|
+
untouched: s.untouched,
|
|
524
|
+
percent: s.percent
|
|
525
|
+
},
|
|
526
|
+
increments: derived.increments.map(inc => ({
|
|
527
|
+
id: inc.id,
|
|
528
|
+
name: inc.name,
|
|
529
|
+
horizon: inc.horizon,
|
|
530
|
+
status: inc.status,
|
|
531
|
+
done: inc.doneCount,
|
|
532
|
+
total: inc.totalCount
|
|
533
|
+
})),
|
|
534
|
+
gaps: derived.gaps.length
|
|
535
|
+
};
|
|
536
|
+
if (currentSummary && currentSummary.updated && sameIgnoringUpdated(currentSummary, next)) {
|
|
537
|
+
next.updated = currentSummary.updated;
|
|
538
|
+
}
|
|
539
|
+
return next;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
module.exports = {
|
|
543
|
+
PRD_PATH,
|
|
544
|
+
ROADMAP_PATH,
|
|
545
|
+
LEDGER_PATH,
|
|
546
|
+
parsePrdRequirements,
|
|
547
|
+
parseRoadmapIncrements,
|
|
548
|
+
derive,
|
|
549
|
+
progressBar,
|
|
550
|
+
renderProgressLines,
|
|
551
|
+
renderLedger,
|
|
552
|
+
writeLedger,
|
|
553
|
+
summarizeForState,
|
|
554
|
+
normalizeStatus,
|
|
555
|
+
slugify
|
|
556
|
+
};
|
package/lib/reverse-sync.js
CHANGED
|
@@ -26,6 +26,8 @@ const drift = require('./drift-detector');
|
|
|
26
26
|
const reviewRequired = require('./review-required');
|
|
27
27
|
const impeccable = require('./impeccable-bridge');
|
|
28
28
|
const sourceSync = require('./source-sync');
|
|
29
|
+
const requirements = require('./requirements');
|
|
30
|
+
const state = require('./state');
|
|
29
31
|
|
|
30
32
|
const FENCE_BEGIN = '<!-- godpowers:linkage:begin -->';
|
|
31
33
|
const FENCE_END = '<!-- godpowers:linkage:end -->';
|
|
@@ -311,6 +313,30 @@ function run(projectRoot, opts = {}) {
|
|
|
311
313
|
});
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
// Refresh the deliverable ledger now that the linkage map is current. Only
|
|
317
|
+
// write the file when the PRD actually declares requirements, to avoid
|
|
318
|
+
// littering pre-PRD projects with an empty ledger.
|
|
319
|
+
let requirementsSummary = null;
|
|
320
|
+
if (opts.runRequirements !== false) {
|
|
321
|
+
try {
|
|
322
|
+
const derived = requirements.derive(projectRoot);
|
|
323
|
+
if (derived.hasRequirements) {
|
|
324
|
+
requirements.writeLedger(projectRoot, derived);
|
|
325
|
+
const currentState = state.read(projectRoot);
|
|
326
|
+
requirementsSummary = requirements.summarizeForState(
|
|
327
|
+
derived,
|
|
328
|
+
currentState && currentState.deliverables
|
|
329
|
+
);
|
|
330
|
+
if (currentState) {
|
|
331
|
+
currentState.deliverables = requirementsSummary;
|
|
332
|
+
state.write(projectRoot, currentState);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
requirementsSummary = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
314
340
|
return {
|
|
315
341
|
scanResult,
|
|
316
342
|
applyResult,
|
|
@@ -318,7 +344,8 @@ function run(projectRoot, opts = {}) {
|
|
|
318
344
|
impeccableFindings,
|
|
319
345
|
footers,
|
|
320
346
|
sourceSyncResult,
|
|
321
|
-
reviewItems
|
|
347
|
+
reviewItems,
|
|
348
|
+
requirements: requirementsSummary
|
|
322
349
|
};
|
|
323
350
|
}
|
|
324
351
|
|
package/lib/state.js
CHANGED
|
@@ -191,6 +191,11 @@ function createInitialState(projectName, opts = {}) {
|
|
|
191
191
|
'drift-count': 0,
|
|
192
192
|
'review-required-items': 0
|
|
193
193
|
},
|
|
194
|
+
deliverables: {
|
|
195
|
+
requirements: { total: 0, done: 0, 'in-progress': 0, untouched: 0, percent: 0 },
|
|
196
|
+
increments: [],
|
|
197
|
+
gaps: 0
|
|
198
|
+
},
|
|
194
199
|
'yolo-decisions': [],
|
|
195
200
|
...opts
|
|
196
201
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "godpowers",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI-powered development system:
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "AI-powered development system: 111 slash commands and 40 specialist agents that take a project from raw idea to hardened production. Runs inside Claude Code, Codex, Cursor, Windsurf, Gemini, and 10+ other AI coding tools.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"godpowers": "./bin/install.js"
|
|
7
7
|
},
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
apiVersion: godpowers/v1
|
|
2
|
+
kind: CommandRouting
|
|
3
|
+
metadata:
|
|
4
|
+
command: /god-progress
|
|
5
|
+
description: Deliverable progress at the requirement and increment level
|
|
6
|
+
tier: 0
|
|
7
|
+
|
|
8
|
+
prerequisites:
|
|
9
|
+
required:
|
|
10
|
+
[]
|
|
11
|
+
|
|
12
|
+
execution:
|
|
13
|
+
spawns: [built-in]
|
|
14
|
+
context: fresh
|
|
15
|
+
writes:
|
|
16
|
+
- .godpowers/REQUIREMENTS.md
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
success-path:
|
|
20
|
+
next-recommended: /god-next
|
|
21
|
+
|
|
22
|
+
failure-path:
|
|
23
|
+
on-error: /god-doctor
|
|
24
|
+
|
|
25
|
+
endoff:
|
|
26
|
+
state-update: tier-0 updated for /god-progress
|
|
27
|
+
events: [agent.start, artifact.created, agent.end]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
apiVersion: godpowers/v1
|
|
2
|
+
kind: Recipe
|
|
3
|
+
metadata:
|
|
4
|
+
name: whats-done
|
|
5
|
+
category: meta
|
|
6
|
+
description: "See how much of the product is built (requirements and increments)"
|
|
7
|
+
|
|
8
|
+
triggers:
|
|
9
|
+
intent-keywords:
|
|
10
|
+
- "how far along"
|
|
11
|
+
- "how far along are we"
|
|
12
|
+
- "whats done"
|
|
13
|
+
- "what is done"
|
|
14
|
+
- "whats left"
|
|
15
|
+
- "what is left"
|
|
16
|
+
- "requirements status"
|
|
17
|
+
- "deliverable progress"
|
|
18
|
+
- "show the checklist"
|
|
19
|
+
- "how much is built"
|
|
20
|
+
|
|
21
|
+
sequences:
|
|
22
|
+
default:
|
|
23
|
+
description: "See which requirements and increments are done, in progress, or not started"
|
|
24
|
+
steps:
|
|
25
|
+
- command: "/god-progress"
|
|
26
|
+
why: "Derives requirement and increment status from disk and refreshes .godpowers/REQUIREMENTS.md"
|
|
27
|
+
|
|
28
|
+
default-sequence: default
|