godpowers 2.6.0 → 2.7.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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +26 -22
  3. package/RELEASE.md +38 -35
  4. package/SKILL.md +46 -48
  5. package/agents/god-deploy-engineer.md +2 -2
  6. package/agents/god-designer.md +3 -2
  7. package/agents/god-greenfieldifier.md +2 -4
  8. package/agents/god-launch-strategist.md +4 -5
  9. package/agents/god-observability-engineer.md +5 -5
  10. package/agents/god-reconciler.md +10 -4
  11. package/agents/god-retrospective.md +1 -1
  12. package/agents/god-updater.md +5 -5
  13. package/bin/install.js +6 -1
  14. package/fixtures/gate/build-pass/.godpowers/state.json +33 -0
  15. package/lib/README.md +2 -0
  16. package/lib/artifact-map.js +15 -3
  17. package/lib/cli-dispatch.js +39 -1
  18. package/lib/context-writer.js +4 -4
  19. package/lib/gate.js +107 -9
  20. package/lib/installer-args.js +24 -0
  21. package/lib/pillars.js +2 -4
  22. package/lib/recipes.js +16 -0
  23. package/lib/router.js +1 -5
  24. package/lib/source-sync.js +1 -1
  25. package/lib/state-advance.js +244 -0
  26. package/lib/state-lock.js +8 -4
  27. package/lib/state-views.js +460 -0
  28. package/lib/state.js +52 -3
  29. package/package.json +1 -1
  30. package/routing/god-audit.yaml +1 -1
  31. package/routing/god-build.yaml +1 -1
  32. package/routing/god-context.yaml +1 -1
  33. package/routing/god-deploy.yaml +3 -1
  34. package/routing/god-design.yaml +2 -2
  35. package/routing/god-launch.yaml +4 -1
  36. package/routing/god-migrate.yaml +0 -1
  37. package/routing/god-mode.yaml +1 -1
  38. package/routing/god-observe.yaml +4 -1
  39. package/routing/god-prd.yaml +1 -1
  40. package/routing/god-reconcile.yaml +2 -5
  41. package/routing/god-sync.yaml +1 -1
  42. package/routing/recipes/returning-after-break.yaml +1 -1
  43. package/schema/state.v1.json +68 -1
  44. package/skills/god-arch.md +1 -1
  45. package/skills/god-build.md +6 -4
  46. package/skills/god-deploy.md +16 -14
  47. package/skills/god-design.md +3 -3
  48. package/skills/god-fast.md +2 -2
  49. package/skills/god-feature.md +1 -1
  50. package/skills/god-harden.md +3 -3
  51. package/skills/god-hotfix.md +1 -1
  52. package/skills/god-init.md +14 -10
  53. package/skills/god-launch.md +14 -12
  54. package/skills/god-lifecycle.md +2 -1
  55. package/skills/god-mode.md +5 -4
  56. package/skills/god-observe.md +15 -13
  57. package/skills/god-pause-work.md +2 -2
  58. package/skills/god-prd.md +5 -4
  59. package/skills/god-quick.md +1 -1
  60. package/skills/god-repo.md +1 -1
  61. package/skills/god-resume-work.md +5 -4
  62. package/skills/god-roadmap-update.md +1 -1
  63. package/skills/god-roadmap.md +1 -1
  64. package/skills/god-rollback.md +1 -1
  65. package/skills/god-skip.md +2 -2
  66. package/skills/god-stack.md +1 -1
  67. package/skills/god-standards.md +1 -1
  68. package/skills/god-status.md +2 -2
  69. package/skills/god-story.md +1 -1
  70. package/skills/god-sync.md +2 -2
  71. package/workflows/bluefield-arc.yaml +2 -4
  72. package/workflows/brownfield-arc.yaml +2 -4
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Generated state views.
3
+ *
4
+ * Writes human-readable markdown views from .godpowers/state.json while
5
+ * preserving user content outside managed fences.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+
12
+ const atomic = require('./atomic-write');
13
+
14
+ const FENCE_BEGIN = '<!-- godpowers:state-view:begin -->';
15
+ const FENCE_END = '<!-- godpowers:state-view:end -->';
16
+ const CHECKSUM_PREFIX = '<!-- godpowers:checksum ';
17
+ const CHECKSUM_SUFFIX = ' -->';
18
+ const PROGRESS_VIEW_PATH = '.godpowers/PROGRESS.md';
19
+ const STATE_VIEW_SPECS = [
20
+ { tierKey: 'tier-1', subStepKey: 'design', relPath: '.godpowers/design/STATE.md' },
21
+ { tierKey: 'tier-2', subStepKey: 'build', relPath: '.godpowers/build/STATE.md' },
22
+ { tierKey: 'tier-3', subStepKey: 'deploy', relPath: '.godpowers/deploy/STATE.md' },
23
+ { tierKey: 'tier-3', subStepKey: 'observe', relPath: '.godpowers/observe/STATE.md' },
24
+ { tierKey: 'tier-3', subStepKey: 'launch', relPath: '.godpowers/launch/STATE.md' }
25
+ ];
26
+ const STATE_VIEW_PATHS = Object.freeze(STATE_VIEW_SPECS.reduce((acc, spec) => {
27
+ acc[spec.subStepKey] = spec.relPath;
28
+ return acc;
29
+ }, {}));
30
+ const KNOWN_SUBSTEP_FIELDS = new Set([
31
+ 'status',
32
+ 'artifact',
33
+ 'artifact-hash',
34
+ 'agent-version',
35
+ 'have-nots-passed',
36
+ 'updated',
37
+ 'notes',
38
+ 'verification'
39
+ ]);
40
+
41
+ const COMPLETE_STATUSES = new Set(['done', 'imported', 'skipped', 'not-required']);
42
+ const ACTIVE_STATUSES = new Set(['in-flight', 'failed', 're-invoked']);
43
+ const TIER_LABELS = {
44
+ 'tier-0': 'Orchestration',
45
+ 'tier-1': 'Planning',
46
+ 'tier-2': 'Building',
47
+ 'tier-3': 'Shipping'
48
+ };
49
+ const SUBSTEP_LABELS = {
50
+ orchestration: 'Orchestration',
51
+ prd: 'PRD',
52
+ arch: 'Architecture',
53
+ roadmap: 'Roadmap',
54
+ stack: 'Stack',
55
+ design: 'Design',
56
+ product: 'Product',
57
+ repo: 'Repo',
58
+ build: 'Build',
59
+ deploy: 'Deploy',
60
+ observe: 'Observe',
61
+ launch: 'Launch',
62
+ harden: 'Harden'
63
+ };
64
+
65
+ function sha(content) {
66
+ return `sha256:${crypto.createHash('sha256').update(content).digest('hex')}`;
67
+ }
68
+
69
+ function tierNumber(tierKey) {
70
+ const match = String(tierKey).match(/^tier-(\d+)$/);
71
+ return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
72
+ }
73
+
74
+ function labelFromKey(key) {
75
+ return String(key)
76
+ .split(/[-_]/)
77
+ .filter(Boolean)
78
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
79
+ .join(' ');
80
+ }
81
+
82
+ function tierComparator(a, b) {
83
+ const byNumber = tierNumber(a) - tierNumber(b);
84
+ return byNumber === 0 ? String(a).localeCompare(String(b)) : byNumber;
85
+ }
86
+
87
+ function isCompleteStatus(status) {
88
+ return COMPLETE_STATUSES.has(status);
89
+ }
90
+
91
+ function isActiveStatus(status) {
92
+ return ACTIVE_STATUSES.has(status);
93
+ }
94
+
95
+ function escapeTable(value) {
96
+ const text = value == null || value === '' ? '-' : String(value);
97
+ return text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
98
+ }
99
+
100
+ function formatValue(value) {
101
+ if (value == null || value === '') return '-';
102
+ if (typeof value === 'object') return JSON.stringify(value);
103
+ return String(value);
104
+ }
105
+
106
+ function subStepForSpec(currentState, spec) {
107
+ return currentState &&
108
+ currentState.tiers &&
109
+ currentState.tiers[spec.tierKey] &&
110
+ currentState.tiers[spec.tierKey][spec.subStepKey]
111
+ ? currentState.tiers[spec.tierKey][spec.subStepKey]
112
+ : null;
113
+ }
114
+
115
+ function existingStateViewSpecs(currentState) {
116
+ return STATE_VIEW_SPECS.filter(spec => subStepForSpec(currentState, spec));
117
+ }
118
+
119
+ function orderedSubSteps(currentState) {
120
+ if (!currentState || !currentState.tiers) return [];
121
+ const steps = [];
122
+ for (const tierKey of Object.keys(currentState.tiers).sort(tierComparator)) {
123
+ const tier = currentState.tiers[tierKey] || {};
124
+ for (const [subStepKey, subStep] of Object.entries(tier)) {
125
+ const status = subStep && subStep.status ? subStep.status : 'pending';
126
+ steps.push({
127
+ tierKey,
128
+ tierNumber: tierNumber(tierKey),
129
+ tierLabel: TIER_LABELS[tierKey] || labelFromKey(tierKey),
130
+ subStepKey,
131
+ subStepLabel: SUBSTEP_LABELS[subStepKey] || labelFromKey(subStepKey),
132
+ status,
133
+ artifact: subStep && subStep.artifact,
134
+ updated: subStep && subStep.updated
135
+ });
136
+ }
137
+ }
138
+ return steps.map((step, index) => ({ ...step, ordinal: index + 1 }));
139
+ }
140
+
141
+ function progressSummary(currentState) {
142
+ const steps = orderedSubSteps(currentState);
143
+ const total = steps.length;
144
+ const completed = steps.filter(step => isCompleteStatus(step.status)).length;
145
+
146
+ let currentIndex = steps.findIndex(step => isActiveStatus(step.status));
147
+ if (currentIndex < 0) {
148
+ currentIndex = steps.findIndex(step => !isCompleteStatus(step.status));
149
+ }
150
+ if (currentIndex < 0 && total > 0) currentIndex = total - 1;
151
+
152
+ return {
153
+ percent: total === 0 ? 0 : Math.round((completed / total) * 100),
154
+ completed,
155
+ total,
156
+ current: currentIndex >= 0 ? steps[currentIndex] : null
157
+ };
158
+ }
159
+
160
+ function buildProgressBody(currentState) {
161
+ const project = currentState && currentState.project ? currentState.project : {};
162
+ const summary = progressSummary(currentState);
163
+ const lines = [];
164
+
165
+ lines.push('# Godpowers Progress');
166
+ lines.push('');
167
+ lines.push('- [DECISION] This file is a generated human-readable view of `.godpowers/state.json`.');
168
+ lines.push('- [DECISION] The managed section may be replaced by Godpowers whenever project state changes.');
169
+ lines.push('- [DECISION] Edit project state through Godpowers commands rather than editing this managed section.');
170
+ lines.push(`- [DECISION] Project: ${project.name || 'unnamed'}.`);
171
+ lines.push(`- [DECISION] Lifecycle phase: ${(currentState && currentState['lifecycle-phase']) || 'unknown'}.`);
172
+ if (summary.total > 0) {
173
+ lines.push(`- [HYPOTHESIS] Workflow progress is ${summary.percent} percent with ${summary.completed} of ${summary.total} tracked steps complete.`);
174
+ if (summary.current) {
175
+ lines.push(`- [HYPOTHESIS] Current step is ${summary.current.tierLabel}: ${summary.current.subStepLabel} with status \`${summary.current.status}\`.`);
176
+ }
177
+ } else {
178
+ lines.push('- [HYPOTHESIS] Workflow progress cannot be computed because no tracked steps exist.');
179
+ }
180
+ lines.push('');
181
+ lines.push('## Workflow Steps');
182
+ lines.push('');
183
+ lines.push('| Step | Tier | Sub-step | Status | Artifact | Updated |');
184
+ lines.push('|---|---|---|---|---|---|');
185
+ for (const step of orderedSubSteps(currentState)) {
186
+ lines.push([
187
+ step.ordinal,
188
+ escapeTable(step.tierLabel),
189
+ escapeTable(step.subStepLabel),
190
+ escapeTable(step.status),
191
+ escapeTable(step.artifact),
192
+ escapeTable(step.updated)
193
+ ].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
194
+ }
195
+ lines.push('');
196
+ return lines.join('\n');
197
+ }
198
+
199
+ function buildVerificationLines(subStep) {
200
+ const commands = subStep &&
201
+ subStep.verification &&
202
+ Array.isArray(subStep.verification.commands)
203
+ ? subStep.verification.commands
204
+ : [];
205
+ const lines = [];
206
+
207
+ lines.push('## Verification Commands');
208
+ lines.push('');
209
+ if (commands.length === 0) {
210
+ lines.push('- [HYPOTHESIS] No verification command evidence is recorded in `state.json` for this step.');
211
+ lines.push('');
212
+ return lines;
213
+ }
214
+
215
+ lines.push('| Command | Status | Exit code | Ran at | Duration ms | Diagnostics |');
216
+ lines.push('|---|---|---|---|---|---|');
217
+ for (const command of commands) {
218
+ lines.push([
219
+ command.command,
220
+ command.status,
221
+ command.exitCode,
222
+ command.ranAt,
223
+ command.durationMs,
224
+ command.diagnostics
225
+ ].map(escapeTable).join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
226
+ }
227
+ lines.push('');
228
+ return lines;
229
+ }
230
+
231
+ function buildEvidenceLines(subStep) {
232
+ const entries = Object.entries(subStep || {})
233
+ .filter(([key]) => !KNOWN_SUBSTEP_FIELDS.has(key))
234
+ .sort(([a], [b]) => a.localeCompare(b));
235
+ const lines = [];
236
+
237
+ lines.push('## Evidence Fields');
238
+ lines.push('');
239
+ if (entries.length === 0) {
240
+ lines.push('- [HYPOTHESIS] No additional evidence fields are recorded in `state.json` for this step.');
241
+ lines.push('');
242
+ return lines;
243
+ }
244
+
245
+ lines.push('| Field | Value |');
246
+ lines.push('|---|---|');
247
+ for (const [key, value] of entries) {
248
+ lines.push(`| ${escapeTable(key)} | ${escapeTable(formatValue(value))} |`);
249
+ }
250
+ lines.push('');
251
+ return lines;
252
+ }
253
+
254
+ function buildStateViewBody(currentState, spec) {
255
+ const project = currentState && currentState.project ? currentState.project : {};
256
+ const subStep = subStepForSpec(currentState, spec) || {};
257
+ const status = subStep.status || 'pending';
258
+ const tierLabel = TIER_LABELS[spec.tierKey] || labelFromKey(spec.tierKey);
259
+ const subStepLabel = SUBSTEP_LABELS[spec.subStepKey] || labelFromKey(spec.subStepKey);
260
+ const lines = [];
261
+
262
+ lines.push(`# Godpowers ${subStepLabel} State`);
263
+ lines.push('');
264
+ lines.push(`- [DECISION] This file is a generated human-readable view of \`.godpowers/state.json\` for \`${spec.tierKey}.${spec.subStepKey}\`.`);
265
+ lines.push('- [DECISION] The managed section may be replaced by Godpowers whenever project state changes.');
266
+ lines.push('- [DECISION] Edit project state through Godpowers commands or owning command wrappers rather than editing this managed section.');
267
+ lines.push(`- [DECISION] Project: ${project.name || 'unnamed'}.`);
268
+ lines.push(`- [DECISION] Step: ${tierLabel}: ${subStepLabel}.`);
269
+ lines.push(`- [DECISION] Status: \`${status}\`.`);
270
+ if (subStep.artifact) {
271
+ lines.push(`- [DECISION] Artifact: \`.godpowers/${subStep.artifact}\`.`);
272
+ } else {
273
+ lines.push('- [HYPOTHESIS] No artifact path is recorded in `state.json` for this step.');
274
+ }
275
+ if (subStep.updated) {
276
+ lines.push(`- [DECISION] Updated: ${subStep.updated}.`);
277
+ } else {
278
+ lines.push('- [HYPOTHESIS] No updated timestamp is recorded in `state.json` for this step.');
279
+ }
280
+ if (subStep.notes) {
281
+ lines.push(`- [DECISION] Notes: ${String(subStep.notes).replace(/\r?\n/g, ' ')}.`);
282
+ }
283
+ lines.push('');
284
+ lines.push(...buildVerificationLines(subStep));
285
+ lines.push(...buildEvidenceLines(subStep));
286
+ return lines.join('\n');
287
+ }
288
+
289
+ function parseManaged(filePath) {
290
+ if (!fs.existsSync(filePath)) {
291
+ return {
292
+ exists: false,
293
+ hasFence: false,
294
+ before: '',
295
+ body: '',
296
+ checksum: null,
297
+ after: '',
298
+ validChecksum: null
299
+ };
300
+ }
301
+ const content = fs.readFileSync(filePath, 'utf8');
302
+ const beginIdx = content.indexOf(FENCE_BEGIN);
303
+ const endIdx = content.indexOf(FENCE_END);
304
+ if (beginIdx === -1 || endIdx === -1 || endIdx < beginIdx) {
305
+ return {
306
+ exists: true,
307
+ hasFence: false,
308
+ before: content,
309
+ body: '',
310
+ checksum: null,
311
+ after: '',
312
+ validChecksum: null
313
+ };
314
+ }
315
+
316
+ const fenced = content.slice(beginIdx + FENCE_BEGIN.length, endIdx);
317
+ const after = content.slice(endIdx + FENCE_END.length);
318
+ const lines = fenced.replace(/^\r?\n/, '').replace(/\r?\n$/, '').split(/\r?\n/);
319
+ const checksumLine = lines[0] || '';
320
+ const checksum = checksumLine.startsWith(CHECKSUM_PREFIX) && checksumLine.endsWith(CHECKSUM_SUFFIX)
321
+ ? checksumLine.slice(CHECKSUM_PREFIX.length, -CHECKSUM_SUFFIX.length)
322
+ : null;
323
+ const body = checksum ? lines.slice(1).join('\n') : lines.join('\n');
324
+ return {
325
+ exists: true,
326
+ hasFence: true,
327
+ before: content.slice(0, beginIdx),
328
+ body,
329
+ checksum,
330
+ after,
331
+ validChecksum: checksum ? checksum === sha(body) : false
332
+ };
333
+ }
334
+
335
+ function fencedBlock(body) {
336
+ return `${FENCE_BEGIN}\n${CHECKSUM_PREFIX}${sha(body)}${CHECKSUM_SUFFIX}\n${body}\n${FENCE_END}`;
337
+ }
338
+
339
+ function maybeWarn(parsed, filePath, opts) {
340
+ if (!parsed.hasFence || parsed.validChecksum !== false) return;
341
+ const relPath = opts.relPath || filePath;
342
+ const warning = `Managed state view checksum mismatch in ${relPath}; replacing generated section from state.json.`;
343
+ if (typeof opts.onWarning === 'function') opts.onWarning(warning);
344
+ }
345
+
346
+ function nextManagedContent(filePath, body, opts = {}) {
347
+ const parsed = parseManaged(filePath);
348
+ maybeWarn(parsed, filePath, opts);
349
+ const block = fencedBlock(body);
350
+ if (!parsed.exists) return `${block}\n`;
351
+ if (!parsed.hasFence) {
352
+ const sep = parsed.before.endsWith('\n\n') ? '' : (parsed.before.endsWith('\n') ? '\n' : '\n\n');
353
+ return `${parsed.before}${sep}${block}\n`;
354
+ }
355
+ return `${parsed.before}${block}${parsed.after}`;
356
+ }
357
+
358
+ function writeManaged(filePath, body, opts = {}) {
359
+ const next = nextManagedContent(filePath, body, opts);
360
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === next) {
361
+ return { path: filePath, written: false };
362
+ }
363
+ atomic.writeFileAtomic(filePath, next);
364
+ return { path: filePath, written: true };
365
+ }
366
+
367
+ async function writeManagedAsync(filePath, body, opts = {}) {
368
+ const next = nextManagedContent(filePath, body, opts);
369
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === next) {
370
+ return { path: filePath, written: false };
371
+ }
372
+ await atomic.writeFileAtomicAsync(filePath, next);
373
+ return { path: filePath, written: true };
374
+ }
375
+
376
+ function progressPath(projectRoot) {
377
+ return path.join(projectRoot, PROGRESS_VIEW_PATH);
378
+ }
379
+
380
+ function stateViewPath(projectRoot, specOrStep) {
381
+ const spec = typeof specOrStep === 'string'
382
+ ? STATE_VIEW_SPECS.find(item => item.subStepKey === specOrStep || item.relPath === specOrStep)
383
+ : specOrStep;
384
+ if (!spec || !spec.relPath) return null;
385
+ return path.join(projectRoot, spec.relPath);
386
+ }
387
+
388
+ function viewPathsForState(currentState) {
389
+ return [
390
+ PROGRESS_VIEW_PATH,
391
+ ...existingStateViewSpecs(currentState).map(spec => spec.relPath)
392
+ ];
393
+ }
394
+
395
+ function writeProgress(projectRoot, currentState, opts = {}) {
396
+ return writeManaged(progressPath(projectRoot), buildProgressBody(currentState), {
397
+ ...opts,
398
+ relPath: PROGRESS_VIEW_PATH
399
+ });
400
+ }
401
+
402
+ async function writeProgressAsync(projectRoot, currentState, opts = {}) {
403
+ return writeManagedAsync(progressPath(projectRoot), buildProgressBody(currentState), {
404
+ ...opts,
405
+ relPath: PROGRESS_VIEW_PATH
406
+ });
407
+ }
408
+
409
+ function writeStateView(projectRoot, currentState, spec, opts = {}) {
410
+ return writeManaged(stateViewPath(projectRoot, spec), buildStateViewBody(currentState, spec), {
411
+ ...opts,
412
+ relPath: spec.relPath
413
+ });
414
+ }
415
+
416
+ async function writeStateViewAsync(projectRoot, currentState, spec, opts = {}) {
417
+ return writeManagedAsync(stateViewPath(projectRoot, spec), buildStateViewBody(currentState, spec), {
418
+ ...opts,
419
+ relPath: spec.relPath
420
+ });
421
+ }
422
+
423
+ function writeAll(projectRoot, currentState, opts = {}) {
424
+ return [
425
+ writeProgress(projectRoot, currentState, opts),
426
+ ...existingStateViewSpecs(currentState).map(spec => writeStateView(projectRoot, currentState, spec, opts))
427
+ ];
428
+ }
429
+
430
+ async function writeAllAsync(projectRoot, currentState, opts = {}) {
431
+ const results = [await writeProgressAsync(projectRoot, currentState, opts)];
432
+ for (const spec of existingStateViewSpecs(currentState)) {
433
+ results.push(await writeStateViewAsync(projectRoot, currentState, spec, opts));
434
+ }
435
+ return results;
436
+ }
437
+
438
+ module.exports = {
439
+ FENCE_BEGIN,
440
+ FENCE_END,
441
+ CHECKSUM_PREFIX,
442
+ PROGRESS_VIEW_PATH,
443
+ STATE_VIEW_PATHS,
444
+ STATE_VIEW_SPECS,
445
+ buildProgressBody,
446
+ buildStateViewBody,
447
+ parseManaged,
448
+ writeManaged,
449
+ writeManagedAsync,
450
+ writeProgress,
451
+ writeProgressAsync,
452
+ writeStateView,
453
+ writeStateViewAsync,
454
+ writeAll,
455
+ writeAllAsync,
456
+ progressPath,
457
+ stateViewPath,
458
+ viewPathsForState,
459
+ sha
460
+ };
package/lib/state.js CHANGED
@@ -10,6 +10,7 @@ const path = require('path');
10
10
  const crypto = require('crypto');
11
11
  const asyncFs = require('./fs-async');
12
12
  const atomic = require('./atomic-write');
13
+ const stateViews = require('./state-views');
13
14
 
14
15
  const STATE_VERSION = '1.0.0';
15
16
  const COMPLETE_STATUSES = new Set(['done', 'imported', 'skipped', 'not-required']);
@@ -94,6 +95,42 @@ function read(projectRoot) {
94
95
  }
95
96
  }
96
97
 
98
+ function isUnsafePathSegment(segment) {
99
+ return segment === '__proto__' || segment === 'constructor' || segment === 'prototype';
100
+ }
101
+
102
+ function valueFromPath(source, dottedPath) {
103
+ if (!source || !dottedPath) return undefined;
104
+ return dottedPath.split('.').reduce((acc, segment) => {
105
+ if (!acc || isUnsafePathSegment(segment)) return undefined;
106
+ return acc[segment];
107
+ }, source);
108
+ }
109
+
110
+ function isInitializedState(currentState) {
111
+ return Boolean(
112
+ currentState &&
113
+ typeof currentState === 'object' &&
114
+ currentState.project &&
115
+ typeof currentState.project.name === 'string' &&
116
+ currentState.project.name.trim() &&
117
+ currentState.tiers &&
118
+ typeof currentState.tiers === 'object'
119
+ );
120
+ }
121
+
122
+ function isInitialized(projectRoot) {
123
+ return isInitializedState(read(projectRoot));
124
+ }
125
+
126
+ function valueAtPath(currentState, dottedPath) {
127
+ if (dottedPath === 'initialized') return isInitializedState(currentState);
128
+ const rootValue = valueFromPath(currentState, dottedPath);
129
+ if (rootValue !== undefined) return rootValue;
130
+ if (!currentState || !currentState.tiers || dottedPath.startsWith('tiers.')) return undefined;
131
+ return valueFromPath(currentState.tiers, dottedPath);
132
+ }
133
+
97
134
  /**
98
135
  * Async state.json reader for callers that should not block the event loop.
99
136
  *
@@ -132,14 +169,18 @@ function normalizeForWrite(state) {
132
169
  *
133
170
  * @param {string} projectRoot
134
171
  * @param {GodpowersState} state
172
+ * @param {{ refreshViews?: boolean, onStateViewWarning?: Function }} [opts]
135
173
  * @returns {GodpowersState}
136
174
  */
137
- function write(projectRoot, state) {
175
+ function write(projectRoot, state, opts = {}) {
138
176
  normalizeForWrite(state);
139
177
 
140
178
  const file = statePath(projectRoot);
141
179
  fs.mkdirSync(path.dirname(file), { recursive: true });
142
180
  atomic.writeJsonAtomic(file, state);
181
+ if (opts.refreshViews !== false) {
182
+ stateViews.writeAll(projectRoot, state, { onWarning: opts.onStateViewWarning });
183
+ }
143
184
  return state;
144
185
  }
145
186
 
@@ -148,11 +189,16 @@ function write(projectRoot, state) {
148
189
  *
149
190
  * @param {string} projectRoot
150
191
  * @param {GodpowersState} state
192
+ * @param {{ refreshViews?: boolean, onStateViewWarning?: Function }} [opts]
151
193
  * @returns {Promise<GodpowersState>}
152
194
  */
153
- async function writeAsync(projectRoot, state) {
195
+ async function writeAsync(projectRoot, state, opts = {}) {
154
196
  normalizeForWrite(state);
155
- return asyncFs.writeJson(statePath(projectRoot), state);
197
+ await asyncFs.writeJson(statePath(projectRoot), state);
198
+ if (opts.refreshViews !== false) {
199
+ await stateViews.writeAllAsync(projectRoot, state, { onWarning: opts.onStateViewWarning });
200
+ }
201
+ return state;
156
202
  }
157
203
 
158
204
  function createInitialState(projectName, opts = {}) {
@@ -376,6 +422,9 @@ module.exports = {
376
422
  hashFile,
377
423
  detectDrift,
378
424
  statePath,
425
+ isInitialized,
426
+ isInitializedState,
427
+ valueAtPath,
379
428
  orderedSubSteps,
380
429
  progressSummary,
381
430
  renderProgressLine,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "godpowers",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "AI-powered development system: 112 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"
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  prerequisites:
10
10
  required:
11
- - check: file:.godpowers/PROGRESS.md
11
+ - check: state:initialized == true
12
12
  auto-complete: /god-init
13
13
  human-required: true
14
14
 
@@ -21,7 +21,7 @@ execution:
21
21
  secondary-spawns: [god-executor, god-spec-reviewer, god-quality-reviewer]
22
22
  writes:
23
23
  - .godpowers/build/PLAN.md
24
- - .godpowers/build/STATE.md
24
+ - .godpowers/state.json
25
25
  - source code
26
26
 
27
27
  standards:
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  prerequisites:
10
10
  required:
11
- - check: file:.godpowers/PROGRESS.md
11
+ - check: state:initialized == true
12
12
  reason: "/god-context needs an initialized project"
13
13
  auto-complete: /god-init
14
14
 
@@ -19,7 +19,9 @@ execution:
19
19
  spawns: [god-deploy-engineer]
20
20
  context: fresh
21
21
  writes:
22
- - .godpowers/deploy/STATE.md
22
+ - .godpowers/state.json
23
+ - deploy config
24
+ - .godpowers/deploy/WAITING-FOR-EXTERNAL-ACCESS.md when needed
23
25
 
24
26
  standards:
25
27
  substitution-test: true
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  prerequisites:
10
10
  required:
11
- - check: file:.godpowers/PROGRESS.md
11
+ - check: state:initialized == true
12
12
  reason: "/god-design needs an initialized project"
13
13
  auto-complete: /god-init
14
14
  - check: file:.godpowers/prd/PRD.md
@@ -37,7 +37,7 @@ execution:
37
37
  writes:
38
38
  - DESIGN.md
39
39
  - PRODUCT.md
40
- - .godpowers/design/STATE.md
40
+ - .godpowers/state.json
41
41
 
42
42
  standards:
43
43
  have-nots:
@@ -22,7 +22,10 @@ execution:
22
22
  spawns: [god-launch-strategist]
23
23
  context: fresh
24
24
  writes:
25
- - .godpowers/launch/STATE.md
25
+ - .godpowers/state.json
26
+ - landing copy
27
+ - OG cards
28
+ - launch runbook
26
29
 
27
30
  standards:
28
31
  substitution-test: true
@@ -27,7 +27,6 @@ execution:
27
27
  - .godpowers/arch/ARCH.md
28
28
  - .godpowers/roadmap/ROADMAP.md
29
29
  - .godpowers/stack/DECISION.md
30
- - .godpowers/build/STATE.md
31
30
  - .godpowers/state.json
32
31
  - .planning/GODPOWERS-SYNC.md
33
32
  - .legacy-planning/GODPOWERS-SYNC.md
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  prerequisites:
10
10
  required:
11
- - check: file:.godpowers/PROGRESS.md OR mode-A-greenfield
11
+ - check: state:initialized == true OR mode-A-greenfield
12
12
  auto-complete: /god-init
13
13
  human-required: true
14
14
  - check: safe-sync-clear
@@ -19,7 +19,10 @@ execution:
19
19
  spawns: [god-observability-engineer]
20
20
  context: fresh
21
21
  writes:
22
- - .godpowers/observe/STATE.md
22
+ - .godpowers/state.json
23
+ - alert definitions
24
+ - dashboard definitions
25
+ - runbooks
23
26
 
24
27
  standards:
25
28
  substitution-test: true
@@ -8,7 +8,7 @@ metadata:
8
8
 
9
9
  prerequisites:
10
10
  required:
11
- - check: file:.godpowers/PROGRESS.md
11
+ - check: state:initialized == true
12
12
  auto-complete: /god-init
13
13
  human-required: true
14
14
  recommended: