@polymorphism-tech/morph-spec 8.29.0 → 8.32.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 +11 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +67 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -1
- package/framework/MORPH.md +4 -3
- package/framework/agents.json +6 -6
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +68 -45
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +39 -4
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +14 -4
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +12 -4
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/branch.js +170 -0
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/lifecycle-audit.js +127 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +40 -1
- package/src/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- package/src/utils/skills-installer.js +187 -187
|
@@ -55,14 +55,21 @@ export function fleetCommand(options = {}) {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
console.log(chalk.white.bold(
|
|
58
|
-
|
|
58
|
+
// PHASE at 18: `aguardando-merge` (the pr-pending synthesized phase, see
|
|
59
|
+
// fleet-scanner.js) is 16 chars — 1 wider than the col width `list.js`
|
|
60
|
+
// already uses (17) for the identical string. A narrower column here
|
|
61
|
+
// would misalign every column after it just for that one row.
|
|
62
|
+
col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 18) + col('TASKS', 8)
|
|
59
63
|
+ col('GATES PEND.', 20) + col('WHERE', 24) + col('REMOTO', 16) + col('CLAIM', 16)
|
|
60
64
|
+ col('PORTS', 12) + col('HANDOFF', 9) + 'ATIVIDADE',
|
|
61
65
|
));
|
|
62
66
|
for (const e of fleet.entries) {
|
|
63
67
|
const tasks = e.tasks ? `${e.tasks.done}/${e.tasks.total}` : '-';
|
|
64
68
|
const gates = e.pendingGates.length ? e.pendingGates.join(',') : '-';
|
|
65
|
-
|
|
69
|
+
// pr-pending has no worktree and isn't root-resident either — its branch
|
|
70
|
+
// is the only place it exists, so show that instead of the misleading
|
|
71
|
+
// 'raiz' a location-blind check would print.
|
|
72
|
+
const where = (e.location === 'worktree' || e.location === 'pr-pending') ? e.branch : 'raiz';
|
|
66
73
|
const ports = e.env ? `${e.env.range[0]}-${e.env.range[1]}` : '-';
|
|
67
74
|
const handoff = e.handoff?.exists ? 'sim' : '-';
|
|
68
75
|
const activity = [
|
|
@@ -73,7 +80,7 @@ export function fleetCommand(options = {}) {
|
|
|
73
80
|
// length and append the padding by hand.
|
|
74
81
|
const claimPlain = e.claim ? `${e.claim.live ? 'sessão' : 'morta'} (${claimAge(e.claim)})` : '-';
|
|
75
82
|
console.log(
|
|
76
|
-
col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase,
|
|
83
|
+
col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 18) + col(tasks, 8)
|
|
77
84
|
+ col(gates, 20) + col(where, 24) + col(remoteCell(e.remote), 16)
|
|
78
85
|
+ claimCell(e.claim) + ' '.repeat(Math.max(1, 16 - claimPlain.length))
|
|
79
86
|
+ col(ports, 12) + col(handoff, 9) + activity,
|
|
@@ -35,6 +35,7 @@ import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, append
|
|
|
35
35
|
import { join, resolve, dirname } from 'path';
|
|
36
36
|
import chalk from 'chalk';
|
|
37
37
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
38
|
+
import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
|
|
38
39
|
// The reparse scan lives in hooks/shared because the PreToolUse guardrail needs
|
|
39
40
|
// the SAME answer this file does, and hooks can never import from `src/`.
|
|
40
41
|
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
@@ -767,23 +768,6 @@ export function unlinkWorktreeInfraLinks(worktreePath, mainCwd) {
|
|
|
767
768
|
*/
|
|
768
769
|
const IGNORED_SHIELDABLE_PREFIXES = ['.morph/'];
|
|
769
770
|
|
|
770
|
-
/**
|
|
771
|
-
* Well-known, regenerable build artifacts — `bin/`, `obj/` (dotnet), a real
|
|
772
|
-
* `node_modules` left behind outside the setup-created junction, and
|
|
773
|
-
* `*.tsbuildinfo` (tsc incremental cache). These are the everyday leftovers of
|
|
774
|
-
* running a build/test cycle inside a worktree, not user data: they are fully
|
|
775
|
-
* reproducible from source, and requiring `--discard-ignored` for every finish
|
|
776
|
-
* that ever ran a build turns the flag from "explicit consent for a real risk"
|
|
777
|
-
* into a rubber stamp nobody reads anymore. Auto-discarding them (like any
|
|
778
|
-
* `discarded` entry, WITHOUT a backup — see the note below) is safe precisely
|
|
779
|
-
* because "regenerable" is a stronger guarantee than "backed up".
|
|
780
|
-
*
|
|
781
|
-
* Deliberately narrow — do NOT add speculative patterns (`dist/`, `.next/`)
|
|
782
|
-
* beyond what's proven to be pure noise; anything else stays in `blocking` by
|
|
783
|
-
* default, requiring the explicit `--discard-ignored` consent.
|
|
784
|
-
*
|
|
785
|
-
* @param {string} p - forward-slash relative path from `git status --ignored`
|
|
786
|
-
*/
|
|
787
771
|
/**
|
|
788
772
|
* Whether a dirty root-side path is EXACTLY a sibling feature's live worktree
|
|
789
773
|
* registration — see the call site (primaryRootUnexplainedDirt, the root-clean
|
|
@@ -811,16 +795,9 @@ function isLiveSiblingWorktreeRegistration(relPath, root, feature) {
|
|
|
811
795
|
}
|
|
812
796
|
}
|
|
813
797
|
|
|
814
|
-
function isDisposableBuildArtifact(p) {
|
|
815
|
-
if (/\.tsbuildinfo$/.test(p)) return true;
|
|
816
|
-
const segments = p.split('/').filter(Boolean);
|
|
817
|
-
return segments.some((s) => s === 'bin' || s === 'obj' || s === 'node_modules');
|
|
818
|
-
}
|
|
819
|
-
|
|
820
798
|
/**
|
|
821
|
-
* Split the worktree's GITIGNORED leftovers into what this command may
|
|
822
|
-
*
|
|
823
|
-
* one) and what it must refuse to destroy.
|
|
799
|
+
* Split the worktree's GITIGNORED leftovers into what this command may delete
|
|
800
|
+
* outright, what it must COPY OUT first, and what it cannot even enumerate.
|
|
824
801
|
*
|
|
825
802
|
* Why this exists: `git worktree remove` deletes gitignored files outright, and
|
|
826
803
|
* the clean-check that gates it uses `git status --porcelain`, which omits them
|
|
@@ -829,14 +806,33 @@ function isDisposableBuildArtifact(p) {
|
|
|
829
806
|
* disposable; morph must not, because "not in git" is exactly the property that
|
|
830
807
|
* makes a file unrecoverable.
|
|
831
808
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
809
|
+
* The buckets, and why refusing is no longer one of them: the earlier version
|
|
810
|
+
* blocked on ANY unrecognised gitignored path, which in practice meant blocking
|
|
811
|
+
* on `.env` files that were byte-identical to the primary root's — nothing was
|
|
812
|
+
* at stake, and the only escape offered (`--discard-ignored`) also destroyed the
|
|
813
|
+
* genuinely unique `keys/` next to them. `triageIgnoredPaths` answers the
|
|
814
|
+
* question that actually decides whether bytes are lost (see ignored-triage.js),
|
|
815
|
+
* so the command can PROCEED in every case, backing up only what exists nowhere
|
|
816
|
+
* else. A refusal survives for exactly one reason now: the backup FAILED.
|
|
817
|
+
*
|
|
818
|
+
* shieldable — morph's own bookkeeping + anything EXCLUSIVE to this worktree.
|
|
819
|
+
* Copied to `finish-backup` before the removal; a failed copy is
|
|
820
|
+
* what declines the removal.
|
|
821
|
+
* discarded — build artifacts (regenerable) and REPLICABLE files (the root
|
|
822
|
+
* holds the same path with the same SHA-256). Deleted without a
|
|
823
|
+
* backup because neither loses bytes.
|
|
824
|
+
* blocking — the fail-CLOSED sentinel only: `git status --ignored` itself
|
|
825
|
+
* failed, so we cannot enumerate what is at stake.
|
|
826
|
+
*
|
|
827
|
+
* `--discard-ignored` no longer means "destroy the unique ones too, blind"; it
|
|
828
|
+
* means SKIP THE BACKUP — the same removal, without the copy.
|
|
834
829
|
*
|
|
835
830
|
* @param {string} worktreePath
|
|
831
|
+
* @param {string} rootPath - the primary root, for the replica comparison
|
|
836
832
|
* @param {{ discardIgnored?: boolean }} options
|
|
837
|
-
* @returns {{ shieldable: string[], blocking: string[], discarded: string[] }}
|
|
833
|
+
* @returns {{ shieldable: string[], blocking: string[], discarded: string[], replicable: string[] }}
|
|
838
834
|
*/
|
|
839
|
-
function classifyIgnoredLeftovers(worktreePath, options = {}) {
|
|
835
|
+
function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
|
|
840
836
|
let out;
|
|
841
837
|
try {
|
|
842
838
|
// `-z` + execFile: NUL-delimited, no shell, no C-style quoting to undo —
|
|
@@ -850,30 +846,30 @@ function classifyIgnoredLeftovers(worktreePath, options = {}) {
|
|
|
850
846
|
// empirically, not assumed.
|
|
851
847
|
out = execFileSync('git', ['status', '--porcelain', '--ignored=matching', '-z'], { cwd: worktreePath, stdio: 'pipe' }).toString();
|
|
852
848
|
} catch {
|
|
853
|
-
return { shieldable: [], blocking: ['<git status --ignored falhou — não foi possível enumerar o que seria destruído>'], discarded: [] };
|
|
849
|
+
return { shieldable: [], blocking: ['<git status --ignored falhou — não foi possível enumerar o que seria destruído>'], discarded: [], replicable: [] };
|
|
854
850
|
}
|
|
855
851
|
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
const disposable = [];
|
|
852
|
+
const morphOwned = [];
|
|
853
|
+
const rest = [];
|
|
859
854
|
for (const record of out.split('\0')) {
|
|
860
855
|
if (!record || record.length < 4) continue;
|
|
861
856
|
if (record.slice(0, 2) !== '!!') continue; // only ignored entries; the rest is the clean-check's job
|
|
862
857
|
const p = record.slice(3).replace(/\\/g, '/');
|
|
863
|
-
if (IGNORED_SHIELDABLE_PREFIXES.some((prefix) => p.startsWith(prefix)))
|
|
864
|
-
else
|
|
865
|
-
|
|
858
|
+
if (IGNORED_SHIELDABLE_PREFIXES.some((prefix) => p.startsWith(prefix))) morphOwned.push(p);
|
|
859
|
+
else rest.push(p);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const { replicable, disposable, exclusive } = triageIgnoredPaths({ worktreePath, rootPath, paths: rest });
|
|
863
|
+
const shieldable = [...morphOwned, ...exclusive];
|
|
864
|
+
|
|
865
|
+
// Explicit consent = SKIP THE BACKUP, not "widen what dies". The same paths
|
|
866
|
+
// are removed either way; the flag only decides whether a copy is made first.
|
|
867
|
+
// The list is never thrown away — these are the bytes that exist nowhere
|
|
868
|
+
// else, and reporting them is the least this can do.
|
|
869
|
+
if (options.discardIgnored) {
|
|
870
|
+
return { shieldable: [], blocking: [], discarded: [...disposable, ...replicable, ...shieldable], replicable };
|
|
866
871
|
}
|
|
867
|
-
|
|
868
|
-
// thrown away. These are the files that exist nowhere else, and they are the
|
|
869
|
-
// ones about to die; reporting them is the least this can do. (Backing them
|
|
870
|
-
// up instead is not an option: this bucket is where arbitrarily large
|
|
871
|
-
// leftovers land, so a copy is unbounded. The user declared them
|
|
872
|
-
// disposable — they still get told.)
|
|
873
|
-
if (options.discardIgnored) return { shieldable, blocking: [], discarded: [...disposable, ...blockingRest] };
|
|
874
|
-
// Known build artifacts are discarded by default, no flag required — they
|
|
875
|
-
// are reproducible from source, unlike anything else in `blockingRest`.
|
|
876
|
-
return { shieldable, blocking: blockingRest, discarded: disposable };
|
|
872
|
+
return { shieldable, blocking: [], discarded: [...disposable, ...replicable], replicable };
|
|
877
873
|
}
|
|
878
874
|
|
|
879
875
|
/**
|
|
@@ -1177,7 +1173,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1177
1173
|
const unlinked = unlinkWorktreeInfraLinks(worktreePath, root);
|
|
1178
1174
|
|
|
1179
1175
|
const scan = findReparsePoints(worktreePath);
|
|
1180
|
-
const ignored = classifyIgnoredLeftovers(worktreePath, options);
|
|
1176
|
+
const ignored = classifyIgnoredLeftovers(worktreePath, root, options);
|
|
1181
1177
|
const blockers = [...scan.points, ...scan.unprovable];
|
|
1182
1178
|
|
|
1183
1179
|
// Back up ONLY once the removal is otherwise cleared: nothing is destroyed on
|
|
@@ -1257,14 +1253,22 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1257
1253
|
const removeCmd = (addressByTarget || !isValidFeatureName(feature))
|
|
1258
1254
|
? `morph-spec worktree remove --target "${worktreePath}"`
|
|
1259
1255
|
: `morph-spec worktree remove ${feature}`;
|
|
1256
|
+
const backupDir = join(root, '.git', 'morph-spec', 'finish-backup');
|
|
1257
|
+
const sample = (list) => `${list.slice(0, 5).join(', ')}${list.length > 5 ? `, +${list.length - 5}` : ''}`;
|
|
1260
1258
|
const declineReason = worktreeRemoved
|
|
1261
1259
|
? null
|
|
1262
1260
|
: ignored.blocking.length
|
|
1263
|
-
? `worktree NÃO removido:
|
|
1261
|
+
? `worktree NÃO removido: não foi possível enumerar os arquivos gitignored (${sample(ignored.blocking)}), então não dá para provar que a remoção não destrói nada. Rode \`git status --ignored\` dentro do worktree para ver o que está travando e tente de novo com \`${removeCmd}\``
|
|
1264
1262
|
: blockers.length
|
|
1265
1263
|
? `worktree NÃO removido: ainda contém link(s)/caminho(s) não verificáveis (${blockers.join(', ')}) que a remoção seguiria para fora, apagando o alvo na raiz. Remova-os e rode \`${removeCmd}\``
|
|
1266
1264
|
: ignored.shieldable.length && ignoredBackup === null
|
|
1267
|
-
|
|
1265
|
+
// The ONLY gitignored refusal left. The old hint was circular — it
|
|
1266
|
+
// said "salve o que importa e rode <the command that just failed>".
|
|
1267
|
+
// This one names WHAT is exclusive, what was replicable (so the
|
|
1268
|
+
// reader stops worrying about those), and WHERE the copy was going.
|
|
1269
|
+
? `worktree NÃO removido: o backup falhou. Estes arquivos existem só neste worktree e a remoção os apagaria: ${sample(ignored.shieldable)}. `
|
|
1270
|
+
+ (ignored.replicable.length ? `(${sample(ignored.replicable)} são idênticos aos da raiz — esses não são o problema.) ` : '')
|
|
1271
|
+
+ `O backup iria para \`${backupDir}\`; libere espaço/permissão ali e rode \`${removeCmd}\`, ou copie os arquivos você mesmo e rode \`${removeCmd} --discard-ignored\` para pular o backup`
|
|
1268
1272
|
: removalFailure?.kind === 'in-use'
|
|
1269
1273
|
? `worktree NÃO removido: o diretório está EM USO por outro processo (instância do Claude Code, VS Code, dev server ou terminal com cwd em "${worktreePath}"). A infra compartilhada ficou DESMONTADA de propósito — feche o que está usando o diretório e rode \`${removeCmd}\`. Se preferir continuar trabalhando nele, restaure a infra com \`morph-spec worktree link --all\``
|
|
1270
1274
|
: `worktree NÃO removido: a remoção falhou${removalFailure?.detail ? ` (${removalFailure.detail})` : ''}. A infra compartilhada ficou DESMONTADA de propósito — rode \`${removeCmd}\` para tentar de novo, ou \`morph-spec worktree link --all\` para voltar a trabalhar nele`;
|
|
@@ -1273,6 +1277,14 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1273
1277
|
worktreeRemoved,
|
|
1274
1278
|
ignoredBackup,
|
|
1275
1279
|
discarded: ignored.discarded,
|
|
1280
|
+
replicable: ignored.replicable,
|
|
1281
|
+
// The infra reparse points this teardown dropped BEFORE letting git near
|
|
1282
|
+
// the directory. Reported because it is the load-bearing safety step of the
|
|
1283
|
+
// whole operation and was previously invisible: the user had no way to see
|
|
1284
|
+
// that `.claude` was a junction into the primary root, nor that it was
|
|
1285
|
+
// unlinked rather than followed. On a removal it is the proof the root's
|
|
1286
|
+
// real `.claude/` was never at risk.
|
|
1287
|
+
unlinked,
|
|
1276
1288
|
blockers,
|
|
1277
1289
|
ignoredBlocking: ignored.blocking,
|
|
1278
1290
|
shieldable: ignored.shieldable,
|
|
@@ -1405,13 +1417,24 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1405
1417
|
...(branch ? { branch } : {}),
|
|
1406
1418
|
...(feature ? { feature } : {}),
|
|
1407
1419
|
};
|
|
1420
|
+
// Reported on BOTH paths: on a removal it is the proof the primary root's
|
|
1421
|
+
// real `.claude/`/`.morph/framework/` were unlinked rather than followed; on
|
|
1422
|
+
// a refusal it is what `relinked` did or did not put back.
|
|
1423
|
+
if (teardown.unlinked.length) out.unlinked = teardown.unlinked;
|
|
1408
1424
|
if (teardown.worktreeRemoved) {
|
|
1409
1425
|
if (teardown.discarded.length) out.discardedIgnored = teardown.discarded;
|
|
1426
|
+
// Broken out of `discardedIgnored` on purpose: "deleted" and "deleted
|
|
1427
|
+
// because the identical bytes are still in the root" are not the same
|
|
1428
|
+
// report, and the second is the one that needs no follow-up from the user.
|
|
1429
|
+
if (teardown.replicable.length) out.replicatedInRoot = teardown.replicable;
|
|
1410
1430
|
if (teardown.ignoredBackup) out.ignoredBackup = teardown.ignoredBackup;
|
|
1431
|
+
if (teardown.shieldable.length) out.backedUp = teardown.shieldable;
|
|
1411
1432
|
} else {
|
|
1412
1433
|
out.refused = teardown.refused;
|
|
1413
1434
|
if (teardown.blockers.length) out.reparsePoints = teardown.blockers;
|
|
1414
1435
|
if (teardown.ignoredBlocking.length) out.ignoredFiles = teardown.ignoredBlocking;
|
|
1436
|
+
if (teardown.shieldable.length) out.exclusiveIgnored = teardown.shieldable;
|
|
1437
|
+
if (teardown.replicable.length) out.replicatedInRoot = teardown.replicable;
|
|
1415
1438
|
if (teardown.infraLeftUnlinked.length) out.infraLeftUnlinked = teardown.infraLeftUnlinked;
|
|
1416
1439
|
out.hint = teardown.declineReason;
|
|
1417
1440
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
cwdIsInsideWorktree,
|
|
14
14
|
} from '../project/worktree.js';
|
|
15
15
|
import { detectDefaultBranch, hasOriginRemote } from '../../lib/git/default-branch.js';
|
|
16
|
+
import { resolveGh } from '../../lib/git/gh.js';
|
|
16
17
|
import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js';
|
|
17
18
|
import {
|
|
18
19
|
composeProjectName,
|
|
@@ -326,7 +327,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
326
327
|
branch: expectedBranch,
|
|
327
328
|
base: target,
|
|
328
329
|
archived: archivePath.replace(/\\/g, '/'),
|
|
329
|
-
...(typeof openPr === 'string' ? { pr: openPr } : {}),
|
|
330
|
+
...(typeof openPr === 'string' ? { pr: { number: prNumberFromUrl(openPr), url: openPr } } : {}),
|
|
330
331
|
hint: `'${feature}' já foi arquivada e ${expectedBranch} já tem PR aberto — o finish anterior concluiu. Falta apenas mergear o PR.`,
|
|
331
332
|
});
|
|
332
333
|
process.exit(1);
|
|
@@ -487,7 +488,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
487
488
|
// VALID (it still names a live directory), so nothing to clean up here —
|
|
488
489
|
// only report where it lives and how to close it later.
|
|
489
490
|
emit({
|
|
490
|
-
finished: true, mode: 'merge', feature, merged: expectedBranch, kept: true, worktreeRemoved: false,
|
|
491
|
+
finished: true, integrated: true, mode: 'merge', feature, merged: expectedBranch, kept: true, worktreeRemoved: false,
|
|
491
492
|
worktree: { path: worktreePath, branch: expectedBranch },
|
|
492
493
|
hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
|
|
493
494
|
...backupNote, ...ucNote,
|
|
@@ -526,10 +527,26 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
526
527
|
}
|
|
527
528
|
tryGit(['branch', '-d', expectedBranch], root);
|
|
528
529
|
// The root index (gitignored) was already updated by the archive (same cwd).
|
|
529
|
-
emit({ finished: true, mode: 'merge', feature, merged: expectedBranch, into: defaultBranch, worktreeRemoved: false, nextStep: MERGE_NEXT_STEP, ...ucNote });
|
|
530
|
+
emit({ finished: true, integrated: true, mode: 'merge', feature, merged: expectedBranch, into: defaultBranch, worktreeRemoved: false, nextStep: MERGE_NEXT_STEP, ...ucNote });
|
|
530
531
|
process.exit(0);
|
|
531
532
|
}
|
|
532
533
|
|
|
534
|
+
/**
|
|
535
|
+
* O número do PR a partir da url que o `gh pr create` imprime.
|
|
536
|
+
*
|
|
537
|
+
* Existe porque o número é o que um humano usa para falar do PR ("mergeia o
|
|
538
|
+
* #51"), e a url é a única coisa que o `gh` devolve. Um formato inesperado
|
|
539
|
+
* devolve `null` em vez de um palpite: o payload prefere admitir que não sabe
|
|
540
|
+
* o número a afirmar um errado.
|
|
541
|
+
*
|
|
542
|
+
* @param {string|null} url
|
|
543
|
+
* @returns {number|null}
|
|
544
|
+
*/
|
|
545
|
+
export function prNumberFromUrl(url) {
|
|
546
|
+
const m = typeof url === 'string' ? url.match(/\/pull\/(\d+)(?:[/?#]|$)/) : null;
|
|
547
|
+
return m ? Number(m[1]) : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
533
550
|
// ── PR integration (push + gh, no local merge) ───────────────────────────────
|
|
534
551
|
|
|
535
552
|
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch }) {
|
|
@@ -571,13 +588,23 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
571
588
|
dropFromRootIndex(root, feature);
|
|
572
589
|
}
|
|
573
590
|
|
|
591
|
+
const prNumber = prNumberFromUrl(prUrl);
|
|
592
|
+
const prLabel = prNumber ? `#${prNumber}` : 'o PR';
|
|
593
|
+
|
|
574
594
|
const out = {
|
|
575
595
|
finished: true,
|
|
596
|
+
// O comando terminou; a INTEGRAÇÃO não aconteceu. Eram a mesma coisa no
|
|
597
|
+
// payload antigo, e é dessa colisão que sai o "shipped" para uma feature
|
|
598
|
+
// cujo PR seguia aberto: quem lê `finished: true` sem mais nada conclui
|
|
599
|
+
// que acabou. A default só recebe archive/{f} quando o PR mergear.
|
|
600
|
+
integrated: false,
|
|
601
|
+
awaiting: 'merge',
|
|
576
602
|
mode: 'pr',
|
|
577
603
|
feature,
|
|
578
604
|
pushed: expectedBranch,
|
|
579
605
|
base: defaultBranch,
|
|
580
|
-
pr: prUrl,
|
|
606
|
+
pr: { number: prNumber, url: prUrl },
|
|
607
|
+
nextStep: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
|
|
581
608
|
...promotedNote(arch),
|
|
582
609
|
...(shedBackup ? { shedBackup } : {}),
|
|
583
610
|
...(teardown
|
|
@@ -626,6 +653,7 @@ const MERGE_NEXT_STEP = 'integração LOCAL — nada foi publicado. Rode `git pu
|
|
|
626
653
|
function mergeOutput({ feature, branch, worktreePath, teardown, shedBackup }) {
|
|
627
654
|
return {
|
|
628
655
|
finished: true,
|
|
656
|
+
integrated: true,
|
|
629
657
|
mode: 'merge',
|
|
630
658
|
feature,
|
|
631
659
|
merged: branch,
|
|
@@ -641,6 +669,15 @@ function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
|
|
|
641
669
|
const out = {};
|
|
642
670
|
if (teardown.ignoredBackup) out.worktreeIgnoredBackup = teardown.ignoredBackup;
|
|
643
671
|
if (teardown.worktreeRemoved && teardown.discarded.length) out.discardedIgnored = teardown.discarded;
|
|
672
|
+
// WHAT was copied out, and what needed no copy because the primary root holds
|
|
673
|
+
// the identical bytes. Reported on both paths: after a removal these two lists
|
|
674
|
+
// are the whole answer to "did I just lose something?", and the user should
|
|
675
|
+
// not have to open the backup directory to find out.
|
|
676
|
+
if (teardown.shieldable?.length) out.worktreeIgnoredBackedUp = teardown.shieldable;
|
|
677
|
+
if (teardown.replicable?.length) out.replicatedInRoot = teardown.replicable;
|
|
678
|
+
// The infra reparse points dropped BEFORE git touched the directory — the
|
|
679
|
+
// proof the primary root's real `.claude/` was unlinked, never followed.
|
|
680
|
+
if (teardown.unlinked?.length) out.unlinked = teardown.unlinked;
|
|
644
681
|
if (!teardown.worktreeRemoved) {
|
|
645
682
|
out.path = worktreePath;
|
|
646
683
|
out.branch = branch;
|
|
@@ -688,28 +725,6 @@ function gitStatusPorcelain(cwd) {
|
|
|
688
725
|
}
|
|
689
726
|
}
|
|
690
727
|
|
|
691
|
-
/**
|
|
692
|
-
* Resolve the `gh` invocation. Defaults to the `gh` binary on PATH. Overridable
|
|
693
|
-
* via MORPH_GH_BIN: a plain string names a different binary (a corporate wrapper,
|
|
694
|
-
* `hub`, …); a JSON array is a full argv prefix — e.g.
|
|
695
|
-
* `["/path/to/node","/path/to/gh-shim.js"]` — which is how the tests inject a
|
|
696
|
-
* cross-platform stub that execFileSync can actually spawn (Node ≥20 refuses to
|
|
697
|
-
* run `.cmd`/`.bat` shims without `shell:true`, and shell:true would reopen the
|
|
698
|
-
* arg-injection hole the whole file avoids).
|
|
699
|
-
*
|
|
700
|
-
* @returns {{ bin: string, prefix: string[] }}
|
|
701
|
-
*/
|
|
702
|
-
function resolveGh() {
|
|
703
|
-
const raw = process.env.MORPH_GH_BIN;
|
|
704
|
-
if (raw && raw.trim().startsWith('[')) {
|
|
705
|
-
try {
|
|
706
|
-
const arr = JSON.parse(raw);
|
|
707
|
-
if (Array.isArray(arr) && arr.length) return { bin: String(arr[0]), prefix: arr.slice(1).map(String) };
|
|
708
|
-
} catch { /* not JSON — treat as a plain binary name below */ }
|
|
709
|
-
}
|
|
710
|
-
return { bin: raw && raw.trim() ? raw.trim() : 'gh', prefix: [] };
|
|
711
|
-
}
|
|
712
|
-
|
|
713
728
|
function ghAuthOk() {
|
|
714
729
|
const { bin, prefix } = resolveGh();
|
|
715
730
|
try {
|
|
@@ -7,6 +7,8 @@ import { missingIdentity } from '../../core/state/feature-identity.js';
|
|
|
7
7
|
import { derivePhaseFrom } from '../../../framework/hooks/shared/state-reader.js';
|
|
8
8
|
import { isGitRepo } from '../project/worktree.js';
|
|
9
9
|
import { morphBranches, currentBranch, branchView } from '../../lib/worktree/feature-truth.js';
|
|
10
|
+
import { buildPrIndex } from '../../lib/git/pr-index.js';
|
|
11
|
+
import { pendingPrFeatures, prForFeature } from '../../lib/git/pending-prs.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* `morph-spec list [--all] [--json] [--no-branch]` — the feature audit board.
|
|
@@ -176,16 +178,51 @@ function flagArchivedElsewhere(cwd, rows) {
|
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A linha de uma feature que existe SÓ como PR aberto — sem pasta ativa nem
|
|
183
|
+
* arquivada nesta árvore. É o estado que o `finish --pr` deixa: o scaffold do
|
|
184
|
+
* root foi apagado pelo shed e o archive viaja no diff do PR, chegando na
|
|
185
|
+
* default apenas no merge. Sem esta linha o trabalho fica sem dono no quadro,
|
|
186
|
+
* e uma branch sem dono é candidata a poda.
|
|
187
|
+
*/
|
|
188
|
+
function pendingRow(p) {
|
|
189
|
+
return {
|
|
190
|
+
feature: p.feature,
|
|
191
|
+
name: null,
|
|
192
|
+
description: null,
|
|
193
|
+
workType: null,
|
|
194
|
+
status: 'in_progress',
|
|
195
|
+
phase: 'aguardando-merge',
|
|
196
|
+
gates: { proposal: false, uiux: false, plan: false, review: false },
|
|
197
|
+
selfAssessed: false,
|
|
198
|
+
flags: ['pr-pending'],
|
|
199
|
+
location: 'pr-pending',
|
|
200
|
+
createdAt: p.createdAt,
|
|
201
|
+
updatedAt: null,
|
|
202
|
+
source: 'pr',
|
|
203
|
+
branch: p.branch,
|
|
204
|
+
worktree: null,
|
|
205
|
+
pr: { number: p.number, state: p.state, url: p.url, ageDays: p.ageDays },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
179
209
|
const mark = (on) => (on ? chalk.green('✓') : chalk.gray('·'));
|
|
180
210
|
const truncate = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
181
211
|
|
|
182
212
|
function whereLabel(row) {
|
|
213
|
+
if (row.location === 'pr-pending') return `branch ${row.branch}`;
|
|
183
214
|
if (row.location === 'archived') return '-';
|
|
184
215
|
if (row.worktree) return row.worktree.exists ? `worktree ${basename(row.worktree.path)}` : '⚠ worktree ausente';
|
|
185
216
|
if (row.source === 'branch') return `branch ${row.branch}`;
|
|
186
217
|
return 'raiz';
|
|
187
218
|
}
|
|
188
219
|
|
|
220
|
+
const prCell = (pr) => {
|
|
221
|
+
if (!pr) return '-';
|
|
222
|
+
const state = String(pr.state || '').toLowerCase();
|
|
223
|
+
return `#${pr.number} ${state}`;
|
|
224
|
+
};
|
|
225
|
+
|
|
189
226
|
export function listCommand(options = {}) {
|
|
190
227
|
const cwd = options.cwd || process.cwd();
|
|
191
228
|
const rows = readFeaturesIn(cwd, 'features', 'active');
|
|
@@ -195,6 +232,32 @@ export function listCommand(options = {}) {
|
|
|
195
232
|
// Commander's `--no-branch` sets options.branch = false; anything else (undefined/true) reads the branch.
|
|
196
233
|
if (options.branch !== false) resolveBranchTruth(cwd, rows);
|
|
197
234
|
|
|
235
|
+
// Estado do PR — uma consulta ao `gh` por invocação, compartilhada pela
|
|
236
|
+
// coluna e pela síntese. `--no-branch` (o modo offline rápido) pula as duas.
|
|
237
|
+
//
|
|
238
|
+
// `prIndex` continua `null` quando a pergunta não pôde ser feita (sem `gh`,
|
|
239
|
+
// sem auth, remoto não-GitHub, ou `--no-branch`). É o mesmo sinal que
|
|
240
|
+
// `--json` já expõe como `pr: null` por linha — a tabela texto usa o mesmo
|
|
241
|
+
// fato para decidir se a coluna PR sequer aparece (achado #2): índice `null`
|
|
242
|
+
// degrada para o comportamento de antes, sem coluna e sem `-` que afirme
|
|
243
|
+
// ausência.
|
|
244
|
+
let prIndex = null;
|
|
245
|
+
if (options.branch !== false) {
|
|
246
|
+
prIndex = buildPrIndex(cwd);
|
|
247
|
+
for (const row of rows) {
|
|
248
|
+
const pr = prForFeature(prIndex, row.feature);
|
|
249
|
+
row.pr = pr ? { number: pr.number, state: pr.state, url: pr.url, ageDays: null } : null;
|
|
250
|
+
}
|
|
251
|
+
const known = new Set(rows.map((r) => r.feature));
|
|
252
|
+
// `null` propaga desconhecido: nenhuma linha sintetizada, e nenhuma
|
|
253
|
+
// afirmação de que não há pendência.
|
|
254
|
+
for (const p of pendingPrFeatures(cwd, { index: prIndex }) || []) {
|
|
255
|
+
if (!known.has(p.feature)) rows.push(pendingRow(p));
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
for (const row of rows) row.pr = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
198
261
|
rows.sort((a, b) => a.feature.localeCompare(b.feature));
|
|
199
262
|
|
|
200
263
|
if (options.json) {
|
|
@@ -208,22 +271,32 @@ export function listCommand(options = {}) {
|
|
|
208
271
|
}
|
|
209
272
|
|
|
210
273
|
const col = (s, w) => String(s ?? '-').padEnd(w);
|
|
274
|
+
// A coluna PR só aparece quando o índice foi construído com sucesso (achado
|
|
275
|
+
// #2) — um índice `null` não vira uma coluna cheia de `-`, que afirmaria
|
|
276
|
+
// "sem PR" para algo que na verdade é "não sabemos".
|
|
277
|
+
const showPr = Boolean(prIndex);
|
|
278
|
+
// `pr-pending` é estado normal de trabalho em voo, não inconsistência —
|
|
279
|
+
// não deve marcar `⚠` nem contar no rodapé (achado #6). Continua em
|
|
280
|
+
// `flags` no `--json`: é metadado real, só não é anomalia.
|
|
281
|
+
const warnFlags = (r) => r.flags.filter((f) => f !== 'pr-pending');
|
|
211
282
|
console.log(chalk.white.bold(
|
|
212
|
-
col('FEATURE', 22) + col('TYPE', 9) + col('STATUS', 13) + col('PHASE',
|
|
283
|
+
col('FEATURE', 22) + col('TYPE', 9) + col('STATUS', 13) + col('PHASE', 17) + col('WHERE', 22) + (showPr ? col('PR', 12) : '')
|
|
213
284
|
+ col('G1', 4) + col('G2', 4) + col('G3', 4) + col('', 3) + 'DESCRIPTION',
|
|
214
285
|
));
|
|
215
286
|
for (const r of rows) {
|
|
216
|
-
const flag = r.
|
|
217
|
-
const desc =
|
|
218
|
-
|
|
287
|
+
const flag = warnFlags(r).length ? chalk.yellow('⚠') : ' ';
|
|
288
|
+
const desc = r.location === 'pr-pending'
|
|
289
|
+
? chalk.yellow(`aguardando merge${r.pr?.ageDays != null ? ` há ${r.pr.ageDays}d` : ''} — sem cópia local`)
|
|
290
|
+
: (r.selfAssessed ? chalk.yellow('[G3 self-assessed] ') : '')
|
|
291
|
+
+ (r.description ? truncate(r.description, 34) : chalk.yellow('(sem descrição)'));
|
|
219
292
|
// padEnd counts the invisible color codes; pad the plain marks manually.
|
|
220
293
|
console.log(
|
|
221
|
-
col(r.feature, 22) + col(r.workType, 9) + col(r.status, 13) + col(r.phase,
|
|
294
|
+
col(r.feature, 22) + col(r.workType, 9) + col(r.status, 13) + col(r.phase, 17) + col(whereLabel(r), 22) + (showPr ? col(prCell(r.pr), 12) : '')
|
|
222
295
|
+ mark(r.gates.proposal) + ' ' + mark(r.gates.plan) + ' ' + mark(r.gates.review) + ' '
|
|
223
296
|
+ flag + ' ' + desc,
|
|
224
297
|
);
|
|
225
298
|
}
|
|
226
|
-
const flagged = rows.filter((r) => r.
|
|
299
|
+
const flagged = rows.filter((r) => warnFlags(r).length);
|
|
227
300
|
if (flagged.length) {
|
|
228
301
|
console.log(chalk.gray(`\n⚠ ${flagged.length} com inconsistência — rode 'morph-spec doctor' para detalhes.`));
|
|
229
302
|
}
|
|
@@ -49,6 +49,13 @@ export const OUTPUT_SCHEMA = {
|
|
|
49
49
|
mandate: { filename: 'mandate.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
50
50
|
tasks: { filename: 'tasks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan', mutableFields: ['status', 'outputs', 'notes'] },
|
|
51
51
|
decisions: { filename: 'decisions.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
52
|
+
// Veredito REUSAR/ESTENDER/CRIAR por conceito que a feature introduz, com a
|
|
53
|
+
// justificativa de cada CRIAR. É o ARTEFATO do ciclo de reuso (a prosa está
|
|
54
|
+
// em morph-plan §2b, a cobrança em src/lib/validators/reuse/). NÃO protegido,
|
|
55
|
+
// como decisions.md: é registro vivo — o implementador que descobre um alvo
|
|
56
|
+
// melhor no meio da task precisa poder anotar, e congelá-lo transformaria a
|
|
57
|
+
// correção num bloqueio de gate.
|
|
58
|
+
reuseMap: { filename: 'reuse-map.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
52
59
|
// N files, one per system use case — the behaviour layer the plan distils
|
|
53
60
|
// BEFORE spec.md/tasks.json. Deliberately NOT protected: editing a use case
|
|
54
61
|
// after Gate 2 is the supported way to change behaviour and re-run implement;
|