@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.
- package/CLAUDE.md +12 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +28 -36
- package/docs/QUICKSTART.md +31 -191
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-preflight.md +3 -3
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +84 -95
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/mcp/registry.json +4 -4
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +10 -2
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +11 -18
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +39 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +49 -109
- package/framework/skills/core/morph-plan/references/critique.md +43 -0
- package/framework/skills/core/morph-plan/references/interview.md +37 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +51 -28
- package/framework/standards/STANDARDS.meta.json +16 -19
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +70 -0
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +15 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +3 -14
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +2 -2
- package/src/commands/state/create.js +1 -6
- package/src/commands/state/gate-check.js +38 -31
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/installers/memory-installer.js +3 -3
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +61 -109
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +28 -4
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
|
@@ -1,119 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mockups: {{titleCase FEATURE_NAME}}
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<!-- Uma `## Tela:` por tela nova ou alterada. As opções navegáveis ficam em 1-design/opcoes/{tela}.html;
|
|
4
|
+
a escolhida vira 1-design/prototipos/{tela}.html, com os prints {tela}-1280.png e {tela}-390.png ao lado,
|
|
5
|
+
e um seletor de estado (data-estado="vazio|carregando|erro", ?estado=) com {tela}-{estado}-390.png de cada
|
|
6
|
+
estado que o checklist diz que se aplica. Faltando um, o Gate uiux pausa. -->
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Screen Inventory
|
|
11
|
-
|
|
12
|
-
| Screen ID | Name | Route | Priority |
|
|
13
|
-
|-----------|------|-------|----------|
|
|
14
|
-
| **S001** | {Screen Name} | /path | High |
|
|
15
|
-
| **S002** | {Screen Name} | /path | Medium |
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Tela: {Primary Screen}
|
|
8
|
+
## Tela: {Nome da tela}
|
|
20
9
|
|
|
21
10
|
**Protótipo:** 1-design/prototipos/{tela}.html
|
|
22
|
-
|
|
23
|
-
O protótipo renderizado é a fonte visual da tela; os prints `{tela}-1280.png` e `{tela}-390.png` ficam ao lado dele, e sem os três arquivos o Gate uiux pausa. O layout em texto abaixo é opcional.
|
|
24
|
-
|
|
25
|
-
### Desktop Layout
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
┌─────────────────────────────────────┐
|
|
29
|
-
│ Header [User ▼] │
|
|
30
|
-
├─────────────────────────────────────┤
|
|
31
|
-
│ │
|
|
32
|
-
│ [Title] │
|
|
33
|
-
│ │
|
|
34
|
-
│ ┌──────────────────────────────┐ │
|
|
35
|
-
│ │ Main Content Area │ │
|
|
36
|
-
│ │ │ │
|
|
37
|
-
│ │ [Card 1] [Card 2] │ │
|
|
38
|
-
│ │ │ │
|
|
39
|
-
│ │ ┌────────────────────────┐ │ │
|
|
40
|
-
│ │ │ Data Table │ │ │
|
|
41
|
-
│ │ │ ... │ │ │
|
|
42
|
-
│ │ └────────────────────────┘ │ │
|
|
43
|
-
│ └──────────────────────────────┘ │
|
|
44
|
-
│ │
|
|
45
|
-
└─────────────────────────────────────┘
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Mobile Layout
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
┌──────────────────────┐
|
|
52
|
-
│ ☰ Title [User ▼]│
|
|
53
|
-
├──────────────────────┤
|
|
54
|
-
│ │
|
|
55
|
-
│ [Card 1] │
|
|
56
|
-
│ │
|
|
57
|
-
│ [Card 2] │
|
|
58
|
-
│ │
|
|
59
|
-
│ [View All Items] │
|
|
60
|
-
│ │
|
|
61
|
-
└──────────────────────┘
|
|
62
|
-
```
|
|
11
|
+
**Direção escolhida:** <qual opção de opcoes/{tela}.html e por quê, numa frase>
|
|
63
12
|
|
|
64
13
|
### Checklist de tela
|
|
65
14
|
|
|
66
|
-
Troque cada `…` pela decisão
|
|
15
|
+
Troque cada `…` pela decisão. "Não se aplica" vale se escrito; `…`, vazio ou "a definir" reprovam o Gate uiux.
|
|
67
16
|
|
|
68
17
|
| Item | Decisão |
|
|
69
18
|
|---|---|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
| Estado vazio | … |
|
|
76
|
-
| Estado carregando | … |
|
|
77
|
-
| Estado de erro | … |
|
|
78
|
-
| Mobile | … |
|
|
79
|
-
| Abas | … |
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## States
|
|
84
|
-
|
|
85
|
-
### Loading
|
|
86
|
-
- Show skeleton placeholders
|
|
87
|
-
- Spinner for primary content
|
|
88
|
-
|
|
89
|
-
### Empty
|
|
90
|
-
- Icon + message
|
|
91
|
-
- Clear call-to-action button
|
|
92
|
-
|
|
93
|
-
### Error
|
|
94
|
-
- Warning icon
|
|
95
|
-
- User-friendly message
|
|
96
|
-
- Retry button
|
|
97
|
-
|
|
98
|
-
### Success (After Action)
|
|
99
|
-
- Toast notification
|
|
100
|
-
- Auto-dismiss after 3s
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Interactions
|
|
105
|
-
|
|
106
|
-
- **Hover**: Subtle elevation increase
|
|
107
|
-
- **Click**: Scale down (0.98)
|
|
108
|
-
- **Transitions**: Fade-in 200ms
|
|
19
|
+
| Lista no servidor | … |
|
|
20
|
+
| Estados vazio e carregando | … |
|
|
21
|
+
| Erro com próxima ação | … |
|
|
22
|
+
| Uma ação primária | … |
|
|
23
|
+
| Celular | … |
|
|
109
24
|
|
|
110
|
-
|
|
25
|
+
### Componentes
|
|
111
26
|
|
|
112
|
-
|
|
113
|
-
- [ ] 44x44px minimum touch targets
|
|
114
|
-
- [ ] Focus indicators visible
|
|
115
|
-
- [ ] Proper heading hierarchy (H1 → H2 → H3)
|
|
27
|
+
- <componente do design system reusado, ou novo e por quê>
|
|
116
28
|
|
|
117
|
-
|
|
29
|
+
### Fluxo
|
|
118
30
|
|
|
119
|
-
|
|
31
|
+
1. <passo do usuário até o resultado>
|
package/package.json
CHANGED
package/src/commands/e2e/e2e.js
CHANGED
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
deleteStamp,
|
|
17
17
|
getStackStatus,
|
|
18
18
|
isDockerAvailable,
|
|
19
|
+
isHostOnlyStack,
|
|
19
20
|
lintComposeIsolation,
|
|
21
|
+
resetE2eData,
|
|
20
22
|
readStamp,
|
|
21
23
|
resolveComposeFile,
|
|
22
24
|
resolveSpecCommand,
|
|
@@ -34,8 +36,9 @@ import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js
|
|
|
34
36
|
const EXIT_USAGE = 2;
|
|
35
37
|
const EXIT_BUSY = 3;
|
|
36
38
|
|
|
37
|
-
// No compose file is a usage error here, unlike in the verify node where
|
|
38
|
-
|
|
39
|
+
// No compose file and no e2e.hostServices is a usage error here, unlike in the verify node where
|
|
40
|
+
// it is a graded skip. `composeFile` is null for a host-only stack.
|
|
41
|
+
function resolveContext(featureName, { json = false, onMissing } = {}) {
|
|
39
42
|
const root = resolvePrimaryRoot(process.cwd());
|
|
40
43
|
const found = findWorktreeForFeature(featureName, root);
|
|
41
44
|
const cwd = found?.path ?? root;
|
|
@@ -43,11 +46,13 @@ function resolveContext(featureName, { json = false } = {}) {
|
|
|
43
46
|
const config = readConfig(cwd) ?? readConfig(root);
|
|
44
47
|
const composeFile = resolveComposeFile(cwd, config);
|
|
45
48
|
|
|
46
|
-
if (!composeFile) {
|
|
49
|
+
if (!composeFile && !isHostOnlyStack(config, composeFile)) {
|
|
47
50
|
const message =
|
|
48
|
-
`No compose file found in ${cwd} (compose.yaml, docker-compose.yml, …). ` +
|
|
49
|
-
'Set
|
|
50
|
-
|
|
51
|
+
`No compose file found in ${cwd} (compose.yaml, docker-compose.yml, …) and no e2e.hostServices. ` +
|
|
52
|
+
'Set e2e.composeFile in .morph/config/config.json if it lives elsewhere, ' +
|
|
53
|
+
'or declare the app in e2e.hostServices when it has no dependency to containerize.';
|
|
54
|
+
if (onMissing) onMissing(message, cwd);
|
|
55
|
+
else if (json) console.log(JSON.stringify({ error: message, tree: cwd }, null, 2));
|
|
51
56
|
else console.error(chalk.red(`\n❌ ${message}\n`));
|
|
52
57
|
return null;
|
|
53
58
|
}
|
|
@@ -86,7 +91,7 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
86
91
|
|
|
87
92
|
// Isolation BEFORE the docker check: a compose that hardcodes host ports or container names
|
|
88
93
|
// collides with a sibling worktree whatever we inject.
|
|
89
|
-
const lint = lintComposeIsolation(readComposeText(ctx.cwd, ctx.composeFile));
|
|
94
|
+
const lint = ctx.composeFile ? lintComposeIsolation(readComposeText(ctx.cwd, ctx.composeFile)) : { findings: [] };
|
|
90
95
|
const isolation = decideIsolation({ findings: lint.findings, insideWorktree: ctx.insideWorktree });
|
|
91
96
|
if (isolation.status === 'fail') {
|
|
92
97
|
if (options.json) {
|
|
@@ -106,7 +111,7 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
const exec = options._exec ?? defaultExec;
|
|
109
|
-
if (!isDockerAvailable(ctx.cwd, exec)) {
|
|
114
|
+
if (ctx.composeFile && !isDockerAvailable(ctx.cwd, exec)) {
|
|
110
115
|
console.error(chalk.red('\n❌ Docker is not available in this environment.\n'));
|
|
111
116
|
process.exit(EXIT_USAGE);
|
|
112
117
|
}
|
|
@@ -149,7 +154,7 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
149
154
|
tree: ctx.tree,
|
|
150
155
|
composeFile: ctx.composeFile,
|
|
151
156
|
status: failed ? 'fail' : 'pass',
|
|
152
|
-
reused: result.stack.reused,
|
|
157
|
+
reused: ctx.composeFile ? result.stack.reused : hostServices.reused,
|
|
153
158
|
reason: result.reason,
|
|
154
159
|
auth: { status: auth.status, reason: auth.reason },
|
|
155
160
|
...(hostServices.status === 'skip' ? {} : { hostServices: hostServices.services }),
|
|
@@ -164,7 +169,9 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
164
169
|
if (result.output) console.error(result.output);
|
|
165
170
|
for (const w of result.warnings) console.error(chalk.yellow(`⚠️ ${w}`));
|
|
166
171
|
} else {
|
|
167
|
-
const how =
|
|
172
|
+
const how = !ctx.composeFile
|
|
173
|
+
? chalk.gray(`(${result.stack.reason})`)
|
|
174
|
+
: result.stack.reused ? chalk.gray(`(${result.stack.reason})`) : chalk.gray('(built)');
|
|
168
175
|
console.log(chalk.green(`\n✓ Stack up — ${ctx.projectName} ${how}`));
|
|
169
176
|
console.log(chalk.gray(` tree: ${ctx.tree}`));
|
|
170
177
|
for (const s of hostServices.services) {
|
|
@@ -190,18 +197,24 @@ export async function e2eStatusCommand(featureName, options = {}) {
|
|
|
190
197
|
if (!ctx) process.exit(EXIT_USAGE);
|
|
191
198
|
|
|
192
199
|
const exec = options._exec ?? defaultExec;
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
const hostServices = ctx.gitCommonDir ? hostServicesStatus(ctx.gitCommonDir, ctx.projectName) : [];
|
|
201
|
+
const status = ctx.composeFile
|
|
202
|
+
? getStackStatus(ctx.cwd, {
|
|
203
|
+
composeFile: ctx.composeFile,
|
|
204
|
+
projectName: ctx.projectName,
|
|
205
|
+
env: ctx.env,
|
|
206
|
+
}, exec)
|
|
207
|
+
: {
|
|
208
|
+
up: hostServices.length > 0 && hostServices.every((h) => h.alive),
|
|
209
|
+
healthy: hostServices.length > 0 && hostServices.every((h) => h.alive && h.health !== false),
|
|
210
|
+
services: [],
|
|
211
|
+
};
|
|
198
212
|
|
|
199
213
|
const stamp = computeSourceStamp(ctx.cwd, exec);
|
|
200
214
|
const previous = ctx.gitCommonDir ? readStamp(ctx.gitCommonDir, ctx.projectName) : null;
|
|
201
215
|
const fresh = Boolean(stamp && previous?.stamp === stamp);
|
|
202
216
|
|
|
203
217
|
const spec = resolveSpecCommand(ctx.cwd, ctx.config, ctx.config?.project?.frontendPath);
|
|
204
|
-
const hostServices = ctx.gitCommonDir ? hostServicesStatus(ctx.gitCommonDir, ctx.projectName) : [];
|
|
205
218
|
|
|
206
219
|
const payload = {
|
|
207
220
|
feature: featureName,
|
|
@@ -228,7 +241,7 @@ export async function e2eStatusCommand(featureName, options = {}) {
|
|
|
228
241
|
console.log('');
|
|
229
242
|
console.log(chalk.bold(`E2E stack — ${ctx.projectName}`));
|
|
230
243
|
console.log(chalk.gray(`tree: ${ctx.tree}`));
|
|
231
|
-
console.log(chalk.gray(`compose: ${ctx.composeFile}`));
|
|
244
|
+
console.log(chalk.gray(`compose: ${ctx.composeFile ?? 'none (host services only)'}`));
|
|
232
245
|
console.log(chalk.gray(`url: ${payload.baseUrl}`));
|
|
233
246
|
console.log('');
|
|
234
247
|
console.log(` up ${status.up ? chalk.green('yes') : chalk.gray('no')}`);
|
|
@@ -257,12 +270,14 @@ export async function e2eDownCommand(featureName, options = {}) {
|
|
|
257
270
|
if (!ctx) process.exit(EXIT_USAGE);
|
|
258
271
|
|
|
259
272
|
const hostServices = stopHostServices(ctx.gitCommonDir, ctx.projectName);
|
|
260
|
-
const result =
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
const result = ctx.composeFile
|
|
274
|
+
? tearDownStack(ctx.cwd, {
|
|
275
|
+
composeFile: ctx.composeFile,
|
|
276
|
+
projectName: ctx.projectName,
|
|
277
|
+
env: ctx.env,
|
|
278
|
+
volumes: Boolean(options.volumes),
|
|
279
|
+
}, options._exec ?? defaultExec)
|
|
280
|
+
: { status: 'pass', output: '' };
|
|
266
281
|
const stampDeleted = deleteStamp(ctx.gitCommonDir, ctx.projectName);
|
|
267
282
|
const cleanup = afterHeavyRun({ projectPath: ctx.cwd, config: readHeavyConfig(ctx.cwd) });
|
|
268
283
|
|
|
@@ -288,6 +303,56 @@ export async function e2eDownCommand(featureName, options = {}) {
|
|
|
288
303
|
process.exit(0);
|
|
289
304
|
}
|
|
290
305
|
|
|
306
|
+
// Between the verifier's flows: the next flow starts from the seeded state, not from the rows the
|
|
307
|
+
// previous one left. Exit 1 only on fail; skip (nothing configured to reset) is exit 0.
|
|
308
|
+
export async function e2eResetCommand(featureName, options = {}) {
|
|
309
|
+
const emit = (payload) => {
|
|
310
|
+
if (options.json) {
|
|
311
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
312
|
+
} else if (payload.status === 'fail') {
|
|
313
|
+
console.error(chalk.red(`\n✗ ${payload.reason}\n`));
|
|
314
|
+
if (payload.output) console.error(payload.output);
|
|
315
|
+
} else if (payload.status === 'skip') {
|
|
316
|
+
console.log(chalk.gray(`\n○ e2e reset: ${payload.reason}\n`));
|
|
317
|
+
} else {
|
|
318
|
+
const what = payload.reset.mode === 'command'
|
|
319
|
+
? `e2e.resetCommand (${payload.reset.command})`
|
|
320
|
+
: payload.reset.mode === 'volumes'
|
|
321
|
+
? `recreated ${payload.reset.services.join(', ')} (volumes ${payload.reset.volumes.join(', ')})`
|
|
322
|
+
: 'no data reset';
|
|
323
|
+
console.log(chalk.green(`\n✓ e2e reset — ${what}`));
|
|
324
|
+
if (payload.auth.reason) console.log(chalk.gray(` auth: ${payload.auth.reason}`));
|
|
325
|
+
console.log('');
|
|
326
|
+
}
|
|
327
|
+
process.exit(payload.status === 'fail' ? 1 : 0);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const ctx = resolveContext(featureName, {
|
|
331
|
+
...options,
|
|
332
|
+
onMissing: (message, tree) => emit({ feature: featureName, tree, status: 'fail', reason: message }),
|
|
333
|
+
});
|
|
334
|
+
if (!ctx) return;
|
|
335
|
+
|
|
336
|
+
const result = resetE2eData(ctx.cwd, {
|
|
337
|
+
config: ctx.config,
|
|
338
|
+
composeFile: ctx.composeFile,
|
|
339
|
+
projectName: ctx.projectName,
|
|
340
|
+
gitCommonDir: ctx.gitCommonDir,
|
|
341
|
+
env: ctx.env,
|
|
342
|
+
}, options._exec ?? defaultExec);
|
|
343
|
+
|
|
344
|
+
emit({
|
|
345
|
+
feature: featureName,
|
|
346
|
+
project: ctx.projectName,
|
|
347
|
+
tree: ctx.tree,
|
|
348
|
+
status: result.status,
|
|
349
|
+
reason: result.reason,
|
|
350
|
+
reset: result.reset,
|
|
351
|
+
auth: result.auth,
|
|
352
|
+
...(result.status === 'fail' && result.output ? { output: result.output } : {}),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
291
356
|
function stoppedAnything({ killed, skipped, warnings }) {
|
|
292
357
|
return killed.length + skipped.length + warnings.length > 0;
|
|
293
358
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { loadGraph, getFreshness, staleCause } from '../../lib/graph/graph-loader.js';
|
|
4
|
+
import { parseDiff, buildDiffTree, renderDiffTree, declarationDrift } from '../../lib/graph/diff-tree.js';
|
|
5
|
+
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
6
|
+
|
|
7
|
+
const git = (cwd, ...args) => execFileSync('git', args, { cwd, stdio: 'pipe' }).toString();
|
|
8
|
+
|
|
9
|
+
function fail(options, payload, lines) {
|
|
10
|
+
if (options.json) console.log(JSON.stringify({ ok: false, ...payload }, null, 2));
|
|
11
|
+
else for (const [tone, text] of lines) console.error(chalk[tone](text));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Only the committed range: the graph is a snapshot of HEAD, so a diff of the working tree would put
|
|
16
|
+
// lines the graph never saw against symbols at the wrong place.
|
|
17
|
+
export async function graphDiffCommand(options = {}) {
|
|
18
|
+
const projectPath = options.path || process.cwd();
|
|
19
|
+
const loaded = loadGraph(projectPath);
|
|
20
|
+
if (!loaded.ok) {
|
|
21
|
+
fail(options, { reason: 'no-graph', detail: loaded.reason }, [
|
|
22
|
+
['yellow', `\n○ Sem grafo (${loaded.reason}): leia o diff cru (git diff).\n`],
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const freshness = getFreshness(projectPath, loaded.graph);
|
|
27
|
+
if (freshness.state !== 'fresh') {
|
|
28
|
+
fail(options, { reason: 'stale-graph', freshness }, [
|
|
29
|
+
['yellow', `\n○ O grafo não é deste commit (${staleCause(freshness)}): a árvore sairia com símbolos e linhas errados.`],
|
|
30
|
+
['gray', ' Rode `morph-spec graph refresh` e repita; ou leia o diff cru (git diff).\n'],
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let base = options.base;
|
|
35
|
+
try {
|
|
36
|
+
base ??= git(projectPath, 'merge-base', detectDefaultBranch(projectPath) || 'main', 'HEAD').trim();
|
|
37
|
+
} catch {
|
|
38
|
+
fail(options, { reason: 'no-base' }, [['yellow', '\n○ Sem base para o diff: passe --base <ref>.\n']]);
|
|
39
|
+
}
|
|
40
|
+
const changes = parseDiff(git(projectPath, 'diff', '-U0', '--no-renames', '--no-color', base, 'HEAD'));
|
|
41
|
+
let uncommitted = [];
|
|
42
|
+
try {
|
|
43
|
+
const dirty = new Set(git(projectPath, 'diff', '--name-only', 'HEAD').split('\n').filter(Boolean));
|
|
44
|
+
uncommitted = [...changes.keys()].filter((f) => dirty.has(f));
|
|
45
|
+
} catch { uncommitted = []; }
|
|
46
|
+
|
|
47
|
+
const tree = buildDiffTree(loaded.graph, changes);
|
|
48
|
+
const drift = declarationDrift(tree, (file) => {
|
|
49
|
+
try { return git(projectPath, 'show', `HEAD:${file}`).split(/\r?\n/); } catch { return []; }
|
|
50
|
+
});
|
|
51
|
+
if (drift.length) {
|
|
52
|
+
fail(options, { reason: 'graph-drift', drift }, [
|
|
53
|
+
['yellow', `\n○ O grafo diz ser deste commit, mas ${drift.length} símbolo(s) não estão na linha que ele aponta (ex.: ${drift[0].label} em ${drift[0].file}:${drift[0].line}).`],
|
|
54
|
+
['gray', ' A extração leu arquivos diferentes do commit. Rode `morph-spec graph build` e repita; ou leia o diff cru (git diff).\n'],
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
const payload = { ok: true, base, head: freshness.headCommit, ...tree, uncommitted };
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { added, modified, entries, files } = tree.counts;
|
|
64
|
+
console.log(chalk.green(`\n✅ ${added} novo(s) +, ${modified} alterado(s) ~, ${entries} entrada(s), em ${files} arquivo(s) — ${String(base).slice(0, 8)}..${String(freshness.headCommit).slice(0, 8)}`));
|
|
65
|
+
for (const line of renderDiffTree(tree)) console.log(` ${line}`);
|
|
66
|
+
if (tree.unmapped.length) {
|
|
67
|
+
console.log(chalk.gray(`\n Sem símbolo tocado (só import/topo do arquivo, config, doc, ou fora do .graphifyignore) — leia no diff:`));
|
|
68
|
+
for (const f of tree.unmapped) console.log(chalk.gray(` ${f}`));
|
|
69
|
+
}
|
|
70
|
+
if (uncommitted.length) {
|
|
71
|
+
console.log(chalk.yellow(`\n ⚠ Mudança não commitada em ${uncommitted.length} arquivo(s) do diff: ela não está nesta árvore. Commit e repita.`));
|
|
72
|
+
}
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
@@ -37,7 +37,7 @@ export async function graphStatusCommand(options = {}) {
|
|
|
37
37
|
console.log(chalk.yellow(`\n○ Sem grafo (${loaded.reason})`));
|
|
38
38
|
console.log(chalk.gray(` ${loaded.path}`));
|
|
39
39
|
console.log(chalk.gray(' Gere com: morph-spec graph build'));
|
|
40
|
-
console.log(chalk.gray(' Sem grafo,
|
|
40
|
+
console.log(chalk.gray(' Sem grafo, as skills seguem por grep normalmente.\n'));
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -111,11 +111,9 @@ function printMetricsBody(m) {
|
|
|
111
111
|
const tov = m.testOutsideVerify;
|
|
112
112
|
const timed = (t) => `${String(t.runs).padStart(4)}× ${fmtDuration(t.measuredMs / 1000).padStart(8)}`
|
|
113
113
|
+ (t.unmeasured ? chalk.gray(` (${t.unmeasured} sem duração)`) : '');
|
|
114
|
-
console.log('\n' + chalk.white.bold('Teste fora do verify
|
|
114
|
+
console.log('\n' + chalk.white.bold('Teste fora do verify')
|
|
115
115
|
+ chalk.gray(` · de ${tov.total.runs} testes+verifies, ${fmtDuration(tov.total.measuredMs / 1000)} cronometrados`
|
|
116
116
|
+ (tov.total.unmeasured ? `, ${tov.total.unmeasured} sem duração` : '')));
|
|
117
|
-
console.log(` sub-agente rodando verify cheio ${timed(tov.subagentVerify)}`);
|
|
118
|
-
console.log(` implementador, 3ª rodada em diante ${timed(tov.implementerRound3Plus)}`);
|
|
119
117
|
console.log(` agente de review rodando teste ${timed(tov.reviewTests)}`);
|
|
120
118
|
console.log(chalk.gray(` ${tov.reviewTests.byPersona.runs}× pela persona morph-* (o que o hook test-budget reconhece);`
|
|
121
119
|
+ ' o resto veio de um nome de dispatch (ver-, rev-, gate3-…)'));
|
|
@@ -163,7 +161,7 @@ function printMetricsBody(m) {
|
|
|
163
161
|
return;
|
|
164
162
|
}
|
|
165
163
|
for (const [gate, g] of gates) {
|
|
166
|
-
console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas → ${g.approvedAfterPause} aprovadas como estavam, `
|
|
164
|
+
console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas (${g.pausedForHuman} para o humano) → ${g.approvedAfterPause} aprovadas como estavam, `
|
|
167
165
|
+ `${g.reworkedAfterPause} retrabalhadas${g.unapproved ? `, ${g.unapproved} desaprovações` : ''}`);
|
|
168
166
|
for (const [signal, s] of Object.entries(g.bySignal)) {
|
|
169
167
|
console.log(chalk.gray(` ${signal}: ${s.paused} pausas, ${s.approvedAfterPause} aprovadas como estavam, ${s.reworkedAfterPause} retrabalhadas`));
|
|
@@ -35,7 +35,7 @@ export function telemetryCommand(options = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
section('Skills', summary.skills, s => `${chalk.green(s.name.padEnd(24))} ${s.count}`);
|
|
38
|
-
section('
|
|
38
|
+
section('Sub-agents despachados', summary.personas, p => `${chalk.magenta(p.name.padEnd(24))} ${p.count}`);
|
|
39
39
|
section('Base de conhecimento (standards lidos explicitamente)', summary.standards,
|
|
40
40
|
s => `${chalk.yellow(String(s.count).padStart(3))}× ${s.path}`);
|
|
41
41
|
if (summary.standards.length || summary.tools.some(t => t.name === 'Read')) {
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
getInstalledCLIVersion
|
|
17
17
|
} from '../../utils/version-checker.js';
|
|
18
18
|
import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
|
|
19
|
-
import { checkMissingAgents, checkAgentsDrift, maxTurnsError
|
|
19
|
+
import { checkMissingAgents, checkAgentsDrift, maxTurnsError } from '../../utils/agents-installer.js';
|
|
20
20
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
21
|
+
import { stackRuleGaps } from '../../lib/installers/stack-sync.js';
|
|
21
22
|
import { auditGraph } from '../../lib/graph/graph-health.js';
|
|
22
23
|
import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
|
|
23
24
|
import { validateTestNodes, morphConfigPath, discoverFrontendPackages } from '../../lib/tasks/test-nodes.js';
|
|
@@ -114,27 +115,27 @@ export function validateAgentsJson(agentsData) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const version = agentsData.version || 'unknown';
|
|
117
|
-
const
|
|
118
|
+
const isCurrent = version.startsWith('6.');
|
|
118
119
|
const v4Fields = [];
|
|
119
|
-
const
|
|
120
|
+
const missingFields = [];
|
|
120
121
|
|
|
121
122
|
for (const [id, data] of agentEntries) {
|
|
122
123
|
if (data.tier !== undefined) v4Fields.push(`${id}.tier`);
|
|
123
124
|
if (data.active_phases !== undefined) v4Fields.push(`${id}.active_phases`);
|
|
124
125
|
if (data.reports_to !== undefined) v4Fields.push(`${id}.reports_to`);
|
|
125
126
|
if (data.teammate !== undefined) v4Fields.push(`${id}.teammate`);
|
|
126
|
-
if (!data.title)
|
|
127
|
-
if (!Array.isArray(data.domains))
|
|
128
|
-
if (typeof data.persona !== 'string' || !data.persona)
|
|
127
|
+
if (!data.title) missingFields.push(`${id}.title`);
|
|
128
|
+
if (!Array.isArray(data.domains)) missingFields.push(`${id}.domains`);
|
|
129
|
+
if (typeof data.persona !== 'string' || !data.persona) missingFields.push(`${id}.persona`);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
if (
|
|
132
|
+
if (isCurrent) {
|
|
132
133
|
checks.push({ name: `agents.json schema (v${version})`, status: 'ok' });
|
|
133
134
|
} else {
|
|
134
135
|
checks.push({
|
|
135
136
|
name: `agents.json schema (v${version})`,
|
|
136
137
|
status: 'warn',
|
|
137
|
-
msg:
|
|
138
|
+
msg: "expected v6.x — run 'morph-spec update'"
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -155,26 +156,12 @@ export function validateAgentsJson(agentsData) {
|
|
|
155
156
|
? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
|
|
156
157
|
: { name: 'agents.json maxTurns', status: 'ok' });
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
.filter(Boolean);
|
|
161
|
-
checks.push(surfacesErrors.length > 0
|
|
162
|
-
? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
|
|
163
|
-
: { name: 'agents.json surfaces', status: 'ok' });
|
|
164
|
-
|
|
165
|
-
const effortErrors = agentEntries
|
|
166
|
-
.map(([id, data]) => effortError(id, data?.effort))
|
|
167
|
-
.filter(Boolean);
|
|
168
|
-
checks.push(effortErrors.length > 0
|
|
169
|
-
? { name: 'agents.json effort', status: 'error', msg: effortErrors.join(' | ') }
|
|
170
|
-
: { name: 'agents.json effort', status: 'ok' });
|
|
171
|
-
|
|
172
|
-
if (missingV5Fields.length > 0 && isV5) {
|
|
173
|
-
const sample = missingV5Fields.slice(0, 3).join(', ');
|
|
159
|
+
if (missingFields.length > 0 && isCurrent) {
|
|
160
|
+
const sample = missingFields.slice(0, 3).join(', ');
|
|
174
161
|
checks.push({
|
|
175
|
-
name: 'agents.json
|
|
162
|
+
name: 'agents.json required fields',
|
|
176
163
|
status: 'warn',
|
|
177
|
-
msg: `${
|
|
164
|
+
msg: `${missingFields.length} missing: ${sample}${missingFields.length > 3 ? '...' : ''}`
|
|
178
165
|
});
|
|
179
166
|
}
|
|
180
167
|
|
|
@@ -194,7 +181,6 @@ const REQUIRED_AGENTS = [
|
|
|
194
181
|
'verifier',
|
|
195
182
|
'reviewer',
|
|
196
183
|
'security',
|
|
197
|
-
'scout'
|
|
198
184
|
];
|
|
199
185
|
|
|
200
186
|
async function doctorFullCommand(frameworkRoot) {
|
|
@@ -589,15 +575,6 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
589
575
|
}
|
|
590
576
|
}
|
|
591
577
|
|
|
592
|
-
const { reapAgentWorktrees } = await import('../../lib/worktree/agent-reaper.js');
|
|
593
|
-
const reap = await reapAgentWorktrees(targetPath);
|
|
594
|
-
if (reap.ok) {
|
|
595
|
-
const gone = reap.reaped.map((r) => r.path ?? r.branch);
|
|
596
|
-
prune.agentReap = { reaped: gone, branchesDeleted: reap.branchesDeleted, preserved: reap.preserved, ...(reap.busy ? { busy: true } : {}) };
|
|
597
|
-
if (!options.json && gone.length) logger.success(`sobras de sub-agent removidas: ${gone.join(', ')}`);
|
|
598
|
-
if (!options.json && reap.busy) logger.warn('reaper de sub-agent já em execução em outro processo; nada foi feito nesta rodada');
|
|
599
|
-
}
|
|
600
|
-
|
|
601
578
|
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
602
579
|
const { applyEditorSettings } = await import('../../lib/worktree/vscode-settings.js');
|
|
603
580
|
const editor = applyEditorSettings(resolvePrimaryRoot(targetPath));
|
|
@@ -660,9 +637,6 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
660
637
|
if (problems.some(prunable) && !options.prune) {
|
|
661
638
|
logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos e os diretórios vazios.');
|
|
662
639
|
}
|
|
663
|
-
if (issues.some((i) => i.type === 'agent-worktree-leftover' && i.reapable) && !options.prune) {
|
|
664
|
-
logger.dim('\n Rode `morph-spec worktree reap` (ou `doctor --worktrees --prune`) para remover as sobras de sub-agent que já pararam.');
|
|
665
|
-
}
|
|
666
640
|
if (!problems.length) {
|
|
667
641
|
logger.success('Nenhuma inconsistência — os itens acima são informativos.');
|
|
668
642
|
return true;
|
|
@@ -1058,6 +1032,16 @@ export async function doctorCommand(options = {}) {
|
|
|
1058
1032
|
hasWarnings = true;
|
|
1059
1033
|
}
|
|
1060
1034
|
|
|
1035
|
+
const gaps = stackRuleGaps(targetPath);
|
|
1036
|
+
if (gaps?.stackMissing) {
|
|
1037
|
+
checks.push({ name: 'project.stack', status: 'warn', msg: 'ausente/unknown no config.json — nenhuma regra de stack instalada; morph-spec stack-set <stack> (a do Morph: standard architecture-stack-baseline), antes da sessão que implementa' });
|
|
1038
|
+
hasWarnings = true;
|
|
1039
|
+
}
|
|
1040
|
+
if (gaps?.missing.length) {
|
|
1041
|
+
checks.push({ name: 'stack rules', status: 'warn', msg: `faltando para [${gaps.tags.join(', ')}]: ${gaps.missing.join(', ')} — rode morph-spec verify ou update` });
|
|
1042
|
+
hasWarnings = true;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1061
1045
|
const agentsPathClaude = join(claudePath, 'agents');
|
|
1062
1046
|
if (await pathExists(agentsPathClaude)) {
|
|
1063
1047
|
const agentFiles = await fs.readdir(agentsPathClaude);
|
|
@@ -32,17 +32,16 @@ export function fleetCommand(options = {}) {
|
|
|
32
32
|
|
|
33
33
|
console.log(chalk.white.bold(
|
|
34
34
|
// PHASE at 18 fits `aguardando-merge`; narrower misaligns every column after it.
|
|
35
|
-
col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 18) + col('
|
|
35
|
+
col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 18) + col('CENÁRIOS', 10)
|
|
36
36
|
+ col('GATES PEND.', 20) + col('WHERE', 24) + col('REMOTO', 16) + col('CLAIM', 16)
|
|
37
|
-
+ col('PORTS', 12) +
|
|
37
|
+
+ col('PORTS', 12) + 'ATIVIDADE',
|
|
38
38
|
));
|
|
39
39
|
for (const e of fleet.entries) {
|
|
40
|
-
const
|
|
40
|
+
const scenarios = e.scenarios ? `${e.scenarios.done}/${e.scenarios.total}` : '-';
|
|
41
41
|
const gates = e.pendingGates.length ? e.pendingGates.join(',') : '-';
|
|
42
42
|
// pr-pending exists only on its branch, never in a worktree or the root.
|
|
43
43
|
const where = (e.location === 'worktree' || e.location === 'pr-pending') ? e.branch : 'raiz';
|
|
44
44
|
const ports = e.env ? `${e.env.range[0]}-${e.env.range[1]}` : '-';
|
|
45
|
-
const handoff = e.handoff?.exists ? 'sim' : '-';
|
|
46
45
|
const activity = [
|
|
47
46
|
e.lastCommit ? `commit ${e.lastCommit}` : null,
|
|
48
47
|
Number.isInteger(e.dirty) && e.dirty > 0 ? chalk.yellow(`${e.dirty} dirty`) : null,
|
|
@@ -51,10 +50,10 @@ export function fleetCommand(options = {}) {
|
|
|
51
50
|
// length and append the padding by hand.
|
|
52
51
|
const claimPlain = e.claim ? `${e.claim.live ? 'sessão' : 'morta'} (${claimAge(e.claim)})` : '-';
|
|
53
52
|
console.log(
|
|
54
|
-
col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 18) + col(
|
|
53
|
+
col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 18) + col(scenarios, 10)
|
|
55
54
|
+ col(gates, 20) + col(where, 24) + col(remoteCell(e.remote), 16)
|
|
56
55
|
+ claimCell(e.claim) + ' '.repeat(Math.max(1, 16 - claimPlain.length))
|
|
57
|
-
+ col(ports, 12) +
|
|
56
|
+
+ col(ports, 12) + activity,
|
|
58
57
|
);
|
|
59
58
|
}
|
|
60
59
|
const claimed = fleet.entries.filter((e) => e.claim?.live);
|