@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -7,6 +7,8 @@ import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType,
|
|
|
7
7
|
import { detectWorkType } from '../../lib/work-type-detector.js';
|
|
8
8
|
import { runWorktreeSetup, materializeState, isGitRepo, branchExists, buildBranchName } from '../project/worktree.js';
|
|
9
9
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
10
|
+
import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
|
|
11
|
+
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Common flow (no `--worktree`): still isolate the feature on its own branch
|
|
@@ -140,7 +142,7 @@ function resolveWorkType(options, now) {
|
|
|
140
142
|
* worktree on branch `morph/{feature}`.
|
|
141
143
|
*
|
|
142
144
|
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
143
|
-
* the skills call this at
|
|
145
|
+
* the skills call this at plan start so the authoritative state exists
|
|
144
146
|
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
145
147
|
* existing feature is reported and left untouched — no re-stamping, no
|
|
146
148
|
* re-scaffolding, no worktree setup.
|
|
@@ -211,6 +213,8 @@ export async function createCommand(featureName, options = {}) {
|
|
|
211
213
|
|
|
212
214
|
let worktreeResult = null;
|
|
213
215
|
let branchResult = null;
|
|
216
|
+
let planSkeleton = null;
|
|
217
|
+
let gitignoreAdded = [];
|
|
214
218
|
|
|
215
219
|
if (created) {
|
|
216
220
|
// Scaffold the phase folders this work type starts with.
|
|
@@ -275,6 +279,21 @@ export async function createCommand(featureName, options = {}) {
|
|
|
275
279
|
// Commander's `--no-checkout` sets options.checkout === false.
|
|
276
280
|
branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
|
|
277
281
|
}
|
|
282
|
+
|
|
283
|
+
// protect-spec-files freezes an existing file once its approval is stamped,
|
|
284
|
+
// so a type born pre-approved must create it itself. With a worktree it goes
|
|
285
|
+
// only there: an extra file in the root copy makes `finish` refuse a dirty root.
|
|
286
|
+
if (wt.scaffold.includes('2-plan') && !wt.skippedGates.includes('plan')) {
|
|
287
|
+
const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
|
|
288
|
+
try {
|
|
289
|
+
planSkeleton = writePlanSkeleton(featureName, home);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
planSkeleton = { written: false, error: err.message };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
|
|
296
|
+
gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
|
|
278
297
|
}
|
|
279
298
|
|
|
280
299
|
// `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
|
|
@@ -286,6 +305,8 @@ export async function createCommand(featureName, options = {}) {
|
|
|
286
305
|
const out = { feature: featureName, created, ...finalFeature };
|
|
287
306
|
if (worktreeResult) out.worktree = worktreeResult;
|
|
288
307
|
if (branchResult) out.branch = branchResult;
|
|
308
|
+
if (planSkeleton) out.planSkeleton = planSkeleton;
|
|
309
|
+
if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
|
|
289
310
|
// The move of the PRIMARY checkout is never silent — under --json it goes
|
|
290
311
|
// to stderr so it survives `... --json > out.json` instead of vanishing.
|
|
291
312
|
if (branchResult?.movedFrom) {
|
|
@@ -300,6 +321,12 @@ export async function createCommand(featureName, options = {}) {
|
|
|
300
321
|
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
|
|
301
322
|
console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
|
|
302
323
|
console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
|
|
324
|
+
if (gitignoreAdded.length) {
|
|
325
|
+
console.log(chalk.gray(` .gitignore: ${gitignoreAdded.join(' ')} adicionados (projeto .NET)`));
|
|
326
|
+
}
|
|
327
|
+
if (planSkeleton?.written) {
|
|
328
|
+
console.log(chalk.gray(` 2-plan/plan.md: esqueleto escrito (gramática: morph-spec plan-check --help)`));
|
|
329
|
+
}
|
|
303
330
|
if (wt.skippedGates.length) {
|
|
304
331
|
console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
|
|
305
332
|
}
|
|
@@ -219,7 +219,7 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
219
219
|
// Untracked (never committed) — nothing for git rm to act on.
|
|
220
220
|
}
|
|
221
221
|
// `git rm` only ever removes TRACKED files — a scaffolded phase folder
|
|
222
|
-
// with no artifact in it (
|
|
222
|
+
// with no artifact in it (2-plan/ with nothing written yet) has no
|
|
223
223
|
// tracked blob and survives as an empty directory on disk. This bit
|
|
224
224
|
// real after --with-worktree: teardownWorktree's own registration-clear
|
|
225
225
|
// commits the root's feature.json on its way through, so `git rm`
|
|
@@ -65,7 +65,7 @@ export function buildPrTitle(feature, fj) {
|
|
|
65
65
|
return prefix + (raw.length <= room ? raw : `${raw.slice(0, room - 1)}…`);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `
|
|
68
|
+
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
69
|
|
|
70
70
|
/**
|
|
71
71
|
* `morph-spec finish <feature> --pr|--merge` — the single, symmetric close of a
|
|
@@ -449,8 +449,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
449
449
|
// ── MERGE precondition, BEFORE the archive ────────────────────────────────
|
|
450
450
|
// `--merge` lands its merge commit in the PRIMARY ROOT, so the root has to be
|
|
451
451
|
// clean. That check used to live inside integrateMerge — i.e. AFTER
|
|
452
|
-
// runArchive had already moved the feature to .morph/archive/,
|
|
453
|
-
//
|
|
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
454
|
// refusal on a half-finished feature, and the retry then failed for a
|
|
455
455
|
// different reason entirely (the feature was no longer in features/).
|
|
456
456
|
//
|
|
@@ -709,7 +709,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
709
709
|
|
|
710
710
|
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
|
|
711
711
|
const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
|
|
712
|
-
const
|
|
712
|
+
const adrNote = adrIndexNote(arch);
|
|
713
713
|
|
|
714
714
|
if (isWorktreeFlow) {
|
|
715
715
|
// The root stays on the default; shed this feature's own root-side scaffold,
|
|
@@ -748,7 +748,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
748
748
|
dropFromRootIndex(root, feature);
|
|
749
749
|
emit({
|
|
750
750
|
...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, shedBackup, kept: true }),
|
|
751
|
-
...
|
|
751
|
+
...adrNote,
|
|
752
752
|
});
|
|
753
753
|
process.exit(0);
|
|
754
754
|
}
|
|
@@ -766,7 +766,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
766
766
|
? null
|
|
767
767
|
: await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
|
|
768
768
|
dropFromRootIndex(root, feature);
|
|
769
|
-
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...
|
|
769
|
+
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...adrNote, e2e });
|
|
770
770
|
process.exit(0);
|
|
771
771
|
}
|
|
772
772
|
|
|
@@ -797,7 +797,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
797
797
|
const publish = publicationState({ root, defaultBranch, push: options.push });
|
|
798
798
|
const e2e = reclaimStack(feature, { root, worktreePath: null, options });
|
|
799
799
|
// The root index (gitignored) was already updated by the archive (same cwd).
|
|
800
|
-
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...
|
|
800
|
+
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...adrNote, e2e });
|
|
801
801
|
process.exit(0);
|
|
802
802
|
}
|
|
803
803
|
|
|
@@ -887,7 +887,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
887
887
|
base: defaultBranch,
|
|
888
888
|
pr: { number: prNumber, url: prUrl },
|
|
889
889
|
nextStep: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
|
|
890
|
-
...
|
|
890
|
+
...adrIndexNote(arch),
|
|
891
891
|
...(shedBackup ? { shedBackup } : {}),
|
|
892
892
|
...(e2e ? { e2e } : {}),
|
|
893
893
|
...(teardown
|
|
@@ -917,23 +917,11 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
917
917
|
// ── output helpers ────────────────────────────────────────────────────────
|
|
918
918
|
|
|
919
919
|
/**
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
* (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
|
|
923
|
-
* promoted copies that stayed superseded because the acervo said so
|
|
924
|
-
* (`keptSuperseded`), the ones the feature's spec.md revoked
|
|
925
|
-
* (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
|
|
926
|
-
* revocations it could not apply
|
|
927
|
-
* (`revocationProblems`). Every key is absent when there is nothing to report,
|
|
928
|
-
* so a feature that touches no acervo keeps the exact output shape it had
|
|
929
|
-
* before any of them existed.
|
|
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.
|
|
930
922
|
*/
|
|
931
|
-
function
|
|
923
|
+
function adrIndexNote(arch) {
|
|
932
924
|
return {
|
|
933
|
-
...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
|
|
934
|
-
...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
|
|
935
|
-
...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
|
|
936
|
-
...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
|
|
937
925
|
...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
|
|
938
926
|
...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
|
|
939
927
|
};
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
2
|
import { getFeature } from '../../core/state/state-manager.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { evaluatorReportState, evaluatorMediaSection, verdictSignal } from '../../lib/evaluator-report.js';
|
|
3
|
+
import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor } from '../../lib/risk-detector.js';
|
|
4
|
+
import { reviewReports, reviewSignal, requiredReviewRoles, verifierMediaSection } from '../../lib/review-reports.js';
|
|
7
5
|
import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
|
|
8
6
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
9
7
|
import { minimatch } from 'minimatch';
|
|
10
8
|
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
11
9
|
import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
|
|
12
10
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
13
|
-
import {
|
|
11
|
+
import { readPlanChecks } from '../../lib/plan/checks.js';
|
|
14
12
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
15
|
* `--signals` on `gate-check` is NOT a general override like `gate-decision`'s
|
|
18
|
-
* — everything else here is read straight off disk (workType,
|
|
16
|
+
* — everything else here is read straight off disk (workType, sliceCount, sha,
|
|
19
17
|
* diffstat, verification), and letting a caller override those would let a
|
|
20
18
|
* careless/hostile `--signals` silently defeat the policy this command exists
|
|
21
19
|
* to make deterministic (e.g. overriding `workType` away from "hotfix" would
|
|
@@ -28,67 +26,39 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
|
|
|
28
26
|
const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
|
-
* Os
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* 2 congelou. Nao ha julgamento envolvido: ou a task pinou o standard, ou nao
|
|
35
|
-
* pinou. Um sinal derivado de opiniao seria sobrescrivivel na pratica ainda que
|
|
36
|
-
* nao estivesse em OVERRIDABLE_SIGNAL_KEYS.
|
|
37
|
-
*
|
|
38
|
-
* A forma dos valores e a de `tasks.json` (`standards/...`), nao a do disco
|
|
39
|
-
* (`framework/standards/...`): e o vocabulario que o plano usa.
|
|
29
|
+
* Os standards que fazem uma feature ser "de mídia", como os ids de
|
|
30
|
+
* STANDARDS.json. Fato do plano congelado: ou uma fatia pinou o standard em
|
|
31
|
+
* `standards:`, ou não pinou.
|
|
40
32
|
*/
|
|
41
|
-
export const
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
33
|
+
export const MEDIA_STANDARD_IDS = [
|
|
34
|
+
'ai-agents-modalities-image-gen',
|
|
35
|
+
'ai-agents-media-video',
|
|
36
|
+
'ai-agents-media-pipeline',
|
|
45
37
|
];
|
|
46
38
|
|
|
47
39
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Isto e uma FUNCAO EXPORTADA, e nao uma expressao dentro de `collectSignals`,
|
|
52
|
-
* porque um predicado que decide se um guardrail arma tem de ser testavel
|
|
53
|
-
* sozinho — o irmao `productionCode` tem quatro casos, e este nao tinha nenhum.
|
|
54
|
-
* Um predicado que sempre devolve `false` desarma a clausula em silencio, e
|
|
55
|
-
* silencio e o defeito que esta epic catalogou sete vezes.
|
|
56
|
-
*
|
|
57
|
-
* **O prefixo `framework/` e normalizado, e essa e a razao de existir da
|
|
58
|
-
* funcao.** O vocabulario do plano e `standards/...` (o exemplo do
|
|
59
|
-
* `tasks.schema.json`, e a unica forma medida hoje nos `standards[]` desta
|
|
60
|
-
* arvore), mas o disco e `framework/standards/...` — e a mesma prosa das skills
|
|
61
|
-
* cita as duas formas. Um plano que escrevesse a forma do disco desarmaria o
|
|
62
|
-
* guardrail sem erro nenhum: `standards --find`, `validate` e o proprio Gate 3
|
|
63
|
-
* continuariam verdes. Aceitar as duas grafias custa uma linha; recusar uma
|
|
64
|
-
* delas em silencio custa o gate.
|
|
65
|
-
*
|
|
66
|
-
* Fail-open por AUSENCIA (nao por opiniao): lista vazia, `standards` ausente ou
|
|
67
|
-
* de tipo errado ⇒ `false` ⇒ nao pausa. Sem plano legivel nao ha como AFIRMAR
|
|
68
|
-
* que a feature gera midia, e uma regra nova nao trava feature legada.
|
|
69
|
-
*
|
|
70
|
-
* @param {Array<object>} tasks - `tasks.json` → `tasks[]` (qualquer coisa nao-array vira []).
|
|
71
|
-
* @returns {boolean} true se ao menos uma task pinou um standard de midia.
|
|
40
|
+
* O plano escreve o standard pelo id ou pelo caminho (`ai-agents/media-video.md`,
|
|
41
|
+
* com ou sem `framework/` ou `standards/` na frente): as duas grafias viram o id.
|
|
72
42
|
*/
|
|
73
|
-
export function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
t.standards.some((s) =>
|
|
79
|
-
typeof s === 'string' &&
|
|
80
|
-
MEDIA_STANDARD_PATHS.includes(s.replace(/^framework\//, ''))));
|
|
43
|
+
export function standardIdOf(value) {
|
|
44
|
+
return String(value ?? '').trim().replace(/\\/g, '/')
|
|
45
|
+
.replace(/^(\.morph\/)?(framework\/)?(standards\/)?/, '')
|
|
46
|
+
.replace(/\.md$/, '')
|
|
47
|
+
.replace(/\//g, '-');
|
|
81
48
|
}
|
|
82
49
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Uma feature "gera mídia" quando alguma fatia extraída pinou um dos
|
|
52
|
+
* standards de mídia. Predicado exportado porque decide se um guardrail arma:
|
|
53
|
+
* um que sempre devolve `false` desarma a pausa de mídia da review e a rota opus em
|
|
54
|
+
* silêncio.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ standards?: string[] }[]|null|undefined} slices
|
|
57
|
+
*/
|
|
58
|
+
export function featureGeneratesMedia(slices) {
|
|
59
|
+
if (!Array.isArray(slices)) return false;
|
|
60
|
+
return slices.some((s) => Array.isArray(s?.standards)
|
|
61
|
+
&& s.standards.some((id) => MEDIA_STANDARD_IDS.includes(standardIdOf(id))));
|
|
92
62
|
}
|
|
93
63
|
|
|
94
64
|
/**
|
|
@@ -126,6 +96,37 @@ function collectPromptModel(cwd, defaultBranch) {
|
|
|
126
96
|
};
|
|
127
97
|
}
|
|
128
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Quais relatórios o ship exige: a regra única que gate-check, approve e
|
|
101
|
+
* `report-check --before-eval` leem. Coleta do git, nunca decide.
|
|
102
|
+
*
|
|
103
|
+
* @returns {{ required: string[], security: { required: boolean, reasons: string[] } }}
|
|
104
|
+
*/
|
|
105
|
+
export function collectReviewRequirement(cwd, feature) {
|
|
106
|
+
let changed = [];
|
|
107
|
+
try { changed = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changed = []; }
|
|
108
|
+
const security = securityReviewFor(changed);
|
|
109
|
+
return { required: requiredReviewRoles({ workType: feature?.workType, securityRequired: security.required }), security };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** O sinal `reviewReports` do ship, lido do disco. */
|
|
113
|
+
export function collectReviewSignal(cwd, featureName, feature, planChecks = readPlanChecks(cwd, featureName)) {
|
|
114
|
+
const { required } = collectReviewRequirement(cwd, feature);
|
|
115
|
+
const checkIds = Array.isArray(planChecks.checks?.checks) ? planChecks.checks.checks.map((c) => c.id) : null;
|
|
116
|
+
return reviewSignal(reviewReports(cwd, featureName), { required, checkIds });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Why an automatic signature may not close the plan gate — the same rule that
|
|
121
|
+
* makes gate-check pause it, read here by approve and advance.
|
|
122
|
+
*
|
|
123
|
+
* @returns {string|null}
|
|
124
|
+
*/
|
|
125
|
+
export function planAutoApprovalBlock(cwd, featureName) {
|
|
126
|
+
const reason = planChecksPause(readPlanChecks(cwd, featureName).status);
|
|
127
|
+
return reason ? reason.replace('{feature}', featureName) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
129
130
|
/**
|
|
130
131
|
* Collect every signal `shouldPauseGate()` (risk-detector.js) is documented
|
|
131
132
|
* to consume, straight off disk — the deterministic call site risk-detector
|
|
@@ -139,9 +140,8 @@ function collectPromptModel(cwd, defaultBranch) {
|
|
|
139
140
|
* alimentam a regra da dispensa de e2e (também só no ship): um nó e2e que
|
|
140
141
|
* PULOU não reprova o rollup, logo `verificationStatus` sai 'pass' e a razão
|
|
141
142
|
* literal é o único fato que distingue dispensa legítima de ausência de prova.
|
|
142
|
-
* `
|
|
143
|
-
*
|
|
144
|
-
* task, que risk-detector.js recusa como dispensa da feature.
|
|
143
|
+
* `planChecks` (só no gate do plano) é o estado dos checks extraídos contra o
|
|
144
|
+
* plano; `sliceCount` e as `standards` das fatias saem da mesma extração.
|
|
145
145
|
* `verification` completo, `e2eSkipClass`, diffstat e sha seguem como contexto
|
|
146
146
|
* de auditoria persistido, sem regra própria.
|
|
147
147
|
*
|
|
@@ -152,7 +152,8 @@ function collectPromptModel(cwd, defaultBranch) {
|
|
|
152
152
|
* @returns {object}
|
|
153
153
|
*/
|
|
154
154
|
export function collectSignals(featureName, feature, cwd, gateName) {
|
|
155
|
-
const
|
|
155
|
+
const planChecks = readPlanChecks(cwd, featureName);
|
|
156
|
+
const slices = planChecks.checks?.slices ?? null;
|
|
156
157
|
const defaultBranch = detectDefaultBranch(cwd);
|
|
157
158
|
const sha = getHeadSha(cwd);
|
|
158
159
|
const verification = feature.verification || null;
|
|
@@ -163,14 +164,12 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
163
164
|
// anti-spoof rationale as workType (hotfix's forced pause).
|
|
164
165
|
gate: gateName,
|
|
165
166
|
workType: feature.workType || 'feature',
|
|
166
|
-
|
|
167
|
+
sliceCount: slices ? slices.length : null,
|
|
167
168
|
standardsViolations: [],
|
|
168
169
|
designSystemDivergence: false,
|
|
169
170
|
};
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
// dispatch se fatia no plano, não depois de um sub-agent bater no teto.
|
|
173
|
-
if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
|
|
172
|
+
if (gateName === 'plan') signals.planChecks = planChecks.status;
|
|
174
173
|
|
|
175
174
|
// Só onde o checklist decide algo: no fecho do design e no ship. Fato de
|
|
176
175
|
// disco, fora de OVERRIDABLE_SIGNAL_KEYS.
|
|
@@ -179,10 +178,10 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
if (gateName === 'review') {
|
|
182
|
-
signals.
|
|
181
|
+
signals.reviewReports = collectReviewSignal(cwd, featureName, feature, planChecks);
|
|
183
182
|
signals.mediaClaims = {
|
|
184
|
-
section:
|
|
185
|
-
featureGeneratesMedia: featureGeneratesMedia(
|
|
183
|
+
section: verifierMediaSection(cwd, featureName),
|
|
184
|
+
featureGeneratesMedia: featureGeneratesMedia(slices),
|
|
186
185
|
};
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -207,15 +206,9 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
207
206
|
signals.e2eStatus = verification?.nodes?.e2e ?? null;
|
|
208
207
|
signals.e2eReason = verification?.e2eReason ?? null;
|
|
209
208
|
signals.e2eSkipClass = signals.e2eStatus === 'skip' ? classifyE2eSkip(signals.e2eReason) : null;
|
|
210
|
-
// De QUEM é o carimbo. `verify {f} {task}` grava 'task:{id}'; só 'feature'
|
|
211
|
-
// responde pela app inteira, e é por isso que este sinal existe: a razão
|
|
212
|
-
// 'task group "docs" does not touch runtime' é dispensa legítima do que
|
|
213
|
-
// AQUELA task verificou, e sem o escopo ela dispensaria o Gate 3 da feature.
|
|
214
|
-
// Fato de disco como os outros três — jamais sobrescrevível por --signals.
|
|
215
|
-
signals.verificationScope = verification?.scope ?? null;
|
|
216
209
|
|
|
217
210
|
// O nó de eval, separado do rollup pela mesma razão que o e2e: um eval que
|
|
218
|
-
// PULOU
|
|
211
|
+
// PULOU não avermelha `verification.status`, então o gate
|
|
219
212
|
// leria 'pass' tanto para "o eval rodou e passou" quanto para "o eval nunca
|
|
220
213
|
// rodou". `null` aqui é "este projeto não declara nó de eval" — DISTINTO de
|
|
221
214
|
// 'skip', que é "declara, e esta corrida não o executou".
|
|
@@ -242,8 +235,8 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
242
235
|
* Golden rule: this command does NOT change gate policy. It is a faithful
|
|
243
236
|
* caller of risk-detector.js's existing, already-tested rules — nothing here
|
|
244
237
|
* adds a new pause condition. Where the skills' natural-language risk tables
|
|
245
|
-
* (morph-
|
|
246
|
-
* risk-detector.js
|
|
238
|
+
* (morph-plan/morph-review/morph-apply) diverge from risk-detector.js,
|
|
239
|
+
* risk-detector.js wins.
|
|
247
240
|
*/
|
|
248
241
|
export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
249
242
|
try {
|
|
@@ -259,7 +252,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
259
252
|
process.exit(1);
|
|
260
253
|
}
|
|
261
254
|
|
|
262
|
-
// Owner-tree guard: gate-check reads workType/
|
|
255
|
+
// Owner-tree guard: gate-check reads workType/checks/verdict/git off cwd
|
|
263
256
|
// AND writes the resulting decision through persistGateDecision — both
|
|
264
257
|
// halves are wrong from a tree that isn't this feature's own, so refuse
|
|
265
258
|
// rather than compute a decision from stale reads and then fail to save it.
|
|
@@ -289,7 +282,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
289
282
|
if (rejected.length > 0) {
|
|
290
283
|
console.error(chalk.yellow(
|
|
291
284
|
`\n⚠️ --signals ignored for keys not derived from disk-facts you may safely override: ${rejected.join(', ')}\n` +
|
|
292
|
-
` Only ${OVERRIDABLE_SIGNAL_KEYS.join(', ')} may be overridden here — everything else is read from feature.json/
|
|
285
|
+
` Only ${OVERRIDABLE_SIGNAL_KEYS.join(', ')} may be overridden here — everything else is read from feature.json/checks.json/git.\n` +
|
|
293
286
|
` Use \`morph-spec gate-decision\` directly for a fully manual decision.\n`
|
|
294
287
|
));
|
|
295
288
|
}
|
|
@@ -303,9 +296,14 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
303
296
|
const signals = { ...collectSignals(featureName, feature, cwd, gateName), ...overrides };
|
|
304
297
|
|
|
305
298
|
const trust = options.trust || readTrustFromConfig();
|
|
306
|
-
const
|
|
299
|
+
const computed = shouldPauseGate({ trust }, signals);
|
|
300
|
+
const manual = feature.gateDecisions?.[gateName];
|
|
301
|
+
const manualPause = manual?.source === 'manual' && manual.pause === true;
|
|
302
|
+
const { pause, reason } = manualPause ? { pause: true, reason: manual.reason } : computed;
|
|
307
303
|
|
|
308
|
-
const entry = await persistGateDecision(featureName, gateName, {
|
|
304
|
+
const entry = await persistGateDecision(featureName, gateName, {
|
|
305
|
+
pause, reason, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
|
|
306
|
+
});
|
|
309
307
|
|
|
310
308
|
if (options.json) {
|
|
311
309
|
console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
|
|
@@ -317,6 +315,9 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
317
315
|
? chalk.yellow(`\n⏸️ Gate "${gateName}" — PAUSE (${featureName})`)
|
|
318
316
|
: chalk.green(`\n▶️ Gate "${gateName}" — auto-pass (${featureName})`));
|
|
319
317
|
console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
|
|
318
|
+
if (manualPause) {
|
|
319
|
+
console.log(chalk.gray(` pausa manual mantida (sinais calculados: ${computed.reason}); só \`morph-spec gate-decision ${featureName} ${gateName} --pause false\` a libera`));
|
|
320
|
+
}
|
|
320
321
|
if (allSignals.length > 0) {
|
|
321
322
|
console.log(chalk.gray(` risk signals present: ${allSignals.join('; ')}`));
|
|
322
323
|
}
|
|
@@ -5,7 +5,7 @@ import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
|
5
5
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
6
6
|
import { mockupsHash } from '../../lib/specs/screen-checklist.js';
|
|
7
7
|
|
|
8
|
-
export const VALID_GATES = ['
|
|
8
|
+
export const VALID_GATES = ['plan', 'uiux', 'review'];
|
|
9
9
|
|
|
10
10
|
function parsePauseValue(raw) {
|
|
11
11
|
const v = String(raw).toLowerCase();
|
|
@@ -43,13 +43,13 @@ export function readTrustFromConfig() {
|
|
|
43
43
|
*
|
|
44
44
|
* @param {string} featureName
|
|
45
45
|
* @param {string} gateName
|
|
46
|
-
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string }} decision
|
|
46
|
+
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string, source: 'manual'|'gate-check' }} decision
|
|
47
47
|
* @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string, mockupsHash?: string|null }>}
|
|
48
48
|
* the exact entry written (decidedAt is server-side, generated here). A uiux
|
|
49
49
|
* decision also carries the hash of the mockup it saw: it answers that
|
|
50
50
|
* mockup's pause, not the next one's.
|
|
51
51
|
*/
|
|
52
|
-
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd() } = {}) {
|
|
52
|
+
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd(), source } = {}) {
|
|
53
53
|
const entry = {
|
|
54
54
|
pause,
|
|
55
55
|
reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
@@ -57,6 +57,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
|
|
|
57
57
|
trust: trust || readTrustFromConfig(),
|
|
58
58
|
decidedAt: new Date().toISOString(),
|
|
59
59
|
...(gateName === 'uiux' ? { mockupsHash: mockupsHash(cwd, featureName) } : {}),
|
|
60
|
+
source,
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
@@ -118,7 +119,7 @@ export async function gateDecisionCommand(featureName, gateName, options = {}) {
|
|
|
118
119
|
process.exit(1);
|
|
119
120
|
}
|
|
120
121
|
if (!signals || typeof signals !== 'object' || Array.isArray(signals)) {
|
|
121
|
-
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"
|
|
122
|
+
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"standardsViolations":[]}\'\n'));
|
|
122
123
|
process.exit(1);
|
|
123
124
|
}
|
|
124
125
|
}
|
|
@@ -128,6 +129,7 @@ export async function gateDecisionCommand(featureName, gateName, options = {}) {
|
|
|
128
129
|
reason: options.reason,
|
|
129
130
|
signals,
|
|
130
131
|
trust: options.trust,
|
|
132
|
+
source: 'manual',
|
|
131
133
|
});
|
|
132
134
|
|
|
133
135
|
if (options.json) {
|
|
@@ -87,6 +87,6 @@ export function handoffCommand(featureName, options = {}) {
|
|
|
87
87
|
if (tree !== cwd) {
|
|
88
88
|
console.log(chalk.yellow(` resolvido para ${tree} — a cópia da raiz é esqueleto de nascimento, não a verdade.`));
|
|
89
89
|
}
|
|
90
|
-
console.log(chalk.gray(` fase ${result.meta.phase} ·
|
|
90
|
+
console.log(chalk.gray(` fase ${result.meta.phase} · fatias ${result.meta.slicesDone}/${result.meta.slicesTotal} · ${result.meta.generatedAt}`));
|
|
91
91
|
console.log(chalk.gray(' A próxima sessão lê este documento antes de continuar (ponteiros, não cópias).\n'));
|
|
92
92
|
}
|
|
@@ -4,7 +4,7 @@ import { basename, join } from 'path';
|
|
|
4
4
|
import { derivePhase } from '../../core/state/state-manager.js';
|
|
5
5
|
import { gateOrderViolations } from '../../core/state/gate-order.js';
|
|
6
6
|
import { missingIdentity } from '../../core/state/feature-identity.js';
|
|
7
|
-
import { derivePhaseFrom } from '../../../framework/hooks/shared/state-reader.js';
|
|
7
|
+
import { derivePhaseFrom, fileSetArtifactProbe } 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
10
|
import { buildPrIndex } from '../../lib/git/pr-index.js';
|
|
@@ -13,7 +13,7 @@ import { pendingPrFeatures, prForFeature } from '../../lib/git/pending-prs.js';
|
|
|
13
13
|
/**
|
|
14
14
|
* `morph-spec list [--all] [--json] [--no-branch]` — the feature audit board.
|
|
15
15
|
*
|
|
16
|
-
* One row per feature: slug × workType × status × phase × gates (G1/
|
|
16
|
+
* One row per feature: slug × workType × status × phase × gates (G1/Gd/G3) ×
|
|
17
17
|
* description, plus a flags column for anomalies (missing description, gate
|
|
18
18
|
* order, dangling worktree pointer). Complements `delete`: see what's dead
|
|
19
19
|
* before removing it. Replaces the ad-hoc "loop over every feature.json in
|
|
@@ -37,7 +37,7 @@ import { pendingPrFeatures, prForFeature } from '../../lib/git/pending-prs.js';
|
|
|
37
37
|
* `morph-spec fleet`'s job, not this one's.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
const G = ['
|
|
40
|
+
const G = ['plan', 'uiux', 'review'];
|
|
41
41
|
|
|
42
42
|
function readFeaturesIn(baseDir, subdir, location) {
|
|
43
43
|
const root = join(baseDir, '.morph', subdir);
|
|
@@ -76,7 +76,7 @@ function shapeRow(name, fj, location, phase, { corruptDisk = false, worktree = n
|
|
|
76
76
|
const gates = {};
|
|
77
77
|
for (const g of G) gates[g] = fj.approvalGates?.[g]?.approved === true;
|
|
78
78
|
|
|
79
|
-
// Gate 3 approved WITHOUT the independent
|
|
79
|
+
// Gate 3 approved WITHOUT the independent review reports (`approve --self-assessed`).
|
|
80
80
|
// A recorded degradation is only worth recording if it is visible — this is
|
|
81
81
|
// the surface that makes it so (D2).
|
|
82
82
|
const selfAssessed = fj.approvalGates?.review?.selfAssessed === true;
|
|
@@ -150,7 +150,7 @@ function resolveBranchTruth(cwd, rows) {
|
|
|
150
150
|
if (head === featureBranch) continue; // cwd IS the checkout — disk is freshest, never go backwards
|
|
151
151
|
const view = branchView(cwd, row.feature, featureBranch);
|
|
152
152
|
if (!view?.fj) continue;
|
|
153
|
-
const phase = derivePhaseFrom(view.fj, (
|
|
153
|
+
const phase = derivePhaseFrom(view.fj, fileSetArtifactProbe(view.files));
|
|
154
154
|
rows[i] = shapeRow(row.feature, view.fj, row.location, phase, {
|
|
155
155
|
corruptDisk: row.flags.includes('corrupt'),
|
|
156
156
|
worktree: row.worktree, // root-owned — never trust the branch copy for this
|
|
@@ -193,7 +193,7 @@ function pendingRow(p) {
|
|
|
193
193
|
workType: null,
|
|
194
194
|
status: 'in_progress',
|
|
195
195
|
phase: 'aguardando-merge',
|
|
196
|
-
gates: {
|
|
196
|
+
gates: { plan: false, uiux: false, review: false },
|
|
197
197
|
selfAssessed: false,
|
|
198
198
|
flags: ['pr-pending'],
|
|
199
199
|
location: 'pr-pending',
|
|
@@ -281,7 +281,7 @@ export function listCommand(options = {}) {
|
|
|
281
281
|
const warnFlags = (r) => r.flags.filter((f) => f !== 'pr-pending');
|
|
282
282
|
console.log(chalk.white.bold(
|
|
283
283
|
col('FEATURE', 22) + col('TYPE', 9) + col('STATUS', 13) + col('PHASE', 17) + col('WHERE', 22) + (showPr ? col('PR', 12) : '')
|
|
284
|
-
+ col('G1', 4) + col('
|
|
284
|
+
+ col('G1', 4) + col('Gd', 4) + col('G3', 4) + col('', 3) + 'DESCRIPTION',
|
|
285
285
|
));
|
|
286
286
|
for (const r of rows) {
|
|
287
287
|
const flag = warnFlags(r).length ? chalk.yellow('⚠') : ' ';
|
|
@@ -292,7 +292,7 @@ export function listCommand(options = {}) {
|
|
|
292
292
|
// padEnd counts the invisible color codes; pad the plain marks manually.
|
|
293
293
|
console.log(
|
|
294
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) : '')
|
|
295
|
-
+ mark(r.gates.
|
|
295
|
+
+ mark(r.gates.plan) + ' ' + mark(r.gates.uiux) + ' ' + mark(r.gates.review) + ' '
|
|
296
296
|
+ flag + ' ' + desc,
|
|
297
297
|
);
|
|
298
298
|
}
|
|
@@ -300,6 +300,6 @@ export function listCommand(options = {}) {
|
|
|
300
300
|
if (flagged.length) {
|
|
301
301
|
console.log(chalk.gray(`\n⚠ ${flagged.length} com inconsistência — rode 'morph-spec doctor' para detalhes.`));
|
|
302
302
|
}
|
|
303
|
-
console.log(chalk.gray(`\nG1=
|
|
303
|
+
console.log(chalk.gray(`\nG1=plan · Gd=uiux (design) · G3=review${options.all || options.archived ? ' · inclui arquivadas' : ''}`));
|
|
304
304
|
process.exit(0);
|
|
305
305
|
}
|