godpowers 3.0.2 → 3.11.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/lib/gate.js CHANGED
@@ -12,6 +12,7 @@ const artifactMap = require('./artifact-map');
12
12
  const linter = require('./artifact-linter');
13
13
  const router = require('./router');
14
14
  const stateStore = require('./state');
15
+ const evidence = require('./evidence');
15
16
 
16
17
  function relToAbs(projectRoot, relPath) {
17
18
  return path.join(projectRoot, relPath);
@@ -267,40 +268,47 @@ function checkStateStepEvidence(projectRoot, tier, result) {
267
268
  return subStep;
268
269
  }
269
270
 
270
- function checkBuildEvidence(result, buildStep) {
271
+ // Executed-evidence requirement for executable-gated tiers. Generalized from
272
+ // the original build-only check: a substep whose key is in
273
+ // evidence.EXECUTED_REQUIRED_SUBSTEPS must record at least one passed
274
+ // verification command and zero failed ones. Finding ids and summary keys are
275
+ // tier-prefixed so the build tier keeps its existing `build-verification-*`
276
+ // contract while harden gains `harden-verification-*`.
277
+ function checkExecutedEvidence(result, step, tier) {
271
278
  const relPath = '.godpowers/state.json';
272
- if (!buildStep) return;
273
- const failedCommands = commandsWithStatus(buildStep, 'fail');
279
+ if (!step) return;
280
+ const label = tier.charAt(0).toUpperCase() + tier.slice(1);
281
+ const failedCommands = commandsWithStatus(step, 'fail');
274
282
  if (failedCommands.length > 0) {
275
283
  const finding = makeFinding(
276
- 'build-verification-failed-command',
284
+ `${tier}-verification-failed-command`,
277
285
  'error',
278
286
  relPath,
279
- `Build state records failed verification command(s): ${failedCommands.join(', ')}.`
287
+ `${label} state records failed verification command(s): ${failedCommands.join(', ')}.`
280
288
  );
281
289
  result.findings.push(finding);
282
290
  addFindingSummary(result.summary, finding.severity);
283
291
  result.checks.push(makeCheck(
284
- 'build-verification-failed-command',
292
+ `${tier}-verification-failed-command`,
285
293
  'fail',
286
294
  relPath,
287
295
  finding.reason
288
296
  ));
289
- result.summary.buildVerificationFailedCommands = failedCommands;
297
+ result.summary[`${tier}VerificationFailedCommands`] = failedCommands;
290
298
  return;
291
299
  }
292
- const passedCommands = commandsWithStatus(buildStep, 'pass');
300
+ const passedCommands = commandsWithStatus(step, 'pass');
293
301
  if (passedCommands.length === 0) {
294
302
  const finding = makeFinding(
295
- 'build-verification-evidence',
303
+ `${tier}-verification-evidence`,
296
304
  'error',
297
305
  relPath,
298
- 'Build state does not record exact project verification commands that passed.'
306
+ `${label} state does not record exact project verification commands that passed.`
299
307
  );
300
308
  result.findings.push(finding);
301
309
  addFindingSummary(result.summary, finding.severity);
302
310
  result.checks.push(makeCheck(
303
- 'build-verification-evidence',
311
+ `${tier}-verification-evidence`,
304
312
  'fail',
305
313
  relPath,
306
314
  finding.reason
@@ -308,12 +316,12 @@ function checkBuildEvidence(result, buildStep) {
308
316
  return;
309
317
  }
310
318
  result.checks.push(makeCheck(
311
- 'build-verification-evidence',
319
+ `${tier}-verification-evidence`,
312
320
  'pass',
313
321
  relPath,
314
- `state.json records ${passedCommands.length} passed build verification command(s).`
322
+ `state.json records ${passedCommands.length} passed ${tier} verification command(s).`
315
323
  ));
316
- result.summary.buildVerificationCommands = passedCommands;
324
+ result.summary[`${tier}VerificationCommands`] = passedCommands;
317
325
  }
318
326
 
319
327
  function checkHardenCriticals(projectRoot, result) {
@@ -378,7 +386,10 @@ function check(opts = {}) {
378
386
 
379
387
  checkArtifacts(projectRoot, tier, artifacts, opts, result);
380
388
  const stateStep = checkStateStepEvidence(projectRoot, tier, result);
381
- if (tier === 'build') checkBuildEvidence(result, stateStep);
389
+ const stepRef = artifactMap.stateStepForTier(tier);
390
+ if (stepRef && evidence.EXECUTED_REQUIRED_SUBSTEPS.has(stepRef.subStepKey)) {
391
+ checkExecutedEvidence(result, stateStep, tier);
392
+ }
382
393
  if (tier === 'harden') checkHardenCriticals(projectRoot, result);
383
394
  return finalize(result);
384
395
  }
@@ -13,7 +13,16 @@ const COMMANDS = new Set([
13
13
  'extension-scaffold',
14
14
  'surface',
15
15
  'demo',
16
- 'gate'
16
+ 'gate',
17
+ 'verify',
18
+ 'can-close',
19
+ 'route',
20
+ 'report',
21
+ 'reflect',
22
+ 'memory',
23
+ 'lesson',
24
+ 'outcome',
25
+ 'import-ledger'
17
26
  ]);
18
27
 
19
28
  function parseArgs(argv, cwd = process.cwd()) {
@@ -33,6 +42,36 @@ function parseArgs(argv, cwd = process.cwd()) {
33
42
  extensionAgent: null,
34
43
  extensionWorkflow: null,
35
44
  tier: null,
45
+ verifyCommand: null,
46
+ routePrompt: null,
47
+ substep: null,
48
+ claim: null,
49
+ timeout: null,
50
+ attest: false,
51
+ evidence: null,
52
+ since: null,
53
+ peek: false,
54
+ reflectAction: null,
55
+ outcome: null,
56
+ observation: null,
57
+ rootCause: null,
58
+ nextAction: null,
59
+ lesson: null,
60
+ memoryAction: null,
61
+ memoryKey: null,
62
+ memoryValue: null,
63
+ category: null,
64
+ lessonAction: null,
65
+ lessonText: null,
66
+ tags: null,
67
+ scope: null,
68
+ outcomeAction: null,
69
+ outcomeSlug: null,
70
+ outcomeGoal: null,
71
+ outcomeVerify: null,
72
+ budget: null,
73
+ reason: null,
74
+ importFrom: null,
36
75
  apply: false,
37
76
  dryRun: false,
38
77
  runtimes: [],
@@ -54,6 +93,27 @@ function parseArgs(argv, cwd = process.cwd()) {
54
93
  opts.stateAction = arg;
55
94
  continue;
56
95
  }
96
+ if (opts.command === 'verify' && opts.verifyCommand === null && !arg.startsWith('-')) {
97
+ opts.verifyCommand = arg;
98
+ continue;
99
+ }
100
+ if (opts.command === 'route' && opts.routePrompt === null && !arg.startsWith('-')) {
101
+ opts.routePrompt = arg;
102
+ continue;
103
+ }
104
+ if (opts.command === 'memory' && !arg.startsWith('-')) {
105
+ if (opts.memoryAction === null) { opts.memoryAction = arg; continue; }
106
+ if (opts.memoryKey === null) { opts.memoryKey = arg; continue; }
107
+ if (opts.memoryValue === null) { opts.memoryValue = arg; continue; }
108
+ }
109
+ if (opts.command === 'lesson' && !arg.startsWith('-')) {
110
+ if (opts.lessonAction === null) { opts.lessonAction = arg; continue; }
111
+ if (opts.lessonText === null) { opts.lessonText = arg; continue; }
112
+ }
113
+ if (opts.command === 'outcome' && !arg.startsWith('-')) {
114
+ if (opts.outcomeAction === null) { opts.outcomeAction = arg; continue; }
115
+ if (opts.outcomeSlug === null) { opts.outcomeSlug = arg; continue; }
116
+ }
57
117
 
58
118
  switch (arg) {
59
119
  case '--json':
@@ -95,6 +155,126 @@ function parseArgs(argv, cwd = process.cwd()) {
95
155
  i++;
96
156
  }
97
157
  break;
158
+ case '--substep':
159
+ if (args[i + 1]) {
160
+ opts.substep = args[i + 1];
161
+ i++;
162
+ }
163
+ break;
164
+ case '--claim':
165
+ if (args[i + 1]) {
166
+ opts.claim = args[i + 1];
167
+ i++;
168
+ }
169
+ break;
170
+ case '--timeout':
171
+ if (args[i + 1]) {
172
+ opts.timeout = args[i + 1];
173
+ i++;
174
+ }
175
+ break;
176
+ case '--evidence':
177
+ if (args[i + 1]) {
178
+ opts.evidence = args[i + 1];
179
+ i++;
180
+ }
181
+ break;
182
+ case '--attest':
183
+ opts.attest = true;
184
+ break;
185
+ case '--peek':
186
+ opts.peek = true;
187
+ break;
188
+ case '--since':
189
+ if (args[i + 1]) {
190
+ opts.since = args[i + 1];
191
+ i++;
192
+ }
193
+ break;
194
+ case '--action':
195
+ if (args[i + 1]) {
196
+ opts.reflectAction = args[i + 1];
197
+ i++;
198
+ }
199
+ break;
200
+ case '--outcome':
201
+ if (args[i + 1]) {
202
+ opts.outcome = args[i + 1];
203
+ i++;
204
+ }
205
+ break;
206
+ case '--observation':
207
+ if (args[i + 1]) {
208
+ opts.observation = args[i + 1];
209
+ i++;
210
+ }
211
+ break;
212
+ case '--root-cause':
213
+ if (args[i + 1]) {
214
+ opts.rootCause = args[i + 1];
215
+ i++;
216
+ }
217
+ break;
218
+ case '--next':
219
+ if (args[i + 1]) {
220
+ opts.nextAction = args[i + 1];
221
+ i++;
222
+ }
223
+ break;
224
+ case '--lesson':
225
+ if (args[i + 1]) {
226
+ opts.lesson = args[i + 1];
227
+ i++;
228
+ }
229
+ break;
230
+ case '--category':
231
+ if (args[i + 1]) {
232
+ opts.category = args[i + 1];
233
+ i++;
234
+ }
235
+ break;
236
+ case '--tags':
237
+ if (args[i + 1]) {
238
+ opts.tags = args[i + 1];
239
+ i++;
240
+ }
241
+ break;
242
+ case '--scope':
243
+ if (args[i + 1]) {
244
+ opts.scope = args[i + 1];
245
+ i++;
246
+ }
247
+ break;
248
+ case '--goal':
249
+ if (args[i + 1]) {
250
+ opts.outcomeGoal = args[i + 1];
251
+ i++;
252
+ }
253
+ break;
254
+ case '--verify':
255
+ if (args[i + 1]) {
256
+ opts.outcomeVerify = args[i + 1];
257
+ i++;
258
+ }
259
+ break;
260
+ case '--budget':
261
+ if (args[i + 1]) {
262
+ opts.budget = args[i + 1];
263
+ i++;
264
+ }
265
+ break;
266
+ case '--reason':
267
+ if (args[i + 1]) {
268
+ opts.reason = args[i + 1];
269
+ i++;
270
+ }
271
+ break;
272
+ case '--from':
273
+ if (args[i + 1]) {
274
+ opts.importFrom = args[i + 1];
275
+ i++;
276
+ }
277
+ break;
98
278
  case '--project':
99
279
  if (args[i + 1]) {
100
280
  opts.project = path.resolve(args[i + 1]);
@@ -150,6 +330,44 @@ function parseArgs(argv, cwd = process.cwd()) {
150
330
  opts.step = arg.slice('--step='.length);
151
331
  } else if (arg.startsWith('--status=')) {
152
332
  opts.status = arg.slice('--status='.length);
333
+ } else if (arg.startsWith('--substep=')) {
334
+ opts.substep = arg.slice('--substep='.length);
335
+ } else if (arg.startsWith('--claim=')) {
336
+ opts.claim = arg.slice('--claim='.length);
337
+ } else if (arg.startsWith('--timeout=')) {
338
+ opts.timeout = arg.slice('--timeout='.length);
339
+ } else if (arg.startsWith('--evidence=')) {
340
+ opts.evidence = arg.slice('--evidence='.length);
341
+ } else if (arg.startsWith('--since=')) {
342
+ opts.since = arg.slice('--since='.length);
343
+ } else if (arg.startsWith('--action=')) {
344
+ opts.reflectAction = arg.slice('--action='.length);
345
+ } else if (arg.startsWith('--outcome=')) {
346
+ opts.outcome = arg.slice('--outcome='.length);
347
+ } else if (arg.startsWith('--observation=')) {
348
+ opts.observation = arg.slice('--observation='.length);
349
+ } else if (arg.startsWith('--root-cause=')) {
350
+ opts.rootCause = arg.slice('--root-cause='.length);
351
+ } else if (arg.startsWith('--next=')) {
352
+ opts.nextAction = arg.slice('--next='.length);
353
+ } else if (arg.startsWith('--lesson=')) {
354
+ opts.lesson = arg.slice('--lesson='.length);
355
+ } else if (arg.startsWith('--category=')) {
356
+ opts.category = arg.slice('--category='.length);
357
+ } else if (arg.startsWith('--tags=')) {
358
+ opts.tags = arg.slice('--tags='.length);
359
+ } else if (arg.startsWith('--scope=')) {
360
+ opts.scope = arg.slice('--scope='.length);
361
+ } else if (arg.startsWith('--goal=')) {
362
+ opts.outcomeGoal = arg.slice('--goal='.length);
363
+ } else if (arg.startsWith('--verify=')) {
364
+ opts.outcomeVerify = arg.slice('--verify='.length);
365
+ } else if (arg.startsWith('--budget=')) {
366
+ opts.budget = arg.slice('--budget='.length);
367
+ } else if (arg.startsWith('--reason=')) {
368
+ opts.reason = arg.slice('--reason='.length);
369
+ } else if (arg.startsWith('--from=')) {
370
+ opts.importFrom = arg.slice('--from='.length);
153
371
  } else if (arg.startsWith('--profile=')) {
154
372
  opts.profile = arg.slice('--profile='.length);
155
373
  } else if (arg.startsWith('--') && RUNTIMES[arg.slice(2)]) {
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Quarterback: the entry-level router.
3
+ *
4
+ * A thin decision layer that composes the existing structural router
5
+ * (lib/router.js) and the fuzzy-intent playbook (lib/recipes.js) rather than
6
+ * duplicating them. It adds exactly two genes that Godpowers lacks at entry:
7
+ * - refuse-on-red: never start new work when the latest executed verdict is
8
+ * red or harden findings carry an unresolved Critical (the [10] route).
9
+ * - proportional ceremony: do not open an arc for a one-line fix (the [90]
10
+ * route).
11
+ * Everything else delegates to router.suggestNext() and recipes.matchIntent().
12
+ *
13
+ * Read-only: route() never mutates state. See docs/FUSION-ARCHITECTURE.md 4.3.
14
+ *
15
+ * @typedef {Object} Play
16
+ * @property {string} route One of recover, resume, recovery, brownfield,
17
+ * research, review, full, feature, trivial.
18
+ * @property {string} reason Why this route was chosen.
19
+ * @property {string|null} nextCommand The command (or null to answer inline).
20
+ * @property {string} ceremony none | light | focused | full | inherit.
21
+ * @property {string} verificationStrategy none | artifact+attested | executed-where-gated.
22
+ * @property {string} chatPolicy Always "stay in this chat as executor".
23
+ * @property {boolean} mutatesState Always false.
24
+ * @property {{ classification: string, latestVerdict: string, activeArc: string|null, openFindings: boolean }} evidence
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const router = require('./router');
31
+ const recipes = require('./recipes');
32
+ const evidence = require('./evidence');
33
+ const stateStore = require('./state');
34
+
35
+ const CONTINUATION_KEYWORDS = ['continue', 'resume', 'next step', 'keep going', "what's next", 'whats next', 'pick up', 'carry on'];
36
+ const INCIDENT_KEYWORDS = ['incident', 'outage', 'hotfix', 'postmortem', 'post-mortem', 'rollback', 'regression', 'production is down', 'broke prod', 'sev1', 'sev 1'];
37
+ const BROWNFIELD_KEYWORDS = ['inherited', 'inherit', 'existing codebase', 'legacy', 'brownfield', 'took over', 'onboard onto', 'understand this repo', 'archaeology'];
38
+ const RESEARCH_KEYWORDS = ['spike', 'explore', 'research', 'prototype', 'proof of concept', 'proof-of-concept', 'poc', 'evaluate', 'not sure which', 'unsure', 'investigate'];
39
+ const REVIEW_KEYWORDS = ['audit', 'review', 'critique', 'find risks', 'find bugs', 'security review', 'assess', 'what could go wrong', 'red team'];
40
+ const FULL_KEYWORDS = ['idea to production', 'ship it all', 'end to end', 'end-to-end', 'full arc', 'god mode', 'build the whole', 'whole thing', 'from scratch to launch', 'take it to production'];
41
+ const TRIVIAL_KEYWORDS = ['typo', 'rename', 'one-line', 'one line', 'quick question', 'what is', 'how do i', 'how do you', 'tweak', 'small fix', 'change the wording', 'bump the'];
42
+
43
+ function hasAny(text, keywords) {
44
+ return keywords.some((kw) => text.includes(kw));
45
+ }
46
+
47
+ function classify(prompt) {
48
+ const text = String(prompt || '').toLowerCase().trim();
49
+ if (text === '') return 'continue';
50
+ if (hasAny(text, CONTINUATION_KEYWORDS)) return 'continue';
51
+ if (hasAny(text, INCIDENT_KEYWORDS)) return 'incident';
52
+ if (hasAny(text, FULL_KEYWORDS)) return 'full';
53
+ if (hasAny(text, BROWNFIELD_KEYWORDS)) return 'brownfield';
54
+ if (hasAny(text, RESEARCH_KEYWORDS)) return 'research';
55
+ if (hasAny(text, REVIEW_KEYWORDS)) return 'review';
56
+ if (hasAny(text, TRIVIAL_KEYWORDS)) return 'trivial';
57
+ return 'feature';
58
+ }
59
+
60
+ function latestVerdict(projectRoot) {
61
+ const records = evidence.history({ projectRoot });
62
+ for (let i = records.length - 1; i >= 0; i--) {
63
+ const record = records[i];
64
+ if (record && record.kind === 'executed') {
65
+ return record.verified ? 'green' : 'red';
66
+ }
67
+ }
68
+ return 'none';
69
+ }
70
+
71
+ function openFindings(projectRoot) {
72
+ const findings = path.join(projectRoot, '.godpowers', 'harden', 'FINDINGS.md');
73
+ // Only treat findings as open when the file exists and carries a Critical or a
74
+ // blocked launch gate. router.hasNoCriticalFindings is fail-closed (false when
75
+ // the file is absent), so guard on existence to avoid false "red" on projects
76
+ // that have not run harden yet.
77
+ if (!fs.existsSync(findings)) return false;
78
+ return !router.hasNoCriticalFindings(projectRoot);
79
+ }
80
+
81
+ function activeArc(projectRoot) {
82
+ const state = stateStore.read(projectRoot);
83
+ if (!state) return null;
84
+ return state['active-arc'] || state.arc || state['lifecycle-phase'] || null;
85
+ }
86
+
87
+ function recipeCommand(prompt, projectRoot) {
88
+ const matches = recipes.matchIntent(prompt, projectRoot);
89
+ if (!matches.length || matches[0].score < 10) return null;
90
+ const recipe = matches[0].recipe;
91
+ const name = recipe['default-sequence'] || 'default';
92
+ const steps = recipes.getSequence(recipe, name);
93
+ const first = steps[0] && steps[0].command;
94
+ return first ? String(first).split(/\s+/)[0] : null;
95
+ }
96
+
97
+ function play(route, reason, nextCommand, ceremony, verificationStrategy, ev) {
98
+ return {
99
+ route,
100
+ reason,
101
+ nextCommand,
102
+ ceremony,
103
+ verificationStrategy,
104
+ chatPolicy: 'stay in this chat as executor',
105
+ mutatesState: false,
106
+ evidence: ev
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Decide the entry play for a prompt. First match wins down the priority ladder.
112
+ *
113
+ * @param {string} prompt Free-text user intent (may be empty).
114
+ * @param {{ projectRoot?: string }} [opts]
115
+ * @returns {Play}
116
+ */
117
+ function route(prompt, opts = {}) {
118
+ const projectRoot = path.resolve(opts.projectRoot || process.cwd());
119
+ const ev = {
120
+ classification: classify(prompt),
121
+ latestVerdict: latestVerdict(projectRoot),
122
+ activeArc: activeArc(projectRoot),
123
+ openFindings: openFindings(projectRoot)
124
+ };
125
+ const next = router.suggestNext(projectRoot);
126
+ const initialized = stateStore.isInitialized(projectRoot);
127
+
128
+ // [10] recover: refuse-on-red. Never start new work on a red check.
129
+ if (ev.latestVerdict === 'red') {
130
+ return play('recover', 'Latest executed verification is red; debug and re-verify before new work.',
131
+ '/god-debug', 'focused', 'executed-where-gated', ev);
132
+ }
133
+ if (ev.openFindings) {
134
+ return play('recover', 'Harden findings carry an unresolved Critical or a blocked launch gate; resolve before new work.',
135
+ '/god-debug', 'focused', 'executed-where-gated', ev);
136
+ }
137
+
138
+ // [20] resume: an active arc with non-done substeps plus continuation intent.
139
+ const hasOpenArc = initialized && next && next.command && next.command !== '/god-init'
140
+ && next.tier !== 'steady-state';
141
+ if (hasOpenArc && ev.classification === 'continue') {
142
+ return play('resume', `Active arc has open work: ${next.reason}.`,
143
+ next.command, 'inherit', 'executed-where-gated', ev);
144
+ }
145
+
146
+ // [30]-[90]: classification-based new work. Delegate to recipes/router.
147
+ switch (ev.classification) {
148
+ case 'incident':
149
+ return play('recovery', 'Incident, hotfix, or postmortem intent.',
150
+ recipeCommand(prompt, projectRoot) || '/god-hotfix', 'focused', 'executed-where-gated', ev);
151
+ case 'brownfield':
152
+ return play('brownfield', 'Inheriting or understanding existing code.',
153
+ recipeCommand(prompt, projectRoot) || '/god-archaeology', 'full', 'artifact+attested', ev);
154
+ case 'research':
155
+ return play('research', 'Uncertain technology; time-box a spike or exploration.',
156
+ recipeCommand(prompt, projectRoot) || '/god-spike', 'light', 'artifact+attested', ev);
157
+ case 'review':
158
+ return play('review', 'Find risks, critique, or audit; no new feature work.',
159
+ recipeCommand(prompt, projectRoot) || '/god-code-review', 'light', 'artifact+attested', ev);
160
+ case 'full':
161
+ return play('full', 'Idea-to-production request; run the full arc.',
162
+ '/god-mode', 'full', 'executed-where-gated', ev);
163
+ case 'trivial':
164
+ return play('trivial', 'Single reversible edit or question; do not open an arc.',
165
+ '/god-fast', 'none', 'none', ev);
166
+ case 'continue':
167
+ // Continuation intent but no open arc: point at the structural next step.
168
+ return play('resume', next && next.reason ? next.reason : 'Continue from current state.',
169
+ next ? next.command : '/god-init', initialized ? 'inherit' : 'full', 'executed-where-gated', ev);
170
+ default:
171
+ return play('feature', 'Ordinary multi-step feature.',
172
+ recipeCommand(prompt, projectRoot) || '/god-feature', 'full', 'executed-where-gated', ev);
173
+ }
174
+ }
175
+
176
+ module.exports = {
177
+ route,
178
+ classify,
179
+ // Internals exposed for tests.
180
+ _latestVerdict: latestVerdict,
181
+ _openFindings: openFindings,
182
+ _activeArc: activeArc
183
+ };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Work report: the chat play-by-play (Phase 3 visibility gene).
3
+ *
4
+ * Rebound from Mythify's build_work_report: cursor-based, reads the evidence
5
+ * ledger, surfaces an "Attention" section for reds, and advances a cursor
6
+ * unless --peek. The cursor lives at .godpowers/ledger/reports/cursor.json so a
7
+ * fresh session can emit only what is new since the last report.
8
+ *
9
+ * Read-mostly: report() reads the ledger and, unless peek is set, advances the
10
+ * report cursor. It never mutates state.json.
11
+ *
12
+ * @typedef {Object} WorkReport
13
+ * @property {string} since "last" or "all".
14
+ * @property {boolean} peek Whether the cursor was left unadvanced.
15
+ * @property {Object[]} records The ledger records in the window, oldest first.
16
+ * @property {Object[]} attention Executed records that did not verify (reds).
17
+ * @property {{ total: number, passed: number, failed: number, attested: number }} summary
18
+ * @property {{ previous: string|null, next: string|null }} cursor
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const atomic = require('./atomic-write');
25
+ const evidence = require('./evidence');
26
+
27
+ function reportsDir(projectRoot) {
28
+ return path.join(projectRoot, '.godpowers', 'ledger', 'reports');
29
+ }
30
+
31
+ function cursorPath(projectRoot) {
32
+ return path.join(reportsDir(projectRoot), 'cursor.json');
33
+ }
34
+
35
+ function readCursor(projectRoot) {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(cursorPath(projectRoot), 'utf8'));
38
+ } catch (_) {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function writeCursor(projectRoot, cursor) {
44
+ fs.mkdirSync(reportsDir(projectRoot), { recursive: true });
45
+ atomic.writeJsonAtomic(cursorPath(projectRoot), cursor);
46
+ }
47
+
48
+ function summarize(window) {
49
+ const executed = window.filter((r) => r && r.kind === 'executed');
50
+ return {
51
+ total: window.length,
52
+ passed: executed.filter((r) => r.verified).length,
53
+ failed: executed.filter((r) => !r.verified).length,
54
+ attested: window.filter((r) => r && r.kind === 'attested').length
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Build the work report for records since the last cursor (or all records).
60
+ *
61
+ * @param {{ since?: string, peek?: boolean, projectRoot?: string }} [opts]
62
+ * @returns {WorkReport}
63
+ */
64
+ function report(opts = {}) {
65
+ const projectRoot = path.resolve(opts.projectRoot || process.cwd());
66
+ const since = opts.since === 'all' ? 'all' : 'last';
67
+ const peek = Boolean(opts.peek);
68
+
69
+ const records = evidence.read(projectRoot); // oldest first (append order)
70
+ const cursor = readCursor(projectRoot);
71
+ const lastTs = cursor.lastTs || null;
72
+
73
+ const window = since === 'all'
74
+ ? records.slice()
75
+ : records.filter((r) => r && r.timestamp && (!lastTs || r.timestamp > lastTs));
76
+
77
+ const attention = window.filter((r) => r && r.kind === 'executed' && !r.verified);
78
+ const newest = window.length ? window[window.length - 1].timestamp : lastTs;
79
+
80
+ if (!peek && newest && newest !== lastTs) {
81
+ writeCursor(projectRoot, { lastTs: newest });
82
+ }
83
+
84
+ return {
85
+ since,
86
+ peek,
87
+ records: window,
88
+ attention,
89
+ summary: summarize(window),
90
+ cursor: { previous: lastTs, next: newest || null }
91
+ };
92
+ }
93
+
94
+ function describeRecord(record) {
95
+ const label = record.claim || record.command || '(unlabeled)';
96
+ if (record.kind === 'attested') {
97
+ return ` ATTESTED ${record.substep || '-'} ${label}`;
98
+ }
99
+ const verdict = record.verified ? 'PASS' : 'FAIL';
100
+ return ` ${verdict} ${record.substep || '-'} exit ${record.exit_code} ${label}`;
101
+ }
102
+
103
+ function render(result) {
104
+ const lines = [];
105
+ lines.push('Godpowers Work Report');
106
+ lines.push('');
107
+ if (result.records.length === 0) {
108
+ lines.push(result.since === 'all'
109
+ ? 'No verification records yet.'
110
+ : 'Nothing new since the last report.');
111
+ return lines.join('\n');
112
+ }
113
+ lines.push(`Since: ${result.since}${result.peek ? ' (peek, cursor not advanced)' : ''}`);
114
+ lines.push('');
115
+ lines.push('Play-by-play:');
116
+ for (const record of result.records) {
117
+ lines.push(describeRecord(record));
118
+ }
119
+ if (result.attention.length > 0) {
120
+ lines.push('');
121
+ lines.push('Attention (unverified):');
122
+ for (const record of result.attention) {
123
+ lines.push(describeRecord(record));
124
+ }
125
+ }
126
+ const s = result.summary;
127
+ lines.push('');
128
+ lines.push(`Summary: ${s.passed} passed, ${s.failed} failed, ${s.attested} attested (${s.total} record(s))`);
129
+ return lines.join('\n');
130
+ }
131
+
132
+ module.exports = {
133
+ report,
134
+ render,
135
+ reportsDir,
136
+ cursorPath
137
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "godpowers",
3
- "version": "3.0.2",
3
+ "version": "3.11.0",
4
4
  "description": "AI-powered development system: 120 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"