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.
- package/CHANGELOG.md +45 -0
- package/README.md +46 -34
- package/RELEASE.md +49 -37
- 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 +11 -6
- package/extensions/data-pack/manifest.yaml +1 -1
- package/extensions/data-pack/package.json +1 -1
- package/extensions/launch-pack/manifest.yaml +1 -1
- package/extensions/launch-pack/package.json +1 -1
- package/extensions/security-pack/manifest.yaml +1 -1
- package/extensions/security-pack/package.json +1 -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 +39 -1
- package/lib/command-families.js +13 -8
- package/lib/context-writer.js +4 -4
- package/lib/gate.js +107 -9
- package/lib/install-profiles.js +39 -13
- package/lib/installer-args.js +25 -1
- 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 +2 -2
- package/routing/god-audit.yaml +1 -1
- package/routing/god-build.yaml +1 -1
- package/routing/god-capture.yaml +41 -0
- package/routing/god-context.yaml +1 -1
- package/routing/god-deploy.yaml +3 -1
- package/routing/god-design.yaml +2 -2
- package/routing/god-extend.yaml +47 -0
- package/routing/god-fix.yaml +37 -0
- package/routing/god-launch.yaml +4 -1
- package/routing/god-lifecycle.yaml +1 -1
- package/routing/god-locate.yaml +1 -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-plan.yaml +45 -0
- package/routing/god-prd.yaml +1 -1
- package/routing/god-reconcile.yaml +2 -5
- package/routing/god-ship.yaml +39 -0
- 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-capture.md +45 -0
- package/skills/god-deploy.md +16 -14
- package/skills/god-design.md +3 -3
- package/skills/god-doctor.md +1 -1
- package/skills/god-extend.md +48 -0
- package/skills/god-fast.md +2 -2
- package/skills/god-feature.md +1 -1
- package/skills/god-fix.md +43 -0
- 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 +8 -1
- package/skills/god-locate.md +6 -0
- package/skills/god-mode.md +5 -4
- package/skills/god-observe.md +15 -13
- package/skills/god-pause-work.md +2 -2
- package/skills/god-plan.md +51 -0
- 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-check.md +1 -0
- 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-ship.md +44 -0
- 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 +14 -9
- package/skills/god-story.md +1 -1
- package/skills/god-sync.md +2 -2
- package/skills/god-version.md +1 -1
- package/workflows/bluefield-arc.yaml +2 -4
- package/workflows/brownfield-arc.yaml +2 -4
package/lib/gate.js
CHANGED
|
@@ -11,6 +11,7 @@ const path = require('path');
|
|
|
11
11
|
const artifactMap = require('./artifact-map');
|
|
12
12
|
const linter = require('./artifact-linter');
|
|
13
13
|
const router = require('./router');
|
|
14
|
+
const stateStore = require('./state');
|
|
14
15
|
|
|
15
16
|
function relToAbs(projectRoot, relPath) {
|
|
16
17
|
return path.join(projectRoot, relPath);
|
|
@@ -168,12 +169,108 @@ function extractCommandStatuses(text) {
|
|
|
168
169
|
return entries;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
function commandName(entry) {
|
|
173
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
174
|
+
const value = entry.command || entry.cmd || entry.name;
|
|
175
|
+
return value ? String(value).trim() : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeVerificationStatus(entry) {
|
|
179
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
180
|
+
const raw = entry.status || entry.result || entry.verdict;
|
|
181
|
+
if (raw) {
|
|
182
|
+
const text = String(raw).trim().toLowerCase();
|
|
183
|
+
if (/^(pass|passed|green|success|succeeded|ok)$/.test(text)) return 'pass';
|
|
184
|
+
if (/^(fail|failed|red|error)$/.test(text)) return 'fail';
|
|
185
|
+
}
|
|
186
|
+
if (Number.isInteger(entry.exitCode)) return entry.exitCode === 0 ? 'pass' : 'fail';
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stateVerificationCommands(subStep) {
|
|
191
|
+
if (!subStep || typeof subStep !== 'object') return [];
|
|
192
|
+
const verification = subStep.verification && typeof subStep.verification === 'object'
|
|
193
|
+
? subStep.verification
|
|
194
|
+
: {};
|
|
195
|
+
const commands = verification.commands ||
|
|
196
|
+
subStep.verificationCommands ||
|
|
197
|
+
subStep['verification-commands'] ||
|
|
198
|
+
[];
|
|
199
|
+
return Array.isArray(commands) ? commands : [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function commandsWithStatus(subStep, wantedStatus) {
|
|
203
|
+
const commands = [];
|
|
204
|
+
for (const entry of stateVerificationCommands(subStep)) {
|
|
205
|
+
const name = commandName(entry);
|
|
206
|
+
if (!name) continue;
|
|
207
|
+
if (normalizeVerificationStatus(entry) !== wantedStatus) continue;
|
|
208
|
+
if (!commands.includes(name)) commands.push(name);
|
|
209
|
+
}
|
|
210
|
+
return commands;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function checkStateStepEvidence(projectRoot, tier, result) {
|
|
214
|
+
const stepRef = artifactMap.stateStepForTier(tier);
|
|
215
|
+
if (!stepRef) return null;
|
|
216
|
+
|
|
217
|
+
const relPath = '.godpowers/state.json';
|
|
218
|
+
const currentState = stateStore.read(projectRoot);
|
|
219
|
+
if (!currentState) {
|
|
220
|
+
const finding = makeFinding(
|
|
221
|
+
`state:${tier}:missing`,
|
|
222
|
+
'error',
|
|
223
|
+
relPath,
|
|
224
|
+
`${tier} gate requires structured state evidence in state.json.`
|
|
225
|
+
);
|
|
226
|
+
result.findings.push(finding);
|
|
227
|
+
addFindingSummary(result.summary, finding.severity);
|
|
228
|
+
result.checks.push(makeCheck(`state:${tier}:status`, 'fail', relPath, finding.reason));
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tierState = currentState.tiers && currentState.tiers[stepRef.tierKey];
|
|
233
|
+
const subStep = tierState && tierState[stepRef.subStepKey];
|
|
234
|
+
if (!subStep) {
|
|
235
|
+
const finding = makeFinding(
|
|
236
|
+
`state:${tier}:step-missing`,
|
|
237
|
+
'error',
|
|
238
|
+
relPath,
|
|
239
|
+
`state.json does not record ${stepRef.tierKey}.${stepRef.subStepKey}.`
|
|
240
|
+
);
|
|
241
|
+
result.findings.push(finding);
|
|
242
|
+
addFindingSummary(result.summary, finding.severity);
|
|
243
|
+
result.checks.push(makeCheck(`state:${tier}:status`, 'fail', relPath, finding.reason));
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const status = subStep.status || 'pending';
|
|
248
|
+
const complete = stateStore.isCompleteStatus(status);
|
|
249
|
+
if (!complete) {
|
|
250
|
+
const finding = makeFinding(
|
|
251
|
+
`state:${tier}:incomplete`,
|
|
252
|
+
'error',
|
|
253
|
+
relPath,
|
|
254
|
+
`${stepRef.tierKey}.${stepRef.subStepKey} status is ${status}, expected a complete status.`
|
|
255
|
+
);
|
|
256
|
+
result.findings.push(finding);
|
|
257
|
+
addFindingSummary(result.summary, finding.severity);
|
|
258
|
+
}
|
|
259
|
+
result.checks.push(makeCheck(
|
|
260
|
+
`state:${tier}:status`,
|
|
261
|
+
complete ? 'pass' : 'fail',
|
|
262
|
+
relPath,
|
|
263
|
+
complete
|
|
264
|
+
? `${stepRef.tierKey}.${stepRef.subStepKey} records complete status ${status}.`
|
|
265
|
+
: `${stepRef.tierKey}.${stepRef.subStepKey} must be complete before this gate passes.`
|
|
266
|
+
));
|
|
267
|
+
return subStep;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function checkBuildEvidence(result, buildStep) {
|
|
271
|
+
const relPath = '.godpowers/state.json';
|
|
272
|
+
if (!buildStep) return;
|
|
273
|
+
const failedCommands = commandsWithStatus(buildStep, 'fail');
|
|
177
274
|
if (failedCommands.length > 0) {
|
|
178
275
|
const finding = makeFinding(
|
|
179
276
|
'build-verification-failed-command',
|
|
@@ -192,7 +289,7 @@ function checkBuildEvidence(projectRoot, result) {
|
|
|
192
289
|
result.summary.buildVerificationFailedCommands = failedCommands;
|
|
193
290
|
return;
|
|
194
291
|
}
|
|
195
|
-
const passedCommands =
|
|
292
|
+
const passedCommands = commandsWithStatus(buildStep, 'pass');
|
|
196
293
|
if (passedCommands.length === 0) {
|
|
197
294
|
const finding = makeFinding(
|
|
198
295
|
'build-verification-evidence',
|
|
@@ -214,7 +311,7 @@ function checkBuildEvidence(projectRoot, result) {
|
|
|
214
311
|
'build-verification-evidence',
|
|
215
312
|
'pass',
|
|
216
313
|
relPath,
|
|
217
|
-
`
|
|
314
|
+
`state.json records ${passedCommands.length} passed build verification command(s).`
|
|
218
315
|
));
|
|
219
316
|
result.summary.buildVerificationCommands = passedCommands;
|
|
220
317
|
}
|
|
@@ -280,7 +377,8 @@ function check(opts = {}) {
|
|
|
280
377
|
}
|
|
281
378
|
|
|
282
379
|
checkArtifacts(projectRoot, tier, artifacts, opts, result);
|
|
283
|
-
|
|
380
|
+
const stateStep = checkStateStepEvidence(projectRoot, tier, result);
|
|
381
|
+
if (tier === 'build') checkBuildEvidence(result, stateStep);
|
|
284
382
|
if (tier === 'harden') checkHardenCriticals(projectRoot, result);
|
|
285
383
|
return finalize(result);
|
|
286
384
|
}
|
package/lib/install-profiles.js
CHANGED
|
@@ -2,27 +2,33 @@ const COMMON = [
|
|
|
2
2
|
'god',
|
|
3
3
|
'god-help',
|
|
4
4
|
'god-version',
|
|
5
|
-
'god-
|
|
6
|
-
'god-status',
|
|
7
|
-
'god-progress',
|
|
8
|
-
'god-doctor',
|
|
9
|
-
'god-settings'
|
|
5
|
+
'god-status'
|
|
10
6
|
];
|
|
11
7
|
|
|
12
8
|
const PROFILE_SKILLS = {
|
|
13
9
|
core: [
|
|
14
10
|
...COMMON,
|
|
15
11
|
'god-init',
|
|
12
|
+
'god-plan',
|
|
16
13
|
'god-mode',
|
|
17
14
|
'god-build',
|
|
15
|
+
'god-fix',
|
|
18
16
|
'god-review',
|
|
17
|
+
'god-ship',
|
|
18
|
+
'god-audit',
|
|
19
|
+
'god-capture',
|
|
19
20
|
'god-sync',
|
|
20
|
-
'god-
|
|
21
|
-
'god-
|
|
21
|
+
'god-undo',
|
|
22
|
+
'god-extend'
|
|
22
23
|
],
|
|
23
24
|
builder: [
|
|
24
25
|
...COMMON,
|
|
26
|
+
'god-next',
|
|
27
|
+
'god-progress',
|
|
28
|
+
'god-doctor',
|
|
29
|
+
'god-settings',
|
|
25
30
|
'god-init',
|
|
31
|
+
'god-plan',
|
|
26
32
|
'god-mode',
|
|
27
33
|
'god-discuss',
|
|
28
34
|
'god-explore',
|
|
@@ -35,6 +41,7 @@ const PROFILE_SKILLS = {
|
|
|
35
41
|
'god-stack',
|
|
36
42
|
'god-repo',
|
|
37
43
|
'god-build',
|
|
44
|
+
'god-fix',
|
|
38
45
|
'god-add-tests',
|
|
39
46
|
'god-feature',
|
|
40
47
|
'god-story',
|
|
@@ -43,13 +50,28 @@ const PROFILE_SKILLS = {
|
|
|
43
50
|
'god-story-verify',
|
|
44
51
|
'god-story-close',
|
|
45
52
|
'god-review',
|
|
53
|
+
'god-ship',
|
|
54
|
+
'god-audit',
|
|
55
|
+
'god-capture',
|
|
46
56
|
'god-test-runtime',
|
|
47
57
|
'god-sync',
|
|
58
|
+
'god-undo',
|
|
59
|
+
'god-extend',
|
|
48
60
|
'god-quick',
|
|
49
61
|
'god-fast'
|
|
50
62
|
],
|
|
51
63
|
maintainer: [
|
|
52
64
|
...COMMON,
|
|
65
|
+
'god-next',
|
|
66
|
+
'god-progress',
|
|
67
|
+
'god-doctor',
|
|
68
|
+
'god-settings',
|
|
69
|
+
'god-audit',
|
|
70
|
+
'god-fix',
|
|
71
|
+
'god-ship',
|
|
72
|
+
'god-capture',
|
|
73
|
+
'god-undo',
|
|
74
|
+
'god-extend',
|
|
53
75
|
'god-hygiene',
|
|
54
76
|
'god-update-deps',
|
|
55
77
|
'god-docs',
|
|
@@ -61,7 +83,6 @@ const PROFILE_SKILLS = {
|
|
|
61
83
|
'god-agent-audit',
|
|
62
84
|
'god-context',
|
|
63
85
|
'god-context-scan',
|
|
64
|
-
'god-locate',
|
|
65
86
|
'god-scan',
|
|
66
87
|
'god-link',
|
|
67
88
|
'god-review-changes',
|
|
@@ -89,6 +110,12 @@ const PROFILE_SKILLS = {
|
|
|
89
110
|
],
|
|
90
111
|
suite: [
|
|
91
112
|
...COMMON,
|
|
113
|
+
'god-next',
|
|
114
|
+
'god-progress',
|
|
115
|
+
'god-doctor',
|
|
116
|
+
'god-settings',
|
|
117
|
+
'god-sync',
|
|
118
|
+
'god-undo',
|
|
92
119
|
'god-suite-init',
|
|
93
120
|
'god-suite-status',
|
|
94
121
|
'god-suite-sync',
|
|
@@ -96,7 +123,6 @@ const PROFILE_SKILLS = {
|
|
|
96
123
|
'god-suite-release',
|
|
97
124
|
'god-workstream',
|
|
98
125
|
'god-pr-branch',
|
|
99
|
-
'god-sync',
|
|
100
126
|
'god-reconcile',
|
|
101
127
|
'god-review',
|
|
102
128
|
'god-quick',
|
|
@@ -105,20 +131,20 @@ const PROFILE_SKILLS = {
|
|
|
105
131
|
};
|
|
106
132
|
|
|
107
133
|
const PROFILE_DESCRIPTIONS = {
|
|
108
|
-
core: 'front door, status,
|
|
109
|
-
builder: 'core plus planning
|
|
134
|
+
core: 'front door, status, verbs, and autonomous compatibility',
|
|
135
|
+
builder: 'core plus planning leaves, stories, and runtime verification',
|
|
110
136
|
maintainer: 'core plus hygiene, deps, docs, repair, automation, and extensions',
|
|
111
137
|
suite: 'core plus multi-repo suite and workstream coordination',
|
|
112
138
|
full: 'all shipped slash commands'
|
|
113
139
|
};
|
|
114
140
|
|
|
115
141
|
function normalizeProfiles(value) {
|
|
116
|
-
if (!value) return ['
|
|
142
|
+
if (!value) return ['core'];
|
|
117
143
|
const raw = String(value)
|
|
118
144
|
.split(',')
|
|
119
145
|
.map(part => part.trim().toLowerCase())
|
|
120
146
|
.filter(Boolean);
|
|
121
|
-
const profiles = raw.length > 0 ? raw : ['
|
|
147
|
+
const profiles = raw.length > 0 ? raw : ['core'];
|
|
122
148
|
for (const profile of profiles) {
|
|
123
149
|
if (profile !== 'full' && !PROFILE_SKILLS[profile]) {
|
|
124
150
|
throw new Error(`Unknown install profile: ${profile}`);
|
package/lib/installer-args.js
CHANGED
|
@@ -4,6 +4,7 @@ const { RUNTIMES } = require('./installer-runtimes');
|
|
|
4
4
|
const COMMANDS = new Set([
|
|
5
5
|
'status',
|
|
6
6
|
'next',
|
|
7
|
+
'state',
|
|
7
8
|
'quick-proof',
|
|
8
9
|
'mcp-info',
|
|
9
10
|
'automation-status',
|
|
@@ -20,6 +21,9 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
20
21
|
project: cwd,
|
|
21
22
|
json: false,
|
|
22
23
|
brief: false,
|
|
24
|
+
stateAction: null,
|
|
25
|
+
step: null,
|
|
26
|
+
status: null,
|
|
23
27
|
extensionName: null,
|
|
24
28
|
extensionOutput: cwd,
|
|
25
29
|
extensionSkill: null,
|
|
@@ -32,7 +36,7 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
32
36
|
all: false,
|
|
33
37
|
help: false,
|
|
34
38
|
uninstall: false,
|
|
35
|
-
profile: '
|
|
39
|
+
profile: 'core',
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -41,6 +45,10 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
41
45
|
opts.command = arg;
|
|
42
46
|
continue;
|
|
43
47
|
}
|
|
48
|
+
if (opts.command === 'state' && !opts.stateAction && !arg.startsWith('-')) {
|
|
49
|
+
opts.stateAction = arg;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
switch (arg) {
|
|
46
54
|
case '--json':
|
|
@@ -55,6 +63,18 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
55
63
|
i++;
|
|
56
64
|
}
|
|
57
65
|
break;
|
|
66
|
+
case '--step':
|
|
67
|
+
if (args[i + 1]) {
|
|
68
|
+
opts.step = args[i + 1];
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case '--status':
|
|
73
|
+
if (args[i + 1]) {
|
|
74
|
+
opts.status = args[i + 1];
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
58
78
|
case '--project':
|
|
59
79
|
if (args[i + 1]) {
|
|
60
80
|
opts.project = path.resolve(args[i + 1]);
|
|
@@ -104,6 +124,10 @@ function parseArgs(argv, cwd = process.cwd()) {
|
|
|
104
124
|
opts.extensionWorkflow = arg.slice('--workflow='.length);
|
|
105
125
|
} else if (arg.startsWith('--tier=')) {
|
|
106
126
|
opts.tier = arg.slice('--tier='.length);
|
|
127
|
+
} else if (arg.startsWith('--step=')) {
|
|
128
|
+
opts.step = arg.slice('--step='.length);
|
|
129
|
+
} else if (arg.startsWith('--status=')) {
|
|
130
|
+
opts.status = arg.slice('--status='.length);
|
|
107
131
|
} else if (arg.startsWith('--profile=')) {
|
|
108
132
|
opts.profile = arg.slice('--profile='.length);
|
|
109
133
|
} else if (arg.startsWith('--') && RUNTIMES[arg.slice(2)]) {
|
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
|
+
};
|