@polymorphism-tech/morph-spec 4.7.1 → 4.7.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 (232) hide show
  1. package/.morph/.morphversion +5 -0
  2. package/.morph/analytics/threads-log.jsonl +5 -0
  3. package/.morph/config/config.json +8 -0
  4. package/.morph/framework/agents.json +1815 -0
  5. package/.morph/framework/hooks/README.md +205 -0
  6. package/.morph/framework/hooks/claude-code/notification/approval-reminder.js +54 -0
  7. package/.morph/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
  8. package/.morph/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
  9. package/.morph/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
  10. package/.morph/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
  11. package/.morph/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
  12. package/.morph/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
  13. package/.morph/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
  14. package/.morph/framework/hooks/claude-code/statusline.py +538 -0
  15. package/.morph/framework/hooks/claude-code/statusline.sh +7 -0
  16. package/.morph/framework/hooks/claude-code/stop/validate-completion.js +88 -0
  17. package/.morph/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
  18. package/.morph/framework/hooks/git/commit-msg/conventional-commits.sh +33 -0
  19. package/.morph/framework/hooks/git/pre-commit/agents.sh +25 -0
  20. package/.morph/framework/hooks/git/pre-commit/orchestrator.sh +64 -0
  21. package/.morph/framework/hooks/git/pre-commit/specs.sh +50 -0
  22. package/.morph/framework/hooks/git/pre-push/run-tests.sh +44 -0
  23. package/.morph/framework/hooks/shared/hook-response.js +45 -0
  24. package/.morph/framework/hooks/shared/phase-utils.js +129 -0
  25. package/.morph/framework/hooks/shared/state-reader.js +138 -0
  26. package/.morph/framework/hooks/shared/stdin-reader.js +26 -0
  27. package/.morph/framework/standards/STANDARDS.json +933 -0
  28. package/.morph/framework/standards/ai-agents/blazor-ui.md +364 -0
  29. package/.morph/framework/standards/ai-agents/production.md +415 -0
  30. package/.morph/framework/standards/ai-agents/setup.md +418 -0
  31. package/.morph/framework/standards/ai-agents/team-orchestration.md +479 -0
  32. package/.morph/framework/standards/ai-agents/workflows.md +354 -0
  33. package/.morph/framework/standards/architecture/ddd/aggregates.md +120 -0
  34. package/.morph/framework/standards/architecture/ddd/bounded-contexts.md +105 -0
  35. package/.morph/framework/standards/architecture/ddd/complexity-levels.md +108 -0
  36. package/.morph/framework/standards/architecture/ddd/entities.md +99 -0
  37. package/.morph/framework/standards/architecture/ddd/ubiquitous-language.md +58 -0
  38. package/.morph/framework/standards/architecture/ddd/value-objects.md +124 -0
  39. package/.morph/framework/standards/backend/api/minimal-api.md +494 -0
  40. package/.morph/framework/standards/backend/api/rest.md +492 -0
  41. package/.morph/framework/standards/backend/api/validation.md +88 -0
  42. package/.morph/framework/standards/backend/authentication/passkeys.md +428 -0
  43. package/.morph/framework/standards/backend/database/ef-core.md +199 -0
  44. package/.morph/framework/standards/backend/database/migrations.md +393 -0
  45. package/.morph/framework/standards/backend/database/postgresql/database.md +352 -0
  46. package/.morph/framework/standards/backend/database/repository-patterns.md +528 -0
  47. package/.morph/framework/standards/backend/database/vector-search-rag.md +541 -0
  48. package/.morph/framework/standards/backend/dotnet/async.md +366 -0
  49. package/.morph/framework/standards/backend/dotnet/core.md +117 -0
  50. package/.morph/framework/standards/backend/dotnet/di.md +439 -0
  51. package/.morph/framework/standards/backend/dotnet/program-cs-checklist.md +92 -0
  52. package/.morph/framework/standards/backend/integrations/asaas/asaas-api.md +216 -0
  53. package/.morph/framework/standards/backend/integrations/clerk/clerk-auth.md +290 -0
  54. package/.morph/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
  55. package/.morph/framework/standards/backend/integrations/resend/resend-email.md +385 -0
  56. package/.morph/framework/standards/context/analytics.md +96 -0
  57. package/.morph/framework/standards/context/bundles.md +110 -0
  58. package/.morph/framework/standards/context/priming.md +78 -0
  59. package/.morph/framework/standards/core/architecture.md +185 -0
  60. package/.morph/framework/standards/core/coding.md +214 -0
  61. package/.morph/framework/standards/core/git-branching-strategy.md +403 -0
  62. package/.morph/framework/standards/core/git.md +185 -0
  63. package/.morph/framework/standards/core/testing.md +295 -0
  64. package/.morph/framework/standards/data/nosql/blob-storage.md +102 -0
  65. package/.morph/framework/standards/data/nosql/cache/redis.md +97 -0
  66. package/.morph/framework/standards/data/nosql/cosmos-db.md +118 -0
  67. package/.morph/framework/standards/data/vector-search/azure-ai-search.md +121 -0
  68. package/.morph/framework/standards/data/vector-search/rag-chunking.md +104 -0
  69. package/.morph/framework/standards/frontend/blazor/design-checklist.md +222 -0
  70. package/.morph/framework/standards/frontend/blazor/fluent-ui-setup.md +595 -0
  71. package/.morph/framework/standards/frontend/blazor/fluent-ui.md +137 -0
  72. package/.morph/framework/standards/frontend/blazor/html-conversion.md +184 -0
  73. package/.morph/framework/standards/frontend/blazor/lifecycle.md +195 -0
  74. package/.morph/framework/standards/frontend/blazor/pitfalls.md +198 -0
  75. package/.morph/framework/standards/frontend/blazor/state.md +191 -0
  76. package/.morph/framework/standards/frontend/design-system/animations.md +151 -0
  77. package/.morph/framework/standards/frontend/design-system/naming.md +64 -0
  78. package/.morph/framework/standards/frontend/nextjs/app-router.md +123 -0
  79. package/.morph/framework/standards/frontend/nextjs/components.md +132 -0
  80. package/.morph/framework/standards/frontend/nextjs/data-fetching.md +126 -0
  81. package/.morph/framework/standards/frontend/nextjs/forms.md +128 -0
  82. package/.morph/framework/standards/frontend/nextjs/naming-conventions.md +67 -0
  83. package/.morph/framework/standards/frontend/nextjs/nextjs-patterns.md +215 -0
  84. package/.morph/framework/standards/frontend/nextjs/project-structure.md +102 -0
  85. package/.morph/framework/standards/frontend/nextjs/state-management.md +72 -0
  86. package/.morph/framework/standards/frontend/nextjs/testing.md +111 -0
  87. package/.morph/framework/standards/infrastructure/azure/azure.md +624 -0
  88. package/.morph/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
  89. package/.morph/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
  90. package/.morph/framework/standards/infrastructure/azure/devops/local-development.md +520 -0
  91. package/.morph/framework/standards/infrastructure/azure/services/functions.md +486 -0
  92. package/.morph/framework/standards/infrastructure/azure/services/service-bus.md +459 -0
  93. package/.morph/framework/standards/infrastructure/azure/services/storage.md +407 -0
  94. package/.morph/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -0
  95. package/.morph/framework/standards/infrastructure/supabase/mcp-setup.md +252 -0
  96. package/.morph/framework/standards/infrastructure/supabase/supabase-auth.md +176 -0
  97. package/.morph/framework/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
  98. package/.morph/framework/standards/infrastructure/supabase/supabase-rls.md +184 -0
  99. package/.morph/framework/standards/infrastructure/supabase/supabase-storage.md +153 -0
  100. package/.morph/framework/standards/integration/api/graphql.md +91 -0
  101. package/.morph/framework/standards/integration/api/grpc.md +114 -0
  102. package/.morph/framework/standards/integration/api/rest-design.md +95 -0
  103. package/.morph/framework/standards/integration/event-driven/cqrs.md +101 -0
  104. package/.morph/framework/standards/integration/event-driven/event-sourcing.md +124 -0
  105. package/.morph/framework/standards/integration/event-driven/service-bus.md +95 -0
  106. package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
  107. package/.morph/framework/standards/observability/logging.md +131 -0
  108. package/.morph/framework/standards/observability/metrics.md +121 -0
  109. package/.morph/framework/standards/observability/monitoring.md +114 -0
  110. package/.morph/framework/standards/observability/tracing.md +132 -0
  111. package/.morph/framework/standards/workflows/parallel-execution.md +112 -0
  112. package/.morph/framework/standards/workflows/thread-management.md +113 -0
  113. package/.morph/framework/templates/.idea/morph-templates.xml +92 -0
  114. package/.morph/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  115. package/.morph/framework/templates/IDE-SNIPPETS.md +266 -0
  116. package/.morph/framework/templates/README.md +814 -0
  117. package/.morph/framework/templates/REGISTRY.json +1888 -0
  118. package/.morph/framework/templates/code/dotnet/backend/repository.cs +141 -0
  119. package/.morph/framework/templates/code/dotnet/backend/service.cs +139 -0
  120. package/.morph/framework/templates/code/dotnet/contracts/Commands.cs +74 -0
  121. package/.morph/framework/templates/code/dotnet/contracts/Entities.cs +25 -0
  122. package/.morph/framework/templates/code/dotnet/contracts/Queries.cs +74 -0
  123. package/.morph/framework/templates/code/dotnet/contracts/README.md +74 -0
  124. package/.morph/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -0
  125. package/.morph/framework/templates/code/dotnet/contracts/contracts-level1.cs +69 -0
  126. package/.morph/framework/templates/code/dotnet/contracts/contracts-level2.cs +86 -0
  127. package/.morph/framework/templates/code/dotnet/contracts/contracts-level3.cs +41 -0
  128. package/.morph/framework/templates/code/dotnet/database/migration.cs +83 -0
  129. package/.morph/framework/templates/code/dotnet/frontend/component.razor +239 -0
  130. package/.morph/framework/templates/code/dotnet/jobs/agent.cs +163 -0
  131. package/.morph/framework/templates/code/dotnet/jobs/job.cs +171 -0
  132. package/.morph/framework/templates/code/dotnet/test.cs +239 -0
  133. package/.morph/framework/templates/code/sql/rls-policy.sql +57 -0
  134. package/.morph/framework/templates/code/sql/supabase-migration.sql +100 -0
  135. package/.morph/framework/templates/code/sql/supabase-migration.template.sql +113 -0
  136. package/.morph/framework/templates/code/typescript/contracts.ts +168 -0
  137. package/.morph/framework/templates/context/CONTEXT-FEATURE.md +276 -0
  138. package/.morph/framework/templates/context/CONTEXT.md +181 -0
  139. package/.morph/framework/templates/docs/clarifications.md +253 -0
  140. package/.morph/framework/templates/docs/onboarding.md +123 -0
  141. package/.morph/framework/templates/docs/proposal.md +182 -0
  142. package/.morph/framework/templates/docs/schema-analysis.md +119 -0
  143. package/.morph/framework/templates/docs/spec.md +198 -0
  144. package/.morph/framework/templates/docs/ui-components.md +124 -0
  145. package/.morph/framework/templates/docs/ui-design-system.md +76 -0
  146. package/.morph/framework/templates/docs/ui-flows.md +167 -0
  147. package/.morph/framework/templates/docs/ui-mockups.md +98 -0
  148. package/.morph/framework/templates/docs/user-stories.md +34 -0
  149. package/.morph/framework/templates/examples/design-system-examples.md +357 -0
  150. package/.morph/framework/templates/examples/spec-examples.md +90 -0
  151. package/.morph/framework/templates/feature/decisions.md +187 -0
  152. package/.morph/framework/templates/feature/recap.md +146 -0
  153. package/.morph/framework/templates/feature/tasks.md +199 -0
  154. package/.morph/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -0
  155. package/.morph/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -0
  156. package/.morph/framework/templates/frontend/nextjs/env.mjs.hbs +32 -0
  157. package/.morph/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -0
  158. package/.morph/framework/templates/frontend/nextjs/page.tsx.hbs +22 -0
  159. package/.morph/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -0
  160. package/.morph/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -0
  161. package/.morph/framework/templates/infrastructure/azure/Dockerfile.example +82 -0
  162. package/.morph/framework/templates/infrastructure/azure/README.md +286 -0
  163. package/.morph/framework/templates/infrastructure/azure/app-insights.bicep +63 -0
  164. package/.morph/framework/templates/infrastructure/azure/app-service.bicep +164 -0
  165. package/.morph/framework/templates/infrastructure/azure/container-app-env.bicep +49 -0
  166. package/.morph/framework/templates/infrastructure/azure/container-app.bicep +156 -0
  167. package/.morph/framework/templates/infrastructure/azure/deploy-checklist.md +426 -0
  168. package/.morph/framework/templates/infrastructure/azure/deploy.ps1 +229 -0
  169. package/.morph/framework/templates/infrastructure/azure/deploy.sh +208 -0
  170. package/.morph/framework/templates/infrastructure/azure/key-vault.bicep +91 -0
  171. package/.morph/framework/templates/infrastructure/azure/main.bicep +189 -0
  172. package/.morph/framework/templates/infrastructure/azure/parameters.dev.json +29 -0
  173. package/.morph/framework/templates/infrastructure/azure/parameters.prod.json +29 -0
  174. package/.morph/framework/templates/infrastructure/azure/parameters.staging.json +29 -0
  175. package/.morph/framework/templates/infrastructure/azure/sql-database.bicep +103 -0
  176. package/.morph/framework/templates/infrastructure/azure/storage.bicep +106 -0
  177. package/.morph/framework/templates/infrastructure/docker/Dockerfile.template +58 -0
  178. package/.morph/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -0
  179. package/.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
  180. package/.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
  181. package/.morph/framework/templates/infrastructure/docker/easypanel.template.json +54 -0
  182. package/.morph/framework/templates/infrastructure/github/README.md +593 -0
  183. package/.morph/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
  184. package/.morph/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
  185. package/.morph/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
  186. package/.morph/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
  187. package/.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
  188. package/.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
  189. package/.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
  190. package/.morph/framework/templates/integrations/asaas-client.cs +387 -0
  191. package/.morph/framework/templates/integrations/asaas-webhook.cs +351 -0
  192. package/.morph/framework/templates/integrations/azure-identity-config.cs +288 -0
  193. package/.morph/framework/templates/integrations/clerk-config.cs +258 -0
  194. package/.morph/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -0
  195. package/.morph/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
  196. package/.morph/framework/templates/meta-prompts/hops/hop-retry.md +78 -0
  197. package/.morph/framework/templates/meta-prompts/hops/hop-validation.md +97 -0
  198. package/.morph/framework/templates/meta-prompts/hops/hop-wrapper.md +36 -0
  199. package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
  200. package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
  201. package/.morph/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
  202. package/.morph/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
  203. package/.morph/framework/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
  204. package/.morph/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
  205. package/.morph/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
  206. package/.morph/framework/templates/project-structure/dotnet-ddd.md +70 -0
  207. package/.morph/framework/templates/saas/subscription.cs +347 -0
  208. package/.morph/framework/templates/saas/tenant.cs +338 -0
  209. package/.morph/framework/templates/state.template.json +17 -0
  210. package/.morph/framework/templates/ui/FluentDesignTheme.cs +149 -0
  211. package/.morph/framework/templates/ui/MudTheme.cs +281 -0
  212. package/.morph/framework/templates/ui/design-system.css +226 -0
  213. package/.morph/logs/tool-failures.log +17 -0
  214. package/.morph/memory/pre-compact-2026-02-24T17-43-30-049Z.json +16 -0
  215. package/.morph/plans/eager-watching-bunny.md +105 -0
  216. package/.morph/plans/temporal-seeking-nebula.md +45 -0
  217. package/.morph/state.json +48 -0
  218. package/CLAUDE.md +1 -1
  219. package/README.md +2 -2
  220. package/bin/morph-spec.js +0 -9
  221. package/framework/CLAUDE.md +1 -1
  222. package/framework/hooks/README.md +10 -6
  223. package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
  224. package/framework/hooks/claude-code/post-tool-use/dispatch.js +1 -1
  225. package/framework/hooks/claude-code/stop/validate-completion.js +1 -1
  226. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +1 -1
  227. package/package.json +1 -1
  228. package/src/commands/project/init.js +15 -42
  229. package/src/commands/project/update.js +22 -37
  230. package/src/lib/installers/mcp-installer.js +18 -3
  231. package/src/utils/hooks-installer.js +5 -15
  232. package/src/commands/project/detect.js +0 -114
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SessionStart Hook: Inject MORPH-SPEC Context
5
+ *
6
+ * Event: SessionStart | Matcher: startup|resume|compact
7
+ *
8
+ * Reads state.json and injects a summary as additionalContext so Claude
9
+ * knows the current morph-spec state at session start.
10
+ *
11
+ * Fail-open: exits 0 on any error.
12
+ */
13
+
14
+ import { loadState, getActiveFeature, getPendingGates, getMissingOutputs } from '../../shared/state-reader.js';
15
+ import { stateExists } from '../../shared/state-reader.js';
16
+ import { injectContext, pass } from '../../shared/hook-response.js';
17
+ import { readFileSync, existsSync } from 'fs';
18
+ import { join } from 'path';
19
+
20
+ const SPEC_MAX_CHARS = 3000;
21
+
22
+ try {
23
+ if (!stateExists()) pass();
24
+
25
+ const state = loadState();
26
+ if (!state?.features || Object.keys(state.features).length === 0) pass();
27
+
28
+ const active = getActiveFeature();
29
+ const lines = ['MORPH-SPEC Status:'];
30
+
31
+ if (active) {
32
+ const { name, feature } = active;
33
+ lines.push(`- Active feature: ${name} (phase: ${feature.phase}, workflow: ${feature.workflow || 'auto'})`);
34
+ lines.push(`- Status: ${feature.status}`);
35
+
36
+ // Task progress
37
+ if (feature.tasks) {
38
+ lines.push(`- Tasks: ${feature.tasks.completed || 0}/${feature.tasks.total || 0} completed`);
39
+ }
40
+
41
+ // Pending approvals
42
+ const pending = getPendingGates(name);
43
+ if (pending.length > 0) {
44
+ lines.push(`- Pending approvals: ${pending.join(', ')}`);
45
+ }
46
+
47
+ // Next required output
48
+ const missing = getMissingOutputs(name);
49
+ if (missing.length > 0) {
50
+ const next = missing[0];
51
+ lines.push(`- Next required output: ${next.type} → ${next.path}`);
52
+ }
53
+
54
+ // Checkpoints
55
+ if (feature.checkpoints?.length > 0) {
56
+ const lastCp = feature.checkpoints[feature.checkpoints.length - 1];
57
+ lines.push(`- Last checkpoint: #${lastCp.checkpointNum} (${lastCp.passed ? 'passed' : 'failed'})`);
58
+ }
59
+
60
+ // Active feature spec (truncated for context budget)
61
+ const specPath = join(process.cwd(), `.morph/features/${name}/1-design/spec.md`);
62
+ if (existsSync(specPath)) {
63
+ try {
64
+ const specContent = readFileSync(specPath, 'utf-8');
65
+ const truncated = specContent.length > SPEC_MAX_CHARS
66
+ ? specContent.slice(0, SPEC_MAX_CHARS) + `\n\n[... spec truncated — full file at .morph/features/${name}/1-design/spec.md]`
67
+ : specContent;
68
+ lines.push('');
69
+ lines.push(`--- Active feature spec (${name}/1-design/spec.md) ---`);
70
+ lines.push(truncated);
71
+ lines.push('--- End spec ---');
72
+ } catch {
73
+ // Non-blocking: skip spec injection on read error
74
+ }
75
+ }
76
+ } else {
77
+ // Show summary of all features
78
+ const featureNames = Object.keys(state.features);
79
+ lines.push(`- Features: ${featureNames.length} (${featureNames.join(', ')})`);
80
+
81
+ for (const [name, feature] of Object.entries(state.features)) {
82
+ lines.push(` - ${name}: phase=${feature.phase}, status=${feature.status}`);
83
+ }
84
+ }
85
+
86
+ // Remind about key commands
87
+ lines.push('');
88
+ lines.push('Key commands: morph-spec status <feature> | morph-spec phase advance <feature> | morph-spec approve <feature> <gate>');
89
+
90
+ injectContext(lines.join('\n'));
91
+ } catch {
92
+ // Fail-open
93
+ process.exit(0);
94
+ }
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env python3
2
+ # framework/hooks/claude-code/statusline.py
3
+ # Claude Code statusline for morph-spec
4
+ # Receives JSON via stdin from Claude Code after each response
5
+
6
+ import sys
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import time
11
+ import re
12
+ import hashlib
13
+ from pathlib import Path
14
+ from datetime import datetime, timezone
15
+
16
+ # Ensure UTF-8 output on Windows (stdout defaults to CP1252 otherwise)
17
+ if hasattr(sys.stdout, 'reconfigure'):
18
+ sys.stdout.reconfigure(encoding='utf-8')
19
+
20
+ # ANSI colors
21
+ R = '\033[0m' # Reset
22
+ BOLD = '\033[1m'
23
+ CYAN = '\033[36m'
24
+ MAGENTA = '\033[35m'
25
+ GREEN = '\033[32m'
26
+ YELLOW = '\033[33m'
27
+ RED = '\033[31m'
28
+ BLUE = '\033[34m'
29
+ GRAY = '\033[90m'
30
+ WHITE = '\033[97m'
31
+
32
+
33
+ # ── MORPH framework constants (derived from phases.json / trust-manager.js) ──
34
+
35
+ # Ordered core phases for pipeline mini-map (5 positions, optional phases mapped)
36
+ # uiux maps to position 2 (same slot as design — they're mutually exclusive in practice)
37
+ PHASE_POSITIONS = {
38
+ 'proposal': 1, 'setup': 1,
39
+ 'uiux': 2, 'design': 2,
40
+ 'clarify': 3,
41
+ 'tasks': 4,
42
+ 'implement': 5, 'sync': 5,
43
+ }
44
+ PHASE_ABBREV = {
45
+ 'proposal': 'prop', 'setup': 'setup',
46
+ 'uiux': 'ui', 'design': 'design',
47
+ 'clarify': 'clarify', 'tasks': 'tasks',
48
+ 'implement': 'impl', 'sync': 'sync',
49
+ }
50
+ PIPELINE_TOTAL = 5
51
+
52
+ # Approval gates per phase (from phases.json pausePoints)
53
+ PHASE_GATES = {
54
+ 'proposal': 'proposal',
55
+ 'uiux': 'uiux',
56
+ 'design': 'design',
57
+ 'tasks': 'tasks',
58
+ }
59
+
60
+ # Trust level thresholds and badges (from trust-manager.js)
61
+ # passRate >= threshold → level
62
+ TRUST_LEVELS = [
63
+ (0.95, 'maximum', GREEN + BOLD, '◆◆◆◆'),
64
+ (0.90, 'high', GREEN, '◆◆◆○'),
65
+ (0.80, 'medium', YELLOW, '◆◆○○'),
66
+ (0.00, 'low', RED, '◆○○○'),
67
+ ]
68
+
69
+ CHECKPOINT_FREQUENCY = 3 # matches llm-interaction.json default
70
+
71
+
72
+ # ── General helpers ──────────────────────────────────────────────────────────
73
+
74
+ def ctx_color(pct):
75
+ """Color based on context usage. 80% = Claude's auto-compact threshold."""
76
+ if pct < 60:
77
+ return GREEN
78
+ if pct < 80:
79
+ return YELLOW
80
+ return RED
81
+
82
+
83
+ def progress_bar(pct, width=8):
84
+ filled = int(pct / 100 * width)
85
+ empty = width - filled
86
+ return f"{'█' * filled}{'░' * empty}"
87
+
88
+
89
+ def format_tokens(n):
90
+ if n >= 1_000_000:
91
+ return f"{n / 1_000_000:.1f}m"
92
+ if n >= 1000:
93
+ return f"{n // 1000}k"
94
+ return str(n)
95
+
96
+
97
+ # ── MORPH feature helpers ────────────────────────────────────────────────────
98
+
99
+ def calculate_trust(checkpoints):
100
+ """Return (level_str, color, badge, pass_rate) from checkpoint array."""
101
+ if not checkpoints:
102
+ return 'low', RED, '○○○○', 0.0
103
+ total = len(checkpoints)
104
+ passed = sum(1 for c in checkpoints if c.get('passed'))
105
+ rate = passed / total
106
+ for threshold, level, color, badge in TRUST_LEVELS:
107
+ if rate >= threshold:
108
+ return level, color, badge, rate
109
+ return 'low', RED, '○○○○', rate
110
+
111
+
112
+ def get_phase_minimap(phase):
113
+ """Return colored dot strip + phase abbrev, e.g. '●►○○○ design'."""
114
+ pos = PHASE_POSITIONS.get(phase)
115
+ if pos is None:
116
+ return None
117
+ dots = ''
118
+ for i in range(1, PIPELINE_TOTAL + 1):
119
+ if i < pos:
120
+ dots += f"{GREEN}●{R}"
121
+ elif i == pos:
122
+ dots += f"{CYAN}►{R}"
123
+ else:
124
+ dots += f"{GRAY}○{R}"
125
+ abbrev = PHASE_ABBREV.get(phase, phase)
126
+ return f"{dots} {CYAN}{abbrev}{R}"
127
+
128
+
129
+ def get_checkpoint_countdown(tasks_done):
130
+ """Tasks remaining until next checkpoint fires (frequency=3)."""
131
+ if tasks_done <= 0:
132
+ return None
133
+ remaining = CHECKPOINT_FREQUENCY - (tasks_done % CHECKPOINT_FREQUENCY)
134
+ return 0 if remaining == CHECKPOINT_FREQUENCY else remaining
135
+
136
+
137
+ def get_next_gate(phase, approval_gates):
138
+ """Return the upcoming gate for this phase if not yet triggered in state."""
139
+ gate_id = PHASE_GATES.get(phase)
140
+ if not gate_id:
141
+ return None
142
+ # If the gate already appears in approvalGates (approved or pending),
143
+ # it's either done or already shown as pending — don't duplicate.
144
+ if gate_id in (approval_gates or {}):
145
+ return None
146
+ return gate_id
147
+
148
+
149
+ def get_all_active_features(cwd):
150
+ """Return list of all in_progress features with enriched MORPH metadata."""
151
+ state_path = Path(cwd) / '.morph' / 'state.json'
152
+ if not state_path.exists():
153
+ return []
154
+ try:
155
+ state = json.loads(state_path.read_text())
156
+ features = state.get('features', {})
157
+ result = []
158
+ for name, feat in features.items():
159
+ if feat.get('status') != 'in_progress':
160
+ continue
161
+ phase = feat.get('phase', '?')
162
+ tasks = feat.get('tasks', {})
163
+ done = tasks.get('completed', 0)
164
+ total = tasks.get('total', 0)
165
+ gates = feat.get('approvalGates', {})
166
+ checkpts = feat.get('checkpoints', [])
167
+
168
+ pending = [g for g, v in gates.items() if not v.get('approved')]
169
+ trust_lvl, trust_color, trust_badge, trust_rate = calculate_trust(checkpts)
170
+ countdown = get_checkpoint_countdown(done)
171
+ next_gate = get_next_gate(phase, gates)
172
+ minimap = get_phase_minimap(phase)
173
+
174
+ result.append({
175
+ 'name': name,
176
+ 'phase': phase,
177
+ 'tasks_done': done,
178
+ 'tasks_total': total,
179
+ 'pending': pending[0] if pending else None,
180
+ 'trust_level': trust_lvl,
181
+ 'trust_color': trust_color,
182
+ 'trust_badge': trust_badge,
183
+ 'trust_rate': trust_rate,
184
+ 'countdown': countdown,
185
+ 'next_gate': next_gate,
186
+ 'minimap': minimap,
187
+ })
188
+ return result
189
+ except Exception:
190
+ return []
191
+
192
+
193
+ # ── Git helpers ───────────────────────────────────────────────────────────────
194
+
195
+ def get_git_info(cwd):
196
+ """Get git branch and diff stats. Uses a 5s file cache to avoid lag."""
197
+ try:
198
+ cache_file = Path(cwd) / '.morph' / '.git-cache'
199
+ try:
200
+ if cache_file.exists():
201
+ age = time.time() - cache_file.stat().st_mtime
202
+ if age < 5:
203
+ return cache_file.read_text().strip()
204
+ except Exception:
205
+ pass
206
+
207
+ branch = subprocess.check_output(
208
+ ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
209
+ cwd=cwd, stderr=subprocess.DEVNULL, timeout=2
210
+ ).decode().strip()
211
+
212
+ # Diff stats: insertions/deletions from staged + unstaged changes
213
+ ins, dels = 0, 0
214
+ for cmd in [['diff', '--shortstat'], ['diff', '--cached', '--shortstat']]:
215
+ try:
216
+ out = subprocess.check_output(
217
+ ['git'] + cmd, cwd=cwd, stderr=subprocess.DEVNULL, timeout=2
218
+ ).decode()
219
+ m = re.search(r'(\d+) insertion', out)
220
+ if m:
221
+ ins += int(m.group(1))
222
+ m = re.search(r'(\d+) deletion', out)
223
+ if m:
224
+ dels += int(m.group(1))
225
+ except Exception:
226
+ pass
227
+
228
+ parts = [f"{BLUE} {branch}{R}"]
229
+ if ins or dels:
230
+ parts.append(f"{GREEN}+{ins}{R}{GRAY},{R}{RED}-{dels}{R}")
231
+
232
+ result = ' '.join(parts)
233
+ try:
234
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
235
+ cache_file.write_text(result)
236
+ except Exception:
237
+ pass
238
+ return result
239
+ except Exception:
240
+ return ""
241
+
242
+
243
+ def get_worktree_info(cwd):
244
+ """Detect if running in a git worktree (not the main worktree)."""
245
+ try:
246
+ out = subprocess.check_output(
247
+ ['git', 'worktree', 'list', '--porcelain'],
248
+ cwd=cwd, stderr=subprocess.DEVNULL, timeout=2
249
+ ).decode()
250
+ entries, current = [], {}
251
+ for line in out.splitlines():
252
+ if line.startswith('worktree '):
253
+ if current:
254
+ entries.append(current)
255
+ current = {'path': line.split(' ', 1)[1]}
256
+ elif line.startswith('branch '):
257
+ current['branch'] = line.split(' ', 1)[1]
258
+ if current:
259
+ entries.append(current)
260
+ if len(entries) > 1:
261
+ cwd_r = str(Path(cwd).resolve())
262
+ for entry in entries[1:]:
263
+ if str(Path(entry.get('path', '')).resolve()) == cwd_r:
264
+ branch = entry.get('branch', '').replace('refs/heads/', '')
265
+ return f"{MAGENTA}worktree:{branch}{R}"
266
+ except Exception:
267
+ pass
268
+ return ""
269
+
270
+
271
+ # ── Transcript / JSONL helpers ────────────────────────────────────────────────
272
+
273
+ def read_transcript_jsonl(path):
274
+ """Read and parse JSONL transcript file. Returns list of parsed entries."""
275
+ entries = []
276
+ try:
277
+ with open(path, 'r', encoding='utf-8') as f:
278
+ for line in f:
279
+ line = line.strip()
280
+ if not line:
281
+ continue
282
+ try:
283
+ entries.append(json.loads(line))
284
+ except Exception:
285
+ pass
286
+ except Exception:
287
+ pass
288
+ return entries
289
+
290
+
291
+ def get_token_metrics(entries):
292
+ """Sum token usage from all non-sidechain JSONL entries."""
293
+ total_input, total_output, total_cached = 0, 0, 0
294
+ for entry in entries:
295
+ if entry.get('isSidechain') or entry.get('isApiErrorMessage'):
296
+ continue
297
+ usage = (entry.get('message') or {}).get('usage') or {}
298
+ if not usage:
299
+ continue
300
+ total_input += usage.get('input_tokens', 0)
301
+ total_output += usage.get('output_tokens', 0)
302
+ total_cached += (
303
+ usage.get('cache_creation_input_tokens', 0) +
304
+ usage.get('cache_read_input_tokens', 0)
305
+ )
306
+ return {'input': total_input, 'output': total_output, 'cached': total_cached}
307
+
308
+
309
+ def parse_timestamp(ts):
310
+ """Parse ISO 8601 timestamp to Unix float. Returns None on error."""
311
+ try:
312
+ return datetime.fromisoformat(ts.replace('Z', '+00:00')).timestamp()
313
+ except Exception:
314
+ return None
315
+
316
+
317
+ def get_session_duration(entries):
318
+ """Elapsed time since first message. Returns 'Xhr Ym' or None."""
319
+ now = time.time()
320
+ for entry in entries:
321
+ ts = entry.get('timestamp')
322
+ if not ts:
323
+ continue
324
+ t = parse_timestamp(ts)
325
+ if t is None:
326
+ continue
327
+ elapsed_s = now - t
328
+ hours = int(elapsed_s // 3600)
329
+ minutes = int((elapsed_s % 3600) // 60)
330
+ if hours == 0:
331
+ return f"{minutes}m"
332
+ elif minutes == 0:
333
+ return f"{hours}hr"
334
+ else:
335
+ return f"{hours}hr {minutes}m"
336
+ return None
337
+
338
+
339
+ def get_session_name(entries):
340
+ """Find the most recent /rename title. Returns string or None."""
341
+ for entry in reversed(entries):
342
+ if entry.get('type') == 'custom-title' and entry.get('customTitle'):
343
+ return entry['customTitle']
344
+ return None
345
+
346
+
347
+ def get_block_start(transcript_path, entries):
348
+ """Find start of current 5-hour billing block. Cached per transcript."""
349
+ h = hashlib.sha256(transcript_path.encode()).hexdigest()[:16]
350
+ cache_dir = Path.home() / '.cache' / 'morph-spec'
351
+ cache_file = cache_dir / f'block-{h}.json'
352
+ now = time.time()
353
+ block_s = 5 * 3600
354
+
355
+ try:
356
+ if cache_file.exists():
357
+ cached = json.loads(cache_file.read_text())
358
+ start = cached.get('block_start')
359
+ if start and (now - start) < block_s:
360
+ return start
361
+ except Exception:
362
+ pass
363
+
364
+ start = None
365
+ for entry in entries:
366
+ ts = entry.get('timestamp')
367
+ if not ts:
368
+ continue
369
+ t = parse_timestamp(ts)
370
+ if t is None:
371
+ continue
372
+ if (now - t) <= block_s:
373
+ start = t
374
+ break
375
+
376
+ if start is None:
377
+ return None
378
+
379
+ try:
380
+ cache_dir.mkdir(parents=True, exist_ok=True)
381
+ cache_file.write_text(json.dumps({'block_start': start}))
382
+ except Exception:
383
+ pass
384
+ return start
385
+
386
+
387
+ def format_block_timer(block_start):
388
+ """Format block timer as 'bar Xhr Ym' relative to 5-hour window."""
389
+ now = time.time()
390
+ elapsed_s = max(0.0, now - block_start)
391
+ pct = min(elapsed_s / (5 * 3600) * 100, 100)
392
+ hours = int(elapsed_s // 3600)
393
+ minutes = int((elapsed_s % 3600) // 60)
394
+ if hours == 0:
395
+ time_str = f"{minutes}m"
396
+ elif minutes == 0:
397
+ time_str = f"{hours}hr"
398
+ else:
399
+ time_str = f"{hours}hr {minutes}m"
400
+ return f"{progress_bar(pct, 6)} {time_str}"
401
+
402
+
403
+ # ── Main ──────────────────────────────────────────────────────────────────────
404
+
405
+ def main():
406
+ try:
407
+ raw = sys.stdin.read()
408
+ if not raw.strip():
409
+ sys.exit(0)
410
+ data = json.loads(raw)
411
+ except Exception:
412
+ sys.exit(0)
413
+
414
+ cwd = data.get('cwd', os.getcwd())
415
+ transcript_path = data.get('transcript_path')
416
+
417
+ # Read JSONL transcript once — shared by session clock, block timer,
418
+ # token metrics, and session name.
419
+ entries = read_transcript_jsonl(transcript_path) if transcript_path else []
420
+
421
+ # ── MORPH feature lines (one line per active feature) ────────────────────
422
+ features = get_all_active_features(cwd)
423
+ for feat in features:
424
+ parts = [f"{CYAN}{BOLD}{feat['name']}{R}"]
425
+
426
+ # Phase pipeline mini-map: ●●►○○ design
427
+ if feat['minimap']:
428
+ parts.append(feat['minimap'])
429
+
430
+ # Task progress bar
431
+ if feat['tasks_total'] > 0:
432
+ pct = feat['tasks_done'] / feat['tasks_total'] * 100
433
+ bar = progress_bar(pct, 6)
434
+ parts.append(f"{GREEN}{bar} {feat['tasks_done']}/{feat['tasks_total']}{R}")
435
+
436
+ # Checkpoint countdown: how many tasks until next validation fires
437
+ if feat['countdown'] is not None:
438
+ if feat['countdown'] == 0:
439
+ parts.append(f"{GREEN}ckpt!{R}") # just hit checkpoint
440
+ elif feat['countdown'] == 1:
441
+ parts.append(f"{YELLOW}ckpt:1{R}") # 1 task away — heads up
442
+ else:
443
+ parts.append(f"{GRAY}ckpt:{feat['countdown']}{R}")
444
+
445
+ # Trust level badge: ◆◆◆○ high
446
+ tc = feat['trust_color']
447
+ parts.append(f"{tc}{feat['trust_badge']}{R}")
448
+
449
+ # Pending approval gate (blocking — already triggered, not yet approved)
450
+ if feat['pending']:
451
+ parts.append(f"{YELLOW}⏳ {feat['pending']} pending{R}")
452
+
453
+ # Upcoming gate (not yet triggered — reminds what comes at end of phase)
454
+ if feat['next_gate']:
455
+ parts.append(f"{GRAY}→gate:{feat['next_gate']}{R}")
456
+
457
+ print(' | '.join(parts))
458
+
459
+ # ── Session info line (always shown) ─────────────────────────────────────
460
+ parts2 = []
461
+
462
+ # Session name (set via /rename)
463
+ if entries:
464
+ session_name = get_session_name(entries)
465
+ if session_name:
466
+ parts2.append(f"{CYAN}{BOLD}📌 {session_name}{R}")
467
+
468
+ # Model
469
+ model = data.get('model', {})
470
+ model_name = model.get('display_name', model.get('id', ''))
471
+ if model_name:
472
+ short = model_name.replace('Claude ', '').replace(' (claude.ai)', '')
473
+ parts2.append(f"{WHITE}{BOLD}🤖 {short}{R}")
474
+
475
+ # Session clock (elapsed time since first message)
476
+ if entries:
477
+ duration = get_session_duration(entries)
478
+ if duration:
479
+ parts2.append(f"{YELLOW}⏱ {duration}{R}")
480
+
481
+ # Block timer (progress through current 5-hour billing window)
482
+ if entries and transcript_path:
483
+ block_start = get_block_start(transcript_path, entries)
484
+ if block_start is not None:
485
+ parts2.append(f"{YELLOW}blk:{format_block_timer(block_start)}{R}")
486
+
487
+ # Context window (60% = yellow, 80% = red/auto-compact threshold)
488
+ ctx = data.get('context_window', {})
489
+ if ctx:
490
+ used_pct = ctx.get('used_percentage', 0)
491
+ cur = ctx.get('current_usage', 0)
492
+ total_ctx = ctx.get('context_window_size', 0)
493
+ color = ctx_color(used_pct)
494
+ bar = progress_bar(used_pct, 8)
495
+ toks = f"{format_tokens(cur)}/{format_tokens(total_ctx)}"
496
+ suffix = f" {RED}~cmpct{R}" if used_pct >= 80 else ""
497
+ parts2.append(f"{color}{bar} {used_pct:.0f}%{R} ({toks}){suffix}")
498
+
499
+ # Token breakdown from JSONL (session totals: input / output / cached)
500
+ if entries:
501
+ tok = get_token_metrics(entries)
502
+ tok_parts = []
503
+ if tok['input']:
504
+ tok_parts.append(f"in:{format_tokens(tok['input'])}")
505
+ if tok['output']:
506
+ tok_parts.append(f"out:{format_tokens(tok['output'])}")
507
+ if tok['cached']:
508
+ tok_parts.append(f"↩{format_tokens(tok['cached'])}")
509
+ if tok_parts:
510
+ parts2.append(f"{GRAY}{' '.join(tok_parts)}{R}")
511
+
512
+ # Cost
513
+ cost = data.get('cost', {})
514
+ if cost.get('total_cost_usd'):
515
+ usd = cost['total_cost_usd']
516
+ parts2.append(f"{GRAY}${usd:.3f}{R}")
517
+
518
+ # Agent name (if running in agent mode)
519
+ agent = data.get('agent', {})
520
+ if agent.get('name'):
521
+ parts2.append(f"{BLUE}agent:{agent['name']}{R}")
522
+
523
+ # Git info (branch + diff stats, 5s cached)
524
+ git = get_git_info(cwd)
525
+ if git:
526
+ parts2.append(git)
527
+
528
+ # Worktree info
529
+ wt = get_worktree_info(cwd)
530
+ if wt:
531
+ parts2.append(wt)
532
+
533
+ if parts2:
534
+ print(' | '.join(parts2))
535
+
536
+
537
+ if __name__ == '__main__':
538
+ main()
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # morph-spec statusline — installed globally to ~/.claude/statusline.sh
3
+ # Claude Code invokes this with JSON via stdin after each response.
4
+ # Requires: Python 3 available as `python3` on PATH.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ python3 "${SCRIPT_DIR}/statusline.py" 2>/dev/null || true