@polymorphism-tech/morph-spec 8.47.0 → 9.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.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -6,42 +6,33 @@ import {
|
|
|
6
6
|
} from '@phosphor-icons/react';
|
|
7
7
|
import PhaseRail, { segmentStates } from '../components/PhaseRail.jsx';
|
|
8
8
|
import KnowledgeMarkdown from '../components/KnowledgeMarkdown.jsx';
|
|
9
|
-
import EntityLink
|
|
9
|
+
import EntityLink from '../components/EntityLink.jsx';
|
|
10
10
|
import ActivityFeed from '../components/ActivityFeed.jsx';
|
|
11
11
|
import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
12
12
|
import { usePref, useNow } from '../hooks/usePref.js';
|
|
13
13
|
import { Segmented, isWorking } from './Features.jsx';
|
|
14
14
|
import { apiBase, projectHref } from '../lib/project.js';
|
|
15
15
|
import { PHASES, GATE_LABEL, STATUS_LABEL, LOCATION_LABEL, phaseMeta } from '../lib/phases.js';
|
|
16
|
-
import { timeAgo, dateTime, num,
|
|
16
|
+
import { timeAgo, dateTime, num, bytes, verdictLabel } from '../lib/format.js';
|
|
17
17
|
|
|
18
18
|
const TABS = [
|
|
19
19
|
{ id: 'overview', label: 'Visão geral' },
|
|
20
|
-
{ id: '
|
|
20
|
+
{ id: 'slices', label: 'Fatias' },
|
|
21
|
+
{ id: 'checks', label: 'Cenários' },
|
|
21
22
|
{ id: 'docs', label: 'Documentos' },
|
|
22
23
|
{ id: 'activity', label: 'Atividade' },
|
|
23
24
|
];
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
blocked: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
|
|
30
|
-
skipped: { label: 'pulada', tone: 'text-[var(--color-faint)]', icon: Circle },
|
|
31
|
-
pending: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
|
|
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
|
+
pendente: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** A pending task whose dependencies are all done can start now. */
|
|
42
|
-
function isReady(t, byId) {
|
|
43
|
-
if (t.status !== 'pending') return false;
|
|
44
|
-
return (t.dependencies || []).every((d) => ['done', 'completed', 'skipped'].includes(byId[d]?.status));
|
|
32
|
+
// An unfinished slice whose dependencies all have a final report can start now.
|
|
33
|
+
function isReady(sl, byId) {
|
|
34
|
+
if (sl.status === 'feito') return false;
|
|
35
|
+
return (sl.depends || []).every((d) => byId[d]?.status === 'feito');
|
|
45
36
|
}
|
|
46
37
|
|
|
47
38
|
/** `vscode://file/…` opens the file in the editor from the browser. */
|
|
@@ -77,16 +68,15 @@ export function nextAction(f) {
|
|
|
77
68
|
if (f.status === 'abandoned') return 'Abandonada.';
|
|
78
69
|
const paused = f.gates.find((g) => g.paused);
|
|
79
70
|
if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
|
|
80
|
-
if (f.inEvaluation) return '
|
|
81
|
-
if (f.phase === 'implement' && f.
|
|
82
|
-
const open = f.
|
|
83
|
-
if (open) return `${f.
|
|
84
|
-
return 'Todas as
|
|
71
|
+
if (f.inEvaluation) return 'Os relatórios do Gate 3 chegaram. A decisão sai dos vereditos, dos bloqueadores e dos cenários provados.';
|
|
72
|
+
if (f.phase === 'implement' && f.slicesList?.length) {
|
|
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.';
|
|
85
76
|
}
|
|
86
77
|
const text = {
|
|
87
|
-
|
|
78
|
+
plan: 'Plano sendo escrito: cenários, decisões e fatias. O gate do plano aprova.',
|
|
88
79
|
uiux: 'Desenho de UI/UX em andamento.',
|
|
89
|
-
plan: 'Plano sendo escrito: spec, use cases e tasks. O Gate 2 aprova.',
|
|
90
80
|
implement: 'Implementação em andamento.',
|
|
91
81
|
review: 'Em revisão.',
|
|
92
82
|
};
|
|
@@ -149,13 +139,41 @@ function NodeChips({ nodes }) {
|
|
|
149
139
|
);
|
|
150
140
|
}
|
|
151
141
|
|
|
142
|
+
const REVIEW_ROLE_LABEL = { verifier: 'Verificador', reviewer: 'Revisor', security: 'Segurança' };
|
|
143
|
+
const REVIEW_STATUS_LABEL = { missing: 'sem relatório', partial: 'parcial', unverdicted: 'sem veredito', ok: null };
|
|
144
|
+
|
|
145
|
+
function ReviewRows({ reviews }) {
|
|
146
|
+
if (!reviews?.length) return null;
|
|
147
|
+
return (
|
|
148
|
+
<ul className="mt-4 space-y-2 text-sm">
|
|
149
|
+
{reviews.map((r) => {
|
|
150
|
+
const state = REVIEW_STATUS_LABEL[r.status] ?? r.verdict;
|
|
151
|
+
const tone = r.verdict === 'SHIP' && r.status === 'ok' ? 'text-[var(--color-ok)]' : r.verdict === 'FIX' ? 'text-[var(--color-bad)]' : 'text-[var(--color-faint)]';
|
|
152
|
+
return (
|
|
153
|
+
<li key={r.role} className="grid grid-cols-[6.5rem_minmax(0,1fr)_auto] items-baseline gap-3">
|
|
154
|
+
<span className="text-[var(--color-dim)]">{REVIEW_ROLE_LABEL[r.role] || r.role}</span>
|
|
155
|
+
<span className={tone}>
|
|
156
|
+
{state}{r.blockers ? ` · ${r.blockers} bloq.` : ''}
|
|
157
|
+
{r.reviewedSha && <span className="ml-2 font-mono text-xs text-[var(--color-faint)]">{r.reviewedSha.slice(0, 7)}</span>}
|
|
158
|
+
</span>
|
|
159
|
+
{r.status !== 'missing' && (
|
|
160
|
+
<Link to={`?tab=docs&doc=${encodeURIComponent(r.path)}`} className="text-xs text-[var(--color-dim)] underline decoration-[var(--color-hair-strong)] underline-offset-2 hover:text-[var(--color-fg)]">abrir</Link>
|
|
161
|
+
)}
|
|
162
|
+
</li>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</ul>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
152
169
|
function VerdictBlock({ f }) {
|
|
153
170
|
return (
|
|
154
171
|
<div>
|
|
155
172
|
<p className="flex items-baseline gap-2">
|
|
156
173
|
<span className="text-4xl font-semibold tracking-tight">{f.verdict ?? '—'}</span>
|
|
157
|
-
<span className="text-sm text-[var(--color-dim)]">{f.verdict ? `${f.blockers ?? 0} bloqueador(es)` : 'sem veredito do
|
|
174
|
+
<span className="text-sm text-[var(--color-dim)]">{f.verdict ? `${f.blockers ?? 0} bloqueador(es)` : 'sem veredito do Gate 3'}</span>
|
|
158
175
|
</p>
|
|
176
|
+
<ReviewRows reviews={f.reviews} />
|
|
159
177
|
{f.blockerList?.length > 0 && (
|
|
160
178
|
<ul className="mt-4 space-y-1.5 text-xs">
|
|
161
179
|
{f.blockerList.map((b) => (
|
|
@@ -178,7 +196,6 @@ function CostBlock({ cost }) {
|
|
|
178
196
|
['Tokens da sessão', cost.sessionUsage ? num(tokens(cost.sessionUsage)) : 'desconhecido', cost.sessionUsage ? `${num(cost.sessionUsage.cacheReadTokens)} lidos do cache` : cost.legacyUsageSamples ? 'telemetria v1, sem delta somável' : null],
|
|
179
197
|
['Tokens dos sub-agents', cost.agentUsage ? num(tokens(cost.agentUsage)) : 'desconhecido', cost.agentUsage ? `${cost.agentUsage.agents} sub-agents` : null],
|
|
180
198
|
['Despachos', String(cost.dispatches?.count ?? 0), cost.dispatches?.promptChars ? `${num(cost.dispatches.promptChars)} caracteres de prompt` : null],
|
|
181
|
-
['Tempo em tasks', duration(cost.taskDurationSeconds), null],
|
|
182
199
|
['Lido para o contexto', bytes(cost.bytesTotal), null],
|
|
183
200
|
];
|
|
184
201
|
return (
|
|
@@ -211,9 +228,7 @@ function OverviewTab({ f, cost }) {
|
|
|
211
228
|
<Panel title="Avaliação"><VerdictBlock f={f} /></Panel>
|
|
212
229
|
<Panel title="Último verify">
|
|
213
230
|
{f.verification && (
|
|
214
|
-
<p className="mb-3 text-xs text-[var(--color-faint)]">
|
|
215
|
-
escopo {f.verification.scope || '—'}, {timeAgo(f.verification.ts)}
|
|
216
|
-
</p>
|
|
231
|
+
<p className="mb-3 text-xs text-[var(--color-faint)]">{timeAgo(f.verification.ts)}</p>
|
|
217
232
|
)}
|
|
218
233
|
<NodeChips nodes={f.verificationNodes} />
|
|
219
234
|
</Panel>
|
|
@@ -223,81 +238,89 @@ function OverviewTab({ f, cost }) {
|
|
|
223
238
|
);
|
|
224
239
|
}
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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;
|
|
229
277
|
}
|
|
230
278
|
|
|
231
|
-
function
|
|
232
|
-
const st =
|
|
279
|
+
function SliceRow({ sl, f, open, onToggle, ready }) {
|
|
280
|
+
const st = SLICE_STATUS[sl.status] || SLICE_STATUS.pendente;
|
|
233
281
|
const StIcon = st.icon;
|
|
234
|
-
const
|
|
235
|
-
const verify = t.verification;
|
|
282
|
+
const report = reportPathFor(f, sl.id);
|
|
236
283
|
return (
|
|
237
284
|
<li>
|
|
238
285
|
<button
|
|
239
286
|
type="button"
|
|
240
287
|
onClick={onToggle}
|
|
241
288
|
aria-expanded={open}
|
|
242
|
-
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)
|
|
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]"
|
|
243
290
|
>
|
|
244
291
|
<StIcon weight="light" size={17} className={st.tone} aria-label={st.label} />
|
|
245
|
-
<span className="font-mono text-xs text-[var(--color-dim)]">{
|
|
292
|
+
<span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
|
|
246
293
|
<span className="flex min-w-0 items-center gap-2">
|
|
247
|
-
<span className="truncate text-sm text-[var(--color-fg)]">{
|
|
294
|
+
<span className="truncate text-sm text-[var(--color-fg)]">{sl.title}</span>
|
|
248
295
|
{ready && <span className="shrink-0 text-xs text-[var(--color-phase-implement)]">pronta</span>}
|
|
249
|
-
{secs ? <span className="shrink-0 text-xs text-[var(--color-faint)]">{duration(secs)}</span> : null}
|
|
250
|
-
</span>
|
|
251
|
-
<span className="hidden truncate text-xs text-[var(--color-dim)] md:block">{t.agent ? `morph-${t.agent}` : '—'}</span>
|
|
252
|
-
<span className="hidden text-xs md:block">
|
|
253
|
-
{verify ? <span className={verify.status === 'pass' ? 'text-[var(--color-ok)]' : 'text-[var(--color-bad)]'}>verify {verify.status}</span> : <span className="text-[var(--color-faint)]">—</span>}
|
|
254
296
|
</span>
|
|
255
|
-
<span className="hidden
|
|
297
|
+
<span className="hidden truncate text-xs text-[var(--color-dim)] md:block">{sl.persona ? `morph-${sl.persona}` : '—'}</span>
|
|
256
298
|
<span className="hidden text-[var(--color-faint)] md:block">{open ? <CaretDown weight="light" size={14} /> : <CaretRight weight="light" size={14} />}</span>
|
|
257
299
|
</button>
|
|
258
300
|
{open && (
|
|
259
301
|
<div className="grid gap-5 px-5 pb-5 pl-[4.75rem] md:grid-cols-2">
|
|
260
|
-
<div className="space-y-4 text-sm">
|
|
261
|
-
{t.description && <p className="leading-relaxed text-[var(--color-dim)]">{t.description}</p>}
|
|
262
|
-
{Array.isArray(t.doneCriteria) && t.doneCriteria.length > 0 && (
|
|
263
|
-
<div>
|
|
264
|
-
<p className="mb-1.5 text-xs text-[var(--color-faint)]">Critérios de pronto</p>
|
|
265
|
-
<ul className="list-disc space-y-1 pl-4 text-[var(--color-dim)] marker:text-[var(--color-faint)]">
|
|
266
|
-
{t.doneCriteria.map((c, i) => <li key={i}>{c}</li>)}
|
|
267
|
-
</ul>
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
{t.notes && <p className="text-[var(--color-dim)]"><span className="text-[var(--color-faint)]">Notas: </span>{t.notes}</p>}
|
|
271
|
-
</div>
|
|
272
302
|
<dl className="space-y-3 text-sm">
|
|
273
|
-
{
|
|
303
|
+
{sl.persona && (
|
|
274
304
|
<div><dt className="text-xs text-[var(--color-faint)]">Persona</dt>
|
|
275
|
-
<dd><EntityLink kind="persona" id={
|
|
305
|
+
<dd><EntityLink kind="persona" id={sl.persona}>morph-{sl.persona}</EntityLink></dd></div>
|
|
276
306
|
)}
|
|
277
|
-
{
|
|
278
|
-
<div><dt className="text-xs text-[var(--color-faint)]">
|
|
279
|
-
<dd className="mt-1 flex flex-wrap gap-1.5">
|
|
280
|
-
{t.standards.map((s) => (
|
|
281
|
-
<EntityLink key={s} kind="standard" id={standardKey(s)} chip title={s}>{standardKey(s).replace(/\.md$/, '')}</EntityLink>
|
|
282
|
-
))}
|
|
283
|
-
</dd></div>
|
|
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>
|
|
284
309
|
)}
|
|
285
|
-
{
|
|
286
|
-
<div><dt className="text-xs text-[var(--color-faint)]">Depende de</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{
|
|
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>
|
|
287
312
|
)}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{
|
|
313
|
+
</dl>
|
|
314
|
+
<dl className="space-y-3 text-sm">
|
|
315
|
+
{sl.files?.length > 0 && (
|
|
291
316
|
<div><dt className="text-xs text-[var(--color-faint)]">Arquivos</dt>
|
|
292
|
-
<dd className="mt-1 space-y-0.5 font-mono text-[11px] text-[var(--color-dim)]">{
|
|
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>
|
|
293
318
|
)}
|
|
294
|
-
{
|
|
319
|
+
{report && (
|
|
295
320
|
<div className="flex flex-wrap gap-2 pt-1">
|
|
296
|
-
{
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
</Link>
|
|
300
|
-
))}
|
|
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>
|
|
301
324
|
</div>
|
|
302
325
|
)}
|
|
303
326
|
</dl>
|
|
@@ -308,44 +331,37 @@ function TaskRow({ t, f, open, onToggle, duration: secs, ready }) {
|
|
|
308
331
|
}
|
|
309
332
|
|
|
310
333
|
const BOARD_COLUMNS = [
|
|
311
|
-
{ id: '
|
|
312
|
-
{ id: '
|
|
313
|
-
{ id: '
|
|
314
|
-
{ id: 'done', label: 'Feitas', match: (t) => t.status === 'done' || t.status === 'completed' || t.status === 'skipped' },
|
|
334
|
+
{ id: 'pendente', label: 'Pendentes' },
|
|
335
|
+
{ id: 'parcial', label: 'Parciais' },
|
|
336
|
+
{ id: 'feito', label: 'Feitas' },
|
|
315
337
|
];
|
|
316
338
|
|
|
317
|
-
function
|
|
318
|
-
const
|
|
339
|
+
function SliceCard({ sl, f, ready }) {
|
|
340
|
+
const report = reportPathFor(f, sl.id);
|
|
319
341
|
return (
|
|
320
342
|
<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)]">
|
|
321
343
|
<p className="flex items-baseline gap-2">
|
|
322
|
-
<span className="font-mono text-xs text-[var(--color-dim)]">{
|
|
344
|
+
<span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
|
|
323
345
|
{ready && <span className="text-xs text-[var(--color-phase-implement)]">pronta</span>}
|
|
324
346
|
</p>
|
|
325
|
-
<p className="mt-1 text-sm leading-snug text-[var(--color-fg)]">{
|
|
326
|
-
{
|
|
327
|
-
<EntityLink kind="persona" id={
|
|
347
|
+
<p className="mt-1 text-sm leading-snug text-[var(--color-fg)]">{sl.title}</p>
|
|
348
|
+
{sl.persona && (
|
|
349
|
+
<EntityLink kind="persona" id={sl.persona} className="mt-2 inline-block text-xs">morph-{sl.persona}</EntityLink>
|
|
328
350
|
)}
|
|
329
|
-
{
|
|
330
|
-
|
|
331
|
-
<
|
|
332
|
-
{t.verification && <span className={t.verification.status === 'pass' ? 'text-[var(--color-ok)]' : 'text-[var(--color-bad)]'}>verify {t.verification.status}</span>}
|
|
333
|
-
</span>
|
|
334
|
-
<span className="text-[var(--color-faint)]">{secs ? duration(secs) : ''}</span>
|
|
335
|
-
</div>
|
|
336
|
-
{reports.length > 0 && (
|
|
337
|
-
<Link to={`?tab=docs&doc=${encodeURIComponent(reports[0])}`} 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>
|
|
351
|
+
{sl.depends?.length > 0 && <p className="mt-1 text-xs text-[var(--color-faint)]">depende de {sl.depends.join(', ')}</p>}
|
|
352
|
+
{report && (
|
|
353
|
+
<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>
|
|
338
354
|
)}
|
|
339
355
|
</li>
|
|
340
356
|
);
|
|
341
357
|
}
|
|
342
358
|
|
|
343
|
-
function
|
|
359
|
+
function SlicesBoard({ f, byId }) {
|
|
344
360
|
return (
|
|
345
361
|
<div className="-mx-4 overflow-x-auto px-4 pb-2 md:mx-0 md:px-0">
|
|
346
362
|
<div className="grid auto-cols-[16rem] grid-flow-col gap-3 md:auto-cols-[minmax(12rem,1fr)]">
|
|
347
363
|
{BOARD_COLUMNS.map((c) => {
|
|
348
|
-
const items = f.
|
|
364
|
+
const items = f.slicesList.filter((sl) => sl.status === c.id);
|
|
349
365
|
return (
|
|
350
366
|
<section key={c.id} className="bezel min-w-0" aria-label={`${c.label}: ${items.length}`}>
|
|
351
367
|
<div className="bezel-core flex h-full min-h-[12rem] flex-col p-2.5">
|
|
@@ -354,7 +370,7 @@ function TasksBoard({ f, durations, byId }) {
|
|
|
354
370
|
</h3>
|
|
355
371
|
{items.length === 0
|
|
356
372
|
? <p className="px-1.5 text-xs text-[var(--color-faint)]">Nenhuma.</p>
|
|
357
|
-
: <ul className="space-y-2">{items.map((
|
|
373
|
+
: <ul className="space-y-2">{items.map((sl) => <SliceCard key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)} />)}</ul>}
|
|
358
374
|
</div>
|
|
359
375
|
</section>
|
|
360
376
|
);
|
|
@@ -364,34 +380,27 @@ function TasksBoard({ f, durations, byId }) {
|
|
|
364
380
|
);
|
|
365
381
|
}
|
|
366
382
|
|
|
367
|
-
function
|
|
383
|
+
function SlicesTab({ f, view = 'list', onView = () => {} }) {
|
|
368
384
|
const [open, setOpen] = useState(null);
|
|
369
|
-
if (!f.
|
|
370
|
-
return <Panel><p className="text-sm text-[var(--color-dim)]">
|
|
385
|
+
if (!f.slicesList.length) {
|
|
386
|
+
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>;
|
|
371
387
|
}
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const ready = f.tasksList.filter((t) => isReady(t, byId)).length;
|
|
388
|
+
const byId = Object.fromEntries(f.slicesList.map((sl) => [sl.id, sl]));
|
|
389
|
+
const ready = f.slicesList.filter((sl) => isReady(sl, byId)).length;
|
|
375
390
|
return (
|
|
376
391
|
<div className="space-y-4">
|
|
377
392
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
378
|
-
<Segmented label="Visualização das
|
|
393
|
+
<Segmented label="Visualização das fatias" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
|
|
379
394
|
<p className="text-sm text-[var(--color-dim)]">
|
|
380
|
-
{f.
|
|
395
|
+
{f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{ready ? `, ${ready} prontas para começar` : ''}
|
|
381
396
|
</p>
|
|
382
397
|
</div>
|
|
383
|
-
{f
|
|
384
|
-
<p className="flex items-start gap-2 text-sm text-[var(--color-warn)]">
|
|
385
|
-
<WarningCircle weight="light" size={17} className="mt-0.5 shrink-0" />
|
|
386
|
-
O tasks.json não passa no schema ({f.tasksErrors.length} erros). As tasks aparecem como estão no arquivo.
|
|
387
|
-
</p>
|
|
388
|
-
)}
|
|
389
|
-
{view === 'board' ? <TasksBoard f={f} durations={durations} byId={byId} /> : (
|
|
398
|
+
{view === 'board' ? <SlicesBoard f={f} byId={byId} /> : (
|
|
390
399
|
<div className="bezel">
|
|
391
400
|
<ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
|
|
392
|
-
{f.
|
|
393
|
-
<
|
|
394
|
-
open={open ===
|
|
401
|
+
{f.slicesList.map((sl) => (
|
|
402
|
+
<SliceRow key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)}
|
|
403
|
+
open={open === sl.id} onToggle={() => setOpen(open === sl.id ? null : sl.id)} />
|
|
395
404
|
))}
|
|
396
405
|
</ul>
|
|
397
406
|
</div>
|
|
@@ -436,7 +445,7 @@ function DocViewer({ project, feature, path, dir }) {
|
|
|
436
445
|
|
|
437
446
|
function DocsTab({ f, doc, onDoc, project }) {
|
|
438
447
|
const groups = groupDocuments(f.documents);
|
|
439
|
-
const selected = doc || f.documents.find((d) => d.path.endsWith('
|
|
448
|
+
const selected = doc || f.documents.find((d) => d.path.endsWith('plan.md'))?.path || f.documents.find((d) => d.path.endsWith('.md'))?.path || null;
|
|
440
449
|
return (
|
|
441
450
|
<div className="grid gap-5 lg:grid-cols-[16rem_minmax(0,1fr)]">
|
|
442
451
|
<nav aria-label="Documentos da feature" className="lg:sticky lg:top-4 lg:self-start">
|
|
@@ -481,7 +490,7 @@ function DocsTab({ f, doc, onDoc, project }) {
|
|
|
481
490
|
);
|
|
482
491
|
}
|
|
483
492
|
|
|
484
|
-
export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = null,
|
|
493
|
+
export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = null, slicesView = 'list', onSlicesView = () => {}, now = Date.now(), project, projectName }) {
|
|
485
494
|
const verdict = verdictLabel(f);
|
|
486
495
|
const working = latest && isWorking({ ...f, lastActivity: latest }, now);
|
|
487
496
|
return (
|
|
@@ -500,7 +509,7 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
|
|
|
500
509
|
</p>
|
|
501
510
|
</div>
|
|
502
511
|
<div className="flex shrink-0 gap-6 text-right">
|
|
503
|
-
<div><p className="text-2xl font-semibold tabular-nums">{f.
|
|
512
|
+
<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>
|
|
504
513
|
<div><p className="text-2xl font-semibold">{verdict ?? '—'}</p><p className="text-xs text-[var(--color-faint)]">veredito</p></div>
|
|
505
514
|
</div>
|
|
506
515
|
</div>
|
|
@@ -546,14 +555,15 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
|
|
|
546
555
|
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)]',
|
|
547
556
|
].join(' ')}
|
|
548
557
|
>
|
|
549
|
-
{t.label}{t.id === '
|
|
558
|
+
{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}` : ''}
|
|
550
559
|
</button>
|
|
551
560
|
))}
|
|
552
561
|
</div>
|
|
553
562
|
|
|
554
563
|
<div className="rise-3">
|
|
555
564
|
{tab === 'overview' && <OverviewTab f={f} cost={cost} />}
|
|
556
|
-
{tab === '
|
|
565
|
+
{tab === 'slices' && <SlicesTab f={f} view={slicesView} onView={onSlicesView} />}
|
|
566
|
+
{tab === 'checks' && <ChecksTab f={f} />}
|
|
557
567
|
{tab === 'docs' && <DocsTab f={f} doc={doc} onDoc={onDoc} project={project} />}
|
|
558
568
|
{tab === 'activity' && (
|
|
559
569
|
<div className="bezel"><div className="bezel-core px-5 py-5"><ActivityFeed project={project} feature={f.name} /></div></div>
|
|
@@ -572,7 +582,7 @@ export default function FeatureDetail() {
|
|
|
572
582
|
// The cost rides on the activity endpoint (it owns the event stream); one
|
|
573
583
|
// line of feed is enough to get it, and it refreshes with the log.
|
|
574
584
|
const costQuery = useMorphQuery(`${apiBase(project)}/activity?feature=${encodeURIComponent(name)}&limit=1`, { scope: 'activity', project });
|
|
575
|
-
const [
|
|
585
|
+
const [slicesView, setSlicesView] = usePref('slices.view', 'list');
|
|
576
586
|
const now = useNow();
|
|
577
587
|
|
|
578
588
|
const onTab = useCallback((id) => setParams((p) => { const n = new URLSearchParams(p); n.set('tab', id); if (id !== 'docs') n.delete('doc'); return n; }), [setParams]);
|
|
@@ -592,7 +602,7 @@ export default function FeatureDetail() {
|
|
|
592
602
|
<FeatureDetailView
|
|
593
603
|
f={data} tab={tab} onTab={onTab} doc={doc} onDoc={onDoc}
|
|
594
604
|
cost={costQuery.data?.cost ?? null} latest={costQuery.data?.items?.[0] ?? null}
|
|
595
|
-
|
|
605
|
+
slicesView={slicesView} onSlicesView={setSlicesView} now={now} project={project}
|
|
596
606
|
/>
|
|
597
607
|
);
|
|
598
608
|
}
|
|
@@ -41,15 +41,15 @@ function VerifyMark({ verification }) {
|
|
|
41
41
|
: <XCircle weight="light" className="text-[var(--color-bad)]" size={16} aria-label="último verify falhou" />;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function
|
|
45
|
-
if (!
|
|
46
|
-
const pct = Math.round((
|
|
44
|
+
function SliceMeter({ slices, wide = false }) {
|
|
45
|
+
if (!slices?.total) return <span className="text-xs text-[var(--color-faint)]">sem fatias</span>;
|
|
46
|
+
const pct = Math.round((slices.done / slices.total) * 100);
|
|
47
47
|
return (
|
|
48
|
-
<div className="flex items-center gap-2" title={`${
|
|
48
|
+
<div className="flex items-center gap-2" title={`${slices.done} de ${slices.total} fatias feitas`}>
|
|
49
49
|
<div className={`h-1 overflow-hidden rounded-full bg-white/[0.07] ${wide ? 'flex-1' : 'w-12'}`}>
|
|
50
50
|
<div className="h-full rounded-full bg-[var(--color-fg)]/70" style={{ width: `${pct}%` }} />
|
|
51
51
|
</div>
|
|
52
|
-
<span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{
|
|
52
|
+
<span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{slices.done}/{slices.total}</span>
|
|
53
53
|
</div>
|
|
54
54
|
);
|
|
55
55
|
}
|
|
@@ -106,9 +106,9 @@ export function FeatureRow({ f, now, project, projectName }) {
|
|
|
106
106
|
<PhaseRail feature={f} />
|
|
107
107
|
{/* One line on a phone; on desktop the wrapper dissolves into the row's grid. */}
|
|
108
108
|
<div className="flex items-center gap-5 md:contents">
|
|
109
|
-
<
|
|
109
|
+
<SliceMeter slices={f.slices} />
|
|
110
110
|
<VerifyMark verification={f.verification} />
|
|
111
|
-
<span className="font-mono text-sm tabular-nums text-[var(--color-dim)]" title={verdict ? 'veredito do
|
|
111
|
+
<span className="font-mono text-sm tabular-nums text-[var(--color-dim)]" title={verdict ? 'veredito do Gate 3' : 'sem veredito'}>
|
|
112
112
|
{verdict ?? '—'}
|
|
113
113
|
</span>
|
|
114
114
|
<span className="flex items-center gap-1.5 text-xs text-[var(--color-faint)]" title={LOCATION_LABEL[f.location]}>
|
|
@@ -165,7 +165,7 @@ function BoardCard({ f, now, project }) {
|
|
|
165
165
|
{f.lastActivity?.title && !isFinished(f) && (
|
|
166
166
|
<p className="mt-1.5 line-clamp-2 text-xs text-[var(--color-dim)]">{f.lastActivity.title}</p>
|
|
167
167
|
)}
|
|
168
|
-
<div className="mt-3"><
|
|
168
|
+
<div className="mt-3"><SliceMeter slices={f.slices} wide /></div>
|
|
169
169
|
<div className="mt-2.5 flex items-center justify-between text-xs text-[var(--color-faint)]">
|
|
170
170
|
<span className="flex items-center gap-2">
|
|
171
171
|
<VerifyMark verification={f.verification} />
|
|
@@ -329,7 +329,7 @@ export function FeaturesView({
|
|
|
329
329
|
)}
|
|
330
330
|
{info.initialized && !info.legacyStateOnly && features.length === 0 && (
|
|
331
331
|
<Empty title="Nenhuma feature ainda">
|
|
332
|
-
No Claude Code, comece uma com <code className="font-mono text-[var(--color-fg)]">/morph-proposal</code
|
|
332
|
+
No Claude Code, comece uma com <code className="font-mono text-[var(--color-fg)]">/morph-proposal</code>, que entrevista e escreve o plano. Ela aparece aqui assim que o <code className="font-mono">feature.json</code> nascer.
|
|
333
333
|
</Empty>
|
|
334
334
|
)}
|
|
335
335
|
{features.length > 0 && visible.length === 0 && (
|
|
@@ -27,9 +27,9 @@ export function Flow() {
|
|
|
27
27
|
<p className="mt-1 max-w-3xl text-sm leading-relaxed text-[var(--color-dim)]">
|
|
28
28
|
Você descreve o que quer; o Claude conduz cada fase com uma skill, despacha personas especialistas como sub-agents e para nos gates.
|
|
29
29
|
Com trust <span className="font-mono text-[var(--color-fg)]">auto</span>, um gate só pausa para você quando aparece um sinal de risco.
|
|
30
|
-
No fim,
|
|
30
|
+
No fim, verificador e revisor independentes dão o veredito (SHIP ou FIX).
|
|
31
31
|
</p>
|
|
32
|
-
<ol className="mt-6 grid gap-3 md:grid-cols-
|
|
32
|
+
<ol className="mt-6 grid gap-3 md:grid-cols-4">
|
|
33
33
|
{PHASES.map((p) => {
|
|
34
34
|
const people = personasIn(p.id);
|
|
35
35
|
return (
|
|
@@ -11,10 +11,9 @@ const phaseColor = (id) => `var(--color-phase-${id})`;
|
|
|
11
11
|
|
|
12
12
|
/** What each approval gate checks, in the words a reader of the pipeline needs. */
|
|
13
13
|
const GATE_CHECKS = {
|
|
14
|
-
|
|
15
|
-
uiux: 'Confere o design das telas (mockups e checklist de tela). É opcional: só existe quando a feature passa pela fase de UI/UX.',
|
|
16
|
-
|
|
17
|
-
review: 'Confere a avaliação independente: um avaliador em contexto novo dá o veredito (SHIP ou FIX) e só bloqueia com evidência, com o verify verde.',
|
|
14
|
+
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).',
|
|
15
|
+
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.',
|
|
16
|
+
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.',
|
|
18
17
|
};
|
|
19
18
|
|
|
20
19
|
const GATE_PREFIX = 'gate-';
|
|
@@ -30,7 +30,7 @@ function categoryColors(standards) {
|
|
|
30
30
|
return Object.fromEntries(cats.map((c, i) => [c, PALETTE[i % PALETTE.length]]));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// Resolve `?sel=` to a standard: by id first, then by the path a plan cites.
|
|
34
34
|
export function findStandard(list, sel) {
|
|
35
35
|
if (!sel) return null;
|
|
36
36
|
return list.find((s) => s.id === sel) || list.find((s) => s.path === standardKey(sel)) || null;
|
|
@@ -15,7 +15,7 @@ import { loadTelemetryAllTrees, aggregateCost, dedupeEvents } from '../../lib/te
|
|
|
15
15
|
import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
16
16
|
|
|
17
17
|
/** Kinds a person follows by default; the rest are one click away. */
|
|
18
|
-
export const DEFAULT_KINDS = ['prompt', 'question', 'skill', 'dispatch', 'handback', '
|
|
18
|
+
export const DEFAULT_KINDS = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'verify', 'gate', 'failure'];
|
|
19
19
|
export const ALL_KINDS = [...DEFAULT_KINDS, 'write', 'read', 'command', 'tool'];
|
|
20
20
|
|
|
21
21
|
const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|finish)\b/;
|
|
@@ -50,14 +50,14 @@ function nodesSummary(nodes) {
|
|
|
50
50
|
* @returns {null | {
|
|
51
51
|
* id: string, ts: string, feature: string|null, phase: string|null,
|
|
52
52
|
* kind: string, title: string, detail: string|null, status: 'ok'|'fail'|null,
|
|
53
|
-
* refs: { persona?: string, skill?: string,
|
|
53
|
+
* refs: { persona?: string, skill?: string, path?: string },
|
|
54
54
|
* nodes?: Array<{name: string, status: string|null}>
|
|
55
55
|
* }}
|
|
56
56
|
*/
|
|
57
57
|
export function describeEvent(e) {
|
|
58
58
|
if (!e || typeof e !== 'object' || SILENT_EVENTS.has(e.event)) return null;
|
|
59
59
|
const base = {
|
|
60
|
-
id: e.eventId || `${e.ts}:${e.event}:${e.tool || ''}
|
|
60
|
+
id: e.eventId || `${e.ts}:${e.event}:${e.tool || ''}`,
|
|
61
61
|
ts: e.ts || null,
|
|
62
62
|
feature: e.feature || null,
|
|
63
63
|
phase: e.phase || null,
|
|
@@ -78,18 +78,10 @@ export function describeEvent(e) {
|
|
|
78
78
|
if (text.startsWith('<')) return null;
|
|
79
79
|
return line('prompt', 'Você escreveu', { detail: clip(text) });
|
|
80
80
|
}
|
|
81
|
-
if (e.event === 'TaskStart') return line('task', `${e.task} começou`, { refs: { task: e.task } });
|
|
82
|
-
if (e.event === 'TaskEnd') {
|
|
83
|
-
const done = e.status === 'done';
|
|
84
|
-
return line('task', done ? `${e.task} concluída` : `${e.task} terminou (${e.status ?? 'sem status'})`, {
|
|
85
|
-
status: done ? 'ok' : null, refs: { task: e.task },
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
81
|
if (e.event === 'Verify') {
|
|
89
82
|
const ok = e.status === 'pass';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
status: ok ? 'ok' : 'fail', nodes: nodesSummary(e.nodes), refs: e.task ? { task: e.task } : {},
|
|
83
|
+
return line('verify', `verify ${ok ? 'passou' : 'falhou'}`, {
|
|
84
|
+
status: ok ? 'ok' : 'fail', nodes: nodesSummary(e.nodes),
|
|
93
85
|
detail: e.persisted === false && e.reason ? `não carimbou: ${e.reason}` : null,
|
|
94
86
|
});
|
|
95
87
|
}
|
|
@@ -344,8 +336,6 @@ export function featureCost(events, feature) {
|
|
|
344
336
|
dispatches: c.dispatches,
|
|
345
337
|
bytesTotal: c.bytesTotal,
|
|
346
338
|
bytesByKind: c.bytesByKind,
|
|
347
|
-
taskDurationSeconds: c.taskDurationSeconds,
|
|
348
|
-
tasks: c.tasks,
|
|
349
339
|
events: c.total,
|
|
350
340
|
};
|
|
351
341
|
}
|
|
@@ -13,7 +13,7 @@ const GENERIC_DISPATCH = 'morph-{id}';
|
|
|
13
13
|
*
|
|
14
14
|
* v5 has no phase list on the persona: a phase dispatches a persona when its
|
|
15
15
|
* skill (`phases.json → skill`) names `morph-{personaId}` on a dispatch line
|
|
16
|
-
* (morph-uiux → ui-designer, morph-plan → scout, morph-review →
|
|
16
|
+
* (morph-uiux → ui-designer, morph-plan → scout, morph-review → verifier, reviewer, security).
|
|
17
17
|
* A skill that dispatches the generic `morph-{id}` runs every implementer —
|
|
18
18
|
* the same `isImplementer` the validator and the installer use.
|
|
19
19
|
*
|