@polymorphism-tech/morph-spec 8.37.0 → 8.38.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 +19 -6
- package/README.md +2 -2
- package/bin/morph-spec.js +16 -7
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -3
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +76 -27
- package/framework/docs/CLI.md +55 -18
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/morph-workflow.md +1 -0
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +36 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +39 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +72 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +34 -9
- package/framework/standards/STANDARDS.json +187 -12
- package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -3
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +64 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +12 -6
- package/framework/templates/feature/decisions.md +124 -122
- package/package.json +1 -1
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +82 -10
- package/src/commands/project/update.js +25 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/scanner/agents-scanner.js +5 -1
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/evaluator-report.js +25 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +172 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/setup-infra.js +29 -18
- package/src/utils/agents-installer.js +281 -12
- package/src/utils/hooks-installer.js +75 -15
|
@@ -21,6 +21,8 @@ import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js'
|
|
|
21
21
|
import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
|
|
22
22
|
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
23
23
|
import { runArchive } from './archive.js';
|
|
24
|
+
import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
25
|
+
import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
|
|
24
26
|
|
|
25
27
|
/** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
|
|
26
28
|
const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
|
|
@@ -119,6 +121,14 @@ export async function finishCommand(feature, options = {}) {
|
|
|
119
121
|
// would otherwise get a silent merge they never asked for.
|
|
120
122
|
let resumed = false;
|
|
121
123
|
|
|
124
|
+
// ONE stamp for every backup this run writes: the shed's and the teardown's
|
|
125
|
+
// folders then count as a single backup under the retention.
|
|
126
|
+
const backupStamp = newBackupStamp();
|
|
127
|
+
|
|
128
|
+
// The backup retention rides the same single point, AFTER this run's own
|
|
129
|
+
// backups exist (the shed and the teardown both precede every success emit).
|
|
130
|
+
// This run's stamp is protected: the prune never takes the backup it just
|
|
131
|
+
// made room for, even when backups stamped by a clock running ahead outrank it.
|
|
122
132
|
const emit = (obj) => {
|
|
123
133
|
if (resumed) obj = { ...obj, resumed: true };
|
|
124
134
|
if (obj?.finished === true) {
|
|
@@ -126,6 +136,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
126
136
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
127
137
|
if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
|
|
128
138
|
} catch { /* fail-open */ }
|
|
139
|
+
const retention = enforceBackupRetention(root, { protectStamps: [backupStamp] });
|
|
140
|
+
if (retention) obj = { ...obj, backupRetention: retention };
|
|
129
141
|
}
|
|
130
142
|
console.log(JSON.stringify(obj));
|
|
131
143
|
};
|
|
@@ -413,7 +425,7 @@ export async function finishCommand(feature, options = {}) {
|
|
|
413
425
|
emit({ finished: false, error: 'status-failed', feature });
|
|
414
426
|
process.exit(1);
|
|
415
427
|
}
|
|
416
|
-
if (dirty.
|
|
428
|
+
if (withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim())).length) {
|
|
417
429
|
emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
|
|
418
430
|
process.exit(1);
|
|
419
431
|
}
|
|
@@ -475,6 +487,9 @@ export async function finishCommand(feature, options = {}) {
|
|
|
475
487
|
}
|
|
476
488
|
|
|
477
489
|
// ── ARCHIVE (inside morph/{f}) ────────────────────────────────────────────
|
|
490
|
+
// Every refusal is behind us: the leftover of a killed `graph refresh` goes now,
|
|
491
|
+
// before the archive — never before a precondition that could still refuse.
|
|
492
|
+
dropLeftoverIgnore(cleanCwd);
|
|
478
493
|
const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
|
|
479
494
|
if (!arch.ok) {
|
|
480
495
|
emit({ finished: false, error: `archive-${arch.error}`, feature });
|
|
@@ -489,9 +504,9 @@ export async function finishCommand(feature, options = {}) {
|
|
|
489
504
|
|
|
490
505
|
// ── INTEGRATE ─────────────────────────────────────────────────────────────
|
|
491
506
|
if (mode === 'merge') {
|
|
492
|
-
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch });
|
|
507
|
+
return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
|
|
493
508
|
}
|
|
494
|
-
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch });
|
|
509
|
+
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
|
|
495
510
|
}
|
|
496
511
|
|
|
497
512
|
/**
|
|
@@ -582,6 +597,30 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
582
597
|
|
|
583
598
|
if (defaultBranch) out.defaultBranch = defaultBranch;
|
|
584
599
|
|
|
600
|
+
// A retenção dos backups também é mutação do finish real — a AUTOMÁTICA, que
|
|
601
|
+
// não toca backup sem manifesto (esses só o `doctor --worktrees --prune`
|
|
602
|
+
// poda). A contagem é a de AGORA: o backup que este finish gravar ainda pode
|
|
603
|
+
// empurrar mais um para fora.
|
|
604
|
+
try {
|
|
605
|
+
const retention = inspectFinishBackups(root);
|
|
606
|
+
if (retention.policy.skipped) {
|
|
607
|
+
out.backupRetention = { skipped: retention.policy.skipped };
|
|
608
|
+
warnings.push(`a retenção dos backups do finish será PULADA: ${retention.policy.skipped}`);
|
|
609
|
+
} else if (retention.outsideRetention || retention.overdueSecrets.length || retention.legacy) {
|
|
610
|
+
out.backupRetention = {
|
|
611
|
+
outsideRetention: retention.outsideRetention,
|
|
612
|
+
overdueSecrets: retention.overdueSecrets.length,
|
|
613
|
+
...(retention.legacy ? { legacyPending: retention.legacy } : {}),
|
|
614
|
+
};
|
|
615
|
+
if (retention.outsideRetention || retention.overdueSecrets.length) {
|
|
616
|
+
out.wouldDo.push(
|
|
617
|
+
`aplicar a retenção de .git/morph-spec/finish-backup: apagar ${retention.outsideRetention} backup(s) fora da política`
|
|
618
|
+
+ ` e ${retention.overdueSecrets.length} arquivo(s) .env* além de ${retention.policy.secretMaxAgeDays} dias`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch { /* o pré-voo nunca lança */ }
|
|
623
|
+
|
|
585
624
|
// ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
|
|
586
625
|
if (worktreePath) {
|
|
587
626
|
out.worktreePath = worktreePath.replace(/\\/g, '/');
|
|
@@ -664,14 +703,14 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
664
703
|
|
|
665
704
|
// ── merge integration (local, no push) ───────────────────────────────────────
|
|
666
705
|
|
|
667
|
-
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch }) {
|
|
706
|
+
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
|
|
668
707
|
const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
|
|
669
708
|
const ucNote = promotedNote(arch);
|
|
670
709
|
|
|
671
710
|
if (isWorktreeFlow) {
|
|
672
711
|
// The root stays on the default; shed this feature's own root-side scaffold,
|
|
673
712
|
// then guard the whole root before landing the merge commit.
|
|
674
|
-
const { shedBackup } = shedRootFeatureScope(root, feature);
|
|
713
|
+
const { shedBackup } = shedRootFeatureScope(root, feature, { stamp: backupStamp });
|
|
675
714
|
const backupNote = shedBackup ? { shedBackup } : {};
|
|
676
715
|
|
|
677
716
|
// Final gate before the merge commit lands. The pre-archive check above
|
|
@@ -721,7 +760,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
721
760
|
const deferred = sessionAnchoredInWorktree(worktreePath);
|
|
722
761
|
const teardown = deferred
|
|
723
762
|
? null
|
|
724
|
-
: await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
|
|
763
|
+
: await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
|
|
725
764
|
dropFromRootIndex(root, feature);
|
|
726
765
|
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
|
|
727
766
|
process.exit(0);
|
|
@@ -776,7 +815,7 @@ export function prNumberFromUrl(url) {
|
|
|
776
815
|
|
|
777
816
|
// ── PR integration (push + gh, no local merge) ───────────────────────────────
|
|
778
817
|
|
|
779
|
-
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch }) {
|
|
818
|
+
async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp }) {
|
|
780
819
|
try {
|
|
781
820
|
execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
|
|
782
821
|
} catch (err) {
|
|
@@ -812,13 +851,13 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
812
851
|
// later merges (carrying features/{f} → archive/{f}), the default would hold
|
|
813
852
|
// the feature in BOTH features/ and archive/ at once — active and archived.
|
|
814
853
|
// The branch/PR owns the authoritative copy; the root scaffold is transient.
|
|
815
|
-
shedBackup = shedRootFeatureScope(root, feature).shedBackup;
|
|
854
|
+
shedBackup = shedRootFeatureScope(root, feature, { stamp: backupStamp }).shedBackup;
|
|
816
855
|
// Same deferral as --merge: never tear down the worktree the running
|
|
817
856
|
// session is anchored in (see sessionAnchoredInWorktree).
|
|
818
857
|
if (sessionAnchoredInWorktree(worktreePath)) {
|
|
819
858
|
teardownDeferred = true;
|
|
820
859
|
} else {
|
|
821
|
-
teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false });
|
|
860
|
+
teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false, stamp: backupStamp });
|
|
822
861
|
}
|
|
823
862
|
// The shed removed the root's features/{f} scaffold from disk, but runArchive
|
|
824
863
|
// dropped the feature only from the WORKTREE's index (its cwd). Drop it from
|
|
@@ -874,16 +913,25 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
874
913
|
// ── output helpers ────────────────────────────────────────────────────────
|
|
875
914
|
|
|
876
915
|
/**
|
|
877
|
-
* Report the
|
|
878
|
-
* any
|
|
879
|
-
* `usecase-promoter.js`'s reservation-backed guard)
|
|
880
|
-
*
|
|
881
|
-
*
|
|
916
|
+
* Report what the archive did to the acervo: use cases promoted into
|
|
917
|
+
* `.morph/specs/usecases/`, any REFUSED for a numbering collision
|
|
918
|
+
* (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
|
|
919
|
+
* promoted copies that stayed superseded because the acervo said so
|
|
920
|
+
* (`keptSuperseded`), the ones the feature's spec.md revoked
|
|
921
|
+
* (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
|
|
922
|
+
* revocations it could not apply
|
|
923
|
+
* (`revocationProblems`). Every key is absent when there is nothing to report,
|
|
924
|
+
* so a feature that touches no acervo keeps the exact output shape it had
|
|
925
|
+
* before any of them existed.
|
|
882
926
|
*/
|
|
883
927
|
function promotedNote(arch) {
|
|
884
928
|
return {
|
|
885
929
|
...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
|
|
886
930
|
...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
|
|
931
|
+
...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
|
|
932
|
+
...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
|
|
933
|
+
...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
|
|
934
|
+
...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
|
|
887
935
|
};
|
|
888
936
|
}
|
|
889
937
|
|
|
@@ -4,6 +4,7 @@ import { getFeature } from '../../core/state/state-manager.js';
|
|
|
4
4
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
5
5
|
import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
|
|
6
6
|
import { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
|
|
7
|
+
import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
|
|
7
8
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
8
9
|
import { minimatch } from 'minimatch';
|
|
9
10
|
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
@@ -195,6 +196,12 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
195
196
|
designSystemDivergence: false,
|
|
196
197
|
};
|
|
197
198
|
|
|
199
|
+
// Só onde o checklist decide algo: no fecho do design e no ship. Fato de
|
|
200
|
+
// disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
|
|
201
|
+
if (gateName === 'uiux' || gateName === 'review') {
|
|
202
|
+
signals.screenChecklist = collectScreenChecklist(cwd, featureName, gateName);
|
|
203
|
+
}
|
|
204
|
+
|
|
198
205
|
const evalScore = compositeEvalScore(feature);
|
|
199
206
|
if (evalScore !== undefined) {
|
|
200
207
|
signals.evalScore = evalScore;
|
|
@@ -392,7 +399,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
392
399
|
const trust = options.trust || readTrustFromConfig();
|
|
393
400
|
const { pause, reason } = shouldPauseGate({ trust }, signals);
|
|
394
401
|
|
|
395
|
-
const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust });
|
|
402
|
+
const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust, cwd });
|
|
396
403
|
|
|
397
404
|
if (options.json) {
|
|
398
405
|
console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
5
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
6
|
+
import { mockupsHash } from '../../lib/specs/screen-checklist.js';
|
|
6
7
|
|
|
7
8
|
export const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
|
|
8
9
|
|
|
@@ -42,17 +43,20 @@ export function readTrustFromConfig() {
|
|
|
42
43
|
*
|
|
43
44
|
* @param {string} featureName
|
|
44
45
|
* @param {string} gateName
|
|
45
|
-
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string }} decision
|
|
46
|
-
* @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string }>}
|
|
47
|
-
* the exact entry written (decidedAt is server-side, generated here).
|
|
46
|
+
* @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string }} decision
|
|
47
|
+
* @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string, mockupsHash?: string|null }>}
|
|
48
|
+
* the exact entry written (decidedAt is server-side, generated here). A uiux
|
|
49
|
+
* decision also carries the hash of the mockup it saw: it answers that
|
|
50
|
+
* mockup's pause, not the next one's.
|
|
48
51
|
*/
|
|
49
|
-
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust } = {}) {
|
|
52
|
+
export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd() } = {}) {
|
|
50
53
|
const entry = {
|
|
51
54
|
pause,
|
|
52
55
|
reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
53
56
|
signals,
|
|
54
57
|
trust: trust || readTrustFromConfig(),
|
|
55
58
|
decidedAt: new Date().toISOString(),
|
|
59
|
+
...(gateName === 'uiux' ? { mockupsHash: mockupsHash(cwd, featureName) } : {}),
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
@@ -23,12 +23,18 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Exit 0 only for `ok` — anything needing an action exits non-zero so a caller
|
|
25
25
|
* can branch on it. `--json` always prints the machine-readable verdict.
|
|
26
|
+
*
|
|
27
|
+
* The output also carries `tree` (see tree-state.js): the working tree the
|
|
28
|
+
* orchestrator would otherwise ask git for, call by call, right after the same
|
|
29
|
+
* dispatch. It is information only and never changes the verdict.
|
|
26
30
|
*/
|
|
27
31
|
|
|
28
32
|
import chalk from 'chalk';
|
|
29
33
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
30
34
|
import { join } from 'path';
|
|
31
|
-
import { readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
35
|
+
import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
36
|
+
import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
|
|
37
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* The four mandatory items of the report contract, as detectable markers.
|
|
@@ -51,6 +57,16 @@ const REPORT_MARKERS = [
|
|
|
51
57
|
/** (1) "what was done" — prose beyond the structural markers. */
|
|
52
58
|
const MIN_PROSE_CHARS = 80;
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The partial-report marker of the dispatch contract (clause 5, ADR-004): a
|
|
62
|
+
* sub-agent nearing its `maxTurns` ceiling writes `PARCIAL` on the FIRST line.
|
|
63
|
+
* Read on purpose, never inferred from a missing score: the score marker above
|
|
64
|
+
* is loose by design, and a partial saying "auto-score: pendente (0-10 no
|
|
65
|
+
* final)" matches it — with the other items present it would audit as `ok`.
|
|
66
|
+
* Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
|
|
67
|
+
*/
|
|
68
|
+
const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
69
|
+
|
|
54
70
|
function reportPath(cwd, feature, taskId) {
|
|
55
71
|
return join(cwd, '.morph', 'features', feature, '3-implement', 'reports', `${taskId}.md`);
|
|
56
72
|
}
|
|
@@ -99,7 +115,11 @@ export function inspectReport(path) {
|
|
|
99
115
|
.trim();
|
|
100
116
|
if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
119
|
+
const partial = PARTIAL_MARKER.test(firstLine);
|
|
120
|
+
if (partial) missing.push('partial');
|
|
121
|
+
|
|
122
|
+
return { exists: true, complete: missing.length === 0, missing, partial };
|
|
103
123
|
}
|
|
104
124
|
|
|
105
125
|
/** How many of a task's declared outputs actually exist on disk. */
|
|
@@ -206,6 +226,7 @@ function observedEvidence(cwd, feature, task) {
|
|
|
206
226
|
const writes = [];
|
|
207
227
|
for (const e of events) {
|
|
208
228
|
if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
|
|
229
|
+
if (isFailedToolCall(e)) continue;
|
|
209
230
|
const ts = Date.parse(e?.ts ?? '');
|
|
210
231
|
if (!Number.isFinite(ts) || ts < startMs || ts > endMs) continue;
|
|
211
232
|
const target = toRepoRelative(e.target, cwd);
|
|
@@ -249,6 +270,10 @@ function decide(report, evidence, stale) {
|
|
|
249
270
|
? { verdict: 'report-stale', action: 'refresh-from-evidence' }
|
|
250
271
|
: { verdict: 'ok', action: 'proceed' };
|
|
251
272
|
}
|
|
273
|
+
// A PARCIAL report is an agent that stopped short (turn ceiling): the work
|
|
274
|
+
// resumes from its last `wip` commit — completing it DEGRADADO would stamp
|
|
275
|
+
// unfinished work as done.
|
|
276
|
+
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
252
277
|
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
253
278
|
if (evidence.outputsExpected === 0) {
|
|
254
279
|
// No declared outputs means no evidence either way. Re-dispatching costs a
|
|
@@ -273,13 +298,36 @@ const ACTION_HINT = {
|
|
|
273
298
|
proceed: 'relatório íntegro — siga o fluxo normal.',
|
|
274
299
|
'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: a task seguiu escrevendo arquivos depois que ele foi gravado, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
|
|
275
300
|
'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre a falha de protocolo no recap.',
|
|
301
|
+
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a task NÃO está pronta. Não carimbe DEGRADADO nem pontue — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
|
|
276
302
|
'accept-degraded': 'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório canônico a partir da mensagem de retorno, carimbado como DEGRADADO, e registre a falha de protocolo no recap e nas `notes` da task.',
|
|
277
303
|
redispatch: 're-despache a task UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
278
304
|
};
|
|
279
305
|
|
|
306
|
+
function snapshotPath(cwd, feature) {
|
|
307
|
+
return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The tree block, compared with the snapshot taken before the evaluator was
|
|
312
|
+
* dispatched when there is one. `beforeEval` takes a new snapshot after the
|
|
313
|
+
* comparison, so the output still shows what changed since the previous one.
|
|
314
|
+
*/
|
|
315
|
+
function treeFor(cwd, feature, beforeEval) {
|
|
316
|
+
const path = snapshotPath(cwd, feature);
|
|
317
|
+
let snapshot = null;
|
|
318
|
+
try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
|
|
319
|
+
const tree = treeState(cwd, { snapshot });
|
|
320
|
+
if (!beforeEval) return { tree };
|
|
321
|
+
const next = snapshotTree(cwd);
|
|
322
|
+
if (!next) return { tree, beforeEval: null };
|
|
323
|
+
atomicWriteJson(path, next);
|
|
324
|
+
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length } };
|
|
325
|
+
}
|
|
326
|
+
|
|
280
327
|
/**
|
|
281
328
|
* @param {string} feature
|
|
282
|
-
* @param {string|undefined} taskId
|
|
329
|
+
* @param {string|string[]|undefined} taskId - several ids audit a parallel
|
|
330
|
+
* batch whose tasks are not `done` yet, which the whole-feature mode skips
|
|
283
331
|
* @param {{ json?: boolean, cwd?: string }} [options]
|
|
284
332
|
*/
|
|
285
333
|
export function reportCheckCommand(feature, taskId, options = {}) {
|
|
@@ -303,18 +351,26 @@ export function reportCheckCommand(feature, taskId, options = {}) {
|
|
|
303
351
|
emit({ error: `2-plan/tasks.json de "${feature}" está corrompido`, feature }, 1);
|
|
304
352
|
}
|
|
305
353
|
|
|
354
|
+
if (Array.isArray(taskId)) {
|
|
355
|
+
const unknown = taskId.filter((id) => !tasks.some((t) => t.id === id));
|
|
356
|
+
if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
|
|
357
|
+
const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
|
|
358
|
+
const clean = results.every((r) => r.verdict === 'ok');
|
|
359
|
+
emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
360
|
+
}
|
|
361
|
+
|
|
306
362
|
if (taskId) {
|
|
307
363
|
const task = tasks.find((t) => t.id === taskId);
|
|
308
364
|
if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
|
|
309
365
|
const result = checkTask(cwd, feature, task);
|
|
310
|
-
emit({ feature, ...result, hint: ACTION_HINT[result.action] }, result.verdict === 'ok' ? 0 : 1);
|
|
366
|
+
emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval) }, result.verdict === 'ok' ? 0 : 1);
|
|
311
367
|
}
|
|
312
368
|
|
|
313
369
|
// Whole-feature audit: every DONE task owes a report; a pending one does not.
|
|
314
370
|
const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
|
|
315
371
|
const evaluator = inspectReport(evaluatorPath(cwd, feature));
|
|
316
372
|
const clean = results.every((r) => r.verdict === 'ok');
|
|
317
|
-
emit({ feature, results, evaluator, clean }, clean ? 0 : 1);
|
|
373
|
+
emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
318
374
|
}
|
|
319
375
|
|
|
320
376
|
function printHuman(obj) {
|
|
@@ -339,6 +395,7 @@ function printHuman(obj) {
|
|
|
339
395
|
if (obj.task) {
|
|
340
396
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature} ${obj.task}`));
|
|
341
397
|
line(obj);
|
|
398
|
+
printTree(obj.tree);
|
|
342
399
|
console.log('');
|
|
343
400
|
return;
|
|
344
401
|
}
|
|
@@ -346,9 +403,33 @@ function printHuman(obj) {
|
|
|
346
403
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
|
|
347
404
|
if (!obj.results.length) console.log(chalk.gray(' nenhuma task done — nada a auditar'));
|
|
348
405
|
for (const r of obj.results) line(r);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
406
|
+
if (obj.evaluator) {
|
|
407
|
+
console.log(
|
|
408
|
+
` ${obj.evaluator.exists ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
|
|
409
|
+
+ `evaluator-report.md ${obj.evaluator.exists ? (obj.evaluator.complete ? 'íntegro' : obj.evaluator.partial ? 'PARCIAL' : 'incompleto') : 'ausente'}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
printTree(obj.tree);
|
|
353
413
|
console.log('');
|
|
354
414
|
}
|
|
415
|
+
|
|
416
|
+
function printTree(tree) {
|
|
417
|
+
if (!tree) {
|
|
418
|
+
console.log(chalk.gray(' árvore: fora de um repositório git — não conferida'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const stat = tree.diffstat ? ` (+${tree.diffstat.insertions} −${tree.diffstat.deletions})` : '';
|
|
422
|
+
console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
|
|
423
|
+
for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
|
|
424
|
+
if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
|
|
425
|
+
if (tree.markers.length) {
|
|
426
|
+
console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
|
|
427
|
+
}
|
|
428
|
+
if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
|
|
429
|
+
const since = tree.sinceBeforeEval;
|
|
430
|
+
if (since) {
|
|
431
|
+
console.log(since.changedCount
|
|
432
|
+
? chalk.yellow(` ⚠ desde a foto de ${since.at}: ${since.changedCount} arquivo(s) mudaram — ${since.changed.join(', ')}`)
|
|
433
|
+
: chalk.green(` ✓ nada mudou fora de .morph/ desde a foto de ${since.at}`));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* how `morph-plan` gets that protection; omitting it degrades to "reserved,
|
|
12
12
|
* no owner recorded" — still collision-free, just unable to name the
|
|
13
13
|
* conflicting feature if one ever surfaces.
|
|
14
|
+
*
|
|
15
|
+
* `morph-spec usecase check <feature>` — see `usecaseCheckCommand` below.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import chalk from 'chalk';
|
|
@@ -18,6 +20,11 @@ import { existsSync, readdirSync } from 'fs';
|
|
|
18
20
|
import { join } from 'path';
|
|
19
21
|
import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
|
|
20
22
|
import { reserveUseCases } from '../../lib/specs/usecase-registry.js';
|
|
23
|
+
import { checkUseCases } from '../../lib/specs/usecase-check.js';
|
|
24
|
+
import { checkLifecycle } from '../../lib/specs/acervo-lifecycle.js';
|
|
25
|
+
import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
|
|
26
|
+
import { isValidFeatureName } from '../project/worktree.js';
|
|
27
|
+
import { printLifecycleWarnings } from '../tasks/dag.js';
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Best-effort feature-name inference when `--feature` is omitted: only when
|
|
@@ -75,3 +82,62 @@ export function usecaseReserveCommand(options = {}) {
|
|
|
75
82
|
else console.log('');
|
|
76
83
|
process.exit(0);
|
|
77
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `morph-spec usecase check <feature> [--json]` — the use-case half of `dag`,
|
|
88
|
+
* runnable before `tasks.json` exists: the feature's use cases and the
|
|
89
|
+
* revocations its `spec.md` declares, against the acervo's lifecycle. With a
|
|
90
|
+
* `tasks.json`, also the task → use case plumbing `dag` checks.
|
|
91
|
+
*
|
|
92
|
+
* Exit 1 only for what `dag` also refuses (a task pointing at a use case that
|
|
93
|
+
* does not exist, a tasks.json that does not validate); lifecycle findings are
|
|
94
|
+
* warnings.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} feature
|
|
97
|
+
* @param {{ json?: boolean, cwd?: string }} [options]
|
|
98
|
+
*/
|
|
99
|
+
export async function usecaseCheckCommand(feature, options = {}) {
|
|
100
|
+
const cwd = options.cwd || process.cwd();
|
|
101
|
+
const fail = (error) => {
|
|
102
|
+
if (options.json) console.log(JSON.stringify({ feature, error }));
|
|
103
|
+
else console.error(chalk.red(`\n❌ ${error}\n`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!isValidFeatureName(feature)) fail(`nome de feature inválido: "${feature}"`);
|
|
108
|
+
const featureDir = join(cwd, '.morph', 'features', feature);
|
|
109
|
+
if (!existsSync(featureDir)) fail(`feature não encontrada: .morph/features/${feature}`);
|
|
110
|
+
|
|
111
|
+
const tasksPath = join(featureDir, '2-plan', 'tasks.json');
|
|
112
|
+
let tasksData = null;
|
|
113
|
+
if (existsSync(tasksPath)) {
|
|
114
|
+
try {
|
|
115
|
+
({ data: tasksData } = await parseTasksJsonLenient(tasksPath));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
fail(`tasks.json inválido: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const useCases = tasksData ? checkUseCases(tasksData, join(featureDir, '2-plan', 'usecases')) : null;
|
|
122
|
+
const lifecycle = checkLifecycle({ cwd, featureDir, tasksData });
|
|
123
|
+
const broken = useCases?.errors.length > 0;
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify({ feature, useCases, lifecycle }, null, 2));
|
|
127
|
+
process.exit(broken ? 1 : 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (broken) {
|
|
131
|
+
console.error(chalk.red(`\n❌ Use cases — ${useCases.errors.length} erro(s)\n`));
|
|
132
|
+
for (const e of useCases.errors) console.error(chalk.red(` ${e}`));
|
|
133
|
+
}
|
|
134
|
+
if (useCases?.warnings.length) {
|
|
135
|
+
console.error(chalk.yellow(`\n⚠ Use cases — ${useCases.warnings.length} aviso(s)`));
|
|
136
|
+
for (const w of useCases.warnings) console.error(chalk.yellow(` ${w}`));
|
|
137
|
+
}
|
|
138
|
+
printLifecycleWarnings(lifecycle);
|
|
139
|
+
if (!broken && !useCases?.warnings.length && !lifecycle.warnings.length) {
|
|
140
|
+
console.log(chalk.green(`\n✅ ${feature}: use cases e acervo sem achados\n`));
|
|
141
|
+
}
|
|
142
|
+
process.exit(broken ? 1 : 0);
|
|
143
|
+
}
|
|
@@ -6,6 +6,15 @@ import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
|
6
6
|
import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
|
|
7
7
|
import { checkUseCases } from '../../lib/specs/usecase-check.js';
|
|
8
8
|
import { checkArtifactCriteria } from '../../lib/specs/done-criteria-check.js';
|
|
9
|
+
import { checkLifecycle } from '../../lib/specs/acervo-lifecycle.js';
|
|
10
|
+
|
|
11
|
+
/** Shared by `dag` and `usecase check` — the two surfaces of the same finding. */
|
|
12
|
+
export function printLifecycleWarnings(lifecycle) {
|
|
13
|
+
if (!lifecycle?.warnings?.length) return;
|
|
14
|
+
const n = lifecycle.warnings.length;
|
|
15
|
+
console.error(chalk.yellow(`\n⚠ Ciclo de vida do acervo — ${n} ${n === 1 ? 'aviso' : 'avisos'}`));
|
|
16
|
+
for (const w of lifecycle.warnings) console.error(chalk.yellow(` ${w}`));
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
|
|
@@ -19,7 +28,8 @@ import { checkArtifactCriteria } from '../../lib/specs/done-criteria-check.js';
|
|
|
19
28
|
*/
|
|
20
29
|
export async function dagCommand(featureName, options = {}) {
|
|
21
30
|
try {
|
|
22
|
-
const
|
|
31
|
+
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
32
|
+
const planDir = join(featureDir, '2-plan');
|
|
23
33
|
const tasksPath = join(planDir, 'tasks.json');
|
|
24
34
|
if (!existsSync(tasksPath)) {
|
|
25
35
|
console.error(chalk.red(`\n❌ tasks.json not found for feature "${featureName}"`));
|
|
@@ -58,10 +68,14 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
58
68
|
// own doneCriteria can describe an artifact with no use case in sight.
|
|
59
69
|
const artifactCriteria = checkArtifactCriteria(data, join(planDir, 'usecases'));
|
|
60
70
|
|
|
71
|
+
// Acervo lifecycle: a task citing a superseded use case, and the spec.md
|
|
72
|
+
// revocation rows the finish would fail to apply. Warnings only.
|
|
73
|
+
const lifecycle = checkLifecycle({ cwd: process.cwd(), featureDir, tasksData: data });
|
|
74
|
+
|
|
61
75
|
const plan = planDispatch(data);
|
|
62
76
|
|
|
63
77
|
if (options.json) {
|
|
64
|
-
console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria }, null, 2));
|
|
78
|
+
console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria, lifecycle }, null, 2));
|
|
65
79
|
if (ucCheck.errors.length) process.exit(1);
|
|
66
80
|
return;
|
|
67
81
|
}
|
|
@@ -83,6 +97,7 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
83
97
|
for (const w of artifactCriteria.warnings) console.error(chalk.yellow(` ${w}`));
|
|
84
98
|
console.error(chalk.gray(' Teste que lê texto de arquivo não prova comportamento (morph-plan §2, "Por que artefato").'));
|
|
85
99
|
}
|
|
100
|
+
printLifecycleWarnings(lifecycle);
|
|
86
101
|
|
|
87
102
|
console.log(chalk.bold(`\n📊 DAG — ${featureName}`));
|
|
88
103
|
console.log('━'.repeat(60));
|
|
@@ -36,7 +36,7 @@ import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
|
|
|
36
36
|
*/
|
|
37
37
|
const OUTPUT_SECTIONS = {
|
|
38
38
|
// morph-brainstorming emits: Problema, Escopo, Usuários e fluxos críticos,
|
|
39
|
-
// Stack, Riscos e dependências, Compliance de stack.
|
|
39
|
+
// Stack, Riscos e dependências, Compliance de stack, Conflitos com o acervo.
|
|
40
40
|
//
|
|
41
41
|
// "Critérios de aceite" is now OPTIONAL, not required: the behaviour layer
|
|
42
42
|
// moved to the use cases (2-plan/usecases/), whose post-conditions must be
|
|
@@ -44,6 +44,10 @@ const OUTPUT_SECTIONS = {
|
|
|
44
44
|
// `optional` — not `recommended` — precisely because the skill never emits it
|
|
45
45
|
// anymore: a warning on every new proposal would be systematic noise. Legacy
|
|
46
46
|
// proposals that DO carry the section keep validating exactly as before.
|
|
47
|
+
//
|
|
48
|
+
// "Conflitos com o acervo" is recommended: morph-brainstorming always emits
|
|
49
|
+
// it ("nenhum" included), so its absence means the acervo check never ran.
|
|
50
|
+
// Not required — proposals written before the section existed stay valid.
|
|
47
51
|
'proposal.md': {
|
|
48
52
|
required: [
|
|
49
53
|
['Problema', 'Problem'],
|
|
@@ -51,6 +55,7 @@ const OUTPUT_SECTIONS = {
|
|
|
51
55
|
],
|
|
52
56
|
recommended: [
|
|
53
57
|
['Riscos', 'Risks'],
|
|
58
|
+
['Conflitos', 'Conflicts'],
|
|
54
59
|
],
|
|
55
60
|
optional: [
|
|
56
61
|
['Critérios de aceite', 'Critério', 'Aceite', 'Acceptance Criteria', 'Acceptance', 'Success Criteria', 'Success Metrics'],
|
|
@@ -6,6 +6,8 @@ import { runVerify, readTask, VerifyUsageError } from '../../lib/validators/veri
|
|
|
6
6
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
7
7
|
import { getHeadSha } from '../../lib/git/default-branch.js';
|
|
8
8
|
import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
|
|
9
|
+
import { buildVerifyEvent, recordVerifyEvent, findRepeatedVerify } from '../../lib/telemetry/verify-event.js';
|
|
10
|
+
import { treeFingerprint } from '../../lib/git/tree-fingerprint.js';
|
|
9
11
|
|
|
10
12
|
/** ✓ pass · ✗ fail · – skip */
|
|
11
13
|
function statusIcon(status) {
|
|
@@ -237,6 +239,9 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
// Taken before the run: it is the tree the run is about to read.
|
|
243
|
+
const fingerprint = treeFingerprint(process.cwd());
|
|
244
|
+
|
|
240
245
|
let result;
|
|
241
246
|
try {
|
|
242
247
|
result = await runVerify(process.cwd(), featureName, taskId || null, {
|
|
@@ -335,6 +340,16 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
335
340
|
await updateFeature(featureName, 'verification', stamp);
|
|
336
341
|
}
|
|
337
342
|
|
|
343
|
+
// A feature-scope run over the exact tree of the previous one proves nothing
|
|
344
|
+
// new. Advisory only: the stamp and the exit code are the same either way.
|
|
345
|
+
let repeatOf = null;
|
|
346
|
+
try {
|
|
347
|
+
const cwd = process.cwd();
|
|
348
|
+
if (!taskId && persist) repeatOf = findRepeatedVerify(cwd, { feature: featureName, task: null, scope: result.scope, fingerprint });
|
|
349
|
+
const tree = resolve(cwd).replace(/\\/g, '/') === resolvePrimaryRoot(cwd) ? 'root' : 'worktree';
|
|
350
|
+
recordVerifyEvent(cwd, buildVerifyEvent(result, { sha: getHeadSha(cwd), tree, persisted: persist, reason, fingerprint }));
|
|
351
|
+
} catch { /* telemetry never changes the verdict */ }
|
|
352
|
+
|
|
338
353
|
// The refusal always goes to stderr, so it survives --json (whose stdout has
|
|
339
354
|
// to stay parseable) and is never something the reader has to go looking for.
|
|
340
355
|
if (result.baseline && !result.baseline.recorded) {
|
|
@@ -344,10 +359,14 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
344
359
|
}
|
|
345
360
|
|
|
346
361
|
if (options.json) {
|
|
347
|
-
const output = { ...result, persisted: persist, ...(persist ? {} : { reason }) };
|
|
362
|
+
const output = { ...result, persisted: persist, ...(persist ? {} : { reason }), repeatOf };
|
|
348
363
|
console.log(JSON.stringify(output, null, 2));
|
|
349
364
|
} else {
|
|
350
365
|
console.log(formatVerifyResult(result));
|
|
366
|
+
if (repeatOf) {
|
|
367
|
+
console.log(chalk.yellow(`⚠️ Mesma árvore do verify de feature de ${repeatOf.ts} (${repeatOf.status}): nada mudou fora de .morph/ desde então. `
|
|
368
|
+
+ 'Esta rodada não prova nada novo — reuse o resultado anterior, a menos que o ambiente (Docker, banco, serviço externo) tenha mudado.\n'));
|
|
369
|
+
}
|
|
351
370
|
if (result.baseline?.recorded) {
|
|
352
371
|
for (const u of result.baseline.updates) {
|
|
353
372
|
console.log(chalk.green(` baseline ${u.id}: ${u.from ?? '—'} → ${u.to}`));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { personaStandardIds } from '../../utils/agents-installer.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Scan agents.json (v5) — flat domain personas, no tiers.
|
|
@@ -18,7 +19,10 @@ export function scanAgents(frameworkDir) {
|
|
|
18
19
|
color: agent.color || null,
|
|
19
20
|
domains: agent.domains || [],
|
|
20
21
|
keywords: agent.keywords || [],
|
|
21
|
-
|
|
22
|
+
// Every standard the persona can cite, surfaces included — the same
|
|
23
|
+
// reading the installer and the validator use.
|
|
24
|
+
standards: personaStandardIds(agent),
|
|
25
|
+
surfaces: agent.surfaces || null,
|
|
22
26
|
stack: agent.stack || ['*'],
|
|
23
27
|
persona: agent.persona || '',
|
|
24
28
|
}));
|