@polymorphism-tech/morph-spec 9.8.0 → 10.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 (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, readFileSync } from 'fs';
3
+ import { existsSync, mkdirSync } from 'fs';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { dirname, join, resolve } from 'path';
6
- import { homedir } from 'os';
7
6
  import { stateExists } from '../../shared/state-reader.js';
8
7
  import { resolveGitCommonDir } from '../../shared/git-paths.js';
9
8
  import { runMorphSpec } from '../../shared/morph-cli.js';
@@ -34,43 +33,6 @@ export function slugify(name) {
34
33
  return slug || `wt-${Date.now().toString(36)}`;
35
34
  }
36
35
 
37
- function readJsonSafe(path) {
38
- try { return JSON.parse(readFileSync(path, 'utf8').replace(/^/, '')); } catch { return null; }
39
- }
40
-
41
- // `null` when unset, distinct from an explicit `'fresh'`, which overrides the feature-branch default.
42
- export function readBaseRefSetting(root, home = homedir()) {
43
- let value = null;
44
- for (const p of [
45
- join(home, '.claude', 'settings.json'),
46
- join(root, '.claude', 'settings.json'),
47
- join(root, '.claude', 'settings.local.json'),
48
- ]) {
49
- const s = readJsonSafe(p);
50
- const v = s?.worktree?.baseRef;
51
- if (typeof v === 'string' && v) value = v;
52
- }
53
- return value;
54
- }
55
-
56
- function headRef(cwd) {
57
- try {
58
- const name = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
59
- if (name && name !== 'HEAD') return name;
60
- return git(['rev-parse', 'HEAD'], cwd) || null;
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- // Unset → the `morph/<f>` branch `cwd` is on: a sub-agent tree born from the default branch lacks the feature's work.
67
- export function resolveBase(setting, cwd) {
68
- if (setting === 'head') return headRef(cwd);
69
- if (setting) return null;
70
- const current = headRef(cwd);
71
- return current && /^morph\/.+/.test(current) ? current : null;
72
- }
73
-
74
36
  function isKnownFeature(root, name) {
75
37
  if (existsSync(join(root, '.morph', 'features', name, 'feature.json'))) return true;
76
38
  try {
@@ -92,14 +54,14 @@ function defaultBranch(root) {
92
54
  return null;
93
55
  }
94
56
 
95
- function gitFallback(root, name, base) {
57
+ function gitFallback(root, name) {
96
58
  const branch = `morph-task/${name}`;
97
59
  const container = newWorktreesDir(root);
98
60
  const path = `${container}/${name}`;
99
61
  if (existsSync(path)) return { path };
100
62
  try { git(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], root); }
101
63
  catch {
102
- const from = base || defaultBranch(root);
64
+ const from = defaultBranch(root);
103
65
  git(from ? ['branch', branch, from] : ['branch', branch], root);
104
66
  }
105
67
  mkdirSync(container, { recursive: true });
@@ -113,21 +75,20 @@ export async function run(payload) {
113
75
  if (!root) return { error: `worktree-create: "${start}" is not inside a git repository` };
114
76
 
115
77
  const name = slugify(payload?.name);
116
- const base = resolveBase(readBaseRefSetting(root), start);
117
78
 
118
79
  if (!stateExists(root)) {
119
- try { return gitFallback(root, name, base); }
80
+ try { return gitFallback(root, name); }
120
81
  catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
121
82
  }
122
83
 
123
84
  const args = isKnownFeature(root, name)
124
85
  ? ['worktree', 'setup', name, '--json']
125
- : ['worktree', 'provision', name, '--json', ...(base ? ['--base', base] : [])];
86
+ : ['worktree', 'provision', name, '--json'];
126
87
 
127
88
  const r = runMorphSpec(args, { cwd: root });
128
89
  if (r.unavailable) {
129
90
  process.stderr.write('worktree-create: morph-spec CLI not found — creating the tree with plain git under worktrees/ (no infra links, no port block). Run `morph-spec worktree link --all` inside it.\n');
130
- try { return gitFallback(root, name, base); }
91
+ try { return gitFallback(root, name); }
131
92
  catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
132
93
  }
133
94
  if (r.stderr?.trim()) process.stderr.write(r.stderr);
@@ -49,11 +49,7 @@ export async function run(payload) {
49
49
  // CLAUDE_PROJECT_DIR first: it is what the project hooks resolve `.morph/framework` against.
50
50
  const anchored = anchorSessionCommand(command, process.env.CLAUDE_PROJECT_DIR || payload?.cwd || '');
51
51
  if (anchored) {
52
- return updatedInputResult(
53
- { ...payload.tool_input, command: anchored.command },
54
- 'PreToolUse',
55
- `⚓ âncora da sessão exportada para o CLI: ${ANCHOR_KEY}=${anchored.anchor} — o finish/worktree remove adia o desmonte do worktree em que esta sessão está ancorada`,
56
- );
52
+ return updatedInputResult({ ...payload.tool_input, command: anchored.command }, 'PreToolUse');
57
53
  }
58
54
 
59
55
  const dir = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -63,11 +59,7 @@ export async function run(payload) {
63
59
  const rewritten = rewriteCommand(command, loadEnvFile(manifest));
64
60
  if (!rewritten) return null;
65
61
 
66
- return updatedInputResult(
67
- { ...payload.tool_input, command: rewritten.command },
68
- 'PreToolUse',
69
- `🔌 worktree env injetado (.morph/worktree.env): ${rewritten.injected.join(' ')}`,
70
- );
62
+ return updatedInputResult({ ...payload.tool_input, command: rewritten.command }, 'PreToolUse');
71
63
  } catch {
72
64
  return null;
73
65
  }
Binary file
@@ -9,10 +9,10 @@ export const GATE_STEPS = ['decision', 'approve', 'unapprove'];
9
9
  * The trail `cost --metrics` turns into "what did the human do with each pause":
10
10
  * `gateDecisions` keeps only the latest decision, so the history lives here.
11
11
  */
12
- export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, approvedBy }) {
12
+ export function recordGateEvent(projectPath, { feature, gate, step, pause, signal, source, resolver, approvedBy }) {
13
13
  if (!existsSync(join(projectPath || process.cwd(), '.morph'))) return;
14
14
  const event = { event: GATE_EVENT, feature, gate, step };
15
- if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null });
15
+ if (step === 'decision') Object.assign(event, { pause: pause === true, signal: signal ?? null, source: source ?? null, ...(pause === true ? { resolver: resolver ?? 'human' } : {}) });
16
16
  else event.approvedBy = approvedBy ?? null;
17
17
  appendTelemetryEvent(event, projectPath);
18
18
  }
@@ -8,6 +8,7 @@ import {
8
8
  readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync,
9
9
  } from 'fs';
10
10
  import { dirname, join, resolve } from 'path';
11
+ import { homedir } from 'os';
11
12
  import { fileURLToPath } from 'url';
12
13
  import { readPrimaryHead, resolveGitCommonDir, workTreeTop } from './git-paths.js';
13
14
  import { findFirstOnPath, quoteForCmd } from './morph-cli.js';
@@ -20,6 +21,8 @@ import {
20
21
 
21
22
  export { primaryTree };
22
23
 
24
+ export const GRAPHIFY_PACKAGE = 'graphifyy';
25
+
23
26
  // Ficam fora de propósito: `.graphify_root` guarda o caminho absoluto da raiz, `cache/`
24
27
  // é indexado pelo mtime da origem, e excluir `.graphifyignore` esconderia o da raiz.
25
28
  const SEED_FILES = ['manifest.json', '.graphify_analysis.json', '.graphify_labels.json', 'graph.json'];
@@ -131,12 +134,26 @@ function appendRefreshLog(dir, entry) {
131
134
  } catch { /* log é diagnóstico, nunca bloqueia */ }
132
135
  }
133
136
 
137
+ // The tool venv's own executable, for when the shim on PATH is missing: `uv tool install` writes the shim
138
+ // under $HOME, so an install run with another HOME leaves the venv in place and the shim elsewhere.
139
+ function venvGraphify(env, platform) {
140
+ const win = platform === 'win32';
141
+ const home = (win ? env.USERPROFILE : env.HOME) || homedir();
142
+ const venvs = [
143
+ join(env.UV_TOOL_DIR || (win ? join(env.APPDATA || join(home, 'AppData', 'Roaming'), 'uv', 'tools')
144
+ : join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'uv', 'tools')), GRAPHIFY_PACKAGE),
145
+ join(env.PIPX_HOME || (win ? join(home, 'pipx') : join(home, '.local', 'pipx')), 'venvs', GRAPHIFY_PACKAGE),
146
+ ];
147
+ const file = win ? join('Scripts', 'graphify.exe') : join('bin', 'graphify');
148
+ return venvs.map((v) => join(v, file)).find((f) => existsSync(f)) ?? null;
149
+ }
150
+
134
151
  // No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat` passa pelo shell.
135
- export function resolveGraphify(env = process.env) {
136
- const names = process.platform === 'win32'
152
+ export function resolveGraphify(env = process.env, platform = process.platform) {
153
+ const names = platform === 'win32'
137
154
  ? ['graphify.exe', 'graphify.cmd', 'graphify.bat']
138
155
  : ['graphify'];
139
- const file = findFirstOnPath(names, env);
156
+ const file = findFirstOnPath(names, env) ?? venvGraphify(env, platform);
140
157
  return file ? { file, shell: /\.(cmd|bat)$/i.test(file) } : null;
141
158
  }
142
159
 
@@ -45,6 +45,8 @@ function skipWrappers(tokens) {
45
45
  }
46
46
  }
47
47
 
48
+ // The package managers' "run in that directory" flags: `npm --prefix <dir> test` is still `npm test`.
49
+ const PM_DIR_FLAGS = new Set(['--prefix', '--cwd', '-C', '--dir']);
48
50
  const DOCKER_VALUE_FLAGS = new Set(['-f', '--file', '-p', '--project-name', '--profile', '--env-file', '--project-directory', '-H', '--host', '--context']);
49
51
 
50
52
  function firstArg(args, valueFlags = new Set()) {
@@ -59,7 +61,8 @@ function kindOfTokens(tokens) {
59
61
  const [head, ...args] = tokens;
60
62
  if (!head || args.some((a) => INFO_FLAGS.has(a))) return null;
61
63
  const exe = executable(head);
62
- const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS : undefined);
64
+ const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
65
+ : PACKAGE_MANAGERS.has(exe) ? PM_DIR_FLAGS : undefined);
63
66
 
64
67
  if (exe === 'dotnet') return DOTNET_HEAVY.has(sub) ? `dotnet ${sub}` : null;
65
68
  if (exe === 'msbuild') return 'msbuild';
@@ -148,6 +151,34 @@ function testKindOfTokens(tokens) {
148
151
  return kind && TEST_LABEL.test(kind) ? kind : null;
149
152
  }
150
153
 
154
+ const DOTNET_FILTERS = /^(--filter|--tests|--treenode-filter|\/testcasefilter:)/i;
155
+ const NAME_FILTERS = /^(-t|-g|--grep|--testNamePattern|--test-name-pattern|--testPathPattern|--filter)(=|$)/;
156
+ // A bare word may be a flag's value (`--reporter dot`); only a path-shaped argument selects files.
157
+ const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
158
+ const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
159
+
160
+ function isFilteredRun(kind, tokens) {
161
+ const runnerAt = executable(tokens[0]) === 'node'
162
+ ? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
163
+ : 0;
164
+ const args = tokens.slice(runnerAt + 1);
165
+ if (kind.startsWith('dotnet')) return args.some((a) => DOTNET_FILTERS.test(a));
166
+ for (let i = 0; i < args.length; i++) {
167
+ if (PATH_VALUE_FLAGS.has(args[i])) { i++; continue; }
168
+ if (NAME_FILTERS.test(args[i]) || (!args[i].startsWith('-') && TEST_PATH.test(args[i]))) return true;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ /** `{ kind, filtered }` for the first test run in `command`, `null` when there is none. */
174
+ export function testRunCall(command) {
175
+ for (const tokens of commandTokens(command)) {
176
+ const kind = testKindOfTokens(tokens);
177
+ if (kind) return { kind, filtered: isFilteredRun(kind, tokens) };
178
+ }
179
+ return null;
180
+ }
181
+
151
182
  export function testRunKind(command) {
152
183
  for (const tokens of commandTokens(command)) {
153
184
  const kind = testKindOfTokens(tokens);
@@ -12,7 +12,7 @@ export const HEAVY_DEFAULTS = Object.freeze({
12
12
  maxHoldMinutes: 90,
13
13
  shutdownBuildServer: true,
14
14
  disableNodeReuse: false,
15
- gateBash: false,
15
+ gateBash: true,
16
16
  });
17
17
 
18
18
  export const HEAVY_KINDS = Object.freeze(['unit', 'build', 'e2e']);
@@ -4,26 +4,71 @@
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { derivePhaseForFeature } from './state-reader.js';
7
- import { sliceProgress, readySlices } from './slice-progress.js';
7
+ import { implementProgress } from './report-progress.js';
8
8
 
9
9
  export const FIX_ATTEMPTS = 3;
10
10
 
11
11
  const step = (action, command, reason, extra = {}) => ({ action, command, reason, ...extra });
12
12
 
13
+ const HOW = {
14
+ plan: (f) => [
15
+ `Rode \`/morph-plan ${f}\`: escreva 2-plan/plan.md e rode \`morph-spec plan-check ${f}\` até zero erro.`,
16
+ ],
17
+ gate: (f) => [
18
+ `Rode \`morph-spec gate-check ${f} plan\`; se pausar, AskUserQuestion "Gate do plano — Plano de ${f} aprovado?".`,
19
+ ],
20
+ design: (f) => [
21
+ `Rode \`/morph-uiux ${f}\` (opções em 1-design/opcoes/, protótipo e prints); depois \`morph-spec gate-check ${f} uiux\`.`,
22
+ `Se pausar, AskUserQuestion "Gate uiux — Design de ${f} aprovado?".`,
23
+ ],
24
+ implement: (f) => [
25
+ 'Implemente os Cn pendentes em sequência; ao fechar cada um, marque `feito` + evidência (teste ou arquivo:linha) '
26
+ + `em 3-implement/report.md e commite \`wip(${f}): Cn …\`.`,
27
+ ],
28
+ decide: () => [
29
+ 'Leia a pergunta do Cn bloqueado em 3-implement/report.md ## Notas; decida pelo plano/ADR ou faça AskUserQuestion com ela.',
30
+ 'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando.',
31
+ ],
32
+ verify: (f) => [
33
+ `\`morph-spec verify ${f} --json\` sem filtro; passou de 10 min, run_in_background.`,
34
+ ],
35
+ fix: (f) => [
36
+ `Leia o \`feedback\` do último verify, corrija a causa e rode \`morph-spec verify ${f} --json\` de novo, sem filtro.`,
37
+ ],
38
+ escalate: () => [
39
+ 'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
40
+ ],
41
+ review: () => [
42
+ 'Siga .claude/skills/morph-apply/references/review.md.',
43
+ ],
44
+ finish: (f) => [
45
+ `AskUserQuestion header "Integração" (PR ou merge); depois \`morph-spec finish ${f} --pr\` ou \`--merge\`.`,
46
+ ],
47
+ status: (f) => [
48
+ `Rode \`morph-spec status ${f}\` e confira a fase em .morph/features/${f}/feature.json.`,
49
+ ],
50
+ };
51
+
13
52
  /**
14
- * @param {{ feature: string, phase: string, slices: object|null, gates: object,
53
+ * @param {{ feature: string, phase: string, progress: object|null, gates: object,
15
54
  * verification: { status?: string }|null, verificationStale?: boolean, failStreak?: number }} s
16
55
  * `verificationStale` undefined = unknown (the hook has no git): read as fresh.
17
56
  */
18
57
  export function decideNext(s) {
58
+ const n = decide(s);
59
+ const how = (HOW[n.action] ?? HOW.status)(s.feature);
60
+ const hotfix = s.workType === 'hotfix' && n.command.startsWith('/morph-hotfix');
61
+ return { ...n, how: hotfix ? ['Fluxo de hotfix: siga .claude/skills/morph-apply/references/hotfix.md.', ...how] : how };
62
+ }
63
+
64
+ function decide(s) {
19
65
  const f = s.feature;
20
66
  const approved = (g) => s.gates?.[g]?.approved === true;
21
- // The hotfix pipeline (scout, human approval, ship) lives in its own skill, end to end.
22
67
  const skill = (name) => (s.workType === 'hotfix' ? `/morph-hotfix ${f}` : `/${name} ${f}`);
23
68
 
24
69
  switch (s.phase) {
25
70
  case 'plan':
26
- return s.slices
71
+ return s.progress
27
72
  ? step('gate', `morph-spec gate-check ${f} plan`, 'plano extraído: o gate do plano decide a pausa')
28
73
  : step('plan', `/morph-plan ${f}`, 'sem checks.json: escreva o plan.md e rode plan-check até zero erro');
29
74
 
@@ -31,26 +76,20 @@ export function decideNext(s) {
31
76
  return step('design', `morph-spec gate-check ${f} uiux`, 'complete 1-design/ (morph-uiux) e passe o gate de uiux');
32
77
 
33
78
  case 'implement': {
34
- if (!s.slices) return step('plan', skill('morph-plan'), 'sem checks.json: plan.md curto (uma fatia) e plan-check até zero erro');
35
- if (s.slices.done < s.slices.total) {
36
- const ready = readySlices(s.slices);
37
- if (!ready.length) {
38
- const waiting = s.slices.slices.filter((sl) => sl.status === 'bloqueado').map((sl) => sl.id);
39
- if (waiting.length) {
40
- return step('decide', 'AskUserQuestion (decisão da fatia)',
41
- 'relatório BLOQUEADO: decida pelo plano/ADR ou pergunte, e retome o MESMO agente com a resposta — nunca re-despache',
42
- { slices: waiting });
43
- }
44
- return step('blocked', `morph-spec report-check ${f}`, 'fatias pendentes sem dependência satisfeita');
45
- }
46
- const partial = ready.filter((sl) => sl.status === 'parcial').map((sl) => sl.id);
47
- return step('dispatch', skill('morph-apply'),
48
- partial.length ? `retome ${partial.join(', ')} (PARCIAL) e despache as prontas` : 'despache as fatias prontas',
49
- { slices: ready.map((sl) => sl.id) });
79
+ const p = s.progress;
80
+ if (!p) return step('plan', skill('morph-plan'), 'sem checks.json: plan.md curto e plan-check até zero erro');
81
+ const blocked = p.checks.filter((c) => c.status === 'bloqueado').map((c) => c.id);
82
+ if (blocked.length) {
83
+ return step('decide', 'AskUserQuestion (decisão do cenário)',
84
+ 'Cn bloqueado no report.md: decida pelo plano/ADR ou pergunte, e siga implementando', { checks: blocked });
85
+ }
86
+ const pending = p.checks.filter((c) => c.status === 'pendente').map((c) => c.id);
87
+ if (pending.length) {
88
+ return step('implement', skill('morph-apply'), 'implemente nesta sessão e marque cada Cn feito no report.md', { checks: pending });
50
89
  }
51
90
  const v = s.verification;
52
91
  if (!v?.status || s.verificationStale === true) {
53
- return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : 'todas as fatias relatadas, sem verify');
92
+ return step('verify', `morph-spec verify ${f}`, v?.status ? 'a árvore andou desde o último verify' : 'todo Cn feito, sem verify');
54
93
  }
55
94
  if (v.status === 'fail') {
56
95
  const streak = s.failStreak ?? 0;
@@ -77,8 +116,8 @@ export function decideNext(s) {
77
116
  }
78
117
 
79
118
  export function formatNext(n) {
80
- const slices = n.slices?.length ? ` [${n.slices.join(', ')}]` : '';
81
- return `próximo: ${n.action}${slices} → ${n.command} — ${n.reason}`;
119
+ const ids = n.checks?.length ? ` [${n.checks.join(', ')}]` : '';
120
+ return `próximo: ${n.action}${ids} → ${n.command} — ${n.reason}`;
82
121
  }
83
122
 
84
123
  /** Facts readable from `hooks/shared`; the CLI adds `verificationStale` (git) on top. Null without feature.json. */
@@ -99,7 +138,7 @@ export function collectNextSignals(featureName, cwd) {
99
138
  workType: feature.workType ?? 'feature',
100
139
  worktreePath: feature.worktree?.path ?? null,
101
140
  phase,
102
- slices: sliceProgress(featureDir),
141
+ progress: implementProgress(featureDir),
103
142
  gates: feature.approvals || feature.approvalGates || {},
104
143
  verification: feature.verification ?? null,
105
144
  failStreak: feature.verifications?.feature?.failStreak ?? 0,
@@ -12,10 +12,10 @@ export const PHASE_DIRS = {
12
12
  export const OUTPUT_PHASE_MAP = {
13
13
  plan: 'plan',
14
14
  checks: 'plan',
15
- uiDesignSystem: 'uiux',
15
+ planCritique: 'plan',
16
+ planView: 'plan',
16
17
  uiMockups: 'uiux',
17
- uiComponents: 'uiux',
18
- uiFlows: 'uiux',
18
+ implementReport: 'implement',
19
19
  evidencias: 'implement',
20
20
  verifierReport: 'review',
21
21
  reviewerReport: 'review',
@@ -25,10 +25,10 @@ export const OUTPUT_PHASE_MAP = {
25
25
  export const FILENAME_TO_OUTPUT = {
26
26
  'plan.md': 'plan',
27
27
  'checks.json': 'checks',
28
- 'design-system.md': 'uiDesignSystem',
28
+ 'critica.md': 'planCritique',
29
+ 'plan.html': 'planView',
29
30
  'mockups.md': 'uiMockups',
30
- 'components.md': 'uiComponents',
31
- 'flows.md': 'uiFlows',
31
+ 'report.md': 'implementReport',
32
32
  'verifier.md': 'verifierReport',
33
33
  'reviewer.md': 'reviewerReport',
34
34
  'security.md': 'securityReport',
@@ -37,10 +37,7 @@ export const FILENAME_TO_OUTPUT = {
37
37
  export const PROTECTED_SPEC_FILES = {
38
38
  'plan.md': 'plan',
39
39
  'checks.json': 'plan',
40
- 'design-system.md': 'uiux',
41
40
  'mockups.md': 'uiux',
42
- 'components.md': 'uiux',
43
- 'flows.md': 'uiux',
44
41
  };
45
42
 
46
43
 
@@ -0,0 +1,86 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
5
+ export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
6
+
7
+ export const STATUSES = ['pendente', 'feito', 'bloqueado'];
8
+
9
+ export function checksPath(featureDir) {
10
+ return join(featureDir, '2-plan', 'checks.json');
11
+ }
12
+
13
+ export function reportPath(featureDir) {
14
+ return join(featureDir, '3-implement', 'report.md');
15
+ }
16
+
17
+ const ROW = /^\|\s*(C\d+)\s*\|\s*([^|]*?)\s*\|\s*(.*?)\s*\|?\s*$/;
18
+
19
+ /** Status per Cn from the report's table; an unknown status reads as `pendente`. */
20
+ export function parseReport(text) {
21
+ const rows = new Map();
22
+ for (const line of String(text ?? '').split(/\r?\n/)) {
23
+ const m = ROW.exec(line);
24
+ if (!m) continue;
25
+ const status = m[2].toLowerCase();
26
+ rows.set(m[1], { status: STATUSES.includes(status) ? status : 'pendente', evidence: m[3] });
27
+ }
28
+ return rows;
29
+ }
30
+
31
+ // Features started before 10.0 have per-slice reports and no report.md: a Cn counts as done when a
32
+ // slice that covers it has a final report, so an in-flight feature keeps moving after the upgrade.
33
+ function legacyRows(checks, readSliceReport) {
34
+ const rows = new Map();
35
+ for (const slice of Array.isArray(checks.slices) ? checks.slices : []) {
36
+ const first = String(readSliceReport(slice.id) ?? '').split(/\r?\n/).find((l) => l.trim() !== '');
37
+ if (first === undefined) continue;
38
+ const status = /^[\s#>*_]*BLOQUEADO\b/.test(first) ? 'bloqueado' : PARTIAL_MARKER.test(first) ? null : 'feito';
39
+ if (!status) continue;
40
+ for (const id of slice.covers ?? []) {
41
+ if (rows.get(id)?.status !== 'bloqueado') rows.set(id, { status, evidence: `${slice.id}.md` });
42
+ }
43
+ }
44
+ return rows;
45
+ }
46
+
47
+ /** @returns {{ total, done, blocked, pending, checks: Array<{id, status, evidence}>, legacy: boolean } | null} */
48
+ export function implementProgressFrom(checksText, reportText, readSliceReport = () => null) {
49
+ let checks;
50
+ try {
51
+ checks = JSON.parse(checksText ?? '');
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (!Array.isArray(checks?.checks)) return null;
56
+ const legacy = reportText == null && Array.isArray(checks.slices);
57
+ const rows = legacy ? legacyRows(checks, readSliceReport) : parseReport(reportText);
58
+ const out = checks.checks
59
+ .filter((c) => c && typeof c.id === 'string')
60
+ .map((c) => ({ id: c.id, status: rows.get(c.id)?.status ?? 'pendente', evidence: rows.get(c.id)?.evidence ?? '' }));
61
+ const count = (status) => out.filter((c) => c.status === status).length;
62
+ return { total: out.length, done: count('feito'), blocked: count('bloqueado'), pending: count('pendente'), checks: out, legacy };
63
+ }
64
+
65
+ function readOrNull(path) {
66
+ try {
67
+ return existsSync(path) ? readFileSync(path, 'utf8') : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function implementProgress(featureDir) {
74
+ const sliceDir = join(featureDir, '3-implement', 'reports');
75
+ return implementProgressFrom(
76
+ readOrNull(checksPath(featureDir)),
77
+ readOrNull(reportPath(featureDir)),
78
+ (id) => (existsSync(sliceDir) ? readOrNull(join(sliceDir, `${id}.md`)) : null),
79
+ );
80
+ }
81
+
82
+ /** The report template body for a feature's checks: every Cn starts `pendente`. */
83
+ export function reportSkeleton(featureName, checkIds) {
84
+ const rows = checkIds.map((id) => `| ${id} | pendente | |`).join('\n');
85
+ return `# ${featureName}\n\n| Cn | status | evidência |\n|----|--------|-----------|\n${rows}\n\n## Notas\n\n`;
86
+ }
@@ -131,8 +131,9 @@ export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
131
131
  // not advance the phase.
132
132
  export const PHASE_ARTIFACTS = [
133
133
  ['4-review', 'review', 'verifier.md'],
134
+ ['3-implement', 'implement', 'report.md'],
134
135
  ['3-implement', 'implement', 'reports/'],
135
- ['1-design', 'uiux', 'design-system.md'],
136
+ ['1-design', 'uiux', 'mockups.md'],
136
137
  ['2-plan', 'plan', 'plan.md'],
137
138
  ];
138
139
 
@@ -180,7 +181,7 @@ export function derivePhaseFrom(feature, hasArtifact) {
180
181
 
181
182
  // An approved gate advances the phase before the next artifact exists, or
182
183
  // that artifact could never be written.
183
- const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
184
+ const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/mockups.md`);
184
185
  const implied = gateImpliedPhaseFrom(feature, designStarted);
185
186
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
186
187
  }