@polymorphism-tech/morph-spec 10.1.1 → 10.5.2
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/README.md +3 -7
- package/bin/morph-spec.js +10 -41
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +1 -2
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-preflight.md +3 -4
- package/framework/docs/CLI.md +45 -22
- package/framework/evals/review-checklist.md +5 -5
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/core/heavy-gate.js +21 -2
- package/framework/hooks/claude-code/core/validate-completion.js +0 -35
- package/framework/hooks/shared/e2e-check.js +44 -7
- package/framework/hooks/shared/heavy-command.js +12 -1
- package/framework/hooks/shared/heavy-config.js +12 -0
- package/framework/hooks/shared/host-services.js +9 -6
- package/framework/hooks/shared/next-step.js +12 -8
- package/framework/phases.json +4 -4
- package/framework/rules/plan-grammar.md +4 -1
- package/framework/schemas/config.schema.json +59 -1
- package/framework/schemas/feature.schema.json +15 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +8 -5
- package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
- package/framework/skills/core/morph-apply/references/machine.md +3 -2
- package/framework/skills/core/morph-apply/references/review.md +9 -7
- package/framework/skills/core/morph-plan/SKILL.md +7 -6
- package/framework/skills/core/morph-plan/references/critique.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +2 -2
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- 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/plan.md +2 -1
- package/framework/templates/examples/design-system-examples.md +0 -3
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- 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/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/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/project/doctor.js +0 -1
- package/src/commands/project/heavy.js +13 -0
- package/src/commands/state/approve.js +9 -109
- package/src/commands/state/report-check.js +44 -2
- package/src/commands/validation/verify.js +57 -7
- package/src/core/state/state-manager.js +0 -24
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/heavy/cleanup.js +8 -2
- package/src/lib/heavy/gate.js +24 -1
- package/src/lib/heavy/reclaim.js +107 -0
- package/src/lib/heavy/watchdog.js +46 -0
- package/src/lib/next-step.js +1 -0
- package/src/lib/plan/checks.js +1 -1
- package/src/lib/plan/critique.js +23 -3
- package/src/lib/procs/prune.js +23 -1
- package/src/lib/review-confirmation.js +67 -0
- package/src/lib/review-reports.js +1 -0
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/tasks/test-nodes.js +10 -0
- package/src/lib/tasks/test-runner.js +37 -43
- package/src/lib/telemetry/telemetry-reader.js +1 -2
- package/src/lib/telemetry/verify-event.js +7 -0
- package/src/lib/validators/verify-cache.js +63 -0
- package/src/lib/validators/verify-runner.js +131 -26
- package/src/lib/verify/affected.js +141 -0
- package/src/lib/verify/detached.js +154 -0
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/hooks-installer.js +0 -20
- package/bin/validate.js +0 -212
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/src/commands/observability/telemetry.js +0 -60
- package/src/commands/validation/validate-feature.js +0 -337
|
@@ -20,10 +20,6 @@ function auditReport(featurePath) {
|
|
|
20
20
|
: [];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
24
|
-
|
|
25
|
-
const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
|
|
26
|
-
|
|
27
23
|
export async function run(payload) {
|
|
28
24
|
try {
|
|
29
25
|
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
|
|
@@ -42,37 +38,6 @@ export async function run(payload) {
|
|
|
42
38
|
|
|
43
39
|
const currentPhase = feature.phase || derivePhaseForFeature(name, cwd);
|
|
44
40
|
|
|
45
|
-
const watchPath = join(memDir, 'phase-watch.json');
|
|
46
|
-
let phaseWatch = {};
|
|
47
|
-
try {
|
|
48
|
-
if (existsSync(watchPath)) phaseWatch = JSON.parse(readFileSync(watchPath, 'utf-8'));
|
|
49
|
-
} catch { /* corrupt file — start fresh */ }
|
|
50
|
-
|
|
51
|
-
const lastPhase = phaseWatch[name];
|
|
52
|
-
|
|
53
|
-
// Always stored, so the alert fires once per transition.
|
|
54
|
-
try {
|
|
55
|
-
phaseWatch[name] = currentPhase;
|
|
56
|
-
writeFileSync(watchPath, JSON.stringify(phaseWatch, null, 2), 'utf-8');
|
|
57
|
-
} catch {}
|
|
58
|
-
|
|
59
|
-
const isPhaseTransition = lastPhase && lastPhase !== currentPhase;
|
|
60
|
-
const enteringImplement = COMPACT_TRIGGER_PHASES.has(currentPhase);
|
|
61
|
-
const comingFromHeavyPhase = HEAVY_SOURCE_PHASES.has(lastPhase);
|
|
62
|
-
|
|
63
|
-
if (isPhaseTransition && enteringImplement && comingFromHeavyPhase) {
|
|
64
|
-
warnings.push(
|
|
65
|
-
`⚡ Phase advanced: ${lastPhase} → implement`,
|
|
66
|
-
``,
|
|
67
|
-
`💡 Recommended before starting implementation:`,
|
|
68
|
-
` Run /compact to free up context — plan.md and the design`,
|
|
69
|
-
` loaded during planning take significant space. A compact now gives`,
|
|
70
|
-
` Claude a clean slate focused on writing code.`,
|
|
71
|
-
``,
|
|
72
|
-
` After /compact, resume with: /morph-apply ${name}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
41
|
// Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
|
|
77
42
|
try {
|
|
78
43
|
const featurePath = join(cwd, '.morph', 'features', name);
|
|
@@ -187,23 +187,40 @@ export function decideIsolation({ findings = [], insideWorktree = false } = {})
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// HEAD alone would let a stack built before an uncommitted edit read as fresh: status and diff
|
|
190
|
-
// make any edit move the stamp.
|
|
190
|
+
// make any edit move the stamp. `.morph/` stays out, config.json aside: the verify's own commits
|
|
191
|
+
// must not rebuild a stack whose source did not change. `null` outside a git repo.
|
|
191
192
|
export function computeSourceStamp(cwd, exec = defaultExec) {
|
|
192
193
|
const head = exec('git', ['rev-parse', 'HEAD'], { cwd });
|
|
193
194
|
if (head.code !== 0) return null;
|
|
194
195
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
196
|
+
const tree = exec('git', ['ls-tree', 'HEAD'], { cwd });
|
|
197
|
+
const status = exec('git', ['status', '--porcelain', ...OUTSIDE_MORPH], { cwd });
|
|
198
|
+
const diff = exec('git', ['diff', 'HEAD', ...OUTSIDE_MORPH], { cwd });
|
|
199
|
+
const entries = tree.code === 0
|
|
200
|
+
? tree.output.split('\n').filter((line) => line.split('\t')[1] !== '.morph').join('\n')
|
|
201
|
+
: head.output.trim();
|
|
197
202
|
|
|
198
203
|
return createHash('sha1')
|
|
199
|
-
.update(
|
|
204
|
+
.update(entries)
|
|
200
205
|
.update('\0')
|
|
201
206
|
.update(status.code === 0 ? status.output : '')
|
|
202
207
|
.update('\0')
|
|
203
208
|
.update(diff.code === 0 ? diff.output : '')
|
|
209
|
+
.update('\0')
|
|
210
|
+
.update(readConfigText(cwd))
|
|
204
211
|
.digest('hex');
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
const OUTSIDE_MORPH = ['--', '.', ':(exclude).morph'];
|
|
215
|
+
|
|
216
|
+
function readConfigText(cwd) {
|
|
217
|
+
try {
|
|
218
|
+
return readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8');
|
|
219
|
+
} catch {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
207
224
|
export function stampPath(gitCommonDir, projectName) {
|
|
208
225
|
return join(gitCommonDir, 'morph-spec', `e2e-stamp-${projectName}.json`);
|
|
209
226
|
}
|
|
@@ -775,6 +792,11 @@ export function bringUpE2e(
|
|
|
775
792
|
return result({ status: 'pass', stage: null, reason: stack.reason, hostServices: host, auth });
|
|
776
793
|
}
|
|
777
794
|
|
|
795
|
+
function specTimeoutFor(config) {
|
|
796
|
+
const configured = config?.e2e?.specTimeoutMs;
|
|
797
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_SPEC_TIMEOUT;
|
|
798
|
+
}
|
|
799
|
+
|
|
778
800
|
/** @returns {{ status: 'pass'|'fail'|'skip', command: string|null, output: string, reason: string|null, warnings: string[] }} */
|
|
779
801
|
export function runE2eCheck(cwd, options = {}) {
|
|
780
802
|
const {
|
|
@@ -787,7 +809,7 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
787
809
|
forceFresh = false,
|
|
788
810
|
insideWorktree = isSecondaryWorktree(cwd),
|
|
789
811
|
healthTimeout = DEFAULT_HEALTH_TIMEOUT,
|
|
790
|
-
specTimeout =
|
|
812
|
+
specTimeout = specTimeoutFor(config),
|
|
791
813
|
beforeHeavy = null,
|
|
792
814
|
countSpecs = null,
|
|
793
815
|
_exec: exec = defaultExec,
|
|
@@ -858,6 +880,12 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
858
880
|
}
|
|
859
881
|
}
|
|
860
882
|
|
|
883
|
+
// Playwright's list reporter names a failure as ` 1) [chromium] › e2e/a.spec.ts:3:5 › title`.
|
|
884
|
+
function failedSpecNames(output) {
|
|
885
|
+
const names = [...String(output).matchAll(/^\s+\d+\)\s+(.+›.+)$/gm)].map((match) => match[1].trim());
|
|
886
|
+
return [...new Set(names)];
|
|
887
|
+
}
|
|
888
|
+
|
|
861
889
|
// The suite gets the stack's env (with PLAYWRIGHT_BASE_URL): without it the config hits a hardcoded
|
|
862
890
|
// localhost:3000 while the app sits on the feature's port, an ECONNREFUSED that looks like a broken app.
|
|
863
891
|
function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec) {
|
|
@@ -885,18 +913,27 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv,
|
|
|
885
913
|
// Counted from the FULL output, before the tail cut; unparseable is null, never 0.
|
|
886
914
|
const count = typeof countSpecs === 'function' ? (countSpecs(run.output) ?? null) : null;
|
|
887
915
|
|
|
888
|
-
|
|
916
|
+
// One rerun of the failed specs only: a flake must not cost a whole verify. `--last-failed`
|
|
917
|
+
// needs Playwright 1.44+; an older one exits non-zero, and the node stays red.
|
|
918
|
+
const retry = run.code !== 0 && spec.args.includes('playwright')
|
|
919
|
+
? exec(spec.command, [...spec.args, '--last-failed'], { cwd: spec.cwd, timeout: specTimeout, env: composeEnv })
|
|
920
|
+
: null;
|
|
921
|
+
|
|
922
|
+
if (run.code !== 0 && retry?.code !== 0) {
|
|
889
923
|
return {
|
|
890
924
|
status: 'fail',
|
|
891
925
|
command: commandLine,
|
|
892
|
-
output: tailLines(run.output, OUTPUT_TAIL_LINES),
|
|
926
|
+
output: tailLines(retry?.output ?? run.output, OUTPUT_TAIL_LINES),
|
|
893
927
|
reason: 'e2e specs failed against the running stack',
|
|
894
928
|
count,
|
|
895
929
|
};
|
|
896
930
|
}
|
|
897
931
|
|
|
932
|
+
const flakySpecs = retry ? failedSpecNames(run.output) : [];
|
|
933
|
+
|
|
898
934
|
return {
|
|
899
935
|
status: 'pass',
|
|
936
|
+
flakySpecs,
|
|
900
937
|
command: commandLine,
|
|
901
938
|
output: '',
|
|
902
939
|
reason: notes.length ? notes.join('; ') : null,
|
|
@@ -57,6 +57,14 @@ function firstArg(args, valueFlags = new Set()) {
|
|
|
57
57
|
return undefined;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const POWERSHELL_HOSTS = new Set(['powershell', 'pwsh']);
|
|
61
|
+
const TEST_SCRIPT_SUFFIX = 'test-script';
|
|
62
|
+
|
|
63
|
+
function runsTestScript(args) {
|
|
64
|
+
const script = args.find((a) => /\.ps1$/i.test(a));
|
|
65
|
+
return Boolean(script) && /test/i.test(script.replace(/^.*[\\/]/, ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function kindOfTokens(tokens) {
|
|
61
69
|
const [head, ...args] = tokens;
|
|
62
70
|
if (!head || args.some((a) => INFO_FLAGS.has(a))) return null;
|
|
@@ -64,6 +72,7 @@ function kindOfTokens(tokens) {
|
|
|
64
72
|
const sub = firstArg(args, exe === 'docker' || exe === 'docker-compose' ? DOCKER_VALUE_FLAGS
|
|
65
73
|
: PACKAGE_MANAGERS.has(exe) ? PM_DIR_FLAGS : undefined);
|
|
66
74
|
|
|
75
|
+
if (POWERSHELL_HOSTS.has(exe)) return runsTestScript(args) ? `${exe} ${TEST_SCRIPT_SUFFIX}` : null;
|
|
67
76
|
if (exe === 'dotnet') return DOTNET_HEAVY.has(sub) ? `dotnet ${sub}` : null;
|
|
68
77
|
if (exe === 'msbuild') return 'msbuild';
|
|
69
78
|
if (exe === 'vitest' || exe === 'jest') return exe;
|
|
@@ -115,7 +124,7 @@ export const REVIEW_AGENT_TYPES = new Set(['morph-verifier', 'morph-reviewer', '
|
|
|
115
124
|
export const QUEUE_FULL_EXIT = 3;
|
|
116
125
|
|
|
117
126
|
const LIST_FLAGS = new Set(['--list', '--listTests', '--list-tests']);
|
|
118
|
-
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?))$/;
|
|
127
|
+
const TEST_LABEL = /^(dotnet (test|vstest)|(npx )?(vitest|jest|playwright test)|(npm|pnpm|yarn|bun) (test|run test(:\S+)?)|(powershell|pwsh) test-script)$/;
|
|
119
128
|
const NODE_RUNNERS = new Set(['vitest', 'jest']);
|
|
120
129
|
|
|
121
130
|
function morphArgs(tokens) {
|
|
@@ -158,6 +167,8 @@ const TEST_PATH = /[\\/]|\.(spec|test)\b|\.[cm]?[jt]sx?$/;
|
|
|
158
167
|
const PATH_VALUE_FLAGS = new Set(['-c', '--config', '--project', '--root', '--workspace', '--outputFile', '--reporter', ...PM_DIR_FLAGS]);
|
|
159
168
|
|
|
160
169
|
function isFilteredRun(kind, tokens) {
|
|
170
|
+
// A script's arguments are its own contract: no flag here proves it narrowed the run.
|
|
171
|
+
if (kind.endsWith(TEST_SCRIPT_SUFFIX)) return false;
|
|
161
172
|
const runnerAt = executable(tokens[0]) === 'node'
|
|
162
173
|
? tokens.findIndex((t, i) => i > 0 && executable(t).replace(/\.[cm]?js$/, '') === kind)
|
|
163
174
|
: 0;
|
|
@@ -10,6 +10,11 @@ export const HEAVY_DEFAULTS = Object.freeze({
|
|
|
10
10
|
minFreeMemGbByKind: Object.freeze({ unit: 1.5, build: 4, e2e: 4 }),
|
|
11
11
|
waitTimeoutSec: 600,
|
|
12
12
|
maxHoldMinutes: 90,
|
|
13
|
+
reclaimIdleMinutes: 20,
|
|
14
|
+
reclaimContainers: Object.freeze(['morph-pgtest']),
|
|
15
|
+
detachedWaitMinutes: 60,
|
|
16
|
+
criticalFreeMemGb: 1,
|
|
17
|
+
yieldToUser: true,
|
|
13
18
|
shutdownBuildServer: true,
|
|
14
19
|
disableNodeReuse: false,
|
|
15
20
|
gateBash: true,
|
|
@@ -27,6 +32,11 @@ const VALID = {
|
|
|
27
32
|
minFreeMemGbByKind: isPlainObject,
|
|
28
33
|
waitTimeoutSec: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
29
34
|
maxHoldMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v > 0,
|
|
35
|
+
reclaimIdleMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
36
|
+
yieldToUser: isBool,
|
|
37
|
+
reclaimContainers: (v) => Array.isArray(v) && v.every((p) => typeof p === 'string' && /^[\w.-]+$/.test(p)),
|
|
38
|
+
detachedWaitMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
39
|
+
criticalFreeMemGb: isGb,
|
|
30
40
|
shutdownBuildServer: isBool,
|
|
31
41
|
disableNodeReuse: isBool,
|
|
32
42
|
gateBash: isBool,
|
|
@@ -66,6 +76,8 @@ export function resolveHeavyConfig(config, env = process.env) {
|
|
|
66
76
|
if (/^(off|0|false)$/i.test(env.MORPH_HEAVY ?? '')) out.enabled = false;
|
|
67
77
|
const envSlots = Number(env.MORPH_HEAVY_SLOTS);
|
|
68
78
|
if (env.MORPH_HEAVY_SLOTS !== undefined && VALID.slots(envSlots)) out.slots = envSlots;
|
|
79
|
+
const envWait = Number(env.MORPH_HEAVY_WAIT_SEC);
|
|
80
|
+
if (env.MORPH_HEAVY_WAIT_SEC && VALID.waitTimeoutSec(envWait)) out.waitTimeoutSec = envWait;
|
|
69
81
|
|
|
70
82
|
return { ...out, warnings };
|
|
71
83
|
}
|
|
@@ -134,7 +134,7 @@ function probeHealth(url, timeoutMs = 1_500) {
|
|
|
134
134
|
|
|
135
135
|
// Only the recorded identity may be killed: a recycled PID belongs to someone else.
|
|
136
136
|
function killRecord(record, readCreatedAt) {
|
|
137
|
-
if (!
|
|
137
|
+
if (!processRunning(record.pid)) {
|
|
138
138
|
return { killed: false, warning: `hostService "${record.name}": PID ${record.pid} was no longer running; record dropped` };
|
|
139
139
|
}
|
|
140
140
|
const current = readCreatedAt(record.pid);
|
|
@@ -157,11 +157,14 @@ function waitUntil(predicate) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// The process that spawned the shell blocks in a sync loop and never reaps it: a zombie is not alive.
|
|
160
|
-
function
|
|
160
|
+
export function processRunning(pid) {
|
|
161
161
|
if (!pidAlive(pid)) return false;
|
|
162
162
|
if (process.platform === 'win32') return true;
|
|
163
163
|
const r = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8' });
|
|
164
|
-
|
|
164
|
+
if (r.error) return true;
|
|
165
|
+
// ps exits non-zero when the process vanished between pidAlive and here.
|
|
166
|
+
const stat = String(r.stdout).trim();
|
|
167
|
+
return stat !== '' && !stat.startsWith('Z');
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
function groupAlive(pgid) {
|
|
@@ -325,13 +328,13 @@ function launch(svc, ctx, deadline, exec) {
|
|
|
325
328
|
|
|
326
329
|
if (!svc.health) {
|
|
327
330
|
sleep(Math.min(NO_HEALTH_GRACE_MS, Math.max(remaining(), 0)));
|
|
328
|
-
if (!
|
|
331
|
+
if (!processRunning(child.pid)) return { ...fail('exited right after start', readLogTail(log)), record };
|
|
329
332
|
return { ok: true, record };
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
while (true) {
|
|
333
336
|
if (probeHealth(svc.health)) return { ok: true, record };
|
|
334
|
-
if (!
|
|
337
|
+
if (!processRunning(child.pid)) return { ...fail('exited before its health check went green', readLogTail(log)), record };
|
|
335
338
|
if (remaining() <= 0) {
|
|
336
339
|
return { ...fail(`health ${svc.health} not green within ${svc.timeoutMs} ms`, readLogTail(log)), record };
|
|
337
340
|
}
|
|
@@ -340,7 +343,7 @@ function launch(svc, ctx, deadline, exec) {
|
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
function isReusable(record, svc, ctx) {
|
|
343
|
-
if (!record || !
|
|
346
|
+
if (!record || !processRunning(record.pid)) return false;
|
|
344
347
|
if (ctx.readCreatedAt(record.pid) !== record.createdAt) return false;
|
|
345
348
|
return svc.health ? probeHealth(svc.health) : true;
|
|
346
349
|
}
|
|
@@ -30,10 +30,10 @@ const HOW = {
|
|
|
30
30
|
'Registre a resposta em ## Notas, volte o Cn para `pendente` e siga implementando; resposta que muda escopo vai por `unapprove`.',
|
|
31
31
|
],
|
|
32
32
|
verify: (f) => [
|
|
33
|
-
`\`morph-spec verify ${f} --json
|
|
33
|
+
`\`morph-spec verify ${f} --affected --json --detach\`, em run_in_background: só os testes que o diff alcança; repetir o comando retoma a rodada.`,
|
|
34
34
|
],
|
|
35
35
|
fix: (f) => [
|
|
36
|
-
`Leia o \`feedback\` do último verify, corrija a causa e rode
|
|
36
|
+
`Leia o \`feedback\` do último verify de ${f}, corrija a causa e rode o comando acima de novo, com \`--json\`.`,
|
|
37
37
|
],
|
|
38
38
|
escalate: () => [
|
|
39
39
|
'AskUserQuestion com o texto do reason (aceitar/continuar/parar) e siga a resposta; não rode outro verify antes dela.',
|
|
@@ -42,7 +42,7 @@ const HOW = {
|
|
|
42
42
|
'Siga .claude/skills/morph-apply/references/review.md.',
|
|
43
43
|
],
|
|
44
44
|
finish: (f) => [
|
|
45
|
-
|
|
45
|
+
`\`morph-spec context refresh\`, commit, e \`morph-spec finish ${f} --pr\` (\`--merge\` com \`finish.mode: "merge"\` no config), sem perguntar.`,
|
|
46
46
|
],
|
|
47
47
|
status: (f) => [
|
|
48
48
|
`Rode \`morph-spec status ${f}\` e confira a fase em .morph/features/${f}/feature.json.`,
|
|
@@ -87,19 +87,22 @@ function decide(s) {
|
|
|
87
87
|
if (pending.length) {
|
|
88
88
|
return step('implement', skill('morph-apply'), 'implemente nesta sessão e marque cada Cn feito no report.md', { checks: pending });
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const fullFresh = s.verification?.status && s.verificationStale !== true;
|
|
91
|
+
const affectedFresh = s.affected?.status && s.affectedStale !== true;
|
|
92
|
+
const v = fullFresh ? s.verification : affectedFresh ? s.affected : null;
|
|
93
|
+
if (!v) {
|
|
94
|
+
const ran = s.verification?.status || s.affected?.status;
|
|
95
|
+
return step('verify', `morph-spec verify ${f} --affected`, ran ? 'a árvore andou desde o último verify' : 'todo Cn feito, sem verify');
|
|
93
96
|
}
|
|
94
97
|
if (v.status === 'fail') {
|
|
95
|
-
const streak = s.failStreak ?? 0;
|
|
98
|
+
const streak = (fullFresh ? s.failStreak : s.affected.failStreak) ?? 0;
|
|
96
99
|
if (streak > 0 && streak % FIX_ATTEMPTS === 0) {
|
|
97
100
|
return step('escalate', 'AskUserQuestion (aceitar/continuar/parar)',
|
|
98
101
|
`${streak} verifies vermelhos seguidos, nunca uma tentativa a mais por conta própria. `
|
|
99
102
|
+ `aceitar → ${skill('morph-review')} (o verify vermelho pausa o gate: o humano assina lá); `
|
|
100
103
|
+ 'continuar → corrija e rode o verify de novo; parar → encerre aqui. Segue `escalate` até a árvore andar');
|
|
101
104
|
}
|
|
102
|
-
return step('fix', `morph-spec verify ${f}`,
|
|
105
|
+
return step('fix', `morph-spec verify ${f}${fullFresh ? '' : ' --affected'}`,
|
|
103
106
|
`corrija pelo feedback do verify e rode de novo (${FIX_ATTEMPTS - (streak % FIX_ATTEMPTS)} tentativa(s) antes de perguntar)`);
|
|
104
107
|
}
|
|
105
108
|
return step('review', skill('morph-review'), 'verify verde: Gate 3');
|
|
@@ -142,5 +145,6 @@ export function collectNextSignals(featureName, cwd) {
|
|
|
142
145
|
gates: feature.approvals || feature.approvalGates || {},
|
|
143
146
|
verification: feature.verification ?? null,
|
|
144
147
|
failStreak: feature.verifications?.feature?.failStreak ?? 0,
|
|
148
|
+
affected: feature.verifications?.affected ?? null,
|
|
145
149
|
};
|
|
146
150
|
}
|
package/framework/phases.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"displayName": "Plan",
|
|
9
9
|
"goals": [
|
|
10
10
|
"Understand the business problem and scan the stack (brownfield) or assume the default (greenfield)",
|
|
11
|
-
"Write plan.md — scenarios with proof, decisions
|
|
11
|
+
"Write plan.md — scenarios with proof, decisions and reuse",
|
|
12
12
|
"Run plan-check to produce checks.json and reach the plan gate"
|
|
13
13
|
],
|
|
14
14
|
"requiredOutputs": [
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"folder": "3-implement",
|
|
53
53
|
"displayName": "Implementation",
|
|
54
54
|
"goals": [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"One
|
|
55
|
+
"Implement the whole plan in this session, in sequence",
|
|
56
|
+
"Mark a scenario `feito` only with its evidence; close the phase on a green, unfiltered verify",
|
|
57
|
+
"One row per scenario in 3-implement/report.md — the implementation record"
|
|
58
58
|
],
|
|
59
59
|
"requiredOutputs": [],
|
|
60
60
|
"optionalOutputs": [
|
|
@@ -13,6 +13,9 @@ paths:
|
|
|
13
13
|
|
|
14
14
|
- **Teste é por risco, não por código novo.** `prova: teste` exige `risco:` (`regra` | `dinheiro` | `contrato` | `concorrência` | `dado` | `regressão`): ramo de regra de negócio, valor, contrato externo, corrida, integridade de dado ou o bug real. Sem risco (CRUD, DTO, mapping, wiring, constante, texto, layout) a prova é `leitura` — o verificador fecha por `arquivo:linha` — ou `e2e` quando o fluxo é o que importa.
|
|
15
15
|
- **Degrau de retrabalho** (opcional, junto de `prova:`): `degrau:` `refazer` | `desfazer` | `migrar` | `irreversível`; sem a linha, `refazer`. `migrar` é toda migration, aditiva ou não (drop, rename, backfill, coluna nova); `irreversível` apaga dado que alguém lê ou quebra contrato externo. Os dois pausam o gate do plano para o usuário, e o Gate 3 pausa quando a feature toca `migrations/` sem nenhum dos dois, ou um arquivo de contrato (`openapi.yaml`, `swagger.json`, `.proto`) sem `irreversível`; o revisor do Gate 3 caça o resto (webhook, dado apagado fora de migration, envio a terceiro).
|
|
16
|
+
- **Quem tem `risco:` prova-se executando.** `risco:` com `prova: leitura` é erro.
|
|
17
|
+
- **`e2e` dispara a ação do dono até o efeito** (salvar, ativar, enviar): conferir que o controle aparece não prova. Dois estados (rascunho e ativo) são dois passos no mesmo `Cn`.
|
|
18
|
+
- **Número que soma ou confere com outro** roda sobre dado com a forma do de produção; fixture feita à mão só tem as formas que você já imaginou.
|
|
16
19
|
- **Observável**: que asserção, ou que passo no navegador, prova? Sem resposta, reescreva ou descarte.
|
|
17
20
|
- **Artefato não é comportamento.** Cenário que cita `grep`, "contém a string", "o binário", "nenhum arquivo", "arquivo/pasta/diretório em" ou glob `**/` descreve o repositório. Tente o efeito em runtime; se o artefato É o entregável, justifique em `> Por que artefato:` logo abaixo (nunca na mesma linha, nunca `> [!note]`, que o parser não lê).
|
|
18
21
|
- **Saída de LLM** prova-se por `eval` de runtime, nunca pelo texto do prompt: evidência em `3-implement/evidencias/{Cn}.md`; com nó `runner: "evals"` (standard `ai-agents-evals-with-cache`) o cenário mora no dataset.
|
|
@@ -50,4 +53,4 @@ Só quando a feature tem algo que muda de estado (pedido, aprovação, assinatur
|
|
|
50
53
|
|
|
51
54
|
## O que o plan-check recusa
|
|
52
55
|
|
|
53
|
-
Veredito de reuso fora do enum, id `C`/`ADR` duplicado, `## Errata` fora do fim, `## Cenários` vazio, `teste` sem `risco:`, bug sem cenário de regressão, `## Padrões assumidos` ausente ou fora de `1..N`, `## Estados` com linha que não é `origem → destino: gatilho`. Avisa: padrão que toca credencial, permissão, pagamento ou apagar dado, chave desconhecida (inclusive o `## Fatias` de planos antigos), ADR sem **Alcance**.
|
|
56
|
+
Veredito de reuso fora do enum, id `C`/`ADR` duplicado, `## Errata` fora do fim, `## Cenários` vazio, `teste` sem `risco:`, `risco:` com `leitura`, bug sem cenário de regressão, `## Padrões assumidos` ausente ou fora de `1..N`, `## Estados` com linha que não é `origem → destino: gatilho`. Avisa: padrão que toca credencial, permissão, pagamento ou apagar dado, chave desconhecida (inclusive o `## Fatias` de planos antigos), ADR sem **Alcance**.
|
|
@@ -54,11 +54,26 @@
|
|
|
54
54
|
"enum": ["dotnet", "vitest", "jest", "node-test", "playwright", "none", "evals"],
|
|
55
55
|
"description": "Chooses the test-count extractor and whether a prebuild applies. Inferred from `command` when omitted; `none` means the count is UNKNOWN, never zero. `evals` is the ONLY value that is never inferred - it is a declaration that this suite reads a committed response cache, and it REQUIRES the `evals` block below."
|
|
56
56
|
},
|
|
57
|
+
"affected": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["flag"],
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"description": "Rarely needed: `verify --affected` reads an inline filter, a `$Filter` parameter of the script or a script that forwards its arguments by itself. Declare this only for a command none of those reach, which otherwise runs whole. Wins over the detection.",
|
|
62
|
+
"properties": {
|
|
63
|
+
"flag": { "type": "string", "minLength": 1, "description": "The argument of the command that takes a test filter, e.g. -Filter for a wrapper script." },
|
|
64
|
+
"baseFilter": { "type": "string", "description": "Filter the command applies by default, kept alongside the selection, e.g. Category!=Eval." }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
57
67
|
"expectedCount": {
|
|
58
68
|
"type": "integer",
|
|
59
69
|
"minimum": 0,
|
|
60
70
|
"description": "Recorded baseline, kept only for `node-test` and `evals` (ignored, and removed by --record-baseline, on the other runners). Written ONLY by `morph-spec verify --record-baseline`, from a green run — never typed by hand."
|
|
61
71
|
},
|
|
72
|
+
"parallel": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"default": false,
|
|
75
|
+
"description": "Run this node at the same time as the other nodes with parallel: true. Declare it only on suites that share no database, port or build output: nothing in the harness can check that. Ignored on an e2e: true node and in a round chasing a red suite."
|
|
76
|
+
},
|
|
62
77
|
"timeoutMs": {
|
|
63
78
|
"type": "integer",
|
|
64
79
|
"minimum": 1,
|
|
@@ -122,8 +137,14 @@
|
|
|
122
137
|
"e2e": {
|
|
123
138
|
"type": "object",
|
|
124
139
|
"additionalProperties": true,
|
|
125
|
-
"description": "The verification stack of `e2e up` and of the verify e2e node. Only hostServices and resetCommand are described here; the other keys (auth, command, portEnvNames, ...) stay open.",
|
|
140
|
+
"description": "The verification stack of `e2e up` and of the verify e2e node. Only specTimeoutMs, hostServices and resetCommand are described here; the other keys (auth, command, portEnvNames, ...) stay open.",
|
|
126
141
|
"properties": {
|
|
142
|
+
"specTimeoutMs": {
|
|
143
|
+
"type": "number",
|
|
144
|
+
"exclusiveMinimum": 0,
|
|
145
|
+
"default": 300000,
|
|
146
|
+
"description": "Limit of each Playwright spec run of the verify e2e node, in ms."
|
|
147
|
+
},
|
|
127
148
|
"resetCommand": {
|
|
128
149
|
"type": "string",
|
|
129
150
|
"minLength": 1,
|
|
@@ -216,6 +237,14 @@
|
|
|
216
237
|
}
|
|
217
238
|
}
|
|
218
239
|
},
|
|
240
|
+
"finish": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"description": "How a finished feature reaches the default branch.",
|
|
243
|
+
"properties": {
|
|
244
|
+
"mode": { "type": "string", "enum": ["pr", "merge"], "default": "pr" }
|
|
245
|
+
},
|
|
246
|
+
"additionalProperties": false
|
|
247
|
+
},
|
|
219
248
|
"heavy": {
|
|
220
249
|
"type": "object",
|
|
221
250
|
"additionalProperties": false,
|
|
@@ -249,6 +278,35 @@
|
|
|
249
278
|
"e2e": { "type": "number", "minimum": 0 }
|
|
250
279
|
}
|
|
251
280
|
},
|
|
281
|
+
"reclaimIdleMinutes": {
|
|
282
|
+
"type": "number",
|
|
283
|
+
"minimum": 0,
|
|
284
|
+
"default": 20,
|
|
285
|
+
"description": "Below the RAM floor, before waiting, `verify`, `e2e up` and `heavy run` bring down the e2e stacks of this repo nobody is using: an archived feature's (with volumes) and an active feature's idle for this many minutes, with no heavy slot (volumes kept). 0 turns it off."
|
|
286
|
+
},
|
|
287
|
+
"reclaimContainers": {
|
|
288
|
+
"type": "array",
|
|
289
|
+
"items": { "type": "string", "pattern": "^[\\w.-]+$" },
|
|
290
|
+
"default": ["morph-pgtest"],
|
|
291
|
+
"description": "Name prefixes of disposable test containers that reclaim removes when no heavy slot may be testing. A killed test run leaves these behind."
|
|
292
|
+
},
|
|
293
|
+
"detachedWaitMinutes": {
|
|
294
|
+
"type": "number",
|
|
295
|
+
"minimum": 0,
|
|
296
|
+
"default": 60,
|
|
297
|
+
"description": "How long a `verify --detach` waits in the queue. It runs out of any shell's timeout, so with several sessions open it waits for its turn instead of exiting 3."
|
|
298
|
+
},
|
|
299
|
+
"yieldToUser": {
|
|
300
|
+
"type": "boolean",
|
|
301
|
+
"default": true,
|
|
302
|
+
"description": "Heavy runs leave room for what the person is using: each run keeps to its share of the cores (cores / slots) through DOTNET_PROCESSOR_COUNT and vitest --maxWorkers. Priority is left alone on purpose: below normal, a process starves on a busy machine and fails by its own timeouts."
|
|
303
|
+
},
|
|
304
|
+
"criticalFreeMemGb": {
|
|
305
|
+
"type": "number",
|
|
306
|
+
"minimum": 0,
|
|
307
|
+
"default": 1,
|
|
308
|
+
"description": "While a `verify --detach` runs, free RAM below this for a minute reclaims idle stacks and then stops that run with exit 3, no stamp. The floors judge the start of a node; this judges the middle. 0 turns it off."
|
|
309
|
+
},
|
|
252
310
|
"waitTimeoutSec": {
|
|
253
311
|
"type": "number",
|
|
254
312
|
"minimum": 0,
|
|
@@ -105,6 +105,20 @@
|
|
|
105
105
|
"updatedAt": { "type": "string", "format": "date-time" }
|
|
106
106
|
},
|
|
107
107
|
"additionalProperties": false
|
|
108
|
+
},
|
|
109
|
+
"affected": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"description": "Last `verify --affected`. Opens the Gate 3 dispatch and never stands in for the complete run the review gate reads.",
|
|
112
|
+
"required": ["status", "nodes", "attempts", "updatedAt"],
|
|
113
|
+
"properties": {
|
|
114
|
+
"status": { "type": "string", "enum": ["pass", "fail"] },
|
|
115
|
+
"nodes": { "type": "object" },
|
|
116
|
+
"attempts": { "type": "integer", "minimum": 1 },
|
|
117
|
+
"failStreak": { "type": "integer", "minimum": 0 },
|
|
118
|
+
"updatedAt": { "type": "string", "format": "date-time" },
|
|
119
|
+
"sha": { "type": ["string", "null"] }
|
|
120
|
+
},
|
|
121
|
+
"additionalProperties": false
|
|
108
122
|
}
|
|
109
123
|
},
|
|
110
124
|
"additionalProperties": false
|
|
@@ -131,6 +145,7 @@
|
|
|
131
145
|
},
|
|
132
146
|
"additionalProperties": false
|
|
133
147
|
},
|
|
148
|
+
"flaky": { "type": "array", "items": { "type": "string" }, "description": "Nodes or e2e specs that were red and turned green with no code change on the way to this stamp. Absent when none. Evidence for the verifier, never a gate input." },
|
|
134
149
|
"e2eReason": { "type": ["string", "null"], "description": "The LITERAL reason the e2e node skipped (e2e-check.js's own wording), or null when it did not skip. risk-detector.js's classifyE2eSkip() reads it: 'project does not containerize' / 'does not touch runtime' are legitimate dispensations and pass; 'docker is not available' / 'requested' (--skip-e2e) / anything unclassifiable — null included — are an absence of proof and pause Gate 3. A skip recorded WITHOUT a reason is refused on purpose: a nameless dispensation is a silent no-op." },
|
|
135
150
|
"sha": { "type": ["string", "null"], "description": "HEAD commit SHA at verify time (git rev-parse HEAD); null when not in a git repo or no commits yet." },
|
|
136
151
|
"ts": { "type": "string", "format": "date-time", "description": "Server-side timestamp, same instant as the corresponding verifications entry's updatedAt." },
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando for iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.2"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use quando o usuário pedir "use /migrate-lp-to-umbraco {projeto}" ou descrever a necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.2"
|
|
8
8
|
stacks:
|
|
9
9
|
- umbraco
|
|
10
10
|
---
|
|
@@ -4,7 +4,7 @@ description: Implementa o plano aprovado de uma feature morph-spec inteiro nesta
|
|
|
4
4
|
version: core
|
|
5
5
|
when_to_use: Use com o gate do plano aprovado ("/morph-apply {feature}", "implementar feature"), em "/morph-review {feature}" (entra no §5), em "/morph-hotfix {feature}" ou incidente de produção (references/hotfix.md), ou quando `morph-spec next` mandar implement, verify, fix, review ou finish
|
|
6
6
|
user-invocable: true
|
|
7
|
-
cliVersion: "10.
|
|
7
|
+
cliVersion: "10.5.2"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -24,7 +24,8 @@ O plano inteiro, nesta sessão, em sequência. Sem sub-agent para implementar: o
|
|
|
24
24
|
- As regras de cada domínio (.NET, EF, MAF, Next.js, infra) chegam sozinhas: a rule do stack carrega quando você toca o arquivo. O texto inteiro de um standard: `morph-spec standards --find` → `--show <id>#<âncora>`.
|
|
25
25
|
- Antes de criar um tipo, confira a tabela `## Reuso` e `morph-spec graph explain <nome>` (sem grafo, `grep -rn`).
|
|
26
26
|
- Rode build e teste quando quiser; teste pesado fora do `verify` por `morph-spec heavy run -- <cmd>`. Exit 3 = fila cheia: uma chamada nova no máximo (`references/machine.md`).
|
|
27
|
-
-
|
|
27
|
+
- **Execute antes de marcar `feito`.** `teste`: rode e veja falhar sem a mudança. Soma ou conferência de número: ao menos uma vez sobre dado com a forma do de produção. `e2e` fica para a passada do §3.
|
|
28
|
+
- Ao fechar um `Cn`, atualize a linha dele no `report.md` (`feito` + a evidência: o que você executou e viu, teste, `arquivo:linha`); commite por lote coerente (`wip(<f>): C2-C3 …`), não por `Cn`. Decisão ou fato novo → uma linha em `## Notas`.
|
|
28
29
|
- Mídia gerada por IA: `references/media.md`. Erro difícil: `references/debugging.md`.
|
|
29
30
|
|
|
30
31
|
`Cn` que depende de uma decisão que não é sua: status `bloqueado` e a pergunta em `## Notas`; o `next` devolve `decide` → `AskUserQuestion`.
|
|
@@ -40,9 +41,11 @@ O plano inteiro, nesta sessão, em sequência. Sem sub-agent para implementar: o
|
|
|
40
41
|
|
|
41
42
|
## 3. Verify, sem filtro
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
**Passada de bancada:** com `Cn` `prova: e2e`, antes do verify e uma vez só (cada edição recompila a stack): `morph-spec e2e up {feature}` e dispare cada fluxo pela ferramenta da sessão (Playwright MCP, ou o spec do fluxo) até o efeito, não até o controle aparecer. É o que tira do Gate 3 o que uma execução acha. Não subiu ou exit 3: uma linha em `## Notas` e siga para o verify, sem parar a sessão.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Todo `Cn` `feito`: commite tudo e rode `morph-spec verify {feature} --affected --json --detach`: build, validadores e só os testes que o diff alcança. Migration, contrato, arquivo que alcança o projeto inteiro ou código compartilhado sobem sozinhos para a rodada completa. A completa, sem filtro, roda uma vez, depois dos relatórios do Gate 3 (`references/review.md` §6): é a prova que o Gate 3 e o CI leem, e `--affected` nunca a substitui. Sempre em `run_in_background`, esperando a notificação **no mesmo turno**, sem ler o log em laço. O `--detach` roda fora da sessão: encerrada a espera sem resultado, rode o mesmo comando, que retoma a rodada em vez de repetir. Nó `node-test`/`evals` sem `expectedCount`, ou teste apagado de propósito: `--record-baseline` na rodada completa, que é sem filtro.
|
|
47
|
+
|
|
48
|
+
Vermelho → corrija com `verify.feedback` e rode a mesma rodada de novo. Na completa, com código novo, as suítes vermelhas rodam primeiro e o resto só se passarem; sem mudança de código, só o que ficou vermelho roda. Mudou algo fora da árvore (`.env`, banco): `--fresh`. `next` conta: `fix` enquanto há tentativa, `escalate` na 3ª vermelha seguida → `AskUserQuestion` ("Verify de {feature} vermelho após 3 tentativas — aceitar com ressalva, continuar tentando, ou parar?").
|
|
46
49
|
|
|
47
50
|
## 4. Ramo por tipo
|
|
48
51
|
|
|
@@ -55,7 +58,7 @@ Verify verde: siga `references/review.md` — foto `--before-eval`, verificador
|
|
|
55
58
|
|
|
56
59
|
## 6. Fechar
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
`morph-spec context refresh` (regera o inventário em `.morph/context/README.md`; `design-system.md` você atualiza só se mudou token) e commite o que faltar (relatórios do review): o `finish` exige árvore limpa. Modo: `finish.mode` do config do projeto, padrão `pr`, sem perguntar: `morph-spec finish {feature} --pr|--merge`. `--pr` não integra: a feature fica `aguardando-merge` até o merge. O `finish` arquiva o `feature.json`, o `plan.md` e o recap; o report e a review ficam no histórico do git. Encerrado, aponte o recap ao usuário (`.morph/archive/{feature}/recap.html`, abra no navegador; `morph-spec recap {feature} --piper` narra com o Piper local).
|
|
59
62
|
|
|
60
63
|
## Arquivo de teste
|
|
61
64
|
|
|
@@ -28,7 +28,7 @@ A mudança mínima que tira a falha do ar — refatoração e hardening fora do
|
|
|
28
28
|
|
|
29
29
|
## 5. Gate 3 — sempre humano
|
|
30
30
|
|
|
31
|
-
Escreva você `4-review/verifier.md`: `**Verdict:** SHIP|FIX`, `**Reviewed-SHA:** {git rev-parse HEAD}`, `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, evidência = saída do verify), `## Bloqueadores` (`Nenhum.` vale). `morph-spec gate-check {feature} review` pausa sempre. `AskUserQuestion`, header "Gate 3":
|
|
31
|
+
Escreva você `4-review/verifier.md`: `**Verdict:** SHIP|FIX`, `**Reviewed-SHA:** {git rev-parse HEAD}`, `## Checks` (`| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |`, evidência = saída do verify), `## Bloqueadores` (`Nenhum.` vale). `morph-spec gate-check {feature} review` pausa sempre. Hotfix não despacha a revisão de segurança: se o diff toca caminho sensível (auth, segredo, pagamento, migration), diga isso ao humano no gate, porque a atenção é dele. `AskUserQuestion`, header "Gate 3":
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
question: "Gate 3 — Hotfix de {feature} pronto para ship?"
|
|
@@ -14,8 +14,9 @@ Para manter a máquina utilizável enquanto você compila e testa. Aplique quand
|
|
|
14
14
|
| Sintoma | Comando | Como ler e o que fazer |
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| Máquina ou editor lento, pouca RAM | `morph-spec doctor --procs` (só lê) | Cada linha diz órfão ou protegido, com a razão. Sem órfão, a pressão vem de fora (vmmem do WSL, IDE): diga isso e passe a sugestão de `.wslconfig` que o relatório traz. Com órfão, mostre a lista e ofereça `--prune`. |
|
|
17
|
-
| Comando voltou `⏳ … não rodou` ou exit 3 | `morph-spec heavy status` | Mostra quem segura cada vaga, há quanto tempo, e a RAM livre. A vaga solta sozinha (processo morto ou timeout). Vaga presa por sessão viva: diga ao usuário quem a segura. Se a causa for RAM e nenhuma vaga estiver presa, rode `doctor --procs
|
|
18
|
-
|
|
|
17
|
+
| Comando voltou `⏳ … não rodou` ou exit 3 | `morph-spec heavy status` | Mostra quem segura cada vaga, há quanto tempo, e a RAM livre. A vaga solta sozinha (processo morto ou timeout). Vaga presa por sessão viva: diga ao usuário quem a segura. Se a causa for RAM e nenhuma vaga estiver presa, rode `doctor --procs`: as stacks paradas a fila já derrubou sozinha. |
|
|
18
|
+
| O Claude Code encerrou um comando de fundo por pouca memória | o mesmo `morph-spec verify … --detach` | O `--detach` não morre com a sessão: repetir retoma a rodada. Outro comando pesado não se repete por conta própria: avise o usuário. |
|
|
19
|
+
| Várias sessões abertas, testando ao mesmo tempo | nada: a fila resolve | São 2 vagas e uma suíte de `verify` por vez. O `verify --detach` espera a vez (até `heavy.detachedWaitMinutes`), então espera longa é fila, não problema: não cancele nem peça ao usuário. Abaixo do piso, a fila derruba sozinha as stacks ociosas; `heavy reclaim` faz o mesmo à mão. |
|
|
19
20
|
| Worktree sobrando ou fora do contêiner | `morph-spec doctor --worktrees` | Lista cada árvore com a razão; remova só por `worktree remove`. |
|
|
20
21
|
| A worktree precisa de arquivo fora do git (`.env`, segredos) | `.worktreeinclude` na raiz do repo | Um padrão por linha, sintaxe do gitignore. Copia só o que casa **e** é ignorado; nunca `node_modules`, build ou `.git`. O `setup` e o `provision` aplicam. |
|
|
21
22
|
| Editor pesado por causa das worktrees | `worktree.root` em `.morph/config/config.json` | Põe as worktrees fora da árvore aberta (caminho absoluto fora do projeto). `init` e `update` já gravam as exclusões no `.vscode/settings.json`. |
|