godpowers 2.6.0 → 3.0.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 (96) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +46 -34
  3. package/RELEASE.md +49 -37
  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 +11 -6
  14. package/extensions/data-pack/manifest.yaml +1 -1
  15. package/extensions/data-pack/package.json +1 -1
  16. package/extensions/launch-pack/manifest.yaml +1 -1
  17. package/extensions/launch-pack/package.json +1 -1
  18. package/extensions/security-pack/manifest.yaml +1 -1
  19. package/extensions/security-pack/package.json +1 -1
  20. package/fixtures/gate/build-pass/.godpowers/state.json +33 -0
  21. package/lib/README.md +2 -0
  22. package/lib/artifact-map.js +15 -3
  23. package/lib/cli-dispatch.js +39 -1
  24. package/lib/command-families.js +13 -8
  25. package/lib/context-writer.js +4 -4
  26. package/lib/gate.js +107 -9
  27. package/lib/install-profiles.js +39 -13
  28. package/lib/installer-args.js +25 -1
  29. package/lib/pillars.js +2 -4
  30. package/lib/recipes.js +16 -0
  31. package/lib/router.js +1 -5
  32. package/lib/source-sync.js +1 -1
  33. package/lib/state-advance.js +244 -0
  34. package/lib/state-lock.js +8 -4
  35. package/lib/state-views.js +460 -0
  36. package/lib/state.js +52 -3
  37. package/package.json +2 -2
  38. package/routing/god-audit.yaml +1 -1
  39. package/routing/god-build.yaml +1 -1
  40. package/routing/god-capture.yaml +41 -0
  41. package/routing/god-context.yaml +1 -1
  42. package/routing/god-deploy.yaml +3 -1
  43. package/routing/god-design.yaml +2 -2
  44. package/routing/god-extend.yaml +47 -0
  45. package/routing/god-fix.yaml +37 -0
  46. package/routing/god-launch.yaml +4 -1
  47. package/routing/god-lifecycle.yaml +1 -1
  48. package/routing/god-locate.yaml +1 -1
  49. package/routing/god-migrate.yaml +0 -1
  50. package/routing/god-mode.yaml +1 -1
  51. package/routing/god-observe.yaml +4 -1
  52. package/routing/god-plan.yaml +45 -0
  53. package/routing/god-prd.yaml +1 -1
  54. package/routing/god-reconcile.yaml +2 -5
  55. package/routing/god-ship.yaml +39 -0
  56. package/routing/god-sync.yaml +1 -1
  57. package/routing/recipes/returning-after-break.yaml +1 -1
  58. package/schema/state.v1.json +68 -1
  59. package/skills/god-arch.md +1 -1
  60. package/skills/god-build.md +6 -4
  61. package/skills/god-capture.md +45 -0
  62. package/skills/god-deploy.md +16 -14
  63. package/skills/god-design.md +3 -3
  64. package/skills/god-doctor.md +1 -1
  65. package/skills/god-extend.md +48 -0
  66. package/skills/god-fast.md +2 -2
  67. package/skills/god-feature.md +1 -1
  68. package/skills/god-fix.md +43 -0
  69. package/skills/god-harden.md +3 -3
  70. package/skills/god-hotfix.md +1 -1
  71. package/skills/god-init.md +14 -10
  72. package/skills/god-launch.md +14 -12
  73. package/skills/god-lifecycle.md +8 -1
  74. package/skills/god-locate.md +6 -0
  75. package/skills/god-mode.md +5 -4
  76. package/skills/god-observe.md +15 -13
  77. package/skills/god-pause-work.md +2 -2
  78. package/skills/god-plan.md +51 -0
  79. package/skills/god-prd.md +5 -4
  80. package/skills/god-quick.md +1 -1
  81. package/skills/god-repo.md +1 -1
  82. package/skills/god-resume-work.md +5 -4
  83. package/skills/god-roadmap-check.md +1 -0
  84. package/skills/god-roadmap-update.md +1 -1
  85. package/skills/god-roadmap.md +1 -1
  86. package/skills/god-rollback.md +1 -1
  87. package/skills/god-ship.md +44 -0
  88. package/skills/god-skip.md +2 -2
  89. package/skills/god-stack.md +1 -1
  90. package/skills/god-standards.md +1 -1
  91. package/skills/god-status.md +14 -9
  92. package/skills/god-story.md +1 -1
  93. package/skills/god-sync.md +2 -2
  94. package/skills/god-version.md +1 -1
  95. package/workflows/bluefield-arc.yaml +2 -4
  96. package/workflows/brownfield-arc.yaml +2 -4
package/lib/state-lock.js CHANGED
@@ -46,6 +46,10 @@ const state = require('./state');
46
46
 
47
47
  const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
48
48
 
49
+ function writeLockState(projectRoot, nextState) {
50
+ return state.write(projectRoot, nextState, { refreshViews: false });
51
+ }
52
+
49
53
  function nowIso(offsetMs) {
50
54
  const d = offsetMs ? new Date(Date.now() + offsetMs) : new Date();
51
55
  return d.toISOString();
@@ -102,7 +106,7 @@ function acquire(projectRoot, opts = {}) {
102
106
  (existing.scope === scope || existing.scope === 'all' || scope === 'all')) {
103
107
  existing.expires = nowIso(ttlMs);
104
108
  s.lock = existing;
105
- state.write(projectRoot, s);
109
+ writeLockState(projectRoot, s);
106
110
  return { acquired: true, lock: existing, reentrant: true };
107
111
  }
108
112
  if (scopesConflict(existing.scope || 'all', scope)) {
@@ -134,7 +138,7 @@ function acquire(projectRoot, opts = {}) {
134
138
  };
135
139
  const reclaimedFrom = existing && isStale(existing) ? existing.holder : null;
136
140
  s.lock = lock;
137
- state.write(projectRoot, s);
141
+ writeLockState(projectRoot, s);
138
142
  return {
139
143
  acquired: true,
140
144
  lock,
@@ -158,7 +162,7 @@ function release(projectRoot, holder) {
158
162
  return { released: false, reason: 'wrong-holder', heldBy: lock.holder };
159
163
  }
160
164
  s.lock = null;
161
- state.write(projectRoot, s);
165
+ writeLockState(projectRoot, s);
162
166
  return { released: true, releasedAt: nowIso() };
163
167
  }
164
168
 
@@ -176,7 +180,7 @@ function reclaim(projectRoot, holder) {
176
180
  }
177
181
  const prev = lock.holder;
178
182
  s.lock = null;
179
- state.write(projectRoot, s);
183
+ writeLockState(projectRoot, s);
180
184
  return { reclaimed: true, previousHolder: prev };
181
185
  }
182
186
 
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "godpowers",
3
- "version": "2.6.0",
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.",
3
+ "version": "3.0.0",
4
+ "description": "AI-powered development system: 117 slash commands and 40 specialist agents that take a project from raw idea to hardened production. Runs inside Claude Code, Codex, Cursor, Windsurf, Gemini, and 10+ other AI coding tools.",
5
5
  "bin": {
6
6
  "godpowers": "./bin/install.js"
7
7
  },
@@ -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:
@@ -0,0 +1,41 @@
1
+ apiVersion: godpowers/v1
2
+ kind: CommandRouting
3
+ metadata:
4
+ command: /god-capture
5
+ description: Capture verb dispatcher
6
+ tier: 0
7
+ family: capture
8
+
9
+ prerequisites:
10
+ required: []
11
+
12
+ execution:
13
+ spawns: [built-in]
14
+ context: fresh
15
+ reads:
16
+ - routing/god-note.yaml
17
+ - routing/god-add-todo.yaml
18
+ - routing/god-add-backlog.yaml
19
+ - routing/god-plant-seed.yaml
20
+ writes: []
21
+
22
+ dispatch:
23
+ note: /god-note
24
+ todo: /god-add-todo
25
+ backlog: /god-add-backlog
26
+ seed: /god-plant-seed
27
+
28
+ success-path:
29
+ next-recommended: varies
30
+ outcome:
31
+ type: contextual
32
+ label: Capture dispatch route
33
+ reason: The next route depends on whether the item is a note, todo, backlog item, or seed.
34
+ allowed-next: [/god-note, /god-add-todo, /god-add-backlog, /god-plant-seed]
35
+
36
+ failure-path:
37
+ on-error: /god-doctor
38
+
39
+ endoff:
40
+ state-update: no direct state update; selected leaf command owns state
41
+ events: [agent.start, decision.route, agent.end]
@@ -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