@polymorphism-tech/morph-spec 9.0.0 → 9.1.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -24,113 +24,48 @@ import { runArchive } from './archive.js';
|
|
|
24
24
|
import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
25
25
|
import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
|
|
26
26
|
|
|
27
|
-
/** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
|
|
28
27
|
const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
|
|
29
28
|
|
|
30
29
|
/** Git's soft limit for a subject line. */
|
|
31
30
|
const PR_TITLE_MAX = 72;
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* Was always `morph: {slug}` — so every PR in a repo whose hand-made ones follow
|
|
37
|
-
* conventional commits (`fix(ui): coluna CRM nos dossiês…`) arrived shapeless
|
|
38
|
-
* and said nothing about what changed (D4).
|
|
39
|
-
*
|
|
40
|
-
* Now: `{type}({slug}): {subject}` — the type from the feature's `workType`, the
|
|
41
|
-
* slug kept as the scope (it is the traceability the old title at least had),
|
|
42
|
-
* and the subject from the feature's own identity: `name` first (a human
|
|
43
|
-
* deliberately titled it), then `description`, first line only. With neither,
|
|
44
|
-
* degrade to `{type}: {slug}` rather than emit an empty subject.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} feature
|
|
47
|
-
* @param {object|null} fj - the feature.json
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
32
|
+
// `{type}({slug}): {subject}`, the subject from `name` (unless it is the default slug), then
|
|
33
|
+
// `description`, first line only; with neither, `{type}: {slug}`.
|
|
50
34
|
export function buildPrTitle(feature, fj) {
|
|
51
35
|
const type = PR_TYPE_BY_WORK_TYPE[fj?.workType] || 'chore';
|
|
52
36
|
|
|
53
37
|
const named = typeof fj?.name === 'string' ? fj.name.trim() : '';
|
|
54
|
-
// The default `name` IS the slug
|
|
55
|
-
// only repeat the scope — treat it as absent.
|
|
38
|
+
// The default `name` IS the slug, which as a subject would only repeat the scope.
|
|
56
39
|
const described = typeof fj?.description === 'string' ? fj.description.trim() : '';
|
|
57
40
|
const raw = (named && named !== feature ? named : described).split('\n')[0].trim();
|
|
58
41
|
if (!raw) return `${type}: ${feature}`;
|
|
59
42
|
|
|
60
43
|
const prefix = `${type}(${feature}): `;
|
|
61
44
|
const room = PR_TITLE_MAX - prefix.length;
|
|
62
|
-
//
|
|
63
|
-
// than ship a title that is all prefix.
|
|
45
|
+
// No room left for a subject: drop the scope rather than ship a title that is all prefix.
|
|
64
46
|
if (room < 12) return `${type}: ${raw}`.slice(0, PR_TITLE_MAX);
|
|
65
47
|
return prefix + (raw.length <= room ? raw : `${raw.slice(0, room - 1)}…`);
|
|
66
48
|
}
|
|
67
49
|
|
|
68
50
|
const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `plan.md` com as decisões, índice de ADRs, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
* Order (see the plan's backbone): validate → ARCHIVE inside the `morph/{f}`
|
|
75
|
-
* branch (committed, so the archived state travels in one integration) →
|
|
76
|
-
* integrate:
|
|
77
|
-
* --merge : merge `--no-ff` into the default branch, LOCAL (no push).
|
|
78
|
-
* --pr : push `morph/{f}` + `gh pr create` (does NOT merge locally).
|
|
79
|
-
* → teardown the worktree (PROVE-or-DECLINE) when there is one.
|
|
80
|
-
*
|
|
81
|
-
* Every refusal is fail-CLOSED, exit 1, and — since the `--pr` remote
|
|
82
|
-
* preconditions run BEFORE the archive — a refused `--pr` leaves no side effect.
|
|
83
|
-
* `finished` is the integration (the actual value); `worktreeRemoved` is the
|
|
84
|
-
* best-effort teardown — two independent signals, as before.
|
|
85
|
-
*
|
|
86
|
-
* RESUMABLE. `merge-conflict`, `checkout-default-failed`, `push-failed` and
|
|
87
|
-
* `pr-create-failed` all fail AFTER the archive, and legitimately so: the
|
|
88
|
-
* archive commit is what travels with the branch. Re-running then found no
|
|
89
|
-
* `features/{f}` and refused from scratch, so every mid-way failure demanded a
|
|
90
|
-
* hand repair. Now a second run that finds the feature ARCHIVED on its own
|
|
91
|
-
* branch, not yet merged into the default, picks up at the integration and says
|
|
92
|
-
* `resumed: true` in its output — see Validation 0c.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} feature
|
|
95
|
-
* `--dry-run` desvia para {@link finishPreflight} ANTES de qualquer validação
|
|
96
|
-
* que saia com exit 1 e ANTES de qualquer mutação: o pré-voo existe para
|
|
97
|
-
* EXPLICAR, e um pré-voo que recusa pelos mesmos motivos do comando real não
|
|
98
|
-
* explica nada.
|
|
99
|
-
*
|
|
100
|
-
* @param {{ pr?: boolean, merge?: boolean, keep?: boolean, keepVolumes?: boolean,
|
|
101
|
-
* push?: boolean, force?: boolean, discardIgnored?: boolean, dryRun?: boolean,
|
|
102
|
-
* cwd?: string }} [options]
|
|
103
|
-
*/
|
|
52
|
+
// Every refusal is fail-CLOSED and happens BEFORE the archive, the first mutation. Resumable: a
|
|
53
|
+
// later run that finds the feature archived on its branch but not integrated picks up there.
|
|
54
|
+
// `--dry-run` diverts to finishPreflight before any exit-1 validation: a preflight explains.
|
|
104
55
|
export async function finishCommand(feature, options = {}) {
|
|
105
56
|
const cwd = options.cwd || process.cwd();
|
|
106
57
|
const root = resolvePrimaryRoot(cwd);
|
|
107
58
|
const rootNorm = resolve(root).replace(/\\/g, '/');
|
|
108
59
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// that outlives its feature only expires on its own TTL.
|
|
113
|
-
//
|
|
114
|
-
// The e2e stack is NOT reclaimed here: by the time a success is emitted the
|
|
115
|
-
// worktree that owns the compose file may already be gone. Each integration
|
|
116
|
-
// path calls `reclaimStack` immediately BEFORE its worktree teardown (or
|
|
117
|
-
// before its final emit in the common flow) — see stack-teardown.js.
|
|
118
|
-
//
|
|
119
|
-
// Set by Validation 0c when this run picks up an already-archived feature.
|
|
120
|
-
// Stamped onto EVERY payload emitted from that point on, success or refusal:
|
|
121
|
-
// a resume merges a branch the user did not archive in this run, and that has
|
|
122
|
-
// to be visible. Someone who mistypes a name and lands on an archived feature
|
|
123
|
-
// would otherwise get a silent merge they never asked for.
|
|
60
|
+
// The claim is released on every success through emit (fail-open: a stale claim expires on its
|
|
61
|
+
// TTL). The e2e stack is reclaimed by each path BEFORE its worktree teardown, never here.
|
|
62
|
+
// `resumed` goes on every payload, so a merge of a feature archived by an earlier run is visible.
|
|
124
63
|
let resumed = false;
|
|
125
64
|
|
|
126
|
-
// ONE stamp for every backup this run writes
|
|
127
|
-
// folders then count as a single backup under the retention.
|
|
65
|
+
// ONE stamp for every backup this run writes, so they count as a single backup in the retention.
|
|
128
66
|
const backupStamp = newBackupStamp();
|
|
129
67
|
|
|
130
|
-
//
|
|
131
|
-
// backups exist (the shed and the teardown both precede every success emit).
|
|
132
|
-
// This run's stamp is protected: the prune never takes the backup it just
|
|
133
|
-
// made room for, even when backups stamped by a clock running ahead outrank it.
|
|
68
|
+
// Retention runs after this run's own backups exist, and never prunes this run's stamp.
|
|
134
69
|
const emit = (obj) => {
|
|
135
70
|
if (resumed) obj = { ...obj, resumed: true };
|
|
136
71
|
if (obj?.finished === true) {
|
|
@@ -146,15 +81,12 @@ export async function finishCommand(feature, options = {}) {
|
|
|
146
81
|
console.log(JSON.stringify(obj));
|
|
147
82
|
};
|
|
148
83
|
|
|
149
|
-
//
|
|
150
|
-
// git/gh call below) and the same gate the worktree path always applied.
|
|
84
|
+
// Anti command-injection: the name flows into every git/gh call below.
|
|
151
85
|
if (!isValidFeatureName(feature)) {
|
|
152
86
|
emit({ finished: false, error: 'invalid-feature-name', feature });
|
|
153
87
|
process.exit(1);
|
|
154
88
|
}
|
|
155
89
|
|
|
156
|
-
// Mode: exactly one of --pr / --merge (they are alternative integrations — the
|
|
157
|
-
// finish merges OR opens a PR, never both, never neither).
|
|
158
90
|
const wantPr = !!options.pr;
|
|
159
91
|
const wantMerge = !!options.merge;
|
|
160
92
|
if (wantPr === wantMerge) {
|
|
@@ -163,15 +95,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
163
95
|
}
|
|
164
96
|
const mode = wantPr ? 'pr' : 'merge';
|
|
165
97
|
|
|
166
|
-
// `--push` publica a
|
|
167
|
-
//
|
|
168
|
-
// já publica `morph/{f}` por conta própria. A combinação é um pedido que o
|
|
169
|
-
// comando não consegue honrar — e aceitar em silêncio o que não se pode
|
|
170
|
-
// honrar é exatamente o defeito que esta série de fixes existe para tirar do
|
|
171
|
-
// CLI (o `score` que gravava um zero fabricado, o `approve` que saía 0 sem
|
|
172
|
-
// aprovar nada). Recusa ALTA, e AQUI: junto do gate de modo, antes do
|
|
173
|
-
// archive — que é a primeira mutação do finish. Uma recusa depois dele
|
|
174
|
-
// deixaria a feature meio fechada, pior que o no-op que ela substitui.
|
|
98
|
+
// `--push` publica a default depois de um merge local, e o `--pr` não faz merge local: recusa
|
|
99
|
+
// AQUI, antes do archive (a primeira mutação), em vez de aceitar em silêncio o que não honra.
|
|
175
100
|
if (wantPr && options.push) {
|
|
176
101
|
emit({
|
|
177
102
|
finished: false,
|
|
@@ -184,23 +109,16 @@ export async function finishCommand(feature, options = {}) {
|
|
|
184
109
|
|
|
185
110
|
const expectedBranch = buildBranchName(feature);
|
|
186
111
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// the PRIMARY ROOT itself when the common flow checked the branch out there.
|
|
190
|
-
// A match whose path IS the root is the common flow (no separate worktree to
|
|
191
|
-
// tear down); a match elsewhere is the worktree flow.
|
|
112
|
+
// findWorktreeForFeature also matches the PRIMARY ROOT when the common flow checked the branch
|
|
113
|
+
// out there: a match at the root is the common flow.
|
|
192
114
|
const found = findWorktreeForFeature(feature, root);
|
|
193
115
|
const isWorktreeFlow = !!found && found.path.replace(/\\/g, '/') !== rootNorm;
|
|
194
116
|
|
|
195
117
|
let worktreePath = null;
|
|
196
|
-
let featureCwd = root; // where the feature's own tree (and
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// divergente, cwd dentro do worktree, feature ausente), e o pré-voo existe
|
|
201
|
-
// justamente para ser rodado ANTES de o estado estar bom. Ele reporta o que
|
|
202
|
-
// encontrar — inclusive o cwd dentro do worktree, como AVISO — e sai 0 sem
|
|
203
|
-
// ter tocado em nada.
|
|
118
|
+
let featureCwd = root; // where the feature's own tree (and its review) lives
|
|
119
|
+
|
|
120
|
+
// Pré-voo AQUI, antes das validações que saem com exit 1: ele existe para rodar antes de o estado
|
|
121
|
+
// estar bom, reporta o que encontrar e sai 0 sem tocar em nada.
|
|
204
122
|
if (options.dryRun) {
|
|
205
123
|
emit(finishPreflight({
|
|
206
124
|
feature,
|
|
@@ -209,17 +127,14 @@ export async function finishCommand(feature, options = {}) {
|
|
|
209
127
|
cwd,
|
|
210
128
|
mode,
|
|
211
129
|
branch: expectedBranch,
|
|
212
|
-
// `options` INTEIRO, e
|
|
213
|
-
//
|
|
214
|
-
// destruído), então um pré-voo que não recebe as flags do comando prevê
|
|
215
|
-
// outro comando. Ver B1 na errata E4.
|
|
130
|
+
// `options` INTEIRO: `--discard-ignored` inverte a classificação, e um pré-voo sem as flags do
|
|
131
|
+
// comando prevê outro comando.
|
|
216
132
|
options,
|
|
217
133
|
}));
|
|
218
134
|
return;
|
|
219
135
|
}
|
|
220
136
|
|
|
221
137
|
if (isWorktreeFlow) {
|
|
222
|
-
// Validation 0: the worktree must actually be ON the branch we will merge.
|
|
223
138
|
if (found.branch !== found.expectedBranch) {
|
|
224
139
|
emit({
|
|
225
140
|
finished: false,
|
|
@@ -235,14 +150,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
235
150
|
worktreePath = found.path;
|
|
236
151
|
featureCwd = worktreePath;
|
|
237
152
|
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// directory, so `git worktree remove` fails — and by then the archive is
|
|
241
|
-
// committed, the branch merged, and the infra junctions already dropped.
|
|
242
|
-
// The result is a worse state than never having tried: an orphan directory,
|
|
243
|
-
// a de-registered worktree, and a `relinkHint` telling the user to repair
|
|
244
|
-
// by hand. Refuse HERE, before the first mutation — the cost is one
|
|
245
|
-
// `cd`, and it is the exact failure observed in the ProspectPRO session.
|
|
153
|
+
// Windows locks the process cwd, so `git worktree remove` would fail AFTER the archive and the
|
|
154
|
+
// merge, leaving an orphan directory. Refuse here, before the first mutation.
|
|
246
155
|
if (cwdIsInsideWorktree(cwd, worktreePath)) {
|
|
247
156
|
emit({
|
|
248
157
|
finished: false,
|
|
@@ -255,15 +164,9 @@ export async function finishCommand(feature, options = {}) {
|
|
|
255
164
|
process.exit(1);
|
|
256
165
|
}
|
|
257
166
|
} else {
|
|
258
|
-
//
|
|
259
|
-
// branch (so the archive commits on it and we can checkout the default to
|
|
260
|
-
// merge). A feature with NO morph/{f} branch was born before this flow —
|
|
261
|
-
// route it explicitly to archive-without-integrate, never a generic refusal.
|
|
167
|
+
// A feature with no morph/{f} branch predates this flow: route it to archive-without-integrate.
|
|
262
168
|
if (!branchExists(expectedBranch, root)) {
|
|
263
|
-
// No branch AND no
|
|
264
|
-
// feature that is not here at all (wrong name, wrong tree, or already
|
|
265
|
-
// archived). Routing that to `archive legacy` would tell the user to
|
|
266
|
-
// archive something that does not exist.
|
|
169
|
+
// No branch AND no folder is not a legacy feature: it is not here at all.
|
|
267
170
|
if (!existsSync(join(root, '.morph', 'features', feature))) {
|
|
268
171
|
emit({ finished: false, error: 'feature-not-found', feature, ...featureElsewhereHint(root, root, feature) });
|
|
269
172
|
process.exit(1);
|
|
@@ -291,19 +194,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
291
194
|
featureCwd = root;
|
|
292
195
|
}
|
|
293
196
|
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// gone entirely, already ARCHIVED and awaiting integration (resume), or
|
|
297
|
-
// already integrated (nothing to do).
|
|
298
|
-
//
|
|
299
|
-
// Originally all three arrived as `review-not-approved`, because a missing
|
|
300
|
-
// feature.json parses to null and the optional chaining below fell straight
|
|
301
|
-
// through — being told "Gate 3 not approved" about an approved gate is what
|
|
302
|
-
// sent the user looking in the wrong place after an interrupted finish.
|
|
303
|
-
//
|
|
304
|
-
// Keyed on the DIRECTORY, not on the parse: readFeatureJson also returns null
|
|
305
|
-
// for malformed JSON, and a legacy feature with a folder but no (or broken)
|
|
306
|
-
// feature.json is a real thing that `--force` is expected to push through.
|
|
197
|
+
// Not in `features/`: gone, archived awaiting integration (resume), or already integrated. Keyed
|
|
198
|
+
// on the DIRECTORY, not the parse: a legacy folder with a broken feature.json is --force's case.
|
|
307
199
|
if (!existsSync(join(featureCwd, '.morph', 'features', feature))) {
|
|
308
200
|
const archivePath = join(featureCwd, '.morph', 'archive', feature);
|
|
309
201
|
if (!existsSync(archivePath)) {
|
|
@@ -311,19 +203,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
311
203
|
process.exit(1);
|
|
312
204
|
}
|
|
313
205
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// reaches this line has already proved we are standing on `morph/{f}`
|
|
317
|
-
// (worktree flow: found.branch === expectedBranch; common flow: branchExists
|
|
318
|
-
// + currentBranch === expectedBranch). So the archive step is done and only
|
|
319
|
-
// the integration is missing. Which is exactly what a failed merge/push
|
|
320
|
-
// leaves behind, and what used to require repairing by hand.
|
|
321
|
-
//
|
|
322
|
-
// The one thing still to establish is whether the integration ALSO
|
|
323
|
-
// happened. A branch already contained in the default is a finish that
|
|
324
|
-
// COMPLETED (`--keep`, or a `--pr` whose PR was merged) — re-merging it is
|
|
325
|
-
// the hazard this check exists to stop, and it is why resume is keyed on
|
|
326
|
-
// the branch's relationship to the default rather than on the archive alone.
|
|
206
|
+
// Every path here already stands on `morph/{f}`, so only the integration is missing. A branch
|
|
207
|
+
// already contained in the default COMPLETED: re-merging it is the hazard this check stops.
|
|
327
208
|
const target = detectDefaultBranch(root);
|
|
328
209
|
if (!target) {
|
|
329
210
|
emit({ finished: false, error: 'no-default-branch', hint: 'não foi possível resolver a branch default (origin/HEAD, main ou master)', feature });
|
|
@@ -331,9 +212,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
331
212
|
}
|
|
332
213
|
const merged = branchMergedInto(expectedBranch, target, root);
|
|
333
214
|
if (merged === null) {
|
|
334
|
-
// PROVE-or-DECLINE
|
|
335
|
-
// establish the branch's state, so we do not act on a guess. Refusing is
|
|
336
|
-
// recoverable; a duplicate merge is not.
|
|
215
|
+
// PROVE-or-DECLINE: refusing is recoverable, a duplicate merge is not.
|
|
337
216
|
emit({
|
|
338
217
|
finished: false,
|
|
339
218
|
error: 'resume-undecidable',
|
|
@@ -358,17 +237,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
358
237
|
process.exit(1);
|
|
359
238
|
}
|
|
360
239
|
|
|
361
|
-
// "Already integrated" is
|
|
362
|
-
//
|
|
363
|
-
// deliberately UNMERGED (that is what the open PR is for), so the ancestor
|
|
364
|
-
// test says "not merged" and would wave the resume through — straight into
|
|
365
|
-
// a second `gh pr create` on a head that already has one. The common flow
|
|
366
|
-
// makes that trivially reachable: integratePr never leaves `morph/{f}`, so
|
|
367
|
-
// simply re-running the command lands here.
|
|
368
|
-
//
|
|
369
|
-
// `gh pr create` refuses a duplicate, so the damage was never two PRs — it
|
|
370
|
-
// was `pr-create-failed` standing in for "this already finished", which is
|
|
371
|
-
// the same collapsed diagnosis the `feature-not-found` split removed.
|
|
240
|
+
// "Already integrated" is per mode: a completed `--pr` leaves its branch unmerged on purpose, so
|
|
241
|
+
// the ancestor test alone would wave a second `gh pr create` through.
|
|
372
242
|
if (mode === 'pr') {
|
|
373
243
|
const openPr = openPrFor(expectedBranch, root);
|
|
374
244
|
if (openPr === null) {
|
|
@@ -400,17 +270,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
400
270
|
resumed = true;
|
|
401
271
|
}
|
|
402
272
|
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
// NOT re-checked on a resume, deliberately. The archive IS the gate's receipt:
|
|
407
|
-
// runArchive refuses `review-not-approved` itself, so a feature that reached
|
|
408
|
-
// `archive/` either passed Gate 3 or was explicitly forced past it. Re-reading
|
|
409
|
-
// it here would only add a way to fail — an archive whose feature.json is
|
|
410
|
-
// absent or unparseable (legacy shapes, a forced close) parses to null and
|
|
411
|
-
// falls straight through to `review-not-approved`, which is the exact
|
|
412
|
-
// collapsed diagnosis the `feature-not-found` fix removed. Resume integrates
|
|
413
|
-
// what was already archived; it does not re-litigate why.
|
|
273
|
+
// Not re-checked on a resume: the archive IS the gate's receipt (runArchive refuses without
|
|
274
|
+
// the review gate), and an archive with no readable feature.json would only fail here wrongly.
|
|
414
275
|
const fj = readFeatureJson(featureCwd, feature, resumed ? 'archive' : 'features');
|
|
415
276
|
if (!resumed && !options.force && !(fj?.approvalGates?.review?.approved === true || fj?.approvals?.review?.approved === true)) {
|
|
416
277
|
emit({ finished: false, error: 'review-not-approved', feature });
|
|
@@ -421,8 +282,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
421
282
|
? String(fj.description).trim()
|
|
422
283
|
: `Feature ${feature} — integrada via morph-spec finish --pr.`;
|
|
423
284
|
|
|
424
|
-
//
|
|
425
|
-
// (work committed), so the scoped archive commit contains only the move.
|
|
285
|
+
// Clean, so the scoped archive commit contains only the move.
|
|
426
286
|
const cleanCwd = isWorktreeFlow ? worktreePath : root;
|
|
427
287
|
const dirty = gitStatusPorcelain(cleanCwd);
|
|
428
288
|
if (dirty === null) {
|
|
@@ -434,8 +294,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
434
294
|
process.exit(1);
|
|
435
295
|
}
|
|
436
296
|
|
|
437
|
-
//
|
|
438
|
-
// "master"; refuse clearly when it cannot be resolved.
|
|
297
|
+
// Never assume "master".
|
|
439
298
|
const defaultBranch = detectDefaultBranch(root);
|
|
440
299
|
if (!defaultBranch) {
|
|
441
300
|
emit({ finished: false, error: 'no-default-branch', hint: 'não foi possível resolver a branch default (origin/HEAD, main ou master)', feature });
|
|
@@ -446,20 +305,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
446
305
|
process.exit(1);
|
|
447
306
|
}
|
|
448
307
|
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
// clean. That check used to live inside integrateMerge — i.e. AFTER
|
|
452
|
-
// runArchive had already moved the feature to .morph/archive/, updated the
|
|
453
|
-
// ADR index and COMMITTED. A root dirtied by unrelated work therefore got a
|
|
454
|
-
// refusal on a half-finished feature, and the retry then failed for a
|
|
455
|
-
// different reason entirely (the feature was no longer in features/).
|
|
456
|
-
//
|
|
457
|
-
// `exemptOwnScope` because shedRootFeatureScope has not run yet: this
|
|
458
|
-
// feature's own root-side scaffold is still on disk and is not a reason to
|
|
459
|
-
// refuse — the shed backs it up and discards it moments later.
|
|
460
|
-
//
|
|
461
|
-
// `--pr` is exempt by design: integratePr pushes from the root but never
|
|
462
|
-
// checks out or merges there, so root dirt cannot corrupt it.
|
|
308
|
+
// BEFORE the archive: a dirty root refused after it left a half-finished feature. The own scope
|
|
309
|
+
// is exempt (the shed discards it next), and `--pr` never checks out or merges in the root.
|
|
463
310
|
if (mode === 'merge' && isWorktreeFlow) {
|
|
464
311
|
const rootPre = primaryRootUnexplainedDirt(root, feature, { exemptOwnScope: true });
|
|
465
312
|
if (!rootPre.ok) {
|
|
@@ -477,8 +324,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
477
324
|
}
|
|
478
325
|
}
|
|
479
326
|
|
|
480
|
-
//
|
|
481
|
-
// effect (nothing archived, nothing committed). ─────────────────────────
|
|
327
|
+
// BEFORE the archive, so a refused --pr has no side effect.
|
|
482
328
|
if (mode === 'pr') {
|
|
483
329
|
if (!hasOriginRemote(root)) {
|
|
484
330
|
emit({ finished: false, error: 'no-origin-remote', hint: 'não há remoto `origin` para abrir PR — configure um ou use --merge', feature });
|
|
@@ -490,9 +336,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
490
336
|
}
|
|
491
337
|
}
|
|
492
338
|
|
|
493
|
-
//
|
|
494
|
-
// Every refusal is behind us: the leftover of a killed `graph refresh` goes now,
|
|
495
|
-
// before the archive — never before a precondition that could still refuse.
|
|
339
|
+
// Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
|
|
496
340
|
dropLeftoverIgnore(cleanCwd);
|
|
497
341
|
const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
|
|
498
342
|
if (!arch.ok) {
|
|
@@ -506,66 +350,26 @@ export async function finishCommand(feature, options = {}) {
|
|
|
506
350
|
process.exit(1);
|
|
507
351
|
}
|
|
508
352
|
|
|
509
|
-
// ── INTEGRATE ─────────────────────────────────────────────────────────────
|
|
510
353
|
if (mode === 'merge') {
|
|
511
354
|
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
|
|
512
355
|
}
|
|
513
356
|
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
|
|
514
357
|
}
|
|
515
358
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
*
|
|
519
|
-
* Exportado porque é a frase que carrega o fato: uma junction/symlink dentro do
|
|
520
|
-
* worktree aponta para conteúdo que vive na RAIZ, e `rm -rf` / `git worktree
|
|
521
|
-
* remove --force` atravessam o link. Já aconteceu: um `worktree remove --force`
|
|
522
|
-
* atravessou a junction e apagou o `node_modules` da raiz.
|
|
523
|
-
*/
|
|
359
|
+
// A junction/symlink in the worktree points at content in the ROOT, and `rm -rf` or
|
|
360
|
+
// `git worktree remove --force` traverse it.
|
|
524
361
|
export const REPARSE_WARNING =
|
|
525
362
|
'estes são junctions/symlinks: apagá-los fora do morph-spec apaga o conteúdo REAL na raiz';
|
|
526
363
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
* NÃO MUTA NADA. Nenhum commit, nenhum archive, nenhum worktree removido,
|
|
531
|
-
* nenhum `gh` que crie coisa alguma, nenhum `git fetch`. Ele responde, ANTES,
|
|
532
|
-
* as três perguntas que hoje só se respondem depois do estrago:
|
|
533
|
-
*
|
|
534
|
-
* 1. O que este `finish` vai COPIAR para o backup e o que vai DESCARTAR?
|
|
535
|
-
* (`classifyIgnoredLeftovers` — a MESMA função que o teardown consulta;
|
|
536
|
-
* um segundo enumerador aqui faria o pré-voo prometer o que o comando não
|
|
537
|
-
* cumpre, e um dry-run que mente é pior que nenhum.)
|
|
538
|
-
* 2. Que reparse points existem, e o que significa apagá-los por fora?
|
|
539
|
-
* (`findReparsePoints`, exaustivo e fail-closed.)
|
|
540
|
-
* 3. A base andou? — a pergunta INVERSA da de #89. Lá se mede "quantos
|
|
541
|
-
* commits EU tenho que o upstream não tem"; aqui, "quantos commits
|
|
542
|
-
* `origin/{default}` tem que a MINHA base não tem", que é o que decide se
|
|
543
|
-
* este merge vai ser trivial ou uma surpresa. Mesma função
|
|
544
|
-
* (`commitsAhead`), argumentos invertidos.
|
|
545
|
-
*
|
|
546
|
-
* `cwd` dentro do worktree é AVISO, não recusa: o caminho real continua
|
|
547
|
-
* recusando antes da primeira mutação (Validation 0b), e recusar aqui negaria
|
|
548
|
-
* ao usuário justamente a explicação que ele veio buscar — do lugar mais
|
|
549
|
-
* provável de estar quando a pede.
|
|
550
|
-
*
|
|
551
|
-
* SEM `git fetch` implícito: um fetch dentro de um comando que promete não
|
|
552
|
-
* mexer em nada é efeito colateral de rede. O pré-voo AVISA que o dado pode
|
|
553
|
-
* estar velho e sugere o fetch.
|
|
554
|
-
*
|
|
555
|
-
* @param {{ feature: string, root: string, worktreePath: string|null,
|
|
556
|
-
* cwd: string, mode: 'pr'|'merge', branch: string,
|
|
557
|
-
* options?: { discardIgnored?: boolean, keep?: boolean } }} args
|
|
558
|
-
* @returns {object} o payload emitido — nunca lança
|
|
559
|
-
*/
|
|
364
|
+
// NÃO MUTA NADA: nenhum commit, archive, remoção, `gh` que crie algo nem `git fetch` (efeito de
|
|
365
|
+
// rede). Usa as MESMAS funções do comando real, senão o pré-voo promete o que o comando não
|
|
366
|
+
// cumpre. `cwd` dentro do worktree é AVISO, não recusa. Nunca lança.
|
|
560
367
|
export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch, options = {} }) {
|
|
561
368
|
const warnings = [];
|
|
562
369
|
|
|
563
|
-
// ── A base andou? (vale COM e SEM worktree — UC-16 A1) ────────────────────
|
|
564
370
|
const defaultBranch = detectDefaultBranch(root);
|
|
565
371
|
const baseRef = defaultBranch ? resolveBaseRef(defaultBranch, root) : null;
|
|
566
|
-
// `commitsAhead
|
|
567
|
-
// feature como BASE e o baseRef como BRANCH, contamos o que a default tem e a
|
|
568
|
-
// feature não — exatamente a pergunta inversa.
|
|
372
|
+
// `commitsAhead` conta `base..branch`: invertidos, contam o que a default tem e a feature não.
|
|
569
373
|
const commits = baseRef && branchExists(branch, root)
|
|
570
374
|
? commitsAhead(branch, baseRef, root)
|
|
571
375
|
: null;
|
|
@@ -577,9 +381,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
577
381
|
mode,
|
|
578
382
|
branch,
|
|
579
383
|
baseAdvanced: { ref: baseRef, commits },
|
|
580
|
-
// `null` é DESCONHECIDO
|
|
581
|
-
// para perguntar" e "a default não andou" são fatos opostos, e confundi-los
|
|
582
|
-
// é como um `--dry-run` passa confiança que não tem.
|
|
384
|
+
// `null` é DESCONHECIDO, nunca 0: "não deu para perguntar" e "não andou" são fatos opostos.
|
|
583
385
|
baseAdvancedNote: commits === null
|
|
584
386
|
? (baseRef === null
|
|
585
387
|
? 'sem contraparte remota nem branch default resolvível — a contagem é DESCONHECIDA, não zero'
|
|
@@ -601,10 +403,8 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
601
403
|
|
|
602
404
|
if (defaultBranch) out.defaultBranch = defaultBranch;
|
|
603
405
|
|
|
604
|
-
// A retenção
|
|
605
|
-
//
|
|
606
|
-
// poda). A contagem é a de AGORA: o backup que este finish gravar ainda pode
|
|
607
|
-
// empurrar mais um para fora.
|
|
406
|
+
// A retenção automática também é mutação do finish real; a contagem é a de AGORA, antes do
|
|
407
|
+
// backup que este finish gravar.
|
|
608
408
|
try {
|
|
609
409
|
const retention = inspectFinishBackups(root);
|
|
610
410
|
if (retention.policy.skipped) {
|
|
@@ -625,7 +425,6 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
625
425
|
}
|
|
626
426
|
} catch { /* o pré-voo nunca lança */ }
|
|
627
427
|
|
|
628
|
-
// ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
|
|
629
428
|
if (worktreePath) {
|
|
630
429
|
out.worktreePath = worktreePath.replace(/\\/g, '/');
|
|
631
430
|
|
|
@@ -647,24 +446,17 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
647
446
|
);
|
|
648
447
|
}
|
|
649
448
|
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
// destruído sem backup — então prever com `options` vazio é prever OUTRO
|
|
653
|
-
// comando. Medido antes do conserto: o pré-voo prometia salvar caminhos que
|
|
654
|
-
// o `--discard-ignored` destruía. Normalizado com `=== true` para que
|
|
655
|
-
// `undefined` e `false` sejam a mesma coisa, como no `worktree remove`.
|
|
449
|
+
// As MESMAS flags do comando real: `--discard-ignored` inverte a classificação, então prever com
|
|
450
|
+
// `options` vazio é prever outro comando.
|
|
656
451
|
const discardIgnored = options.discardIgnored === true;
|
|
657
452
|
const ignored = classifyIgnoredLeftovers(worktreePath, root, { discardIgnored });
|
|
658
453
|
out.discardIgnored = discardIgnored;
|
|
659
454
|
out.ignored = {
|
|
660
|
-
//
|
|
661
|
-
// aqui, ou difere do que está na raiz. Com `--discard-ignored` esta lista
|
|
662
|
-
// sai VAZIA por construção — a flag significa "pule o backup".
|
|
455
|
+
// Copiado para o backup; vazio por construção sob `--discard-ignored`.
|
|
663
456
|
shieldable: ignored.shieldable,
|
|
664
457
|
// Idêntico byte a byte ao da raiz (SHA-256): apagar não perde nada.
|
|
665
458
|
replicable: ignored.replicable,
|
|
666
|
-
//
|
|
667
|
-
// descartado sem backup.
|
|
459
|
+
// Descartado sem backup.
|
|
668
460
|
discardable: ignored.discarded,
|
|
669
461
|
};
|
|
670
462
|
if (ignored.blocking.length) {
|
|
@@ -673,12 +465,8 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
673
465
|
}
|
|
674
466
|
|
|
675
467
|
if (discardIgnored) {
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
// aqui — desapareceria justamente no modo destrutivo. Perguntar à MESMA
|
|
679
|
-
// função, sem a flag, é a única forma de nomeá-los sem recriar a regra
|
|
680
|
-
// (uma segunda cópia da triagem é o defeito que o export de
|
|
681
|
-
// `classifyIgnoredLeftovers` existe para evitar).
|
|
468
|
+
// Sob a flag `shieldable` sai vazia: perguntar à MESMA função sem a flag nomeia o que existe só
|
|
469
|
+
// aqui, sem recriar a regra.
|
|
682
470
|
const semFlag = classifyIgnoredLeftovers(worktreePath, root);
|
|
683
471
|
out.ignored.discardableExclusive = semFlag.shieldable;
|
|
684
472
|
if (semFlag.shieldable.length) {
|
|
@@ -705,23 +493,17 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
705
493
|
return out;
|
|
706
494
|
}
|
|
707
495
|
|
|
708
|
-
// ── merge integration (local, no push) ───────────────────────────────────────
|
|
709
|
-
|
|
710
496
|
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
|
|
711
497
|
const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
|
|
712
498
|
const adrNote = adrIndexNote(arch);
|
|
713
499
|
|
|
714
500
|
if (isWorktreeFlow) {
|
|
715
|
-
//
|
|
716
|
-
// then guard the whole root before landing the merge commit.
|
|
501
|
+
// Shed this feature's root-side scaffold, then guard the whole root before the merge commit.
|
|
717
502
|
const { shedBackup } = shedRootFeatureScope(root, feature, { stamp: backupStamp });
|
|
718
503
|
const backupNote = shedBackup ? { shedBackup } : {};
|
|
719
504
|
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// a refusal HERE means the shed itself dirtied the root — a bug signal, not
|
|
723
|
-
// user error. Kept because this is the last moment before we mutate the
|
|
724
|
-
// default branch, and it is nearly free.
|
|
505
|
+
// The pre-archive check already refused unrelated dirt, so a refusal here means the shed
|
|
506
|
+
// dirtied the root: a bug signal. Last moment before mutating the default branch.
|
|
725
507
|
const rootClean = primaryRootUnexplainedDirt(root, feature);
|
|
726
508
|
if (!rootClean.ok) {
|
|
727
509
|
emit({ finished: false, error: 'primary-root-status-failed', feature, ...backupNote });
|
|
@@ -753,14 +535,10 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
753
535
|
process.exit(0);
|
|
754
536
|
}
|
|
755
537
|
|
|
756
|
-
// The stack comes down BEFORE the worktree
|
|
757
|
-
// mount and the port block all belong to that tree, and once it is gone
|
|
758
|
-
// there is nothing left to address the containers by.
|
|
538
|
+
// The stack comes down BEFORE the worktree, which holds its compose file, bind mount and ports.
|
|
759
539
|
const e2e = reclaimStack(feature, { root, worktreePath, options });
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
// kill every hook of that session. The integration is done; only the
|
|
763
|
-
// teardown waits (see sessionAnchoredInWorktree).
|
|
540
|
+
// A session ANCHORED in the worktree resolves its hooks through it: tearing it down now would
|
|
541
|
+
// kill every hook of that session, so only the teardown waits.
|
|
764
542
|
const deferred = sessionAnchoredInWorktree(worktreePath);
|
|
765
543
|
const teardown = deferred
|
|
766
544
|
? null
|
|
@@ -770,8 +548,6 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
770
548
|
process.exit(0);
|
|
771
549
|
}
|
|
772
550
|
|
|
773
|
-
// COMMON flow: we are on morph/{f} in the root, archive committed. Move to the
|
|
774
|
-
// default and merge the feature branch in.
|
|
775
551
|
try {
|
|
776
552
|
execFileSync('git', ['checkout', defaultBranch], { cwd: root, stdio: 'pipe' });
|
|
777
553
|
} catch (err) {
|
|
@@ -786,8 +562,6 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
786
562
|
finished: false,
|
|
787
563
|
error: 'merge-conflict',
|
|
788
564
|
output: gitErr(err),
|
|
789
|
-
// The abort left the working dir on the default WITHOUT the feature — say
|
|
790
|
-
// where the intact work is so the user can recover it.
|
|
791
565
|
hint: `conflito ao mergear ${expectedBranch} em ${defaultBranch}. Seu trabalho está intacto em ${expectedBranch} — rode \`git checkout ${expectedBranch}\` para voltar a ele`,
|
|
792
566
|
feature,
|
|
793
567
|
});
|
|
@@ -796,29 +570,16 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
796
570
|
tryGit(['branch', '-d', expectedBranch], root);
|
|
797
571
|
const publish = publicationState({ root, defaultBranch, push: options.push });
|
|
798
572
|
const e2e = reclaimStack(feature, { root, worktreePath: null, options });
|
|
799
|
-
// The root index (gitignored) was already updated by the archive (same cwd).
|
|
800
573
|
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...adrNote, e2e });
|
|
801
574
|
process.exit(0);
|
|
802
575
|
}
|
|
803
576
|
|
|
804
|
-
|
|
805
|
-
* O número do PR a partir da url que o `gh pr create` imprime.
|
|
806
|
-
*
|
|
807
|
-
* Existe porque o número é o que um humano usa para falar do PR ("mergeia o
|
|
808
|
-
* #51"), e a url é a única coisa que o `gh` devolve. Um formato inesperado
|
|
809
|
-
* devolve `null` em vez de um palpite: o payload prefere admitir que não sabe
|
|
810
|
-
* o número a afirmar um errado.
|
|
811
|
-
*
|
|
812
|
-
* @param {string|null} url
|
|
813
|
-
* @returns {number|null}
|
|
814
|
-
*/
|
|
577
|
+
// `null` for an unexpected format: better to admit not knowing the number than state a wrong one.
|
|
815
578
|
export function prNumberFromUrl(url) {
|
|
816
579
|
const m = typeof url === 'string' ? url.match(/\/pull\/(\d+)(?:[/?#]|$)/) : null;
|
|
817
580
|
return m ? Number(m[1]) : null;
|
|
818
581
|
}
|
|
819
582
|
|
|
820
|
-
// ── PR integration (push + gh, no local merge) ───────────────────────────────
|
|
821
|
-
|
|
822
583
|
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp }) {
|
|
823
584
|
try {
|
|
824
585
|
execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
|
|
@@ -836,37 +597,27 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
836
597
|
process.exit(1);
|
|
837
598
|
}
|
|
838
599
|
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
// feature yet — honest until the PR merges).
|
|
600
|
+
// The worktree flow keeps the branch (the open PR needs it); the common flow stays on morph/{f},
|
|
601
|
+
// since the default has no feature until the PR merges.
|
|
842
602
|
let teardown = null;
|
|
843
603
|
let shedBackup = null;
|
|
844
|
-
//
|
|
845
|
-
// --keep, where the worktree (and its stack) stays alive on purpose.
|
|
604
|
+
// Skipped on --keep, where the worktree and its stack stay alive on purpose.
|
|
846
605
|
const e2e = isWorktreeFlow
|
|
847
606
|
? (options.keep ? null : reclaimStack(feature, { root, worktreePath, options }))
|
|
848
607
|
: reclaimStack(feature, { root, worktreePath: null, options });
|
|
849
608
|
let teardownDeferred = false;
|
|
850
609
|
if (isWorktreeFlow && !options.keep) {
|
|
851
|
-
// Shed
|
|
852
|
-
//
|
|
853
|
-
// UNTRACKED; without this, teardown's clearRootWorktreeRegistration would
|
|
854
|
-
// `git add` + commit that scaffold onto the default branch. Then when the PR
|
|
855
|
-
// later merges (carrying features/{f} → archive/{f}), the default would hold
|
|
856
|
-
// the feature in BOTH features/ and archive/ at once — active and archived.
|
|
857
|
-
// The branch/PR owns the authoritative copy; the root scaffold is transient.
|
|
610
|
+
// Shed BEFORE teardown: otherwise clearRootWorktreeRegistration commits the untracked root
|
|
611
|
+
// scaffold to the default, and the merged PR leaves the feature in both features/ and archive/.
|
|
858
612
|
shedBackup = shedRootFeatureScope(root, feature, { stamp: backupStamp }).shedBackup;
|
|
859
|
-
// Same deferral as --merge
|
|
860
|
-
// session is anchored in (see sessionAnchoredInWorktree).
|
|
613
|
+
// Same deferral as --merge (sessionAnchoredInWorktree).
|
|
861
614
|
if (sessionAnchoredInWorktree(worktreePath)) {
|
|
862
615
|
teardownDeferred = true;
|
|
863
616
|
} else {
|
|
864
617
|
teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false, stamp: backupStamp });
|
|
865
618
|
}
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
// the ROOT index too — exactly as --merge does — or `doctor`'s lifecycle audit
|
|
869
|
-
// flags a `stale-index` phantom (index lists {f}, no folder on disk).
|
|
619
|
+
// runArchive dropped the feature only from the WORKTREE index: drop it from the root's too, or
|
|
620
|
+
// `doctor` flags a `stale-index` phantom.
|
|
870
621
|
dropFromRootIndex(root, feature);
|
|
871
622
|
}
|
|
872
623
|
|
|
@@ -875,10 +626,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
875
626
|
|
|
876
627
|
const out = {
|
|
877
628
|
finished: true,
|
|
878
|
-
// O comando terminou
|
|
879
|
-
// payload antigo, e é dessa colisão que sai o "shipped" para uma feature
|
|
880
|
-
// cujo PR seguia aberto: quem lê `finished: true` sem mais nada conclui
|
|
881
|
-
// que acabou. A default só recebe archive/{f} quando o PR mergear.
|
|
629
|
+
// O comando terminou, a INTEGRAÇÃO não: a default só recebe archive/{f} quando o PR mergear.
|
|
882
630
|
integrated: false,
|
|
883
631
|
awaiting: 'merge',
|
|
884
632
|
mode: 'pr',
|
|
@@ -894,8 +642,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
894
642
|
? { worktreeRemoved: teardown.worktreeRemoved, ...teardownExtras(teardown, worktreePath, expectedBranch, false) }
|
|
895
643
|
: {
|
|
896
644
|
worktreeRemoved: false,
|
|
897
|
-
//
|
|
898
|
-
// with a worktree still alive — the common flow has none to report.
|
|
645
|
+
// Only --keep leaves `teardown` null with a worktree still alive.
|
|
899
646
|
...(isWorktreeFlow && options.keep
|
|
900
647
|
? {
|
|
901
648
|
worktree: { path: worktreePath, branch: expectedBranch },
|
|
@@ -914,12 +661,6 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
914
661
|
process.exit(0);
|
|
915
662
|
}
|
|
916
663
|
|
|
917
|
-
// ── output helpers ────────────────────────────────────────────────────────
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* What the archive did to the ADR index (`adrIndex`) and the revocations it
|
|
921
|
-
* could not apply (`revocationProblems`); both absent when there is nothing to report.
|
|
922
|
-
*/
|
|
923
664
|
function adrIndexNote(arch) {
|
|
924
665
|
return {
|
|
925
666
|
...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
|
|
@@ -927,34 +668,13 @@ function adrIndexNote(arch) {
|
|
|
927
668
|
};
|
|
928
669
|
}
|
|
929
670
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
* o `resolveBaseRef` do branch-classifier assumem. Um nome só, aqui, para que o
|
|
933
|
-
* comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais possam
|
|
934
|
-
* nomear remotos diferentes.
|
|
935
|
-
*/
|
|
671
|
+
// Um nome só, para que o comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais
|
|
672
|
+
// nomeiem remotos diferentes.
|
|
936
673
|
const PUSH_REMOTE = 'origin';
|
|
937
674
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
*
|
|
942
|
-
* A contagem REUSA `resolveBaseRef` + `commitsAhead` (branch-classifier), em vez
|
|
943
|
-
* de uma terceira cópia de `rev-list --count`. Detalhe que decide o P4:
|
|
944
|
-
* `resolveBaseRef` degrada para a branch LOCAL quando não existe ref de
|
|
945
|
-
* rastreamento remoto — comparar a branch consigo mesma devolveria um `0` de
|
|
946
|
-
* aparência honesta para um repo que nunca deu push. Por isso a comparação só
|
|
947
|
-
* vale quando o ref resolvido é diferente do nome da branch; caso contrário
|
|
948
|
-
* `ahead` fica `null` ("incomparável"), nunca zero.
|
|
949
|
-
*
|
|
950
|
-
* Fail-open por desenho: o merge JÁ aconteceu quando esta função roda, então
|
|
951
|
-
* nenhum resultado daqui — nem um push que falhou — pode transformar um sucesso
|
|
952
|
-
* em exit 1. A falha vira `pushError` no payload.
|
|
953
|
-
*
|
|
954
|
-
* @param {{ root: string, defaultBranch: string, push?: boolean }} ctx
|
|
955
|
-
* @returns {{ remote: string, branch: string, ahead: number|null, pushed: boolean,
|
|
956
|
-
* published?: number|null, pushError?: string }}
|
|
957
|
-
*/
|
|
675
|
+
// `resolveBaseRef` degrada para a branch LOCAL sem ref remoto: comparada consigo mesma daria um
|
|
676
|
+
// `0` falso, então `ahead` fica `null`. Fail-open: o merge JÁ aconteceu, e um push que falha
|
|
677
|
+
// vira `pushError`, nunca exit 1.
|
|
958
678
|
function publicationState({ root, defaultBranch, push }) {
|
|
959
679
|
const baseRef = resolveBaseRef(defaultBranch, root);
|
|
960
680
|
const tracked = !!baseRef && baseRef !== defaultBranch;
|
|
@@ -978,19 +698,8 @@ function publicationState({ root, defaultBranch, push }) {
|
|
|
978
698
|
return { ...state, published: ahead, ahead: remaining, pushed: true };
|
|
979
699
|
}
|
|
980
700
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
*
|
|
984
|
-
* O comando se chama `finish`, e no modo `--merge` ele NÃO publica nada por
|
|
985
|
-
* conta própria: o merge é local, o remoto continua sem a feature. Quem lê
|
|
986
|
-
* "finished: true" conclui o contrário — foi pegadinha repetida em sessões
|
|
987
|
-
* diferentes. Três saídas de `--merge` montavam esse aviso por conta própria, e
|
|
988
|
-
* uma delas (o `--keep`) simplesmente esquecia dele; um produtor único é o que
|
|
989
|
-
* impede a divergência de voltar.
|
|
990
|
-
*
|
|
991
|
-
* @param {ReturnType<typeof publicationState>} publish
|
|
992
|
-
* @returns {string}
|
|
993
|
-
*/
|
|
701
|
+
// PRODUTOR ÚNICO: no `--merge` o merge é local e o remoto continua sem a feature; cada saída
|
|
702
|
+
// que montava o aviso por conta própria podia esquecê-lo.
|
|
994
703
|
function mergeNextStep(publish) {
|
|
995
704
|
const { remote, branch, ahead, pushed, published, pushError } = publish;
|
|
996
705
|
const pushCmd = `\`git push ${remote} ${branch}\``;
|
|
@@ -1005,25 +714,15 @@ function mergeNextStep(publish) {
|
|
|
1005
714
|
if (pushError) {
|
|
1006
715
|
return `integração LOCAL — o push pedido por --push FALHOU e nada foi publicado. Rode ${pushCmd} manualmente.`;
|
|
1007
716
|
}
|
|
1008
|
-
// Sem contraparte remota não se inventa contagem
|
|
717
|
+
// Sem contraparte remota não se inventa contagem: fica a frase estática.
|
|
1009
718
|
if (ahead === null) {
|
|
1010
719
|
return 'integração LOCAL — nada foi publicado. Rode `git push` para levar ao remoto.';
|
|
1011
720
|
}
|
|
1012
721
|
return `integração LOCAL — nada foi publicado: ${branch} está ${ahead} commit(s) à frente de ${remote}/${branch}. Rode ${pushCmd} para levar ao remoto (ou re-execute o finish com --push).`;
|
|
1013
722
|
}
|
|
1014
723
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
* normal, worktree `--keep` e fluxo comum) passam por aqui — era a terceira
|
|
1018
|
-
* cópia, montada à mão no `--keep`, que saía sem `nextStep` nenhum.
|
|
1019
|
-
*
|
|
1020
|
-
* `teardown === null` cobre os dois casos sem desmonte: o `--keep` (worktree
|
|
1021
|
-
* vivo de propósito) e o fluxo comum (não há worktree algum).
|
|
1022
|
-
*
|
|
1023
|
-
* @param {{ feature: string, branch: string, defaultBranch: string,
|
|
1024
|
-
* publish: ReturnType<typeof publicationState>, worktreePath?: string|null,
|
|
1025
|
-
* teardown?: object|null, shedBackup?: string|null, kept?: boolean }} args
|
|
1026
|
-
*/
|
|
724
|
+
// CONSTRUTOR ÚNICO das três saídas de sucesso do `--merge`. `teardown === null` cobre o `--keep`
|
|
725
|
+
// e o fluxo comum.
|
|
1027
726
|
function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = null, teardown = null, shedBackup = null, kept = false, deferred = false }) {
|
|
1028
727
|
return {
|
|
1029
728
|
finished: true,
|
|
@@ -1040,9 +739,7 @@ function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = n
|
|
|
1040
739
|
...(shedBackup ? { shedBackup } : {}),
|
|
1041
740
|
...(kept
|
|
1042
741
|
? {
|
|
1043
|
-
//
|
|
1044
|
-
// VALID (it still names a live directory), so nothing to clean up here —
|
|
1045
|
-
// only report where it lives and how to close it later.
|
|
742
|
+
// The root registration stays VALID (it names a live directory): nothing to clean up.
|
|
1046
743
|
worktree: { path: worktreePath, branch },
|
|
1047
744
|
hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
|
|
1048
745
|
}
|
|
@@ -1051,12 +748,7 @@ function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = n
|
|
|
1051
748
|
};
|
|
1052
749
|
}
|
|
1053
750
|
|
|
1054
|
-
|
|
1055
|
-
* Hint for a teardown deferred because the session is anchored in the worktree
|
|
1056
|
-
* (both modes). Names the safe command, never `git worktree remove`. `--merge`
|
|
1057
|
-
* would have deleted the (now integrated) branch, so its hint carries
|
|
1058
|
-
* `--delete-branch`; `--pr` keeps the branch — the PR needs it.
|
|
1059
|
-
*/
|
|
751
|
+
// Names the safe command, never `git worktree remove`. `--pr` keeps the branch: the PR needs it.
|
|
1060
752
|
function deferredTeardownHint(feature, worktreePath, { deleteBranch = false } = {}) {
|
|
1061
753
|
return `worktree mantido em ${worktreePath}: a sessão do Claude Code que rodou o finish está ancorada nele, `
|
|
1062
754
|
+ 'e desmontá-lo agora quebraria todos os hooks dela (eles resolvem `.morph/framework` por ele) — '
|
|
@@ -1064,19 +756,14 @@ function deferredTeardownHint(feature, worktreePath, { deleteBranch = false } =
|
|
|
1064
756
|
+ 'de uma sessão aberta na raiz do repo.';
|
|
1065
757
|
}
|
|
1066
758
|
|
|
1067
|
-
/** Shared teardown reporting for both modes (kept identical to the old finish). */
|
|
1068
759
|
function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
|
|
1069
760
|
const out = {};
|
|
1070
761
|
if (teardown.ignoredBackup) out.worktreeIgnoredBackup = teardown.ignoredBackup;
|
|
1071
762
|
if (teardown.worktreeRemoved && teardown.discarded.length) out.discardedIgnored = teardown.discarded;
|
|
1072
|
-
//
|
|
1073
|
-
// the identical bytes. Reported on both paths: after a removal these two lists
|
|
1074
|
-
// are the whole answer to "did I just lose something?", and the user should
|
|
1075
|
-
// not have to open the backup directory to find out.
|
|
763
|
+
// After a removal these two lists answer "did I just lose something?".
|
|
1076
764
|
if (teardown.shieldable?.length) out.worktreeIgnoredBackedUp = teardown.shieldable;
|
|
1077
765
|
if (teardown.replicable?.length) out.replicatedInRoot = teardown.replicable;
|
|
1078
|
-
//
|
|
1079
|
-
// proof the primary root's real `.claude/` was unlinked, never followed.
|
|
766
|
+
// Proof the primary root's real `.claude/` was unlinked, never followed.
|
|
1080
767
|
if (teardown.unlinked?.length) out.unlinked = teardown.unlinked;
|
|
1081
768
|
if (!teardown.worktreeRemoved) {
|
|
1082
769
|
out.path = worktreePath;
|
|
@@ -1084,16 +771,8 @@ function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
|
|
|
1084
771
|
if (teardown.blockers.length) out.reparsePoints = teardown.blockers;
|
|
1085
772
|
if (teardown.ignoredBlocking.length) out.ignoredFiles = teardown.ignoredBlocking;
|
|
1086
773
|
out.cleanupHint = teardown.declineReason;
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
// non-removal passes through a moment with the infra loose. What happens
|
|
1090
|
-
// next depends on WHY, and the two must not be reported alike:
|
|
1091
|
-
// REFUSED — we proved removal unsafe, the user keeps working here, so the
|
|
1092
|
-
// teardown re-links (A4).
|
|
1093
|
-
// FAILED — removal was the intent and a lock got in the way. The infra
|
|
1094
|
-
// stays down ON PURPOSE, so the retry (or anything pasted from an old
|
|
1095
|
-
// transcript) has nothing to traverse. Say so explicitly, or the user
|
|
1096
|
-
// reads a bare "failed" and reaches for the destructive command.
|
|
774
|
+
// REFUSED re-links (the user keeps working here); FAILED leaves the infra down ON PURPOSE so a
|
|
775
|
+
// retry has nothing to traverse. Say which, or a bare "failed" invites the destructive command.
|
|
1097
776
|
out.relinked = teardown.relinked === true;
|
|
1098
777
|
if (!teardown.refused) {
|
|
1099
778
|
if (teardown.infraLeftUnlinked?.length) out.infraLeftUnlinked = teardown.infraLeftUnlinked;
|
|
@@ -1107,8 +786,6 @@ function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
|
|
|
1107
786
|
return out;
|
|
1108
787
|
}
|
|
1109
788
|
|
|
1110
|
-
// ── small git helpers ────────────────────────────────────────────────────────
|
|
1111
|
-
|
|
1112
789
|
function currentBranch(cwd) {
|
|
1113
790
|
try {
|
|
1114
791
|
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
@@ -1143,27 +820,8 @@ function gitErr(err) {
|
|
|
1143
820
|
return (((err.stdout && err.stdout.toString()) || '') + ((err.stderr && err.stderr.toString()) || '')).slice(0, 2000) || (err.message || '').slice(0, 500);
|
|
1144
821
|
}
|
|
1145
822
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
* the `feature-not-found` payload.
|
|
1149
|
-
*
|
|
1150
|
-
* Exists because "not found" is the least useful thing we can say about a
|
|
1151
|
-
* feature the user just worked on. The two real cases:
|
|
1152
|
-
*
|
|
1153
|
-
* - ARCHIVED: an earlier finish archived it and then failed to integrate.
|
|
1154
|
-
* What is missing is the merge, not the whole finish — say so, because the
|
|
1155
|
-
* user's instinct is to re-run `finish` and get refused again.
|
|
1156
|
-
* - ON ITS BRANCH: the feature was created on `morph/{f}` and the command is
|
|
1157
|
-
* running from the default branch, where `.morph/features/{f}` does not
|
|
1158
|
-
* exist yet. Naming the branch turns a dead end into one checkout.
|
|
1159
|
-
*
|
|
1160
|
-
* Fail-open in every direction: a hint that cannot be computed is simply absent.
|
|
1161
|
-
*
|
|
1162
|
-
* @param {string} featureCwd - the tree we looked in
|
|
1163
|
-
* @param {string} root - the primary root
|
|
1164
|
-
* @param {string} feature
|
|
1165
|
-
* @returns {{ archived?: string, hint?: string, onBranch?: string }}
|
|
1166
|
-
*/
|
|
823
|
+
// Where a feature not in `features/` went: ARCHIVED (only the merge is missing) or on its
|
|
824
|
+
// `morph/{f}` branch. Fail-open: a hint that cannot be computed is absent.
|
|
1167
825
|
function featureElsewhereHint(featureCwd, root, feature) {
|
|
1168
826
|
try {
|
|
1169
827
|
const archived = join(featureCwd, '.morph', 'archive', feature);
|
|
@@ -1189,12 +847,6 @@ function featureElsewhereHint(featureCwd, root, feature) {
|
|
|
1189
847
|
return { hint: `'${feature}' não existe em .morph/features/ nesta árvore. Confira o nome com \`morph-spec list --all\`.` };
|
|
1190
848
|
}
|
|
1191
849
|
|
|
1192
|
-
/**
|
|
1193
|
-
* @param {string} cwd
|
|
1194
|
-
* @param {string} feature
|
|
1195
|
-
* @param {'features'|'archive'} [bucket] - `archive` on a resume, where the
|
|
1196
|
-
* feature.json has already been moved (and stamped `completed`).
|
|
1197
|
-
*/
|
|
1198
850
|
function readFeatureJson(cwd, feature, bucket = 'features') {
|
|
1199
851
|
try {
|
|
1200
852
|
return JSON.parse(readFileSync(join(cwd, '.morph', bucket, feature, 'feature.json'), 'utf8'));
|
|
@@ -1204,19 +856,8 @@ function readFeatureJson(cwd, feature, bucket = 'features') {
|
|
|
1204
856
|
}
|
|
1205
857
|
|
|
1206
858
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
* Three-valued ON PURPOSE. `git merge-base --is-ancestor` answers by exit code,
|
|
1211
|
-
* and the two failure modes mean opposite things: **1** is a real "no, not
|
|
1212
|
-
* merged", while **128** is "one of these refs does not resolve" and proves
|
|
1213
|
-
* nothing at all. Collapsing them to a boolean would read an unresolvable ref as
|
|
1214
|
-
* "not merged" and let a resume re-merge a completed feature.
|
|
1215
|
-
*
|
|
1216
|
-
* @param {string} branch
|
|
1217
|
-
* @param {string} target
|
|
1218
|
-
* @param {string} cwd
|
|
1219
|
-
* @returns {boolean|null} null when the comparison could not be made
|
|
859
|
+
* Three-valued ON PURPOSE: `merge-base --is-ancestor` exit 1 is "not merged", 128 is "a ref does
|
|
860
|
+
* not resolve". `null` when the comparison could not be made.
|
|
1220
861
|
*/
|
|
1221
862
|
export function branchMergedInto(branch, target, cwd) {
|
|
1222
863
|
try {
|
|
@@ -1227,20 +868,8 @@ export function branchMergedInto(branch, target, cwd) {
|
|
|
1227
868
|
}
|
|
1228
869
|
}
|
|
1229
870
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
* "already integrated" — a completed `--pr` leaves the branch unmerged on
|
|
1233
|
-
* purpose, so `branchMergedInto` cannot see it.
|
|
1234
|
-
*
|
|
1235
|
-
* Three-valued for the same reason: an unanswerable query (no `gh`, no auth, no
|
|
1236
|
-
* network) is not evidence of "no PR", and treating it as such would resume
|
|
1237
|
-
* into a duplicate `gh pr create`.
|
|
1238
|
-
*
|
|
1239
|
-
* @param {string} branch
|
|
1240
|
-
* @param {string} cwd
|
|
1241
|
-
* @returns {string|boolean|null} the PR url, `true` (open PR, url unavailable),
|
|
1242
|
-
* `false` (none open), or `null` when the question could not be asked
|
|
1243
|
-
*/
|
|
871
|
+
// Three-valued: an unanswerable query (no `gh`, auth or network) is not evidence of "no PR".
|
|
872
|
+
// Returns the PR url, `true` (open, url unknown), `false` (none) or `null`.
|
|
1244
873
|
function openPrFor(branch, cwd) {
|
|
1245
874
|
const { bin, prefix } = resolveGh();
|
|
1246
875
|
try {
|
|
@@ -1255,13 +884,7 @@ function openPrFor(branch, cwd) {
|
|
|
1255
884
|
}
|
|
1256
885
|
}
|
|
1257
886
|
|
|
1258
|
-
|
|
1259
|
-
* Reclaim the feature's verification stack (containers, ports, named volumes,
|
|
1260
|
-
* freshness stamp) from the tree that owns it. `--keep-volumes` keeps the
|
|
1261
|
-
* named volumes (e.g. a seeded database) for the next feature. Best-effort by
|
|
1262
|
-
* contract — a stack that refuses to come down must never fail a finish that
|
|
1263
|
-
* already merged, so this never throws.
|
|
1264
|
-
*/
|
|
887
|
+
// Never throws: a stack that refuses to come down must not fail a finish that already merged.
|
|
1265
888
|
function reclaimStack(feature, { root, worktreePath, options }) {
|
|
1266
889
|
try {
|
|
1267
890
|
return tearDownFeatureStack(feature, { root, worktreePath, volumes: !options.keepVolumes });
|
|
@@ -1270,7 +893,7 @@ function reclaimStack(feature, { root, worktreePath, options }) {
|
|
|
1270
893
|
}
|
|
1271
894
|
}
|
|
1272
895
|
|
|
1273
|
-
|
|
896
|
+
// Never via saveState, which would re-materialize the feature folder.
|
|
1274
897
|
function dropFromRootIndex(root, feature) {
|
|
1275
898
|
const rootPath = join(root, '.morph', 'state.json');
|
|
1276
899
|
if (!existsSync(rootPath)) return;
|