@polymorphism-tech/morph-spec 8.32.0 → 8.34.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.
Files changed (176) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +26 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +71 -68
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +218 -39
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +37 -4
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -1,14 +1,14 @@
1
- /**
2
- * Stub: validation dispatch was driven by tier-4 agents in v4.
3
- * In v5, validation is handled by morph-eval skill invoked directly
4
- * by the Evaluator role. This function is kept for call-site compatibility.
5
- *
6
- * @module emit-validator-dispatch
7
- */
8
-
9
- /**
10
- * No-op in v5. Validation dispatch is now handled by morph-eval skill.
11
- */
12
- export async function emitValidatorDispatch(_featureName, _phase, _cwd) {
13
- // v5: evaluation driven by morph-eval skill, not CLI dispatch
14
- }
1
+ /**
2
+ * Stub: validation dispatch was driven by tier-4 agents in v4.
3
+ * In v5, validation is handled by morph-eval skill invoked directly
4
+ * by the Evaluator role. This function is kept for call-site compatibility.
5
+ *
6
+ * @module emit-validator-dispatch
7
+ */
8
+
9
+ /**
10
+ * No-op in v5. Validation dispatch is now handled by morph-eval skill.
11
+ */
12
+ export async function emitValidatorDispatch(_featureName, _phase, _cwd) {
13
+ // v5: evaluation driven by morph-eval skill, not CLI dispatch
14
+ }
@@ -1,14 +1,14 @@
1
- /**
2
- * Shared issue counting utility for validators
3
- *
4
- *
5
- * @param {Array} issues - Array of validation issues with type property
6
- * @returns {{errors: number, warnings: number, infos: number}} Count by type
7
- */
8
- export function countIssues(issues) {
9
- return {
10
- errors: issues.filter((i) => i.type === 'error').length,
11
- warnings: issues.filter((i) => i.type === 'warning').length,
12
- infos: issues.filter((i) => i.type === 'info').length,
13
- };
14
- }
1
+ /**
2
+ * Shared issue counting utility for validators
3
+ *
4
+ *
5
+ * @param {Array} issues - Array of validation issues with type property
6
+ * @returns {{errors: number, warnings: number, infos: number}} Count by type
7
+ */
8
+ export function countIssues(issues) {
9
+ return {
10
+ errors: issues.filter((i) => i.type === 'error').length,
11
+ warnings: issues.filter((i) => i.type === 'warning').length,
12
+ infos: issues.filter((i) => i.type === 'info').length,
13
+ };
14
+ }
@@ -48,7 +48,8 @@ const OUTPUT_TAIL_LINES = 20;
48
48
  * @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
49
49
  * @param {Object} [options]
50
50
  * @param {string} [options.rubric] - Force a rubric id (highest precedence)
51
- * @param {number} [options.timeout] - Build timeout in ms (default 120000)
51
+ * @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
52
+ * (default: 120000 for build; for tests, see resolveTestTimeout())
52
53
  * @param {boolean} [options.skipTests] - Skip the tests node
53
54
  * @param {boolean} [options.skipBuild] - Skip the build node
54
55
  * @param {boolean} [options.skipE2e] - Skip the e2e node
@@ -86,7 +87,18 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
86
87
  appendBuildFeedback(feedback, nodes.build);
87
88
 
88
89
  // 2. tests — skipped entirely (no invocation) when build already failed.
89
- nodes.tests = await runTestsNode({ projectPath, skipTests, buildNode: nodes.build, _runTestSuite });
90
+ // The tests node gets the SAME --timeout the user typed, but only when they
91
+ // typed one: `timeout` above is defaulted for the build, and passing that
92
+ // default down would silently outrank config.project.testTimeoutMs and the
93
+ // per-project scaling (see resolveTestTimeout()).
94
+ const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
95
+ nodes.tests = await runTestsNode({
96
+ projectPath,
97
+ skipTests,
98
+ buildNode: nodes.build,
99
+ timeout: explicitTimeout,
100
+ _runTestSuite,
101
+ });
90
102
  appendTestsFeedback(feedback, nodes.tests);
91
103
 
92
104
  // 3. validators — ALWAYS runs, even on a red build (free static feedback).
@@ -174,7 +186,7 @@ function appendBuildFeedback(feedback, buildNode) {
174
186
  // Tests node
175
187
  // ============================================================================
176
188
 
177
- async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }) {
189
+ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTestSuite }) {
178
190
  const command = resolveTestCommand(projectPath);
179
191
 
180
192
  if (skipTests) {
@@ -192,7 +204,7 @@ async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }
192
204
  }
193
205
 
194
206
  const start = Date.now();
195
- const result = await _runTestSuite(projectPath);
207
+ const result = await _runTestSuite(projectPath, { timeout });
196
208
  const durationMs = Date.now() - start;
197
209
 
198
210
  if (result.skipped) {
@@ -205,6 +217,20 @@ async function runTestsNode({ projectPath, skipTests, buildNode, _runTestSuite }
205
217
  };
206
218
  }
207
219
 
220
+ // A timeout has no verdict to report: the tail of a truncated log reads like
221
+ // a passing run cut mid-sentence. Lead with what actually happened.
222
+ if (result.timedOut) {
223
+ const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
224
+ return {
225
+ status: 'fail',
226
+ command,
227
+ durationMs,
228
+ timedOut: true,
229
+ reason: result.message,
230
+ output: tail ? `${result.message}\n${tail}` : result.message,
231
+ };
232
+ }
233
+
208
234
  return {
209
235
  status: result.passed ? 'pass' : 'fail',
210
236
  command,
@@ -328,10 +354,17 @@ async function runE2eNode({ projectPath, featureName, taskId, skipE2e, freshE2e,
328
354
  durationMs,
329
355
  reason: result.reason ?? null,
330
356
  output: result.output || '',
357
+ // Non-fatal findings (e.g. a compose with fixed ports at the ROOT, where
358
+ // nothing collides yet). Surfaced so the same compose does not fail
359
+ // unannounced the day the feature moves into a worktree.
360
+ warnings: Array.isArray(result.warnings) ? result.warnings : [],
331
361
  };
332
362
  }
333
363
 
334
364
  function appendE2eFeedback(feedback, e2eNode) {
365
+ for (const warning of e2eNode.warnings || []) {
366
+ feedback.push(`[e2e] warning: ${warning}`);
367
+ }
335
368
  if (e2eNode.status !== 'fail') return;
336
369
  if (e2eNode.reason) feedback.push(`[e2e] ${e2eNode.reason}`);
337
370
  for (const line of linesOf(e2eNode.output)) {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dirs.js — the ONE place the worktree container directory is named.
3
+ *
4
+ * Every reader that resolves worktrees BY CONVENTION (test discovery, graph
5
+ * ignore list, the installed .gitignore, the architecture scanner, the
6
+ * dashboard scanner) imports from here. Duplicating the string is how the
7
+ * C1 bug comes back: a reader keeps the old name and silently goes blind.
8
+ *
9
+ * Readers that go through `git worktree list` (fleet, finish, worktree list)
10
+ * never depended on this — git records absolute paths.
11
+ */
12
+
13
+ /**
14
+ * The directory new worktrees are created under, relative to the primary root.
15
+ *
16
+ * NO leading dot, deliberately (C1). Jest escapes the `.` when it interpolates
17
+ * an absolute `rootDir` into the `testMatch` glob, so `<root>/.worktrees/...`
18
+ * stops matching and an entire frontend suite reports **0 tests without an
19
+ * error** — the suite "passes" having tested nothing. The dot bought nothing:
20
+ * the directory is gitignored either way.
21
+ */
22
+ export const WORKTREES_DIR = 'worktrees';
23
+
24
+ /**
25
+ * The pre-C1 directory name. New worktrees never land here, but every reader
26
+ * that resolves worktrees BY CONVENTION must still accept it, so a machine
27
+ * that already had `.worktrees/{f}` keeps working with no manual migration.
28
+ */
29
+ export const LEGACY_WORKTREES_DIR = '.worktrees';
30
+
31
+ /** Both names, new first — the read order for convention-based lookups. */
32
+ export const WORKTREE_DIR_NAMES = [WORKTREES_DIR, LEGACY_WORKTREES_DIR];
@@ -21,6 +21,8 @@
21
21
 
22
22
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
23
23
  import { join } from 'path';
24
+ // src/ → hooks/shared/ is the allowed direction (hooks are installed standalone).
25
+ import { composeProjectName } from '../../../framework/hooks/shared/e2e-check.js';
24
26
 
25
27
  const DEFAULT_BASE_PORT = 4000;
26
28
  const DEFAULT_BLOCK_SIZE = 10;
@@ -124,9 +126,46 @@ export function releaseEnvBlock(feature, { gitCommonDir }) {
124
126
  }
125
127
 
126
128
  /**
127
- * Render the `.morph/worktree.env` contents for a block, stack-aware.
128
- * `PORT` covers next/vite/node dev servers; dotnet gets its own URL on base+1
129
- * so a full-stack worktree runs web and api side by side inside one block.
129
+ * Fixed offsets inside a block. The block has 10 ports; four are named, the
130
+ * rest (+4..+9) are the project's to use. A new offset added here MUST also be
131
+ * published by the compose template (`${VAR:-default}:container`), or it is a
132
+ * port nobody listens on.
133
+ */
134
+ export const PORT_OFFSETS = Object.freeze({ web: 0, api: 1, postgres: 2, redis: 3 });
135
+
136
+ /**
137
+ * The ONE port-layout contract. Two writers derive from it — the
138
+ * `.morph/worktree.env` manifest (`buildEnvFileContent`) and the env handed to
139
+ * `docker compose` (`src/lib/e2e/compose-env.js`) — so a worktree's file and
140
+ * its running stack can never describe different worlds.
141
+ *
142
+ * `COMPOSE_PROJECT_NAME` is part of the layout on purpose: a hand-run
143
+ * `docker compose up` inside the worktree must land in the isolated project,
144
+ * not in the root's. Every value is a string (env-safe); key order is stable.
145
+ *
146
+ * @param {{ base: number, range: [number, number] }} block
147
+ * @param {{ stack?: string, feature?: string }} [opts]
148
+ * @returns {Record<string,string>}
149
+ */
150
+ export function portLayout(block, { stack = '', feature = '' } = {}) {
151
+ const env = {};
152
+ if (feature) env.COMPOSE_PROJECT_NAME = composeProjectName(feature);
153
+ env.MORPH_PORT_BASE = String(block.base);
154
+ env.MORPH_PORT_RANGE = `${block.range[0]}-${block.range[1]}`;
155
+ env.PORT = String(block.base + PORT_OFFSETS.web);
156
+ env.API_PORT = String(block.base + PORT_OFFSETS.api);
157
+ env.POSTGRES_PORT = String(block.base + PORT_OFFSETS.postgres);
158
+ env.REDIS_PORT = String(block.base + PORT_OFFSETS.redis);
159
+ if (/\bdotnet\b/i.test(String(stack))) {
160
+ env.ASPNETCORE_URLS = `http://localhost:${block.base + PORT_OFFSETS.api}`;
161
+ }
162
+ return env;
163
+ }
164
+
165
+ /**
166
+ * Render the `.morph/worktree.env` contents for a block: a header, the
167
+ * feature, and exactly the `portLayout()` pairs — nothing else, so the file
168
+ * cannot drift from what compose receives.
130
169
  *
131
170
  * @param {string} feature
132
171
  * @param {{ base: number, range: [number, number] }} block
@@ -135,15 +174,10 @@ export function releaseEnvBlock(feature, { gitCommonDir }) {
135
174
  */
136
175
  export function buildEnvFileContent(feature, block, stack = '') {
137
176
  const lines = [
138
- `# Managed by morph-spec — fleet env desta worktree (feature: ${feature}).`,
139
- '# Derivado e local: nunca commitar, nunca editar à mão (regenerado pelo worktree setup).',
177
+ `# Managed by morph-spec — ambiente dedicado desta worktree (feature: ${feature}).`,
178
+ '# Derivado, git-invisível, regenerado por `worktree setup|link`; nunca editar nem commitar.',
140
179
  `MORPH_FEATURE=${feature}`,
141
- `MORPH_PORT_BASE=${block.base}`,
142
- `MORPH_PORT_RANGE=${block.range[0]}-${block.range[1]}`,
143
- `PORT=${block.base}`,
180
+ ...Object.entries(portLayout(block, { stack, feature })).map(([k, v]) => `${k}=${v}`),
144
181
  ];
145
- if (/\bdotnet\b/i.test(String(stack))) {
146
- lines.push(`ASPNETCORE_URLS=http://localhost:${block.base + 1}`);
147
- }
148
182
  return `${lines.join('\n')}\n`;
149
183
  }
@@ -247,9 +247,47 @@ export function auditWorktrees(cwd = process.cwd()) {
247
247
 
248
248
  const feature = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1] || null;
249
249
  if (!feature) {
250
- // Only worth reporting when it sits in morph's own worktree container —
251
- // a worktree the user keeps elsewhere is none of morph's business.
252
250
  const inContainer = WORKTREE_DIR_NAMES.some((d) => path.startsWith(`${rootNorm}/${d}/`));
251
+ const task = entry.branch?.match(/^refs\/heads\/morph-task\/(.+)$/)?.[1] || null;
252
+
253
+ if (!inContainer) {
254
+ // A worktree OUTSIDE morph's container was created behind morph's back
255
+ // (a native Claude Code run before the WorktreeCreate hook, a raw
256
+ // `git worktree add ../x`, another skill). It has no junctions, no port
257
+ // block, and nothing morph runs there — the exact shape that let a
258
+ // root's `.claude/` be emptied. Reported as `warn` (never a failure,
259
+ // NEVER pruned): the work inside may be the only copy.
260
+ const work = unintegratedWork(root, entry.branch, prIndex);
261
+ issues.push({
262
+ type: 'out-of-container',
263
+ severity: 'warn',
264
+ path,
265
+ detail: `${path} é um worktree fora de worktrees/ (${entry.branch || 'HEAD destacado'}) — criado sem o morph: sem junctions, sem portas, invisível ao \`fleet\`${work.unintegrated ? `; carrega trabalho NÃO integrado (${work.evidence})` : ''}`,
266
+ fix: work.unintegrated
267
+ ? `integre a branch primeiro; depois \`morph-spec worktree remove --target "${path}"\`. Novos worktrees: \`morph-spec worktree setup <f>\` ou \`worktree provision <n>\``
268
+ : `morph-spec worktree remove --target "${path}" — e crie os próximos por \`morph-spec worktree setup <f>\` / \`worktree provision <n>\``,
269
+ });
270
+ continue;
271
+ }
272
+
273
+ if (task) {
274
+ // `morph-task/*`: a task-isolation tree provisioned by the WorktreeCreate
275
+ // hook. Clean → nothing to say (the orchestrator removes it after the
276
+ // merge). With commits the default does not have → report, never
277
+ // suggest removal.
278
+ const work = unintegratedWork(root, entry.branch, prIndex);
279
+ if (work.unintegrated) {
280
+ issues.push({
281
+ type: 'unintegrated-work',
282
+ severity: 'info',
283
+ path,
284
+ detail: `${path} é um worktree de isolamento de task (${entry.branch}) com trabalho NÃO integrado (${work.evidence})`,
285
+ fix: `integre a branch (\`git merge --no-ff morph-task/${task}\` no orquestrador) e depois \`morph-spec worktree remove --target "${path}"\``,
286
+ });
287
+ }
288
+ continue;
289
+ }
290
+
253
291
  if (inContainer) {
254
292
  // Same hazard as `no-feature` below, and it needs the same guard:
255
293
  // "invisible to fleet" says nothing about whether the WORK is safe
@@ -1,11 +1,11 @@
1
- /**
2
- * Map an agents.json ID to the Claude Code subagent_type.
3
- *
4
- * @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
5
- * @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
6
- * @returns {string} subagent_type matching .claude/agents/ filename stem
7
- */
8
- export function agentIdToSubagentType(id, tier) {
9
- const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
10
- return `${prefix}${id}`;
11
- }
1
+ /**
2
+ * Map an agents.json ID to the Claude Code subagent_type.
3
+ *
4
+ * @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
5
+ * @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
6
+ * @returns {string} subagent_type matching .claude/agents/ filename stem
7
+ */
8
+ export function agentIdToSubagentType(id, tier) {
9
+ const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
10
+ return `${prefix}${id}`;
11
+ }