godpowers 2.5.2 → 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.
- package/CHANGELOG.md +40 -0
- package/README.md +49 -19
- package/RELEASE.md +41 -29
- package/SKILL.md +46 -48
- package/agents/god-deploy-engineer.md +2 -2
- package/agents/god-designer.md +3 -2
- package/agents/god-greenfieldifier.md +2 -4
- package/agents/god-launch-strategist.md +4 -5
- package/agents/god-observability-engineer.md +5 -5
- package/agents/god-reconciler.md +10 -4
- package/agents/god-retrospective.md +1 -1
- package/agents/god-updater.md +5 -5
- package/bin/install.js +9 -1
- package/fixtures/gate/build-pass/.godpowers/state.json +33 -0
- package/lib/README.md +2 -0
- package/lib/artifact-map.js +15 -3
- package/lib/cli-dispatch.js +51 -1
- package/lib/context-writer.js +4 -4
- package/lib/gate.js +107 -9
- package/lib/host-capabilities.js +53 -3
- package/lib/installer-args.js +25 -0
- package/lib/mcp-info.js +93 -0
- package/lib/pillars.js +2 -4
- package/lib/recipes.js +16 -0
- package/lib/router.js +1 -5
- package/lib/source-sync.js +1 -1
- package/lib/state-advance.js +244 -0
- package/lib/state-lock.js +8 -4
- package/lib/state-views.js +460 -0
- package/lib/state.js +52 -3
- package/package.json +7 -2
- package/routing/god-audit.yaml +1 -1
- package/routing/god-build.yaml +1 -1
- package/routing/god-context.yaml +1 -1
- package/routing/god-deploy.yaml +3 -1
- package/routing/god-design.yaml +2 -2
- package/routing/god-launch.yaml +4 -1
- package/routing/god-migrate.yaml +0 -1
- package/routing/god-mode.yaml +1 -1
- package/routing/god-observe.yaml +4 -1
- package/routing/god-prd.yaml +1 -1
- package/routing/god-reconcile.yaml +2 -5
- package/routing/god-sync.yaml +1 -1
- package/routing/recipes/returning-after-break.yaml +1 -1
- package/schema/state.v1.json +68 -1
- package/skills/god-arch.md +1 -1
- package/skills/god-build.md +6 -4
- package/skills/god-deploy.md +16 -14
- package/skills/god-design.md +3 -3
- package/skills/god-fast.md +2 -2
- package/skills/god-feature.md +1 -1
- package/skills/god-harden.md +3 -3
- package/skills/god-hotfix.md +1 -1
- package/skills/god-init.md +14 -10
- package/skills/god-launch.md +14 -12
- package/skills/god-lifecycle.md +2 -1
- package/skills/god-mode.md +5 -4
- package/skills/god-next.md +2 -1
- package/skills/god-observe.md +15 -13
- package/skills/god-pause-work.md +2 -2
- package/skills/god-prd.md +5 -4
- package/skills/god-quick.md +1 -1
- package/skills/god-repo.md +1 -1
- package/skills/god-resume-work.md +5 -4
- package/skills/god-roadmap-update.md +1 -1
- package/skills/god-roadmap.md +1 -1
- package/skills/god-rollback.md +1 -1
- package/skills/god-skip.md +2 -2
- package/skills/god-stack.md +1 -1
- package/skills/god-standards.md +1 -1
- package/skills/god-status.md +6 -5
- package/skills/god-story.md +1 -1
- package/skills/god-sync.md +2 -2
- package/workflows/bluefield-arc.yaml +2 -4
- 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
|
-
|
|
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.
|
|
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"
|
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
"test:linter": "node scripts/test-artifact-linter.js",
|
|
23
23
|
"test:diff": "node scripts/test-artifact-diff.js",
|
|
24
24
|
"test:e2e": "node tests/integration/full-arc.test.js",
|
|
25
|
+
"test:mcp": "npm --workspace @godpowers/mcp test",
|
|
25
26
|
"coverage": "c8 --reporter=text --reporter=lcov node scripts/run-tests.js",
|
|
26
27
|
"coverage:lib": "c8 --include=lib/**/*.js --check-coverage --lines 90 --reporter=text node scripts/run-tests.js",
|
|
27
28
|
"test:audit": "npm audit --omit=dev && git diff --check && npm run test:surface",
|
|
28
29
|
"pack:check": "node scripts/check-package-contents.js",
|
|
29
|
-
"
|
|
30
|
+
"pack:mcp:check": "npm --workspace @godpowers/mcp run pack:check",
|
|
31
|
+
"release:check": "npm run coverage:lib && npm run test:audit && npm run pack:check && npm run pack:mcp:check",
|
|
30
32
|
"lint": "node scripts/static-check.js"
|
|
31
33
|
},
|
|
34
|
+
"workspaces": [
|
|
35
|
+
"packages/mcp"
|
|
36
|
+
],
|
|
32
37
|
"keywords": [
|
|
33
38
|
"ai",
|
|
34
39
|
"ai-agent",
|
package/routing/god-audit.yaml
CHANGED
package/routing/god-build.yaml
CHANGED
package/routing/god-context.yaml
CHANGED
package/routing/god-deploy.yaml
CHANGED
|
@@ -19,7 +19,9 @@ execution:
|
|
|
19
19
|
spawns: [god-deploy-engineer]
|
|
20
20
|
context: fresh
|
|
21
21
|
writes:
|
|
22
|
-
- .godpowers/
|
|
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
|
package/routing/god-design.yaml
CHANGED
|
@@ -8,7 +8,7 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
prerequisites:
|
|
10
10
|
required:
|
|
11
|
-
- check:
|
|
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/
|
|
40
|
+
- .godpowers/state.json
|
|
41
41
|
|
|
42
42
|
standards:
|
|
43
43
|
have-nots:
|
package/routing/god-launch.yaml
CHANGED
package/routing/god-migrate.yaml
CHANGED
package/routing/god-mode.yaml
CHANGED
package/routing/god-observe.yaml
CHANGED