@polymorphism-tech/morph-spec 8.43.0 → 8.45.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 +16 -13
- package/README.md +10 -10
- package/bin/morph-spec.js +10 -13
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -4
- package/docs/QUICKSTART.md +6 -6
- package/framework/CLAUDE.md +6 -8
- package/framework/MORPH.md +39 -45
- package/framework/agents.json +8 -9
- package/framework/commands/morph-apply.md +1 -3
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/docs/CLI.md +22 -17
- package/framework/evals/review-checklist.md +90 -0
- package/framework/evals/trace-schema.md +2 -12
- package/framework/hooks/README.md +2 -3
- package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
- package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
- package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
- package/framework/hooks/claude-code/core/validate-completion.js +3 -56
- package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/build-check.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +1 -1
- package/framework/hooks/shared/phase-utils.js +0 -5
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/phases.json +4 -6
- package/framework/rubrics/backend.json +1 -1
- package/framework/rubrics/default.json +1 -1
- package/framework/rubrics/frontend.json +1 -1
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/rules/morph-workflow.md +6 -6
- package/framework/schemas/feature.schema.json +2 -28
- package/framework/schemas/tasks.schema.json +6 -2
- package/framework/skills/README.md +1 -2
- 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 +145 -246
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
- package/framework/skills/core/morph-implement/SKILL.md +101 -201
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +171 -287
- package/framework/skills/core/morph-proposal/SKILL.md +4 -5
- package/framework/skills/core/morph-review/SKILL.md +109 -430
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +2 -2
- package/framework/skills/core/morph-uiux/SKILL.md +2 -3
- package/framework/standards/STANDARDS.json +54 -10
- package/framework/standards/ai-agents/agent-archetypes.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/agent-spec.md +1 -1
- package/framework/standards/ai-agents/batching.md +1 -1
- package/framework/standards/ai-agents/context-providers.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
- package/framework/standards/ai-agents/cost-and-budget.md +1 -1
- package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/mcp-server.md +1 -1
- package/framework/standards/ai-agents/mcp-tools.md +1 -1
- package/framework/standards/ai-agents/media-pipeline.md +1 -1
- package/framework/standards/ai-agents/media-video.md +1 -1
- package/framework/standards/ai-agents/middleware-patterns.md +1 -1
- package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
- package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
- package/framework/standards/ai-agents/modalities-vision.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
- package/framework/standards/ai-agents/observability-patterns.md +3 -3
- package/framework/standards/ai-agents/production.md +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +1 -1
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
- package/framework/standards/ai-agents/service-tier-flex.md +1 -1
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/ai-agents/testing-ai.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +226 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/README.md +1 -1
- package/framework/templates/REGISTRY.json +0 -81
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/spec.md +19 -205
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +2 -2
- package/src/commands/e2e/e2e.js +1 -1
- package/src/commands/project/heavy.js +1 -1
- package/src/commands/project/status.js +3 -3
- package/src/commands/project/worktree.js +34 -16
- package/src/commands/state/approve.js +9 -50
- package/src/commands/state/finish.js +4 -0
- package/src/commands/state/gate-check.js +18 -107
- package/src/commands/state/gate-decision.js +1 -1
- package/src/commands/state/report-check.js +42 -17
- package/src/commands/validation/mutate.js +1 -1
- package/src/commands/validation/scout-check.js +28 -0
- package/src/commands/validation/validate-feature.js +5 -32
- package/src/commands/validation/verify.js +16 -25
- package/src/core/paths/output-schema.js +0 -2
- package/src/core/state/state-manager.js +1 -2
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/lib/format.js +5 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Rubrics.jsx +7 -11
- package/src/dashboard/scanner/docs-scanner.js +1 -2
- package/src/dashboard/scanner/features-scanner.js +3 -4
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/output-collector.js +0 -1
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/e2e/compose-env.js +5 -2
- package/src/lib/evaluator-report.js +52 -334
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/handoff-generator.js +4 -5
- package/src/lib/git/evidence-staleness.js +39 -0
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/mutation/manifest.js +8 -2
- package/src/lib/mutation/report.js +5 -3
- package/src/lib/mutation/runner.js +9 -7
- package/src/lib/risk-detector.js +63 -125
- package/src/lib/scout/scout-check.js +293 -0
- package/src/lib/tasks/eval-node.js +4 -34
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +7 -5
- package/src/lib/tasks/test-nodes.js +1 -1
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/reuse-validator.js +6 -8
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/lib/worktree/env-allocator.js +35 -5
- package/src/scripts/setup-infra.js +1 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +5 -9
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +3 -10
- package/src/utils/skills-installer.js +1 -1
- package/framework/evals/proof-rubrics/README.md +0 -76
- package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
- package/framework/evals/proof-rubrics/fiacao.md +0 -54
- package/framework/evals/proof-rubrics/guard-cego.md +0 -52
- package/framework/evals/proof-rubrics/media-claims.md +0 -109
- package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
- package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
- package/framework/evals/rubrics/README.md +0 -45
- package/framework/evals/rubrics/architecture.md +0 -103
- package/framework/evals/rubrics/code-quality.md +0 -98
- package/framework/evals/rubrics/condensed.md +0 -57
- package/framework/evals/rubrics/contracts.md +0 -67
- package/framework/evals/rubrics/test-coverage.md +0 -99
- package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
- package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
- package/framework/evals/suites/standard.yaml +0 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
- package/framework/skills/core/morph-eval/SKILL.md +0 -64
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/framework/templates/docs/clarifications.md +0 -253
- package/framework/templates/docs/onboarding.md +0 -122
- package/framework/templates/docs/schema-analysis.md +0 -119
- package/framework/templates/docs/user-stories.md +0 -34
- package/src/commands/state/score.js +0 -147
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polymorphism-tech/morph-spec",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.45.0",
|
|
4
4
|
"description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"generate:check": "node scripts/generate-refs.js --check && node scripts/generate-ai-pin-props.js --check && node scripts/api-probe/probe.mjs --check",
|
|
45
45
|
"generate-standards": "node scripts/generate-standards-registry.js",
|
|
46
46
|
"generate-standards:check": "node scripts/generate-standards-registry.js --check",
|
|
47
|
-
"validate": "node scripts/validate-framework.js
|
|
47
|
+
"validate": "node scripts/validate-framework.js",
|
|
48
48
|
"freshness": "node scripts/check-freshness.js",
|
|
49
49
|
"verify": "npm run generate:check && npm run generate-standards:check && npm run validate && npm run freshness && npm test",
|
|
50
50
|
"test": "node scripts/run-tests.mjs",
|
package/src/commands/e2e/e2e.js
CHANGED
|
@@ -178,7 +178,7 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
178
178
|
if (options.json) {
|
|
179
179
|
console.log(JSON.stringify({ status: 'busy', feature: featureName, busy: { reason: err.reason, waitedMs: err.waitedMs } }, null, 2));
|
|
180
180
|
}
|
|
181
|
-
console.error(chalk.yellow(`\n⏳ e2e up não rodou: ${err.message}.
|
|
181
|
+
console.error(chalk.yellow(`\n⏳ e2e up não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\`).\n`));
|
|
182
182
|
process.exit(EXIT_BUSY);
|
|
183
183
|
} finally {
|
|
184
184
|
gate.release();
|
|
@@ -45,7 +45,7 @@ export function heavyRunCommand(command, options = {}) {
|
|
|
45
45
|
gate.enter(file);
|
|
46
46
|
} catch (err) {
|
|
47
47
|
if (!(err instanceof HeavyBusyError)) throw err;
|
|
48
|
-
console.error(chalk.yellow(`\n⏳ ${file} não rodou: ${err.message}.
|
|
48
|
+
console.error(chalk.yellow(`\n⏳ ${file} não rodou: ${err.message}. Não é vermelho — a fila já esperou o prazo dela: não repita em laço — siga sem esta rodada e avise o usuário (\`morph-spec heavy status\` mostra quem segura os slots).\n`));
|
|
49
49
|
process.exit(EXIT_BUSY);
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -64,7 +64,7 @@ function checkPhaseOutputs(phaseKey, feature) {
|
|
|
64
64
|
const phaseOutputMap = {
|
|
65
65
|
proposal: ['proposal', 'stackScan'],
|
|
66
66
|
uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
|
|
67
|
-
plan: ['spec', '
|
|
67
|
+
plan: ['spec', 'tasks', 'decisions'],
|
|
68
68
|
implement: ['recap'],
|
|
69
69
|
review: ['reviewReport']
|
|
70
70
|
};
|
|
@@ -102,7 +102,7 @@ function formatPhase(phaseKey, currentPhase, feature, options = {}) {
|
|
|
102
102
|
const phaseOutputMap = {
|
|
103
103
|
proposal: ['proposal', 'stackScan'],
|
|
104
104
|
uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
|
|
105
|
-
plan: ['spec', '
|
|
105
|
+
plan: ['spec', 'tasks', 'decisions'],
|
|
106
106
|
implement: ['recap'],
|
|
107
107
|
review: ['reviewReport']
|
|
108
108
|
};
|
|
@@ -160,7 +160,7 @@ function getNextSteps(feature) {
|
|
|
160
160
|
// Check for missing outputs in current phase
|
|
161
161
|
const phaseOutputMap = {
|
|
162
162
|
proposal: ['proposal'],
|
|
163
|
-
plan: ['spec', '
|
|
163
|
+
plan: ['spec', 'tasks'],
|
|
164
164
|
implement: ['recap'],
|
|
165
165
|
review: ['reviewReport']
|
|
166
166
|
};
|
|
@@ -882,19 +882,21 @@ export async function allocateWorktreeEnv(worktreePath, root, envKey, { morphFea
|
|
|
882
882
|
} catch { morphFeature = null; }
|
|
883
883
|
}
|
|
884
884
|
try {
|
|
885
|
-
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
|
|
885
|
+
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent, resolvePortAliases } =
|
|
886
886
|
await import('../../lib/worktree/env-allocator.js');
|
|
887
887
|
const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
888
888
|
if (!common) return null;
|
|
889
889
|
const gitCommonDir = resolve(root, common);
|
|
890
890
|
const block = allocateEnvBlock(envKey, { gitCommonDir, ...resolveFleetConfig(root) });
|
|
891
891
|
let stack = '';
|
|
892
|
+
let aliases = {};
|
|
892
893
|
try {
|
|
893
894
|
const cfg = JSON.parse(readFileSync(join(root, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
|
|
894
895
|
stack = cfg?.project?.stack || '';
|
|
896
|
+
aliases = resolvePortAliases(cfg).aliases;
|
|
895
897
|
} catch { /* fail-open — stackless env still carries PORT */ }
|
|
896
898
|
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
897
|
-
writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature }), 'utf8');
|
|
899
|
+
writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature, aliases }), 'utf8');
|
|
898
900
|
excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
|
|
899
901
|
return block;
|
|
900
902
|
} catch {
|
|
@@ -1238,10 +1240,17 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
|
|
|
1238
1240
|
}
|
|
1239
1241
|
|
|
1240
1242
|
/**
|
|
1241
|
-
* Clear the root-side `.worktree` registration for a finished feature
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1243
|
+
* Clear the root-side `.worktree` registration for a finished feature. Runs
|
|
1244
|
+
* whenever the worktree is REMOVED — including `--pr`, where no merge landed —
|
|
1245
|
+
* so a stale registration never trips the anti-dupe guard or the next finish's
|
|
1246
|
+
* root-clean.
|
|
1247
|
+
*
|
|
1248
|
+
* A registration that was committed on the default branch is cleared by a commit
|
|
1249
|
+
* scoped to the feature's own pathspec. An untracked scaffold (what
|
|
1250
|
+
* `create --worktree` leaves at the root) is never committed: the branch owns
|
|
1251
|
+
* the feature, and committing the skeleton would put it on the default next to
|
|
1252
|
+
* the archived copy. When the feature is already archived the skeleton is shed,
|
|
1253
|
+
* backup first, by the same primitive `finish` uses.
|
|
1245
1254
|
*
|
|
1246
1255
|
* Written DIRECTLY (never saveState, which re-persists every sibling's
|
|
1247
1256
|
* feature.json and can dirty the root outside this command's pathspec). The thin
|
|
@@ -1250,9 +1259,12 @@ export function primaryRootUnexplainedDirt(root, feature, opts = {}) {
|
|
|
1250
1259
|
*
|
|
1251
1260
|
* @param {string} root
|
|
1252
1261
|
* @param {string} feature
|
|
1262
|
+
* @param {{ stamp?: string }} [opts] - the caller's backup stamp, for the shed
|
|
1263
|
+
* @returns {Promise<{ shedBackup: string|null }>}
|
|
1253
1264
|
*/
|
|
1254
|
-
export async function clearRootWorktreeRegistration(root, feature) {
|
|
1265
|
+
export async function clearRootWorktreeRegistration(root, feature, { stamp } = {}) {
|
|
1255
1266
|
const featPathspec = `.morph/features/${feature}`;
|
|
1267
|
+
const gitOpts = { cwd: root, stdio: 'pipe' };
|
|
1256
1268
|
try {
|
|
1257
1269
|
const rootFjPath = join(root, '.morph', 'features', feature, 'feature.json');
|
|
1258
1270
|
if (existsSync(rootFjPath)) {
|
|
@@ -1265,16 +1277,19 @@ export async function clearRootWorktreeRegistration(root, feature) {
|
|
|
1265
1277
|
}
|
|
1266
1278
|
} catch { /* fail-open */ }
|
|
1267
1279
|
|
|
1268
|
-
// Clearing that field can itself dirty the root (a committed registration
|
|
1269
|
-
// survives the shed's checkout and the merge). Committing is in contract —
|
|
1270
|
-
// finish already writes commits — and scoped to this feature's path only.
|
|
1271
1280
|
try {
|
|
1272
|
-
const
|
|
1273
|
-
if (
|
|
1274
|
-
|
|
1275
|
-
|
|
1281
|
+
const tracked = execFileSync('git', ['ls-files', '--', featPathspec], gitOpts).toString().trim() !== '';
|
|
1282
|
+
if (!tracked) {
|
|
1283
|
+
if (!existsSync(join(root, '.morph', 'archive', feature))) return { shedBackup: null };
|
|
1284
|
+
return shedRootFeatureScope(root, feature, { stamp });
|
|
1285
|
+
}
|
|
1286
|
+
let unchanged = true;
|
|
1287
|
+
try { execFileSync('git', ['diff', '--quiet', 'HEAD', '--', featPathspec], gitOpts); } catch { unchanged = false; }
|
|
1288
|
+
if (!unchanged) {
|
|
1289
|
+
execFileSync('git', ['commit', '-m', `morph-spec finish: clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
|
|
1276
1290
|
}
|
|
1277
1291
|
} catch { /* fail-open — a dirty root is a nuisance, not a reason to fail */ }
|
|
1292
|
+
return { shedBackup: null };
|
|
1278
1293
|
}
|
|
1279
1294
|
|
|
1280
1295
|
/**
|
|
@@ -1423,7 +1438,7 @@ export function pickRemovalFailure(stderrs) {
|
|
|
1423
1438
|
* @returns {Promise<{ worktreeRemoved: boolean, ignoredBackup: string|null, discarded: string[],
|
|
1424
1439
|
* blockers: string[], ignoredBlocking: string[], shieldable: string[], declineReason: string|null,
|
|
1425
1440
|
* relinked: boolean, refused: boolean, infraLeftUnlinked: string[],
|
|
1426
|
-
* removalFailure: {kind: string, detail: string}|null }>}
|
|
1441
|
+
* removalFailure: {kind: string, detail: string}|null, rootShedBackup: string|null }>}
|
|
1427
1442
|
*/
|
|
1428
1443
|
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, relinkOnFailure = false, stamp }) {
|
|
1429
1444
|
// Drop the infra junctions FIRST, without following them — this is what makes
|
|
@@ -1453,6 +1468,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1453
1468
|
|
|
1454
1469
|
let worktreeRemoved = false;
|
|
1455
1470
|
let removalFailure = null;
|
|
1471
|
+
let rootShedBackup = null;
|
|
1456
1472
|
if (canRemove) {
|
|
1457
1473
|
const attempt = await attemptWorktreeRemoval(worktreePath, root);
|
|
1458
1474
|
worktreeRemoved = attempt.removed;
|
|
@@ -1473,7 +1489,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1473
1489
|
const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
1474
1490
|
if (common) releaseEnvBlock(feature, { gitCommonDir: resolve(root, common) });
|
|
1475
1491
|
} catch { /* fail-open */ }
|
|
1476
|
-
await clearRootWorktreeRegistration(root, feature);
|
|
1492
|
+
({ shedBackup: rootShedBackup } = await clearRootWorktreeRegistration(root, feature, { stamp }));
|
|
1477
1493
|
}
|
|
1478
1494
|
|
|
1479
1495
|
// Rollback (A4) — ONLY on a REFUSAL, never on a FAILURE. The two look alike
|
|
@@ -1568,6 +1584,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1568
1584
|
refused,
|
|
1569
1585
|
infraLeftUnlinked,
|
|
1570
1586
|
removalFailure,
|
|
1587
|
+
rootShedBackup,
|
|
1571
1588
|
};
|
|
1572
1589
|
}
|
|
1573
1590
|
|
|
@@ -1789,6 +1806,7 @@ export async function runWorktreeRemove(featureArg, options = {}) {
|
|
|
1789
1806
|
if (teardown.replicable.length) out.replicatedInRoot = teardown.replicable;
|
|
1790
1807
|
if (teardown.ignoredBackup) out.ignoredBackup = teardown.ignoredBackup;
|
|
1791
1808
|
if (teardown.shieldable.length) out.backedUp = teardown.shieldable;
|
|
1809
|
+
if (teardown.rootShedBackup) out.shedBackup = teardown.rootShedBackup;
|
|
1792
1810
|
} else {
|
|
1793
1811
|
out.refused = teardown.refused;
|
|
1794
1812
|
if (teardown.blockers.length) out.reparsePoints = teardown.blockers;
|
|
@@ -5,9 +5,10 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
|
|
7
7
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
8
|
-
import { evaluatorReportState,
|
|
8
|
+
import { evaluatorReportState, VERDICT_TOKEN } from '../../lib/evaluator-report.js';
|
|
9
9
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
10
10
|
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
11
|
+
import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The literal token `review-report.md` carries to declare which tree it
|
|
@@ -57,7 +58,7 @@ function reviewStaleness(featureName) {
|
|
|
57
58
|
// of staleness — say "unstamped" rather than block on a guess.
|
|
58
59
|
return { status: 'unstamped', head };
|
|
59
60
|
}
|
|
60
|
-
if (reviewed
|
|
61
|
+
if (!treeMovedSince(cwd, reviewed, head)) return { status: 'ok', reviewed, head };
|
|
61
62
|
|
|
62
63
|
let ahead = 0;
|
|
63
64
|
try {
|
|
@@ -246,18 +247,6 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
246
247
|
process.exit(1);
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
// Gate 3 evaluator backstop: the composite score of a feature comes from
|
|
250
|
-
// the INDEPENDENT evaluator (persona `evaluator`, fresh context) dispatched
|
|
251
|
-
// by morph-review — never from self-assessment by whoever implemented it.
|
|
252
|
-
// Nothing enforced that. When the dispatch was impossible (observed: a
|
|
253
|
-
// session instruction forbade sub-agents), the flow still produced a
|
|
254
|
-
// review-report.md carrying a composite score and a verdict, with a caveat
|
|
255
|
-
// in prose nobody is obliged to read. A gate that yields a grade without
|
|
256
|
-
// its evaluator is not a gate.
|
|
257
|
-
//
|
|
258
|
-
// The escape hatch is EXPLICIT and RECORDED (`--self-assessed`), never
|
|
259
|
-
// silent — the silence is the defect. `--force` deliberately does NOT
|
|
260
|
-
// bypass this: it is the out-of-order recovery flag, a different concern.
|
|
261
250
|
// Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
|
|
262
251
|
// moves. Observed: review-report.md written at b30ac6e, then T3 landed and
|
|
263
252
|
// inverted the default of a business rule; the report stayed in the repo
|
|
@@ -345,25 +334,16 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
345
334
|
process.exit(1);
|
|
346
335
|
}
|
|
347
336
|
} else {
|
|
348
|
-
// `existsSync` puro não bastava: um evaluator-report SEM o token
|
|
349
|
-
// `**Composite-Score:**` passava aqui E era descartado pelo gate-check
|
|
350
|
-
// (que então caía no proxy de taskScores). O resultado era um Gate 3
|
|
351
|
-
// carimbado `approvedBy: "auto"` sobre a média das auto-avaliações de
|
|
352
|
-
// quem implementou — precisamente o que MORPH.md §5 proíbe. Arquivo
|
|
353
|
-
// presente sem veredito legível é relatório OCO, não relatório.
|
|
354
337
|
const report = evaluatorReportState(process.cwd(), featureName);
|
|
355
338
|
if (report.status !== 'ok') {
|
|
356
339
|
if (!options.selfAssessed) {
|
|
357
340
|
if (report.status === 'missing') {
|
|
358
341
|
console.error(chalk.red('\n❌ Gate 3 exige o avaliador independente — `4-review/evaluator-report.md` não existe.'));
|
|
359
342
|
} else {
|
|
360
|
-
console.error(chalk.red(
|
|
361
|
-
console.error(chalk.
|
|
362
|
-
console.error(chalk.gray(' Sem ela o gate-check não lê o score real e cai na média das auto-avaliações.'));
|
|
363
|
-
console.error(chalk.gray(' Peça ao avaliador o relatório completo (morph-review §4) ou complete a linha.'));
|
|
343
|
+
console.error(chalk.red(`\n❌ Gate 3: o \`4-review/evaluator-report.md\` não tem \`${VERDICT_TOKEN} SHIP|FIX\`.`));
|
|
344
|
+
console.error(chalk.gray(' Peça ao avaliador o relatório completo (morph-review §4) ou retome-o por SendMessage.'));
|
|
364
345
|
}
|
|
365
|
-
console.error(chalk.gray(' O
|
|
366
|
-
console.error(chalk.gray(' despachado pela morph-review, nunca de auto-avaliação de quem implementou.'));
|
|
346
|
+
console.error(chalk.gray(' O veredito vem do avaliador (persona `evaluator`, contexto fresco), nunca de quem implementou.'));
|
|
367
347
|
console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
|
|
368
348
|
console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
|
|
369
349
|
process.exit(1);
|
|
@@ -371,30 +351,9 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
371
351
|
selfAssessed = true;
|
|
372
352
|
}
|
|
373
353
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// vigente) — mas aprovar em silêncio um relatório com notas superadas
|
|
378
|
-
// esconde do humano exatamente o fato que ele precisa ver: a nota MUDOU,
|
|
379
|
-
// e numa das rodadas medidas em campo ela CAIU (8.9 → 8.3, quando uma
|
|
380
|
-
// correção abriu um bloqueante).
|
|
381
|
-
//
|
|
382
|
-
// Aqui é o lugar: `approve` é a precondição humana do Gate 3 e a única
|
|
383
|
-
// superfície onde uma pessoa lê o veredito antes de carimbá-lo. Sem este
|
|
384
|
-
// bloco, `scoreTokens`/`superseded` seriam mitigação inerte — campo que
|
|
385
|
-
// ninguém lê não mitiga nada.
|
|
386
|
-
if (report.status === 'ok' && report.scoreTokens > 1) {
|
|
387
|
-
console.log(chalk.yellow(
|
|
388
|
-
`
|
|
389
|
-
⚠️ O relatório do avaliador tem ${report.scoreTokens} vereditos (o arquivo é append-only).`
|
|
390
|
-
));
|
|
391
|
-
console.log(chalk.gray(` Vigente (o que o gate usa): ${report.score}`));
|
|
392
|
-
console.log(chalk.gray(` Superado(s), na ordem do arquivo: ${report.superseded.join(', ')}`));
|
|
393
|
-
if (report.superseded.some((n) => n > report.score)) {
|
|
394
|
-
console.log(chalk.yellow(' A nota CAIU em relação a uma rodada anterior — confira o que a correção'));
|
|
395
|
-
console.log(chalk.yellow(' abriu antes de aprovar; o relatório mantém a análise das duas rodadas.'));
|
|
396
|
-
}
|
|
397
|
-
console.log('');
|
|
354
|
+
if (report.status === 'ok' && report.superseded.length > 0) {
|
|
355
|
+
console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
|
|
356
|
+
console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
|
|
398
357
|
}
|
|
399
358
|
}
|
|
400
359
|
}
|
|
@@ -65,6 +65,8 @@ 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, `decisions.md`, use cases promovidos, `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
|
+
|
|
68
70
|
/**
|
|
69
71
|
* `morph-spec finish <feature> --pr|--merge` — the single, symmetric close of a
|
|
70
72
|
* feature's lifecycle, for BOTH the worktree flow and the common flow.
|
|
@@ -132,6 +134,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
132
134
|
const emit = (obj) => {
|
|
133
135
|
if (resumed) obj = { ...obj, resumed: true };
|
|
134
136
|
if (obj?.finished === true) {
|
|
137
|
+
// The one path every finished:true takes; the --pr payload is built by hand, outside mergeOutput.
|
|
138
|
+
obj = { ...obj, memoryNote: MEMORY_NOTE };
|
|
135
139
|
try {
|
|
136
140
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
137
141
|
if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
|
|
@@ -3,11 +3,12 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
3
3
|
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
|
-
import {
|
|
6
|
+
import { evaluatorReportState, evaluatorMediaSection } from '../../lib/evaluator-report.js';
|
|
7
7
|
import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
|
|
8
8
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
9
9
|
import { minimatch } from 'minimatch';
|
|
10
10
|
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
11
|
+
import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
|
|
11
12
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
12
13
|
import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
|
|
13
14
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
@@ -23,13 +24,8 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
|
|
|
23
24
|
* - standardsViolations / designSystemDivergence: still an LLM's
|
|
24
25
|
* natural-language read (see the R4 report — no deterministic detector
|
|
25
26
|
* exists in JS yet).
|
|
26
|
-
* - evalScore: escape hatch for the TRUE Gate-3 composite score. Since R4
|
|
27
|
-
* the evaluator-report carries a machine-readable `**Composite-Score:**`
|
|
28
|
-
* token that this command parses by itself (see evaluatorCompositeScore),
|
|
29
|
-
* so the override is only needed for legacy reports without the token —
|
|
30
|
-
* there the taskScores-average proxy is the fallback.
|
|
31
27
|
*/
|
|
32
|
-
const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'
|
|
28
|
+
const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
31
|
* Os paths de standard que fazem uma feature ser "de midia".
|
|
@@ -95,31 +91,6 @@ function readTasks(featureName) {
|
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
/**
|
|
99
|
-
* Composite eval score PROXY: the average of `lastScore` across every task
|
|
100
|
-
* recorded in `feature.taskScores`, rounded to 1 decimal. `lastScore` (not
|
|
101
|
-
* `bestScore`) on purpose — a gate check reports current-state risk, and
|
|
102
|
-
* `bestScore` can be a stale high-water mark from an attempt the code no
|
|
103
|
-
* longer matches. "average" mirrors morph-apply's own Gate-3 risk-signal
|
|
104
|
-
* prose ("Score médio < 9 no review-report" — SKILL.md §5).
|
|
105
|
-
*
|
|
106
|
-
* This is explicitly NOT the true Gate-3 composite score (see
|
|
107
|
-
* OVERRIDABLE_SIGNAL_KEYS doc above) — undefined (key omitted) when no task
|
|
108
|
-
* has been scored yet, so shouldPauseGate's `typeof === 'number'` check
|
|
109
|
-
* naturally skips it instead of pausing on a false 0.
|
|
110
|
-
*
|
|
111
|
-
* @param {object} feature
|
|
112
|
-
* @returns {number|undefined}
|
|
113
|
-
*/
|
|
114
|
-
function compositeEvalScore(feature) {
|
|
115
|
-
const scores = Object.values(feature.taskScores || {})
|
|
116
|
-
.map((t) => t.lastScore)
|
|
117
|
-
.filter((n) => typeof n === 'number');
|
|
118
|
-
if (scores.length === 0) return undefined;
|
|
119
|
-
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
120
|
-
return Math.round(avg * 10) / 10;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
94
|
/**
|
|
124
95
|
* "Esta feature mexeu em prompt ou em modelo?" — em duas metades, porque uma só
|
|
125
96
|
* não distingue as duas situações que pedem ações diferentes.
|
|
@@ -202,78 +173,25 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
202
173
|
if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
|
|
203
174
|
|
|
204
175
|
// Só onde o checklist decide algo: no fecho do design e no ship. Fato de
|
|
205
|
-
// disco, fora de OVERRIDABLE_SIGNAL_KEYS
|
|
176
|
+
// disco, fora de OVERRIDABLE_SIGNAL_KEYS.
|
|
206
177
|
if (gateName === 'uiux' || gateName === 'review') {
|
|
207
178
|
signals.screenChecklist = collectScreenChecklist(cwd, featureName, gateName);
|
|
208
179
|
}
|
|
209
180
|
|
|
210
|
-
const evalScore = compositeEvalScore(feature);
|
|
211
|
-
if (evalScore !== undefined) {
|
|
212
|
-
signals.evalScore = evalScore;
|
|
213
|
-
signals.evalScoreSource = 'taskScores-proxy';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// No Gate 3, o score verdadeiro do avaliador independente (token
|
|
217
|
-
// Composite-Score no evaluator-report.md) tem precedência sobre o proxy.
|
|
218
181
|
if (gateName === 'review') {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
// `productionCode` viaja DENTRO do sinal para que risk-detector.js continue
|
|
232
|
-
// puro sobre fatos já reunidos: a divisão é "gate-check COLETA do disco,
|
|
233
|
-
// risk-detector DECIDE". Uma feature 100% `docs` não tem o que mutar, e
|
|
234
|
-
// exigir prova dela seria pedir uma cerimônia vazia — que é como uma regra
|
|
235
|
-
// vira ruído e depois é desligada.
|
|
236
|
-
//
|
|
237
|
-
// Task SEM `group` conta como código de produção: `group` é opcional no
|
|
238
|
-
// schema e o default documentado é `backend`. O conservador aqui é assumir
|
|
239
|
-
// que HÁ código — errar para o lado de pedir prova custa uma pausa; errar
|
|
240
|
-
// para o outro lado devolve o gate cego.
|
|
241
|
-
//
|
|
242
|
-
// `tasks` vazio (plano ausente ou ilegível) ⇒ `productionCode: false` ⇒ NÃO
|
|
243
|
-
// pausa. Fail-open deliberado: sem plano, não há como AFIRMAR que existe
|
|
244
|
-
// código de produção, e uma regra nova não pode travar feature legada.
|
|
245
|
-
//
|
|
246
|
-
// Nada disto entra em OVERRIDABLE_SIGNAL_KEYS: são fatos de disco, como
|
|
247
|
-
// `gate`/`workType`/`e2e*`. Um `--signals '{"mutationProof":{"section":true}}'`
|
|
248
|
-
// compraria o Gate 3 exatamente como um `e2eStatus: "pass"` forjado.
|
|
249
|
-
const proof = evaluatorProofState(cwd, featureName);
|
|
250
|
-
signals.mutationProof = {
|
|
251
|
-
section: proof.mutationSection,
|
|
252
|
-
min: minProofScore(proof.proofScores),
|
|
253
|
-
productionCode: tasks.some((t) => (t?.group ?? 'backend') !== 'docs'),
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// PROVA DE MÍDIA, mesmo módulo leitor e mesma disciplina anti-spoof. As
|
|
257
|
-
// quatro afirmações de uma feature de mídia (custo, revisão humana,
|
|
258
|
-
// marcação, hot path não bloqueado) são propriedades ESTRUTURAIS, e por isso
|
|
259
|
-
// `score` limita `architecture` e não `test-coverage` — daí `media` ficar
|
|
260
|
-
// FORA do `min` acima.
|
|
261
|
-
//
|
|
262
|
-
// `featureGeneratesMedia` é determinístico e vem do DISCO, nunca de
|
|
263
|
-
// julgamento: existe ao menos uma task cujo array `standards` cita um dos
|
|
264
|
-
// três paths de standard de mídia. É o mesmo formato do `productionCode`
|
|
265
|
-
// acima e usa um campo que o Gate 2 congelou. Mora numa função exportada
|
|
266
|
-
// (acima) porque um predicado que decide se um guardrail arma precisa de
|
|
267
|
-
// canário próprio — e porque o prefixo `framework/` precisa ser normalizado
|
|
268
|
-
// ali, num lugar só.
|
|
269
|
-
//
|
|
270
|
-
// A isenção é automática e é o comportamento certo: a maioria das features
|
|
271
|
-
// não gera mídia, e exigir a seção delas seria cerimônia vazia — que é como
|
|
272
|
-
// uma regra vira ruído e depois é desligada. `tasks` vazio ⇒ false ⇒ não
|
|
273
|
-
// pausa, o mesmo fail-open por ausência do sinal irmão.
|
|
182
|
+
const report = evaluatorReportState(cwd, featureName);
|
|
183
|
+
signals.evaluatorVerdict = report.status === 'ok'
|
|
184
|
+
? {
|
|
185
|
+
status: 'ok',
|
|
186
|
+
verdict: report.verdict,
|
|
187
|
+
blockers: report.blockers.length,
|
|
188
|
+
blockerIds: report.blockers.map((b) => b.id),
|
|
189
|
+
untyped: report.untyped.length,
|
|
190
|
+
superseded: report.superseded,
|
|
191
|
+
}
|
|
192
|
+
: { status: report.status };
|
|
274
193
|
signals.mediaClaims = {
|
|
275
|
-
section:
|
|
276
|
-
score: mediaProofScore(proof.proofScores),
|
|
194
|
+
section: evaluatorMediaSection(cwd, featureName),
|
|
277
195
|
featureGeneratesMedia: featureGeneratesMedia(tasks),
|
|
278
196
|
};
|
|
279
197
|
}
|
|
@@ -283,7 +201,7 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
283
201
|
// stamp com sha null (checkout sem git) ou stamp ausente lê como NOT stale.
|
|
284
202
|
signals.verification = verification;
|
|
285
203
|
if (verification?.status) signals.verificationStatus = verification.status;
|
|
286
|
-
signals.verificationStale =
|
|
204
|
+
signals.verificationStale = treeMovedSince(cwd, verification?.sha, sha);
|
|
287
205
|
|
|
288
206
|
// O nó e2e, separado do rollup de propósito: um `skip` NÃO reprova
|
|
289
207
|
// `verification.status`, então sem estas duas chaves o gate lê 'pass' tanto
|
|
@@ -373,7 +291,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
373
291
|
process.exit(1);
|
|
374
292
|
}
|
|
375
293
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
376
|
-
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"
|
|
294
|
+
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"standardsViolations":[]}\'\n'));
|
|
377
295
|
process.exit(1);
|
|
378
296
|
}
|
|
379
297
|
|
|
@@ -389,13 +307,6 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
389
307
|
if (key in parsed) overrides[key] = parsed[key];
|
|
390
308
|
}
|
|
391
309
|
|
|
392
|
-
// Um `evalScore` vindo de --signals substitui o VALOR mas não a
|
|
393
|
-
// PROCEDÊNCIA: sem isto o merge cego abaixo deixaria `evalScoreSource`
|
|
394
|
-
// com o que collectSignals escreveu ('taskScores-proxy'), e o sinal de
|
|
395
|
-
// auditoria passaria a mentir sobre de onde veio o número. `signals-
|
|
396
|
-
// override` não está em OVERRIDABLE_SIGNAL_KEYS — o caller não pode
|
|
397
|
-
// forjá-lo, mesmo anti-spoof de `gate`/`workType`.
|
|
398
|
-
if ('evalScore' in overrides) overrides.evalScoreSource = 'signals-override';
|
|
399
310
|
}
|
|
400
311
|
|
|
401
312
|
const cwd = process.cwd();
|
|
@@ -68,7 +68,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
|
|
|
68
68
|
* [--signals <json>] [--trust <mode>]` — record the trust decision for a gate
|
|
69
69
|
* under `gateDecisions` in the authoritative feature.json.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
71
|
+
* The timestamp (`decidedAt`) is
|
|
72
72
|
* stamped server-side and the write is atomic (state-manager). This replaces
|
|
73
73
|
* the manual Edit of feature.json from the skills — which collided with the
|
|
74
74
|
* state-sync hook rewriting the file between the agent's Read and Edit
|
|
@@ -35,9 +35,15 @@ import { join } from 'path';
|
|
|
35
35
|
import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
36
36
|
import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
|
|
37
37
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
38
|
+
import { minimatch } from 'minimatch';
|
|
39
|
+
import { evaluatorVerdictFromText, VERDICTS } from '../../lib/evaluator-report.js';
|
|
40
|
+
import { evaluatorModelFor } from '../../lib/risk-detector.js';
|
|
41
|
+
import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
42
|
+
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
43
|
+
import { featureGeneratesMedia } from './gate-check.js';
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
|
-
* The
|
|
46
|
+
* The mandatory items of the report contract, as detectable markers.
|
|
41
47
|
*
|
|
42
48
|
* HEURISTIC BY CONSTRUCTION, and deliberately loose: the contract asks for
|
|
43
49
|
* content, not for a fixed template, and reports written before this command
|
|
@@ -48,10 +54,8 @@ import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
|
48
54
|
const REPORT_MARKERS = [
|
|
49
55
|
// (2) files touched — a list line carrying a path or a backticked identifier
|
|
50
56
|
{ id: 'files', re: /^\s*[-*]\s+.*[`/\\].*$/m },
|
|
51
|
-
// (
|
|
52
|
-
{ id: '
|
|
53
|
-
// (3) problems found (or an explicit "none")
|
|
54
|
-
{ id: 'problems', re: /problema|issue|blocker|nenhum|impedimento/i },
|
|
57
|
+
// (3) each doneCriteria, met or not, with its evidence
|
|
58
|
+
{ id: 'criteria', re: /crit[ée]rio|doneCriteria|p[óo]s-condi[çc][ãa]o|atendid/i },
|
|
55
59
|
];
|
|
56
60
|
|
|
57
61
|
/** (1) "what was done" — prose beyond the structural markers. */
|
|
@@ -60,9 +64,6 @@ const MIN_PROSE_CHARS = 80;
|
|
|
60
64
|
/**
|
|
61
65
|
* The partial-report marker of the dispatch contract (clause 5, ADR-004): a
|
|
62
66
|
* 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
67
|
* Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
|
|
67
68
|
*/
|
|
68
69
|
const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
@@ -96,12 +97,12 @@ function evaluatorPath(cwd, feature) {
|
|
|
96
97
|
* items can be found in it.
|
|
97
98
|
*/
|
|
98
99
|
export function inspectReport(path) {
|
|
99
|
-
if (!existsSync(path)) return { exists: false, complete: false, missing: ['what', 'files', '
|
|
100
|
+
if (!existsSync(path)) return { exists: false, complete: false, missing: ['what', 'files', 'criteria'] };
|
|
100
101
|
let body = '';
|
|
101
102
|
try {
|
|
102
103
|
body = readFileSync(path, 'utf8');
|
|
103
104
|
} catch {
|
|
104
|
-
return { exists: true, complete: false, missing: ['what', 'files', '
|
|
105
|
+
return { exists: true, complete: false, missing: ['what', 'files', 'criteria'], unreadable: true };
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
|
|
@@ -122,6 +123,29 @@ export function inspectReport(path) {
|
|
|
122
123
|
return { exists: true, complete: missing.length === 0, missing, partial };
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/** The evaluator report is complete when it carries a readable verdict and is not partial. */
|
|
127
|
+
export function inspectEvaluatorReport(path) {
|
|
128
|
+
if (!existsSync(path)) return { exists: false, complete: false };
|
|
129
|
+
let body = '';
|
|
130
|
+
try { body = readFileSync(path, 'utf8'); } catch { return { exists: true, complete: false, unreadable: true }; }
|
|
131
|
+
const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
132
|
+
const partial = PARTIAL_MARKER.test(firstLine);
|
|
133
|
+
const { verdict } = evaluatorVerdictFromText(body);
|
|
134
|
+
return { exists: true, complete: !partial && VERDICTS.includes(verdict), partial, verdict: verdict ?? null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function evaluatorDispatch(cwd, tasks) {
|
|
138
|
+
let changedFiles = [];
|
|
139
|
+
try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
|
|
140
|
+
const watch = readMorphConfig(cwd)?.project?.evals?.watch;
|
|
141
|
+
const globs = Array.isArray(watch) ? watch.filter((g) => typeof g === 'string' && g.trim()) : [];
|
|
142
|
+
return evaluatorModelFor({
|
|
143
|
+
changedFiles,
|
|
144
|
+
promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
|
|
145
|
+
generatesMedia: featureGeneratesMedia(tasks),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
125
149
|
/** How many of a task's declared outputs actually exist on disk. */
|
|
126
150
|
function outputEvidence(cwd, taskOutputs) {
|
|
127
151
|
const expected = Array.isArray(taskOutputs) ? taskOutputs : [];
|
|
@@ -312,16 +336,17 @@ function snapshotPath(cwd, feature) {
|
|
|
312
336
|
* dispatched when there is one. `beforeEval` takes a new snapshot after the
|
|
313
337
|
* comparison, so the output still shows what changed since the previous one.
|
|
314
338
|
*/
|
|
315
|
-
function treeFor(cwd, feature, beforeEval) {
|
|
339
|
+
function treeFor(cwd, feature, beforeEval, tasks = []) {
|
|
316
340
|
const path = snapshotPath(cwd, feature);
|
|
317
341
|
let snapshot = null;
|
|
318
342
|
try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
|
|
319
343
|
const tree = treeState(cwd, { snapshot });
|
|
320
344
|
if (!beforeEval) return { tree };
|
|
345
|
+
const evaluatorModel = evaluatorDispatch(cwd, tasks);
|
|
321
346
|
const next = snapshotTree(cwd);
|
|
322
|
-
if (!next) return { tree, beforeEval: null };
|
|
347
|
+
if (!next) return { tree, beforeEval: null, evaluatorModel };
|
|
323
348
|
atomicWriteJson(path, next);
|
|
324
|
-
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length } };
|
|
349
|
+
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, evaluatorModel };
|
|
325
350
|
}
|
|
326
351
|
|
|
327
352
|
/**
|
|
@@ -356,21 +381,21 @@ export function reportCheckCommand(feature, taskId, options = {}) {
|
|
|
356
381
|
if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
|
|
357
382
|
const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
|
|
358
383
|
const clean = results.every((r) => r.verdict === 'ok');
|
|
359
|
-
emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
384
|
+
emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
|
|
360
385
|
}
|
|
361
386
|
|
|
362
387
|
if (taskId) {
|
|
363
388
|
const task = tasks.find((t) => t.id === taskId);
|
|
364
389
|
if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
|
|
365
390
|
const result = checkTask(cwd, feature, task);
|
|
366
|
-
emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval) }, result.verdict === 'ok' ? 0 : 1);
|
|
391
|
+
emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval, tasks) }, result.verdict === 'ok' ? 0 : 1);
|
|
367
392
|
}
|
|
368
393
|
|
|
369
394
|
// Whole-feature audit: every DONE task owes a report; a pending one does not.
|
|
370
395
|
const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
|
|
371
|
-
const evaluator =
|
|
396
|
+
const evaluator = inspectEvaluatorReport(evaluatorPath(cwd, feature));
|
|
372
397
|
const clean = results.every((r) => r.verdict === 'ok');
|
|
373
|
-
emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
|
|
398
|
+
emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval, tasks) }, clean ? 0 : 1);
|
|
374
399
|
}
|
|
375
400
|
|
|
376
401
|
function printHuman(obj) {
|