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/CHANGELOG.md +229 -0
- package/README.md +16 -10
- package/RELEASE.md +21 -33
- package/bin/install.js +34 -0
- package/fixtures/gate/harden-pass/.godpowers/state.json +26 -0
- package/lib/artifact-map.js +2 -1
- package/lib/cli-dispatch.js +409 -2
- package/lib/evidence/.provenance.json +45 -0
- package/lib/evidence-import.js +147 -0
- package/lib/evidence.js +908 -0
- package/lib/gate.js +26 -15
- package/lib/installer-args.js +219 -1
- package/lib/quarterback.js +183 -0
- package/lib/work-report.js +137 -0
- package/package.json +1 -1
- package/references/orchestration/GOD-ORCHESTRATOR-RUNBOOK.md +9 -4
- package/skills/god-harden.md +5 -2
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
|
-
|
|
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 (!
|
|
273
|
-
const
|
|
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
|
-
|
|
284
|
+
`${tier}-verification-failed-command`,
|
|
277
285
|
'error',
|
|
278
286
|
relPath,
|
|
279
|
-
|
|
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
|
-
|
|
292
|
+
`${tier}-verification-failed-command`,
|
|
285
293
|
'fail',
|
|
286
294
|
relPath,
|
|
287
295
|
finding.reason
|
|
288
296
|
));
|
|
289
|
-
result.summary
|
|
297
|
+
result.summary[`${tier}VerificationFailedCommands`] = failedCommands;
|
|
290
298
|
return;
|
|
291
299
|
}
|
|
292
|
-
const passedCommands = commandsWithStatus(
|
|
300
|
+
const passedCommands = commandsWithStatus(step, 'pass');
|
|
293
301
|
if (passedCommands.length === 0) {
|
|
294
302
|
const finding = makeFinding(
|
|
295
|
-
|
|
303
|
+
`${tier}-verification-evidence`,
|
|
296
304
|
'error',
|
|
297
305
|
relPath,
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
+
`${tier}-verification-evidence`,
|
|
312
320
|
'pass',
|
|
313
321
|
relPath,
|
|
314
|
-
`state.json records ${passedCommands.length} passed
|
|
322
|
+
`state.json records ${passedCommands.length} passed ${tier} verification command(s).`
|
|
315
323
|
));
|
|
316
|
-
result.summary
|
|
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
|
-
|
|
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
|
}
|
package/lib/installer-args.js
CHANGED
|
@@ -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
|
|
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"
|