@polymorphism-tech/morph-spec 7.4.0 → 7.6.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 (66) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +18 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +9 -4
  7. package/framework/MORPH.md +4 -4
  8. package/framework/agents.json +1 -1
  9. package/framework/commands/create-umbraco-lp.md +35 -0
  10. package/framework/commands/migrate-lp-to-umbraco.md +33 -0
  11. package/framework/commands/morph-archive.md +17 -27
  12. package/framework/commands/morph-preflight.md +10 -5
  13. package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
  14. package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
  15. package/framework/hooks/claude-code/core/state-sync.js +16 -48
  16. package/framework/hooks/shared/state-reader.js +74 -8
  17. package/framework/hooks/shared/state-writer.js +124 -0
  18. package/framework/rules/infrastructure-standards.md +4 -4
  19. package/framework/rules/morph-task-tracking.md +69 -49
  20. package/framework/rules/morph-workflow.md +192 -191
  21. package/framework/skills/core/create-umbraco-lp/SKILL.md +272 -0
  22. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +531 -0
  23. package/framework/skills/core/morph-apply/SKILL.md +15 -15
  24. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -5
  25. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  26. package/framework/skills/core/morph-implement/SKILL.md +5 -4
  27. package/framework/skills/core/morph-plan/SKILL.md +6 -6
  28. package/framework/skills/core/morph-proposal/SKILL.md +8 -7
  29. package/framework/skills/core/morph-review/SKILL.md +3 -3
  30. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  31. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  32. package/framework/standards/STANDARDS.json +80 -8
  33. package/framework/standards/ai-agents/durable-workflows-hitl.md +191 -0
  34. package/framework/standards/ai-agents/rag-custom-pgvector.md +246 -243
  35. package/framework/standards/ai-agents/setup.md +3 -0
  36. package/framework/standards/ai-agents/vector-data-extensions.md +170 -0
  37. package/framework/standards/ai-agents/workflows.md +190 -187
  38. package/framework/standards/infrastructure/docker/{easypanel-deploy.md → coolify-deploy.md} +49 -20
  39. package/framework/templates/README.md +7 -7
  40. package/framework/templates/REGISTRY.json +15 -17
  41. package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
  42. package/framework/templates/context/CONTEXT.md +181 -180
  43. package/framework/templates/docs/onboarding.md +122 -118
  44. package/framework/templates/docs/proposal.md +12 -2
  45. package/framework/templates/docs/spec.md +228 -228
  46. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +1 -1
  47. package/framework/templates/infrastructure/docker/coolify.template.json +49 -0
  48. package/framework/templates/infrastructure/github/README.md +6 -6
  49. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +33 -0
  50. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
  51. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
  52. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
  53. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
  54. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
  55. package/package.json +1 -1
  56. package/src/commands/project/update.js +415 -368
  57. package/src/commands/project/worktree.js +212 -210
  58. package/src/commands/state/advance.js +82 -0
  59. package/src/commands/state/archive.js +111 -0
  60. package/src/commands/validation/validate-feature.js +229 -105
  61. package/src/core/state/state-manager.js +238 -66
  62. package/src/dashboard/scanner/state-scanner.js +35 -8
  63. package/src/lib/generators/context-generator.js +12 -8
  64. package/src/utils/file-copier.js +7 -0
  65. package/framework/templates/infrastructure/docker/easypanel.template.json +0 -46
  66. package/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +0 -31
@@ -11,11 +11,14 @@
11
11
  *
12
12
  * Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
13
13
  * the plan contract is frozen, but per-task progress fields (status, outputs,
14
- * notes) may still change during the implement phase. For those files a Write
15
- * is allowed only when the semantic diff touches solely the mutable fields;
16
- * Edit is always blocked because the legitimate progress path is a full Write
17
- * via writeTasksJson(). This lets the implement phase record task status
18
- * without unfreezing the plan.
14
+ * notes) may still change during the implement phase. For those files BOTH a
15
+ * full Write and a targeted Edit are allowed — but only when the semantic diff
16
+ * touches solely the mutable fields. The Edit is simulated (old_string →
17
+ * new_string applied to the current file) and diffed exactly like a Write, so a
18
+ * one-field status flip via Edit is permitted while any change to the frozen
19
+ * plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
20
+ * This is the sanctioned progress path — the agent never has to rewrite the
21
+ * whole file nor reach for a Node script to record task status.
19
22
  *
20
23
  * Fail-open: exits 0 on any error.
21
24
  */
@@ -129,40 +132,39 @@ try {
129
132
  // that touches only those fields. Anything else stays blocked.
130
133
  if (mutableFields) {
131
134
  const toolName = getToolName(payload);
132
- const newContent = payload?.tool_input?.content;
133
-
134
- // Edit is never the legitimate progress path — progress is written as a
135
- // full file via writeTasksJson(). Block Edit; fall through to the message.
136
- if (toolName !== 'Write' || typeof newContent !== 'string') {
137
- logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
138
- block(
139
- `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
140
- `Progress updates (${mutableFields.join(', ')}) must be written as a full-file\n` +
141
- `Write, not an Edit. The plan contract (titles, dependencies, doneCriteria…)\n` +
142
- `is frozen. To change the plan itself, ask the user to revoke the\n` +
143
- `'${requiredGate}' gate in .morph/state.json, then re-present it for approval.`
144
- );
145
- }
146
135
 
147
- // Semantic diff: old = current file on disk, new = proposed content.
148
- // Any failure to read/parse → fail-open (pass), never a false block.
136
+ // Build the proposed document from either a full Write or a targeted Edit,
137
+ // then semantic-diff it against the current file on disk. Any failure to
138
+ // read / parse / apply → fail-open (pass), never a false block.
139
+ const absPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
149
140
  let oldDoc, newDoc;
150
141
  try {
151
- // filePath may already be absolute (Claude Code usually sends absolute
152
- // paths); only join against cwd when it is relative.
153
- const absPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
154
- oldDoc = JSON.parse(readFileSync(absPath, 'utf-8'));
155
- newDoc = JSON.parse(newContent);
142
+ const original = readFileSync(absPath, 'utf-8');
143
+ oldDoc = JSON.parse(original);
144
+
145
+ if (toolName === 'Edit') {
146
+ // Simulate the edit: apply old_string → new_string to the current file.
147
+ const oldStr = payload?.tool_input?.old_string;
148
+ const newStr = payload?.tool_input?.new_string;
149
+ if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
150
+ const edited = payload?.tool_input?.replace_all
151
+ ? original.split(oldStr).join(newStr)
152
+ : original.replace(oldStr, newStr);
153
+ newDoc = JSON.parse(edited);
154
+ } else {
155
+ const newContent = payload?.tool_input?.content;
156
+ if (typeof newContent !== 'string') throw new Error('no content');
157
+ newDoc = JSON.parse(newContent);
158
+ }
156
159
  } catch {
157
- // Cannot compare — fail-open rather than block a possibly-valid write.
158
160
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
159
161
  pass();
160
162
  }
161
163
 
162
164
  const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
163
165
  if (violations.length === 0) {
164
- // Only mutable progress fields changed — this is a legitimate
165
- // implement-phase update.
166
+ // Only mutable progress fields changed — a legitimate implement-phase
167
+ // update via Write OR Edit.
166
168
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
167
169
  pass();
168
170
  }
@@ -170,7 +172,7 @@ try {
170
172
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
171
173
  block(
172
174
  `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
173
- `This Write changes the frozen plan contract:\n` +
175
+ `This ${toolName} changes the frozen plan contract:\n` +
174
176
  violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
175
177
  `During the implement phase only these fields may change: ${mutableFields.join(', ')}.\n` +
176
178
  `To change the plan itself, ask the user to revoke the '${requiredGate}' gate\n` +
@@ -11,11 +11,9 @@
11
11
  * - AskUserQuestion answered with option starting "aprovado" → advance gate
12
12
  */
13
13
 
14
- import { readFileSync, existsSync, renameSync, writeFileSync } from 'fs';
15
- import { join } from 'path';
14
+ import { readFileSync } from 'fs';
16
15
  import { loadState, stateExists } from '../../shared/state-reader.js';
17
-
18
- const STATE_FILE = '.morph/state.json';
16
+ import { persistFeature, setGateApproved, createDefaultFeature } from '../../shared/state-writer.js';
19
17
 
20
18
  const PHASE_FOLDERS = {
21
19
  '0-proposal': 'proposal',
@@ -64,31 +62,12 @@ function extractFilename(filePath) {
64
62
  return normalized.split('/').pop();
65
63
  }
66
64
 
67
- /**
68
- * Atomic write — temp file then rename. Mirrors state-manager.js semantics.
69
- */
70
- function writeStateAtomic(cwd, state) {
71
- const path = join(cwd, STATE_FILE);
72
- const tmpPath = `${path}.tmp.${process.pid}`;
73
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
74
- renameSync(tmpPath, path);
75
- }
76
-
77
65
  function registerFeatureIfMissing(state, featureName) {
78
66
  if (!state.features) state.features = {};
79
67
  if (state.features[featureName]) return false;
80
68
 
81
- const now = new Date().toISOString();
82
- state.features[featureName] = {
83
- status: 'in_progress',
84
- createdAt: now,
85
- updatedAt: now,
86
- approvalGates: { proposal: false, plan: false, review: false },
87
- outputs: {},
88
- };
89
-
90
- if (!state.metadata) state.metadata = {};
91
- state.metadata.totalFeatures = Object.keys(state.features).length;
69
+ // Canonical object-form gates (incl. optional uiux) — no boolean drift.
70
+ state.features[featureName] = createDefaultFeature();
92
71
  return true;
93
72
  }
94
73
 
@@ -151,7 +130,9 @@ function handleWriteEdit(input, cwd) {
151
130
  }
152
131
 
153
132
  if (changed) {
154
- writeStateAtomic(cwd, state);
133
+ // Persist the touched feature to its authoritative feature.json and refresh
134
+ // the thin root index. The feature's state thus travels in its own folder.
135
+ persistFeature(featureName, state.features[featureName], cwd);
155
136
  }
156
137
  return true;
157
138
  }
@@ -166,34 +147,21 @@ function handleAskUserQuestion(input, cwd) {
166
147
  const featureName = findActiveFeature(state);
167
148
  if (!featureName) return false;
168
149
 
169
- let changed = false;
170
150
  for (const [question, answer] of Object.entries(answers)) {
171
151
  if (!isApprovalAnswer(answer)) continue;
172
152
  const gate = gateFromQuestion(question);
173
153
  if (!gate) continue;
174
154
 
175
- state.features[featureName].approvalGates ??= {};
176
- const existing = state.features[featureName].approvalGates[gate];
155
+ const existing = state.features[featureName].approvalGates?.[gate];
177
156
  const alreadyApproved = existing === true || existing?.approved === true;
178
- if (!alreadyApproved) {
179
- // An "aprovado" answer to AskUserQuestion is a human approving in the
180
- // loop — record it as such. Write the canonical gate shape
181
- // ({ approved, timestamp, approvedBy }) used by setApprovalGate so the
182
- // audit trail is consistent and a manual approval is distinguishable
183
- // from a policy auto-approval (approvedBy: "auto").
184
- const now = new Date().toISOString();
185
- state.features[featureName].approvalGates[gate] = {
186
- approved: true,
187
- timestamp: now,
188
- approvedBy: 'manual'
189
- };
190
- state.features[featureName].updatedAt = now;
191
- changed = true;
192
- }
193
- }
194
-
195
- if (changed) {
196
- writeStateAtomic(cwd, state);
157
+ if (alreadyApproved) continue;
158
+
159
+ // An "aprovado" answer to AskUserQuestion is a human approving in the loop.
160
+ // Route through the shared canonical gate writer so the hook and the CLI
161
+ // (`morph-spec approve`) produce one consistent shape
162
+ // ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
163
+ // and a manual approval stays distinguishable from a policy auto-approval.
164
+ setGateApproved(featureName, gate, { approvedBy: 'manual' }, cwd);
197
165
  }
198
166
  return true;
199
167
  }
@@ -7,22 +7,62 @@
7
7
  * - Uses synchronous reads for <100ms performance
8
8
  */
9
9
 
10
- import { readFileSync, existsSync } from 'fs';
10
+ import { readFileSync, existsSync, readdirSync } from 'fs';
11
11
  import { join } from 'path';
12
12
 
13
13
  const STATE_FILE = '.morph/state.json';
14
+ const FEATURES_DIR = '.morph/features';
14
15
 
15
16
  /**
16
- * Load state.json from the project root.
17
- * Fail-open: returns null if file missing or corrupt.
17
+ * Load state from the project root, hydrating each feature from its
18
+ * authoritative per-feature `feature.json`.
19
+ *
20
+ * The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
21
+ * per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
22
+ * (committed). This reader merges them: it reads the thin index for project /
23
+ * metadata and overlays each feature.json so callers see the full feature object.
24
+ * A single corrupt feature.json is skipped, not fatal.
25
+ *
26
+ * Fail-open: returns null on any unexpected error or when there is no state at
27
+ * all (no index and no feature files).
18
28
  * @param {string} [projectPath] - Project root (defaults to cwd)
19
29
  * @returns {Object|null} State object or null
20
30
  */
21
31
  export function loadState(projectPath) {
22
32
  try {
23
- const statePath = join(projectPath || process.cwd(), STATE_FILE);
24
- if (!existsSync(statePath)) return null;
25
- return JSON.parse(readFileSync(statePath, 'utf-8'));
33
+ const base = projectPath || process.cwd();
34
+ const statePath = join(base, STATE_FILE);
35
+
36
+ let state = null;
37
+ if (existsSync(statePath)) {
38
+ state = JSON.parse(readFileSync(statePath, 'utf-8'));
39
+ }
40
+
41
+ const featuresDir = join(base, FEATURES_DIR);
42
+ if (existsSync(featuresDir)) {
43
+ let names = [];
44
+ try {
45
+ names = readdirSync(featuresDir, { withFileTypes: true })
46
+ .filter((e) => e.isDirectory())
47
+ .map((e) => e.name);
48
+ } catch {
49
+ names = [];
50
+ }
51
+ for (const name of names) {
52
+ const fjPath = join(featuresDir, name, 'feature.json');
53
+ if (!existsSync(fjPath)) continue;
54
+ try {
55
+ const fj = JSON.parse(readFileSync(fjPath, 'utf-8'));
56
+ state = state || { features: {} };
57
+ state.features = state.features || {};
58
+ state.features[name] = fj; // feature.json is authoritative
59
+ } catch {
60
+ // skip a single corrupt feature.json — don't blind every hook
61
+ }
62
+ }
63
+ }
64
+
65
+ return state;
26
66
  } catch {
27
67
  return null;
28
68
  }
@@ -119,6 +159,25 @@ export function getFeature(featureName, projectPath) {
119
159
  * @param {string} [projectPath]
120
160
  * @returns {string} Phase name
121
161
  */
162
+ export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
163
+
164
+ /**
165
+ * The phase implied purely by approved gates, independent of which artifacts
166
+ * exist on disk. An approved gate authorizes the NEXT phase, so:
167
+ * review approved → review (terminal)
168
+ * plan approved → implement
169
+ * uiux approved → plan
170
+ * otherwise → proposal (proposal-approval's fork between uiux/plan is
171
+ * resolved by enforce-phase-writes, not here)
172
+ * @returns {string} phase name
173
+ */
174
+ export function gateImpliedPhase(featureName, projectPath) {
175
+ if (isGateApproved(featureName, 'review', projectPath)) return 'review';
176
+ if (isGateApproved(featureName, 'plan', projectPath)) return 'implement';
177
+ if (isGateApproved(featureName, 'uiux', projectPath)) return 'plan';
178
+ return 'proposal';
179
+ }
180
+
122
181
  export function derivePhaseForFeature(featureName, projectPath) {
123
182
  const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
124
183
  const phaseMap = [
@@ -128,10 +187,17 @@ export function derivePhaseForFeature(featureName, projectPath) {
128
187
  ['1-design', 'uiux', 'design-system.md'],
129
188
  ['0-proposal', 'proposal', 'proposal.md'],
130
189
  ];
190
+ let artifactPhase = 'proposal';
131
191
  for (const [folder, phase, canonical] of phaseMap) {
132
- if (existsSync(join(basePath, folder, canonical))) return phase;
192
+ if (existsSync(join(basePath, folder, canonical))) { artifactPhase = phase; break; }
133
193
  }
134
- return 'proposal';
194
+
195
+ // Gate-aware: an approved gate advances the phase even before the next
196
+ // phase's first canonical artifact exists. This breaks the chicken-and-egg
197
+ // where that first artifact could never be written because the phase never
198
+ // advanced to permit it. Take whichever of (artifact, gate-implied) is later.
199
+ const implied = gateImpliedPhase(featureName, projectPath);
200
+ return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
135
201
  }
136
202
 
137
203
  /**
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Shared per-feature state writer for Claude Code hooks.
3
+ *
4
+ * Hooks run inside the user's project and cannot import the CLI's
5
+ * src/core/state-manager.js. This module is the hook-side mirror of the core
6
+ * writers: it persists a feature to its authoritative `feature.json` (committed)
7
+ * and refreshes the thin, gitignored root index (`.morph/state.json`).
8
+ *
9
+ * Fail-open by contract: every function swallows errors and returns a boolean —
10
+ * a hook must never break the agent over a state write.
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+
16
+ const STATE_FILE = '.morph/state.json';
17
+ const STATE_VERSION = '5.0.0';
18
+
19
+ /** Absolute path to a feature's authoritative feature.json. */
20
+ export function featureJsonPath(featureName, projectPath) {
21
+ return join(projectPath || process.cwd(), '.morph', 'features', featureName, 'feature.json');
22
+ }
23
+
24
+ /** Read a feature's feature.json, or null if missing/corrupt. */
25
+ export function readFeatureFile(featureName, projectPath) {
26
+ const p = featureJsonPath(featureName, projectPath);
27
+ if (!existsSync(p)) return null;
28
+ try {
29
+ return JSON.parse(readFileSync(p, 'utf-8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** Atomic JSON write: per-pid temp then rename, with an EPERM retry for Windows. */
36
+ function atomicWriteJson(targetPath, data) {
37
+ const dir = dirname(targetPath);
38
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
39
+ const tmp = `${targetPath}.tmp.${process.pid}`;
40
+ writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
41
+ for (let attempt = 0; attempt <= 5; attempt++) {
42
+ try {
43
+ renameSync(tmp, targetPath);
44
+ return;
45
+ } catch (err) {
46
+ if (err.code === 'EPERM' && attempt < 5) {
47
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
48
+ } else {
49
+ try { unlinkSync(tmp); } catch { /* ignore */ }
50
+ throw err;
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Persist a feature to its authoritative feature.json and refresh the thin root
58
+ * index entry (status + updatedAt) + aggregate metadata. Returns true on
59
+ * success, false fail-open.
60
+ */
61
+ export function persistFeature(featureName, feature, projectPath) {
62
+ try {
63
+ atomicWriteJson(featureJsonPath(featureName, projectPath), feature);
64
+
65
+ const statePath = join(projectPath || process.cwd(), STATE_FILE);
66
+ let root = {};
67
+ if (existsSync(statePath)) {
68
+ try { root = JSON.parse(readFileSync(statePath, 'utf-8')); } catch { root = {}; }
69
+ }
70
+ root.version = root.version || STATE_VERSION;
71
+ root.features = root.features || {};
72
+ root.features[featureName] = { status: feature.status, updatedAt: feature.updatedAt };
73
+ root.metadata = root.metadata || {};
74
+ root.metadata.totalFeatures = Object.keys(root.features).length;
75
+ atomicWriteJson(statePath, root);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Canonical gate-approval writer (hook-side mirror of CLI setApprovalGate).
84
+ * Stamps approvalGates[gate] = { approved, timestamp, approvedBy } — the single
85
+ * canonical shape — onto the feature and persists it. Returns true on success.
86
+ *
87
+ * @param {string} featureName
88
+ * @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
89
+ * @param {{ approvedBy?: string, timestamp?: string }} [meta]
90
+ * @param {string} [projectPath]
91
+ */
92
+ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
93
+ try {
94
+ const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature();
95
+ const now = new Date().toISOString();
96
+ feature.approvalGates = feature.approvalGates || {};
97
+ feature.approvalGates[gate] = {
98
+ approved: true,
99
+ timestamp: meta.timestamp || now,
100
+ approvedBy: meta.approvedBy || 'manual',
101
+ };
102
+ feature.updatedAt = now;
103
+ return persistFeature(featureName, feature, projectPath);
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /** Default feature shape (canonical object-form gates, incl. optional uiux). */
110
+ export function createDefaultFeature() {
111
+ const now = new Date().toISOString();
112
+ return {
113
+ status: 'in_progress',
114
+ createdAt: now,
115
+ updatedAt: now,
116
+ approvalGates: {
117
+ proposal: { approved: false, timestamp: null, approvedBy: null },
118
+ uiux: { approved: false, timestamp: null, approvedBy: null },
119
+ plan: { approved: false, timestamp: null, approvedBy: null },
120
+ review: { approved: false, timestamp: null, approvedBy: null },
121
+ },
122
+ outputs: {},
123
+ };
124
+ }
@@ -3,14 +3,14 @@ paths:
3
3
  - "**/Dockerfile*"
4
4
  - "**/docker-compose*"
5
5
  - "**/.github/**"
6
- - "**/easypanel*"
6
+ - "**/coolify*"
7
7
  stacks:
8
8
  - docker
9
9
  ---
10
10
 
11
11
  # Infrastructure Standards
12
12
 
13
- @.morph/framework/standards/infrastructure/docker/easypanel-deploy.md
13
+ @.morph/framework/standards/infrastructure/docker/coolify-deploy.md
14
14
 
15
15
  ## Infrastructure Templates
16
16
 
@@ -19,9 +19,9 @@ stacks:
19
19
  | .NET API Dockerfile | `.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile` | Multi-stage .NET |
20
20
  | Next.js Dockerfile | `.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile` | Multi-stage Next.js |
21
21
  | Docker Compose | `.morph/framework/templates/infrastructure/docker/docker-compose.template.yml` | Local dev env |
22
- | EasyPanel Config | `.morph/framework/templates/infrastructure/docker/easypanel.template.json` | Deployment config |
22
+ | Coolify Config | `.morph/framework/templates/infrastructure/docker/coolify.template.json` | Deployment config |
23
23
  | .NET Build CI | `.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs` | CI workflow |
24
24
  | Docker Push CI | `.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs` | Build & push |
25
- | Deploy EasyPanel | `.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs` | CD webhook |
25
+ | Deploy Coolify | `.morph/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs` | CD via SSH |
26
26
 
27
27
  Read the template and adapt to your project structure.
@@ -1,49 +1,69 @@
1
- ---
2
- stacks:
3
- - "*"
4
- ---
5
-
6
- # MORPH-SPEC Task Tracking
7
-
8
- > Session tasks are MANDATORY in every MORPH-SPEC phase. They track real progress and must be
9
- > created BEFORE any phase work begins.
10
-
11
- ## Rules
12
-
13
- 1. **Every phase skill MUST create session tasks** using `TaskCreate` before reading files or generating outputs
14
- 2. **Tasks are dynamic** — analyze the phase workflow and create tasks that reflect YOUR actual planned actions for this session
15
- 3. **Every task MUST be updated**: `in_progress` before starting, `completed` after finishing
16
- 4. **Pipeline orchestrators** (`/morph-proposal`, `/morph-apply`) MUST create master tasks for each phase
17
- 5. **Never skip task creation** — even if resuming mid-phase, create tasks for remaining steps
18
- 6. **Granularity**: minimum 2, typically 3-7 tasks per phase — adjust to complexity
19
- 7. **For iterative work** (clarification rounds, quality loops): update task `subject` with round/score progress
20
-
21
- ## Task Update Pattern
22
-
23
- ```
24
- TaskUpdate(taskId, status="in_progress") // before starting the step
25
- // ... do the work ...
26
- TaskUpdate(taskId, status="completed") // after step finishes
27
- ```
28
-
29
- For iterative tasks:
30
- ```
31
- TaskUpdate(taskId, subject="Round 3 — score 82/100")
32
- ```
33
-
34
- ## Deviation Tracking
35
-
36
- When completing a task differently than originally planned, record the reason:
37
-
38
- ```
39
- morph-spec task done <feature> <taskId> --note "Reason for deviation"
40
- ```
41
-
42
- For after-the-fact annotation (supports multiple types):
43
- ```
44
- morph-spec task annotate <feature> <taskId> "Reason" --type deviation
45
- morph-spec task annotate <feature> <taskId> "Context info" --type context
46
- morph-spec task annotate <feature> <taskId> "Resolved" --type blocker-resolved
47
- ```
48
-
49
- Note types: `deviation` (default), `context`, `blocker-resolved`
1
+ ---
2
+ stacks:
3
+ - "*"
4
+ ---
5
+
6
+ # MORPH-SPEC Task Tracking
7
+
8
+ > Session tasks are MANDATORY in every MORPH-SPEC phase. They track real progress and must be
9
+ > created BEFORE any phase work begins.
10
+
11
+ ## Rules
12
+
13
+ 1. **Every phase skill MUST create session tasks** using `TaskCreate` before reading files or generating outputs
14
+ 2. **Tasks are dynamic** — analyze the phase workflow and create tasks that reflect YOUR actual planned actions for this session
15
+ 3. **Every task MUST be updated**: `in_progress` before starting, `completed` after finishing
16
+ 4. **Pipeline orchestrators** (`/morph-proposal`, `/morph-apply`) MUST create master tasks for each phase
17
+ 5. **Never skip task creation** — even if resuming mid-phase, create tasks for remaining steps
18
+ 6. **Granularity**: minimum 2, typically 3-7 tasks per phase — adjust to complexity
19
+ 7. **For iterative work** (clarification rounds, quality loops): update task `subject` with round/score progress
20
+
21
+ ## Task Update Pattern
22
+
23
+ ```
24
+ TaskUpdate(taskId, status="in_progress") // before starting the step
25
+ // ... do the work ...
26
+ TaskUpdate(taskId, status="completed") // after step finishes
27
+ ```
28
+
29
+ For iterative tasks:
30
+ ```
31
+ TaskUpdate(taskId, subject="Round 3 — score 82/100")
32
+ ```
33
+
34
+ ## Persisting Progress to `tasks.json`
35
+
36
+ Native session tasks (`TaskCreate`/`TaskUpdate`) track your live work. To persist
37
+ task progress for the feature, write `.morph/features/{feature}/2-plan/tasks.json`,
38
+ touching ONLY the mutable fields:
39
+
40
+ - `status` — `pending` → `in_progress` → `completed`
41
+ - `outputs` — the files the task produced
42
+ - `notes` — free-text record (deviations, context, resolved blockers)
43
+
44
+ The plan contract — `title`, `description`, `dependencies`, `effort`, `doneCriteria`,
45
+ `group` — is frozen after Gate 2. Any attempt to change it is blocked by the
46
+ `protect-spec-files` hook.
47
+
48
+ Use the `Write` tool (whole-file rewrite) OR the `Edit` tool — `Edit` is allowed
49
+ as long as it touches only `status`, `outputs`, and `notes`. State is then synced
50
+ automatically by the `state-sync` PostToolUse hook; you do not call a CLI to mark
51
+ output.
52
+
53
+ ## Deviation Tracking
54
+
55
+ When completing a task differently than originally planned, record the reason in the
56
+ task's `notes` field. Use the `notes` array to capture deviations, added context, or
57
+ resolved blockers — for example:
58
+
59
+ ```json
60
+ {
61
+ "id": "T3",
62
+ "status": "completed",
63
+ "outputs": ["Features/User/Update/Handler.cs"],
64
+ "notes": [
65
+ "deviation: split validation into a separate validator for testability",
66
+ "blocker-resolved: EF migration conflict fixed by regenerating the snapshot"
67
+ ]
68
+ }
69
+ ```