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
package/lib/pillars.js
CHANGED
|
@@ -96,8 +96,7 @@ const ARTIFACT_PILLAR_MAP = [
|
|
|
96
96
|
{ pattern: /(^|\/)stack\/DECISION\.md$/i, pillars: ['stack'] },
|
|
97
97
|
{ pattern: /(^|\/)roadmap\/ROADMAP\.md$/i, pillars: ['context', 'quality'] },
|
|
98
98
|
{ pattern: /(^|\/)build\/PLAN\.md$/i, pillars: ['quality', 'repo'] },
|
|
99
|
-
{ pattern:
|
|
100
|
-
{ pattern: /(^|\/)observe\/STATE\.md$/i, pillars: ['observe'] },
|
|
99
|
+
{ pattern: /^\.godpowers\/state\.json$/i, pillars: ['context', 'deploy', 'observe'] },
|
|
101
100
|
{ pattern: /(^|\/)harden\/FINDINGS\.md$/i, pillars: ['security', 'auth'] },
|
|
102
101
|
{ pattern: /(^|\/)design\/DESIGN\.md$/i, pillars: ['ui'] },
|
|
103
102
|
{ pattern: /(^|\/)design\/PRODUCT\.md$/i, pillars: ['context', 'ui'] }
|
|
@@ -109,8 +108,7 @@ const GODPOWERS_ARTIFACTS = [
|
|
|
109
108
|
'.godpowers/stack/DECISION.md',
|
|
110
109
|
'.godpowers/roadmap/ROADMAP.md',
|
|
111
110
|
'.godpowers/build/PLAN.md',
|
|
112
|
-
'.godpowers/
|
|
113
|
-
'.godpowers/observe/STATE.md',
|
|
111
|
+
'.godpowers/state.json',
|
|
114
112
|
'.godpowers/harden/FINDINGS.md',
|
|
115
113
|
'.godpowers/design/DESIGN.md',
|
|
116
114
|
'.godpowers/design/PRODUCT.md'
|
package/lib/recipes.js
CHANGED
|
@@ -112,6 +112,14 @@ function evaluateStateCondition(condition, projectRoot) {
|
|
|
112
112
|
if (cond.startsWith('file:')) {
|
|
113
113
|
return fs.existsSync(path.join(projectRoot, cond.slice(5).trim()));
|
|
114
114
|
}
|
|
115
|
+
if (cond.startsWith('state:')) {
|
|
116
|
+
const m = cond.slice(6).trim().match(/^([\w.-]+)\s*==\s*(.+)$/);
|
|
117
|
+
if (!m) return true;
|
|
118
|
+
const [, dottedPath, expected] = m;
|
|
119
|
+
const s = state.read(projectRoot);
|
|
120
|
+
const actual = state.valueAtPath(s, dottedPath);
|
|
121
|
+
return actual === expected.trim() || actual === parseValue(expected.trim());
|
|
122
|
+
}
|
|
115
123
|
if (cond.startsWith('lifecycle-phase ==')) {
|
|
116
124
|
const expected = cond.split('==')[1].trim();
|
|
117
125
|
const s = state.read(projectRoot);
|
|
@@ -131,6 +139,14 @@ function evaluateStateCondition(condition, projectRoot) {
|
|
|
131
139
|
return true;
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
function parseValue(value) {
|
|
143
|
+
if (value === 'true') return true;
|
|
144
|
+
if (value === 'false') return false;
|
|
145
|
+
if (value === 'null') return null;
|
|
146
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
/**
|
|
135
151
|
* Suggest top recipes based on current project state alone (no intent text).
|
|
136
152
|
*/
|
package/lib/router.js
CHANGED
|
@@ -119,11 +119,7 @@ function evaluateCheck(check, projectRoot) {
|
|
|
119
119
|
if (!match) return false;
|
|
120
120
|
const [, dottedPath, expected] = match;
|
|
121
121
|
const s = state.read(projectRoot);
|
|
122
|
-
|
|
123
|
-
const actual = dottedPath.split('.').reduce((acc, k) => {
|
|
124
|
-
if (!acc || k === '__proto__' || k === 'constructor' || k === 'prototype') return undefined;
|
|
125
|
-
return acc[k];
|
|
126
|
-
}, s.tiers || s);
|
|
122
|
+
const actual = state.valueAtPath(s, dottedPath);
|
|
127
123
|
return actual === expected || actual === parseValue(expected);
|
|
128
124
|
}
|
|
129
125
|
|
package/lib/source-sync.js
CHANGED
|
@@ -131,7 +131,7 @@ function progressLines(projectRoot) {
|
|
|
131
131
|
lines.push(summarizeArtifact(projectRoot, '.godpowers/arch/ARCH.md', 'Architecture'));
|
|
132
132
|
lines.push(summarizeArtifact(projectRoot, '.godpowers/roadmap/ROADMAP.md', 'Roadmap'));
|
|
133
133
|
lines.push(summarizeArtifact(projectRoot, '.godpowers/stack/DECISION.md', 'Stack'));
|
|
134
|
-
lines.push(summarizeArtifact(projectRoot, '.godpowers/
|
|
134
|
+
lines.push(summarizeArtifact(projectRoot, '.godpowers/state.json', 'Godpowers state'));
|
|
135
135
|
lines.push('');
|
|
136
136
|
lines.push('## Return Path');
|
|
137
137
|
lines.push('');
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State advance CLI mutation.
|
|
3
|
+
*
|
|
4
|
+
* Moves one tracked Godpowers step to a new status through state.json,
|
|
5
|
+
* cooperative locking, and generated markdown view refresh.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const stateStore = require('./state');
|
|
9
|
+
const stateLock = require('./state-lock');
|
|
10
|
+
const stateViews = require('./state-views');
|
|
11
|
+
|
|
12
|
+
const VALID_STATUSES = new Set([
|
|
13
|
+
'pending',
|
|
14
|
+
'in-flight',
|
|
15
|
+
'done',
|
|
16
|
+
'skipped',
|
|
17
|
+
'imported',
|
|
18
|
+
'failed',
|
|
19
|
+
're-invoked',
|
|
20
|
+
'not-required'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function statusList() {
|
|
24
|
+
return Array.from(VALID_STATUSES).join(', ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function check(status, reason, detail = {}) {
|
|
28
|
+
return {
|
|
29
|
+
id: detail.id || reason,
|
|
30
|
+
status,
|
|
31
|
+
reason: detail.message || reason,
|
|
32
|
+
artifact: detail.artifact || '.godpowers/state.json'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resultFailure(projectRoot, id, reason, opts = {}) {
|
|
37
|
+
return {
|
|
38
|
+
command: 'state advance',
|
|
39
|
+
verdict: 'fail',
|
|
40
|
+
project: projectRoot,
|
|
41
|
+
step: opts.step || null,
|
|
42
|
+
status: opts.status || null,
|
|
43
|
+
previousStatus: null,
|
|
44
|
+
updated: null,
|
|
45
|
+
warnings: opts.warnings || [],
|
|
46
|
+
checks: [check('fail', reason, { id })],
|
|
47
|
+
findings: [{ id, severity: 'error', artifact: '.godpowers/state.json', reason }],
|
|
48
|
+
summary: { updated: false, state: '.godpowers/state.json', views: [stateViews.PROGRESS_VIEW_PATH] }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resultPass(projectRoot, target, previousStatus, status, updated, warnings, views) {
|
|
53
|
+
return {
|
|
54
|
+
command: 'state advance',
|
|
55
|
+
verdict: 'pass',
|
|
56
|
+
project: projectRoot,
|
|
57
|
+
step: {
|
|
58
|
+
tierKey: target.tierKey,
|
|
59
|
+
subStepKey: target.subStepKey,
|
|
60
|
+
tierLabel: target.tierLabel,
|
|
61
|
+
subStepLabel: target.subStepLabel,
|
|
62
|
+
ordinal: target.ordinal
|
|
63
|
+
},
|
|
64
|
+
status,
|
|
65
|
+
previousStatus,
|
|
66
|
+
updated,
|
|
67
|
+
warnings,
|
|
68
|
+
checks: [check('pass', `advanced ${target.tierKey}.${target.subStepKey} to ${status}`, { id: 'state-advanced' })],
|
|
69
|
+
findings: [],
|
|
70
|
+
summary: { updated: true, state: '.godpowers/state.json', views: views || [stateViews.PROGRESS_VIEW_PATH] }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeStep(rawStep) {
|
|
75
|
+
return String(rawStep || '').trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function splitStep(rawStep) {
|
|
79
|
+
const step = normalizeStep(rawStep);
|
|
80
|
+
const compound = step.match(/^(tier-\d+)[.:/](.+)$/);
|
|
81
|
+
if (compound) {
|
|
82
|
+
return { tierKey: compound[1], subStepKey: compound[2] };
|
|
83
|
+
}
|
|
84
|
+
if (/^\d+$/.test(step)) {
|
|
85
|
+
return { ordinal: Number(step) };
|
|
86
|
+
}
|
|
87
|
+
return { subStepKey: step };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveStep(currentState, rawStep) {
|
|
91
|
+
const token = splitStep(rawStep);
|
|
92
|
+
const steps = stateStore.orderedSubSteps(currentState);
|
|
93
|
+
|
|
94
|
+
if (token.ordinal != null) {
|
|
95
|
+
return steps.find(step => step.ordinal === token.ordinal) || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (token.tierKey) {
|
|
99
|
+
return steps.find(step =>
|
|
100
|
+
step.tierKey === token.tierKey && step.subStepKey === token.subStepKey
|
|
101
|
+
) || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const matches = steps.filter(step => step.subStepKey === token.subStepKey);
|
|
105
|
+
if (matches.length === 1) return matches[0];
|
|
106
|
+
if (matches.length > 1) {
|
|
107
|
+
const err = new Error(`ambiguous step: ${rawStep}`);
|
|
108
|
+
err.code = 'AMBIGUOUS_STEP';
|
|
109
|
+
err.matches = matches.map(step => `${step.tierKey}.${step.subStepKey}`);
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateRequest(projectRoot, opts) {
|
|
116
|
+
if (!projectRoot) {
|
|
117
|
+
return resultFailure(projectRoot, 'project-required', 'state advance requires --project=<path>', opts);
|
|
118
|
+
}
|
|
119
|
+
if (!opts.step) {
|
|
120
|
+
return resultFailure(projectRoot, 'step-required', 'state advance requires --step=<step>', opts);
|
|
121
|
+
}
|
|
122
|
+
if (!opts.status) {
|
|
123
|
+
return resultFailure(projectRoot, 'status-required', 'state advance requires --status=<status>', opts);
|
|
124
|
+
}
|
|
125
|
+
if (!VALID_STATUSES.has(opts.status)) {
|
|
126
|
+
return resultFailure(projectRoot, 'status-invalid', `invalid status "${opts.status}"; expected one of ${statusList()}`, opts);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function advance(projectRoot, opts = {}) {
|
|
132
|
+
const request = {
|
|
133
|
+
step: normalizeStep(opts.step),
|
|
134
|
+
status: String(opts.status || '').trim(),
|
|
135
|
+
holder: opts.holder || `godpowers-state-advance:${process.pid}`,
|
|
136
|
+
ttlMs: opts.ttlMs
|
|
137
|
+
};
|
|
138
|
+
const invalid = validateRequest(projectRoot, request);
|
|
139
|
+
if (invalid) return invalid;
|
|
140
|
+
|
|
141
|
+
const initialState = stateStore.read(projectRoot);
|
|
142
|
+
if (!initialState) {
|
|
143
|
+
return resultFailure(projectRoot, 'state-missing', 'state.json not found', request);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let target;
|
|
147
|
+
try {
|
|
148
|
+
target = resolveStep(initialState, request.step);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e.code === 'AMBIGUOUS_STEP') {
|
|
151
|
+
return resultFailure(
|
|
152
|
+
projectRoot,
|
|
153
|
+
'step-ambiguous',
|
|
154
|
+
`ambiguous step "${request.step}"; use one of ${e.matches.join(', ')}`,
|
|
155
|
+
request
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
if (!target) {
|
|
161
|
+
return resultFailure(projectRoot, 'step-not-found', `tracked step not found: ${request.step}`, request);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const lockResult = stateLock.acquire(projectRoot, {
|
|
165
|
+
holder: request.holder,
|
|
166
|
+
scope: `${target.tierKey}.${target.subStepKey}`,
|
|
167
|
+
ttlMs: request.ttlMs
|
|
168
|
+
});
|
|
169
|
+
if (!lockResult.acquired) {
|
|
170
|
+
return resultFailure(
|
|
171
|
+
projectRoot,
|
|
172
|
+
'lock-unavailable',
|
|
173
|
+
`state lock unavailable: held by ${lockResult.holder} on ${lockResult.scope}`,
|
|
174
|
+
request
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const warnings = [];
|
|
179
|
+
try {
|
|
180
|
+
const currentState = stateStore.read(projectRoot);
|
|
181
|
+
const freshTarget = resolveStep(currentState, request.step);
|
|
182
|
+
if (!freshTarget) {
|
|
183
|
+
return resultFailure(projectRoot, 'step-not-found', `tracked step not found after lock: ${request.step}`, request);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tier = currentState.tiers[freshTarget.tierKey] || {};
|
|
187
|
+
const current = tier[freshTarget.subStepKey] || {};
|
|
188
|
+
const previousStatus = current.status || 'pending';
|
|
189
|
+
const updated = opts.now || new Date().toISOString();
|
|
190
|
+
currentState.tiers[freshTarget.tierKey][freshTarget.subStepKey] = {
|
|
191
|
+
...current,
|
|
192
|
+
status: request.status,
|
|
193
|
+
updated
|
|
194
|
+
};
|
|
195
|
+
stateStore.write(projectRoot, currentState, {
|
|
196
|
+
onStateViewWarning: warning => warnings.push(warning)
|
|
197
|
+
});
|
|
198
|
+
return resultPass(
|
|
199
|
+
projectRoot,
|
|
200
|
+
freshTarget,
|
|
201
|
+
previousStatus,
|
|
202
|
+
request.status,
|
|
203
|
+
updated,
|
|
204
|
+
warnings,
|
|
205
|
+
stateViews.viewPathsForState(currentState)
|
|
206
|
+
);
|
|
207
|
+
} finally {
|
|
208
|
+
stateLock.release(projectRoot, request.holder);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function render(result) {
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push('Godpowers State Advance');
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push(`Verdict: ${result.verdict}`);
|
|
217
|
+
if (result.verdict === 'pass') {
|
|
218
|
+
const step = result.step;
|
|
219
|
+
lines.push(`Step: ${step.tierKey}.${step.subStepKey}`);
|
|
220
|
+
lines.push(`Status: ${result.previousStatus} to ${result.status}`);
|
|
221
|
+
lines.push(`Updated: ${result.updated}`);
|
|
222
|
+
for (const warning of result.warnings || []) {
|
|
223
|
+
lines.push(`Warning: ${warning}`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
for (const finding of result.findings || []) {
|
|
227
|
+
lines.push(`Error: ${finding.reason}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function exitCode(result) {
|
|
234
|
+
return result && result.verdict === 'pass' ? 0 : 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
VALID_STATUSES,
|
|
239
|
+
advance,
|
|
240
|
+
render,
|
|
241
|
+
exitCode,
|
|
242
|
+
resolveStep,
|
|
243
|
+
statusList
|
|
244
|
+
};
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
writeLockState(projectRoot, s);
|
|
180
184
|
return { reclaimed: true, previousHolder: prev };
|
|
181
185
|
}
|
|
182
186
|
|