@polymorphism-tech/morph-spec 9.8.0 → 10.1.1

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 (202) hide show
  1. package/CLAUDE.md +12 -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 +28 -36
  6. package/docs/QUICKSTART.md +31 -191
  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-preflight.md +3 -3
  16. package/framework/commands/morph-review.md +15 -0
  17. package/framework/commands/morph-status.md +1 -1
  18. package/framework/docs/CLI.md +84 -95
  19. package/framework/evals/review-checklist.md +45 -53
  20. package/framework/hooks/README.md +4 -4
  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/next-step.js +63 -24
  38. package/framework/hooks/shared/phase-utils.js +6 -9
  39. package/framework/hooks/shared/report-progress.js +86 -0
  40. package/framework/hooks/shared/state-reader.js +3 -2
  41. package/framework/hooks/statusline.js +332 -0
  42. package/framework/mcp/registry.json +4 -4
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +10 -2
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/data-standards.md +1 -1
  47. package/framework/rules/ef-standards.md +20 -0
  48. package/framework/rules/frontend-standards.md +1 -1
  49. package/framework/rules/hangfire-standards.md +1 -1
  50. package/framework/rules/implement-report.md +25 -0
  51. package/framework/rules/infrastructure-standards.md +2 -0
  52. package/framework/rules/neon-auth-standards.md +1 -1
  53. package/framework/rules/neon-standards.md +3 -3
  54. package/framework/rules/nextjs-standards.md +7 -0
  55. package/framework/rules/plan-grammar.md +53 -0
  56. package/framework/rules/ui-design.md +27 -0
  57. package/framework/schemas/config.schema.json +11 -18
  58. package/framework/schemas/feature.schema.json +7 -1
  59. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  60. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  61. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  62. package/framework/skills/core/morph-apply/SKILL.md +39 -166
  63. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  64. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  65. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  66. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  67. package/framework/skills/core/morph-apply/references/review.md +65 -0
  68. package/framework/skills/core/morph-plan/SKILL.md +49 -109
  69. package/framework/skills/core/morph-plan/references/critique.md +43 -0
  70. package/framework/skills/core/morph-plan/references/interview.md +37 -0
  71. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  72. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  73. package/framework/standards/STANDARDS.json +51 -28
  74. package/framework/standards/STANDARDS.meta.json +16 -19
  75. package/framework/standards/ai-agents/agent-session.md +2 -3
  76. package/framework/standards/ai-agents/context-providers.md +2 -2
  77. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  78. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  79. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
  80. package/framework/standards/ai-agents/setup.md +1 -1
  81. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  82. package/framework/standards/architecture/stack-baseline.md +70 -0
  83. package/framework/standards/backend/database/postgresql/database.md +22 -1
  84. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
  85. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
  86. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
  87. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  88. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  89. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  90. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  91. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  92. package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
  93. package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
  94. package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
  95. package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
  96. package/framework/standards/integration/mcp/mcp-tools.md +20 -20
  97. package/framework/templates/REGISTRY.json +1 -2
  98. package/framework/templates/context/ARCHITECTURE.md +1 -1
  99. package/framework/templates/docs/plan.md +15 -12
  100. package/framework/templates/docs/ui-mockups.md +17 -105
  101. package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
  102. package/package.json +1 -1
  103. package/src/commands/e2e/e2e.js +88 -23
  104. package/src/commands/graph/diff.js +74 -0
  105. package/src/commands/graph/status.js +1 -1
  106. package/src/commands/observability/cost.js +2 -4
  107. package/src/commands/observability/telemetry.js +1 -1
  108. package/src/commands/project/doctor.js +23 -39
  109. package/src/commands/project/fleet.js +5 -6
  110. package/src/commands/project/init.js +63 -36
  111. package/src/commands/project/stack-scan.js +3 -42
  112. package/src/commands/project/stack-set.js +26 -0
  113. package/src/commands/project/status.js +12 -13
  114. package/src/commands/project/update.js +39 -3
  115. package/src/commands/project/worktree.js +3 -14
  116. package/src/commands/state/advance.js +17 -17
  117. package/src/commands/state/approve.js +0 -6
  118. package/src/commands/state/archive.js +27 -1
  119. package/src/commands/state/auto-approval.js +2 -2
  120. package/src/commands/state/create.js +1 -6
  121. package/src/commands/state/gate-check.js +38 -31
  122. package/src/commands/state/gate-decision.js +3 -2
  123. package/src/commands/state/next.js +1 -0
  124. package/src/commands/state/recap.js +67 -0
  125. package/src/commands/state/report-check.js +36 -178
  126. package/src/commands/validation/plan-check.js +16 -11
  127. package/src/commands/validation/validate-feature.js +3 -3
  128. package/src/commands/validation/verify.js +3 -6
  129. package/src/core/paths/output-schema.js +3 -3
  130. package/src/core/state/state-manager.js +2 -4
  131. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  132. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  133. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  134. package/src/dashboard/app/pages/Activity.jsx +4 -0
  135. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  136. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  137. package/src/dashboard/app/pages/Features.jsx +7 -7
  138. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  139. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  140. package/src/dashboard/scanner/activity-scanner.js +29 -0
  141. package/src/dashboard/scanner/agents-scanner.js +11 -14
  142. package/src/dashboard/scanner/features-scanner.js +45 -24
  143. package/src/dashboard/scanner/project-api.js +11 -2
  144. package/src/dashboard/scanner/skills-scanner.js +11 -1
  145. package/src/lib/detectors/index.js +1 -1
  146. package/src/lib/e2e/applicability.js +3 -1
  147. package/src/lib/git/tree-state.js +1 -16
  148. package/src/lib/graph/diff-tree.js +127 -0
  149. package/src/lib/graph/graphify-runtime.js +12 -5
  150. package/src/lib/installers/memory-installer.js +3 -3
  151. package/src/lib/installers/rules-installer.js +57 -0
  152. package/src/lib/installers/stack-assets.js +17 -0
  153. package/src/lib/installers/stack-sync.js +219 -0
  154. package/src/lib/plan/checks.js +61 -109
  155. package/src/lib/plan/critique.js +28 -0
  156. package/src/lib/plan/media.js +42 -0
  157. package/src/lib/plan/plan-html.js +125 -0
  158. package/src/lib/plan/plan-parser.js +34 -45
  159. package/src/lib/recap/piper.js +36 -0
  160. package/src/lib/recap/recap.js +232 -0
  161. package/src/lib/review-reports.js +28 -4
  162. package/src/lib/risk-detector.js +90 -11
  163. package/src/lib/specs/screen-checklist.js +33 -48
  164. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  165. package/src/lib/telemetry/metrics.js +27 -29
  166. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  167. package/src/lib/validators/verify-runner.js +0 -7
  168. package/src/lib/worktree/fleet-scanner.js +7 -20
  169. package/src/lib/worktree/worktree-audit.js +0 -37
  170. package/src/scripts/setup-infra.js +15 -50
  171. package/src/utils/agents-installer.js +27 -201
  172. package/src/utils/hooks-installer.js +11 -95
  173. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  174. package/framework/hooks/claude-code/statusline.py +0 -699
  175. package/framework/hooks/claude-code/statusline.sh +0 -11
  176. package/framework/hooks/shared/agent-reap.js +0 -74
  177. package/framework/hooks/shared/slice-progress.js +0 -110
  178. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  179. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  180. package/framework/skills/core/morph-review/SKILL.md +0 -129
  181. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  182. package/framework/standards/PROVENANCE.json +0 -119
  183. package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
  184. package/src/commands/project/worktree-reap.js +0 -34
  185. package/src/commands/state/handoff.js +0 -65
  186. package/src/commands/validation/mutate.js +0 -263
  187. package/src/commands/validation/scout-check.js +0 -28
  188. package/src/lib/generators/handoff-generator.js +0 -172
  189. package/src/lib/mutation/adapters.js +0 -79
  190. package/src/lib/mutation/commands.js +0 -61
  191. package/src/lib/mutation/dotnet-introspection.js +0 -281
  192. package/src/lib/mutation/manifest.js +0 -144
  193. package/src/lib/mutation/plan.js +0 -44
  194. package/src/lib/mutation/report.js +0 -68
  195. package/src/lib/mutation/runner.js +0 -301
  196. package/src/lib/mutation/schemata.js +0 -60
  197. package/src/lib/mutation/snapshot.js +0 -65
  198. package/src/lib/mutation/subset.js +0 -109
  199. package/src/lib/mutation/trx.js +0 -50
  200. package/src/lib/scout/scout-check.js +0 -293
  201. package/src/lib/validators/traceability-check.js +0 -103
  202. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,40 +1,32 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { Link, useParams, useSearchParams } from 'react-router-dom';
3
3
  import {
4
- ArrowLeft, CheckCircle, CircleDashed, PauseCircle, Circle, WarningCircle, FileText, FileCode, CaretDown, CaretRight,
5
- Rows, Kanban, Copy, Check, ArrowSquareOut,
4
+ ArrowLeft, CheckCircle, PauseCircle, Circle, WarningCircle, FileText, FileCode, Copy, Check, ArrowSquareOut,
6
5
  } from '@phosphor-icons/react';
7
6
  import PhaseRail, { segmentStates } from '../components/PhaseRail.jsx';
8
7
  import KnowledgeMarkdown from '../components/KnowledgeMarkdown.jsx';
9
- import EntityLink from '../components/EntityLink.jsx';
10
8
  import ActivityFeed from '../components/ActivityFeed.jsx';
9
+ import SessionMap from '../components/SessionMap.jsx';
11
10
  import { useMorphQuery } from '../hooks/useMorphQuery.js';
12
11
  import { usePref, useNow } from '../hooks/usePref.js';
13
- import { Segmented, isWorking } from './Features.jsx';
12
+ import { isWorking } from './Features.jsx';
14
13
  import { apiBase, projectHref } from '../lib/project.js';
15
14
  import { PHASES, GATE_LABEL, STATUS_LABEL, LOCATION_LABEL, phaseMeta } from '../lib/phases.js';
16
15
  import { timeAgo, dateTime, num, bytes, verdictLabel } from '../lib/format.js';
17
16
 
18
17
  const TABS = [
19
18
  { id: 'overview', label: 'Visão geral' },
20
- { id: 'slices', label: 'Fatias' },
21
19
  { id: 'checks', label: 'Cenários' },
22
20
  { id: 'docs', label: 'Documentos' },
23
21
  { id: 'activity', label: 'Atividade' },
24
22
  ];
25
23
 
26
- const SLICE_STATUS = {
27
- feito: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
28
- parcial: { label: 'parcial', tone: 'text-[var(--color-phase-implement)]', icon: CircleDashed },
29
- bloqueado: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
24
+ const IMPLEMENT_STATUS = {
25
+ feito: { label: 'feito', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
26
+ bloqueado: { label: 'bloqueado', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
30
27
  pendente: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
31
28
  };
32
29
 
33
- function isReady(sl, byId) {
34
- if (sl.status === 'feito' || sl.status === 'bloqueado') return false;
35
- return (sl.depends || []).every((d) => byId[d]?.status === 'feito');
36
- }
37
-
38
30
  export function editorLink(absPath) {
39
31
  return `vscode://file/${encodeURI(String(absPath).replace(/\\/g, '/'))}`;
40
32
  }
@@ -67,15 +59,15 @@ export function nextAction(f) {
67
59
  const paused = f.gates.find((g) => g.paused);
68
60
  if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
69
61
  if (f.inEvaluation) return 'Os relatórios do Gate 3 chegaram. A decisão sai dos vereditos, dos bloqueadores e dos cenários provados.';
70
- if (f.phase === 'implement' && f.slicesList?.length) {
71
- const blocked = f.slicesList.find((sl) => sl.status === 'bloqueado');
72
- if (blocked) return `${blocked.id} está bloqueada: o relatório traz uma pergunta que só você responde, no Claude Code.`;
73
- const open = f.slicesList.find((sl) => sl.status === 'parcial') || f.slicesList.find((sl) => sl.status === 'pendente');
74
- if (open) return `${f.slices.done} de ${f.slices.total} fatias feitas. Próxima: ${open.id}, ${open.title}.`;
75
- return 'Todas as fatias feitas. Falta o verify da feature e a revisão.';
62
+ if (f.phase === 'implement' && f.checksList?.length) {
63
+ const blocked = f.checksList.find((c) => c.implement === 'bloqueado');
64
+ if (blocked) return `${blocked.id} está bloqueado: o report.md traz uma pergunta que só você responde, no Claude Code.`;
65
+ const open = f.checksList.find((c) => c.implement !== 'feito');
66
+ if (open) return `${f.scenarios.done} de ${f.scenarios.total} cenários feitos. Próximo: ${open.id}.`;
67
+ return 'Todos os cenários feitos. Falta o verify da feature e a revisão.';
76
68
  }
77
69
  const text = {
78
- plan: 'Plano sendo escrito: cenários, decisões e fatias. O gate do plano aprova.',
70
+ plan: 'Plano sendo escrito: cenários, decisões e arquitetura. O gate do plano aprova.',
79
71
  uiux: 'Desenho de UI/UX em andamento.',
80
72
  implement: 'Implementação em andamento.',
81
73
  review: 'Em revisão.',
@@ -213,9 +205,12 @@ function CostBlock({ cost }) {
213
205
  );
214
206
  }
215
207
 
216
- function OverviewTab({ f, cost }) {
208
+ function OverviewTab({ f, cost, session, now }) {
217
209
  return (
218
210
  <div className="grid gap-5 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
211
+ {f.status !== 'completed' && (
212
+ <Panel title="Onde parou" className="min-w-0 lg:col-span-2"><SessionMap session={session} now={now} /></Panel>
213
+ )}
219
214
  <div className="space-y-5">
220
215
  {f.description && (
221
216
  <Panel title="Descrição">
@@ -238,173 +233,122 @@ function OverviewTab({ f, cost }) {
238
233
  );
239
234
  }
240
235
 
241
- const CHECK_STATUS_TONE = {
242
- PROVADO: 'text-[var(--color-ok)]',
243
- FALHA: 'text-[var(--color-bad)]',
244
- 'NÃO PROVADO': 'text-[var(--color-warn)]',
245
- };
246
-
247
- function ChecksTab({ f }) {
248
- const cov = f.checksCoverage;
249
- if (!f.checksList?.length) {
250
- return <Panel><p className="text-sm text-[var(--color-dim)]">Sem cenários extraídos: o `plan-check` ainda não gravou o `checks.json`.</p></Panel>;
251
- }
252
- const proven = cov ? cov.total - cov.missing.length - cov.notProven.length : null;
253
- return (
254
- <Panel title={cov ? `${proven} de ${cov.total} provados pelo verificador` : 'Aguardando o relatório do verificador'}>
255
- <ul className="divide-y divide-[var(--color-hair)]">
256
- {f.checksList.map((c) => (
257
- <li key={c.id} className="grid grid-cols-[3rem_minmax(0,1fr)_7rem] gap-3 py-3 text-sm">
258
- <span className="font-mono text-[var(--color-dim)]">{c.id}</span>
259
- <div className="min-w-0">
260
- <p className="text-[var(--color-fg)]">{c.text}</p>
261
- {c.evidence && <p className="mt-1 truncate text-xs text-[var(--color-faint)]" title={c.evidence}>{c.evidence}</p>}
262
- </div>
263
- <div className="text-right">
264
- <p className={CHECK_STATUS_TONE[c.status] || 'text-[var(--color-faint)]'}>{c.status ?? (cov ? 'sem linha' : '—')}</p>
265
- {c.proof && <p className="text-xs text-[var(--color-faint)]">prova: {c.proof}</p>}
266
- </div>
267
- </li>
268
- ))}
269
- </ul>
270
- </Panel>
271
- );
272
- }
273
-
274
- function reportPathFor(f, sliceId) {
275
- const path = `3-implement/reports/${sliceId}.md`;
276
- return f.documents.some((d) => d.path === path) ? path : null;
277
- }
278
-
279
- function SliceRow({ sl, f, open, onToggle, ready }) {
280
- const st = SLICE_STATUS[sl.status] || SLICE_STATUS.pendente;
281
- const StIcon = st.icon;
282
- const report = reportPathFor(f, sl.id);
283
- return (
284
- <li>
285
- <button
286
- type="button"
287
- onClick={onToggle}
288
- aria-expanded={open}
289
- className="fluid grid w-full grid-cols-[1.25rem_2.75rem_minmax(0,1fr)] items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.025] md:grid-cols-[1.25rem_2.75rem_minmax(0,1fr)_9rem_1rem]"
290
- >
291
- <StIcon weight="light" size={17} className={st.tone} aria-label={st.label} />
292
- <span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
293
- <span className="flex min-w-0 items-center gap-2">
294
- <span className="truncate text-sm text-[var(--color-fg)]">{sl.title}</span>
295
- {ready && <span className="shrink-0 text-xs text-[var(--color-phase-implement)]">pronta</span>}
296
- </span>
297
- <span className="hidden truncate text-xs text-[var(--color-dim)] md:block">{sl.persona ? `morph-${sl.persona}` : '—'}</span>
298
- <span className="hidden text-[var(--color-faint)] md:block">{open ? <CaretDown weight="light" size={14} /> : <CaretRight weight="light" size={14} />}</span>
299
- </button>
300
- {open && (
301
- <div className="grid gap-5 px-5 pb-5 pl-[4.75rem] md:grid-cols-2">
302
- <dl className="space-y-3 text-sm">
303
- {sl.persona && (
304
- <div><dt className="text-xs text-[var(--color-faint)]">Persona</dt>
305
- <dd><EntityLink kind="persona" id={sl.persona}>morph-{sl.persona}</EntityLink></dd></div>
306
- )}
307
- {sl.covers?.length > 0 && (
308
- <div><dt className="text-xs text-[var(--color-faint)]">Cobre</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{sl.covers.join(', ')}</dd></div>
309
- )}
310
- {sl.depends?.length > 0 && (
311
- <div><dt className="text-xs text-[var(--color-faint)]">Depende de</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{sl.depends.join(', ')}</dd></div>
312
- )}
313
- </dl>
314
- <dl className="space-y-3 text-sm">
315
- {sl.files?.length > 0 && (
316
- <div><dt className="text-xs text-[var(--color-faint)]">Arquivos</dt>
317
- <dd className="mt-1 space-y-0.5 font-mono text-[11px] text-[var(--color-dim)]">{sl.files.map((o) => <p key={o} className="truncate" title={o}>{o}</p>)}</dd></div>
318
- )}
319
- {report && (
320
- <div className="flex flex-wrap gap-2 pt-1">
321
- <Link to={`?tab=docs&doc=${encodeURIComponent(report)}`} className="fluid rounded-full bg-white/[0.06] px-3 py-1.5 text-xs text-[var(--color-fg)] transition-colors hover:bg-white/[0.1]">
322
- Abrir relatório
323
- </Link>
324
- </div>
325
- )}
326
- </dl>
327
- </div>
328
- )}
329
- </li>
330
- );
236
+ function ImplementIcon({ status }) {
237
+ const st = IMPLEMENT_STATUS[status] || IMPLEMENT_STATUS.pendente;
238
+ const Icon = st.icon;
239
+ return <Icon weight="light" size={17} className={st.tone} aria-label={`implementação: ${st.label}`} />;
331
240
  }
332
241
 
333
- const BOARD_COLUMNS = [
334
- { id: 'pendente', label: 'Pendentes' },
335
- { id: 'parcial', label: 'Parciais' },
336
- { id: 'bloqueado', label: 'Bloqueadas' },
337
- { id: 'feito', label: 'Feitas' },
338
- ];
339
-
340
- function SliceCard({ sl, f, ready }) {
341
- const report = reportPathFor(f, sl.id);
342
- return (
343
- <li className="rounded-xl bg-[var(--color-raised)] px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--color-hair),inset_0_1px_0_rgb(255_255_255_/_0.05)]">
344
- <p className="flex items-baseline gap-2">
345
- <span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
346
- {ready && <span className="text-xs text-[var(--color-phase-implement)]">pronta</span>}
347
- </p>
348
- <p className="mt-1 text-sm leading-snug text-[var(--color-fg)]">{sl.title}</p>
349
- {sl.persona && (
350
- <EntityLink kind="persona" id={sl.persona} className="mt-2 inline-block text-xs">morph-{sl.persona}</EntityLink>
351
- )}
352
- {sl.depends?.length > 0 && <p className="mt-1 text-xs text-[var(--color-faint)]">depende de {sl.depends.join(', ')}</p>}
353
- {report && (
354
- <Link to={`?tab=docs&doc=${encodeURIComponent(report)}`} className="mt-2.5 inline-block text-xs text-[var(--color-dim)] underline decoration-[var(--color-hair-strong)] underline-offset-2 hover:text-[var(--color-fg)]">abrir relatório</Link>
355
- )}
356
- </li>
357
- );
242
+ const SCORE_ROLES = ['verifier', 'reviewer', 'security'];
243
+ const CELL = {
244
+ ok: { icon: '✓', tone: 'bg-[var(--color-ok)]/15 text-[var(--color-ok)]' },
245
+ bad: { icon: '✗', tone: 'bg-[var(--color-bad)]/15 text-[var(--color-bad)]' },
246
+ warn: { icon: '?', tone: 'bg-[var(--color-warn)]/15 text-[var(--color-warn)]' },
247
+ na: { icon: '·', tone: 'bg-white/[0.04] text-[var(--color-faint)]' },
248
+ };
249
+ const CHECK_CELL = { PROVADO: 'ok', FALHA: 'bad', 'NÃO PROVADO': 'warn' };
250
+
251
+ export function scoreCell(round, id, role) {
252
+ const r = round.roles[role];
253
+ if (!r) return { kind: 'na', text: 'sem relatório' };
254
+ const cited = r.blockers.filter((b) => b.checks.includes(id));
255
+ if (cited.length) return { kind: 'bad', text: cited.map((b) => `${b.id}: ${b.evidence}`).join(' · ') };
256
+ if (role === 'verifier') {
257
+ const row = round.checks.find((c) => c.id === id);
258
+ if (!row) return { kind: 'warn', text: 'sem linha no relatório' };
259
+ return { kind: CHECK_CELL[row.status] ?? 'warn', text: `${row.status}. ${row.evidence}` };
260
+ }
261
+ return { kind: 'na', text: `não julga por cenário; veredito ${r.verdict ?? '—'}` };
358
262
  }
359
263
 
360
- function SlicesBoard({ f, byId }) {
361
- return (
362
- <div className="-mx-4 overflow-x-auto px-4 pb-2 md:mx-0 md:px-0">
363
- <div className="grid auto-cols-[16rem] grid-flow-col gap-3 md:auto-cols-[minmax(12rem,1fr)]">
364
- {BOARD_COLUMNS.map((c) => {
365
- const items = f.slicesList.filter((sl) => sl.status === c.id);
366
- return (
367
- <section key={c.id} className="bezel min-w-0" aria-label={`${c.label}: ${items.length}`}>
368
- <div className="bezel-core flex h-full min-h-[12rem] flex-col p-2.5">
369
- <h3 className="flex items-center justify-between px-1.5 pb-2.5 pt-1 text-sm font-medium">
370
- {c.label}<span className="font-mono text-xs text-[var(--color-faint)]">{items.length}</span>
371
- </h3>
372
- {items.length === 0
373
- ? <p className="px-1.5 text-xs text-[var(--color-faint)]">Nenhuma.</p>
374
- : <ul className="space-y-2">{items.map((sl) => <SliceCard key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)} />)}</ul>}
375
- </div>
376
- </section>
377
- );
378
- })}
379
- </div>
380
- </div>
381
- );
382
- }
264
+ // Same rule as the gate: a Cn the verifier did not prove keeps the round at FIX, whatever the reports say.
265
+ const roundVerdict = (round, ids) => (Object.values(round.roles).some((r) => r.verdict !== 'SHIP' || r.blockers.length)
266
+ || (round.roles.verifier && ids.some((id) => scoreCell(round, id, 'verifier').kind !== 'ok')) ? 'FIX' : 'SHIP');
267
+ const roundSha = (round) => (round.roles.verifier ?? Object.values(round.roles)[0])?.reviewedSha?.slice(0, 7) ?? null;
268
+ const blockersOf = (round) => SCORE_ROLES.flatMap((role) => (round.roles[role]?.carried ? [] : round.roles[role]?.blockers ?? []).map((b) => ({ ...b, role })));
383
269
 
384
- function SlicesTab({ f, view = 'list', onView = () => {} }) {
270
+ function ChecksTab({ f }) {
271
+ const rounds = f.rounds ?? [];
272
+ const [pick, setPick] = useState(null);
385
273
  const [open, setOpen] = useState(null);
386
- if (!f.slicesList.length) {
387
- return <Panel><p className="text-sm text-[var(--color-dim)]">O plano desta feature ainda não foi conferido pelo <code className="font-mono">morph-spec plan-check</code>. As fatias nascem na fase de plano.</p></Panel>;
274
+ if (!f.checksList?.length) {
275
+ return <Panel><p className="text-sm text-[var(--color-dim)]">Sem cenários extraídos: o `plan-check` ainda não gravou o `checks.json`.</p></Panel>;
388
276
  }
389
- const byId = Object.fromEntries(f.slicesList.map((sl) => [sl.id, sl]));
390
- const ready = f.slicesList.filter((sl) => isReady(sl, byId)).length;
277
+ const i = pick ?? rounds.length - 1;
278
+ const round = rounds[i];
279
+ const proven = round ? f.checksList.filter((c) => scoreCell(round, c.id, 'verifier').kind === 'ok').length : null;
280
+ const blockers = round ? blockersOf(round) : [];
281
+ const closed = round && i > 0 && !blockers.length ? blockersOf(rounds[i - 1]) : [];
282
+ const ids = f.checksList.map((c) => c.id);
283
+ const verdict = round ? roundVerdict(round, ids) : null;
391
284
  return (
392
- <div className="space-y-4">
393
- <div className="flex flex-wrap items-center justify-between gap-3">
394
- <Segmented label="Visualização das fatias" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
395
- <p className="text-sm text-[var(--color-dim)]">
396
- {f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{f.slices.blocked ? `, ${f.slices.blocked} bloqueadas` : ''}{ready ? `, ${ready} prontas para começar` : ''}
397
- </p>
398
- </div>
399
- {view === 'board' ? <SlicesBoard f={f} byId={byId} /> : (
400
- <div className="bezel">
401
- <ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
402
- {f.slicesList.map((sl) => (
403
- <SliceRow key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)}
404
- open={open === sl.id} onToggle={() => setOpen(open === sl.id ? null : sl.id)} />
285
+ <div className="space-y-5">
286
+ <Panel>
287
+ <div className="flex flex-wrap items-baseline gap-4">
288
+ <span className={`text-4xl font-semibold tracking-tight ${verdict === 'SHIP' ? 'text-[var(--color-ok)]' : verdict === 'FIX' ? 'text-[var(--color-bad)]' : ''}`}>{verdict ?? '—'}</span>
289
+ <span className="text-sm text-[var(--color-dim)]">
290
+ {round ? `${proven} de ${f.checksList.length} cenários provados · ${blockers.length} bloqueador(es)` : 'Aguardando os relatórios do Gate 3'}
291
+ </span>
292
+ </div>
293
+ {rounds.length > 1 && (
294
+ <div className="mt-4 flex flex-wrap gap-2">
295
+ {rounds.map((r, k) => (
296
+ <button key={k} type="button" aria-pressed={k === i} onClick={() => setPick(k)}
297
+ className={`fluid inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs shadow-[inset_0_0_0_1px_var(--color-hair)] ${k === i ? 'bg-white/[0.1] text-[var(--color-fg)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]'}`}>
298
+ <span className={`h-2 w-2 rounded-full ${roundVerdict(r, ids) === 'SHIP' ? 'bg-[var(--color-ok)]' : 'bg-[var(--color-bad)]'}`} />
299
+ Rodada {k + 1} · {roundVerdict(r, ids)}{roundSha(r) && <span className="font-mono text-[var(--color-faint)]">{roundSha(r)}</span>}
300
+ </button>
405
301
  ))}
406
- </ul>
302
+ </div>
303
+ )}
304
+ </Panel>
305
+ <Panel>
306
+ <div className="grid grid-cols-[1.25rem_3rem_minmax(0,1fr)_repeat(3,3.5rem)] gap-3 pb-2 text-xs text-[var(--color-faint)]">
307
+ <span /><span>Cn</span><span>cenário</span>
308
+ {SCORE_ROLES.map((role) => <span key={role} className="text-center">{REVIEW_ROLE_LABEL[role]}</span>)}
407
309
  </div>
310
+ <ul className="divide-y divide-[var(--color-hair)]">
311
+ {f.checksList.map((c) => {
312
+ const cells = SCORE_ROLES.map((role) => (round ? scoreCell(round, c.id, role) : { kind: 'na', text: '' }));
313
+ const expanded = open === c.id;
314
+ return (
315
+ <li key={c.id} className="py-3 text-sm">
316
+ <button type="button" onClick={() => setOpen(expanded ? null : c.id)} aria-expanded={expanded}
317
+ className="grid w-full grid-cols-[1.25rem_3rem_minmax(0,1fr)_repeat(3,3.5rem)] items-center gap-3 text-left">
318
+ <ImplementIcon status={c.implement} />
319
+ <span className="font-mono text-[var(--color-dim)]">{c.id}</span>
320
+ <span className={`min-w-0 text-[var(--color-fg)] ${expanded ? '' : 'truncate'}`} title={c.text}>{c.text}</span>
321
+ {cells.map((cell, k) => (
322
+ <span key={k} title={cell.text} className={`mx-auto grid h-8 w-8 place-items-center rounded-lg font-semibold ${CELL[cell.kind].tone}`}>{CELL[cell.kind].icon}</span>
323
+ ))}
324
+ </button>
325
+ {expanded && (
326
+ <div className="mt-3 space-y-2 pl-[4.25rem] text-xs">
327
+ {c.proof && <p className="text-[var(--color-faint)]">prova: {c.proof}</p>}
328
+ {cells.map((cell, k) => cell.kind !== 'na' && (
329
+ <p key={k}><span className="mr-2 text-[var(--color-faint)]">{REVIEW_ROLE_LABEL[SCORE_ROLES[k]]}</span><span className="text-[var(--color-dim)]">{cell.text}</span></p>
330
+ ))}
331
+ </div>
332
+ )}
333
+ </li>
334
+ );
335
+ })}
336
+ </ul>
337
+ </Panel>
338
+ {round && (
339
+ <Panel title={blockers.length ? 'Bloqueadores desta rodada' : closed.length ? 'Fechados nesta rodada' : 'Nenhum bloqueador'}>
340
+ <ul className="space-y-2 text-xs">
341
+ {(blockers.length ? blockers : closed).map((b) => (
342
+ <li key={`${b.role}-${b.id}`} className="grid grid-cols-[3rem_minmax(0,1fr)] gap-3">
343
+ <span className={`font-mono ${blockers.length ? 'text-[var(--color-bad)]' : 'text-[var(--color-ok)]'}`}>{b.id}</span>
344
+ <div className="min-w-0">
345
+ <p className="text-[var(--color-faint)]">{REVIEW_ROLE_LABEL[b.role]} · {b.type} · {b.checks.length ? b.checks.join(', ') : 'sem Cn citado'} · <span className="font-mono">{b.where}</span></p>
346
+ <p className="mt-0.5 truncate text-[var(--color-dim)]" title={b.evidence}>{b.evidence}</p>
347
+ </div>
348
+ </li>
349
+ ))}
350
+ </ul>
351
+ </Panel>
408
352
  )}
409
353
  </div>
410
354
  );
@@ -491,7 +435,7 @@ function DocsTab({ f, doc, onDoc, project }) {
491
435
  );
492
436
  }
493
437
 
494
- export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = null, slicesView = 'list', onSlicesView = () => {}, now = Date.now(), project, projectName }) {
438
+ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, session, latest = null, now = Date.now(), project, projectName }) {
495
439
  const verdict = verdictLabel(f);
496
440
  const working = latest && isWorking({ ...f, lastActivity: latest }, now);
497
441
  return (
@@ -510,7 +454,7 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
510
454
  </p>
511
455
  </div>
512
456
  <div className="flex shrink-0 gap-6 text-right">
513
- <div><p className="text-2xl font-semibold tabular-nums">{f.slices.total ? `${f.slices.done}/${f.slices.total}` : '—'}</p><p className="text-xs text-[var(--color-faint)]">fatias</p></div>
457
+ <div><p className="text-2xl font-semibold tabular-nums">{f.scenarios.total ? `${f.scenarios.done}/${f.scenarios.total}` : '—'}</p><p className="text-xs text-[var(--color-faint)]">cenários</p></div>
514
458
  <div><p className="text-2xl font-semibold">{verdict ?? '—'}</p><p className="text-xs text-[var(--color-faint)]">veredito</p></div>
515
459
  </div>
516
460
  </div>
@@ -556,14 +500,13 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
556
500
  tab === t.id ? 'bg-white/[0.1] text-[var(--color-fg)] shadow-[inset_0_1px_0_rgb(255_255_255_/_0.08)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]',
557
501
  ].join(' ')}
558
502
  >
559
- {t.label}{t.id === 'slices' && f.slices.total ? ` ${f.slices.total}` : ''}{t.id === 'checks' && f.checksList?.length ? ` ${f.checksList.length}` : ''}{t.id === 'docs' ? ` ${f.documents.length}` : ''}
503
+ {t.label}{t.id === 'checks' && f.checksList?.length ? ` ${f.checksList.length}` : ''}{t.id === 'docs' ? ` ${f.documents.length}` : ''}
560
504
  </button>
561
505
  ))}
562
506
  </div>
563
507
 
564
508
  <div className="rise-3">
565
- {tab === 'overview' && <OverviewTab f={f} cost={cost} />}
566
- {tab === 'slices' && <SlicesTab f={f} view={slicesView} onView={onSlicesView} />}
509
+ {tab === 'overview' && <OverviewTab f={f} cost={cost} session={session} now={now} />}
567
510
  {tab === 'checks' && <ChecksTab f={f} />}
568
511
  {tab === 'docs' && <DocsTab f={f} doc={doc} onDoc={onDoc} project={project} />}
569
512
  {tab === 'activity' && (
@@ -582,7 +525,6 @@ export default function FeatureDetail() {
582
525
  const { data, error, loading } = useMorphQuery(`${apiBase(project)}/features/${encodeURIComponent(name)}`, { project });
583
526
  // The cost rides on the activity endpoint (it owns the event stream).
584
527
  const costQuery = useMorphQuery(`${apiBase(project)}/activity?feature=${encodeURIComponent(name)}&limit=1`, { scope: 'activity', project });
585
- const [slicesView, setSlicesView] = usePref('slices.view', 'list');
586
528
  const now = useNow();
587
529
 
588
530
  const onTab = useCallback((id) => setParams((p) => { const n = new URLSearchParams(p); n.set('tab', id); if (id !== 'docs') n.delete('doc'); return n; }), [setParams]);
@@ -602,7 +544,8 @@ export default function FeatureDetail() {
602
544
  <FeatureDetailView
603
545
  f={data} tab={tab} onTab={onTab} doc={doc} onDoc={onDoc}
604
546
  cost={costQuery.data?.cost ?? null} latest={costQuery.data?.items?.[0] ?? null}
605
- slicesView={slicesView} onSlicesView={setSlicesView} now={now} project={project}
547
+ session={costQuery.data ? costQuery.data.session : undefined}
548
+ now={now} project={project}
606
549
  />
607
550
  );
608
551
  }
@@ -40,15 +40,15 @@ function VerifyMark({ verification }) {
40
40
  : <XCircle weight="light" className="text-[var(--color-bad)]" size={16} aria-label="último verify falhou" />;
41
41
  }
42
42
 
43
- function SliceMeter({ slices, wide = false }) {
44
- if (!slices?.total) return <span className="text-xs text-[var(--color-faint)]">sem fatias</span>;
45
- const pct = Math.round((slices.done / slices.total) * 100);
43
+ function ScenarioMeter({ scenarios, wide = false }) {
44
+ if (!scenarios?.total) return <span className="text-xs text-[var(--color-faint)]">sem cenários</span>;
45
+ const pct = Math.round((scenarios.done / scenarios.total) * 100);
46
46
  return (
47
- <div className="flex items-center gap-2" title={`${slices.done} de ${slices.total} fatias feitas`}>
47
+ <div className="flex items-center gap-2" title={`${scenarios.done} de ${scenarios.total} cenários feitos`}>
48
48
  <div className={`h-1 overflow-hidden rounded-full bg-white/[0.07] ${wide ? 'flex-1' : 'w-12'}`}>
49
49
  <div className="h-full rounded-full bg-[var(--color-fg)]/70" style={{ width: `${pct}%` }} />
50
50
  </div>
51
- <span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{slices.done}/{slices.total}</span>
51
+ <span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{scenarios.done}/{scenarios.total}</span>
52
52
  </div>
53
53
  );
54
54
  }
@@ -104,7 +104,7 @@ export function FeatureRow({ f, now, project, projectName }) {
104
104
  <PhaseRail feature={f} />
105
105
  {/* One line on a phone; on desktop the wrapper dissolves into the row's grid. */}
106
106
  <div className="flex items-center gap-5 md:contents">
107
- <SliceMeter slices={f.slices} />
107
+ <ScenarioMeter scenarios={f.scenarios} />
108
108
  <VerifyMark verification={f.verification} />
109
109
  <span className="font-mono text-sm tabular-nums text-[var(--color-dim)]" title={verdict ? 'veredito do Gate 3' : 'sem veredito'}>
110
110
  {verdict ?? '—'}
@@ -163,7 +163,7 @@ function BoardCard({ f, now, project }) {
163
163
  {f.lastActivity?.title && !isFinished(f) && (
164
164
  <p className="mt-1.5 line-clamp-2 text-xs text-[var(--color-dim)]">{f.lastActivity.title}</p>
165
165
  )}
166
- <div className="mt-3"><SliceMeter slices={f.slices} wide /></div>
166
+ <div className="mt-3"><ScenarioMeter scenarios={f.scenarios} wide /></div>
167
167
  <div className="mt-2.5 flex items-center justify-between text-xs text-[var(--color-faint)]">
168
168
  <span className="flex items-center gap-2">
169
169
  <VerifyMark verification={f.verification} />
@@ -10,7 +10,7 @@ import { phaseMeta, GATE_LABEL } from '../lib/phases.js';
10
10
  const phaseColor = (id) => `var(--color-phase-${id})`;
11
11
 
12
12
  const GATE_CHECKS = {
13
- plan: 'Confere o plano: o problema, o que fica de fora, os cenários com a prova de cada um e as fatias. Depois dele o plan.md congela (só a errata cresce).',
13
+ plan: 'Confere o plano: o problema, o que fica de fora, os cenários com a prova de cada um, o reuso e as decisões. Depois dele o plan.md congela (só a errata cresce).',
14
14
  uiux: 'Confere o design das telas (mockups e checklist de tela). É opcional: só existe quando a feature passa pela fase de UI/UX, depois do plano.',
15
15
  review: 'Confere a avaliação independente em contexto novo: verificador e revisor (e segurança, quando o diff toca auth, segredo ou dado pessoal), cada um com veredito SHIP ou FIX sobre o mesmo commit. Só bloqueia com evidência, com o verify verde e todo cenário provado.',
16
16
  };
@@ -2,11 +2,46 @@ import { useState } from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
  import { FolderSimplePlus, X, ArrowRight } from '@phosphor-icons/react';
4
4
  import { FeatureRow, isWorking } from './Features.jsx';
5
- import { useProjects, projectHref, announceProjectsChanged } from '../lib/project.js';
5
+ import { useProjects, projectHref, featureHref, announceProjectsChanged } from '../lib/project.js';
6
6
  import { useNow } from '../hooks/usePref.js';
7
7
  import { timeAgo } from '../lib/format.js';
8
8
 
9
9
  const ACTIVE_SHOWN = 4;
10
+ const GATE_LABEL = { plan: 'Plano', uiux: 'Design', review: 'Gate 3' };
11
+ const GATE_COLOR = { plan: 'var(--color-phase-plan)', uiux: 'var(--color-phase-uiux)', review: 'var(--color-phase-review)' };
12
+
13
+ // One row per gate that waits for the human, oldest first: the longest wait is the next decision.
14
+ export function decisionQueue(projects = []) {
15
+ return projects
16
+ .filter((p) => p.summary)
17
+ .flatMap((p) => p.summary.activeFeatures.flatMap((f) => (f.gates || [])
18
+ .filter((g) => g.paused)
19
+ .map((g) => ({ project: p, feature: f, gate: g.gate, reason: g.reason, since: g.since }))))
20
+ .sort((a, b) => (a.since || '￿').localeCompare(b.since || '￿'));
21
+ }
22
+
23
+ function DecisionRow({ item, now }) {
24
+ const { project, feature, gate, reason, since } = item;
25
+ return (
26
+ <li>
27
+ <Link to={featureHref(project.id, feature.name)}
28
+ className="fluid grid grid-cols-[4.5rem_minmax(0,1fr)] items-start gap-x-4 gap-y-1 px-5 py-3.5 transition-colors hover:bg-white/[0.025] md:grid-cols-[4.5rem_minmax(0,1fr)_auto]">
29
+ <span className="mt-0.5 rounded-full px-2 py-px text-center text-[11px] font-medium shadow-[inset_0_0_0_1px_var(--color-hair-strong)]"
30
+ style={{ color: GATE_COLOR[gate] }}>{GATE_LABEL[gate] ?? gate}</span>
31
+ <div className="min-w-0">
32
+ <div className="flex items-baseline gap-2">
33
+ <span className="truncate font-medium text-[var(--color-fg)]">{feature.name}</span>
34
+ <span className="shrink-0 text-xs text-[var(--color-faint)]">{project.name}</span>
35
+ </div>
36
+ {reason && <p className="mt-0.5 line-clamp-2 text-sm text-[var(--color-dim)]">{reason}</p>}
37
+ </div>
38
+ <span className="col-start-2 whitespace-nowrap text-xs text-[var(--color-faint)] md:col-start-auto md:text-right" title={since || undefined}>
39
+ {since ? `esperando ${timeAgo(since, now)}` : 'sem horário'}
40
+ </span>
41
+ </Link>
42
+ </li>
43
+ );
44
+ }
10
45
 
11
46
  async function send(method, url, body) {
12
47
  const res = await fetch(url, {
@@ -79,7 +114,7 @@ export function PortfolioView({ data, now = Date.now(), onAdd = () => {}, onRemo
79
114
  const ready = projects.filter((p) => p.summary);
80
115
  const totalActive = ready.reduce((n, p) => n + p.summary.active, 0);
81
116
  const totalAwaiting = ready.reduce((n, p) => n + p.summary.awaiting, 0);
82
- const awaiting = ready.flatMap((p) => p.summary.activeFeatures.filter((f) => f.awaiting).map((f) => ({ f, p })));
117
+ const queue = decisionQueue(projects);
83
118
  const ordered = [...projects].sort((a, b) => (b.summary?.lastActivityTs || '').localeCompare(a.summary?.lastActivityTs || ''));
84
119
 
85
120
  return (
@@ -92,12 +127,12 @@ export function PortfolioView({ data, now = Date.now(), onAdd = () => {}, onRemo
92
127
  </p>
93
128
  </header>
94
129
 
95
- {awaiting.length > 0 && (
130
+ {queue.length > 0 && (
96
131
  <section className="rise-2 mb-10">
97
- <h2 className="mb-3 px-1 text-sm font-medium">Esperando você</h2>
132
+ <h2 className="mb-3 px-1 text-sm font-medium">Esperando você <span className="text-[var(--color-faint)]">· a mais antiga primeiro</span></h2>
98
133
  <div className="bezel">
99
134
  <ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
100
- {awaiting.map(({ f, p }) => <FeatureRow key={`${p.id}:${f.name}`} f={f} now={now} project={p.id} projectName={p.name} />)}
135
+ {queue.map((item) => <DecisionRow key={`${item.project.id}:${item.feature.name}:${item.gate}`} item={item} now={now} />)}
101
136
  </ul>
102
137
  </div>
103
138
  </section>
@@ -263,6 +263,35 @@ export function createEventStore(projectDir, { loadTrees = loadTelemetryAllTrees
263
263
  };
264
264
  }
265
265
 
266
+ const SESSION_GAP_MS = 30 * 60 * 1000;
267
+ const SESSION_MARK_KINDS = new Set(['prompt', 'question', 'dispatch', 'handback', 'verify', 'gate', 'failure']);
268
+
269
+ // "Where was I": the feature's most recent stretch of work, split from the previous one by a 30-minute silence.
270
+ export function sessionMap(events, feature, { gapMs = SESSION_GAP_MS } = {}) {
271
+ const lines = describeAll(events).filter((l) => l.feature === feature && l.ts);
272
+ if (lines.length === 0) return null;
273
+ const session = [lines[0]];
274
+ for (let i = 1; i < lines.length; i++) {
275
+ if (Date.parse(session[session.length - 1].ts) - Date.parse(lines[i].ts) > gapMs) break;
276
+ session.push(lines[i]);
277
+ }
278
+ const files = new Map();
279
+ for (const l of session) {
280
+ if (l.kind === 'write' && l.refs.path) files.set(l.refs.path, (files.get(l.refs.path) || 0) + 1);
281
+ }
282
+ const lastGreen = session.find((l) => l.kind === 'verify' && l.status === 'ok');
283
+ const lastFailure = session.find((l) => l.status === 'fail' && (!lastGreen || l.ts > lastGreen.ts)) || null;
284
+ return {
285
+ start: session[session.length - 1].ts,
286
+ end: session[0].ts,
287
+ actions: session.length,
288
+ marks: session.filter((l) => SESSION_MARK_KINDS.has(l.kind)).reverse()
289
+ .map(({ ts, kind, status, title }) => ({ ts, kind, status, title })),
290
+ files: [...files].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([path, edits]) => ({ path, edits })),
291
+ lastFailure: lastFailure && { ts: lastFailure.ts, title: lastFailure.title, detail: lastFailure.detail },
292
+ };
293
+ }
294
+
266
295
  /** `sessionUsage` and `agentUsage` stay `null` when the stream has no summable sample. */
267
296
  export function featureCost(events, feature) {
268
297
  const c = aggregateCost(events, { feature });
@@ -1,15 +1,11 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
- import { personaStandardIds, isImplementer, renderAgentMd, loadKnownMcpIds } from '../../utils/agents-installer.js';
3
+ import { personaStandardIds, renderAgentMd, loadKnownMcpIds } from '../../utils/agents-installer.js';
4
4
 
5
5
  const DISPATCH_LINE = /despach|dispatch|subagent_type/i;
6
6
 
7
- const GENERIC_DISPATCH = 'morph-{id}';
8
-
9
- /**
10
- * Only dispatch lines count: morph-plan cites `morph-dotnet-senior` as a
11
- * spelling example of the `agent` field, which dispatches nobody.
12
- */
7
+ // Only dispatch lines count: a skill that merely names a persona dispatches nobody. Two phases can
8
+ // share a skill, so beyond SKILL.md only the phase's own `references/{phase}.md` is read.
13
9
  export function personaPhases(frameworkDir, agents) {
14
10
  const result = new Map(Object.keys(agents).map((id) => [id, []]));
15
11
  const phasesJsonPath = join(frameworkDir, 'phases.json');
@@ -18,13 +14,15 @@ export function personaPhases(frameworkDir, agents) {
18
14
  const phases = Object.values(JSON.parse(readFileSync(phasesJsonPath, 'utf8')).phases || {})
19
15
  .sort((a, b) => a.index - b.index);
20
16
  for (const phase of phases) {
21
- const skillPath = join(frameworkDir, 'skills', 'core', phase.skill || '', 'SKILL.md');
22
- if (!phase.skill || !existsSync(skillPath)) continue;
23
- const dispatchLines = readFileSync(skillPath, 'utf8').split(/\r?\n/).filter((l) => DISPATCH_LINE.test(l));
24
- const generic = dispatchLines.some((l) => l.includes(GENERIC_DISPATCH));
25
- for (const [id, agent] of Object.entries(agents)) {
17
+ if (!phase.skill) continue;
18
+ const skillDir = join(frameworkDir, 'skills', 'core', phase.skill);
19
+ const sources = [join(skillDir, 'SKILL.md'), join(skillDir, 'references', `${phase.id}.md`)].filter(existsSync);
20
+ const dispatchLines = sources
21
+ .flatMap((f) => readFileSync(f, 'utf8').split(/\r?\n/))
22
+ .filter((l) => DISPATCH_LINE.test(l));
23
+ for (const id of Object.keys(agents)) {
26
24
  const named = new RegExp(`morph-${id}(?![\\w-])`);
27
- if ((generic && isImplementer(agent)) || dispatchLines.some((l) => named.test(l))) {
25
+ if (dispatchLines.some((l) => named.test(l))) {
28
26
  result.get(id).push(phase.id);
29
27
  }
30
28
  }
@@ -52,7 +50,6 @@ export function scanAgents(frameworkDir) {
52
50
  domains: agent.domains || [],
53
51
  keywords: agent.keywords || [],
54
52
  standards: personaStandardIds(agent),
55
- surfaces: agent.surfaces || null,
56
53
  stack: agent.stack || ['*'],
57
54
  activePhases: phasesOf.get(id),
58
55
  persona: agent.persona || '',