@geraldmaron/construct 1.0.0
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/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/prompt-composer.mjs — Assemble the final agent prompt from core file, task packet, and context digest.
|
|
3
|
+
*
|
|
4
|
+
* Called by the orchestrator and MCP dispatch layer before invoking a specialist agent.
|
|
5
|
+
* Inlines role anti-pattern guidance from skills/roles/ when a role directive is present in the prompt.
|
|
6
|
+
* Produces a deterministic, cache-friendly prompt string with a stable token footprint.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { buildContextDigest, readContextState } from './context-state.mjs';
|
|
13
|
+
import { resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from './model-router.mjs';
|
|
14
|
+
import { routeRequest } from './orchestration-policy.mjs';
|
|
15
|
+
import { resolvePromptEntry, resolvePromptMetadata } from './prompt-metadata.mjs';
|
|
16
|
+
import { loadWorkflow } from './workflow-state.mjs';
|
|
17
|
+
|
|
18
|
+
function compactText(text, limit = 1200) {
|
|
19
|
+
if (!text) return '';
|
|
20
|
+
const normalized = String(text).trim();
|
|
21
|
+
return normalized.length <= limit ? normalized : `${normalized.slice(0, limit)}…`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readPromptBody(promptFile, rootDir) {
|
|
25
|
+
const filePath = path.join(rootDir, promptFile);
|
|
26
|
+
if (!fs.existsSync(filePath)) return '';
|
|
27
|
+
return fs.readFileSync(filePath, 'utf8').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBasePrompt(entryOrName, {
|
|
31
|
+
rootDir = process.cwd(),
|
|
32
|
+
registry,
|
|
33
|
+
fallback = '',
|
|
34
|
+
} = {}) {
|
|
35
|
+
return resolvePromptContract(entryOrName, { rootDir, registry, fallback }).prompt;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolvePromptContract(entryOrName, {
|
|
39
|
+
rootDir = process.cwd(),
|
|
40
|
+
registry,
|
|
41
|
+
fallback = '',
|
|
42
|
+
} = {}) {
|
|
43
|
+
const directEntry = entryOrName && typeof entryOrName === 'object' ? entryOrName : null;
|
|
44
|
+
if (directEntry?.prompt) {
|
|
45
|
+
return {
|
|
46
|
+
prompt: String(directEntry.prompt).trim(),
|
|
47
|
+
metadata: resolvePromptMetadata(directEntry.name || entryOrName, { rootDir, registry }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const agentName = directEntry?.name || entryOrName;
|
|
52
|
+
if (!agentName) return { prompt: fallback, metadata: {} };
|
|
53
|
+
|
|
54
|
+
const composed = composePrompt(agentName, { rootDir, registry });
|
|
55
|
+
return {
|
|
56
|
+
prompt: composed.prompt || fallback,
|
|
57
|
+
metadata: composed.metadata || {},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveActiveTask(workflow) {
|
|
62
|
+
if (!workflow || !Array.isArray(workflow.tasks)) return null;
|
|
63
|
+
if (workflow.currentTaskKey) {
|
|
64
|
+
const currentTask = workflow.tasks.find((task) => task.key === workflow.currentTaskKey && !['done', 'skipped'].includes(task.status));
|
|
65
|
+
if (currentTask) return currentTask;
|
|
66
|
+
}
|
|
67
|
+
return workflow.tasks.find((task) => ['in-progress', 'in_progress', 'blocked_needs_user'].includes(task.status)) || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function composePrompt(agentName, {
|
|
71
|
+
rootDir = process.cwd(),
|
|
72
|
+
registry,
|
|
73
|
+
task = null,
|
|
74
|
+
contextState = null,
|
|
75
|
+
hostConstraints = null,
|
|
76
|
+
intent = null,
|
|
77
|
+
workCategory = null,
|
|
78
|
+
} = {}) {
|
|
79
|
+
const entry = resolvePromptEntry(agentName, { rootDir, registry });
|
|
80
|
+
if (!entry?.promptFile) return { metadata: {}, fragments: [], prompt: '' };
|
|
81
|
+
|
|
82
|
+
const metadata = resolvePromptMetadata(agentName, { rootDir, registry });
|
|
83
|
+
const fragments = [];
|
|
84
|
+
|
|
85
|
+
fragments.push({ type: 'core', label: entry.name, content: readPromptBody(entry.promptFile, rootDir) });
|
|
86
|
+
|
|
87
|
+
if (intent || workCategory) {
|
|
88
|
+
fragments.push({
|
|
89
|
+
type: 'task-context',
|
|
90
|
+
label: 'task-classification',
|
|
91
|
+
content: compactText(`Intent: ${intent || 'unknown'}\nWork category: ${workCategory || 'unknown'}`, 200),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (task) {
|
|
96
|
+
const taskBlock = [
|
|
97
|
+
task.title ? `Task: ${task.title}` : null,
|
|
98
|
+
task.owner ? `Owner: ${task.owner}` : null,
|
|
99
|
+
Array.isArray(task.acceptanceCriteria) && task.acceptanceCriteria.length ? `Acceptance: ${task.acceptanceCriteria.join('; ')}` : null,
|
|
100
|
+
Array.isArray(task.readFirst) && task.readFirst.length ? `Read first: ${task.readFirst.join(', ')}` : null,
|
|
101
|
+
Array.isArray(task.doNotChange) && task.doNotChange.length ? `Do not change: ${task.doNotChange.join(', ')}` : null,
|
|
102
|
+
].filter(Boolean).join('\n');
|
|
103
|
+
if (taskBlock) fragments.push({ type: 'task-packet', label: 'workflow-task', content: compactText(taskBlock, 500) });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const digest = buildContextDigest(contextState);
|
|
107
|
+
if (digest) {
|
|
108
|
+
fragments.push({ type: 'context-digest', label: 'context', content: compactText(JSON.stringify(digest), 600) });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (hostConstraints) {
|
|
112
|
+
fragments.push({ type: 'host-constraints', label: 'host', content: compactText(JSON.stringify(hostConstraints), 300) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const prompt = fragments.filter((fragment) => fragment.content).map((fragment) => fragment.content).join('\n\n');
|
|
116
|
+
return { metadata, fragments, prompt };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function summarizePromptComposition(agentName, options = {}) {
|
|
120
|
+
const route = options.route || (options.request ? routeRequest({ request: options.request }) : null);
|
|
121
|
+
const executionContractModel = options.executionContractModel
|
|
122
|
+
|| resolveExecutionContractModelMetadata({
|
|
123
|
+
envValues: options.envValues || {},
|
|
124
|
+
registryModels: options.registryModels || {},
|
|
125
|
+
requestedTier: options.requestedTier || selectModelTierForWorkCategory(route?.workCategory),
|
|
126
|
+
workCategory: route?.workCategory || null,
|
|
127
|
+
});
|
|
128
|
+
const composed = composePrompt(agentName, {
|
|
129
|
+
...options,
|
|
130
|
+
intent: options.intent || route?.intent || null,
|
|
131
|
+
workCategory: options.workCategory || route?.workCategory || null,
|
|
132
|
+
});
|
|
133
|
+
const fragmentTypes = composed.fragments.map((fragment) => fragment.type);
|
|
134
|
+
const composedPromptHash = composed.prompt
|
|
135
|
+
? crypto.createHash('sha256').update(composed.prompt).digest('hex')
|
|
136
|
+
: null;
|
|
137
|
+
const task = options.task || null;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...composed.metadata,
|
|
141
|
+
...(composedPromptHash ? {
|
|
142
|
+
composedPromptHash,
|
|
143
|
+
composedPromptVersion: composedPromptHash.slice(0, 12),
|
|
144
|
+
} : {}),
|
|
145
|
+
promptFragmentCount: composed.fragments.length,
|
|
146
|
+
promptFragmentTypes: fragmentTypes,
|
|
147
|
+
promptHasTaskPacket: fragmentTypes.includes('task-packet'),
|
|
148
|
+
promptHasContextDigest: fragmentTypes.includes('context-digest'),
|
|
149
|
+
promptHasHostConstraints: fragmentTypes.includes('host-constraints'),
|
|
150
|
+
...(task?.key ? { workflowTaskKey: task.key } : {}),
|
|
151
|
+
...(task?.owner ? { workflowTaskOwner: task.owner } : {}),
|
|
152
|
+
...(task?.phase ? { workflowTaskPhase: task.phase } : {}),
|
|
153
|
+
...(route ? {
|
|
154
|
+
routeIntent: route.intent,
|
|
155
|
+
routeTrack: route.track,
|
|
156
|
+
routeWorkCategory: route.workCategory,
|
|
157
|
+
routeSpecialists: route.specialists,
|
|
158
|
+
routeDispatchPlan: route.dispatchPlan,
|
|
159
|
+
} : {}),
|
|
160
|
+
executionContractModel,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function resolveRuntimePromptMetadata(agentName, {
|
|
165
|
+
rootDir = process.cwd(),
|
|
166
|
+
registry,
|
|
167
|
+
workflow = null,
|
|
168
|
+
task = null,
|
|
169
|
+
contextState = null,
|
|
170
|
+
request = null,
|
|
171
|
+
route = null,
|
|
172
|
+
registryModels = {},
|
|
173
|
+
envValues = {},
|
|
174
|
+
executionContractModel = null,
|
|
175
|
+
hostConstraints = null,
|
|
176
|
+
} = {}) {
|
|
177
|
+
const resolvedWorkflow = workflow ?? loadWorkflow(rootDir);
|
|
178
|
+
const resolvedTask = task ?? resolveActiveTask(resolvedWorkflow);
|
|
179
|
+
const resolvedContextState = contextState ?? readContextState(rootDir);
|
|
180
|
+
const resolvedRoute = route || (request ? routeRequest({ request }) : null);
|
|
181
|
+
|
|
182
|
+
return summarizePromptComposition(agentName, {
|
|
183
|
+
rootDir,
|
|
184
|
+
registry,
|
|
185
|
+
task: resolvedTask,
|
|
186
|
+
contextState: resolvedContextState,
|
|
187
|
+
request,
|
|
188
|
+
route: resolvedRoute,
|
|
189
|
+
registryModels,
|
|
190
|
+
envValues,
|
|
191
|
+
executionContractModel,
|
|
192
|
+
hostConstraints,
|
|
193
|
+
intent: resolvedRoute?.intent || null,
|
|
194
|
+
workCategory: resolvedRoute?.workCategory || null,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/prompt-metadata.mjs — prompt identity helpers for telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Resolves an agent or persona name to its git-owned prompt file and returns a
|
|
5
|
+
* stable hash/version fingerprint. Telemetry should send these identifiers to
|
|
6
|
+
* Langfuse instead of embedding full production prompt text in every trace.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
function readJson(file) {
|
|
13
|
+
if (!fs.existsSync(file)) return null;
|
|
14
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeName(name) {
|
|
18
|
+
return String(name ?? '')
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/^cx-/, '')
|
|
21
|
+
.replace(/^opencode\./, '')
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findEntry(registry, agentName) {
|
|
26
|
+
const normalized = normalizeName(agentName);
|
|
27
|
+
const entries = [
|
|
28
|
+
...(registry.personas ?? []),
|
|
29
|
+
...(registry.agents ?? []),
|
|
30
|
+
];
|
|
31
|
+
return entries.find((entry) => {
|
|
32
|
+
const entryName = normalizeName(entry.name);
|
|
33
|
+
return entryName === normalized || `cx-${entryName}` === String(agentName ?? '').trim().toLowerCase();
|
|
34
|
+
}) ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolvePromptEntry(agentName, { rootDir = process.cwd(), registry } = {}) {
|
|
38
|
+
if (!agentName) return null;
|
|
39
|
+
const loadedRegistry = registry ?? readJson(path.join(rootDir, 'agents', 'registry.json'));
|
|
40
|
+
if (!loadedRegistry) return null;
|
|
41
|
+
return findEntry(loadedRegistry, agentName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolvePromptMetadata(agentName, { rootDir = process.cwd(), registry } = {}) {
|
|
45
|
+
if (!agentName) return {};
|
|
46
|
+
const entry = resolvePromptEntry(agentName, { rootDir, registry });
|
|
47
|
+
if (!entry?.promptFile) return {};
|
|
48
|
+
|
|
49
|
+
const promptPath = path.join(rootDir, entry.promptFile);
|
|
50
|
+
if (!fs.existsSync(promptPath)) return {};
|
|
51
|
+
|
|
52
|
+
const content = fs.readFileSync(promptPath, 'utf8');
|
|
53
|
+
const promptHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
54
|
+
return {
|
|
55
|
+
promptName: entry.name,
|
|
56
|
+
promptFile: entry.promptFile,
|
|
57
|
+
promptHash,
|
|
58
|
+
promptVersion: promptHash.slice(0, 12),
|
|
59
|
+
promptSource: 'git',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function enrichMetadataWithPrompt(agentName, metadata = {}, options = {}) {
|
|
64
|
+
return {
|
|
65
|
+
...resolvePromptMetadata(agentName, options),
|
|
66
|
+
...metadata,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/lib/review.mjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/review.mjs — Langfuse performance data pipeline
|
|
4
|
+
*
|
|
5
|
+
* Fetches traces and quality scores from Langfuse, aggregates per-agent
|
|
6
|
+
* metrics, and writes two files:
|
|
7
|
+
*
|
|
8
|
+
* {outDir}/{date}-raw.json — raw aggregated metrics for cx-trace-reviewer
|
|
9
|
+
* {outDir}/{date}.md — standalone markdown report (no AI needed)
|
|
10
|
+
*
|
|
11
|
+
* Usage (via construct review):
|
|
12
|
+
* node lib/review.mjs [--days=N] [--agent=cx-NAME] [--out=PATH] [--json-only]
|
|
13
|
+
*
|
|
14
|
+
* Requires env: LANGFUSE_BASEURL (default: https://cloud.langfuse.com), LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
|
|
21
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const args = Object.fromEntries(
|
|
24
|
+
process.argv.slice(2)
|
|
25
|
+
.filter((a) => a.startsWith("--"))
|
|
26
|
+
.map((a) => {
|
|
27
|
+
const [k, v] = a.slice(2).split("=");
|
|
28
|
+
return [k, v ?? true];
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const days = parseInt(args.days ?? "30", 10);
|
|
33
|
+
const agentFilter = args.agent ?? null;
|
|
34
|
+
const jsonOnly = args["json-only"] === true || args["json-only"] === "true";
|
|
35
|
+
const outDir = (args.out && typeof args.out === "string")
|
|
36
|
+
? path.resolve(args.out)
|
|
37
|
+
: path.join(os.homedir(), ".cx", "performance-reviews");
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const LANGFUSE_BASEURL = (process.env.LANGFUSE_BASEURL ?? "https://cloud.langfuse.com").replace(/\/$/, "");
|
|
42
|
+
|
|
43
|
+
function langfuseHeaders() {
|
|
44
|
+
const key = process.env.LANGFUSE_PUBLIC_KEY;
|
|
45
|
+
const secret = process.env.LANGFUSE_SECRET_KEY;
|
|
46
|
+
if (!key || !secret) throw new Error("LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set.");
|
|
47
|
+
return {
|
|
48
|
+
Accept: "application/json",
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString("base64")}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readSessionEfficiency() {
|
|
55
|
+
const efficiencyPath = path.join(os.homedir(), ".cx", "session-efficiency.json");
|
|
56
|
+
try {
|
|
57
|
+
const stats = JSON.parse(fs.readFileSync(efficiencyPath, "utf8"));
|
|
58
|
+
const readCount = Number(stats.readCount || 0);
|
|
59
|
+
const uniqueFileCount = Number(stats.uniqueFileCount || 0);
|
|
60
|
+
const repeatedReadCount = Number(stats.repeatedReadCount || 0);
|
|
61
|
+
const largeReadCount = Number(stats.largeReadCount || 0);
|
|
62
|
+
const totalBytesRead = Number(stats.totalBytesRead || 0);
|
|
63
|
+
|
|
64
|
+
let score = 1;
|
|
65
|
+
if (readCount > 0) {
|
|
66
|
+
score -= Math.min(0.35, repeatedReadCount * 0.04);
|
|
67
|
+
score -= Math.min(0.25, largeReadCount * 0.05);
|
|
68
|
+
if (totalBytesRead > 500_000) score -= 0.1;
|
|
69
|
+
if (uniqueFileCount > 25) score -= 0.05;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
score: Math.max(0, Math.min(1, Math.round(score * 100) / 100)),
|
|
74
|
+
readCount,
|
|
75
|
+
uniqueFileCount,
|
|
76
|
+
repeatedReadCount,
|
|
77
|
+
largeReadCount,
|
|
78
|
+
totalBytesRead,
|
|
79
|
+
lastUpdatedAt: stats.lastUpdatedAt || null,
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Langfuse API ────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function langfuseGet(path, params = {}) {
|
|
89
|
+
const url = new URL(`${LANGFUSE_BASEURL}${path}`);
|
|
90
|
+
for (const [k, v] of Object.entries(params)) {
|
|
91
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
92
|
+
}
|
|
93
|
+
const res = await fetch(url.toString(), { headers: langfuseHeaders() });
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const body = await res.text().catch(() => "");
|
|
96
|
+
throw new Error(`Langfuse ${path} returned ${res.status}: ${body}`);
|
|
97
|
+
}
|
|
98
|
+
return res.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Data Fetching ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async function fetchData(fromDate, toDate) {
|
|
104
|
+
const from = fromDate.toISOString();
|
|
105
|
+
console.error(`Fetching traces from ${from}...`);
|
|
106
|
+
|
|
107
|
+
const traceParams = { fromTimestamp: from, limit: 100 };
|
|
108
|
+
if (agentFilter) traceParams.name = agentFilter;
|
|
109
|
+
|
|
110
|
+
const tracesJson = await langfuseGet("/api/public/traces", traceParams);
|
|
111
|
+
const rawTraces = tracesJson.data ?? [];
|
|
112
|
+
|
|
113
|
+
// Fetch quality scores for all traces
|
|
114
|
+
const scoreMap = {};
|
|
115
|
+
for (const t of rawTraces) {
|
|
116
|
+
const scoresJson = await langfuseGet("/api/public/scores", { traceId: t.id, name: "quality", limit: 10 }).catch(() => ({ data: [] }));
|
|
117
|
+
for (const s of scoresJson.data ?? []) {
|
|
118
|
+
scoreMap[t.id] = s;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Normalize to shape expected by aggregate()
|
|
123
|
+
const traces = rawTraces.map(t => ({
|
|
124
|
+
id: t.id,
|
|
125
|
+
name: t.metadata?.agentName ?? t.name ?? "unknown",
|
|
126
|
+
timestamp: t.timestamp,
|
|
127
|
+
latency: t.latency != null ? Math.round(t.latency * 1000) : undefined, // Langfuse returns seconds; convert to ms
|
|
128
|
+
input: t.input,
|
|
129
|
+
output: t.output,
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const scores = rawTraces.flatMap(t => {
|
|
133
|
+
const s = scoreMap[t.id];
|
|
134
|
+
if (!s || s.value == null) return [];
|
|
135
|
+
return [{
|
|
136
|
+
traceId: t.id,
|
|
137
|
+
name: s.name ?? "quality",
|
|
138
|
+
value: s.value,
|
|
139
|
+
comment: s.comment ?? "",
|
|
140
|
+
}];
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
console.error(` Fetched ${traces.length} traces, ${scores.length} quality scores`);
|
|
144
|
+
return { traces, scores };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Aggregation ─────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function aggregate(traces, scores, fromDate, toDate) {
|
|
150
|
+
// Index scores by traceId
|
|
151
|
+
const scoresByTrace = new Map();
|
|
152
|
+
for (const score of scores) {
|
|
153
|
+
if (!score.traceId) continue;
|
|
154
|
+
if (!scoresByTrace.has(score.traceId)) scoresByTrace.set(score.traceId, []);
|
|
155
|
+
scoresByTrace.get(score.traceId).push(score);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Group traces by agent name
|
|
159
|
+
const byAgent = new Map();
|
|
160
|
+
for (const trace of traces) {
|
|
161
|
+
const name = trace.name ?? "unknown";
|
|
162
|
+
if (!byAgent.has(name)) byAgent.set(name, []);
|
|
163
|
+
byAgent.get(name).push(trace);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const agentStats = [];
|
|
167
|
+
for (const [agentName, agentTraces] of byAgent.entries()) {
|
|
168
|
+
const qualityScores = [];
|
|
169
|
+
const latencies = [];
|
|
170
|
+
const lowScoreTraces = [];
|
|
171
|
+
|
|
172
|
+
for (const trace of agentTraces) {
|
|
173
|
+
// Latency
|
|
174
|
+
if (trace.latency) latencies.push(trace.latency);
|
|
175
|
+
|
|
176
|
+
// Quality scores (name='quality' convention from sharedGuidance)
|
|
177
|
+
const traceScores = (scoresByTrace.get(trace.id) ?? [])
|
|
178
|
+
.filter((s) => s.name === "quality" || s.name === "Quality");
|
|
179
|
+
|
|
180
|
+
if (traceScores.length > 0) {
|
|
181
|
+
const avg = traceScores.reduce((s, x) => s + x.value, 0) / traceScores.length;
|
|
182
|
+
qualityScores.push({ value: avg, traceId: trace.id, timestamp: trace.timestamp });
|
|
183
|
+
if (avg < 0.5) {
|
|
184
|
+
lowScoreTraces.push({
|
|
185
|
+
id: trace.id,
|
|
186
|
+
score: avg,
|
|
187
|
+
timestamp: trace.timestamp,
|
|
188
|
+
comments: traceScores.map((s) => s.comment).filter(Boolean),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const avgScore = qualityScores.length > 0
|
|
195
|
+
? qualityScores.reduce((s, x) => s + x.value, 0) / qualityScores.length
|
|
196
|
+
: null;
|
|
197
|
+
|
|
198
|
+
latencies.sort((a, b) => a - b);
|
|
199
|
+
const p50 = latencies.length > 0 ? latencies[Math.floor(latencies.length * 0.5)] : null;
|
|
200
|
+
const p90 = latencies.length > 0 ? latencies[Math.floor(latencies.length * 0.9)] : null;
|
|
201
|
+
|
|
202
|
+
const failureRate = agentTraces.length > 0
|
|
203
|
+
? lowScoreTraces.length / agentTraces.length
|
|
204
|
+
: 0;
|
|
205
|
+
|
|
206
|
+
// Trend: compare first half vs second half of the period
|
|
207
|
+
const midpoint = new Date((fromDate.getTime() + toDate.getTime()) / 2);
|
|
208
|
+
const firstHalf = qualityScores.filter((s) => new Date(s.timestamp) < midpoint);
|
|
209
|
+
const secondHalf = qualityScores.filter((s) => new Date(s.timestamp) >= midpoint);
|
|
210
|
+
let trend = "stable";
|
|
211
|
+
if (firstHalf.length >= 2 && secondHalf.length >= 2) {
|
|
212
|
+
const avgFirst = firstHalf.reduce((s, x) => s + x.value, 0) / firstHalf.length;
|
|
213
|
+
const avgSecond = secondHalf.reduce((s, x) => s + x.value, 0) / secondHalf.length;
|
|
214
|
+
const delta = avgSecond - avgFirst;
|
|
215
|
+
if (delta > 0.05) trend = "improving";
|
|
216
|
+
else if (delta < -0.05) trend = "declining";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const status = (() => {
|
|
220
|
+
if (avgScore === null) return "no-data";
|
|
221
|
+
if (avgScore >= 0.75 && failureRate <= 0.1) return "healthy";
|
|
222
|
+
if (avgScore >= 0.65 && failureRate <= 0.2) return "acceptable";
|
|
223
|
+
return "underperforming";
|
|
224
|
+
})();
|
|
225
|
+
|
|
226
|
+
agentStats.push({
|
|
227
|
+
name: agentName,
|
|
228
|
+
invocations: agentTraces.length,
|
|
229
|
+
scoredInvocations: qualityScores.length,
|
|
230
|
+
avgScore: avgScore !== null ? Math.round(avgScore * 100) / 100 : null,
|
|
231
|
+
trend,
|
|
232
|
+
failureRate: Math.round(failureRate * 100) / 100,
|
|
233
|
+
p50Latency: p50 ? Math.round(p50) : null,
|
|
234
|
+
p90Latency: p90 ? Math.round(p90) : null,
|
|
235
|
+
status,
|
|
236
|
+
lowScoreTraces: lowScoreTraces.slice(0, 5),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
agentStats.sort((a, b) => {
|
|
241
|
+
// Underperforming first, then by avg score ascending
|
|
242
|
+
const statusOrder = { underperforming: 0, "no-data": 1, acceptable: 2, healthy: 3 };
|
|
243
|
+
const sa = statusOrder[a.status] ?? 4;
|
|
244
|
+
const sb = statusOrder[b.status] ?? 4;
|
|
245
|
+
if (sa !== sb) return sa - sb;
|
|
246
|
+
return (a.avgScore ?? 1) - (b.avgScore ?? 1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return { agentStats, totalTraces: traces.length, totalScores: scores.length };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Report Generation ───────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
function trendSymbol(trend) {
|
|
255
|
+
return { improving: "↑", declining: "↓", stable: "→" }[trend] ?? "–";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function statusEmoji(status) {
|
|
259
|
+
return { healthy: "✓", acceptable: "~", underperforming: "✗", "no-data": "?" }[status] ?? "?";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatScore(score) {
|
|
263
|
+
if (score === null) return "–";
|
|
264
|
+
return (score * 10).toFixed(1) + "/10";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatMs(ms) {
|
|
268
|
+
if (ms === null) return "–";
|
|
269
|
+
if (ms >= 1000) return (ms / 1000).toFixed(1) + "s";
|
|
270
|
+
return ms + "ms";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildMarkdownReport(metrics, fromDate, toDate, dateStr, efficiencyStats) {
|
|
274
|
+
const { agentStats, totalTraces, totalScores } = metrics;
|
|
275
|
+
const healthy = agentStats.filter((a) => a.status === "healthy");
|
|
276
|
+
const underperforming = agentStats.filter((a) => a.status === "underperforming");
|
|
277
|
+
const noData = agentStats.filter((a) => a.status === "no-data");
|
|
278
|
+
|
|
279
|
+
const overallHealth = underperforming.length === 0 ? "Healthy"
|
|
280
|
+
: underperforming.length <= 2 ? "Needs attention"
|
|
281
|
+
: "Significant issues";
|
|
282
|
+
|
|
283
|
+
const lines = [];
|
|
284
|
+
lines.push(`# Agent Performance Review — ${dateStr}`);
|
|
285
|
+
lines.push(`Period: ${fromDate.toISOString().slice(0, 10)} → ${toDate.toISOString().slice(0, 10)} | Traces: ${totalTraces} | Scored: ${totalScores} | Status: **${overallHealth}**`);
|
|
286
|
+
lines.push(`Generated by \`construct review\` + cx-trace-reviewer`);
|
|
287
|
+
lines.push("");
|
|
288
|
+
|
|
289
|
+
// Summary
|
|
290
|
+
lines.push("## Summary");
|
|
291
|
+
if (underperforming.length === 0 && agentStats.length > 0) {
|
|
292
|
+
lines.push(`All ${agentStats.length} active agents are performing within acceptable thresholds.`);
|
|
293
|
+
} else if (underperforming.length > 0) {
|
|
294
|
+
lines.push(`${underperforming.length} agent(s) need attention: ${underperforming.map((a) => `\`${a.name}\``).join(", ")}.`);
|
|
295
|
+
const worst = underperforming[0];
|
|
296
|
+
if (worst.avgScore !== null) {
|
|
297
|
+
lines.push(`Lowest performer: \`${worst.name}\` (avg score ${formatScore(worst.avgScore)}, failure rate ${Math.round(worst.failureRate * 100)}%).`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (noData.length > 0) {
|
|
301
|
+
lines.push(`${noData.length} agent(s) have no quality scores — ensure validation scoring is active.`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
|
|
305
|
+
if (efficiencyStats) {
|
|
306
|
+
lines.push("## Session Efficiency Snapshot");
|
|
307
|
+
lines.push(`Score: **${efficiencyStats.score.toFixed(2)}** | Reads: ${efficiencyStats.readCount} | Files: ${efficiencyStats.uniqueFileCount} | Repeated reads: ${efficiencyStats.repeatedReadCount} | Large reads: ${efficiencyStats.largeReadCount} | KB read: ${Math.round(efficiencyStats.totalBytesRead / 1024)}`);
|
|
308
|
+
lines.push("");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Metrics table
|
|
312
|
+
lines.push("## Metrics");
|
|
313
|
+
lines.push("| Agent | Invocations | Score | Trend | Fail% | P50 | P90 | Status |");
|
|
314
|
+
lines.push("|---|---|---|---|---|---|---|---|");
|
|
315
|
+
for (const a of agentStats) {
|
|
316
|
+
lines.push(
|
|
317
|
+
`| \`${a.name}\` | ${a.invocations} | ${formatScore(a.avgScore)} | ${trendSymbol(a.trend)} | ${Math.round(a.failureRate * 100)}% | ${formatMs(a.p50Latency)} | ${formatMs(a.p90Latency)} | ${statusEmoji(a.status)} ${a.status} |`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
lines.push("");
|
|
321
|
+
|
|
322
|
+
// Underperformer details
|
|
323
|
+
if (underperforming.length > 0) {
|
|
324
|
+
lines.push("## Underperformer Analysis");
|
|
325
|
+
lines.push("> Run `@cx-trace-reviewer` with this review for AI-generated prompt suggestions.");
|
|
326
|
+
lines.push("");
|
|
327
|
+
for (const a of underperforming) {
|
|
328
|
+
lines.push(`### \`${a.name}\``);
|
|
329
|
+
lines.push(`Invocations: ${a.invocations} | Avg score: ${formatScore(a.avgScore)} | Trend: ${trendSymbol(a.trend)} ${a.trend} | Failure rate: ${Math.round(a.failureRate * 100)}%`);
|
|
330
|
+
lines.push("");
|
|
331
|
+
if (a.lowScoreTraces.length > 0) {
|
|
332
|
+
lines.push("**Low-score trace samples:**");
|
|
333
|
+
for (const t of a.lowScoreTraces) {
|
|
334
|
+
const comments = t.comments.length > 0 ? ` — "${t.comments[0]}"` : "";
|
|
335
|
+
lines.push(`- ${new Date(t.timestamp).toISOString().slice(0, 16)} | score ${formatScore(t.score)}${comments}`);
|
|
336
|
+
}
|
|
337
|
+
lines.push("");
|
|
338
|
+
}
|
|
339
|
+
lines.push("**Prompt suggestion:** _Run `@cx-trace-reviewer` with the raw data for AI-generated recommendations._");
|
|
340
|
+
lines.push("");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Healthy agents
|
|
345
|
+
if (healthy.length > 0) {
|
|
346
|
+
lines.push("## Healthy Agents");
|
|
347
|
+
lines.push("| Agent | Invocations | Score | Trend |");
|
|
348
|
+
lines.push("|---|---|---|---|");
|
|
349
|
+
for (const a of healthy) {
|
|
350
|
+
lines.push(`| \`${a.name}\` | ${a.invocations} | ${formatScore(a.avgScore)} | ${trendSymbol(a.trend)} ${a.trend} |`);
|
|
351
|
+
}
|
|
352
|
+
lines.push("");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Recommended actions
|
|
356
|
+
lines.push("## Recommended Actions");
|
|
357
|
+
if (underperforming.length > 0) {
|
|
358
|
+
lines.push("1. Run `@cx-trace-reviewer` for detailed prompt suggestions on underperformers.");
|
|
359
|
+
let i = 2;
|
|
360
|
+
for (const a of underperforming.slice(0, 3)) {
|
|
361
|
+
lines.push(`${i++}. Review traces for \`${a.name}\` in Langfuse (${LANGFUSE_BASEURL}).`);
|
|
362
|
+
}
|
|
363
|
+
} else if (noData.length > 0) {
|
|
364
|
+
lines.push("1. Ensure validation is scoring agent outputs — call `cx_score` after verification passes.");
|
|
365
|
+
} else {
|
|
366
|
+
lines.push("1. No immediate action required. Continue monitoring.");
|
|
367
|
+
}
|
|
368
|
+
lines.push("");
|
|
369
|
+
|
|
370
|
+
return lines.join("\n");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
async function main() {
|
|
376
|
+
const toDate = new Date();
|
|
377
|
+
const fromDate = new Date(toDate.getTime() - days * 24 * 60 * 60 * 1000);
|
|
378
|
+
const dateStr = toDate.toISOString().slice(0, 10);
|
|
379
|
+
|
|
380
|
+
// Ensure output directory exists
|
|
381
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
382
|
+
|
|
383
|
+
let rawData;
|
|
384
|
+
try {
|
|
385
|
+
rawData = await fetchData(fromDate, toDate);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`Failed to fetch Langfuse data: ${err.message}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const metrics = aggregate(rawData.traces, rawData.scores, fromDate, toDate);
|
|
392
|
+
const efficiencyStats = readSessionEfficiency();
|
|
393
|
+
|
|
394
|
+
// Write raw JSON for cx-trace-reviewer to use
|
|
395
|
+
const rawPath = path.join(outDir, `${dateStr}-raw.json`);
|
|
396
|
+
fs.writeFileSync(rawPath, JSON.stringify({
|
|
397
|
+
generated: toDate.toISOString(),
|
|
398
|
+
period: { from: fromDate.toISOString(), to: toDate.toISOString(), days },
|
|
399
|
+
agentFilter,
|
|
400
|
+
baseUrl: LANGFUSE_BASEURL,
|
|
401
|
+
efficiency: efficiencyStats,
|
|
402
|
+
...metrics,
|
|
403
|
+
}, null, 2) + "\n");
|
|
404
|
+
|
|
405
|
+
if (jsonOnly) {
|
|
406
|
+
console.log(rawPath);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Write markdown report
|
|
411
|
+
const mdPath = path.join(outDir, `${dateStr}.md`);
|
|
412
|
+
const report = buildMarkdownReport(metrics, fromDate, toDate, dateStr, efficiencyStats);
|
|
413
|
+
fs.writeFileSync(mdPath, report);
|
|
414
|
+
|
|
415
|
+
// Print summary to stdout
|
|
416
|
+
const { agentStats } = metrics;
|
|
417
|
+
const underperforming = agentStats.filter((a) => a.status === "underperforming");
|
|
418
|
+
console.log(`\nReview written: ${mdPath}`);
|
|
419
|
+
console.log(`Raw data: ${rawPath}`);
|
|
420
|
+
console.log(`Agents: ${agentStats.length} | Underperforming: ${underperforming.length}`);
|
|
421
|
+
if (underperforming.length > 0) {
|
|
422
|
+
console.log(`\nRun @cx-trace-reviewer with ${rawPath} for prompt suggestions.`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
main().catch((err) => {
|
|
427
|
+
console.error(err);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
});
|