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/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 checkBuildEvidence(projectRoot, result) {
172
- const relPath = '.godpowers/build/STATE.md';
173
- const file = relToAbs(projectRoot, relPath);
174
- if (!fs.existsSync(file)) return;
175
- const text = fs.readFileSync(file, 'utf8');
176
- const failedCommands = extractFailedCommands(text);
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 = extractPassedCommands(text);
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
- `Build state records ${passedCommands.length} passed verification command(s).`
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
- if (tier === 'build') checkBuildEvidence(projectRoot, result);
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
  }
@@ -2,27 +2,33 @@ const COMMON = [
2
2
  'god',
3
3
  'god-help',
4
4
  'god-version',
5
- 'god-next',
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-quick',
21
- 'god-fast'
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, init, build, review, sync, quick edits',
109
- builder: 'core plus planning, design, stories, and runtime verification',
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 ['full'];
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 : ['full'];
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}`);
@@ -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: 'full',
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: /(^|\/)deploy\/STATE\.md$/i, pillars: ['deploy'] },
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/deploy/STATE.md',
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
- if (!s) return false;
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
 
@@ -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/build/STATE.md', 'Build state'));
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
+ };