@polymorphism-tech/morph-spec 2.3.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 (166) hide show
  1. package/CLAUDE.md +446 -1730
  2. package/README.md +515 -516
  3. package/bin/morph-spec.js +366 -294
  4. package/bin/task-manager.js +429 -368
  5. package/bin/validate.js +369 -268
  6. package/content/.claude/commands/morph-apply.md +221 -158
  7. package/content/.claude/commands/morph-deploy.md +529 -0
  8. package/content/.claude/commands/morph-preflight.md +227 -0
  9. package/content/.claude/commands/morph-proposal.md +122 -101
  10. package/content/.claude/commands/morph-status.md +86 -86
  11. package/content/.claude/commands/morph-troubleshoot.md +122 -0
  12. package/content/.claude/skills/infra/azure-deploy-specialist.md +699 -0
  13. package/content/.claude/skills/level-0-meta/README.md +7 -0
  14. package/content/.claude/skills/level-0-meta/code-review.md +226 -0
  15. package/content/.claude/skills/level-0-meta/morph-checklist.md +117 -0
  16. package/content/.claude/skills/level-0-meta/simulation-checklist.md +77 -0
  17. package/content/.claude/skills/level-1-workflows/README.md +7 -0
  18. package/content/.claude/skills/level-1-workflows/morph-replicate.md +213 -0
  19. package/content/.claude/{commands/morph-clarify.md → skills/level-1-workflows/phase-clarify.md} +131 -184
  20. package/content/.claude/{commands/morph-design.md → skills/level-1-workflows/phase-design.md} +213 -275
  21. package/content/.claude/skills/level-1-workflows/phase-setup.md +106 -0
  22. package/content/.claude/skills/level-1-workflows/phase-tasks.md +164 -0
  23. package/content/.claude/{commands/morph-uiux.md → skills/level-1-workflows/phase-uiux.md} +169 -211
  24. package/content/.claude/skills/level-2-domains/README.md +14 -0
  25. package/content/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +192 -0
  26. package/content/.claude/skills/{specialists → level-2-domains/architecture}/po-pm-advisor.md +197 -197
  27. package/content/.claude/skills/level-2-domains/architecture/standards-architect.md +156 -0
  28. package/content/.claude/skills/level-2-domains/backend/dotnet-senior.md +287 -0
  29. package/content/.claude/skills/level-2-domains/backend/ef-modeler.md +113 -0
  30. package/content/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +126 -0
  31. package/content/.claude/skills/level-2-domains/backend/ms-agent-expert.md +109 -0
  32. package/content/.claude/skills/level-2-domains/frontend/blazor-builder.md +210 -0
  33. package/content/.claude/skills/level-2-domains/frontend/nextjs-expert.md +154 -0
  34. package/content/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +191 -0
  35. package/content/.claude/skills/{specialists → level-2-domains/infrastructure}/azure-architect.md +142 -142
  36. package/content/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +126 -0
  37. package/content/.claude/skills/level-2-domains/infrastructure/container-specialist.md +131 -0
  38. package/content/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +119 -0
  39. package/content/.claude/skills/level-2-domains/integrations/asaas-financial.md +130 -0
  40. package/content/.claude/skills/level-2-domains/integrations/azure-identity.md +142 -0
  41. package/content/.claude/skills/level-2-domains/integrations/clerk-auth.md +108 -0
  42. package/content/.claude/skills/level-2-domains/integrations/resend-email.md +119 -0
  43. package/content/.claude/skills/level-2-domains/quality/code-analyzer.md +235 -0
  44. package/content/.claude/skills/level-2-domains/quality/testing-specialist.md +126 -0
  45. package/content/.claude/skills/level-3-technologies/README.md +7 -0
  46. package/content/.claude/skills/level-4-patterns/README.md +7 -0
  47. package/content/.claude/skills/specialists/prompt-engineer.md +189 -0
  48. package/content/.claude/skills/specialists/seo-growth-hacker.md +320 -0
  49. package/content/.morph/config/agents.json +762 -242
  50. package/content/.morph/config/config.template.json +122 -108
  51. package/content/.morph/docs/workflows/design-impl.md +37 -0
  52. package/content/.morph/docs/workflows/enforcement-pipeline.md +668 -0
  53. package/content/.morph/docs/workflows/fast-track.md +29 -0
  54. package/content/.morph/docs/workflows/full-morph.md +76 -0
  55. package/content/.morph/docs/workflows/standard.md +44 -0
  56. package/content/.morph/docs/workflows/ui-refresh.md +39 -0
  57. package/content/.morph/examples/scheduled-reports/decisions.md +158 -0
  58. package/content/.morph/examples/scheduled-reports/proposal.md +95 -0
  59. package/content/.morph/examples/scheduled-reports/spec.md +267 -0
  60. package/content/.morph/hooks/README.md +348 -239
  61. package/content/.morph/hooks/pre-commit-agents.sh +24 -24
  62. package/content/.morph/hooks/task-completed.js +73 -0
  63. package/content/.morph/hooks/teammate-idle.js +68 -0
  64. package/content/.morph/schemas/tasks.schema.json +220 -0
  65. package/content/.morph/standards/agent-framework-blazor-ui.md +359 -0
  66. package/content/.morph/standards/agent-framework-production.md +410 -0
  67. package/content/.morph/standards/agent-framework-setup.md +413 -453
  68. package/content/.morph/standards/agent-framework-workflows.md +349 -0
  69. package/content/.morph/standards/agent-teams-workflow.md +474 -0
  70. package/content/.morph/standards/architecture.md +325 -325
  71. package/content/.morph/standards/azure.md +605 -379
  72. package/content/.morph/standards/dotnet10-migration.md +520 -494
  73. package/content/.morph/templates/CONTEXT-FEATURE.md +276 -0
  74. package/content/.morph/templates/CONTEXT.md +170 -0
  75. package/content/.morph/templates/agent.cs +163 -172
  76. package/content/.morph/templates/clarify-questions.md +159 -0
  77. package/content/.morph/templates/contracts/Commands.cs +74 -0
  78. package/content/.morph/templates/contracts/Entities.cs +25 -0
  79. package/content/.morph/templates/contracts/Queries.cs +74 -0
  80. package/content/.morph/templates/contracts/README.md +74 -0
  81. package/content/.morph/templates/decisions.md +123 -106
  82. package/content/.morph/templates/infra/azure-pipelines-deploy.yml +480 -0
  83. package/content/.morph/templates/infra/deploy-checklist.md +426 -0
  84. package/content/.morph/templates/proposal.md +141 -155
  85. package/content/.morph/templates/recap.md +94 -105
  86. package/content/.morph/templates/simulation.md +353 -0
  87. package/content/.morph/templates/spec.md +149 -148
  88. package/content/.morph/templates/state.template.json +222 -222
  89. package/content/.morph/templates/tasks.md +257 -235
  90. package/content/.morph/templates/ui-components.md +362 -276
  91. package/content/CLAUDE.md +150 -442
  92. package/detectors/structure-detector.js +245 -250
  93. package/docs/README.md +144 -149
  94. package/docs/getting-started.md +301 -302
  95. package/docs/installation.md +361 -361
  96. package/docs/validation-checklist.md +265 -266
  97. package/package.json +80 -80
  98. package/src/commands/advance-phase.js +266 -0
  99. package/src/commands/analyze-blazor-concurrency.js +193 -0
  100. package/src/commands/deploy.js +780 -0
  101. package/src/commands/detect-agents.js +167 -0
  102. package/src/commands/doctor.js +356 -280
  103. package/src/commands/generate-context.js +40 -0
  104. package/src/commands/init.js +258 -245
  105. package/src/commands/lint-fluent.js +352 -0
  106. package/src/commands/rollback-phase.js +185 -0
  107. package/src/commands/session-summary.js +291 -0
  108. package/src/commands/task.js +78 -75
  109. package/src/commands/troubleshoot.js +222 -0
  110. package/src/commands/update.js +192 -159
  111. package/src/commands/validate-blazor-state.js +210 -0
  112. package/src/commands/validate-blazor.js +156 -0
  113. package/src/commands/validate-css.js +84 -0
  114. package/src/commands/validate-phase.js +221 -0
  115. package/src/lib/blazor-concurrency-analyzer.js +288 -0
  116. package/src/lib/blazor-state-validator.js +291 -0
  117. package/src/lib/blazor-validator.js +374 -0
  118. package/src/lib/complexity-analyzer.js +441 -292
  119. package/src/lib/context-generator.js +513 -0
  120. package/src/lib/continuous-validator.js +421 -440
  121. package/src/lib/css-validator.js +352 -0
  122. package/src/lib/decision-constraint-loader.js +109 -0
  123. package/src/lib/design-system-detector.js +187 -0
  124. package/src/lib/design-system-scaffolder.js +299 -0
  125. package/src/lib/hook-executor.js +256 -0
  126. package/src/lib/recap-generator.js +205 -0
  127. package/src/lib/spec-validator.js +258 -0
  128. package/src/lib/standards-context-injector.js +287 -0
  129. package/src/lib/state-manager.js +397 -340
  130. package/src/lib/team-orchestrator.js +322 -0
  131. package/src/lib/troubleshoot-grep.js +194 -0
  132. package/src/lib/troubleshoot-index.js +144 -0
  133. package/src/lib/validation-runner.js +283 -0
  134. package/src/lib/validators/contract-compliance-validator.js +273 -0
  135. package/src/lib/validators/design-system-validator.js +231 -0
  136. package/src/utils/file-copier.js +187 -139
  137. package/content/.claude/commands/morph-costs.md +0 -206
  138. package/content/.claude/commands/morph-setup.md +0 -100
  139. package/content/.claude/commands/morph-tasks.md +0 -319
  140. package/content/.claude/skills/infra/bicep-architect.md +0 -419
  141. package/content/.claude/skills/infra/container-specialist.md +0 -437
  142. package/content/.claude/skills/infra/devops-engineer.md +0 -405
  143. package/content/.claude/skills/integrations/asaas-financial.md +0 -333
  144. package/content/.claude/skills/integrations/azure-identity.md +0 -309
  145. package/content/.claude/skills/integrations/clerk-auth.md +0 -290
  146. package/content/.claude/skills/specialists/ai-system-architect.md +0 -604
  147. package/content/.claude/skills/specialists/cost-guardian.md +0 -110
  148. package/content/.claude/skills/specialists/ef-modeler.md +0 -211
  149. package/content/.claude/skills/specialists/hangfire-orchestrator.md +0 -255
  150. package/content/.claude/skills/specialists/ms-agent-expert.md +0 -263
  151. package/content/.claude/skills/specialists/standards-architect.md +0 -78
  152. package/content/.claude/skills/specialists/ui-ux-designer.md +0 -1100
  153. package/content/.claude/skills/stacks/dotnet-blazor.md +0 -606
  154. package/content/.claude/skills/stacks/dotnet-nextjs.md +0 -402
  155. package/content/.claude/skills/stacks/shopify.md +0 -445
  156. package/content/.morph/config/azure-pricing.json +0 -70
  157. package/content/.morph/config/azure-pricing.schema.json +0 -50
  158. package/content/.morph/hooks/pre-commit-costs.sh +0 -91
  159. package/docs/api/cost-calculator.js.html +0 -513
  160. package/docs/api/design-system-generator.js.html +0 -382
  161. package/docs/api/global.html +0 -5263
  162. package/docs/api/index.html +0 -96
  163. package/docs/api/state-manager.js.html +0 -423
  164. package/src/commands/cost.js +0 -181
  165. package/src/commands/update-pricing.js +0 -206
  166. package/src/lib/cost-calculator.js +0 -429
@@ -1,340 +1,397 @@
1
- /**
2
- * MORPH-SPEC State Manager Library
3
- *
4
- * Manages state.json for tracking features, progress, agents, and checkpoints.
5
- * Used both by CLI commands and internal automation.
6
- */
7
-
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
- import { join, dirname } from 'path';
10
-
11
- const STATE_FILE_NAME = '.morph/state.json';
12
-
13
- // ============================================================================
14
- // Core Functions
15
- // ============================================================================
16
-
17
- /**
18
- * Get the state file path (looks in current working directory)
19
- */
20
- export function getStatePath() {
21
- return join(process.cwd(), STATE_FILE_NAME);
22
- }
23
-
24
- /**
25
- * Check if state file exists
26
- */
27
- export function stateExists() {
28
- return existsSync(getStatePath());
29
- }
30
-
31
- /**
32
- * Load state from disk
33
- * @param {boolean} throwOnError - If false, returns null instead of throwing
34
- * @returns {Object|null} State object or null
35
- */
36
- export function loadState(throwOnError = true) {
37
- const statePath = getStatePath();
38
-
39
- if (!existsSync(statePath)) {
40
- if (throwOnError) {
41
- throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
42
- }
43
- return null;
44
- }
45
-
46
- try {
47
- const content = readFileSync(statePath, 'utf8');
48
- return JSON.parse(content);
49
- } catch (err) {
50
- if (throwOnError) {
51
- throw new Error(`Failed to parse state.json: ${err.message}`);
52
- }
53
- return null;
54
- }
55
- }
56
-
57
- /**
58
- * Save state to disk
59
- * @param {Object} state - State object to save
60
- */
61
- export function saveState(state) {
62
- state.metadata = state.metadata || {};
63
- state.metadata.lastUpdated = new Date().toISOString();
64
-
65
- const statePath = getStatePath();
66
- const stateDir = dirname(statePath);
67
-
68
- // Ensure .morph directory exists
69
- if (!existsSync(stateDir)) {
70
- mkdirSync(stateDir, { recursive: true });
71
- }
72
-
73
- writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
74
- }
75
-
76
- /**
77
- * Initialize new state file
78
- * @param {Object} options - Options
79
- * @param {boolean} options.force - Overwrite existing file
80
- * @param {string} options.projectName - Project name
81
- * @param {string} options.projectType - Project type (e.g., 'blazor-server')
82
- * @returns {Object} Initial state
83
- */
84
- export function initState(options = {}) {
85
- const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
86
-
87
- if (stateExists() && !force) {
88
- throw new Error('State file already exists. Use force=true to overwrite.');
89
- }
90
-
91
- const initialState = {
92
- version: "2.1.1",
93
- project: {
94
- name: projectName,
95
- type: projectType,
96
- createdAt: new Date().toISOString(),
97
- updatedAt: new Date().toISOString()
98
- },
99
- features: {},
100
- metadata: {
101
- totalFeatures: 0,
102
- completedFeatures: 0,
103
- totalCostEstimated: 0,
104
- totalTimeSpent: 0,
105
- lastUpdated: new Date().toISOString()
106
- }
107
- };
108
-
109
- saveState(initialState);
110
- return initialState;
111
- }
112
-
113
- // ============================================================================
114
- // Feature Operations
115
- // ============================================================================
116
-
117
- /**
118
- * Get feature from state
119
- * @param {string} featureName - Feature name
120
- * @returns {Object|null} Feature object or null
121
- */
122
- export function getFeature(featureName) {
123
- const state = loadState();
124
- return state.features[featureName] || null;
125
- }
126
-
127
- /**
128
- * Create or get feature with default structure
129
- * @param {string} featureName - Feature name
130
- * @returns {Object} Feature object
131
- */
132
- function ensureFeature(featureName) {
133
- const state = loadState();
134
-
135
- if (!state.features[featureName]) {
136
- state.features[featureName] = {
137
- status: "draft",
138
- phase: "proposal",
139
- workflow: "auto", // auto | fast-track | standard | full-morph
140
- createdAt: new Date().toISOString(),
141
- updatedAt: new Date().toISOString(),
142
- activeAgents: [],
143
- outputs: {
144
- proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
145
- spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
146
- contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
147
- tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
148
- uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
149
- uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
150
- uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
151
- uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
152
- decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
153
- recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
154
- },
155
- tasks: {
156
- total: 0,
157
- completed: 0,
158
- inProgress: 0,
159
- pending: 0
160
- },
161
- checkpoints: [],
162
- costs: {
163
- estimated: 0,
164
- approved: false,
165
- approvedBy: null,
166
- approvedAt: null
167
- }
168
- };
169
-
170
- state.metadata.totalFeatures++;
171
- saveState(state);
172
- }
173
-
174
- return state.features[featureName];
175
- }
176
-
177
- /**
178
- * Update feature property (supports nested keys like "tasks.completed")
179
- * @param {string} featureName - Feature name
180
- * @param {string} key - Property key (supports dot notation)
181
- * @param {any} value - Value to set
182
- */
183
- export function updateFeature(featureName, key, value) {
184
- ensureFeature(featureName);
185
- const state = loadState(); // Load AFTER ensuring feature exists
186
-
187
- const keys = key.split('.');
188
- let target = state.features[featureName];
189
-
190
- for (let i = 0; i < keys.length - 1; i++) {
191
- if (!target[keys[i]]) {
192
- target[keys[i]] = {};
193
- }
194
- target = target[keys[i]];
195
- }
196
-
197
- const finalKey = keys[keys.length - 1];
198
- target[finalKey] = value;
199
- state.features[featureName].updatedAt = new Date().toISOString();
200
-
201
- saveState(state);
202
- }
203
-
204
- /**
205
- * Update multiple feature properties at once
206
- * @param {string} featureName - Feature name
207
- * @param {Object} updates - Object with key-value pairs to update
208
- */
209
- export function updateFeatureMultiple(featureName, updates) {
210
- ensureFeature(featureName);
211
- const state = loadState(); // Load AFTER ensuring feature exists
212
-
213
- for (const [key, value] of Object.entries(updates)) {
214
- const keys = key.split('.');
215
- let target = state.features[featureName];
216
-
217
- for (let i = 0; i < keys.length - 1; i++) {
218
- if (!target[keys[i]]) {
219
- target[keys[i]] = {};
220
- }
221
- target = target[keys[i]];
222
- }
223
-
224
- const finalKey = keys[keys.length - 1];
225
- target[finalKey] = value;
226
- }
227
-
228
- state.features[featureName].updatedAt = new Date().toISOString();
229
- saveState(state);
230
- }
231
-
232
- /**
233
- * Add checkpoint to feature
234
- * @param {string} featureName - Feature name
235
- * @param {string} note - Checkpoint note
236
- * @returns {Object} Checkpoint object
237
- */
238
- export function addCheckpoint(featureName, note) {
239
- ensureFeature(featureName);
240
- const state = loadState();
241
- const feature = state.features[featureName];
242
-
243
- const checkpoint = {
244
- timestamp: new Date().toISOString(),
245
- phase: feature.phase,
246
- completedTasks: feature.tasks.completed,
247
- note: note
248
- };
249
-
250
- feature.checkpoints.push(checkpoint);
251
- feature.updatedAt = new Date().toISOString();
252
-
253
- saveState(state);
254
- return checkpoint;
255
- }
256
-
257
- /**
258
- * Add agent to feature
259
- * @param {string} featureName - Feature name
260
- * @param {string} agentId - Agent ID
261
- * @returns {boolean} True if added, false if already exists
262
- */
263
- export function addAgent(featureName, agentId) {
264
- ensureFeature(featureName);
265
- const state = loadState(); // Load AFTER ensuring feature exists
266
-
267
- if (!state.features[featureName].activeAgents.includes(agentId)) {
268
- state.features[featureName].activeAgents.push(agentId);
269
- state.features[featureName].updatedAt = new Date().toISOString();
270
- saveState(state);
271
- return true;
272
- }
273
-
274
- return false;
275
- }
276
-
277
- /**
278
- * Remove agent from feature
279
- * @param {string} featureName - Feature name
280
- * @param {string} agentId - Agent ID
281
- * @returns {boolean} True if removed, false if not found
282
- */
283
- export function removeAgent(featureName, agentId) {
284
- const state = loadState();
285
-
286
- if (!state.features[featureName]) {
287
- throw new Error(`Feature '${featureName}' not found.`);
288
- }
289
-
290
- const index = state.features[featureName].activeAgents.indexOf(agentId);
291
- if (index > -1) {
292
- state.features[featureName].activeAgents.splice(index, 1);
293
- state.features[featureName].updatedAt = new Date().toISOString();
294
- saveState(state);
295
- return true;
296
- }
297
-
298
- return false;
299
- }
300
-
301
- /**
302
- * Mark output as created
303
- * @param {string} featureName - Feature name
304
- * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
305
- */
306
- export function markOutput(featureName, outputType) {
307
- ensureFeature(featureName);
308
- const state = loadState();
309
-
310
- if (!state.features[featureName].outputs[outputType]) {
311
- throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem, uiMockups, uiComponents, uiFlows, decisions, recap`);
312
- }
313
-
314
- state.features[featureName].outputs[outputType].created = true;
315
- state.features[featureName].updatedAt = new Date().toISOString();
316
-
317
- saveState(state);
318
- }
319
-
320
- /**
321
- * List all features
322
- * @returns {Array} Array of [featureName, featureObject] tuples
323
- */
324
- export function listFeatures() {
325
- const state = loadState();
326
- return Object.entries(state.features);
327
- }
328
-
329
- /**
330
- * Get project summary
331
- * @returns {Object} Summary with metadata
332
- */
333
- export function getSummary() {
334
- const state = loadState();
335
- return {
336
- project: state.project,
337
- metadata: state.metadata,
338
- featuresCount: Object.keys(state.features).length
339
- };
340
- }
1
+ /**
2
+ * MORPH-SPEC State Manager Library
3
+ *
4
+ * Manages state.json for tracking features, progress, agents, and checkpoints.
5
+ * Used both by CLI commands and internal automation.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+
11
+ const STATE_FILE_NAME = '.morph/state.json';
12
+
13
+ // ============================================================================
14
+ // Core Functions
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Get the state file path (looks in current working directory)
19
+ */
20
+ export function getStatePath() {
21
+ return join(process.cwd(), STATE_FILE_NAME);
22
+ }
23
+
24
+ /**
25
+ * Check if state file exists
26
+ */
27
+ export function stateExists() {
28
+ return existsSync(getStatePath());
29
+ }
30
+
31
+ /**
32
+ * Load state from disk
33
+ * @param {boolean} throwOnError - If false, returns null instead of throwing
34
+ * @returns {Object|null} State object or null
35
+ */
36
+ export function loadState(throwOnError = true) {
37
+ const statePath = getStatePath();
38
+
39
+ if (!existsSync(statePath)) {
40
+ if (throwOnError) {
41
+ throw new Error(`State file not found: ${statePath}\nRun 'morph-spec state init' first.`);
42
+ }
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const content = readFileSync(statePath, 'utf8');
48
+ return JSON.parse(content);
49
+ } catch (err) {
50
+ if (throwOnError) {
51
+ throw new Error(`Failed to parse state.json: ${err.message}`);
52
+ }
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Save state to disk
59
+ * @param {Object} state - State object to save
60
+ */
61
+ export function saveState(state) {
62
+ state.metadata = state.metadata || {};
63
+ state.metadata.lastUpdated = new Date().toISOString();
64
+
65
+ const statePath = getStatePath();
66
+ const stateDir = dirname(statePath);
67
+
68
+ // Ensure .morph directory exists
69
+ if (!existsSync(stateDir)) {
70
+ mkdirSync(stateDir, { recursive: true });
71
+ }
72
+
73
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
74
+ }
75
+
76
+ /**
77
+ * Initialize new state file
78
+ * @param {Object} options - Options
79
+ * @param {boolean} options.force - Overwrite existing file
80
+ * @param {string} options.projectName - Project name
81
+ * @param {string} options.projectType - Project type (e.g., 'blazor-server')
82
+ * @returns {Object} Initial state
83
+ */
84
+ export function initState(options = {}) {
85
+ const { force = false, projectName = '{PROJECT_NAME}', projectType = 'blazor-server' } = options;
86
+
87
+ if (stateExists() && !force) {
88
+ throw new Error('State file already exists. Use force=true to overwrite.');
89
+ }
90
+
91
+ const initialState = {
92
+ version: "2.1.1",
93
+ project: {
94
+ name: projectName,
95
+ type: projectType,
96
+ createdAt: new Date().toISOString(),
97
+ updatedAt: new Date().toISOString()
98
+ },
99
+ features: {},
100
+ metadata: {
101
+ totalFeatures: 0,
102
+ completedFeatures: 0,
103
+ totalTimeSpent: 0,
104
+ lastUpdated: new Date().toISOString()
105
+ }
106
+ };
107
+
108
+ saveState(initialState);
109
+ return initialState;
110
+ }
111
+
112
+ // ============================================================================
113
+ // Feature Operations
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Get feature from state
118
+ * @param {string} featureName - Feature name
119
+ * @returns {Object|null} Feature object or null
120
+ */
121
+ export function getFeature(featureName) {
122
+ const state = loadState();
123
+ return state.features[featureName] || null;
124
+ }
125
+
126
+ /**
127
+ * Create or get feature with default structure
128
+ * @param {string} featureName - Feature name
129
+ * @returns {Object} Feature object
130
+ */
131
+ function ensureFeature(featureName) {
132
+ const state = loadState();
133
+
134
+ if (!state.features[featureName]) {
135
+ state.features[featureName] = {
136
+ status: "draft",
137
+ phase: "proposal",
138
+ workflow: "auto", // auto | fast-track | standard | full-morph
139
+ createdAt: new Date().toISOString(),
140
+ updatedAt: new Date().toISOString(),
141
+ activeAgents: [],
142
+ outputs: {
143
+ proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
144
+ spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
145
+ contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
146
+ tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
147
+ uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
148
+ uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
149
+ uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
150
+ uiFlows: { created: false, path: `.morph/project/outputs/${featureName}/ui-flows.md` },
151
+ decisions: { created: false, path: `.morph/project/outputs/${featureName}/decisions.md` },
152
+ recap: { created: false, path: `.morph/project/outputs/${featureName}/recap.md` }
153
+ },
154
+ tasks: {
155
+ total: 0,
156
+ completed: 0,
157
+ inProgress: 0,
158
+ pending: 0
159
+ },
160
+ checkpoints: []
161
+ };
162
+
163
+ state.metadata.totalFeatures++;
164
+ saveState(state);
165
+ }
166
+
167
+ return state.features[featureName];
168
+ }
169
+
170
+ /**
171
+ * Update feature property (supports nested keys like "tasks.completed")
172
+ * @param {string} featureName - Feature name
173
+ * @param {string} key - Property key (supports dot notation)
174
+ * @param {any} value - Value to set
175
+ */
176
+ export function updateFeature(featureName, key, value) {
177
+ ensureFeature(featureName);
178
+ const state = loadState(); // Load AFTER ensuring feature exists
179
+
180
+ const keys = key.split('.');
181
+ let target = state.features[featureName];
182
+
183
+ for (let i = 0; i < keys.length - 1; i++) {
184
+ if (!target[keys[i]]) {
185
+ target[keys[i]] = {};
186
+ }
187
+ target = target[keys[i]];
188
+ }
189
+
190
+ const finalKey = keys[keys.length - 1];
191
+ target[finalKey] = value;
192
+ state.features[featureName].updatedAt = new Date().toISOString();
193
+
194
+ saveState(state);
195
+ }
196
+
197
+ /**
198
+ * Update multiple feature properties at once
199
+ * @param {string} featureName - Feature name
200
+ * @param {Object} updates - Object with key-value pairs to update
201
+ */
202
+ export function updateFeatureMultiple(featureName, updates) {
203
+ ensureFeature(featureName);
204
+ const state = loadState(); // Load AFTER ensuring feature exists
205
+
206
+ for (const [key, value] of Object.entries(updates)) {
207
+ const keys = key.split('.');
208
+ let target = state.features[featureName];
209
+
210
+ for (let i = 0; i < keys.length - 1; i++) {
211
+ if (!target[keys[i]]) {
212
+ target[keys[i]] = {};
213
+ }
214
+ target = target[keys[i]];
215
+ }
216
+
217
+ const finalKey = keys[keys.length - 1];
218
+ target[finalKey] = value;
219
+ }
220
+
221
+ state.features[featureName].updatedAt = new Date().toISOString();
222
+ saveState(state);
223
+ }
224
+
225
+ /**
226
+ * Add checkpoint to feature
227
+ * @param {string} featureName - Feature name
228
+ * @param {string} note - Checkpoint note
229
+ * @returns {Object} Checkpoint object
230
+ */
231
+ export function addCheckpoint(featureName, note) {
232
+ ensureFeature(featureName);
233
+ const state = loadState();
234
+ const feature = state.features[featureName];
235
+
236
+ const checkpoint = {
237
+ timestamp: new Date().toISOString(),
238
+ phase: feature.phase,
239
+ completedTasks: feature.tasks.completed,
240
+ note: note
241
+ };
242
+
243
+ feature.checkpoints.push(checkpoint);
244
+ feature.updatedAt = new Date().toISOString();
245
+
246
+ saveState(state);
247
+ return checkpoint;
248
+ }
249
+
250
+ /**
251
+ * Add agent to feature
252
+ * @param {string} featureName - Feature name
253
+ * @param {string} agentId - Agent ID
254
+ * @returns {boolean} True if added, false if already exists
255
+ */
256
+ export function addAgent(featureName, agentId) {
257
+ ensureFeature(featureName);
258
+ const state = loadState(); // Load AFTER ensuring feature exists
259
+
260
+ if (!state.features[featureName].activeAgents.includes(agentId)) {
261
+ state.features[featureName].activeAgents.push(agentId);
262
+ state.features[featureName].updatedAt = new Date().toISOString();
263
+ saveState(state);
264
+ return true;
265
+ }
266
+
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Remove agent from feature
272
+ * @param {string} featureName - Feature name
273
+ * @param {string} agentId - Agent ID
274
+ * @returns {boolean} True if removed, false if not found
275
+ */
276
+ export function removeAgent(featureName, agentId) {
277
+ const state = loadState();
278
+
279
+ if (!state.features[featureName]) {
280
+ throw new Error(`Feature '${featureName}' not found.`);
281
+ }
282
+
283
+ const index = state.features[featureName].activeAgents.indexOf(agentId);
284
+ if (index > -1) {
285
+ state.features[featureName].activeAgents.splice(index, 1);
286
+ state.features[featureName].updatedAt = new Date().toISOString();
287
+ saveState(state);
288
+ return true;
289
+ }
290
+
291
+ return false;
292
+ }
293
+
294
+ /**
295
+ * Mark output as created
296
+ * @param {string} featureName - Feature name
297
+ * @param {string} outputType - Output type (proposal, spec, contracts, etc.)
298
+ */
299
+ export function markOutput(featureName, outputType) {
300
+ ensureFeature(featureName);
301
+ const state = loadState();
302
+
303
+ if (!state.features[featureName].outputs[outputType]) {
304
+ throw new Error(`Output type '${outputType}' not valid. Valid types: proposal, spec, contracts, tasks, uiDesignSystem, uiMockups, uiComponents, uiFlows, decisions, recap`);
305
+ }
306
+
307
+ state.features[featureName].outputs[outputType].created = true;
308
+ state.features[featureName].updatedAt = new Date().toISOString();
309
+
310
+ // If marking tasks output, try to sync task count from state tasks array
311
+ if (outputType === 'tasks') {
312
+ syncTasksCount(state.features[featureName]);
313
+ }
314
+
315
+ saveState(state);
316
+ }
317
+
318
+ /**
319
+ * Sync tasks count from tasks array (if exists)
320
+ * @param {Object} feature - Feature object
321
+ */
322
+ function syncTasksCount(feature) {
323
+ // If feature has tasks array (schema 3.0), count them
324
+ if (feature.tasks && Array.isArray(feature.tasks)) {
325
+ const tasks = feature.tasks;
326
+ feature.progress = {
327
+ total: tasks.length,
328
+ completed: tasks.filter(t => t.status === 'completed').length,
329
+ inProgress: tasks.filter(t => t.status === 'in_progress').length,
330
+ pending: tasks.filter(t => t.status === 'pending').length,
331
+ percentage: tasks.length > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / tasks.length) * 100) : 0
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Skip a phase and record it
338
+ * @param {string} featureName - Feature name
339
+ * @param {string} phase - Phase to skip
340
+ * @param {string} reason - Reason for skipping
341
+ */
342
+ export function skipPhase(featureName, phase, reason = '') {
343
+ ensureFeature(featureName);
344
+ const state = loadState();
345
+
346
+ if (!state.features[featureName].skippedPhases) {
347
+ state.features[featureName].skippedPhases = [];
348
+ }
349
+
350
+ // Don't add duplicates
351
+ const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
352
+ if (!existing) {
353
+ state.features[featureName].skippedPhases.push({
354
+ phase,
355
+ reason,
356
+ timestamp: new Date().toISOString()
357
+ });
358
+ }
359
+
360
+ state.features[featureName].updatedAt = new Date().toISOString();
361
+ saveState(state);
362
+ }
363
+
364
+ /**
365
+ * Get skipped phases for a feature
366
+ * @param {string} featureName - Feature name
367
+ * @returns {Array} Array of skipped phases
368
+ */
369
+ export function getSkippedPhases(featureName) {
370
+ const state = loadState();
371
+ if (!state.features[featureName]) {
372
+ return [];
373
+ }
374
+ return state.features[featureName].skippedPhases || [];
375
+ }
376
+
377
+ /**
378
+ * List all features
379
+ * @returns {Array} Array of [featureName, featureObject] tuples
380
+ */
381
+ export function listFeatures() {
382
+ const state = loadState();
383
+ return Object.entries(state.features);
384
+ }
385
+
386
+ /**
387
+ * Get project summary
388
+ * @returns {Object} Summary with metadata
389
+ */
390
+ export function getSummary() {
391
+ const state = loadState();
392
+ return {
393
+ project: state.project,
394
+ metadata: state.metadata,
395
+ featuresCount: Object.keys(state.features).length
396
+ };
397
+ }