@polymorphism-tech/morph-spec 9.1.0 → 9.3.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 +14 -78
- package/README.md +2 -2
- package/bin/morph-spec.js +14 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +7 -10
- package/framework/agents.json +15 -12
- package/framework/commands/morph-status.md +2 -2
- package/framework/docs/CLI.md +24 -16
- package/framework/evals/review-checklist.md +15 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/state-sync.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +23 -0
- package/framework/hooks/claude-code/statusline.py +3 -2
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/gate-event.js +18 -0
- package/framework/hooks/shared/next-step.js +107 -0
- package/framework/hooks/shared/slice-progress.js +18 -5
- package/framework/hooks/shared/state-writer.js +6 -1
- package/framework/mcp/registry.json +31 -1
- package/framework/rules/nextjs-standards.md +1 -1
- package/framework/schemas/feature.schema.json +1 -0
- 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 +6 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +8 -6
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +2 -1
- package/framework/standards/STANDARDS.json +2 -2
- package/framework/standards/STANDARDS.meta.json +1 -1
- package/framework/standards/ai-agents/setup.md +2 -0
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/integration/mcp/mcp-tools.md +3 -1
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/observability/cost.js +12 -0
- package/src/commands/project/status.js +6 -68
- package/src/commands/state/advance.js +1 -1
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/gate-check.js +2 -2
- package/src/commands/state/gate-decision.js +4 -1
- package/src/commands/state/next.js +24 -0
- package/src/commands/state/report-check.js +3 -1
- package/src/commands/validation/mutate.js +9 -6
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +4 -2
- package/src/commands/validation/verify.js +21 -16
- package/src/core/state/state-manager.js +4 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
- package/src/dashboard/scanner/features-scanner.js +5 -2
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ci/workflow-scaffold.js +12 -0
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/generators/handoff-generator.js +11 -25
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/next-step.js +10 -0
- package/src/lib/plan/checks.js +29 -2
- package/src/lib/plan/plan-parser.js +6 -2
- package/src/lib/review-reports.js +6 -3
- package/src/lib/risk-detector.js +20 -7
- package/src/lib/stack/stack-scan.js +13 -1
- package/src/lib/telemetry/metrics.js +46 -0
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/validation-runner.js +6 -38
- package/src/lib/validators/verify-runner.js +37 -22
- package/src/utils/agents-installer.js +24 -1
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
3
|
+
import { nextStepFor } from '../../lib/next-step.js';
|
|
4
|
+
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
5
|
+
|
|
6
|
+
// Read-only: decides in the tree that owns the feature, since the root copy of a worktree feature is a birth skeleton.
|
|
7
|
+
export function nextCommand(featureName, options = {}) {
|
|
8
|
+
const owner = resolveFeatureTree(featureName, process.cwd());
|
|
9
|
+
const next = owner.source === 'none' || owner.source === 'branch-only' ? null : nextStepFor(featureName, owner.tree);
|
|
10
|
+
|
|
11
|
+
if (!next) {
|
|
12
|
+
const error = owner.source === 'branch-only' ? 'feature-not-in-tree' : 'feature-not-found';
|
|
13
|
+
if (options.json) console.log(JSON.stringify({ error, feature: featureName, branch: owner.branch ?? null }));
|
|
14
|
+
else console.error(chalk.red(`\n❌ ${error}: ${featureName}${owner.branch ? ` (morph-spec worktree setup ${featureName})` : ''}\n`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify({ ...next, feature: featureName, tree: owner.tree }));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(formatNext(next));
|
|
23
|
+
if (owner.tree !== process.cwd()) console.log(chalk.gray(` árvore dona: ${owner.tree}`));
|
|
24
|
+
}
|
|
@@ -130,6 +130,7 @@ function decide(report, observed, stale) {
|
|
|
130
130
|
}
|
|
131
131
|
// PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
|
|
132
132
|
// `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
|
|
133
|
+
if (report.exists && report.blocked) return { verdict: 'report-blocked', action: 'ask-human' };
|
|
133
134
|
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
134
135
|
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
135
136
|
if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
|
|
@@ -151,7 +152,8 @@ const ACTION_HINT = {
|
|
|
151
152
|
'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
|
|
152
153
|
'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
|
|
153
154
|
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
|
|
154
|
-
'
|
|
155
|
+
'ask-human': 'relatório BLOQUEADO: o sub-agent parou numa decisão que não é dele (requisito que contradiz o plano ou um ADR, escolha do humano) e o relatório traz a pergunta e as opções. NÃO re-despache — a mesma pergunta volta. Se o plano ou um ADR já decide, decida você e registre no relatório; senão, faça a pergunta por `AskUserQuestion`. Depois retome o MESMO agente por `SendMessage` com a resposta.',
|
|
156
|
+
'accept-degraded':'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
|
|
155
157
|
redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
156
158
|
};
|
|
157
159
|
|
|
@@ -15,7 +15,7 @@ import { groupByNode, loadPlan } from '../../lib/mutation/plan.js';
|
|
|
15
15
|
import { dotnetSubset, jsSubset, mutatedTypesOf, testFilesCiting, testProjectDirsOf } from '../../lib/mutation/subset.js';
|
|
16
16
|
import { isFinished } from '../../dashboard/scanner/feature-status.js';
|
|
17
17
|
import { dotnetAdapter, genericAdapter, spawnExec } from '../../lib/mutation/adapters.js';
|
|
18
|
-
import { runMutation } from '../../lib/mutation/runner.js';
|
|
18
|
+
import { noSubsetReason, runMutation } from '../../lib/mutation/runner.js';
|
|
19
19
|
import { renderSection, summarize } from '../../lib/mutation/report.js';
|
|
20
20
|
import { readJournal, restoreSnapshot } from '../../lib/mutation/snapshot.js';
|
|
21
21
|
|
|
@@ -101,8 +101,7 @@ function testDirsOf(node, root, { projectsOnly = false } = {}) {
|
|
|
101
101
|
return testProjectDirsOf(root, projectsOnly ? targets.filter((t) => !/\.slnx?$/i.test(t)) : targets, cwd);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function subsetFor({ node, group, root
|
|
105
|
-
if (options.subset === false) return { usable: false, reason: '--no-subset' };
|
|
104
|
+
function subsetFor({ node, group, root }) {
|
|
106
105
|
const mutatedFiles = group.regions.map((r) => r.file);
|
|
107
106
|
if (node.runner === 'dotnet') {
|
|
108
107
|
const base = detectDefaultBranch(root);
|
|
@@ -193,6 +192,12 @@ export async function mutateCommand(featureArg, options = {}) {
|
|
|
193
192
|
return fail(`${named}\nNó e2e: true declarado: ${e2eIds}. ${e2eManualProtocol(feature, uncited[0].e2e[0])}`, EXIT.usage);
|
|
194
193
|
}
|
|
195
194
|
|
|
195
|
+
const subsets = groups.map((group) => subsetFor({ node: group.node, group, root }));
|
|
196
|
+
const noSubset = subsets.findIndex((s) => !s.usable);
|
|
197
|
+
if (noSubset >= 0) {
|
|
198
|
+
return fail(`suíte "${groups[noSubset].node.id}": ${noSubsetReason(subsets[noSubset].reason)}.`, EXIT.usage);
|
|
199
|
+
}
|
|
200
|
+
|
|
196
201
|
if (!options.allowDirty) {
|
|
197
202
|
const dirty = gitOut(root, ['status', '--porcelain', '--', ...new Set(regions.map((r) => r.file))]).trim();
|
|
198
203
|
if (dirty) return fail(`há alteração não commitada nos arquivos a mutar:\n${dirty}\nCommite o fix ANTES de mutar (ou use --allow-dirty sabendo que o snapshot é o único caminho de volta).`, EXIT.usage);
|
|
@@ -207,13 +212,11 @@ export async function mutateCommand(featureArg, options = {}) {
|
|
|
207
212
|
const exec = options.exec ?? spawnExec;
|
|
208
213
|
|
|
209
214
|
const merged = { wallMs: 0, mutants: [], timings: [], restore: { files: [], ok: true, treeClean: true }, binary: { proof: 'unavailable', projects: [] }, baseline: {}, method: { schemata: 0, rebuild: 0, subset: {} } };
|
|
210
|
-
const subsets = [];
|
|
211
215
|
try {
|
|
212
216
|
for (const [index, group] of groups.entries()) {
|
|
213
217
|
const workDir = join(base, runId, `g${index}`);
|
|
214
218
|
mkdirSync(workDir, { recursive: true });
|
|
215
|
-
const subset =
|
|
216
|
-
subsets.push(subset);
|
|
219
|
+
const subset = subsets[index];
|
|
217
220
|
const timeoutMs = options.timeoutMs ?? group.node.timeoutMs ?? 900_000;
|
|
218
221
|
const make = group.node.runner === 'dotnet' ? dotnetAdapter : genericAdapter;
|
|
219
222
|
const adapter = make({ node: group.node, root, workDir, exec, subset, timeoutMs });
|
|
@@ -5,7 +5,7 @@ import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
|
5
5
|
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
6
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
-
import { buildChecks, knownPersonas, parallelGroups, planHash, validatePlan } from '../../lib/plan/checks.js';
|
|
8
|
+
import { buildChecks, knownPersonas, parallelGroups, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
9
9
|
import { knownSurfaces } from '../../lib/specs/screen-checklist.js';
|
|
10
10
|
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
11
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
@@ -25,7 +25,9 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
25
25
|
|
|
26
26
|
const text = readFileSync(planPath, 'utf8');
|
|
27
27
|
const plan = parsePlan(text);
|
|
28
|
-
const { errors, warnings } = validatePlan(plan, {
|
|
28
|
+
const { errors, warnings } = validatePlan(plan, {
|
|
29
|
+
personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd), ...planRuleOptions(cwd, featureName),
|
|
30
|
+
});
|
|
29
31
|
warnings.push(...revocationWarnings(cwd, plan));
|
|
30
32
|
const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
|
|
31
33
|
const hash = planHash(text);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { auditTests, renderTestsAudit } from '../../lib/tests-audit/audit.js';
|
|
2
|
+
|
|
3
|
+
export function testsAuditCommand(options = {}) {
|
|
4
|
+
const result = auditTests(process.cwd());
|
|
5
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : renderTestsAudit(result));
|
|
6
|
+
if (!result.ok) process.exit(2);
|
|
7
|
+
}
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
5
|
-
import { knownPersonas, planHash, validatePlan } from '../../lib/plan/checks.js';
|
|
5
|
+
import { knownPersonas, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
6
6
|
|
|
7
7
|
// Matched against section markers (headings and line-start bold labels), never whole-file
|
|
8
8
|
// substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
|
|
@@ -115,7 +115,9 @@ function validateOutput(featurePath, outputFile) {
|
|
|
115
115
|
// `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
|
|
116
116
|
function validatePlanOutput(content, planDir, result) {
|
|
117
117
|
const plan = parsePlan(content);
|
|
118
|
-
const
|
|
118
|
+
const root = path.resolve(planDir, '..', '..', '..', '..');
|
|
119
|
+
const feature = path.basename(path.resolve(planDir, '..'));
|
|
120
|
+
const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(root), ...planRuleOptions(root, feature) });
|
|
119
121
|
result.errors.push(...errors);
|
|
120
122
|
result.warnings.push(...warnings);
|
|
121
123
|
result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
|
|
@@ -81,10 +81,8 @@ function decidePersistence({ cwd, featureName, noPersist }) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function baselineStampBlock(baseline) {
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
if (fell.length === 0) return null;
|
|
87
|
-
return `record-baseline: a contagem caiu em ${fell.map((u) => `${u.id} (${u.from} → ${u.to})`).join(', ')}`;
|
|
84
|
+
if (baseline?.recorded) return null;
|
|
85
|
+
return baseline?.reason ? `record-baseline: ${baseline.reason}` : 'record-baseline';
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
// The single writer of both node maps the schema fixes (`verification.nodes` and the history
|
|
@@ -154,6 +152,17 @@ async function runLocalVerify(featureName, options) {
|
|
|
154
152
|
}
|
|
155
153
|
}
|
|
156
154
|
|
|
155
|
+
function exitWithReusedStamp(featureName, reused, why, options) {
|
|
156
|
+
console.error(chalk.green(`\n↪ ${why} (sha ${reused.sha.slice(0, 8)}, ${reused.ts}): nada mudou fora de .morph/ desde aquela rodada verde.\n`));
|
|
157
|
+
if (options.json) {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
160
|
+
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
161
|
+
}, null, 2));
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
// Exit 2 is "the command cannot mean what you typed" (unknown feature, `--filter` matching
|
|
158
167
|
// nothing, `--record-baseline` without config.json, `--linux` without Docker): never a verdict.
|
|
159
168
|
export async function verifyCommand(featureName, options = {}) {
|
|
@@ -178,6 +187,8 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
178
187
|
|
|
179
188
|
let ci = null;
|
|
180
189
|
if (options.ci) {
|
|
190
|
+
const fresh = reusableLocalStamp(process.cwd(), feature.verification);
|
|
191
|
+
if (fresh) exitWithReusedStamp(featureName, fresh, 'stamp verde e fresco — o CI não roda de novo', options);
|
|
181
192
|
try {
|
|
182
193
|
const outcome = await runCiVerify(process.cwd(), featureName, readMorphConfig(process.cwd()), {
|
|
183
194
|
log: (line) => console.error(chalk.gray(` ${line}`)),
|
|
@@ -189,17 +200,6 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
189
200
|
console.error(chalk.yellow(`\n⏳ ${outcome.reason}. Não é vermelho e não há stamp: não repita em laço — siga e deixe o gate-check decidir.\n`));
|
|
190
201
|
process.exit(3);
|
|
191
202
|
} else {
|
|
192
|
-
const reused = reusableLocalStamp(process.cwd(), feature.verification);
|
|
193
|
-
if (reused) {
|
|
194
|
-
console.error(chalk.green(`\n↪ CI indisponível (${outcome.reason}) — stamp fresco reaproveitado (sha ${reused.sha.slice(0, 8)}, ${reused.ts}): nada mudou fora de .morph/ desde aquela rodada verde.\n`));
|
|
195
|
-
if (options.json) {
|
|
196
|
-
console.log(JSON.stringify({
|
|
197
|
-
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
198
|
-
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
199
|
-
}, null, 2));
|
|
200
|
-
}
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
203
|
console.error(chalk.yellow(`\n↪ CI indisponível (${outcome.reason}) — verify local.\n`));
|
|
204
204
|
}
|
|
205
205
|
} catch (err) {
|
|
@@ -240,6 +240,7 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
240
240
|
status: result.status,
|
|
241
241
|
nodes: nodeStatuses(result),
|
|
242
242
|
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
243
|
+
failStreak: result.status === 'fail' ? (prevEntry?.failStreak ?? 0) + 1 : 0,
|
|
243
244
|
updatedAt: now,
|
|
244
245
|
};
|
|
245
246
|
await updateFeature(featureName, historyKey, compact);
|
|
@@ -277,8 +278,12 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
if (options.json) {
|
|
281
|
+
// Each node's tail is already in `feedback`, prefixed by node: printing it twice doubles what the session ingests.
|
|
282
|
+
const withoutOutput = ({ output: _, ...rest }) => rest;
|
|
283
|
+
const nodes = Object.fromEntries(Object.entries(result.nodes).map(([k, node]) => [k,
|
|
284
|
+
Array.isArray(node.runs) ? { ...withoutOutput(node), runs: node.runs.map(withoutOutput) } : withoutOutput(node)]));
|
|
280
285
|
const output = {
|
|
281
|
-
...result, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup,
|
|
286
|
+
...result, nodes, persisted: persist, ...(persist ? {} : { reason }), repeatOf, cleanup,
|
|
282
287
|
...(ci ? { source: 'ci', ciRun: { id: ci.run.databaseId, url: ci.run.url, sha: ci.run.headSha }, workflowCreated: ci.created } : {}),
|
|
283
288
|
};
|
|
284
289
|
console.log(JSON.stringify(output, null, 2));
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirS
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
4
4
|
import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
|
|
5
|
+
import { recordGateEvent } from '../../../framework/hooks/shared/gate-event.js';
|
|
5
6
|
|
|
6
7
|
const STATE_FILE_NAME = '.morph/state.json';
|
|
7
8
|
const FEATURES_DIR_NAME = '.morph/features';
|
|
@@ -649,6 +650,9 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
|
|
|
649
650
|
|
|
650
651
|
state.features[featureName] = feature;
|
|
651
652
|
saveState(state);
|
|
653
|
+
recordGateEvent(process.cwd(), {
|
|
654
|
+
feature: featureName, gate, step: approved ? 'approve' : 'unapprove', approvedBy: feature.approvalGates[gate].approvedBy,
|
|
655
|
+
});
|
|
652
656
|
}
|
|
653
657
|
|
|
654
658
|
export function getApprovalGate(featureName, gate) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { StrictMode } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
-
import App from './App.jsx';
|
|
5
|
-
import './styles/index.css';
|
|
6
|
-
|
|
7
|
-
createRoot(document.getElementById('root')).render(
|
|
8
|
-
<StrictMode>
|
|
9
|
-
<BrowserRouter>
|
|
10
|
-
<App />
|
|
11
|
-
</BrowserRouter>
|
|
12
|
-
</StrictMode>
|
|
13
|
-
);
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import App from './App.jsx';
|
|
5
|
+
import './styles/index.css';
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</StrictMode>
|
|
13
|
+
);
|
|
@@ -26,11 +26,12 @@ const TABS = [
|
|
|
26
26
|
const SLICE_STATUS = {
|
|
27
27
|
feito: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
|
|
28
28
|
parcial: { label: 'parcial', tone: 'text-[var(--color-phase-implement)]', icon: CircleDashed },
|
|
29
|
+
bloqueado: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
|
|
29
30
|
pendente: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
function isReady(sl, byId) {
|
|
33
|
-
if (sl.status === 'feito') return false;
|
|
34
|
+
if (sl.status === 'feito' || sl.status === 'bloqueado') return false;
|
|
34
35
|
return (sl.depends || []).every((d) => byId[d]?.status === 'feito');
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -67,6 +68,8 @@ export function nextAction(f) {
|
|
|
67
68
|
if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
|
|
68
69
|
if (f.inEvaluation) return 'Os relatórios do Gate 3 chegaram. A decisão sai dos vereditos, dos bloqueadores e dos cenários provados.';
|
|
69
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.`;
|
|
70
73
|
const open = f.slicesList.find((sl) => sl.status === 'parcial') || f.slicesList.find((sl) => sl.status === 'pendente');
|
|
71
74
|
if (open) return `${f.slices.done} de ${f.slices.total} fatias feitas. Próxima: ${open.id}, ${open.title}.`;
|
|
72
75
|
return 'Todas as fatias feitas. Falta o verify da feature e a revisão.';
|
|
@@ -330,6 +333,7 @@ function SliceRow({ sl, f, open, onToggle, ready }) {
|
|
|
330
333
|
const BOARD_COLUMNS = [
|
|
331
334
|
{ id: 'pendente', label: 'Pendentes' },
|
|
332
335
|
{ id: 'parcial', label: 'Parciais' },
|
|
336
|
+
{ id: 'bloqueado', label: 'Bloqueadas' },
|
|
333
337
|
{ id: 'feito', label: 'Feitas' },
|
|
334
338
|
];
|
|
335
339
|
|
|
@@ -389,7 +393,7 @@ function SlicesTab({ f, view = 'list', onView = () => {} }) {
|
|
|
389
393
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
390
394
|
<Segmented label="Visualização das fatias" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
|
|
391
395
|
<p className="text-sm text-[var(--color-dim)]">
|
|
392
|
-
{f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{ready ? `, ${ready} prontas para começar` : ''}
|
|
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` : ''}
|
|
393
397
|
</p>
|
|
394
398
|
</div>
|
|
395
399
|
{view === 'board' ? <SlicesBoard f={f} byId={byId} /> : (
|
|
@@ -123,10 +123,13 @@ export function readSlices(dir) {
|
|
|
123
123
|
covers: Array.isArray(x.covers) ? x.covers : [],
|
|
124
124
|
};
|
|
125
125
|
});
|
|
126
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
counts: { total: progress.total, done: progress.done, partial: progress.partial, blocked: progress.blocked, pending: progress.pending },
|
|
128
|
+
list,
|
|
129
|
+
};
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
const NO_SLICES = { total: 0, done: 0, partial: 0, pending: 0 };
|
|
132
|
+
const NO_SLICES = { total: 0, done: 0, partial: 0, blocked: 0, pending: 0 };
|
|
130
133
|
|
|
131
134
|
function gatesOf(feature) {
|
|
132
135
|
const skipped = new Set(feature.skippedPhases || []);
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import react from '@vitejs/plugin-react';
|
|
3
|
-
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
-
import morphDataPlugin from './vite-plugin-morph-data.js';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { dirname, resolve } from 'path';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = dirname(__filename);
|
|
10
|
-
|
|
11
|
-
const frameworkDir = resolve(__dirname, '..', '..', 'framework');
|
|
12
|
-
const projectDir = process.env.MORPH_PROJECT_DIR || process.cwd();
|
|
13
|
-
|
|
14
|
-
export default defineConfig({
|
|
15
|
-
root: __dirname,
|
|
16
|
-
plugins: [
|
|
17
|
-
react(),
|
|
18
|
-
tailwindcss(),
|
|
19
|
-
morphDataPlugin(frameworkDir, projectDir),
|
|
20
|
-
],
|
|
21
|
-
server: {
|
|
22
|
-
port: 3200,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import morphDataPlugin from './vite-plugin-morph-data.js';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, resolve } from 'path';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const frameworkDir = resolve(__dirname, '..', '..', 'framework');
|
|
12
|
+
const projectDir = process.env.MORPH_PROJECT_DIR || process.cwd();
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
root: __dirname,
|
|
16
|
+
plugins: [
|
|
17
|
+
react(),
|
|
18
|
+
tailwindcss(),
|
|
19
|
+
morphDataPlugin(frameworkDir, projectDir),
|
|
20
|
+
],
|
|
21
|
+
server: {
|
|
22
|
+
port: 3200,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
2
2
|
import { join, posix, relative, resolve } from 'path';
|
|
3
3
|
import { PACKAGE_LOCATIONS } from '../detectors/index.js';
|
|
4
4
|
import { CI_ARTIFACT, CI_RESULT_FILE } from './github-ci.js';
|
|
5
|
+
import { discoverDotnetSolution } from '../tasks/test-runner.js';
|
|
5
6
|
|
|
6
7
|
const LOCKFILES = [
|
|
7
8
|
['package-lock.json', 'npm ci'],
|
|
@@ -50,6 +51,16 @@ function dotnetSetup(root, config) {
|
|
|
50
51
|
];
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// The verify build node runs `dotnet build --no-restore` (right on a warm tree): a fresh checkout restores first,
|
|
55
|
+
// against the same target the build node resolves.
|
|
56
|
+
function dotnetRestore(root, config) {
|
|
57
|
+
if (!usesDotnet(root, config)) return [];
|
|
58
|
+
const backend = config?.project?.backendPath;
|
|
59
|
+
if (typeof backend === 'string') return step('dotnet restore', backend, 'dotnet restore');
|
|
60
|
+
const { target } = discoverDotnetSolution(root);
|
|
61
|
+
return step('dotnet restore', '.', target && target !== '.' ? `dotnet restore "${target}"` : 'dotnet restore');
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
const step = (name, dir, run) => [
|
|
54
65
|
` - name: ${name}`,
|
|
55
66
|
...(dir === '.' ? [] : [` working-directory: ${posix.normalize(dir)}`]),
|
|
@@ -94,6 +105,7 @@ export function renderCiWorkflow(root, config) {
|
|
|
94
105
|
lines.push(...step(`install (${pkg.dir})`, pkg.dir, pkg.install));
|
|
95
106
|
if (pkg.playwright) lines.push(...step(`playwright browsers (${pkg.dir})`, pkg.dir, 'npx playwright install --with-deps chromium'));
|
|
96
107
|
}
|
|
108
|
+
lines.push(...dotnetRestore(root, config));
|
|
97
109
|
lines.push(
|
|
98
110
|
' - name: morph-spec verify',
|
|
99
111
|
' shell: bash',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import { readPlanChecks } from '../plan/checks.js';
|
|
3
|
+
import { changedPathsAgainst, detectDefaultBranch } from '../git/default-branch.js';
|
|
4
|
+
import { resolveComposeFile } from '../../../framework/hooks/shared/e2e-check.js';
|
|
5
|
+
import { E2E_SPEC_LOCATIONS } from '../../../framework/hooks/shared/frontend-locations.js';
|
|
6
|
+
|
|
7
|
+
export const E2E_NOT_APPLICABLE = 'not applicable: no `prova: e2e` scenario and the diff touches no UI, e2e spec or container file';
|
|
8
|
+
|
|
9
|
+
const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
|
|
10
|
+
const RUNTIME_FILE = /(^|\/)(dockerfile[^/]*|\.dockerignore|(docker-)?compose[^/]*\.ya?ml|playwright\.config\.[cm]?[jt]s)$/i;
|
|
11
|
+
const E2E_DIR = /(^|\/)e2e\//i;
|
|
12
|
+
const KNOWN_PACKAGE_DIRS = E2E_SPEC_LOCATIONS.filter((dir) => dir !== '.');
|
|
13
|
+
|
|
14
|
+
function packageDirsOf(config) {
|
|
15
|
+
const declared = [config?.project?.frontendPath, ...(config?.project?.tests ?? []).filter((n) => n?.runner !== 'dotnet').map((n) => n?.cwd)];
|
|
16
|
+
const extra = declared.filter((d) => typeof d === 'string').map((d) => d.replace(/\\/g, '/').replace(/^\.\/|\/$/g, '')).filter((d) => d && d !== '.');
|
|
17
|
+
return [...KNOWN_PACKAGE_DIRS, ...extra];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const touchesRuntime = (file, composeFile, packageDirs) => UI_FILE.test(file)
|
|
21
|
+
|| RUNTIME_FILE.test(file)
|
|
22
|
+
|| E2E_DIR.test(file)
|
|
23
|
+
|| (composeFile && basename(file) === basename(composeFile))
|
|
24
|
+
|| packageDirs.some((dir) => file === dir || file.startsWith(`${dir}/`));
|
|
25
|
+
|
|
26
|
+
/** Unknown (no git, no base, empty diff) is applicable: the e2e only stands down on evidence. */
|
|
27
|
+
export function e2eApplicability(projectPath, featureName, config) {
|
|
28
|
+
const { checks } = readPlanChecks(projectPath, featureName);
|
|
29
|
+
if (checks?.checks?.some((c) => c.proof === 'e2e')) return { applicable: true, reason: 'prova: e2e' };
|
|
30
|
+
const base = detectDefaultBranch(projectPath);
|
|
31
|
+
const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !f.startsWith('.morph/')) : [];
|
|
32
|
+
if (changed.length === 0) return { applicable: true, reason: 'diff unknown' };
|
|
33
|
+
const composeFile = resolveComposeFile(projectPath, config);
|
|
34
|
+
const packageDirs = packageDirsOf(config);
|
|
35
|
+
const hit = changed.find((f) => touchesRuntime(f, composeFile, packageDirs));
|
|
36
|
+
return hit ? { applicable: true, reason: hit } : { applicable: false, reason: E2E_NOT_APPLICABLE };
|
|
37
|
+
}
|
|
@@ -6,6 +6,8 @@ import { join } from 'path';
|
|
|
6
6
|
import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
|
|
7
7
|
import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
|
|
8
8
|
import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
|
|
9
|
+
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
10
|
+
import { nextStepFor } from '../next-step.js';
|
|
9
11
|
|
|
10
12
|
const POINTER_CANDIDATES = [
|
|
11
13
|
['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
|
|
@@ -46,29 +48,13 @@ function readWorktreeEnv(cwd) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
case 'uiux':
|
|
57
|
-
return approved('uiux')
|
|
58
|
-
? `Gate de uiux aprovado — implemente as fatias do plano via /morph-apply ${featureName}.`
|
|
59
|
-
: `Complete \`1-design/design-system.md\` e aprove o gate de uiux (\`morph-spec advance ${featureName}\`).`;
|
|
60
|
-
case 'implement': {
|
|
61
|
-
const ready = readySlices(slices).map((s) => `${s.id} "${s.title}"`).join(', ');
|
|
62
|
-
return ready
|
|
63
|
-
? `Implemente as fatias prontas — ${ready} — via /morph-apply ${featureName} (cada fatia grava 3-implement/reports/{id}.md).`
|
|
64
|
-
: `Todas as fatias têm relatório final — rode \`morph-spec verify ${featureName}\` e siga para a review. Skill: /morph-apply ${featureName}.`;
|
|
65
|
-
}
|
|
66
|
-
case 'review':
|
|
67
|
-
return approved('review')
|
|
68
|
-
? `Gate 3 aprovado — feche: \`morph-spec finish ${featureName} --pr\` (abre PR) ou \`--merge\` (merge local na default). Arquiva + integra num passo.`
|
|
69
|
-
: `Rode /morph-review ${featureName} (verificador ∥ revisor, segurança se exigida) e aprove o Gate 3; depois \`morph-spec finish ${featureName} --pr|--merge\`.`;
|
|
70
|
-
default:
|
|
71
|
-
return `Rode \`morph-spec status ${featureName}\` para orientar o próximo passo.`;
|
|
51
|
+
function nextLine(featureName, cwd) {
|
|
52
|
+
const fallback = `Rode \`morph-spec next ${featureName}\`.`;
|
|
53
|
+
try {
|
|
54
|
+
const next = nextStepFor(featureName, cwd);
|
|
55
|
+
return next ? formatNext(next) : fallback;
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
60
|
|
|
@@ -129,7 +115,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
129
115
|
'',
|
|
130
116
|
`- Gates: ${gateLine}`,
|
|
131
117
|
slices
|
|
132
|
-
? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
|
|
118
|
+
? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''}${slices.blocked ? ` · ${slices.blocked} bloqueada(s)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
|
|
133
119
|
: '- Fatias: sem checks.json ainda (rode plan-check)',
|
|
134
120
|
pendingGates.length ? `- Gates pendentes: ${pendingGates.join(', ')}` : null,
|
|
135
121
|
'',
|
|
@@ -141,7 +127,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
141
127
|
'',
|
|
142
128
|
'## Próxima ação',
|
|
143
129
|
'',
|
|
144
|
-
|
|
130
|
+
nextLine(featureName, cwd),
|
|
145
131
|
'',
|
|
146
132
|
'## Retomar',
|
|
147
133
|
'',
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { dotnetBuildCommands, withFilter, withTrx, withoutBuild } from './commands.js';
|
|
5
|
-
import {
|
|
5
|
+
import { parseTrx } from './trx.js';
|
|
6
6
|
import { stripAnsi } from '../tasks/test-runner.js';
|
|
7
7
|
|
|
8
8
|
const MAX_CAPTURE = 16 * 1024 * 1024;
|
|
@@ -39,14 +39,10 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
|
|
|
39
39
|
const trxDir = join(workDir, 'trx');
|
|
40
40
|
mkdirSync(trxDir, { recursive: true });
|
|
41
41
|
const base = withoutBuild(node.command);
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
scope === 'full' ? base : withFilter(base, filterOf[scope](filter)),
|
|
45
|
-
{ dir: trxDir, name: `${label}.trx` },
|
|
46
|
-
);
|
|
47
|
-
const runTests = async ({ scope, label, env, filter }) => {
|
|
42
|
+
const commandFor = (label) => withTrx(withFilter(base, subset.filter), { dir: trxDir, name: `${label}.trx` });
|
|
43
|
+
const runTests = async ({ label, env }) => {
|
|
48
44
|
const trxPath = join(trxDir, `${label}.trx`);
|
|
49
|
-
const r = await exec(commandFor(
|
|
45
|
+
const r = await exec(commandFor(label), { cwd, env, timeoutMs, label });
|
|
50
46
|
if (!existsSync(trxPath)) {
|
|
51
47
|
return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: r.timedOut ? `timeout de ${timeoutMs} ms` : `sem TRX (exit ${r.status})` };
|
|
52
48
|
}
|
|
@@ -62,12 +58,6 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
|
|
|
62
58
|
buildCommands: dotnetBuildCommands(node, root),
|
|
63
59
|
cwd,
|
|
64
60
|
runTests,
|
|
65
|
-
reproduce: ({ names, label, env }) => runTests({
|
|
66
|
-
scope: 'names',
|
|
67
|
-
label,
|
|
68
|
-
env,
|
|
69
|
-
filter: [...new Set(names.map(methodOf))].map((n) => `FullyQualifiedName~${n}`).join('|'),
|
|
70
|
-
}),
|
|
71
61
|
};
|
|
72
62
|
}
|
|
73
63
|
|
|
@@ -78,9 +68,8 @@ export function genericAdapter({ node, root, exec, subset, timeoutMs }) {
|
|
|
78
68
|
hasSubset: Boolean(subset?.usable),
|
|
79
69
|
buildCommands: [],
|
|
80
70
|
cwd,
|
|
81
|
-
async runTests({
|
|
82
|
-
const
|
|
83
|
-
const r = await exec(command, { cwd, env, timeoutMs, label });
|
|
71
|
+
async runTests({ label, env }) {
|
|
72
|
+
const r = await exec(subset.command, { cwd, env, timeoutMs, label });
|
|
84
73
|
if (r.timedOut) return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: `timeout de ${timeoutMs} ms` };
|
|
85
74
|
if (r.status === 0) return { outcome: 'green', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs };
|
|
86
75
|
const failedTests = genericFailures(`${r.stdout}\n${r.stderr}`);
|