@polymorphism-tech/morph-spec 9.7.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  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 +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { buildRecap, changeSummary, recapScenes, renderRecapHtml } from '../../lib/recap/recap.js';
6
+ import { narrateWithPiper, resolvePiper } from '../../lib/recap/piper.js';
7
+
8
+ export const RECAP_FILES = { html: 'recap.html', data: 'recap.json' };
9
+
10
+ /** Writes `recap.html` + `recap.json` into `targetDir` from the live feature folder. Never throws. */
11
+ export function writeRecap(featureDir, targetDir, feature, { cwd }) {
12
+ try {
13
+ const data = buildRecap(featureDir, feature, { changes: changeSummary(cwd) });
14
+ writeFileSync(join(targetDir, RECAP_FILES.data), JSON.stringify(data, null, 2));
15
+ writeFileSync(join(targetDir, RECAP_FILES.html), renderRecapHtml(data));
16
+ return { ok: true, path: join(targetDir, RECAP_FILES.html) };
17
+ } catch (err) {
18
+ return { ok: false, reason: err.message };
19
+ }
20
+ }
21
+
22
+ // Active feature: rebuilt from its folder and the branch diff. Archived: the data the finish froze,
23
+ // since the archive keeps neither the reports nor the branch.
24
+ export function recapCommand(feature, options = {}) {
25
+ const cwd = options.path || process.cwd();
26
+ const active = join(cwd, '.morph', 'features', feature);
27
+ const archived = join(cwd, '.morph', 'archive', feature);
28
+ let data;
29
+ let dir;
30
+ if (existsSync(active)) {
31
+ dir = active;
32
+ data = buildRecap(active, feature, { changes: changeSummary(cwd) });
33
+ writeFileSync(join(dir, RECAP_FILES.data), JSON.stringify(data, null, 2));
34
+ } else if (existsSync(join(archived, RECAP_FILES.data))) {
35
+ dir = archived;
36
+ data = JSON.parse(readFileSync(join(archived, RECAP_FILES.data), 'utf8'));
37
+ } else {
38
+ const payload = { ok: false, reason: existsSync(archived) ? 'archived-without-recap' : 'feature-not-found' };
39
+ if (options.json) console.log(JSON.stringify(payload));
40
+ else console.error(chalk.yellow(`\n○ ${payload.reason === 'feature-not-found' ? `Feature "${feature}" não existe.` : `"${feature}" foi arquivada antes do recap existir.`}\n`));
41
+ process.exit(1);
42
+ }
43
+
44
+ let narration = 'sistema';
45
+ let path = join(dir, RECAP_FILES.html);
46
+ let note = null;
47
+ if (options.piper) {
48
+ const piper = resolvePiper();
49
+ const audio = piper.ok ? narrateWithPiper(recapScenes(data), piper) : null;
50
+ if (audio) {
51
+ // Audio is megabytes: it goes to a temp file, never into the committed archive.
52
+ path = join(tmpdir(), `morph-recap-${feature}.html`);
53
+ writeFileSync(path, renderRecapHtml(data, { audio }));
54
+ narration = 'piper';
55
+ } else {
56
+ note = piper.ok ? 'o piper falhou numa cena — ficou a voz do sistema' : piper.reason;
57
+ }
58
+ }
59
+ if (narration === 'sistema') writeFileSync(path, renderRecapHtml(data));
60
+
61
+ if (options.json) {
62
+ console.log(JSON.stringify({ ok: true, path, narration, note }));
63
+ return;
64
+ }
65
+ console.log(chalk.green(`\n✅ Recap de ${feature}: ${path}`));
66
+ console.log(chalk.gray(` narração: ${narration === 'piper' ? 'Piper (áudio embutido)' : 'voz do sistema, pelo navegador'}${note ? ` — ${note}` : ''}\n`));
67
+ }
@@ -1,12 +1,7 @@
1
- // Judged from observed evidence, never the agent's word: did the WORK land, independently of
2
- // the REPORT? The `tree` block is information only and never changes the verdict.
3
-
4
1
  import chalk from 'chalk';
5
- import { existsSync, readFileSync, statSync } from 'fs';
2
+ import { existsSync, readFileSync } from 'fs';
6
3
  import { join } from 'path';
7
- import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
8
- import { sliceReportPath, inspectReport, REPORT_MARKERS } from '../../../framework/hooks/shared/slice-progress.js';
9
- import { loadTelemetryAllTrees, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
4
+ import { implementProgress, parseReport, reportPath } from '../../../framework/hooks/shared/report-progress.js';
10
5
  import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
11
6
  import { atomicWriteJson } from '../../core/state/state-manager.js';
12
7
  import { minimatch } from 'minimatch';
@@ -15,9 +10,10 @@ import { verifierModelFor } from '../../lib/risk-detector.js';
15
10
  import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-branch.js';
16
11
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
17
12
  import { readPlanChecks } from '../../lib/plan/checks.js';
18
- import { featureGeneratesMedia, collectReviewRequirement } from './gate-check.js';
19
-
20
- export { inspectReport, REPORT_MARKERS };
13
+ import { isTestPath } from '../../lib/graph/sanitize.js';
14
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
15
+ import { collectReviewRequirement } from './gate-check.js';
16
+ import { featureGeneratesMedia } from '../../lib/plan/media.js';
21
17
 
22
18
  /** Each review report: complete when it carries a readable verdict and is not partial. */
23
19
  export function inspectReviewReports(cwd, feature) {
@@ -30,8 +26,12 @@ export function inspectReviewReports(cwd, feature) {
30
26
  return out;
31
27
  }
32
28
 
29
+ function readOrEmpty(path) {
30
+ try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
31
+ }
32
+
33
33
  // The verifier routes to opus on prompt/model or media; security is dispatched only when required.
34
- function reviewDispatch(cwd, feature, slices) {
34
+ function reviewDispatch(cwd, feature) {
35
35
  let changedFiles = [];
36
36
  try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
37
37
  const watch = readMorphConfig(cwd)?.project?.evals?.watch;
@@ -43,139 +43,45 @@ function reviewDispatch(cwd, feature, slices) {
43
43
  required,
44
44
  verifier: verifierModelFor({
45
45
  promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
46
- generatesMedia: featureGeneratesMedia(slices),
46
+ generatesMedia: featureGeneratesMedia(readOrEmpty(getAbsoluteOutputPath(cwd, feature, 'plan'))),
47
47
  }),
48
48
  reviewer: { model: 'sonnet', reasons: [] },
49
49
  security: { required: security.required, model: 'opus', reasons: security.reasons },
50
50
  };
51
51
  }
52
52
 
53
- // Two clocks: the report mtime comes from the filesystem, the event `ts` from the hook. Two
54
- // seconds absorb their skew and are far less than any real work done after reporting.
55
- export const STALE_TOLERANCE_MS = 2000;
56
-
57
53
  const slash = (p) => String(p || '').replace(/\\/g, '/');
58
-
59
- function toTreeRelative(target, treePath) {
60
- const norm = slash(target);
61
- const root = slash(treePath).replace(/\/+$/, '');
62
- if (!root) return norm;
63
- // Case-insensitive prefix: on Windows the same root arrives as `R:/repo` or `r:/repo`.
64
- return norm.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? norm.slice(root.length + 1) : norm;
65
- }
66
-
67
- const reportSuffix = (feature, sliceId) => `.morph/features/${feature}/3-implement/reports/${sliceId}.md`.toLowerCase();
68
-
69
- function loadWrites(cwd) {
70
- let trees = [];
71
- try { trees = loadTelemetryAllTrees(cwd); } catch { trees = []; }
72
- const events = [];
73
- for (const tree of trees) {
74
- for (const e of tree.events) {
75
- if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
76
- if (isFailedToolCall(e)) continue;
77
- const ts = Date.parse(e?.ts ?? '');
78
- if (!Number.isFinite(ts)) continue;
79
- events.push({ ...e, rel: toTreeRelative(e.target, tree.path), tsMs: ts });
80
- }
54
+ const isTestRef = (p) => isTestPath(p) || /\.(test|spec)\.[a-z]+$|Tests?\.cs$/i.test(p);
55
+
56
+ /** Cn whose evidence in the report cites tests in more than one file, as `{ id, files }`. */
57
+ export function checksSpread(reportText) {
58
+ const spread = [];
59
+ for (const [id, { evidence }] of parseReport(reportText)) {
60
+ const files = [...new Set((evidence.match(/[\w@./\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef))];
61
+ if (files.length > 1) spread.push({ id, files });
81
62
  }
82
- return { available: trees.some((t) => t.events.length > 0), writes: dedupeEvents(events) };
63
+ return spread;
83
64
  }
84
65
 
85
- // The owning dispatch is the latest sub-agent write of the report; a later orchestrator edit
86
- // does not hide it. Without a report, any write to one of the slice files says the work landed.
87
- function observedEvidence(telemetry, feature, slice) {
88
- if (!telemetry.available) return { available: false };
89
- const suffix = reportSuffix(feature, slice.id);
90
- const isReport = (w) => slash(w.target).toLowerCase().endsWith(suffix);
91
- const declared = new Set((slice.files ?? []).map((f) => slash(f).replace(/^\.\//, '').toLowerCase()));
92
- const isSliceFile = (w) => declared.has(w.rel.toLowerCase());
93
-
94
- const reportWrites = telemetry.writes.filter((w) => isReport(w) && w.agentId);
95
- const agentId = reportWrites.length ? reportWrites[reportWrites.length - 1].agentId : null;
96
- if (!agentId) {
97
- return { available: true, agentId: null, sliceWrites: telemetry.writes.filter(isSliceFile).length };
98
- }
99
-
100
- const work = telemetry.writes.filter((w) => w.agentId === agentId && !isReport(w) && !w.rel.includes('.morph/'));
101
- const observed = [...new Set(work.map((w) => w.rel))];
102
- const observedKeys = new Set(observed.map((f) => f.toLowerCase()));
103
- return {
104
- available: true,
105
- agentId,
106
- observed,
107
- lastWriteAt: work.length ? new Date(Math.max(...work.map((w) => w.tsMs))).toISOString() : null,
108
- undeclared: observed.filter((f) => !declared.has(f.toLowerCase())),
109
- unobserved: (slice.files ?? []).filter((f) => !observedKeys.has(slash(f).replace(/^\.\//, '').toLowerCase())),
110
- sliceWrites: work.filter(isSliceFile).length,
111
- };
112
- }
113
-
114
- function isReportStale(reportFsPath, observed) {
115
- if (!observed?.agentId || !observed.lastWriteAt) return false;
116
- try {
117
- return statSync(reportFsPath).mtimeMs + STALE_TOLERANCE_MS < Date.parse(observed.lastWriteAt);
118
- } catch {
119
- return false;
120
- }
121
- }
122
-
123
- function decide(report, observed, stale) {
124
- if (report.exists && report.complete) {
125
- // Structural integrity answers "is the report complete?", never "is it
126
- // still true?": an agent can write it early, keep working and be cut off.
127
- return stale
128
- ? { verdict: 'report-stale', action: 'refresh-from-evidence' }
129
- : { verdict: 'ok', action: 'proceed' };
130
- }
131
- // PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
132
- // `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
133
- if (report.exists && report.blocked) return { verdict: 'report-blocked', action: 'ask-human' };
134
- if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
135
- if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
136
- if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
137
- return observed.sliceWrites > 0
138
- ? { verdict: 'work-landed-report-missing', action: 'accept-degraded' }
139
- : { verdict: 'no-work-no-report', action: 'redispatch' };
140
- }
141
-
142
- function checkSlice(cwd, feature, slice, telemetry) {
143
- const fsPath = sliceReportPath(join(cwd, '.morph', 'features', feature), slice.id);
144
- const report = inspectReport(fsPath, { covers: slice.covers ?? [] });
145
- const observed = observedEvidence(telemetry, feature, slice);
146
- const stale = report.exists && isReportStale(fsPath, observed);
147
- return { slice: slice.id, report, observed, ...decide(report, observed, stale) };
148
- }
149
-
150
- const ACTION_HINT = {
151
- proceed: 'relatório íntegro — siga o fluxo normal.',
152
- 'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
153
- 'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
154
- resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
155
- 'ask-human': 'relatório BLOQUEADO: o sub-agent parou numa decisão que não é dele (requisito que contradiz o plano ou um ADR, escolha do humano) e o relatório traz a pergunta e as opções. NÃO re-despache — a mesma pergunta volta. Se o plano ou um ADR já decide, decida você e registre no relatório; senão, faça a pergunta por `AskUserQuestion`. Depois retome o MESMO agente por `SendMessage` com a resposta.',
156
- 'accept-degraded':'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
157
- redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
158
- };
159
-
160
66
  function snapshotPath(cwd, feature) {
161
67
  return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
162
68
  }
163
69
 
164
70
  // `beforeEval` snapshots after comparing, so the output still shows what changed since the last one.
165
- function treeFor(cwd, feature, beforeEval, slices = []) {
71
+ function treeFor(cwd, feature, beforeEval) {
166
72
  const path = snapshotPath(cwd, feature);
167
73
  let snapshot = null;
168
74
  try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
169
75
  const tree = treeState(cwd, { snapshot });
170
76
  if (!beforeEval) return { tree };
171
- const reviewModels = reviewDispatch(cwd, feature, slices);
77
+ const reviewModels = reviewDispatch(cwd, feature);
172
78
  const next = snapshotTree(cwd);
173
79
  if (!next) return { tree, beforeEval: null, reviewModels };
174
80
  atomicWriteJson(path, next);
175
81
  return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
176
82
  }
177
83
 
178
- export function reportCheckCommand(feature, sliceIds, options = {}) {
84
+ export function reportCheckCommand(feature, options = {}) {
179
85
  const cwd = options.cwd || process.cwd();
180
86
  const emit = (obj, code) => {
181
87
  if (options.json) console.log(JSON.stringify(obj, null, 2));
@@ -187,20 +93,15 @@ export function reportCheckCommand(feature, sliceIds, options = {}) {
187
93
  if (!checks) {
188
94
  emit({ error: `feature "${feature}" sem 2-plan/checks.json legível (${status}) — rode morph-spec plan-check ${feature}`, feature }, 1);
189
95
  }
190
- const slices = checks.slices;
191
- const ids = Array.isArray(sliceIds) ? sliceIds : sliceIds ? [sliceIds] : [];
192
- const unknown = ids.filter((id) => !slices.some((s) => s.id === id));
193
- if (unknown.length) emit({ error: `fatia(s) ${unknown.join(', ')} não existe(m) no plano de ${feature}`, feature }, 1);
194
-
195
- const telemetry = loadWrites(cwd);
196
- const chosen = ids.length ? ids.map((id) => slices.find((s) => s.id === id)) : slices;
197
- const results = chosen.map((s) => {
198
- const r = checkSlice(cwd, feature, s, telemetry);
199
- return { ...r, hint: ACTION_HINT[r.action] };
200
- });
201
- const clean = results.every((r) => r.verdict === 'ok');
202
- const reviews = ids.length ? undefined : inspectReviewReports(cwd, feature);
203
- emit({ feature, results, ...(reviews ? { reviews } : {}), clean, ...treeFor(cwd, feature, options.beforeEval, slices) }, clean ? 0 : 1);
96
+ const featureDir = join(cwd, '.morph', 'features', feature);
97
+ const progress = implementProgress(featureDir);
98
+ const warnings = checksSpread(readOrEmpty(reportPath(featureDir))).map(({ id, files }) =>
99
+ `${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
100
+ const implement = progress
101
+ ? { total: progress.total, done: progress.done, pending: progress.checks.filter((c) => c.status === 'pendente').map((c) => c.id),
102
+ blocked: progress.checks.filter((c) => c.status === 'bloqueado').map((c) => c.id), legacy: progress.legacy, warnings }
103
+ : null;
104
+ emit({ feature, implement, reviews: inspectReviewReports(cwd, feature), ...treeFor(cwd, feature, options.beforeEval) }, 0);
204
105
  }
205
106
 
206
107
  function printHuman(obj) {
@@ -209,17 +110,12 @@ function printHuman(obj) {
209
110
  return;
210
111
  }
211
112
  console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
212
- if (!obj.results.length) console.log(chalk.gray(' nenhuma fatia no plano — nada a auditar'));
213
- for (const r of obj.results) {
214
- const icon = r.verdict === 'ok' ? chalk.green('✓') : chalk.yellow('⚠');
215
- const who = r.observed.agentId ? `agente ${String(r.observed.agentId).slice(0, 8)}` : (r.observed.available ? 'sem dispatch' : 'sem telemetria');
216
- console.log(` ${icon} ${String(r.slice).padEnd(5)} ${r.verdict.padEnd(32)} ${who}`);
217
- if (r.verdict !== 'ok') {
218
- if (r.report.exists && r.report.missing.length) {
219
- console.log(chalk.gray(` faltando no relatório: ${r.report.missing.join(', ')}`));
220
- }
221
- console.log(chalk.gray(` → ${r.hint}`));
222
- }
113
+ const impl = obj.implement;
114
+ if (impl) {
115
+ const open = [...impl.pending, ...impl.blocked];
116
+ console.log(` ${open.length ? chalk.yellow('⚠') : chalk.green('✓')} report.md ${impl.done}/${impl.total} cenários feitos`
117
+ + `${impl.pending.length ? ` · pendentes: ${impl.pending.join(', ')}` : ''}${impl.blocked.length ? ` · bloqueados: ${impl.blocked.join(', ')}` : ''}`);
118
+ for (const w of impl.warnings) console.log(chalk.yellow(` ⚠ ${w}`));
223
119
  }
224
120
  for (const [role, r] of Object.entries(obj.reviews ?? {})) {
225
121
  if (!r.exists && role === 'security') continue;
@@ -245,9 +141,6 @@ function printTree(tree) {
245
141
  console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
246
142
  for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
247
143
  if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
248
- if (tree.markers.length) {
249
- console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
250
- }
251
144
  if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
252
145
  const since = tree.sinceBeforeEval;
253
146
  if (since) {
@@ -1,16 +1,17 @@
1
1
  import chalk from 'chalk';
2
- import { existsSync, readFileSync } from 'fs';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { atomicWriteJson } from '../../core/state/state-manager.js';
5
5
  import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
6
6
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
7
7
  import { parsePlan } from '../../lib/plan/plan-parser.js';
8
- import { buildChecks, knownPersonas, parallelGroups, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
9
- import { knownSurfaces } from '../../lib/specs/screen-checklist.js';
8
+ import { renderPlanHtml } from '../../lib/plan/plan-html.js';
9
+ import { buildChecks, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
10
10
  import { revocationWarnings } from '../../lib/specs/adr-index.js';
11
11
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
12
12
  import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
13
13
  import { reportDotnetIgnores } from '../state/advance.js';
14
+ import { syncStackRules } from '../../lib/installers/stack-sync.js';
14
15
 
15
16
  function readJson(path) {
16
17
  try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
@@ -21,17 +22,14 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
21
22
  const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
22
23
  const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
23
24
  const base = { feature: featureName, plan: planPath, checksPath };
24
- if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [], groups: [] };
25
+ if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [] };
25
26
 
26
27
  const text = readFileSync(planPath, 'utf8');
27
28
  const plan = parsePlan(text);
28
- const { errors, warnings } = validatePlan(plan, {
29
- personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd), ...planRuleOptions(cwd, featureName),
30
- });
29
+ const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(cwd, featureName), text });
31
30
  warnings.push(...revocationWarnings(cwd, plan));
32
- const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
33
31
  const hash = planHash(text);
34
- const result = { ...base, planHash: hash, errors, warnings, groups, written: false };
32
+ const result = { ...base, planHash: hash, errors, warnings, written: false };
35
33
  if (errors.length > 0) return { ...result, ok: false, exit: 1 };
36
34
 
37
35
  const approved = isGateApproved(featureName, 'plan', cwd);
@@ -41,7 +39,9 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
41
39
  }
42
40
 
43
41
  atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
44
- return { ...result, ok: true, exit: 0, written: true };
42
+ const viewPath = getAbsoluteOutputPath(cwd, featureName, 'planView');
43
+ writeFileSync(viewPath, renderPlanHtml(featureName, plan, { planHash: hash }));
44
+ return { ...result, ok: true, exit: 0, written: true, viewPath };
45
45
  }
46
46
 
47
47
  export function planCheckCommand(featureName, options = {}) {
@@ -54,8 +54,13 @@ export function planCheckCommand(featureName, options = {}) {
54
54
  }
55
55
 
56
56
  const result = runPlanCheck(featureName);
57
+ // After the plan validates: in a greenfield project the plan is where the stack is born.
58
+ const stackSync = result.written
59
+ ? syncStackRules(process.cwd(), { log: (line) => console.error(chalk.gray(line)) })
60
+ : null;
57
61
  const gitignoreAdded = result.written ? seedDotnetBuildIgnores(process.cwd(), { featureName }) : [];
58
62
  if (options.json) {
63
+ if (stackSync?.added.length) result.rulesAdded = stackSync.added;
59
64
  if (gitignoreAdded.length) result.gitignoreAdded = gitignoreAdded;
60
65
  if (gitignoreAdded.skipped) result.gitignoreSkipped = gitignoreAdded.skipped;
61
66
  console.log(JSON.stringify(result, null, 2));
@@ -77,7 +82,7 @@ export function planCheckCommand(featureName, options = {}) {
77
82
  console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
78
83
  } else {
79
84
  console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
80
- result.groups.forEach((g, i) => console.log(chalk.gray(` onda ${i + 1}: ${g.join(', ')}${g.length > 1 ? ' (paralelo)' : ''}`)));
85
+ console.log(chalk.gray(` visão do plano: ${result.viewPath}`));
81
86
  reportDotnetIgnores(gitignoreAdded);
82
87
  }
83
88
  process.exit(result.exit);
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { parsePlan } from '../../lib/plan/plan-parser.js';
5
- import { knownPersonas, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
5
+ import { planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
6
6
 
7
7
  // Matched against section markers (headings and line-start bold labels), never whole-file
8
8
  // substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
@@ -117,7 +117,7 @@ function validatePlanOutput(content, planDir, result) {
117
117
  const plan = parsePlan(content);
118
118
  const root = path.resolve(planDir, '..', '..', '..', '..');
119
119
  const feature = path.basename(path.resolve(planDir, '..'));
120
- const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(root), ...planRuleOptions(root, feature) });
120
+ const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(root, feature), text: content });
121
121
  result.errors.push(...errors);
122
122
  result.warnings.push(...warnings);
123
123
  result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
@@ -137,7 +137,7 @@ function validateChecksOutput(content, planDir, result) {
137
137
  result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
138
138
  return;
139
139
  }
140
- result.sections.found.push(`${doc.checks?.length ?? 0} check(s), ${doc.slices?.length ?? 0} slice(s)`);
140
+ result.sections.found.push(`${doc.checks?.length ?? 0} check(s)`);
141
141
  }
142
142
 
143
143
  function validateMarkdownSections(content, sections, result) {
@@ -14,6 +14,7 @@ import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
14
14
  import { memoryBusyHint } from '../../lib/heavy/live-stacks.js';
15
15
  import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
16
16
  import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
17
+ import { syncStackRules } from '../../lib/installers/stack-sync.js';
17
18
 
18
19
  function statusIcon(status) {
19
20
  if (status === 'pass') return chalk.green('✓');
@@ -57,12 +58,6 @@ function formatVerifyResult(result) {
57
58
  for (const line of result.feedback) lines.push(` ${line}`);
58
59
  }
59
60
 
60
- if (result.traceability?.warnings?.length > 0) {
61
- lines.push('');
62
- lines.push(chalk.yellow('Traceability warnings (advisory — never fails verify):'));
63
- for (const w of result.traceability.warnings) lines.push(` ⚠ ${w}`);
64
- }
65
-
66
61
  lines.push('');
67
62
  return lines.join('\n');
68
63
  }
@@ -180,6 +175,8 @@ export async function verifyCommand(featureName, options = {}) {
180
175
  process.exit(2);
181
176
  }
182
177
 
178
+ syncStackRules(process.cwd(), { persistTags: false, log: (line) => console.error(chalk.gray(line)) });
179
+
183
180
  if (options.ci) {
184
181
  const conflict = CI_EXCLUSIVE_FLAGS.find(([flag]) => options[flag])?.[1];
185
182
  if (conflict) {
@@ -309,6 +306,9 @@ export async function verifyCommand(featureName, options = {}) {
309
306
  for (const u of result.baseline.updates) {
310
307
  console.log(chalk.green(` baseline ${u.id}: ${u.from ?? '—'} → ${u.to}`));
311
308
  }
309
+ for (const id of result.baseline.removed ?? []) {
310
+ console.log(chalk.gray(` baseline ${id}: removida (runner sem baseline, só o piso zero)`));
311
+ }
312
312
  console.log(chalk.gray(`Gravada em ${result.baseline.path}
313
313
  `));
314
314
  }
@@ -3,12 +3,12 @@ import { join } from 'path';
3
3
  export const OUTPUT_SCHEMA = {
4
4
  plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
5
5
  checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
6
+ planCritique: { filename: 'critica.md', phaseDir: '2-plan', phase: 'plan', protected: false },
7
+ planView: { filename: 'plan.html', phaseDir: '2-plan', phase: 'plan', protected: false },
6
8
 
7
- uiDesignSystem: { filename: 'design-system.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
8
9
  uiMockups: { filename: 'mockups.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
9
- uiComponents: { filename: 'components.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
10
- uiFlows: { filename: 'flows.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
11
10
 
11
+ implementReport: { filename: 'report.md', phaseDir: '3-implement', phase: 'implement', protected: false },
12
12
  evidencias: { dirname: 'evidencias', filenamePattern: '^C\\d+\\.md$', displayFilename: '{Cn}.md',
13
13
  phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
14
14
 
@@ -450,10 +450,8 @@ export async function markOutput(featureName, outputType) {
450
450
  if (suggestion) {
451
451
  errorMsg += `\n\nDid you mean '${suggestion}'?`;
452
452
  const kebabAlternative = {
453
- 'uiDesignSystem': 'ui-design-system',
454
453
  'uiMockups': 'ui-mockups',
455
- 'uiComponents': 'ui-components',
456
- 'uiFlows': 'ui-flows'
454
+ 'implementReport': 'implement-report'
457
455
  }[suggestion];
458
456
  if (kebabAlternative) {
459
457
  errorMsg += ` (also accepts '${kebabAlternative}')`;
@@ -462,7 +460,7 @@ export async function markOutput(featureName, outputType) {
462
460
 
463
461
  errorMsg += `\n\nValid types:\n`;
464
462
  errorMsg += ` ${validCamelCaseTypes.join(', ')}\n`;
465
- errorMsg += `\nNote: kebab-case is also accepted (e.g., 'ui-design-system', 'stack-scan')`;
463
+ errorMsg += `\nNote: kebab-case is also accepted (e.g., 'ui-mockups', 'implement-report')`;
466
464
 
467
465
  throw new Error(errorMsg);
468
466
  }
@@ -8,7 +8,7 @@ import { useMorphQuery } from '../hooks/useMorphQuery.js';
8
8
  import { apiBase, projectHref, featureHref } from '../lib/project.js';
9
9
 
10
10
  const KIND_LABEL = {
11
- page: 'Página', project: 'Projeto', feature: 'Feature', slice: 'Fatia', skill: 'Skill', persona: 'Persona',
11
+ page: 'Página', project: 'Projeto', feature: 'Feature', skill: 'Skill', persona: 'Persona',
12
12
  standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia',
13
13
  };
14
14
 
@@ -46,9 +46,6 @@ function featureIndex(project, features, projectName = null) {
46
46
  const where = (text) => (projectName ? (text ? `${projectName}: ${text}` : projectName) : text);
47
47
  for (const f of features || []) {
48
48
  out.push({ kind: 'feature', title: f.name, hint: where(f.description), to: featureHref(project, f.name), hay: `${f.name} ${f.description || ''} ${projectName || ''}` });
49
- for (const sl of f.sliceIndex || []) {
50
- out.push({ kind: 'slice', title: `${sl.id} ${sl.title}`, hint: where(f.name), to: featureHref(project, f.name, '?tab=slices'), hay: `${sl.id} ${sl.title} ${f.name} ${projectName || ''}` });
51
- }
52
49
  }
53
50
  return out;
54
51
  }
@@ -112,7 +109,7 @@ export default function CommandPalette({ open, onClose, portfolio, project }) {
112
109
  value={query}
113
110
  onChange={(e) => setQuery(e.target.value)}
114
111
  onKeyDown={onKey}
115
- placeholder="Buscar features, fatias, skills, personas, standards, seções do guia…"
112
+ placeholder="Buscar features, skills, personas, standards, seções do guia…"
116
113
  className="w-full bg-transparent text-base text-[var(--color-fg)] outline-none focus-visible:outline-none placeholder:text-[var(--color-faint)]"
117
114
  aria-controls="palette-results"
118
115
  aria-activedescendant={results[active] ? `palette-${active}` : undefined}