@polymorphism-tech/morph-spec 8.27.0 → 8.29.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 (55) hide show
  1. package/CLAUDE.md +1 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +9 -4
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +3 -2
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/MORPH.md +8 -8
  8. package/framework/agents.json +14 -3
  9. package/framework/commands/create-umbraco-lp.md +1 -1
  10. package/framework/docs/CLI.md +6 -5
  11. package/framework/mcp/registry.json +59 -8
  12. package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
  13. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
  14. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
  15. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  16. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  17. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  18. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  19. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  20. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  21. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  22. package/framework/skills/core/morph-review/SKILL.md +1 -1
  23. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  24. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  25. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  26. package/framework/standards/PROVENANCE.json +41 -0
  27. package/framework/standards/STANDARDS.json +218 -7
  28. package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
  29. package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
  30. package/framework/standards/frontend/design-system/premium-finish.md +160 -0
  31. package/framework/standards/frontend/gsap/gsap-core.md +1 -1
  32. package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
  33. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
  34. package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
  35. package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
  36. package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
  37. package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
  38. package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
  39. package/package.json +1 -1
  40. package/src/commands/project/doctor.js +9 -4
  41. package/src/commands/project/worktree.js +45 -3
  42. package/src/commands/state/advance.js +12 -0
  43. package/src/commands/state/approve.js +20 -0
  44. package/src/commands/state/create.js +4 -1
  45. package/src/commands/state/delete.js +63 -4
  46. package/src/commands/state/finish.js +20 -2
  47. package/src/commands/state/gate-check.js +13 -0
  48. package/src/commands/state/gate-decision.js +9 -0
  49. package/src/commands/state/handoff.js +48 -6
  50. package/src/commands/state/list.js +124 -10
  51. package/src/commands/state/score.js +9 -0
  52. package/src/lib/generators/handoff-generator.js +11 -4
  53. package/src/lib/worktree/feature-truth.js +248 -0
  54. package/src/lib/worktree/fleet-scanner.js +1 -65
  55. package/src/lib/worktree/worktree-audit.js +103 -17
@@ -22,13 +22,14 @@
22
22
  */
23
23
 
24
24
  import { execFileSync } from 'child_process';
25
- import { existsSync, readdirSync, statSync } from 'fs';
25
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
26
26
  import { join, resolve } from 'path';
27
27
  import {
28
28
  isGitRepo,
29
29
  resolvePrimaryRoot,
30
30
  parseWorktreeList,
31
31
  WORKTREE_DIR_NAMES,
32
+ clearDanglingWorktreeRegistration,
32
33
  } from '../../commands/project/worktree.js';
33
34
 
34
35
  const norm = (p) => resolve(p).replace(/\\/g, '/');
@@ -65,6 +66,36 @@ function featureCommittedOnBranch(root, feature) {
65
66
  }
66
67
  }
67
68
 
69
+ /**
70
+ * Every feature's `worktree` registration in the primary root — the OTHER
71
+ * direction from `knownFeatures`/`dirsOnDisk`: not "does git/disk know about
72
+ * this worktree", but "does the ROOT's feature.json still point somewhere
73
+ * real". `create --worktree` writes this pointer once at birth
74
+ * (`{path, branch, createdAt}`) and nothing updates it afterwards except
75
+ * `finish`'s own teardown — so a directory removed any other way (by hand,
76
+ * `git worktree prune` run directly, a failed teardown that still cleared the
77
+ * field, an unrelated script) leaves it dangling with nothing to notice.
78
+ *
79
+ * @param {string} root
80
+ * @returns {Array<{ feature: string, path: string, branch: string|null }>}
81
+ */
82
+ function featureRegistrations(root) {
83
+ const out = [];
84
+ try {
85
+ const dir = join(root, '.morph', 'features');
86
+ if (!existsSync(dir)) return out;
87
+ for (const feature of readdirSync(dir)) {
88
+ const fjPath = join(dir, feature, 'feature.json');
89
+ if (!existsSync(fjPath)) continue;
90
+ try {
91
+ const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
92
+ if (fj?.worktree?.path) out.push({ feature, path: fj.worktree.path, branch: fj.worktree.branch || null });
93
+ } catch { /* corrupt feature.json — nothing to register */ }
94
+ }
95
+ } catch { /* fail-open */ }
96
+ return out;
97
+ }
98
+
68
99
  /** Directories physically present under either worktree root. */
69
100
  function dirsOnDisk(root) {
70
101
  const found = [];
@@ -87,14 +118,25 @@ function dirsOnDisk(root) {
87
118
  * Reconcile git's registry, the disk and morph's features.
88
119
  *
89
120
  * Issue types:
90
- * • registered-missing — git has a worktree whose directory is gone
91
- * • non-morph-branch — a registered worktree under worktrees/ on a branch
92
- * that is not `morph/*`, so `fleet` cannot see it
93
- * • unregistered-dir — a directory under worktrees/ git never registered
94
- * • no-feature — a `morph/{f}` worktree for a feature morph does not know
121
+ * • registered-missing — git has a worktree whose directory is gone
122
+ * • non-morph-branch — a registered worktree under worktrees/ on a
123
+ * branch that is not `morph/*`, so `fleet`
124
+ * cannot see it
125
+ * • unregistered-dir — a directory under worktrees/ git never
126
+ * registered
127
+ * • no-feature — a `morph/{f}` worktree for a feature morph
128
+ * does not know
129
+ * • dangling-registration — a feature's ROOT feature.json still points
130
+ * at a `worktree.path` that no longer exists
131
+ * (the symmetric OTHER direction from
132
+ * `no-feature`: the worktree is gone, but the
133
+ * root's pointer to it survived)
134
+ * • unregistered-registration — a feature's `worktree.path` names a
135
+ * directory that exists but git's own
136
+ * registry does not know as a worktree
95
137
  *
96
138
  * @param {string} [cwd]
97
- * @returns {Array<{ type: string, detail: string, path?: string, fix: string }>}
139
+ * @returns {Array<{ type: string, detail: string, path?: string, feature?: string, fix: string }>}
98
140
  */
99
141
  export function auditWorktrees(cwd = process.cwd()) {
100
142
  if (!isGitRepo(cwd)) return [];
@@ -161,25 +203,69 @@ export function auditWorktrees(cwd = process.cwd()) {
161
203
  });
162
204
  }
163
205
 
206
+ // The other direction: does the ROOT's own bookkeeping still point at a
207
+ // live worktree? `git worktree list`/disk only see what's PHYSICALLY
208
+ // there; a registration surviving past its worktree's removal is invisible
209
+ // to every check above.
210
+ for (const reg of featureRegistrations(root)) {
211
+ const path = norm(reg.path);
212
+ if (!existsSync(reg.path)) {
213
+ issues.push({
214
+ type: 'dangling-registration',
215
+ feature: reg.feature,
216
+ path,
217
+ detail: `"${reg.feature}" registra worktree.path=${path} na raiz, mas o diretório não existe`,
218
+ fix: `morph-spec doctor --worktrees --prune`,
219
+ });
220
+ } else if (!registeredPaths.has(path)) {
221
+ issues.push({
222
+ type: 'unregistered-registration',
223
+ feature: reg.feature,
224
+ path,
225
+ detail: `"${reg.feature}" registra worktree.path=${path}, mas o git não o conhece como worktree — a próxima \`worktree setup\` recusará com exit 3`,
226
+ fix: `inspecione o diretório; se não serve mais, apague-o e rode morph-spec doctor --worktrees --prune`,
227
+ });
228
+ }
229
+ }
230
+
164
231
  return issues;
165
232
  }
166
233
 
167
234
  /**
168
- * Drop git's admin records for worktrees whose directory is already gone.
235
+ * Drop git's admin records for worktrees whose directory is already gone, AND
236
+ * clear any feature's `worktree` registration that names one of those same
237
+ * gone directories.
169
238
  *
170
- * The ONLY prune offered, on purpose: it cannot destroy anything, because
171
- * everything it touches is already absent from disk. Removing a directory that
172
- * still exists is exactly the operation that needs a human looking at it.
239
+ * Both halves are the ONLY prune offered, on purpose: neither can destroy
240
+ * anything, because everything they touch is already absent from disk.
241
+ * Removing a directory that still exists is exactly the operation that needs
242
+ * a human looking at it.
243
+ *
244
+ * `clearDanglingWorktreeRegistration` re-checks the dangling predicate itself
245
+ * (path unset, or still exists → no-op) rather than trusting a pre-computed
246
+ * list, so this stays correct even called against a stale audit snapshot.
173
247
  *
174
248
  * @param {string} [cwd]
175
- * @returns {{ pruned: boolean, error?: string }}
249
+ * @returns {Promise<{ pruned: boolean, error?: string, clearedRegistrations: Array<{feature: string, cleared: boolean}> }>}
176
250
  */
177
- export function pruneWorktrees(cwd = process.cwd()) {
178
- if (!isGitRepo(cwd)) return { pruned: false, error: 'not-a-git-repo' };
251
+ export async function pruneWorktrees(cwd = process.cwd()) {
252
+ if (!isGitRepo(cwd)) return { pruned: false, error: 'not-a-git-repo', clearedRegistrations: [] };
253
+ const root = resolvePrimaryRoot(cwd);
254
+
255
+ let pruned = false;
256
+ let error;
179
257
  try {
180
- execFileSync('git', ['worktree', 'prune'], { cwd: resolvePrimaryRoot(cwd), stdio: 'pipe' });
181
- return { pruned: true };
258
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
259
+ pruned = true;
182
260
  } catch (err) {
183
- return { pruned: false, error: (err.message || 'unknown').slice(0, 200) };
261
+ error = (err.message || 'unknown').slice(0, 200);
184
262
  }
263
+
264
+ const clearedRegistrations = [];
265
+ for (const reg of featureRegistrations(root)) {
266
+ const { cleared } = await clearDanglingWorktreeRegistration(root, reg.feature);
267
+ if (cleared) clearedRegistrations.push({ feature: reg.feature, cleared: true });
268
+ }
269
+
270
+ return { pruned, ...(error ? { error } : {}), clearedRegistrations };
185
271
  }