@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.
- package/.morph/.morphversion +5 -0
- package/.morph/analytics/threads-log.jsonl +5 -0
- package/.morph/config/config.json +8 -0
- package/.morph/framework/agents.json +1815 -0
- package/.morph/framework/hooks/README.md +205 -0
- package/.morph/framework/hooks/claude-code/notification/approval-reminder.js +54 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/dispatch.js +83 -0
- package/.morph/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +42 -0
- package/.morph/framework/hooks/claude-code/pre-compact/save-morph-context.js +61 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +71 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +58 -0
- package/.morph/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +64 -0
- package/.morph/framework/hooks/claude-code/session-start/inject-morph-context.js +94 -0
- package/.morph/framework/hooks/claude-code/statusline.py +538 -0
- package/.morph/framework/hooks/claude-code/statusline.sh +7 -0
- package/.morph/framework/hooks/claude-code/stop/validate-completion.js +88 -0
- package/.morph/framework/hooks/claude-code/user-prompt/enrich-prompt.js +91 -0
- package/.morph/framework/hooks/git/commit-msg/conventional-commits.sh +33 -0
- package/.morph/framework/hooks/git/pre-commit/agents.sh +25 -0
- package/.morph/framework/hooks/git/pre-commit/orchestrator.sh +64 -0
- package/.morph/framework/hooks/git/pre-commit/specs.sh +50 -0
- package/.morph/framework/hooks/git/pre-push/run-tests.sh +44 -0
- package/.morph/framework/hooks/shared/hook-response.js +45 -0
- package/.morph/framework/hooks/shared/phase-utils.js +129 -0
- package/.morph/framework/hooks/shared/state-reader.js +138 -0
- package/.morph/framework/hooks/shared/stdin-reader.js +26 -0
- package/.morph/framework/standards/STANDARDS.json +933 -0
- package/.morph/framework/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/framework/standards/ai-agents/production.md +415 -0
- package/.morph/framework/standards/ai-agents/setup.md +418 -0
- package/.morph/framework/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/framework/standards/ai-agents/workflows.md +354 -0
- package/.morph/framework/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/framework/standards/architecture/ddd/bounded-contexts.md +105 -0
- package/.morph/framework/standards/architecture/ddd/complexity-levels.md +108 -0
- package/.morph/framework/standards/architecture/ddd/entities.md +99 -0
- package/.morph/framework/standards/architecture/ddd/ubiquitous-language.md +58 -0
- package/.morph/framework/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/framework/standards/backend/api/minimal-api.md +494 -0
- package/.morph/framework/standards/backend/api/rest.md +492 -0
- package/.morph/framework/standards/backend/api/validation.md +88 -0
- package/.morph/framework/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/framework/standards/backend/database/ef-core.md +199 -0
- package/.morph/framework/standards/backend/database/migrations.md +393 -0
- package/.morph/framework/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/framework/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/framework/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/framework/standards/backend/dotnet/async.md +366 -0
- package/.morph/framework/standards/backend/dotnet/core.md +117 -0
- package/.morph/framework/standards/backend/dotnet/di.md +439 -0
- package/.morph/framework/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/framework/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/framework/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/framework/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/framework/standards/context/analytics.md +96 -0
- package/.morph/framework/standards/context/bundles.md +110 -0
- package/.morph/framework/standards/context/priming.md +78 -0
- package/.morph/framework/standards/core/architecture.md +185 -0
- package/.morph/framework/standards/core/coding.md +214 -0
- package/.morph/framework/standards/core/git-branching-strategy.md +403 -0
- package/.morph/framework/standards/core/git.md +185 -0
- package/.morph/framework/standards/core/testing.md +295 -0
- package/.morph/framework/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/framework/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/framework/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/framework/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/framework/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/framework/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/framework/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/framework/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/framework/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/framework/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/framework/standards/frontend/blazor/state.md +191 -0
- package/.morph/framework/standards/frontend/design-system/animations.md +151 -0
- package/.morph/framework/standards/frontend/design-system/naming.md +64 -0
- package/.morph/framework/standards/frontend/nextjs/app-router.md +123 -0
- package/.morph/framework/standards/frontend/nextjs/components.md +132 -0
- package/.morph/framework/standards/frontend/nextjs/data-fetching.md +126 -0
- package/.morph/framework/standards/frontend/nextjs/forms.md +128 -0
- package/.morph/framework/standards/frontend/nextjs/naming-conventions.md +67 -0
- package/.morph/framework/standards/frontend/nextjs/nextjs-patterns.md +215 -0
- package/.morph/framework/standards/frontend/nextjs/project-structure.md +102 -0
- package/.morph/framework/standards/frontend/nextjs/state-management.md +72 -0
- package/.morph/framework/standards/frontend/nextjs/testing.md +111 -0
- package/.morph/framework/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/framework/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/framework/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/framework/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/framework/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/framework/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/framework/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/framework/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/framework/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/framework/standards/integration/api/graphql.md +91 -0
- package/.morph/framework/standards/integration/api/grpc.md +114 -0
- package/.morph/framework/standards/integration/api/rest-design.md +95 -0
- package/.morph/framework/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/framework/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/framework/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/framework/standards/integration/mcp/mcp-tools.md +384 -0
- package/.morph/framework/standards/observability/logging.md +131 -0
- package/.morph/framework/standards/observability/metrics.md +121 -0
- package/.morph/framework/standards/observability/monitoring.md +114 -0
- package/.morph/framework/standards/observability/tracing.md +132 -0
- package/.morph/framework/standards/workflows/parallel-execution.md +112 -0
- package/.morph/framework/standards/workflows/thread-management.md +113 -0
- package/.morph/framework/templates/.idea/morph-templates.xml +92 -0
- package/.morph/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/framework/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/framework/templates/README.md +814 -0
- package/.morph/framework/templates/REGISTRY.json +1888 -0
- package/.morph/framework/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/framework/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/framework/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/framework/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level1.cs +69 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level2.cs +86 -0
- package/.morph/framework/templates/code/dotnet/contracts/contracts-level3.cs +41 -0
- package/.morph/framework/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/framework/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/framework/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/framework/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/framework/templates/code/dotnet/test.cs +239 -0
- package/.morph/framework/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/framework/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/framework/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/framework/templates/code/typescript/contracts.ts +168 -0
- package/.morph/framework/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/framework/templates/context/CONTEXT.md +181 -0
- package/.morph/framework/templates/docs/clarifications.md +253 -0
- package/.morph/framework/templates/docs/onboarding.md +123 -0
- package/.morph/framework/templates/docs/proposal.md +182 -0
- package/.morph/framework/templates/docs/schema-analysis.md +119 -0
- package/.morph/framework/templates/docs/spec.md +198 -0
- package/.morph/framework/templates/docs/ui-components.md +124 -0
- package/.morph/framework/templates/docs/ui-design-system.md +76 -0
- package/.morph/framework/templates/docs/ui-flows.md +167 -0
- package/.morph/framework/templates/docs/ui-mockups.md +98 -0
- package/.morph/framework/templates/docs/user-stories.md +34 -0
- package/.morph/framework/templates/examples/design-system-examples.md +357 -0
- package/.morph/framework/templates/examples/spec-examples.md +90 -0
- package/.morph/framework/templates/feature/decisions.md +187 -0
- package/.morph/framework/templates/feature/recap.md +146 -0
- package/.morph/framework/templates/feature/tasks.md +199 -0
- package/.morph/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -0
- package/.morph/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/env.mjs.hbs +32 -0
- package/.morph/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -0
- package/.morph/framework/templates/frontend/nextjs/page.tsx.hbs +22 -0
- package/.morph/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -0
- package/.morph/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -0
- package/.morph/framework/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/framework/templates/infrastructure/azure/README.md +286 -0
- package/.morph/framework/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/framework/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/framework/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/framework/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/framework/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/framework/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/framework/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/framework/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/framework/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/framework/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/framework/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/framework/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/framework/templates/infrastructure/github/README.md +593 -0
- package/.morph/framework/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/framework/templates/integrations/asaas-client.cs +387 -0
- package/.morph/framework/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/framework/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/framework/templates/integrations/clerk-config.cs +258 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/framework/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/framework/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/framework/templates/project-structure/dotnet-ddd.md +70 -0
- package/.morph/framework/templates/saas/subscription.cs +347 -0
- package/.morph/framework/templates/saas/tenant.cs +338 -0
- package/.morph/framework/templates/state.template.json +17 -0
- package/.morph/framework/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/framework/templates/ui/MudTheme.cs +281 -0
- package/.morph/framework/templates/ui/design-system.css +226 -0
- package/.morph/logs/tool-failures.log +17 -0
- package/.morph/memory/pre-compact-2026-02-24T17-43-30-049Z.json +16 -0
- package/.morph/plans/eager-watching-bunny.md +105 -0
- package/.morph/plans/temporal-seeking-nebula.md +45 -0
- package/.morph/state.json +48 -0
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +0 -9
- package/framework/CLAUDE.md +1 -1
- package/framework/hooks/README.md +10 -6
- package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +1 -1
- package/framework/hooks/claude-code/stop/validate-completion.js +1 -1
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/init.js +15 -42
- package/src/commands/project/update.js +22 -37
- package/src/lib/installers/mcp-installer.js +18 -3
- package/src/utils/hooks-installer.js +5 -15
- 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
|