@polymorphism-tech/morph-spec 9.8.0 → 10.1.1

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 (202) hide show
  1. package/CLAUDE.md +12 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +28 -36
  6. package/docs/QUICKSTART.md +31 -191
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-preflight.md +3 -3
  16. package/framework/commands/morph-review.md +15 -0
  17. package/framework/commands/morph-status.md +1 -1
  18. package/framework/docs/CLI.md +84 -95
  19. package/framework/evals/review-checklist.md +45 -53
  20. package/framework/hooks/README.md +4 -4
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/next-step.js +63 -24
  38. package/framework/hooks/shared/phase-utils.js +6 -9
  39. package/framework/hooks/shared/report-progress.js +86 -0
  40. package/framework/hooks/shared/state-reader.js +3 -2
  41. package/framework/hooks/statusline.js +332 -0
  42. package/framework/mcp/registry.json +4 -4
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +10 -2
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/data-standards.md +1 -1
  47. package/framework/rules/ef-standards.md +20 -0
  48. package/framework/rules/frontend-standards.md +1 -1
  49. package/framework/rules/hangfire-standards.md +1 -1
  50. package/framework/rules/implement-report.md +25 -0
  51. package/framework/rules/infrastructure-standards.md +2 -0
  52. package/framework/rules/neon-auth-standards.md +1 -1
  53. package/framework/rules/neon-standards.md +3 -3
  54. package/framework/rules/nextjs-standards.md +7 -0
  55. package/framework/rules/plan-grammar.md +53 -0
  56. package/framework/rules/ui-design.md +27 -0
  57. package/framework/schemas/config.schema.json +11 -18
  58. package/framework/schemas/feature.schema.json +7 -1
  59. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  60. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  61. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  62. package/framework/skills/core/morph-apply/SKILL.md +39 -166
  63. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  64. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  65. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  66. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  67. package/framework/skills/core/morph-apply/references/review.md +65 -0
  68. package/framework/skills/core/morph-plan/SKILL.md +49 -109
  69. package/framework/skills/core/morph-plan/references/critique.md +43 -0
  70. package/framework/skills/core/morph-plan/references/interview.md +37 -0
  71. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  72. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  73. package/framework/standards/STANDARDS.json +51 -28
  74. package/framework/standards/STANDARDS.meta.json +16 -19
  75. package/framework/standards/ai-agents/agent-session.md +2 -3
  76. package/framework/standards/ai-agents/context-providers.md +2 -2
  77. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  78. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  79. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
  80. package/framework/standards/ai-agents/setup.md +1 -1
  81. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  82. package/framework/standards/architecture/stack-baseline.md +70 -0
  83. package/framework/standards/backend/database/postgresql/database.md +22 -1
  84. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
  85. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
  86. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
  87. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  88. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  89. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  90. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  91. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  92. package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
  93. package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
  94. package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
  95. package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
  96. package/framework/standards/integration/mcp/mcp-tools.md +20 -20
  97. package/framework/templates/REGISTRY.json +1 -2
  98. package/framework/templates/context/ARCHITECTURE.md +1 -1
  99. package/framework/templates/docs/plan.md +15 -12
  100. package/framework/templates/docs/ui-mockups.md +17 -105
  101. package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
  102. package/package.json +1 -1
  103. package/src/commands/e2e/e2e.js +88 -23
  104. package/src/commands/graph/diff.js +74 -0
  105. package/src/commands/graph/status.js +1 -1
  106. package/src/commands/observability/cost.js +2 -4
  107. package/src/commands/observability/telemetry.js +1 -1
  108. package/src/commands/project/doctor.js +23 -39
  109. package/src/commands/project/fleet.js +5 -6
  110. package/src/commands/project/init.js +63 -36
  111. package/src/commands/project/stack-scan.js +3 -42
  112. package/src/commands/project/stack-set.js +26 -0
  113. package/src/commands/project/status.js +12 -13
  114. package/src/commands/project/update.js +39 -3
  115. package/src/commands/project/worktree.js +3 -14
  116. package/src/commands/state/advance.js +17 -17
  117. package/src/commands/state/approve.js +0 -6
  118. package/src/commands/state/archive.js +27 -1
  119. package/src/commands/state/auto-approval.js +2 -2
  120. package/src/commands/state/create.js +1 -6
  121. package/src/commands/state/gate-check.js +38 -31
  122. package/src/commands/state/gate-decision.js +3 -2
  123. package/src/commands/state/next.js +1 -0
  124. package/src/commands/state/recap.js +67 -0
  125. package/src/commands/state/report-check.js +36 -178
  126. package/src/commands/validation/plan-check.js +16 -11
  127. package/src/commands/validation/validate-feature.js +3 -3
  128. package/src/commands/validation/verify.js +3 -6
  129. package/src/core/paths/output-schema.js +3 -3
  130. package/src/core/state/state-manager.js +2 -4
  131. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  132. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  133. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  134. package/src/dashboard/app/pages/Activity.jsx +4 -0
  135. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  136. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  137. package/src/dashboard/app/pages/Features.jsx +7 -7
  138. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  139. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  140. package/src/dashboard/scanner/activity-scanner.js +29 -0
  141. package/src/dashboard/scanner/agents-scanner.js +11 -14
  142. package/src/dashboard/scanner/features-scanner.js +45 -24
  143. package/src/dashboard/scanner/project-api.js +11 -2
  144. package/src/dashboard/scanner/skills-scanner.js +11 -1
  145. package/src/lib/detectors/index.js +1 -1
  146. package/src/lib/e2e/applicability.js +3 -1
  147. package/src/lib/git/tree-state.js +1 -16
  148. package/src/lib/graph/diff-tree.js +127 -0
  149. package/src/lib/graph/graphify-runtime.js +12 -5
  150. package/src/lib/installers/memory-installer.js +3 -3
  151. package/src/lib/installers/rules-installer.js +57 -0
  152. package/src/lib/installers/stack-assets.js +17 -0
  153. package/src/lib/installers/stack-sync.js +219 -0
  154. package/src/lib/plan/checks.js +61 -109
  155. package/src/lib/plan/critique.js +28 -0
  156. package/src/lib/plan/media.js +42 -0
  157. package/src/lib/plan/plan-html.js +125 -0
  158. package/src/lib/plan/plan-parser.js +34 -45
  159. package/src/lib/recap/piper.js +36 -0
  160. package/src/lib/recap/recap.js +232 -0
  161. package/src/lib/review-reports.js +28 -4
  162. package/src/lib/risk-detector.js +90 -11
  163. package/src/lib/specs/screen-checklist.js +33 -48
  164. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  165. package/src/lib/telemetry/metrics.js +27 -29
  166. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  167. package/src/lib/validators/verify-runner.js +0 -7
  168. package/src/lib/worktree/fleet-scanner.js +7 -20
  169. package/src/lib/worktree/worktree-audit.js +0 -37
  170. package/src/scripts/setup-infra.js +15 -50
  171. package/src/utils/agents-installer.js +27 -201
  172. package/src/utils/hooks-installer.js +11 -95
  173. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  174. package/framework/hooks/claude-code/statusline.py +0 -699
  175. package/framework/hooks/claude-code/statusline.sh +0 -11
  176. package/framework/hooks/shared/agent-reap.js +0 -74
  177. package/framework/hooks/shared/slice-progress.js +0 -110
  178. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  179. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  180. package/framework/skills/core/morph-review/SKILL.md +0 -129
  181. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  182. package/framework/standards/PROVENANCE.json +0 -119
  183. package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
  184. package/src/commands/project/worktree-reap.js +0 -34
  185. package/src/commands/state/handoff.js +0 -65
  186. package/src/commands/validation/mutate.js +0 -263
  187. package/src/commands/validation/scout-check.js +0 -28
  188. package/src/lib/generators/handoff-generator.js +0 -172
  189. package/src/lib/mutation/adapters.js +0 -79
  190. package/src/lib/mutation/commands.js +0 -61
  191. package/src/lib/mutation/dotnet-introspection.js +0 -281
  192. package/src/lib/mutation/manifest.js +0 -144
  193. package/src/lib/mutation/plan.js +0 -44
  194. package/src/lib/mutation/report.js +0 -68
  195. package/src/lib/mutation/runner.js +0 -301
  196. package/src/lib/mutation/schemata.js +0 -60
  197. package/src/lib/mutation/snapshot.js +0 -65
  198. package/src/lib/mutation/subset.js +0 -109
  199. package/src/lib/mutation/trx.js +0 -50
  200. package/src/lib/scout/scout-check.js +0 -293
  201. package/src/lib/validators/traceability-check.js +0 -103
  202. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,293 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
3
- import { join, relative, resolve } from 'path';
4
- import { workTreeTop } from '../../../framework/hooks/shared/git-paths.js';
5
-
6
- export const LINE_TOLERANCE = 2;
7
-
8
- const SPAN = /`([^`\n]+)`/g;
9
- const SPAN_CITATION = /^(.+\.[A-Za-z0-9]+):(\d+)(?:-(\d+))?$/;
10
- const BARE_CITATION = /(?<![\w.:\\/-])(?:[A-Za-z]:)?[\w.\\/-]*\w\.[A-Za-z0-9]+:(\d+)(?:-(\d+))?\b/g;
11
- const GREP_LINE = /^((?:[A-Za-z]:)?[^\s:]+\.[A-Za-z0-9]+)(?::(\d+):|-(\d+)-)(.*)$/;
12
- const FENCE = /^\s*(```|~~~)/;
13
- const HEADING = /^\s{0,3}#{1,6}\s+(.*)$/;
14
- const INFERENCE_HEADING = /inferid|hip[óo]tese|inferred|hypothes/i;
15
- const INFERENCE_MARKER = /\b(INFERIDO|hip[óo]tese)\b/i;
16
- const COMMAND_SPAN = /^(rg|grep|git\s+grep|morph-spec\s+graph|findstr|Select-String|sls)\b/;
17
- const COUNT = /(?:→|->|=>)\s*\d+|\b\d+\s+(?:ocorr[êe]ncias?|arquivos?|linhas?|hits?|resultados?|matches?|files?|lines?)\b/i;
18
- const PATH_SPAN = /^(?:[\w.-]*[\\/])+[\w.-]+$|^[\w.-]+\.(?:cs|csproj|fs|vb|razor|js|mjs|cjs|ts|tsx|jsx|json|md|py|go|java|sql|ya?ml|xml|props|targets|sln)$/i;
19
-
20
- const VERDICTS = [
21
- /\b(?:VIVO|VIVA|MORTO|MORTA|ALIVE|DEAD)\b/,
22
- /\b(?:c[óo]digo morto|dead code|unused)\b/i,
23
- /\bsem (?:nenhum )?(?:consumidor|chamador|leitor|escritor|setter|uso|refer[êe]ncia|teste)s?\b/i,
24
- /(?<!\b(?:execut|rod)\w*\s)\bnenhum[a]? (?:consumidor|chamador|leitor|escritor|setter|uso|refer[êe]ncia|teste|caller|call site)s?\b/i,
25
- /\bningu[ée]m (?:chama|usa|l[êe]|escreve|grava|consome|referencia)\b/i,
26
- /\bn[ãa]o (?:é|est[áa]) (?:usad|chamad|lid|referenciad|escrit)/i,
27
- /\bnunca (?:é )?(?:usad|chamad|lid|escrit)/i,
28
- /\bn[ãa]o h[áa] (?:consumidor|chamador|leitor|escritor|uso|teste|refer[êe]ncia)/i,
29
- /\bno (?:callers?|consumers?|readers?|writers?|tests?|usages?)\b/i,
30
- ];
31
-
32
- const WALK_SKIP = new Set(['node_modules', '.git', 'bin', 'obj', 'worktrees', '.worktrees']);
33
-
34
- function splitLines(text) {
35
- const lines = text.replace(/^/, '').split(/\r?\n/);
36
- if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
37
- return lines;
38
- }
39
-
40
- const squash = (s) => s.replace(/\s+/g, ' ').trim();
41
-
42
- function lineTokens(line) {
43
- const tokens = [];
44
- const masked = line.replace(SPAN, (whole, inner, index) => {
45
- const cite = inner.trim().match(SPAN_CITATION);
46
- if (cite) tokens.push({ index, cite: { raw: inner.trim(), path: cite[1], start: +cite[2], end: +(cite[3] ?? cite[2]) } });
47
- else tokens.push({ index, quote: inner });
48
- return ' '.repeat(whole.length);
49
- });
50
- for (const m of masked.matchAll(BARE_CITATION)) {
51
- const cite = m[0].match(SPAN_CITATION);
52
- if (cite) tokens.push({ index: m.index, cite: { raw: m[0], path: cite[1], start: +cite[2], end: +(cite[3] ?? cite[2]) } });
53
- }
54
- return tokens.sort((a, b) => a.index - b.index);
55
- }
56
-
57
- function citationsOf(line) {
58
- const tokens = lineTokens(line);
59
- const out = [];
60
- tokens.forEach((t, i) => {
61
- if (!t.cite) return;
62
- let quote = null;
63
- for (const next of tokens.slice(i + 1)) {
64
- if (next.cite) break;
65
- const q = next.quote.trim();
66
- if (COMMAND_SPAN.test(q) || PATH_SPAN.test(q)) continue;
67
- quote = next.quote;
68
- break;
69
- }
70
- out.push({ ...t.cite, quote });
71
- });
72
- return out;
73
- }
74
-
75
- function makeFileLister(root) {
76
- let files = null;
77
- return () => {
78
- if (files) return files;
79
- try {
80
- files = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
81
- cwd: root, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, maxBuffer: 64 * 1024 * 1024,
82
- }).toString().split('\0').filter(Boolean);
83
- } catch {
84
- files = [];
85
- const walk = (dir) => {
86
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
87
- if (WALK_SKIP.has(entry.name)) continue;
88
- const full = join(dir, entry.name);
89
- if (entry.isDirectory()) walk(full);
90
- else files.push(relative(root, full).replace(/\\/g, '/'));
91
- }
92
- };
93
- walk(root);
94
- }
95
- return files;
96
- };
97
- }
98
-
99
- const isFile = (p) => existsSync(p) && statSync(p).isFile();
100
-
101
- function resolveCitedPath(rawPath, root, listFiles) {
102
- const p = rawPath.replace(/\\/g, '/').replace(/^\.\//, '');
103
- if (/^[A-Za-z]:\//.test(p) || p.startsWith('/')) {
104
- return isFile(p) ? { path: p } : { problem: 'missing-file' };
105
- }
106
- const direct = join(root, p);
107
- if (isFile(direct)) return { path: direct };
108
- const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
109
- const suffix = fold(`/${p}`);
110
- const candidates = listFiles().filter((f) => fold(`/${f}`).endsWith(suffix));
111
- if (candidates.length === 1) return { path: join(root, candidates[0]) };
112
- if (candidates.length > 1) return { problem: 'ambiguous-path', candidates };
113
- return { problem: 'missing-file' };
114
- }
115
-
116
- function quoteAppears(quote, lines, start, end) {
117
- const from = Math.max(0, start - 1 - LINE_TOLERANCE);
118
- const to = Math.min(lines.length, end + LINE_TOLERANCE);
119
- const window = squash(lines.slice(from, to).join(' '));
120
- let at = 0;
121
- for (const fragment of quote.split(/\.\.\.|…/).map(squash).filter(Boolean)) {
122
- const found = window.indexOf(fragment, at);
123
- if (found === -1) return false;
124
- at = found + fragment.length;
125
- }
126
- return true;
127
- }
128
-
129
- function auditCitation(cite, root, listFiles, readCache) {
130
- const where = resolveCitedPath(cite.path, root, listFiles);
131
- if (where.problem === 'ambiguous-path') {
132
- return { kind: 'ambiguous-path', detail: `${where.candidates.length} arquivos casam: ${where.candidates.slice(0, 5).join(', ')}` };
133
- }
134
- if (where.problem) return { kind: 'missing-file', detail: `${cite.path} não existe sob ${root}` };
135
- if (!readCache.has(where.path)) readCache.set(where.path, splitLines(readFileSync(where.path, 'utf8')));
136
- const lines = readCache.get(where.path);
137
- cite.resolved = where.path;
138
- if (cite.start < 1 || cite.end < cite.start || cite.end > lines.length) {
139
- return { kind: 'line-out-of-range', detail: `o arquivo tem ${lines.length} linhas` };
140
- }
141
- const quotes = cite.quote != null ? [cite.quote] : cite.fence;
142
- if (!quotes) {
143
- return cite.listing ? null : { kind: 'missing-quote', detail: 'a citação não traz o trecho literal em crase depois dela' };
144
- }
145
- const missing = quotes.find((q) => !quoteAppears(q, lines, cite.start, cite.end));
146
- if (missing != null) {
147
- return {
148
- kind: 'quote-mismatch',
149
- detail: `trecho ausente nas linhas ${Math.max(1, cite.start - LINE_TOLERANCE)}-${Math.min(lines.length, cite.end + LINE_TOLERANCE)}; linha ${cite.start}: ${squash(lines[cite.start - 1])}`,
150
- };
151
- }
152
- cite.verified = true;
153
- return null;
154
- }
155
-
156
- const asMention = (line) => line.replace(SPAN, ' ').replace(/"[^"\n]*"|“[^”\n]*”/g, ' ');
157
-
158
- const isVerdict = (line) =>
159
- VERDICTS[0].test(line.replace(SPAN, ' ')) || VERDICTS.slice(1).some((re) => re.test(asMention(line)));
160
-
161
- const hasSearchCommand = (line) => [...line.matchAll(SPAN)].some((m) => COMMAND_SPAN.test(m[1].trim()));
162
-
163
- function underSearchCommand(lines, i) {
164
- for (let j = i - 1; j >= 0; j--) {
165
- if (hasSearchCommand(lines[j])) return true;
166
- if (HEADING.test(lines[j])) return false;
167
- }
168
- return false;
169
- }
170
-
171
- function auditVerdict(line, evidence, inInference) {
172
- if (!inInference && !INFERENCE_MARKER.test(line)) {
173
- return { kind: 'verdict-outside-inference', detail: 'veredito de ausência/vida fora da seção de hipóteses (Inferido)' };
174
- }
175
- const spans = [...evidence.matchAll(SPAN)].map((m) => m[1].trim());
176
- if (!spans.some((s) => COMMAND_SPAN.test(s))) {
177
- return { kind: 'verdict-without-command', detail: 'hipótese de ausência/vida sem o comando de busca usado, em crase' };
178
- }
179
- if (!COUNT.test(evidence)) return { kind: 'verdict-without-count', detail: 'hipótese de ausência/vida sem a contagem bruta do comando' };
180
- return null;
181
- }
182
-
183
- function fenceAfter(lines, i) {
184
- let j = i + 1;
185
- while (j < lines.length && lines[j].trim() === '') j++;
186
- if (j >= lines.length || !FENCE.test(lines[j])) return null;
187
- const body = [];
188
- for (let k = j + 1; k < lines.length && !FENCE.test(lines[k]); k++) {
189
- const text = lines[k].replace(/^\s*:?\d+[:\s]\s*/, '').trim();
190
- if (text) body.push(text);
191
- }
192
- return body.length ? body : null;
193
- }
194
-
195
- const indentOf = (line) => line.match(/^\s*/)[0].length;
196
- const BULLET = /^\s*([-*+]|\d+[.)])\s/;
197
-
198
- function itemStart(lines, i) {
199
- let start = i;
200
- for (let j = i - 1; j >= 0 && lines[j].trim() !== '' && !HEADING.test(lines[j]); j--) {
201
- if (BULLET.test(lines[j]) && indentOf(lines[j]) < indentOf(lines[start])) start = j;
202
- }
203
- return start;
204
- }
205
-
206
- function continuationEnd(lines, i) {
207
- const indent = indentOf(lines[i]);
208
- let j = i + 1;
209
- while (j < lines.length && lines[j].trim() !== '' && !HEADING.test(lines[j]) && !FENCE.test(lines[j])) {
210
- if (indentOf(lines[j]) <= indent && BULLET.test(lines[j])) break;
211
- j++;
212
- }
213
- return j;
214
- }
215
-
216
- export function resolveScoutRoot(root, cwd = process.cwd()) {
217
- return root ? resolve(cwd, root) : (workTreeTop(cwd) ?? cwd);
218
- }
219
-
220
- export function checkScoutMap(text, { root }) {
221
- const lines = splitLines(text);
222
- const listFiles = makeFileLister(root);
223
- const readCache = new Map();
224
- const failures = [];
225
- const citations = [];
226
- let verdicts = 0;
227
- let inFence = false;
228
- let inferenceLevel = null;
229
- let subsectionStart = null;
230
-
231
- const audit = (cite, lineNo) => {
232
- citations.push(cite);
233
- const problem = auditCitation(cite, root, listFiles, readCache);
234
- if (problem) failures.push({ line: lineNo, citation: cite.raw, cite, ...problem });
235
- };
236
- const subsectionText = (from) => {
237
- let to = from + 1;
238
- while (to < lines.length && !HEADING.test(lines[to])) to++;
239
- return lines.slice(from, to).join('\n');
240
- };
241
-
242
- lines.forEach((line, i) => {
243
- const lineNo = i + 1;
244
- if (FENCE.test(line)) { inFence = !inFence; return; }
245
- if (inFence) {
246
- const grep = line.trim().match(GREP_LINE);
247
- if (grep) {
248
- const n = +(grep[2] ?? grep[3]);
249
- audit({ raw: `${grep[1]}:${n}`, path: grep[1], start: n, end: n, quote: grep[4].trim() || null }, lineNo);
250
- }
251
- return;
252
- }
253
- const heading = line.match(HEADING);
254
- if (heading) {
255
- const level = line.trim().match(/^#+/)[0].length;
256
- if (INFERENCE_HEADING.test(heading[1])) { inferenceLevel = level; subsectionStart = null; }
257
- else if (inferenceLevel != null && level > inferenceLevel) subsectionStart = i;
258
- else { inferenceLevel = null; subsectionStart = null; }
259
- }
260
-
261
- const cites = citationsOf(line);
262
- const residue = cites.reduce((rest, c) => rest.split(c.raw).join(' '), line)
263
- .replace(/[`*_|>()[\]\-+:;,.—–\s]|\d+[.)]/g, '');
264
- for (const cite of cites) {
265
- if (cite.quote == null) {
266
- cite.fence = fenceAfter(lines, i);
267
- cite.listing = residue === '' && underSearchCommand(lines, i);
268
- }
269
- audit(cite, lineNo);
270
- }
271
-
272
- if (!isVerdict(line)) return;
273
- verdicts++;
274
- const evidence = subsectionStart != null
275
- ? subsectionText(subsectionStart)
276
- : lines.slice(itemStart(lines, i), continuationEnd(lines, itemStart(lines, i))).join('\n');
277
- const verdict = auditVerdict(line, evidence, inferenceLevel != null);
278
- if (verdict) failures.push({ line: lineNo, text: squash(line), ...verdict });
279
- });
280
-
281
- const covered = (c) => citations.some((v) => v.verified && v.resolved === c.resolved && v.start <= c.start && c.end <= v.end);
282
- const kept = failures.filter((f) => !(f.kind === 'missing-quote' && covered(f.cite)));
283
- if (citations.length === 0) {
284
- kept.push({ line: null, kind: 'no-citations', detail: 'o mapa não cita nenhum arquivo:linha — nada nele é verificável' });
285
- }
286
- return {
287
- ok: kept.length === 0,
288
- root,
289
- citations: citations.map(({ raw, resolved, quote }) => ({ raw, resolved: resolved ?? null, quote })),
290
- verdicts,
291
- failures: kept.map(({ cite, ...f }) => f),
292
- };
293
- }
@@ -1,103 +0,0 @@
1
- // Advisory: status is only 'pass' | 'warn' | 'skip', and runVerify() keeps it outside
2
- // `nodes` so it never flips the verify rollup.
3
-
4
- import { existsSync, readFileSync } from 'fs';
5
- import { join } from 'path';
6
- import { readPlanChecks } from '../plan/checks.js';
7
- import { sliceProgress, sliceReportPath } from '../../../framework/hooks/shared/slice-progress.js';
8
-
9
- const FILES_BLOCK_RE = /\*\*Arquivos:\*\*([^\r\n]*)\r?\n?((?:^[ \t]*-[ \t].*\r?\n?)*)/gm;
10
-
11
- const BACKTICK_RE = /`([^`]+)`/g;
12
-
13
- // A later span without `/` is ambiguous shorthand and is skipped, not guessed: false
14
- // negatives are acceptable for advisory tooling, false positives are not.
15
- export function extractCitedFiles(reportText) {
16
- const cited = new Set();
17
- if (!reportText) return [];
18
-
19
- FILES_BLOCK_RE.lastIndex = 0;
20
- let blockMatch;
21
- while ((blockMatch = FILES_BLOCK_RE.exec(reportText)) !== null) {
22
- const bullets = blockMatch[2].split(/\r?\n/).filter((l) => /^[ \t]*-[ \t]/.test(l));
23
- for (const line of [blockMatch[1], ...bullets]) {
24
- const spans = [...line.matchAll(BACKTICK_RE)].map((m) => m[1].trim()).filter(Boolean);
25
- spans.forEach((span, idx) => {
26
- if (idx > 0 && !span.includes('/')) return;
27
- if (span.includes('{') || span.includes('}')) return;
28
- if (span.endsWith('/')) return;
29
- cited.add(normalizePath(span));
30
- });
31
- }
32
- }
33
-
34
- return [...cited];
35
- }
36
-
37
- export function normalizePath(p) {
38
- return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
39
- }
40
-
41
- /** null when the plan has no readable extraction. */
42
- export function collectSliceFiles(projectPath, featureName) {
43
- const checks = readPlanChecks(projectPath, featureName).checks;
44
- if (!checks) return null;
45
- return checks.slices.flatMap((slice) => (slice.files ?? [])
46
- .filter((f) => typeof f === 'string' && f.trim())
47
- .map(normalizePath));
48
- }
49
-
50
- function isCovered(file, declared) {
51
- for (const entry of declared) {
52
- if (entry === file) return true;
53
- if (entry.endsWith('/') && file.startsWith(entry)) return true;
54
- }
55
- return false;
56
- }
57
-
58
- function readOrNull(path) {
59
- try {
60
- return existsSync(path) ? readFileSync(path, 'utf8') : null;
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- export function checkOutputsTraceability(projectPath, featureName) {
67
- const featureDir = join(projectPath, '.morph', 'features', featureName);
68
- const progress = sliceProgress(featureDir);
69
- const reports = (progress?.slices ?? [])
70
- .map((slice) => ({ id: slice.id, text: readOrNull(sliceReportPath(featureDir, slice.id)) }))
71
- .filter((r) => r.text !== null);
72
- if (reports.length === 0) {
73
- return { status: 'skip', warnings: [], reason: 'no slice reports', citedCount: 0 };
74
- }
75
-
76
- const citedBy = new Map();
77
- for (const { id, text } of reports) {
78
- for (const file of extractCitedFiles(text)) {
79
- if (!citedBy.has(file)) citedBy.set(file, []);
80
- citedBy.get(file).push(id);
81
- }
82
- }
83
- if (citedBy.size === 0) {
84
- return { status: 'skip', warnings: [], reason: 'no Arquivos: entries found in the slice reports', citedCount: 0 };
85
- }
86
-
87
- const declared = collectSliceFiles(projectPath, featureName);
88
- if (declared === null) {
89
- return { status: 'skip', warnings: [], reason: 'no checks.json', citedCount: citedBy.size };
90
- }
91
-
92
- const warnings = [...citedBy.keys()]
93
- .filter((file) => !isCovered(file, declared))
94
- .sort()
95
- .map((file) => `${file} — cited in reports/${citedBy.get(file).join(', ')} but declared in no slice's arquivos:`);
96
-
97
- return {
98
- status: warnings.length > 0 ? 'warn' : 'pass',
99
- warnings,
100
- reason: null,
101
- citedCount: citedBy.size,
102
- };
103
- }
@@ -1,247 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import {
3
- closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync,
4
- } from 'node:fs';
5
- import { basename, join, resolve } from 'node:path';
6
- import {
7
- cwdIsInsideWorktree, isGitRepo, parseWorktreeList, resolvePrimaryRoot, runWorktreeRemove,
8
- sessionAnchoredInWorktree,
9
- } from '../../commands/project/worktree.js';
10
- import { containerDirs, isUnder } from '../../../framework/hooks/shared/worktree-root.js';
11
- import { withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
12
- import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
13
- import { pidAlive } from '../../../framework/hooks/shared/process-alive.js';
14
- import { readProjectConfig } from '../../../framework/hooks/shared/heavy-config.js';
15
-
16
- export const DEFAULT_GRACE_MINUTES = 30;
17
-
18
- const AGENT_DIR = /^agent-([0-9a-f]+)$/;
19
- const AGENT_BRANCH = /^morph-task\/agent-([0-9a-f]+)$/;
20
- const AGENT_REF = /^refs\/heads\/morph-task\/agent-[0-9a-f]+$/;
21
- const TAIL_BYTES = 64 * 1024;
22
- const LOCK_MAX_AGE_MS = 15 * 60_000;
23
- const SAMPLE = 5;
24
-
25
- const norm = (p) => resolve(p).replace(/\\/g, '/');
26
- const gitOut = (cwd, args, input) => execFileSync('git', args, {
27
- cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', input,
28
- });
29
- const sample = (list) => `${list.slice(0, SAMPLE).join(', ')}${list.length > SAMPLE ? `, +${list.length - SAMPLE}` : ''}`;
30
-
31
- export function resolveGraceMinutes(root, override) {
32
- const valid = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
33
- if (valid(override)) return override;
34
- const configured = readProjectConfig(root)?.worktree?.reap?.graceMinutes;
35
- return valid(configured) ? configured : DEFAULT_GRACE_MINUTES;
36
- }
37
-
38
- /**
39
- * Commits reachable from `tip` and from no other branch, remote or tag; agent branches are left out
40
- * of the comparison, and `--all` is not used (the agent's own HEAD would protect it). `null` = unmeasured.
41
- */
42
- export function exclusiveCommitCount(root, tip) {
43
- if (!tip) return null;
44
- try {
45
- const refs = gitOut(root, ['for-each-ref', '--format=%(refname)', 'refs/heads', 'refs/remotes', 'refs/tags'])
46
- .split(/\r?\n/).filter((r) => r && !AGENT_REF.test(r));
47
- const input = `${[tip, ...refs.map((r) => `^${r}`)].join('\n')}\n`;
48
- const count = Number.parseInt(gitOut(root, ['rev-list', '--count', '--stdin'], input).trim(), 10);
49
- return Number.isInteger(count) ? count : null;
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- function tailOfEvents(file) {
56
- const fd = openSync(file, 'r');
57
- try {
58
- const { size, mtimeMs } = fstatSync(fd);
59
- const len = Math.min(size, TAIL_BYTES);
60
- const buf = Buffer.alloc(len);
61
- readSync(fd, buf, 0, len, size - len);
62
- const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim());
63
- if (size > len) lines.shift();
64
- return { last: lines.at(-1) ?? null, mtimeMs };
65
- } finally {
66
- closeSync(fd);
67
- }
68
- }
69
-
70
- /** `stopped` only when the tree's last event is this agent's own `AgentUsage`. */
71
- export function readAgentStop(worktreePath, hex) {
72
- const file = join(worktreePath, '.morph', 'logs', 'events.jsonl');
73
- if (!existsSync(file)) return { status: 'unknown', detail: 'sem .morph/logs/events.jsonl na árvore' };
74
- let tail;
75
- try { tail = tailOfEvents(file); } catch { return { status: 'unknown', detail: 'events.jsonl ilegível' }; }
76
- let event = null;
77
- try { event = tail.last ? JSON.parse(tail.last) : null; } catch { event = null; }
78
- if (!event || typeof event !== 'object') return { status: 'unknown', detail: 'último evento ilegível' };
79
- if (event.event !== 'AgentUsage') return { status: 'active', detail: `último evento: ${event.event ?? '?'}` };
80
- if (String(event.agentId ?? '').toLowerCase() !== hex) return { status: 'unknown', detail: 'AgentUsage de outro agente' };
81
- return { status: 'stopped', mtimeMs: tail.mtimeMs };
82
- }
83
-
84
- function dirtOf(worktreePath) {
85
- try {
86
- const lines = gitOut(worktreePath, ['status', '--porcelain', '--untracked-files=all']).split(/\r?\n/).filter((l) => l.trim());
87
- return withoutLeftoverIgnore(worktreePath, lines).map((l) => (l.startsWith('??') ? `untracked ${l.slice(3)}` : l.slice(3)));
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- function agentBranches(root) {
94
- try {
95
- return gitOut(root, ['for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/heads/morph-task/'])
96
- .split(/\r?\n/).filter(Boolean)
97
- .map((line) => { const [name, sha] = line.split(' '); return { name, sha, hex: AGENT_BRANCH.exec(name)?.[1] }; })
98
- .filter((b) => b.hex);
99
- } catch {
100
- return [];
101
- }
102
- }
103
-
104
- function branchTouchedMs(root, branch) {
105
- const common = resolveGitCommonDir(root);
106
- if (!common) return null;
107
- try { return statSync(join(common, 'logs', 'refs', 'heads', ...branch.split('/'))).mtimeMs; } catch { return null; }
108
- }
109
-
110
- function agentWorktrees(root) {
111
- const rootNorm = norm(root);
112
- const containers = containerDirs(root);
113
- return parseWorktreeList(root).flatMap((e) => {
114
- const path = norm(e.path);
115
- const hex = AGENT_DIR.exec(basename(path))?.[1];
116
- if (!hex || path === rootNorm) return [];
117
- if (!isUnder(containers, path)) return [];
118
- const branch = e.branch ? e.branch.replace(/^refs\/heads\//, '') : null;
119
- if (branch && branch !== `morph-task/agent-${hex}`) return [];
120
- return [{ path, hex, branch, tip: e.head, locked: e.locked }];
121
- });
122
- }
123
-
124
- function judgeWorktree(item, ctx) {
125
- const keep = (state, reason, detail) => ({ ...item, state, reason, detail });
126
- if (!existsSync(item.path)) return keep('preserved', 'directory-missing', 'o diretório sumiu; `morph-spec doctor --worktrees --prune` limpa o registro');
127
- if (cwdIsInsideWorktree(ctx.cwd, item.path) || sessionAnchoredInWorktree(item.path, ctx.env)) return keep('active', 'anchored');
128
- if (item.locked) return keep('preserved', 'locked', '`git worktree unlock` libera, se o lock já não vale');
129
-
130
- const stop = readAgentStop(item.path, item.hex);
131
- if (stop.status === 'active') return keep('active', 'agent-active', stop.detail);
132
- if (stop.status !== 'stopped') return keep('preserved', 'no-usage-event', stop.detail);
133
- const idleMin = (ctx.now - stop.mtimeMs) / 60_000;
134
- if (idleMin < ctx.graceMinutes) return keep('pending', 'within-grace', `parou há ${Math.floor(idleMin)} min (graça de ${ctx.graceMinutes} min)`);
135
-
136
- const dirt = dirtOf(item.path);
137
- if (dirt === null) return keep('preserved', 'unknown', 'git status falhou');
138
- if (dirt.length) return keep('preserved', 'dirty', sample(dirt));
139
-
140
- const exclusive = exclusiveCommitCount(ctx.root, item.tip);
141
- if (exclusive === null) return keep('preserved', 'unknown', 'não foi possível contar os commits exclusivos');
142
- if (exclusive > 0) return keep('preserved', 'exclusive-commits', `${exclusive} commit(s) só nesta branch`);
143
- return keep('reapable', 'stopped-clean');
144
- }
145
-
146
- function judgeOrphanBranch(branch, ctx) {
147
- const item = { path: null, hex: branch.hex, branch: branch.name, tip: branch.sha };
148
- const keep = (state, reason, detail) => ({ ...item, state, reason, detail });
149
- const exclusive = exclusiveCommitCount(ctx.root, branch.sha);
150
- if (exclusive === null) return keep('preserved', 'unknown', 'não foi possível contar os commits exclusivos');
151
- if (exclusive > 0) return keep('preserved', 'exclusive-commits', `${exclusive} commit(s) só nesta branch`);
152
- const touched = branchTouchedMs(ctx.root, branch.name);
153
- if (touched === null) return keep('preserved', 'unknown', 'sem reflog para medir a idade da branch');
154
- const idleMin = (ctx.now - touched) / 60_000;
155
- if (idleMin < ctx.graceMinutes) return keep('pending', 'within-grace', `branch tocada há ${Math.floor(idleMin)} min (graça de ${ctx.graceMinutes} min)`);
156
- return keep('reapable', 'orphan-branch');
157
- }
158
-
159
- /** Read-only: every agent tree and orphan agent branch with its verdict. */
160
- export function planAgentReap(root, { cwd = root, now = Date.now(), graceMinutes, env = process.env } = {}) {
161
- const ctx = { root, cwd, now, env, graceMinutes: resolveGraceMinutes(root, graceMinutes) };
162
- const trees = agentWorktrees(root);
163
- const withTree = new Set(trees.map((t) => t.branch).filter(Boolean));
164
- const orphans = agentBranches(root).filter((b) => !withTree.has(b.name));
165
- const items = [...trees.map((t) => judgeWorktree(t, ctx)), ...orphans.map((b) => judgeOrphanBranch(b, ctx))];
166
- return { graceMinutes: ctx.graceMinutes, items };
167
- }
168
-
169
- function acquireReapLock(root, now) {
170
- const common = resolveGitCommonDir(root);
171
- if (!common) return null;
172
- const dir = join(common, 'morph-spec');
173
- const file = join(dir, 'agent-reap.lock');
174
- try { mkdirSync(dir, { recursive: true }); } catch { return null; }
175
- for (let attempt = 0; attempt < 2; attempt += 1) {
176
- try {
177
- writeFileSync(file, JSON.stringify({ pid: process.pid, since: now }), { flag: 'wx' });
178
- return () => { try { unlinkSync(file); } catch { /* already gone */ } };
179
- } catch (err) {
180
- if (err.code !== 'EEXIST') return null;
181
- }
182
- let holder = null;
183
- try { holder = JSON.parse(readFileSync(file, 'utf8')); } catch { holder = null; }
184
- const stale = !holder || !pidAlive(holder.pid) || now - Number(holder.since) > LOCK_MAX_AGE_MS;
185
- if (!stale) return null;
186
- try { unlinkSync(file); } catch { return null; }
187
- }
188
- return null;
189
- }
190
-
191
- function deleteAgentBranch(root, branch, sha) {
192
- if (exclusiveCommitCount(root, sha) !== 0) return false;
193
- try { gitOut(root, ['branch', '-D', branch]); return true; } catch { return false; }
194
- }
195
-
196
- const brief = ({ path, branch, hex }) => ({ path, branch, hex });
197
- const why = ({ path, branch, hex, reason, detail }) => ({ path, branch, hex, reason, ...(detail ? { detail } : {}) });
198
-
199
- /**
200
- * Removes the worktrees and branches of stopped, clean agents that hold no commit only they have;
201
- * everything else is listed with the reason and left alone.
202
- */
203
- export async function reapAgentWorktrees(cwd = process.cwd(), {
204
- dryRun = false, graceMinutes, now = Date.now, env = process.env, remove = runWorktreeRemove,
205
- } = {}) {
206
- if (!isGitRepo(cwd)) return { ok: false, error: 'not-a-git-repo', dryRun };
207
- const root = resolvePrimaryRoot(cwd);
208
- const summarize = (plan, extra = {}) => {
209
- const of = (state) => plan.items.filter((i) => i.state === state);
210
- return {
211
- ok: true, dryRun, graceMinutes: plan.graceMinutes,
212
- reaped: [], branchesDeleted: [],
213
- ...(dryRun ? { wouldReap: of('reapable').map(brief) } : {}),
214
- preserved: [...of('preserved'), ...of('pending')].map(why),
215
- active: of('active').length,
216
- ...extra,
217
- };
218
- };
219
-
220
- if (dryRun) return summarize(planAgentReap(root, { cwd, now: now(), graceMinutes, env }));
221
-
222
- const release = acquireReapLock(root, now());
223
- if (!release) return { ...summarize({ graceMinutes: resolveGraceMinutes(root, graceMinutes), items: [] }), busy: true };
224
- try {
225
- const plan = planAgentReap(root, { cwd, now: now(), graceMinutes, env });
226
- const out = summarize(plan);
227
- for (const item of plan.items.filter((i) => i.state === 'reapable')) {
228
- if (item.path) {
229
- const { out: removal } = await remove(null, { target: item.path, ifClean: true, relinkOnFailure: true, cwd: root });
230
- if (!removal.removed) {
231
- out.preserved.push(why({ ...item, reason: 'remove-failed', detail: removal.hint || removal.error }));
232
- continue;
233
- }
234
- out.reaped.push({ ...brief(item), ...(removal.ignoredBackup ? { backup: removal.ignoredBackup } : {}) });
235
- if (item.branch && deleteAgentBranch(root, item.branch, item.tip)) out.branchesDeleted.push(item.branch);
236
- } else if (deleteAgentBranch(root, item.branch, item.tip)) {
237
- out.branchesDeleted.push(item.branch);
238
- out.reaped.push(brief(item));
239
- } else {
240
- out.preserved.push(why({ ...item, reason: 'unknown', detail: 'a branch mudou antes de ser apagada' }));
241
- }
242
- }
243
- return out;
244
- } finally {
245
- release();
246
- }
247
- }