@polymorphism-tech/morph-spec 9.4.0 → 9.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 (106) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -0
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/MORPH.md +23 -33
  8. package/framework/agents.json +6 -7
  9. package/framework/docs/CLI.md +12 -8
  10. package/framework/hooks/README.md +2 -2
  11. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  12. package/framework/hooks/claude-code/core/comment-guard.js +1 -0
  13. package/framework/hooks/claude-code/core/denylist.json +63 -1
  14. package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
  15. package/framework/hooks/claude-code/core/gate-guard.js +0 -8
  16. package/framework/hooks/claude-code/core/heavy-gate.js +5 -3
  17. package/framework/hooks/claude-code/core/loop-detect.js +0 -3
  18. package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
  20. package/framework/hooks/claude-code/core/validate-completion.js +0 -4
  21. package/framework/hooks/claude-code/core/worktree-context.js +0 -2
  22. package/framework/hooks/claude-code/statusline.py +3 -44
  23. package/framework/hooks/shared/build-check.js +12 -19
  24. package/framework/hooks/shared/comment-rules.js +11 -1
  25. package/framework/hooks/shared/feature-locator.js +7 -5
  26. package/framework/hooks/shared/heavy-command.js +12 -0
  27. package/framework/hooks/shared/heavy-config.js +28 -1
  28. package/framework/hooks/shared/heavy-slots.js +41 -5
  29. package/framework/hooks/shared/hook-response.js +0 -4
  30. package/framework/hooks/shared/payload-utils.js +1 -35
  31. package/framework/hooks/shared/phase-utils.js +0 -12
  32. package/framework/hooks/shared/slice-progress.js +0 -2
  33. package/framework/hooks/shared/state-reader.js +0 -19
  34. package/framework/hooks/shared/telemetry-logger.js +1 -1
  35. package/framework/rules/frontend-standards.md +1 -2
  36. package/framework/rules/nextjs-standards.md +9 -8
  37. package/framework/schemas/config.schema.json +12 -1
  38. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  39. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  40. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  41. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  42. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  43. package/framework/skills/core/morph-media/SKILL.md +1 -1
  44. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  45. package/framework/skills/core/morph-review/SKILL.md +1 -1
  46. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  47. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  48. package/framework/standards/STANDARDS.json +32 -29
  49. package/framework/standards/STANDARDS.meta.json +13 -15
  50. package/framework/standards/architecture/code-comments.md +1 -1
  51. package/framework/standards/frontend/admin-app/data-table.md +1 -3
  52. package/framework/standards/frontend/admin-app/page-structure.md +8 -4
  53. package/framework/standards/frontend/nextjs/app-router.md +3 -4
  54. package/framework/standards/frontend/nextjs/components.md +2 -15
  55. package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
  56. package/framework/standards/frontend/nextjs/lint.md +141 -0
  57. package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
  58. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
  59. package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
  60. package/framework/standards/frontend/nextjs/project-structure.md +3 -2
  61. package/framework/standards/frontend/nextjs/state-management.md +6 -7
  62. package/framework/standards/infrastructure/docker/local-compose-isolation.md +1 -0
  63. package/framework/templates/README.md +4 -4
  64. package/framework/templates/REGISTRY.json +21 -309
  65. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
  66. package/package.json +3 -2
  67. package/src/commands/e2e/e2e.js +3 -0
  68. package/src/commands/project/env.js +3 -3
  69. package/src/commands/project/heavy.js +10 -3
  70. package/src/commands/project/worktree.js +2 -1
  71. package/src/commands/state/finish.js +17 -3
  72. package/src/commands/validation/mutate.js +2 -2
  73. package/src/commands/validation/verify.js +3 -0
  74. package/src/core/paths/output-schema.js +0 -9
  75. package/src/core/state/feature-identity.js +0 -2
  76. package/src/core/state/state-manager.js +1 -169
  77. package/src/core/templates/template-renderer.js +0 -23
  78. package/src/dashboard/app/lib/format.js +0 -7
  79. package/src/dashboard/app/lib/phases.js +0 -7
  80. package/src/dashboard/scanner/activity-scanner.js +0 -4
  81. package/src/lib/ai/ai-project-scan.js +1 -6
  82. package/src/lib/e2e/compose-env.js +26 -3
  83. package/src/lib/graph/graphify-runtime.js +0 -1
  84. package/src/lib/heavy/gate.js +23 -4
  85. package/src/lib/heavy/live-stacks.js +64 -0
  86. package/src/lib/mutation/snapshot.js +1 -5
  87. package/src/lib/risk-detector.js +9 -6
  88. package/src/lib/stack/stack-profile.js +0 -4
  89. package/src/lib/stack/stack-scan.js +15 -4
  90. package/src/lib/tasks/eval-node.js +1 -7
  91. package/src/lib/validators/validation-runner.js +0 -43
  92. package/src/lib/validators/verify-runner.js +18 -2
  93. package/src/lib/worktree/docker-audit.js +7 -3
  94. package/src/lib/worktree/env-allocator.js +27 -5
  95. package/src/lib/worktree/ignored-triage.js +8 -0
  96. package/src/utils/agents-installer.js +12 -19
  97. package/src/utils/claude-settings-manager.js +0 -28
  98. package/src/utils/file-copier.js +0 -53
  99. package/framework/hooks/shared/activity-logger.js +0 -131
  100. package/framework/standards/frontend/design-system/naming.md +0 -68
  101. package/src/commands/validation/index.js +0 -2
  102. package/src/lib/validators/code-budget/index.js +0 -2
  103. package/src/lib/validators/contracts/index.js +0 -1
  104. package/src/lib/validators/nextjs/index.js +0 -1
  105. package/src/lib/validators/packages/index.js +0 -1
  106. package/src/lib/validators/reuse/index.js +0 -2
@@ -1,56 +1,54 @@
1
- // features/{{kebabCase featureName}}/components/create-{{kebabCase featureName}}-form.tsx
2
- 'use client';
3
-
4
- import { useForm } from 'react-hook-form';
5
- import { zodResolver } from '@hookform/resolvers/zod';
6
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
7
- import { Input } from '@/components/ui/input';
8
- import { Button } from '@/components/ui/button';
9
- import { useCreate{{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/hooks/use-create-{{kebabCase featureName}}';
10
- import { create{{pascalCase featureName}}Schema, type Create{{pascalCase featureName}}Input } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.schemas';
11
-
12
- interface Create{{pascalCase featureName}}FormProps {
13
- onSuccess?: () => void;
14
- }
15
-
16
- export function Create{{pascalCase featureName}}Form({ onSuccess }: Create{{pascalCase featureName}}FormProps) {
17
- const form = useForm<Create{{pascalCase featureName}}Input>({
18
- resolver: zodResolver(create{{pascalCase featureName}}Schema),
19
- defaultValues: { name: '' },
20
- });
21
-
22
- const { mutate: create{{pascalCase featureName}}, isPending } = useCreate{{pascalCase featureName}}();
23
-
24
- function onSubmit(values: Create{{pascalCase featureName}}Input) {
25
- create{{pascalCase featureName}}(values, {
26
- onSuccess: () => { form.reset(); onSuccess?.(); },
27
- onError: (error) => { form.setError('root', { message: error.message }); },
28
- });
29
- }
30
-
31
- return (
32
- <Form {...form}>
33
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
34
- <FormField
35
- control={form.control}
36
- name="name"
37
- render={({ field }) => (
38
- <FormItem>
39
- <FormLabel>Name</FormLabel>
40
- <FormControl>
41
- <Input placeholder="Enter name" {...field} />
42
- </FormControl>
43
- <FormMessage />
44
- </FormItem>
45
- )}
46
- />
47
- {form.formState.errors.root && (
48
- <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
49
- )}
50
- <Button type="submit" disabled={isPending}>
51
- {isPending ? 'Creating...' : 'Create {{pascalCase featureName}}'}
52
- </Button>
53
- </form>
54
- </Form>
55
- );
56
- }
1
+ // features/{{kebabCase featureName}}/components/create-{{kebabCase featureName}}-form.tsx
2
+ 'use client';
3
+
4
+ import { Controller, useForm } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Button } from '@/components/ui/button';
9
+ import { useCreate{{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/hooks/use-create-{{kebabCase featureName}}';
10
+ import { create{{pascalCase featureName}}Schema, type Create{{pascalCase featureName}}Input } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.schemas';
11
+
12
+ interface Create{{pascalCase featureName}}FormProps {
13
+ onSuccess?: () => void;
14
+ }
15
+
16
+ export function Create{{pascalCase featureName}}Form({ onSuccess }: Create{{pascalCase featureName}}FormProps) {
17
+ const form = useForm<Create{{pascalCase featureName}}Input>({
18
+ resolver: zodResolver(create{{pascalCase featureName}}Schema),
19
+ defaultValues: { name: '' },
20
+ });
21
+
22
+ const { mutate: create{{pascalCase featureName}}, isPending } = useCreate{{pascalCase featureName}}();
23
+
24
+ function onSubmit(values: Create{{pascalCase featureName}}Input) {
25
+ create{{pascalCase featureName}}(values, {
26
+ onSuccess: () => { form.reset(); onSuccess?.(); },
27
+ onError: (error) => { form.setError('root', { message: error.message }); },
28
+ });
29
+ }
30
+
31
+ return (
32
+ <form onSubmit={form.handleSubmit(onSubmit)}>
33
+ <FieldGroup>
34
+ <Controller
35
+ name="name"
36
+ control={form.control}
37
+ render={({ field, fieldState }) => (
38
+ <Field data-invalid={fieldState.invalid}>
39
+ <FieldLabel htmlFor={field.name}>Name</FieldLabel>
40
+ <Input {...field} id={field.name} aria-invalid={fieldState.invalid} placeholder="Enter name" />
41
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
42
+ </Field>
43
+ )}
44
+ />
45
+ {form.formState.errors.root && (
46
+ <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
47
+ )}
48
+ <Button type="submit" disabled={isPending}>
49
+ {isPending ? 'Creating...' : 'Create {{pascalCase featureName}}'}
50
+ </Button>
51
+ </FieldGroup>
52
+ </form>
53
+ );
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.4.0",
3
+ "version": "9.6.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -56,7 +56,8 @@
56
56
  "docs:serve": "npx http-server docs/api -p 8080 -o",
57
57
  "release": "node scripts/release.js",
58
58
  "prepublishOnly": "node scripts/prepublish-check.js",
59
- "postinstall": "node src/scripts/global-install.js"
59
+ "postinstall": "node src/scripts/global-install.js",
60
+ "publish:web": "node scripts/publish-web.mjs"
60
61
  },
61
62
  "dependencies": {
62
63
  "chalk": "^5.3.0",
@@ -28,6 +28,7 @@ import { buildComposeEnv } from '../../lib/e2e/compose-env.js';
28
28
  import { findWorktreeForFeature, resolvePrimaryRoot } from '../project/worktree.js';
29
29
  import { createHeavyGate, HeavyBusyError } from '../../lib/heavy/gate.js';
30
30
  import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
31
+ import { memoryBusyHint } from '../../lib/heavy/live-stacks.js';
31
32
  import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
32
33
 
33
34
  const EXIT_USAGE = 2;
@@ -126,6 +127,8 @@ export async function e2eUpCommand(featureName, options = {}) {
126
127
  console.log(JSON.stringify({ status: 'busy', feature: featureName, busy: { reason: err.reason, waitedMs: err.waitedMs } }, null, 2));
127
128
  }
128
129
  console.error(chalk.yellow(`\n⏳ e2e up não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\`).\n`));
130
+ const hint = memoryBusyHint(err, ctx.cwd);
131
+ if (hint) console.error(chalk.yellow(`${hint}\n`));
129
132
  process.exit(EXIT_BUSY);
130
133
  } finally {
131
134
  gate.release();
@@ -35,7 +35,7 @@ function gitCommonDir(cwd) {
35
35
  const shq = (v) => `'${String(v).replace(/'/g, "'\\''")}'`;
36
36
 
37
37
  /** The env map for a key, or `{ error }`. */
38
- export function resolveWorktreeEnv(featureArg, cwd = process.cwd()) {
38
+ export function resolveWorktreeEnv(featureArg, cwd = process.cwd(), { withCredentials = true } = {}) {
39
39
  const key = featureArg || envKeyFromBranch(currentBranch(cwd));
40
40
  if (!key) {
41
41
  return { error: 'no-feature', message: 'Nenhuma feature: passe o nome ou rode de uma branch morph/{f} / morph-task/{n}.' };
@@ -45,7 +45,7 @@ export function resolveWorktreeEnv(featureArg, cwd = process.cwd()) {
45
45
  return { error: 'not-a-git-repo', message: 'Fora de um repositório git não há bloco de portas.' };
46
46
  }
47
47
  const root = resolvePrimaryRoot(cwd);
48
- const env = buildComposeEnv({ cwd: root, config: readConfig(root), featureName: key, gitCommonDir: common });
48
+ const env = buildComposeEnv({ cwd: root, config: readConfig(root), featureName: key, gitCommonDir: common, withCredentials });
49
49
  return { key, env };
50
50
  }
51
51
 
@@ -57,7 +57,7 @@ export function render(env, format) {
57
57
  }
58
58
 
59
59
  export function envCommand(featureArg, options = {}) {
60
- const result = resolveWorktreeEnv(featureArg || null, process.cwd());
60
+ const result = resolveWorktreeEnv(featureArg || null, process.cwd(), { withCredentials: Boolean(options.withSecrets) });
61
61
  if (result.error) {
62
62
  if (options.json) console.log(JSON.stringify({ error: result.error, message: result.message }));
63
63
  else console.error(chalk.red(`✗ ${result.message}`));
@@ -5,6 +5,8 @@ import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js
5
5
  import { createHeavyGate, HeavyBusyError } from '../../lib/heavy/gate.js';
6
6
  import { describeHolder, heavyDir, heavyStatus } from '../../../framework/hooks/shared/heavy-slots.js';
7
7
  import { resolveSpawn } from '../../../framework/hooks/shared/spawn-command.js';
8
+ import { heavyClass, heavyKind } from '../../../framework/hooks/shared/heavy-command.js';
9
+ import { memoryBusyHint } from '../../lib/heavy/live-stacks.js';
8
10
 
9
11
  const EXIT_BUSY = 3;
10
12
 
@@ -15,6 +17,7 @@ export function heavyStatusCommand(options = {}) {
15
17
  enabled: config.enabled,
16
18
  slots: config.slots,
17
19
  minFreeMemGb: config.minFreeMemGb,
20
+ minFreeMemGbByKind: config.minFreeMemGbByKind,
18
21
  dir: status.dir,
19
22
  freeGb: Number(status.freeGb.toFixed(2)),
20
23
  holders: status.holders.map(({ pid, label, feature, node, cwd, since, leaseId }) => ({ pid, label, feature, node, cwd, since, ...(leaseId ? { leaseId } : {}) })),
@@ -29,8 +32,10 @@ export function heavyStatusCommand(options = {}) {
29
32
  logger.header('Slots de nós pesados (build, testes, e2e)');
30
33
  if (!config.enabled) console.log(chalk.yellow(' ⚠ semáforo desligado (heavy.enabled: false ou MORPH_HEAVY=off)'));
31
34
  console.log(chalk.gray(` diretório: ${report.dir}`));
32
- const memMark = report.freeGb < config.minFreeMemGb ? chalk.yellow('⚠') : chalk.green('✓');
33
- console.log(` ${memMark} RAM livre: ${report.freeGb.toFixed(1)} GB (mínimo ${config.minFreeMemGb} GB)`);
35
+ const floors = config.minFreeMemGbByKind;
36
+ const memMark = report.freeGb < Math.max(...Object.values(floors)) ? chalk.yellow('⚠') : chalk.green('✓');
37
+ const floorText = Object.entries(floors).map(([kind, gb]) => `${kind} ${gb}`).join(' · ');
38
+ console.log(` ${memMark} RAM livre: ${report.freeGb.toFixed(1)} GB (mínimo em GB: ${floorText})`);
34
39
  console.log(` slots em uso: ${status.holders.length}/${config.slots}`);
35
40
  for (const h of status.holders) console.log(chalk.gray(` • ${describeHolder(h)}${h.cwd ? ` — ${h.cwd}` : ''}`));
36
41
  if (status.exclusive) console.log(chalk.yellow(` ⚠ limpeza do servidor de build em curso (${describeHolder(status.exclusive)})`));
@@ -42,10 +47,12 @@ export function heavyRunCommand(command, options = {}) {
42
47
  const [file, ...args] = command;
43
48
  const gate = createHeavyGate({ projectPath: process.cwd(), label: options.label ?? 'run' });
44
49
  try {
45
- gate.enter(file);
50
+ gate.enter(file, heavyClass(heavyKind(command.join(' '))));
46
51
  } catch (err) {
47
52
  if (!(err instanceof HeavyBusyError)) throw err;
48
53
  console.error(chalk.yellow(`\n⏳ ${file} não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\` mostra quem segura os slots).\n`));
54
+ const hint = memoryBusyHint(err, process.cwd());
55
+ if (hint) console.error(chalk.yellow(`${hint}\n`));
49
56
  process.exit(EXIT_BUSY);
50
57
  }
51
58
 
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatS
3
3
  import { join, resolve, dirname, basename } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
6
- import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
6
+ import { isTestArtifact, triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
7
7
  import {
8
8
  finishBackupDir,
9
9
  writeFeatureScopeBackup,
@@ -710,6 +710,7 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
710
710
  if (!record || record.length < 4) return false;
711
711
  const p = record.slice(3).replace(/\\/g, '/');
712
712
  if (opts.exemptOwnScope && p.startsWith(ownScope)) return false;
713
+ if (record.startsWith('?? ') && isTestArtifact(p)) return false;
713
714
  return !isLiveSiblingWorktreeRegistration(p, root, feature);
714
715
  });
715
716
  return { ok: true, dirt };
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'child_process';
2
- import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'fs';
3
3
  import { join, resolve } from 'path';
4
4
  import {
5
5
  findWorktreeForFeature,
@@ -23,6 +23,7 @@ import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.
23
23
  import { runArchive } from './archive.js';
24
24
  import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
25
25
  import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
26
+ import { isTestArtifact } from '../../lib/worktree/ignored-triage.js';
26
27
 
27
28
  const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
28
29
 
@@ -289,7 +290,9 @@ export async function finishCommand(feature, options = {}) {
289
290
  emit({ finished: false, error: 'status-failed', feature });
290
291
  process.exit(1);
291
292
  }
292
- if (withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim())).length) {
293
+ const dirtLines = withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim()));
294
+ const testArtifacts = untrackedTestArtifacts(dirtLines);
295
+ if (dirtLines.length > testArtifacts.length) {
293
296
  emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
294
297
  process.exit(1);
295
298
  }
@@ -338,7 +341,10 @@ export async function finishCommand(feature, options = {}) {
338
341
 
339
342
  // Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
340
343
  dropLeftoverIgnore(cleanCwd);
341
- const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
344
+ if (isWorktreeFlow) {
345
+ for (const p of testArtifacts) rmSync(join(cleanCwd, p), { recursive: true, force: true });
346
+ }
347
+ const arch =runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
342
348
  if (!arch.ok) {
343
349
  emit({ finished: false, error: `archive-${arch.error}`, feature });
344
350
  process.exit(1);
@@ -794,6 +800,14 @@ function currentBranch(cwd) {
794
800
  }
795
801
  }
796
802
 
803
+ /** Untracked Playwright/test output (`?? test-results/`): never work, so it neither blocks nor survives the teardown. */
804
+ function untrackedTestArtifacts(lines) {
805
+ return lines
806
+ .filter((l) => l.startsWith('?? '))
807
+ .map((l) => l.slice(3).replace(/^"|"$/g, '').replace(/\/$/, ''))
808
+ .filter((p) => isTestArtifact(p));
809
+ }
810
+
797
811
  function gitStatusPorcelain(cwd) {
798
812
  try {
799
813
  return execFileSync('git', ['status', '--porcelain'], { cwd, stdio: 'pipe' }).toString();
@@ -113,13 +113,13 @@ function subsetFor({ node, group, root }) {
113
113
 
114
114
  function e2eManualProtocol(feature, node) {
115
115
  const f = feature ?? '<feature>';
116
- const round = `eval "$(morph-spec env ${f})" && morph-spec e2e up ${f} && morph-spec heavy run -- ${node.command}`;
116
+ const round = `eval "$(morph-spec env ${f} --with-secrets)" && morph-spec e2e up ${f} && morph-spec heavy run -- ${node.command}`;
117
117
  return 'mutate só recompila localmente, e a suíte que alcança esse código testa o container do compose: o mutante sairia sobrevivente em falso. ' +
118
118
  `Prove à mão pelo nó "${node.id}", cada passo numa linha só e encadeado por \`&&\` (nunca \`;\`: um passo que falha não pode deixar a suíte rodar sobre o código original; ` +
119
119
  'e o env da feature se perde entre chamadas de Bash): ' +
120
120
  `(1) base verde antes do 1º mutante: \`${round}\`; ` +
121
121
  `(2) por mutante: edite a linha && \`${round}\`; ` +
122
- `(3) restaure, confirme com \`git diff --quiet -- <arquivo>\` e suba a stack de novo (\`eval "$(morph-spec env ${f})" && morph-spec e2e up ${f}\`). ` +
122
+ `(3) restaure, confirme com \`git diff --quiet -- <arquivo>\` e suba a stack de novo (\`eval "$(morph-spec env ${f} --with-secrets)" && morph-spec e2e up ${f}\`). ` +
123
123
  'Registre cada mutante em 4-review (expect, resultado, teste que o matou).';
124
124
  }
125
125
 
@@ -11,6 +11,7 @@ import { buildVerifyEvent, recordVerifyEvent, findRepeatedVerify } from '../../l
11
11
  import { treeFingerprint } from '../../lib/git/tree-fingerprint.js';
12
12
  import { HeavyBusyError } from '../../lib/heavy/gate.js';
13
13
  import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
14
+ import { memoryBusyHint } from '../../lib/heavy/live-stacks.js';
14
15
  import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
15
16
  import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
16
17
 
@@ -146,6 +147,8 @@ async function runLocalVerify(featureName, options) {
146
147
  }, null, 2));
147
148
  }
148
149
  console.error(chalk.yellow(`\n⏳ verify não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\` mostra quem segura os slots).\n`));
150
+ const hint = memoryBusyHint(err, process.cwd());
151
+ if (hint) console.error(chalk.yellow(`${hint}\n`));
149
152
  process.exit(3);
150
153
  }
151
154
  throw err;
@@ -43,15 +43,6 @@ export function getFeatureDir(featureName) {
43
43
  return join('.morph', 'features', featureName).replace(/\\/g, '/');
44
44
  }
45
45
 
46
- export function getTaskReportPath(featureName, taskId) {
47
- return join('.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`)
48
- .replace(/\\/g, '/');
49
- }
50
-
51
- export function getAbsoluteTaskReportPath(projectRoot, featureName, taskId) {
52
- return join(projectRoot, '.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`);
53
- }
54
-
55
46
  /** `multi` types contribute their directory, so `created` means "the folder exists". */
56
47
  export function getAllOutputPaths(featureName) {
57
48
  const outputs = {};
@@ -1,5 +1,3 @@
1
- export const IDENTITY_FIELDS = ['name', 'description', 'workType', 'createdAt'];
2
-
3
1
  const blank = (v) => v === undefined || v === null || String(v).trim() === '';
4
2
 
5
3
  // `workType` is excluded on purpose: absent, it defaults to `feature`.
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
1
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, readdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
4
4
  import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
@@ -248,41 +248,6 @@ export function saveState(state) {
248
248
  });
249
249
  }
250
250
 
251
- const LOCK_DIR_NAME = '.morph/state.json.lock';
252
- const LOCK_MAX_RETRIES = 5;
253
- const LOCK_BASE_MS = 100;
254
- const LOCK_STALE_MS = 30_000;
255
-
256
- export async function withStateLock(fn) {
257
- const lockPath = join(process.cwd(), LOCK_DIR_NAME);
258
-
259
- for (let attempt = 0; attempt <= LOCK_MAX_RETRIES; attempt++) {
260
- try {
261
- mkdirSync(lockPath);
262
- break;
263
- } catch (err) {
264
- if (err.code !== 'EEXIST') throw err;
265
- try {
266
- const lockStat = statSync(lockPath);
267
- if (Date.now() - lockStat.mtimeMs > LOCK_STALE_MS) {
268
- try { rmdirSync(lockPath); } catch { }
269
- continue;
270
- }
271
- } catch { }
272
- if (attempt === LOCK_MAX_RETRIES) {
273
- throw new Error('Could not acquire state lock after retries');
274
- }
275
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_BASE_MS * (2 ** attempt));
276
- }
277
- }
278
-
279
- try {
280
- return await fn();
281
- } finally {
282
- try { rmdirSync(lockPath); } catch { }
283
- }
284
- }
285
-
286
251
  export function initState(options = {}) {
287
252
  const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
288
253
 
@@ -402,29 +367,6 @@ export async function updateFeature(featureName, key, value) {
402
367
  saveState(state);
403
368
  }
404
369
 
405
- export async function updateFeatureMultiple(featureName, updates) {
406
- await ensureFeature(featureName);
407
- const state = loadState(); // Load AFTER ensuring feature exists
408
-
409
- for (const [key, value] of Object.entries(updates)) {
410
- const keys = key.split('.');
411
- let target = state.features[featureName];
412
-
413
- for (let i = 0; i < keys.length - 1; i++) {
414
- if (!target[keys[i]]) {
415
- target[keys[i]] = {};
416
- }
417
- target = target[keys[i]];
418
- }
419
-
420
- const finalKey = keys[keys.length - 1];
421
- target[finalKey] = value;
422
- }
423
-
424
- state.features[featureName].updatedAt = new Date().toISOString();
425
- saveState(state);
426
- }
427
-
428
370
  function normalizeOutputType(type) {
429
371
  if (!type.includes('-')) {
430
372
  return type;
@@ -544,91 +486,6 @@ export async function markOutput(featureName, outputType) {
544
486
  saveState(state);
545
487
  }
546
488
 
547
- export async function trackFileChange(featureName, filePath, action, phase) {
548
- await ensureFeature(featureName);
549
- const state = loadState();
550
-
551
- if (!Array.isArray(state.features[featureName].fileChanges)) {
552
- state.features[featureName].fileChanges = [];
553
- }
554
-
555
- state.features[featureName].fileChanges.push({
556
- path: filePath,
557
- action,
558
- phase: phase || state.features[featureName].phase || 'plan',
559
- timestamp: new Date().toISOString()
560
- });
561
-
562
- state.features[featureName].updatedAt = new Date().toISOString();
563
- saveState(state);
564
- }
565
-
566
- export function getFileChanges(featureName, options = {}) {
567
- const state = loadState();
568
- const feature = state.features[featureName];
569
-
570
- if (!feature) {
571
- throw new Error(`Feature '${featureName}' not found.`);
572
- }
573
-
574
- const changes = feature.fileChanges || [];
575
-
576
- if (options.groupByPhase) {
577
- const grouped = {};
578
- for (const change of changes) {
579
- const phase = change.phase || 'unknown';
580
- if (!grouped[phase]) grouped[phase] = [];
581
- grouped[phase].push(change);
582
- }
583
- return grouped;
584
- }
585
-
586
- return changes;
587
- }
588
-
589
- export async function skipPhase(featureName, phase, reason = '') {
590
- await ensureFeature(featureName);
591
- const state = loadState();
592
-
593
- if (!state.features[featureName].skippedPhases) {
594
- state.features[featureName].skippedPhases = [];
595
- }
596
-
597
- const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
598
- if (!existing) {
599
- state.features[featureName].skippedPhases.push({
600
- phase,
601
- reason,
602
- timestamp: new Date().toISOString()
603
- });
604
- }
605
-
606
- state.features[featureName].updatedAt = new Date().toISOString();
607
- saveState(state);
608
- }
609
-
610
- export function getSkippedPhases(featureName) {
611
- const state = loadState();
612
- if (!state.features[featureName]) {
613
- return [];
614
- }
615
- return state.features[featureName].skippedPhases || [];
616
- }
617
-
618
- export function listFeatures() {
619
- const state = loadState();
620
- return Object.entries(state.features);
621
- }
622
-
623
- export function getSummary() {
624
- const state = loadState();
625
- return {
626
- project: state.project,
627
- metadata: state.metadata,
628
- featuresCount: Object.keys(state.features).length
629
- };
630
- }
631
-
632
489
  export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
633
490
  const state = loadState();
634
491
  const feature = await ensureFeature(featureName);
@@ -666,31 +523,6 @@ export function getApprovalGate(featureName, gate) {
666
523
  return feature.approvalGates[gate] || null;
667
524
  }
668
525
 
669
- export function isPendingApproval(featureName) {
670
- const state = loadState();
671
- const feature = state.features[featureName];
672
-
673
- if (!feature || !feature.approvalGates) {
674
- return false;
675
- }
676
-
677
- const currentPhase = feature.phase || 'plan';
678
-
679
- const phaseGateMap = {
680
- 'plan': 'plan',
681
- 'review': 'review',
682
- 'uiux': 'uiux'
683
- };
684
-
685
- const relevantGate = phaseGateMap[currentPhase];
686
- if (!relevantGate) {
687
- return false;
688
- }
689
-
690
- const gate = feature.approvalGates[relevantGate];
691
- return gate && !gate.approved;
692
- }
693
-
694
526
  export function getApprovalHistory(featureName) {
695
527
  const state = loadState();
696
528
  const feature = state.features[featureName];
@@ -340,15 +340,6 @@ export function renderTemplateString(templateString, variables, options = {}) {
340
340
  }
341
341
  }
342
342
 
343
- export function getTemplatePlaceholders(templatePath) {
344
- if (!existsSync(templatePath)) {
345
- throw new Error(`Template file not found: ${templatePath}`);
346
- }
347
-
348
- const templateContent = readFileSync(templatePath, 'utf-8');
349
- return getTemplatePlaceholdersFromString(templateContent);
350
- }
351
-
352
343
  export function getTemplatePlaceholdersFromString(templateString) {
353
344
  const placeholders = new Set();
354
345
 
@@ -380,18 +371,4 @@ export function getTemplatePlaceholdersFromString(templateString) {
380
371
  return Array.from(placeholders).sort();
381
372
  }
382
373
 
383
- export function validateTemplatePlaceholders(templatePath, variables, optionalPlaceholders = []) {
384
- const placeholders = getTemplatePlaceholders(templatePath);
385
- const providedKeys = Object.keys(variables);
386
-
387
- const missing = placeholders.filter(
388
- p => !providedKeys.includes(p) && !optionalPlaceholders.includes(p)
389
- );
390
-
391
- return {
392
- valid: missing.length === 0,
393
- missing
394
- };
395
- }
396
-
397
374
  export { Handlebars };
@@ -48,13 +48,6 @@ export function bytes(n) {
48
48
  return `${(n / 1024 / 1024).toFixed(1)} MB`;
49
49
  }
50
50
 
51
- export function duration(seconds) {
52
- if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) return '—';
53
- const m = Math.round(seconds / 60);
54
- if (m < 60) return `${m} min`;
55
- return `${Math.floor(m / 60)} h ${m % 60} min`;
56
- }
57
-
58
51
  export function splitFrontmatter(text) {
59
52
  const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text || '');
60
53
  if (!m) return { meta: null, body: text || '' };
@@ -36,13 +36,6 @@ export const STATUS_LABEL = {
36
36
  abandoned: 'Abandonada',
37
37
  };
38
38
 
39
- export const WORKTYPE_LABEL = {
40
- feature: 'feature',
41
- bug: 'bug',
42
- chore: 'chore',
43
- hotfix: 'hotfix',
44
- };
45
-
46
39
  export const LOCATION_LABEL = {
47
40
  root: 'raiz',
48
41
  worktree: 'worktree',
@@ -145,10 +145,6 @@ export function lastActivityByFeature(events) {
145
145
  return out;
146
146
  }
147
147
 
148
- export function loadProjectEvents(projectDir) {
149
- return dedupeEvents(loadTelemetryAllTrees(projectDir).flatMap((t) => t.events));
150
- }
151
-
152
148
  const LIVE_LOG = join('.morph', 'logs', 'events.jsonl');
153
149
 
154
150
  function eventKey(e) {
@@ -2,7 +2,7 @@
2
2
  // `[]` é "perguntei e não achei". Worktrees ficam fora: são o checkout de outra feature.
3
3
 
4
4
  import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
5
- import { join, basename } from 'node:path';
5
+ import { join } from 'node:path';
6
6
  import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
7
7
 
8
8
  export const SCAN_IGNORED_DIRS = new Set([
@@ -246,8 +246,3 @@ export function findPackage(scan, name) {
246
246
  if (!scan || !Array.isArray(scan.packages)) return null;
247
247
  return scan.packages.find((p) => p.name === name) ?? null;
248
248
  }
249
-
250
- export function projectFileNames(scan) {
251
- if (!scan) return [];
252
- return scan.projectFiles.map((f) => basename(f));
253
- }