godpowers 3.0.1 → 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.
Files changed (67) hide show
  1. package/CHANGELOG.md +255 -2
  2. package/README.md +31 -14
  3. package/RELEASE.md +21 -33
  4. package/SKILL.md +71 -112
  5. package/bin/install.js +44 -0
  6. package/fixtures/gate/harden-pass/.godpowers/state.json +26 -0
  7. package/lib/README.md +1 -0
  8. package/lib/artifact-map.js +2 -1
  9. package/lib/cli-dispatch.js +449 -3
  10. package/lib/command-families.js +10 -2
  11. package/lib/evidence/.provenance.json +45 -0
  12. package/lib/evidence-import.js +147 -0
  13. package/lib/evidence.js +908 -0
  14. package/lib/gate.js +26 -15
  15. package/lib/install-profiles.js +4 -1
  16. package/lib/installer-args.js +241 -1
  17. package/lib/quarterback.js +183 -0
  18. package/lib/surface-profile.js +168 -0
  19. package/lib/work-report.js +137 -0
  20. package/package.json +2 -2
  21. package/references/orchestration/GOD-MODE-RUNBOOK.md +9 -14
  22. package/references/orchestration/GOD-ORCHESTRATOR-RUNBOOK.md +40 -82
  23. package/references/shared/DASHBOARD-CONTRACT.md +66 -29
  24. package/references/shared/README.md +1 -1
  25. package/routing/god-demo.yaml +35 -0
  26. package/routing/god-first-run.yaml +34 -0
  27. package/routing/god-surface.yaml +39 -0
  28. package/routing/recipes/try-safely.yaml +26 -0
  29. package/skills/god-agent-audit.md +5 -6
  30. package/skills/god-archaeology.md +5 -6
  31. package/skills/god-audit.md +6 -7
  32. package/skills/god-automation-setup.md +6 -7
  33. package/skills/god-automation-status.md +6 -7
  34. package/skills/god-context-scan.md +7 -8
  35. package/skills/god-demo.md +53 -0
  36. package/skills/god-design-impact.md +5 -6
  37. package/skills/god-discuss.md +5 -6
  38. package/skills/god-docs.md +5 -10
  39. package/skills/god-doctor.md +8 -9
  40. package/skills/god-dogfood.md +7 -10
  41. package/skills/god-explore.md +5 -7
  42. package/skills/god-first-run.md +64 -0
  43. package/skills/god-harden.md +5 -2
  44. package/skills/god-help.md +77 -51
  45. package/skills/god-hygiene.md +5 -6
  46. package/skills/god-lifecycle.md +11 -13
  47. package/skills/god-list-assumptions.md +7 -8
  48. package/skills/god-locate.md +7 -8
  49. package/skills/god-map-codebase.md +5 -6
  50. package/skills/god-migrate.md +6 -15
  51. package/skills/god-next.md +16 -17
  52. package/skills/god-preflight.md +7 -8
  53. package/skills/god-progress.md +7 -8
  54. package/skills/god-reconcile.md +5 -6
  55. package/skills/god-reconstruct.md +5 -7
  56. package/skills/god-refactor.md +6 -7
  57. package/skills/god-roadmap-check.md +6 -7
  58. package/skills/god-scan.md +5 -9
  59. package/skills/god-spike.md +5 -6
  60. package/skills/god-standards.md +5 -6
  61. package/skills/god-status.md +12 -10
  62. package/skills/god-surface.md +61 -0
  63. package/skills/god-sync.md +4 -8
  64. package/skills/god-tech-debt.md +5 -6
  65. package/skills/god-test-runtime.md +4 -8
  66. package/skills/god-version.md +1 -1
  67. package/skills/god.md +53 -52
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
  }
@@ -1,6 +1,9 @@
1
1
  const COMMON = [
2
2
  'god',
3
+ 'god-first-run',
4
+ 'god-demo',
3
5
  'god-help',
6
+ 'god-surface',
4
7
  'god-version',
5
8
  'god-status'
6
9
  ];
@@ -131,7 +134,7 @@ const PROFILE_SKILLS = {
131
134
  };
132
135
 
133
136
  const PROFILE_DESCRIPTIONS = {
134
- core: 'front door, status, verbs, and autonomous compatibility',
137
+ core: 'first-run guidance, front door, status, verbs, surface control, and autonomous compatibility',
135
138
  builder: 'core plus planning leaves, stories, and runtime verification',
136
139
  maintainer: 'core plus hygiene, deps, docs, repair, automation, and extensions',
137
140
  suite: 'core plus multi-repo suite and workstream coordination',
@@ -11,7 +11,18 @@ const COMMANDS = new Set([
11
11
  'automation-setup',
12
12
  'dogfood',
13
13
  'extension-scaffold',
14
- 'gate'
14
+ 'surface',
15
+ 'demo',
16
+ 'gate',
17
+ 'verify',
18
+ 'can-close',
19
+ 'route',
20
+ 'report',
21
+ 'reflect',
22
+ 'memory',
23
+ 'lesson',
24
+ 'outcome',
25
+ 'import-ledger'
15
26
  ]);
16
27
 
17
28
  function parseArgs(argv, cwd = process.cwd()) {
@@ -21,6 +32,7 @@ function parseArgs(argv, cwd = process.cwd()) {
21
32
  project: cwd,
22
33
  json: false,
23
34
  brief: false,
35
+ full: false,
24
36
  stateAction: null,
25
37
  step: null,
26
38
  status: null,
@@ -30,6 +42,38 @@ function parseArgs(argv, cwd = process.cwd()) {
30
42
  extensionAgent: null,
31
43
  extensionWorkflow: null,
32
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,
75
+ apply: false,
76
+ dryRun: false,
33
77
  runtimes: [],
34
78
  global: false,
35
79
  local: false,
@@ -49,6 +93,27 @@ function parseArgs(argv, cwd = process.cwd()) {
49
93
  opts.stateAction = arg;
50
94
  continue;
51
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
+ }
52
117
 
53
118
  switch (arg) {
54
119
  case '--json':
@@ -57,6 +122,21 @@ function parseArgs(argv, cwd = process.cwd()) {
57
122
  case '--brief':
58
123
  opts.brief = true;
59
124
  break;
125
+ case '--full':
126
+ opts.full = true;
127
+ break;
128
+ case '--apply':
129
+ opts.apply = true;
130
+ break;
131
+ case '--dry-run':
132
+ opts.dryRun = true;
133
+ break;
134
+ case '--runtime':
135
+ if (args[i + 1]) {
136
+ opts.runtimes.push(args[i + 1]);
137
+ i++;
138
+ }
139
+ break;
60
140
  case '--tier':
61
141
  if (args[i + 1]) {
62
142
  opts.tier = args[i + 1];
@@ -75,6 +155,126 @@ function parseArgs(argv, cwd = process.cwd()) {
75
155
  i++;
76
156
  }
77
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;
78
278
  case '--project':
79
279
  if (args[i + 1]) {
80
280
  opts.project = path.resolve(args[i + 1]);
@@ -122,12 +322,52 @@ function parseArgs(argv, cwd = process.cwd()) {
122
322
  opts.extensionAgent = arg.slice('--agent='.length);
123
323
  } else if (arg.startsWith('--workflow=')) {
124
324
  opts.extensionWorkflow = arg.slice('--workflow='.length);
325
+ } else if (arg.startsWith('--runtime=')) {
326
+ opts.runtimes.push(arg.slice('--runtime='.length));
125
327
  } else if (arg.startsWith('--tier=')) {
126
328
  opts.tier = arg.slice('--tier='.length);
127
329
  } else if (arg.startsWith('--step=')) {
128
330
  opts.step = arg.slice('--step='.length);
129
331
  } else if (arg.startsWith('--status=')) {
130
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);
131
371
  } else if (arg.startsWith('--profile=')) {
132
372
  opts.profile = arg.slice('--profile='.length);
133
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
+ };