@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,5 +1,4 @@
|
|
|
1
|
-
import { join,
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
1
|
+
import { join, resolve } from 'path';
|
|
3
2
|
import { homedir } from 'os';
|
|
4
3
|
import fs from 'fs-extra';
|
|
5
4
|
import ora from 'ora';
|
|
@@ -28,13 +27,43 @@ import {
|
|
|
28
27
|
setupStitch,
|
|
29
28
|
loadMcpRegistry
|
|
30
29
|
} from '../../lib/installers/mcp-installer.js';
|
|
31
|
-
import { setupInfra
|
|
32
|
-
import { installSkills } from '../../utils/skills-installer.js';
|
|
33
|
-
import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
|
|
30
|
+
import { setupInfra } from '../../scripts/setup-infra.js';
|
|
34
31
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
35
32
|
import { updateCommand } from './update.js';
|
|
36
33
|
import { resolvePrimaryRoot } from './worktree.js';
|
|
37
34
|
import { registerProject } from '../../lib/projects/registry.js';
|
|
35
|
+
import { setProjectStack } from '../../lib/installers/stack-sync.js';
|
|
36
|
+
import { installStackAssets } from '../../lib/installers/stack-assets.js';
|
|
37
|
+
|
|
38
|
+
const GREENFIELD_CHOICES = [
|
|
39
|
+
{ name: 'dotnet+nextjs — stack do Morph: .NET 10 + EF Core/Neon + Next.js/Shadcn (recomendada)', value: { stack: 'dotnet+nextjs', integrations: ['neon'] } },
|
|
40
|
+
{ name: 'dotnet — só a API .NET (EF Core/Neon)', value: { stack: 'dotnet', integrations: ['neon'] } },
|
|
41
|
+
{ name: 'nextjs — só o app web Next.js', value: { stack: 'nextjs', integrations: [] } },
|
|
42
|
+
{ name: 'Decidir depois (o /morph-plan pergunta)', value: null },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Empty project: `--stack` wins; a terminal is asked, the Morph stack first; no terminal leaves it to the plan. */
|
|
46
|
+
export async function chooseGreenfieldStack(options = {}, { isTTY = process.stdin.isTTY, prompt = inquirer.prompt } = {}) {
|
|
47
|
+
if (options.stack) {
|
|
48
|
+
const integrations = String(options.with ?? '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
49
|
+
return { stack: options.stack, integrations };
|
|
50
|
+
}
|
|
51
|
+
if (!isTTY) return null;
|
|
52
|
+
const { stackChoice } = await prompt([{
|
|
53
|
+
type: 'list',
|
|
54
|
+
name: 'stackChoice',
|
|
55
|
+
message: 'Projeto novo: qual stack?',
|
|
56
|
+
choices: GREENFIELD_CHOICES,
|
|
57
|
+
}]);
|
|
58
|
+
return stackChoice;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function reinstallForStack(targetPath, stackTags, spinner) {
|
|
62
|
+
if (stackTags.length === 0) return;
|
|
63
|
+
spinner.start('Re-installing stack-filtered assets...');
|
|
64
|
+
await installStackAssets(targetPath, stackTags);
|
|
65
|
+
spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
|
|
66
|
+
}
|
|
38
67
|
|
|
39
68
|
/**
|
|
40
69
|
* A linked worktree junctions `.claude/` and `.morph/framework/` but not `.morph/config`/state:
|
|
@@ -196,15 +225,17 @@ export async function initCommand(options) {
|
|
|
196
225
|
if (structure?.uiLibrary) logger.dim(`UI Library: ${structure.uiLibrary}`);
|
|
197
226
|
if (structure?.integrations?.length > 0) logger.dim(`Integrations: ${structure.integrations.join(', ')}`);
|
|
198
227
|
|
|
199
|
-
const { standardsChoice } =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
228
|
+
const { standardsChoice } = process.stdin.isTTY
|
|
229
|
+
? await inquirer.prompt([{
|
|
230
|
+
type: 'list',
|
|
231
|
+
name: 'standardsChoice',
|
|
232
|
+
message: 'How should morph-spec handle your project standards?',
|
|
233
|
+
choices: [
|
|
234
|
+
{ name: 'Keep morph-spec defaults (recommended)', value: 'morph-spec' },
|
|
235
|
+
{ name: 'Detect project-specific standards', value: 'detect' }
|
|
236
|
+
]
|
|
237
|
+
}])
|
|
238
|
+
: { standardsChoice: 'morph-spec' };
|
|
208
239
|
|
|
209
240
|
if (standardsChoice === 'detect') {
|
|
210
241
|
spinner.start('Generating project-specific standards...');
|
|
@@ -215,30 +246,27 @@ export async function initCommand(options) {
|
|
|
215
246
|
logger.dim('Review and edit .morph/context/standards.md as needed.');
|
|
216
247
|
}
|
|
217
248
|
|
|
218
|
-
|
|
219
|
-
if (stackTags.length > 0) {
|
|
220
|
-
spinner.start('Re-installing stack-filtered assets...');
|
|
221
|
-
const frameworkDir = join(targetPath, '.morph', 'framework');
|
|
222
|
-
const fwSrcDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'framework');
|
|
223
|
-
|
|
224
|
-
const rulesSrc = join(fwSrcDir, 'rules');
|
|
225
|
-
const rulesDest = join(targetPath, '.claude', 'rules');
|
|
226
|
-
installRulesForStack(rulesSrc, rulesDest, stackTags);
|
|
227
|
-
installCommandsForStack(join(fwSrcDir, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
|
|
228
|
-
|
|
229
|
-
await installSkills(targetPath, { projectTags: stackTags });
|
|
230
|
-
|
|
231
|
-
await installAgents(targetPath, fwSrcDir, { projectTags: stackTags });
|
|
232
|
-
await installDomainAgents(targetPath, fwSrcDir, { projectTags: stackTags });
|
|
233
|
-
|
|
234
|
-
spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
|
|
235
|
-
}
|
|
236
|
-
|
|
249
|
+
await reinstallForStack(targetPath, getProjectTags(config), spinner);
|
|
237
250
|
spinner.start('Continuing...');
|
|
238
251
|
}
|
|
239
252
|
} catch (error) {
|
|
240
253
|
logger.dim(` Stack detection skipped: ${error.message}`);
|
|
241
254
|
}
|
|
255
|
+
} else {
|
|
256
|
+
const choice = await chooseGreenfieldStack(options);
|
|
257
|
+
if (choice) {
|
|
258
|
+
spinner.stop();
|
|
259
|
+
const result = setProjectStack(targetPath, choice.stack, { integrations: choice.integrations, log: (line) => logger.dim(line) });
|
|
260
|
+
if (result.error) {
|
|
261
|
+
logger.warn(`Stack não declarada: ${result.error}`);
|
|
262
|
+
} else {
|
|
263
|
+
config = await readJson(configPath);
|
|
264
|
+
await reinstallForStack(targetPath, getProjectTags(config), spinner);
|
|
265
|
+
}
|
|
266
|
+
spinner.start('Continuing...');
|
|
267
|
+
} else {
|
|
268
|
+
logger.dim(' Stack: não declarada — o /morph-plan pergunta antes de planejar (recomendada: a do Morph)');
|
|
269
|
+
}
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
let mcpSetupResult = null;
|
|
@@ -460,13 +488,12 @@ export async function initCommand(options) {
|
|
|
460
488
|
|
|
461
489
|
const agents = infraResult?.agents;
|
|
462
490
|
if (agents) {
|
|
463
|
-
|
|
464
|
-
logger.dim(` ✓ .claude/agents/ (${total} domain personas installed)`);
|
|
491
|
+
logger.dim(` ✓ .claude/agents/ (${agents.installed ?? 0} Gate 3 agents installed)`);
|
|
465
492
|
} else {
|
|
466
493
|
logger.dim(' ✓ .claude/agents/ (native subagents installed)');
|
|
467
494
|
}
|
|
468
495
|
|
|
469
|
-
logger.dim(' ✓ ~/.claude/statusline.
|
|
496
|
+
logger.dim(' ✓ ~/.claude/statusline.js (global statusline installed)');
|
|
470
497
|
|
|
471
498
|
logger.blank();
|
|
472
499
|
const globalSettingsPath = join(homedir(), '.claude', 'settings.local.json');
|
|
@@ -1,56 +1,17 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { getFeatureDir } from '../../core/paths/output-schema.js';
|
|
5
1
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
6
|
-
import {
|
|
7
|
-
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
2
|
+
import { scanStack, renderStackScan } from '../../lib/stack/stack-scan.js';
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Scan the tree at `cwd`; with a feature, write `2-plan/stack-scan.md`. */
|
|
4
|
+
/** Scan the tree at `cwd`; prints, never writes — the plan reads stdout. */
|
|
12
5
|
export function runStackScan(featureName, cwd = process.cwd()) {
|
|
13
6
|
const result = scanStack(cwd, readMorphConfig(cwd));
|
|
14
|
-
|
|
15
|
-
if (!featureName) return { ...result, markdown, written: null, exit: 0 };
|
|
16
|
-
|
|
17
|
-
const featureDir = join(cwd, getFeatureDir(featureName));
|
|
18
|
-
if (!existsSync(featureDir)) return { ...result, markdown, written: null, exit: 2, error: 'feature-not-found' };
|
|
19
|
-
const planDir = join(featureDir, '2-plan');
|
|
20
|
-
mkdirSync(planDir, { recursive: true });
|
|
21
|
-
const written = join(planDir, STACK_SCAN_FILE);
|
|
22
|
-
writeFileSync(written, markdown, 'utf8');
|
|
23
|
-
return { ...result, markdown, written, exit: 0 };
|
|
7
|
+
return { ...result, markdown: renderStackScan(result, featureName ?? 'projeto'), exit: 0 };
|
|
24
8
|
}
|
|
25
9
|
|
|
26
10
|
export function stackScanCommand(featureName, options = {}) {
|
|
27
|
-
if (featureName) {
|
|
28
|
-
const owner = assertOwnerTree(featureName, process.cwd());
|
|
29
|
-
if (owner.blocked) {
|
|
30
|
-
for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec stack-scan ${featureName}`)) {
|
|
31
|
-
console.error(chalk[tone](text));
|
|
32
|
-
}
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
11
|
const result = runStackScan(featureName);
|
|
38
12
|
if (options.json) {
|
|
39
13
|
const { markdown, ...rest } = result;
|
|
40
14
|
console.log(JSON.stringify(rest, null, 2));
|
|
41
|
-
process.exit(result.exit);
|
|
42
|
-
}
|
|
43
|
-
if (result.error === 'feature-not-found') {
|
|
44
|
-
console.error(chalk.red(`stack-scan: a feature "${featureName}" não existe — rode morph-spec create ${featureName} antes`));
|
|
45
|
-
process.exit(2);
|
|
46
|
-
}
|
|
47
|
-
if (result.written) {
|
|
48
|
-
const high = result.deviations.filter((d) => d.severity === 'Bloqueante' || d.severity === 'Alto').length;
|
|
49
|
-
console.log(chalk.green(`✓ stack-scan: ${join(getFeatureDir(featureName), '2-plan', STACK_SCAN_FILE)} gravado`));
|
|
50
|
-
const summary = result.greenfield ? 'greenfield'
|
|
51
|
-
: !result.audited ? notAuditedReason(result)
|
|
52
|
-
: `${result.deviations.length} desvio(s), ${high} Bloqueante/Alto`;
|
|
53
|
-
console.log(chalk.gray(` ${summary}`));
|
|
54
15
|
} else {
|
|
55
16
|
process.stdout.write(result.markdown);
|
|
56
17
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { setProjectStack } from '../../lib/installers/stack-sync.js';
|
|
3
|
+
import { installStackAssets } from '../../lib/installers/stack-assets.js';
|
|
4
|
+
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
5
|
+
|
|
6
|
+
export async function stackSetCommand(stack, options = {}) {
|
|
7
|
+
const integrations = String(options.with ?? '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
8
|
+
const result = setProjectStack(process.cwd(), stack, {
|
|
9
|
+
integrations,
|
|
10
|
+
log: options.json ? () => {} : (line) => console.log(chalk.gray(line)),
|
|
11
|
+
});
|
|
12
|
+
if (result.error) {
|
|
13
|
+
if (options.json) console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
14
|
+
else console.error(chalk.red(`✗ ${result.error}`));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const { stack: declared, integrations: declaredIntegrations, sync } = result;
|
|
18
|
+
await installStackAssets(process.cwd(), getProjectTags({ project: { stack: declared, integrations: declaredIntegrations } }));
|
|
19
|
+
if (options.json) {
|
|
20
|
+
console.log(JSON.stringify({ stack: declared, integrations: declaredIntegrations, rulesAdded: sync.added, rulesUpdated: sync.updated }, null, 2));
|
|
21
|
+
} else {
|
|
22
|
+
console.log(chalk.green(`✓ stack ${declared}`) + chalk.gray(` — rules, commands e skills da stack instalados (${sync.added.length + sync.updated.length} rule(s) de domínio nova(s))`));
|
|
23
|
+
console.log(chalk.yellow(' As rules carregam no início de uma sessão do Claude Code: implemente numa sessão nova.'));
|
|
24
|
+
}
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadState, getFeature, derivePhase, deriveOutputs } from '../../core/state/state-manager.js';
|
|
3
|
-
import {
|
|
3
|
+
import { implementProgress } from '../../../framework/hooks/shared/report-progress.js';
|
|
4
4
|
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
5
5
|
import { nextStepFor } from '../../lib/next-step.js';
|
|
6
6
|
|
|
@@ -20,8 +20,8 @@ const PHASE_ORDER = [
|
|
|
20
20
|
|
|
21
21
|
const PHASE_OUTPUTS = {
|
|
22
22
|
plan: ['plan', 'checks'],
|
|
23
|
-
uiux: ['
|
|
24
|
-
implement: [],
|
|
23
|
+
uiux: ['uiMockups'],
|
|
24
|
+
implement: ['implementReport'],
|
|
25
25
|
review: ['verifierReport', 'reviewerReport']
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -118,7 +118,7 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
118
118
|
if (!feature.outputs) {
|
|
119
119
|
feature.outputs = deriveOutputs(featureName, process.cwd());
|
|
120
120
|
}
|
|
121
|
-
const
|
|
121
|
+
const scenarios = implementProgress(featurePath);
|
|
122
122
|
|
|
123
123
|
if (options.json) {
|
|
124
124
|
const status = {
|
|
@@ -137,7 +137,7 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
137
137
|
created: output.created,
|
|
138
138
|
path: output.path
|
|
139
139
|
})),
|
|
140
|
-
|
|
140
|
+
scenarios,
|
|
141
141
|
approvals: feature.approvalGates || {},
|
|
142
142
|
next: nextStepFor(featureName, process.cwd())
|
|
143
143
|
};
|
|
@@ -177,14 +177,13 @@ export async function statusCommand(featureName, options = {}) {
|
|
|
177
177
|
}
|
|
178
178
|
console.log('');
|
|
179
179
|
|
|
180
|
-
if (
|
|
181
|
-
console.log(chalk.white.bold('
|
|
182
|
-
const percentage = Math.round((
|
|
183
|
-
console.log(` Total: ${
|
|
184
|
-
console.log(chalk.green(` ✓
|
|
185
|
-
if (
|
|
186
|
-
if (
|
|
187
|
-
if (slices.pending > 0) console.log(chalk.gray(` ⬜ Pendentes: ${slices.pending}`));
|
|
180
|
+
if (scenarios && scenarios.total > 0) {
|
|
181
|
+
console.log(chalk.white.bold('Cenários (3-implement/report.md):'));
|
|
182
|
+
const percentage = Math.round((scenarios.done / scenarios.total) * 100);
|
|
183
|
+
console.log(` Total: ${scenarios.total}`);
|
|
184
|
+
console.log(chalk.green(` ✓ Feitos: ${scenarios.done} (${percentage}%)`));
|
|
185
|
+
if (scenarios.blocked > 0) console.log(chalk.yellow(` ⏸ Bloqueados (esperam decisão): ${scenarios.blocked}`));
|
|
186
|
+
if (scenarios.pending > 0) console.log(chalk.gray(` ⬜ Pendentes: ${scenarios.pending}`));
|
|
188
187
|
console.log('');
|
|
189
188
|
}
|
|
190
189
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, dirname, relative, isAbsolute } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
|
-
import { existsSync, readdirSync } from 'fs';
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -27,9 +27,10 @@ import {
|
|
|
27
27
|
} from '../../utils/version-checker.js';
|
|
28
28
|
import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
|
|
29
29
|
import { installSkills } from '../../utils/skills-installer.js';
|
|
30
|
-
import { installAgents
|
|
30
|
+
import { installAgents } from '../../utils/agents-installer.js';
|
|
31
31
|
import { injectMorphImport } from '../../utils/claude-md-injector.js';
|
|
32
32
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
33
|
+
import { detectStackTags, mergeDetectedTags } from '../../lib/installers/stack-sync.js';
|
|
33
34
|
import { updateClaudeMdAgentsBlock, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
|
|
34
35
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
36
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
@@ -145,6 +146,26 @@ export function stateJsonIsTracked(targetPath) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
const SLICES_HEADING = /^## Fatias\s*$/m;
|
|
150
|
+
|
|
151
|
+
/** Active features still planned in 9.x slices: a plan with `## Fatias` and no `3-implement/report.md` yet. */
|
|
152
|
+
export function legacySliceFeatures(targetPath) {
|
|
153
|
+
const featuresDir = join(targetPath, '.morph', 'features');
|
|
154
|
+
let names;
|
|
155
|
+
try { names = readdirSync(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; }
|
|
156
|
+
return names.filter((name) => {
|
|
157
|
+
const dir = join(featuresDir, name);
|
|
158
|
+
if (existsSync(join(dir, '3-implement', 'report.md'))) return false;
|
|
159
|
+
try { return SLICES_HEADING.test(readFileSync(join(dir, '2-plan', 'plan.md'), 'utf8')); } catch { return false; }
|
|
160
|
+
}).sort();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function legacySliceNotice(names) {
|
|
164
|
+
if (names.length === 0) return null;
|
|
165
|
+
return `Feature(s) planejada(s) em fatias (9.x): ${names.join(', ')}. Plano não aprovado: tire \`## Fatias\` e rode \`morph-spec plan-check <f>\`. `
|
|
166
|
+
+ 'Já em implement: segue pelos `reports/F*.md`; para o formato novo, crie `3-implement/report.md` com uma linha por `Cn` (CHANGELOG 10.0.0).';
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
export function untrackCommittedStateJson(targetPath, fx = createEffects({ dryRun: false })) {
|
|
149
170
|
if (!stateJsonIsTracked(targetPath)) return false;
|
|
150
171
|
return fx.gitIndex('rm --cached --quiet', '.morph/state.json', { cwd: targetPath });
|
|
@@ -487,6 +508,19 @@ export async function updateCommand(options = {}) {
|
|
|
487
508
|
try {
|
|
488
509
|
if (configBackup) projectTags = getProjectTags(configBackup);
|
|
489
510
|
} catch {}
|
|
511
|
+
// The same detection as `syncStackRules`, folded in before the installs: `restoreUserConfig`
|
|
512
|
+
// writes `configBackup`, and a dry run sees `.claude/rules/` before the clean, not after.
|
|
513
|
+
if (configBackup) {
|
|
514
|
+
try {
|
|
515
|
+
const detected = detectStackTags(targetPath, { config: configBackup });
|
|
516
|
+
const merged = mergeDetectedTags(configBackup, detected.persist);
|
|
517
|
+
if (merged.changed) {
|
|
518
|
+
configBackup.project = merged.config.project;
|
|
519
|
+
logger.dim(` ✓ config.json: + ${merged.added.join(', ')} (detectado nos arquivos)`);
|
|
520
|
+
}
|
|
521
|
+
projectTags = [...new Set([...getProjectTags(configBackup), ...detected.tags])];
|
|
522
|
+
} catch {}
|
|
523
|
+
}
|
|
490
524
|
|
|
491
525
|
let commandsCopied = false;
|
|
492
526
|
const commandsSrc = join(frameworkDir, 'commands');
|
|
@@ -504,7 +538,6 @@ export async function updateCommand(options = {}) {
|
|
|
504
538
|
|
|
505
539
|
updateSpinner.text = 'Syncing agents to .claude/agents/...';
|
|
506
540
|
const agentsResult = await installAgents(targetPath, frameworkDir, { projectTags, fx });
|
|
507
|
-
await installDomainAgents(targetPath, frameworkDir, { projectTags, fx });
|
|
508
541
|
|
|
509
542
|
updateSpinner.text = 'Syncing rules to .claude/rules/...';
|
|
510
543
|
const rulesSrc = join(frameworkDir, 'rules');
|
|
@@ -566,6 +599,9 @@ export async function updateCommand(options = {}) {
|
|
|
566
599
|
}
|
|
567
600
|
}
|
|
568
601
|
|
|
602
|
+
const sliceNotice = legacySliceNotice(legacySliceFeatures(targetPath));
|
|
603
|
+
if (sliceNotice) notices.push(sliceNotice);
|
|
604
|
+
|
|
569
605
|
const untracked = untrackCommittedStateJson(targetPath, fx);
|
|
570
606
|
|
|
571
607
|
updateSpinner.text = 'Saving version info...';
|
|
@@ -320,15 +320,6 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
320
320
|
|
|
321
321
|
const graph = seedWorktreeGraph(worktreePath, cwd);
|
|
322
322
|
|
|
323
|
-
// Fail-open; a no-op under `create --worktree`, which regenerates it after its copy.
|
|
324
|
-
try {
|
|
325
|
-
if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
|
|
326
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
327
|
-
// `worktree` is root-owned and absent from this branch copy: pass it so the doc says "Local: worktree".
|
|
328
|
-
writeHandoff(feature, worktreePath, { worktree: { path: worktreePath, branch } });
|
|
329
|
-
}
|
|
330
|
-
} catch {}
|
|
331
|
-
|
|
332
323
|
try {
|
|
333
324
|
const { loadState, saveState } = await import('../../core/state/state-manager.js');
|
|
334
325
|
const state = loadState(false);
|
|
@@ -834,9 +825,9 @@ export function pickRemovalFailure(stderrs) {
|
|
|
834
825
|
|
|
835
826
|
/**
|
|
836
827
|
* PROVE-or-DECLINE. `refused` (proved unsafe) restores the junctions; a git FAILURE leaves them down
|
|
837
|
-
* (`infraLeftUnlinked`)
|
|
828
|
+
* (`infraLeftUnlinked`).
|
|
838
829
|
*/
|
|
839
|
-
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false,
|
|
830
|
+
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, stamp }) {
|
|
840
831
|
// ORDER IS LOAD-BEARING: unlink the junctions BEFORE git touches the dir. `git worktree remove`
|
|
841
832
|
// follows them and deletes the root's installation; a declined path is repaired by the rollback below.
|
|
842
833
|
const unlinked = unlinkWorktreeInfraLinks(worktreePath, root);
|
|
@@ -881,8 +872,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
881
872
|
// would make the next retry (or a raw `--force`) follow the junctions out.
|
|
882
873
|
const refused = !canRemove;
|
|
883
874
|
let relinked = false;
|
|
884
|
-
|
|
885
|
-
if ((refused || relinkAfterFailure) && unlinked.length && existsSync(worktreePath)) {
|
|
875
|
+
if (refused && unlinked.length && existsSync(worktreePath)) {
|
|
886
876
|
try {
|
|
887
877
|
setupInfraLinks(worktreePath, root);
|
|
888
878
|
relinked = true;
|
|
@@ -1055,7 +1045,6 @@ export async function runWorktreeRemove(featureArg, options = {}) {
|
|
|
1055
1045
|
options: { discardIgnored: options.discardIgnored === true },
|
|
1056
1046
|
// The branch is the work: deleting it would discard unintegrated commits. Opt-in only.
|
|
1057
1047
|
deleteBranch: options.deleteBranch === true,
|
|
1058
|
-
relinkOnFailure: options.relinkOnFailure === true,
|
|
1059
1048
|
// Re-run advice must address the worktree the way it was addressable here:
|
|
1060
1049
|
// a `--target` worktree has no feature name that would resolve.
|
|
1061
1050
|
addressByTarget: !featureAddressable,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state-manager.js';
|
|
@@ -8,7 +8,7 @@ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
|
8
8
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
9
9
|
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
10
10
|
import { seedDesignArtifacts } from '../../lib/specs/design-seed.js';
|
|
11
|
-
import {
|
|
11
|
+
import { implementProgress, reportPath, reportSkeleton } from '../../../framework/hooks/shared/report-progress.js';
|
|
12
12
|
import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
|
|
13
13
|
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
14
14
|
|
|
@@ -38,17 +38,25 @@ function missingPhaseArtifacts(phase, featurePath) {
|
|
|
38
38
|
const rel = `${entry.phaseDir}/${entry.filename}`;
|
|
39
39
|
if (!existsSync(join(featurePath, entry.phaseDir, entry.filename))) missing.push(rel);
|
|
40
40
|
}
|
|
41
|
-
if (phase === 'implement') missing.push(...
|
|
41
|
+
if (phase === 'implement') missing.push(...unfinishedScenarios(featurePath));
|
|
42
42
|
return missing;
|
|
43
43
|
} catch {
|
|
44
44
|
return [];
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
49
|
-
const progress =
|
|
48
|
+
function unfinishedScenarios(featurePath) {
|
|
49
|
+
const progress = implementProgress(featurePath);
|
|
50
50
|
if (!progress) return ['2-plan/checks.json'];
|
|
51
|
-
|
|
51
|
+
const open = progress.checks.filter((c) => c.status !== 'feito').map((c) => c.id);
|
|
52
|
+
return open.length ? [`3-implement/report.md (${open.join(', ')} não feito)`] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function seedReport(featurePath, featureName) {
|
|
56
|
+
const path = reportPath(featurePath);
|
|
57
|
+
if (existsSync(path)) return;
|
|
58
|
+
const progress = implementProgress(featurePath);
|
|
59
|
+
if (progress) writeFileSync(path, reportSkeleton(featureName, progress.checks.map((c) => c.id)));
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
// Message-only and fail-open: names the worktree that holds the artifacts instead of demanding
|
|
@@ -114,10 +122,6 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
114
122
|
mkdirSync(join(featurePath, '1-design'), { recursive: true });
|
|
115
123
|
reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
|
|
116
124
|
reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
117
|
-
try {
|
|
118
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
119
|
-
writeHandoff(featureName, process.cwd());
|
|
120
|
-
} catch { /* fail-open */ }
|
|
121
125
|
console.log(chalk.green(`\n✅ "${featureName}": gate plan já aprovado — ramo de design aberto (1-design/).\n`));
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
@@ -129,7 +133,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
129
133
|
return;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
// `implement` has no gate, so this is what keeps unfinished
|
|
136
|
+
// `implement` has no gate, so this is what keeps unfinished scenarios out of review.
|
|
133
137
|
// `--force` bypasses it but still names what was skipped.
|
|
134
138
|
const missingArtifacts = missingPhaseArtifacts(phase, featurePath);
|
|
135
139
|
if (missingArtifacts.length && !options.force) {
|
|
@@ -166,7 +170,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// `setApprovalGate` replaces the gate whole: rewriting an approved gate would turn a human
|
|
169
|
-
// signature into "auto". Skips ONLY the write, so the scaffold
|
|
173
|
+
// signature into "auto". Skips ONLY the write, so the scaffold below still runs.
|
|
170
174
|
const priorGate = gate ? feature.approvalGates?.[gate] : null;
|
|
171
175
|
const alreadyApproved = priorGate?.approved === true;
|
|
172
176
|
|
|
@@ -193,13 +197,9 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
193
197
|
if (!existsSync(nextPath)) mkdirSync(nextPath, { recursive: true });
|
|
194
198
|
}
|
|
195
199
|
if (nextDir === '1-design') reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
|
|
200
|
+
if (nextDir === '3-implement') seedReport(featurePath, featureName);
|
|
196
201
|
if (phase === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
197
202
|
|
|
198
|
-
// A phase transition is when a session hands the feature over: refresh handoff.md.
|
|
199
|
-
try {
|
|
200
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
201
|
-
writeHandoff(featureName, process.cwd());
|
|
202
|
-
} catch { /* fail-open */ }
|
|
203
203
|
|
|
204
204
|
console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
|
|
205
205
|
if (missingArtifacts.length) {
|
|
@@ -260,12 +260,6 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
260
260
|
...(selfAssessed ? { selfAssessed: true } : {}),
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
// Approving is a session handoff too; a regeneration failure must never fail the approval.
|
|
264
|
-
try {
|
|
265
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
266
|
-
writeHandoff(featureName, process.cwd());
|
|
267
|
-
} catch { /* fail-open */ }
|
|
268
|
-
|
|
269
263
|
if (selfAssessed) {
|
|
270
264
|
console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM os relatórios de review independentes (--self-assessed).'));
|
|
271
265
|
console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, renameSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, sep } from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
|
|
6
6
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
7
|
+
import { RECAP_FILES, writeRecap } from './recap.js';
|
|
7
8
|
|
|
8
9
|
function assertContained(scopeDir, name, target) {
|
|
9
10
|
const scopeAbs = resolve(scopeDir) + sep;
|
|
@@ -38,6 +39,17 @@ function readReviewApproved(fjPath) {
|
|
|
38
39
|
* Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
|
|
39
40
|
* worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
|
|
40
41
|
*/
|
|
42
|
+
// plan.md stays: a squash merge drops every file born and pruned inside the branch.
|
|
43
|
+
function pruneArchive(dir) {
|
|
44
|
+
for (const entry of readdirSync(dir)) {
|
|
45
|
+
if (entry === 'feature.json' || Object.values(RECAP_FILES).includes(entry)) continue;
|
|
46
|
+
if (entry !== '2-plan') { rmSync(join(dir, entry), { recursive: true, force: true }); continue; }
|
|
47
|
+
for (const planEntry of readdirSync(join(dir, entry))) {
|
|
48
|
+
if (planEntry !== 'plan.md') rmSync(join(dir, entry, planEntry), { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
export function runArchive(feature, opts = {}) {
|
|
42
54
|
const cwd = opts.cwd || process.cwd();
|
|
43
55
|
|
|
@@ -71,6 +83,10 @@ export function runArchive(feature, opts = {}) {
|
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
const useGit = opts.git !== false && isGitRepo(cwd);
|
|
86
|
+
let archivedFromSha = null;
|
|
87
|
+
if (useGit) {
|
|
88
|
+
try { archivedFromSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim(); } catch { /* no commit yet */ }
|
|
89
|
+
}
|
|
74
90
|
|
|
75
91
|
// `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
|
|
76
92
|
mkdirSync(archiveDir, { recursive: true });
|
|
@@ -96,6 +112,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
96
112
|
const fj = JSON.parse(readFileSync(fjPathAfter, 'utf8'));
|
|
97
113
|
fj.status = 'completed';
|
|
98
114
|
fj.archivedAt = new Date().toISOString();
|
|
115
|
+
if (archivedFromSha) fj.archivedFrom = { sha: archivedFromSha };
|
|
99
116
|
fj.updatedAt = fj.archivedAt;
|
|
100
117
|
writeFileSync(fjPathAfter, JSON.stringify(fj, null, 2), 'utf8');
|
|
101
118
|
if (useGit && staged) {
|
|
@@ -113,6 +130,13 @@ export function runArchive(feature, opts = {}) {
|
|
|
113
130
|
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
// Before the prune: the recap reads the Gate 3 reports the archive does not keep.
|
|
134
|
+
const recap = opts.recap === false ? null : writeRecap(archivePath, archivePath, feature, { cwd });
|
|
135
|
+
pruneArchive(archivePath);
|
|
136
|
+
if (useGit && staged) {
|
|
137
|
+
try { execFileSync('git', ['add', '-A', '--', archivePathspec], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
// Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
|
|
117
141
|
// changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
|
|
118
142
|
let committed = false;
|
|
@@ -144,6 +168,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
144
168
|
|
|
145
169
|
return {
|
|
146
170
|
ok: true, archived: true, committed, staged, archivePath,
|
|
171
|
+
recap: recap?.ok ? recap.path : null,
|
|
147
172
|
revocationProblems: adrIndex.problems,
|
|
148
173
|
adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
|
|
149
174
|
};
|
|
@@ -180,6 +205,7 @@ export async function archiveCommand(featureName, options = {}) {
|
|
|
180
205
|
|
|
181
206
|
console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
|
|
182
207
|
console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
|
|
208
|
+
if (result.recap) console.log(chalk.gray(` recap: ${result.recap} (morph-spec recap ${featureName} --piper narra com o Piper)`));
|
|
183
209
|
if (result.adrIndex) {
|
|
184
210
|
const { added, revoked, narrowed } = result.adrIndex;
|
|
185
211
|
console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
|
|
@@ -30,11 +30,11 @@ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedB
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (gate === 'plan') {
|
|
33
|
-
const block = planAutoApprovalBlock(cwd, featureName);
|
|
33
|
+
const block = planAutoApprovalBlock(cwd, featureName, feature);
|
|
34
34
|
if (block) {
|
|
35
35
|
return {
|
|
36
36
|
title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
|
|
37
|
-
hints: ['
|
|
37
|
+
hints: ['Sem checks.json fresco: plan-check verde e rode de novo. Com degrau: aprove com --approver <nome> ou pela resposta humana. O --force não dispensa nenhum dos dois.'],
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -132,7 +132,7 @@ export async function createCommand(featureName, options = {}) {
|
|
|
132
132
|
const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
|
|
133
133
|
if (!alreadyExists && (description === null || String(description).trim() === '')) {
|
|
134
134
|
console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
|
|
135
|
-
console.error(chalk.gray(' Pass --description "<what/why>" (or --request
|
|
135
|
+
console.error(chalk.gray(' Pass --description "<what/why>" (or --request '<pedido>', which seeds it).'));
|
|
136
136
|
console.error(chalk.gray(' This keeps features identifiable — no more anonymous scaffolds.\n'));
|
|
137
137
|
process.exit(1);
|
|
138
138
|
}
|
|
@@ -185,11 +185,6 @@ export async function createCommand(featureName, options = {}) {
|
|
|
185
185
|
// Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
|
|
186
186
|
const remat = await materializeState(worktreeResult.path);
|
|
187
187
|
worktreeResult.materialized = remat.materialized;
|
|
188
|
-
try {
|
|
189
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
190
|
-
// `worktree` was stripped from this copy above, so pass where it actually lives.
|
|
191
|
-
writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
|
|
192
|
-
} catch { /* fail-open */ }
|
|
193
188
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
194
189
|
}
|
|
195
190
|
} else {
|