@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,1123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/mcp/server.mjs — Construct MCP server exposing workflow, skill, and observability tools.
|
|
4
|
+
*
|
|
5
|
+
* Provides MCP tools for agent health, workflow management, skill retrieval, and Langfuse telemetry.
|
|
6
|
+
* Consumed by Claude Code, OpenCode, and any MCP-compatible host.
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join, resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import * as langfuse from '../telemetry/backends/langfuse.mjs';
|
|
17
|
+
import { summarizePromptComposition } from '../prompt-composer.mjs';
|
|
18
|
+
import { enrichMetadataWithPrompt } from '../prompt-metadata.mjs';
|
|
19
|
+
import { readCurrentModels, resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from '../model-router.mjs';
|
|
20
|
+
import { loadToolkitEnv } from '../toolkit-env.mjs';
|
|
21
|
+
import { loadConstructEnv } from '../env-config.mjs';
|
|
22
|
+
import { inspectContextState } from '../context-state.mjs';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
25
|
+
const ROOT_DIR = resolve(process.env.CX_TOOLKIT_DIR || DEFAULT_ROOT_DIR);
|
|
26
|
+
loadToolkitEnv(ROOT_DIR);
|
|
27
|
+
import {
|
|
28
|
+
addTask,
|
|
29
|
+
addTaskFromIntent,
|
|
30
|
+
addTasksFromPlan,
|
|
31
|
+
alignmentFindings,
|
|
32
|
+
createNeedsMainInputPacket,
|
|
33
|
+
initWorkflow,
|
|
34
|
+
inspectWorkflowHealth,
|
|
35
|
+
loadWorkflow,
|
|
36
|
+
summarizeWorkflow,
|
|
37
|
+
updateTask,
|
|
38
|
+
validateWorkflowState,
|
|
39
|
+
} from '../workflow-state.mjs';
|
|
40
|
+
import { buildPublicHealthSurface, buildStatus } from '../status.mjs';
|
|
41
|
+
import { routeRequest, requiresExecutiveApproval, TERMINAL_STATES } from '../orchestration-policy.mjs';
|
|
42
|
+
|
|
43
|
+
const SECRET_PATTERNS = [
|
|
44
|
+
{ name: 'Anthropic API key', pattern: /ANTHROPIC_API_KEY\s*=\s*(sk-ant-[a-zA-Z0-9\-_]{20,})/i },
|
|
45
|
+
{ name: 'OpenAI API key', pattern: /OPENAI_API_KEY\s*=\s*(sk-[a-zA-Z0-9]{40,})/i },
|
|
46
|
+
{ name: 'OpenRouter key', pattern: /(sk-or-v1-[a-zA-Z0-9]{40,})/ },
|
|
47
|
+
{ name: 'AWS access key', pattern: /(AKIA[0-9A-Z]{16})/ },
|
|
48
|
+
{ name: 'Private key (PEM)', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
|
|
49
|
+
{ name: 'GitHub personal access token', pattern: /(ghp_[a-zA-Z0-9]{36})/ },
|
|
50
|
+
{ name: 'GitHub Actions token', pattern: /(ghs_[a-zA-Z0-9]{36})/ },
|
|
51
|
+
{ name: 'Database URL with credentials', pattern: /DATABASE_URL\s*=\s*(postgresql:\/\/[^@]+:[^@]+@)/i },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const PLACEHOLDER_PATTERNS = [
|
|
55
|
+
/\.\.\./,
|
|
56
|
+
/YOUR_KEY/i,
|
|
57
|
+
/<[^>]+>/,
|
|
58
|
+
/^sk-\.\.\./,
|
|
59
|
+
/^pk-lf-\.\.\./,
|
|
60
|
+
/__[A-Z_]+__/,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function isPlaceholder(value) {
|
|
64
|
+
return PLACEHOLDER_PATTERNS.some((p) => p.test(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isBinary(buffer) {
|
|
68
|
+
const len = Math.min(buffer.length, 512);
|
|
69
|
+
for (let i = 0; i < len; i++) {
|
|
70
|
+
if (buffer[i] === 0) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function exec(cmd, cwd) {
|
|
76
|
+
return execSync(cmd, { stdio: 'pipe', timeout: 10000, cwd }).toString().trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readJSON(filePath) {
|
|
80
|
+
if (!existsSync(filePath)) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tool: agent_health
|
|
89
|
+
function agentHealth(args) {
|
|
90
|
+
const reviewDir = join(homedir(), '.cx', 'performance-reviews');
|
|
91
|
+
|
|
92
|
+
if (!existsSync(reviewDir)) {
|
|
93
|
+
return { error: "No performance reviews found. Run 'construct review' to generate one." };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let files;
|
|
97
|
+
try {
|
|
98
|
+
files = readdirSync(reviewDir);
|
|
99
|
+
} catch {
|
|
100
|
+
return { error: "No performance reviews found. Run 'construct review' to generate one." };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const processed = files
|
|
104
|
+
.filter((f) => f.endsWith('.json') && !f.endsWith('-raw.json'))
|
|
105
|
+
.sort()
|
|
106
|
+
.reverse();
|
|
107
|
+
|
|
108
|
+
const raw = files
|
|
109
|
+
.filter((f) => f.endsWith('-raw.json'))
|
|
110
|
+
.sort()
|
|
111
|
+
.reverse();
|
|
112
|
+
|
|
113
|
+
const candidates = processed.length > 0 ? processed : raw;
|
|
114
|
+
|
|
115
|
+
if (candidates.length === 0) {
|
|
116
|
+
return { error: "No performance reviews found. Run 'construct review' to generate one." };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let data;
|
|
120
|
+
try {
|
|
121
|
+
const content = readFileSync(join(reviewDir, candidates[0]), 'utf8');
|
|
122
|
+
data = JSON.parse(content);
|
|
123
|
+
} catch {
|
|
124
|
+
return { error: "Failed to parse performance review file." };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const reviewDate = data.reviewDate ?? data.generatedAt ?? candidates[0].replace(/\.json$/, '');
|
|
128
|
+
const period = data.period ?? data.windowDays ?? null;
|
|
129
|
+
|
|
130
|
+
let agents = [];
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(data.agents)) {
|
|
133
|
+
agents = data.agents;
|
|
134
|
+
} else if (data.agentReviews) {
|
|
135
|
+
agents = Object.entries(data.agentReviews).map(([name, review]) => ({
|
|
136
|
+
name,
|
|
137
|
+
status: review.status ?? review.health ?? 'unknown',
|
|
138
|
+
avgScore: review.avgScore ?? review.averageScore ?? null,
|
|
139
|
+
trend: review.trend ?? null,
|
|
140
|
+
failureRate: review.failureRate ?? null,
|
|
141
|
+
invocations: review.invocations ?? review.totalCalls ?? null,
|
|
142
|
+
}));
|
|
143
|
+
} else if (data.summary && Array.isArray(data.summary.agents)) {
|
|
144
|
+
agents = data.summary.agents;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (args.agent_name) {
|
|
148
|
+
const target = args.agent_name.toLowerCase();
|
|
149
|
+
agents = agents.filter((a) => (a.name ?? '').toLowerCase().includes(target));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalized = agents.map((a) => ({
|
|
153
|
+
name: a.name ?? 'unknown',
|
|
154
|
+
status: a.status ?? 'unknown',
|
|
155
|
+
avgScore: a.avgScore ?? a.averageScore ?? null,
|
|
156
|
+
trend: a.trend ?? null,
|
|
157
|
+
failureRate: a.failureRate ?? null,
|
|
158
|
+
invocations: a.invocations ?? a.totalCalls ?? null,
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
return { reviewDate, period, agents: normalized };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Tool: summarize_diff
|
|
165
|
+
function summarizeDiff(args) {
|
|
166
|
+
const baseRef = args.base_ref ?? 'HEAD~1';
|
|
167
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
168
|
+
|
|
169
|
+
let stat, nameStatus;
|
|
170
|
+
try {
|
|
171
|
+
stat = exec(`git diff --stat ${baseRef}`, cwd);
|
|
172
|
+
nameStatus = exec(`git diff --name-status ${baseRef}`, cwd);
|
|
173
|
+
} catch {
|
|
174
|
+
return { error: 'Not a git repository or git not available' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const changes = nameStatus
|
|
178
|
+
.split('\n')
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.map((line) => {
|
|
181
|
+
const [status, ...parts] = line.split('\t');
|
|
182
|
+
return { status: status.trim(), file: parts.join('\t') };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Parse insertions/deletions from stat summary line
|
|
186
|
+
const statLines = stat.split('\n');
|
|
187
|
+
const summaryLine = statLines[statLines.length - 1] ?? '';
|
|
188
|
+
const filesMatch = summaryLine.match(/(\d+) files? changed/);
|
|
189
|
+
const insertionsMatch = summaryLine.match(/(\d+) insertions?\(\+\)/);
|
|
190
|
+
const deletionsMatch = summaryLine.match(/(\d+) deletions?\(-\)/);
|
|
191
|
+
|
|
192
|
+
const filesChanged = filesMatch ? parseInt(filesMatch[1], 10) : changes.length;
|
|
193
|
+
const insertions = insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0;
|
|
194
|
+
const deletions = deletionsMatch ? parseInt(deletionsMatch[1], 10) : 0;
|
|
195
|
+
|
|
196
|
+
const added = changes.filter((c) => c.status === 'A').length;
|
|
197
|
+
const modified = changes.filter((c) => c.status === 'M').length;
|
|
198
|
+
const deleted = changes.filter((c) => c.status === 'D').length;
|
|
199
|
+
|
|
200
|
+
const parts = [];
|
|
201
|
+
if (modified > 0) parts.push(`${modified} file${modified > 1 ? 's' : ''} modified`);
|
|
202
|
+
if (added > 0) parts.push(`${added} file${added > 1 ? 's' : ''} added`);
|
|
203
|
+
if (deleted > 0) parts.push(`${deleted} file${deleted > 1 ? 's' : ''} deleted`);
|
|
204
|
+
|
|
205
|
+
const summary =
|
|
206
|
+
parts.length > 0
|
|
207
|
+
? `${parts.join(', ')} (+${insertions}/-${deletions} lines) vs ${baseRef}.`
|
|
208
|
+
: `No changes vs ${baseRef}.`;
|
|
209
|
+
|
|
210
|
+
return { base_ref: baseRef, files_changed: filesChanged, insertions, deletions, changes, summary };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Tool: scan_file
|
|
214
|
+
function scanFile(args) {
|
|
215
|
+
const filePath = resolve(args.file_path);
|
|
216
|
+
|
|
217
|
+
let rawBuffer;
|
|
218
|
+
try {
|
|
219
|
+
rawBuffer = readFileSync(filePath);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return { error: `Cannot read file: ${err.message}` };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (isBinary(rawBuffer)) {
|
|
225
|
+
return { file_path: filePath, secrets: [], quality_issues: [], clean: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const content = rawBuffer.toString('utf8');
|
|
229
|
+
const lines = content.split('\n');
|
|
230
|
+
|
|
231
|
+
// Secret scanning
|
|
232
|
+
const secrets = [];
|
|
233
|
+
for (let i = 0; i < lines.length; i++) {
|
|
234
|
+
const line = lines[i];
|
|
235
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
236
|
+
const match = pattern.exec(line);
|
|
237
|
+
if (!match) continue;
|
|
238
|
+
const captured = match[1] ?? match[0];
|
|
239
|
+
if (isPlaceholder(captured)) continue;
|
|
240
|
+
secrets.push({
|
|
241
|
+
pattern: name,
|
|
242
|
+
line: i + 1,
|
|
243
|
+
masked_value: captured.slice(0, 20) + '...',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Quality scanning
|
|
249
|
+
const quality_issues = [];
|
|
250
|
+
|
|
251
|
+
if (lines.length > 800) {
|
|
252
|
+
quality_issues.push({ type: 'file_too_long', detail: `File has ${lines.length} lines (limit: 800)` });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Detect oversized functions via simple heuristic
|
|
256
|
+
const funcKeywords = /^\s*(function\s+\w+|async\s+function\s+\w+|\w+\s*=\s*(?:async\s+)?\(|def\s+\w+|func\s+\w+)/;
|
|
257
|
+
let funcStart = null;
|
|
258
|
+
let braceDepth = 0;
|
|
259
|
+
let inBlock = false;
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
const line = lines[i];
|
|
263
|
+
|
|
264
|
+
if (funcKeywords.test(line)) {
|
|
265
|
+
funcStart = i + 1;
|
|
266
|
+
braceDepth = 0;
|
|
267
|
+
inBlock = false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (funcStart !== null) {
|
|
271
|
+
for (const ch of line) {
|
|
272
|
+
if (ch === '{') { braceDepth++; inBlock = true; }
|
|
273
|
+
else if (ch === '}') { braceDepth--; }
|
|
274
|
+
}
|
|
275
|
+
if (inBlock && braceDepth <= 0) {
|
|
276
|
+
const funcLen = i + 1 - funcStart + 1;
|
|
277
|
+
if (funcLen > 50) {
|
|
278
|
+
quality_issues.push({
|
|
279
|
+
type: 'function_too_long',
|
|
280
|
+
detail: `Function starting at line ${funcStart} is ${funcLen} lines (limit: 50)`,
|
|
281
|
+
line: funcStart,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
funcStart = null;
|
|
285
|
+
inBlock = false;
|
|
286
|
+
braceDepth = 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// TODO/FIXME/HACK comments
|
|
291
|
+
const todoMatch = /\/\/\s*(TODO|FIXME|HACK)\b|#\s*(TODO|FIXME|HACK)\b/i.exec(line);
|
|
292
|
+
if (todoMatch) {
|
|
293
|
+
const tag = (todoMatch[1] ?? todoMatch[2]).toUpperCase();
|
|
294
|
+
quality_issues.push({
|
|
295
|
+
type: 'comment_marker',
|
|
296
|
+
detail: `${tag} comment`,
|
|
297
|
+
line: i + 1,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
file_path: filePath,
|
|
304
|
+
secrets,
|
|
305
|
+
quality_issues,
|
|
306
|
+
clean: secrets.length === 0 && quality_issues.length === 0,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Tool: project_context
|
|
311
|
+
function projectContext(args) {
|
|
312
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
313
|
+
const contextInspection = inspectContextState(cwd);
|
|
314
|
+
const context_state = contextInspection.state;
|
|
315
|
+
const context = context_state?.markdown || null;
|
|
316
|
+
const has_context_file = contextInspection.hasFile;
|
|
317
|
+
const workflow = loadWorkflow(cwd);
|
|
318
|
+
const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
|
|
319
|
+
const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
|
|
320
|
+
const executionContractModel = resolveExecutionContractModelMetadata({
|
|
321
|
+
envValues: mergedEnv,
|
|
322
|
+
registryModels: registry.models ?? {},
|
|
323
|
+
requestedTier: selectModelTierForWorkCategory(workflow?.tasks?.find((task) => task.key === workflow?.currentTaskKey)?.workCategory),
|
|
324
|
+
workCategory: null,
|
|
325
|
+
});
|
|
326
|
+
const publicHealth = buildPublicHealthSurface({
|
|
327
|
+
cwd,
|
|
328
|
+
contextInspection,
|
|
329
|
+
workflow,
|
|
330
|
+
executionContractModel,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
let recent_commits = [];
|
|
334
|
+
let working_tree_status = 'clean';
|
|
335
|
+
let changed_files = [];
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const log = exec('git log --oneline -10', cwd);
|
|
339
|
+
recent_commits = log.split('\n').filter(Boolean);
|
|
340
|
+
} catch {
|
|
341
|
+
// not a git repo or no commits
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const status = exec('git status --short', cwd);
|
|
346
|
+
const statusLines = status.split('\n').filter(Boolean);
|
|
347
|
+
changed_files = statusLines.map((l) => l.trim());
|
|
348
|
+
working_tree_status = statusLines.length > 0 ? 'dirty' : 'clean';
|
|
349
|
+
} catch {
|
|
350
|
+
// not a git repo
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { cwd, has_context_file, context, context_state, recent_commits, working_tree_status, changed_files, publicHealth };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function orchestrationPolicy(args) {
|
|
357
|
+
const route = routeRequest(args || {});
|
|
358
|
+
const approvalRequired = requiresExecutiveApproval(args?.approval || {});
|
|
359
|
+
return {
|
|
360
|
+
...route,
|
|
361
|
+
approvalRequired,
|
|
362
|
+
terminalStates: TERMINAL_STATES,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Tool: list_skills
|
|
367
|
+
function listSkills() {
|
|
368
|
+
const skillsDir = join(ROOT_DIR, 'skills');
|
|
369
|
+
if (!existsSync(skillsDir)) return { error: 'Skills directory not found.' };
|
|
370
|
+
|
|
371
|
+
const listDirRecursive = (dir, prefix = '') => {
|
|
372
|
+
let results = [];
|
|
373
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
if (entry.isDirectory()) {
|
|
376
|
+
results = results.concat(listDirRecursive(join(dir, entry.name), `${prefix}${entry.name}/`));
|
|
377
|
+
} else if (entry.name.endsWith('.md') && entry.name !== 'SKILL.md') {
|
|
378
|
+
results.push(`${prefix}${entry.name.replace('.md', '')}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return results;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const skills = listDirRecursive(skillsDir).sort();
|
|
385
|
+
return { skills };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Tool: get_skill
|
|
389
|
+
function getSkill(args) {
|
|
390
|
+
const { path: skillPath } = args;
|
|
391
|
+
if (!skillPath) return { error: 'Missing path argument' };
|
|
392
|
+
|
|
393
|
+
const fullPath = join(ROOT_DIR, 'skills', `${skillPath}.md`);
|
|
394
|
+
|
|
395
|
+
if (!existsSync(fullPath)) {
|
|
396
|
+
return { error: `Skill not found: ${skillPath}` };
|
|
397
|
+
}
|
|
398
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
399
|
+
return { content };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Tool: get_template
|
|
403
|
+
function getTemplate(args) {
|
|
404
|
+
const { name } = args;
|
|
405
|
+
if (!name) return { error: 'Missing name argument' };
|
|
406
|
+
const safe = String(name).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
407
|
+
if (!safe) return { error: 'Invalid template name' };
|
|
408
|
+
|
|
409
|
+
const cwd = process.cwd();
|
|
410
|
+
const candidates = [
|
|
411
|
+
{ source: 'project-override', path: join(cwd, '.cx', 'templates', 'docs', `${safe}.md`) },
|
|
412
|
+
{ source: 'shipped-default', path: join(ROOT_DIR, 'templates', 'docs', `${safe}.md`) },
|
|
413
|
+
];
|
|
414
|
+
for (const { source, path: p } of candidates) {
|
|
415
|
+
if (existsSync(p)) {
|
|
416
|
+
return { source, path: p, content: readFileSync(p, 'utf8') };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { error: `Template not found: ${safe}`, available: listTemplatesRaw() };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function listTemplatesRaw() {
|
|
423
|
+
const shipped = [];
|
|
424
|
+
const override = [];
|
|
425
|
+
const shippedDir = join(ROOT_DIR, 'templates', 'docs');
|
|
426
|
+
if (existsSync(shippedDir)) {
|
|
427
|
+
for (const f of readdirSync(shippedDir)) {
|
|
428
|
+
if (f.endsWith('.md')) shipped.push(f.replace(/\.md$/, ''));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const overrideDir = join(process.cwd(), '.cx', 'templates', 'docs');
|
|
432
|
+
if (existsSync(overrideDir)) {
|
|
433
|
+
for (const f of readdirSync(overrideDir)) {
|
|
434
|
+
if (f.endsWith('.md')) override.push(f.replace(/\.md$/, ''));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { shipped: shipped.sort(), overridden: override.sort() };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Tool: list_templates
|
|
441
|
+
function listTemplates() {
|
|
442
|
+
return listTemplatesRaw();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Tool: search_skills
|
|
446
|
+
function searchSkills(args) {
|
|
447
|
+
const { pattern } = args;
|
|
448
|
+
if (!pattern) return { error: 'Missing pattern argument' };
|
|
449
|
+
const skillsDir = join(ROOT_DIR, 'skills');
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Try ripgrep first
|
|
453
|
+
const output = execSync(`rg -i "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
|
|
454
|
+
return { results: output.split('\n').filter(Boolean) };
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// Fallback to basic grep
|
|
457
|
+
try {
|
|
458
|
+
const output = execSync(`grep -ri "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
|
|
459
|
+
return { results: output.split('\n').filter(Boolean) };
|
|
460
|
+
} catch (err2) {
|
|
461
|
+
return { results: [], note: 'No matches found or grep error' };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function workflowStatus(args) {
|
|
467
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
468
|
+
const workflow = loadWorkflow(cwd);
|
|
469
|
+
const workflowHealth = inspectWorkflowHealth(workflow, { cwd });
|
|
470
|
+
const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
|
|
471
|
+
const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
|
|
472
|
+
const executionContractModel = resolveExecutionContractModelMetadata({
|
|
473
|
+
envValues: mergedEnv,
|
|
474
|
+
registryModels: registry.models ?? {},
|
|
475
|
+
requestedTier: selectModelTierForWorkCategory(workflow?.tasks?.find((task) => task.key === workflow?.currentTaskKey)?.workCategory),
|
|
476
|
+
workCategory: null,
|
|
477
|
+
});
|
|
478
|
+
const publicHealth = buildPublicHealthSurface({
|
|
479
|
+
cwd,
|
|
480
|
+
workflow,
|
|
481
|
+
executionContractModel,
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
cwd,
|
|
485
|
+
exists: Boolean(workflow),
|
|
486
|
+
summary: summarizeWorkflow(workflow),
|
|
487
|
+
workflow,
|
|
488
|
+
alignment: {
|
|
489
|
+
status: workflowHealth.alignment.status,
|
|
490
|
+
findings: workflowHealth.alignment.findings,
|
|
491
|
+
},
|
|
492
|
+
publicHealth,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function workflowInit(args) {
|
|
497
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
498
|
+
const title = args.title || 'Untitled workflow';
|
|
499
|
+
const { workflow, created } = initWorkflow(cwd, title, args.spec_ref ?? null);
|
|
500
|
+
return { cwd, created, workflow, summary: summarizeWorkflow(workflow) };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Tool: list_teams
|
|
504
|
+
function listTeams() {
|
|
505
|
+
const teamsPath = join(ROOT_DIR, 'agents', 'teams.json');
|
|
506
|
+
if (!existsSync(teamsPath)) return { error: 'agents/teams.json not found' };
|
|
507
|
+
const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
|
|
508
|
+
return {
|
|
509
|
+
teams: Object.entries(templates).map(([name, t]) => ({
|
|
510
|
+
name,
|
|
511
|
+
description: t.description,
|
|
512
|
+
members: t.members,
|
|
513
|
+
focus: t.focus,
|
|
514
|
+
skills: t.skills,
|
|
515
|
+
promotionGates: t.promotionGates,
|
|
516
|
+
})),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Tool: get_team
|
|
521
|
+
function getTeam(args) {
|
|
522
|
+
const { name } = args;
|
|
523
|
+
if (!name) return { error: 'Missing name argument' };
|
|
524
|
+
const teamsPath = join(ROOT_DIR, 'agents', 'teams.json');
|
|
525
|
+
if (!existsSync(teamsPath)) return { error: 'agents/teams.json not found' };
|
|
526
|
+
const { templates } = JSON.parse(readFileSync(teamsPath, 'utf8'));
|
|
527
|
+
const team = templates[name];
|
|
528
|
+
if (!team) return { error: `Team not found: ${name}`, available: Object.keys(templates) };
|
|
529
|
+
return { name, ...team };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function workflowAddTask(args) {
|
|
533
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
534
|
+
if (args.request) {
|
|
535
|
+
const workflow = addTaskFromIntent(cwd, args.request, {
|
|
536
|
+
key: args.key,
|
|
537
|
+
title: args.title,
|
|
538
|
+
phase: args.phase,
|
|
539
|
+
owner: args.owner,
|
|
540
|
+
files: args.files,
|
|
541
|
+
readFirst: args.readFirst,
|
|
542
|
+
doNotChange: args.doNotChange,
|
|
543
|
+
acceptanceCriteria: args.acceptanceCriteria,
|
|
544
|
+
verification: args.verification,
|
|
545
|
+
overlays: args.overlays,
|
|
546
|
+
challengeRequired: args.challengeRequired,
|
|
547
|
+
challengeStatus: args.challengeStatus,
|
|
548
|
+
tokenBudget: args.tokenBudget,
|
|
549
|
+
status: args.status,
|
|
550
|
+
});
|
|
551
|
+
if (!workflow) return { ok: true, skipped: true, reason: 'immediate-track' };
|
|
552
|
+
return { cwd, workflow, summary: summarizeWorkflow(workflow), source: 'intent' };
|
|
553
|
+
}
|
|
554
|
+
const workflow = addTask(cwd, {
|
|
555
|
+
key: args.key,
|
|
556
|
+
title: args.title,
|
|
557
|
+
phase: args.phase,
|
|
558
|
+
owner: args.owner,
|
|
559
|
+
files: args.files,
|
|
560
|
+
readFirst: args.readFirst,
|
|
561
|
+
doNotChange: args.doNotChange,
|
|
562
|
+
acceptanceCriteria: args.acceptanceCriteria,
|
|
563
|
+
verification: args.verification,
|
|
564
|
+
dependsOn: args.dependsOn,
|
|
565
|
+
overlays: args.overlays,
|
|
566
|
+
challengeRequired: args.challengeRequired,
|
|
567
|
+
challengeStatus: args.challengeStatus,
|
|
568
|
+
});
|
|
569
|
+
return { cwd, workflow, summary: summarizeWorkflow(workflow), source: 'manual' };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function workflowUpdateTask(args) {
|
|
573
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
574
|
+
const workflow = updateTask(cwd, args.key, {
|
|
575
|
+
status: args.status,
|
|
576
|
+
owner: args.owner,
|
|
577
|
+
phase: args.phase,
|
|
578
|
+
note: args.note,
|
|
579
|
+
verification: args.verification,
|
|
580
|
+
overlays: args.overlays,
|
|
581
|
+
challengeRequired: args.challengeRequired,
|
|
582
|
+
challengeStatus: args.challengeStatus,
|
|
583
|
+
});
|
|
584
|
+
return { cwd, workflow, summary: summarizeWorkflow(workflow) };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function workflowNeedsMainInput(args) {
|
|
588
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
589
|
+
const packet = createNeedsMainInputPacket(args);
|
|
590
|
+
const workflow = updateTask(cwd, args.taskKey, {
|
|
591
|
+
status: 'blocked_needs_user',
|
|
592
|
+
note: `${packet.worker}: ${packet.blocker} | question: ${packet.question}`,
|
|
593
|
+
});
|
|
594
|
+
return { cwd, packet, workflow, summary: summarizeWorkflow(workflow) };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function workflowValidate(args) {
|
|
598
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
599
|
+
const workflow = loadWorkflow(cwd);
|
|
600
|
+
const result = validateWorkflowState(workflow);
|
|
601
|
+
return { cwd, ...result };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function workflowImportPlan(args) {
|
|
605
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
606
|
+
const markdown = args.markdown ?? '';
|
|
607
|
+
const { workflow, count } = addTasksFromPlan(cwd, markdown, {
|
|
608
|
+
phase: args.phase,
|
|
609
|
+
owner: args.owner,
|
|
610
|
+
readFirst: Array.isArray(args.readFirst) ? args.readFirst : undefined,
|
|
611
|
+
doNotChange: Array.isArray(args.doNotChange) ? args.doNotChange : undefined,
|
|
612
|
+
acceptanceCriteria: Array.isArray(args.acceptanceCriteria) ? args.acceptanceCriteria : undefined,
|
|
613
|
+
workflowTitle: args.title,
|
|
614
|
+
specRef: args.spec_ref,
|
|
615
|
+
});
|
|
616
|
+
return { cwd, count, workflow, summary: summarizeWorkflow(workflow) };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function resolveReleaseTag(cwd) {
|
|
620
|
+
try {
|
|
621
|
+
return execSync('git rev-parse --short HEAD', { stdio: 'pipe', cwd, timeout: 2000 }).toString().trim() || undefined;
|
|
622
|
+
} catch {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function resolveSessionContext() {
|
|
628
|
+
const cwd = process.cwd();
|
|
629
|
+
let workflowId;
|
|
630
|
+
let workflowPhase;
|
|
631
|
+
let workflowOwner;
|
|
632
|
+
try {
|
|
633
|
+
const wf = loadWorkflow(cwd);
|
|
634
|
+
if (wf) {
|
|
635
|
+
workflowId = wf.id;
|
|
636
|
+
workflowPhase = wf.phase;
|
|
637
|
+
const active = (wf.tasks || []).find((t) => t.status === 'in-progress' || t.status === 'in_progress');
|
|
638
|
+
if (active) workflowOwner = active.owner;
|
|
639
|
+
}
|
|
640
|
+
} catch { /* best effort */ }
|
|
641
|
+
const sessionId = process.env.CLAUDE_SESSION_ID
|
|
642
|
+
|| process.env.CX_SESSION_ID
|
|
643
|
+
|| process.env.OPENCODE_SESSION_ID
|
|
644
|
+
|| workflowId;
|
|
645
|
+
const userId = process.env.USER || process.env.USERNAME || process.env.LOGNAME;
|
|
646
|
+
return { cwd, sessionId, userId, release: resolveReleaseTag(cwd), workflowPhase, workflowOwner, workflowId };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function langfuseHeaders() {
|
|
650
|
+
const key = process.env.LANGFUSE_PUBLIC_KEY;
|
|
651
|
+
const secret = process.env.LANGFUSE_SECRET_KEY;
|
|
652
|
+
if (!key || !secret) throw new Error('LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set.');
|
|
653
|
+
return {
|
|
654
|
+
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
|
|
655
|
+
'Content-Type': 'application/json',
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function langfuseBaseUrl() {
|
|
660
|
+
return (process.env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function cxTrace(args) {
|
|
664
|
+
const ctx = resolveSessionContext();
|
|
665
|
+
const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
|
|
666
|
+
const registryModels = registry.models ?? {};
|
|
667
|
+
const currentModels = readCurrentModels(join(ROOT_DIR, '.env'), registryModels, process.env);
|
|
668
|
+
const route = typeof args.input === 'string' ? routeRequest({ request: args.input }) : null;
|
|
669
|
+
const executionContractModel = resolveExecutionContractModelMetadata({
|
|
670
|
+
envValues: currentModels,
|
|
671
|
+
registryModels,
|
|
672
|
+
requestedTier: selectModelTierForWorkCategory(route?.workCategory),
|
|
673
|
+
workCategory: route?.workCategory || null,
|
|
674
|
+
});
|
|
675
|
+
const runtimePromptMetadata = summarizePromptComposition(args.name, {
|
|
676
|
+
rootDir: ROOT_DIR,
|
|
677
|
+
request: typeof args.input === 'string' ? args.input : '',
|
|
678
|
+
route,
|
|
679
|
+
registryModels,
|
|
680
|
+
envValues: currentModels,
|
|
681
|
+
executionContractModel,
|
|
682
|
+
hostConstraints: {
|
|
683
|
+
runtime: 'mcp',
|
|
684
|
+
providerAgnostic: true,
|
|
685
|
+
telemetryBackend: 'langfuse',
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
const metadata = enrichMetadataWithPrompt(args.name, {
|
|
689
|
+
...(args.metadata && typeof args.metadata === 'object' ? args.metadata : {}),
|
|
690
|
+
...runtimePromptMetadata,
|
|
691
|
+
workflowId: ctx.workflowId,
|
|
692
|
+
workflowPhase: ctx.workflowPhase,
|
|
693
|
+
workflowOwner: ctx.workflowOwner,
|
|
694
|
+
release: ctx.release,
|
|
695
|
+
}, { rootDir: ROOT_DIR });
|
|
696
|
+
const traceId = args.id ?? crypto.randomUUID();
|
|
697
|
+
try {
|
|
698
|
+
const available = await langfuse.isAvailable();
|
|
699
|
+
if (!available) return { ok: false, error: 'Langfuse credentials not configured', id: traceId };
|
|
700
|
+
const teamId = args.metadata?.teamId ?? metadata.teamId;
|
|
701
|
+
const body = {
|
|
702
|
+
id: traceId,
|
|
703
|
+
name: args.name,
|
|
704
|
+
metadata: {
|
|
705
|
+
...metadata,
|
|
706
|
+
agentName: args.name,
|
|
707
|
+
goal: typeof args.input === 'string' ? args.input : JSON.stringify(args.input ?? ''),
|
|
708
|
+
teamId,
|
|
709
|
+
},
|
|
710
|
+
tags: [args.name, teamId].filter(Boolean),
|
|
711
|
+
userId: ctx.userId,
|
|
712
|
+
sessionId: args.session_id || ctx.sessionId,
|
|
713
|
+
input: args.input,
|
|
714
|
+
output: args.output,
|
|
715
|
+
timestamp: args.timestamp ?? new Date().toISOString(),
|
|
716
|
+
release: ctx.release,
|
|
717
|
+
};
|
|
718
|
+
const res = await fetch(`${langfuseBaseUrl()}/api/public/traces`, {
|
|
719
|
+
method: 'POST',
|
|
720
|
+
headers: langfuseHeaders(),
|
|
721
|
+
body: JSON.stringify(body),
|
|
722
|
+
});
|
|
723
|
+
if (!res.ok) {
|
|
724
|
+
const text = await res.text().catch(() => '');
|
|
725
|
+
return { ok: false, error: `Langfuse API error ${res.status}: ${text}`, id: traceId };
|
|
726
|
+
}
|
|
727
|
+
return { ok: true, id: traceId };
|
|
728
|
+
} catch (err) {
|
|
729
|
+
return { ok: false, error: err.message, id: traceId };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function cxScore(args) {
|
|
734
|
+
const traceId = args.trace_id ?? '';
|
|
735
|
+
try {
|
|
736
|
+
const available = await langfuse.isAvailable();
|
|
737
|
+
if (!available) return { ok: false, error: 'Langfuse credentials not configured' };
|
|
738
|
+
const body = {
|
|
739
|
+
id: crypto.randomUUID(),
|
|
740
|
+
traceId,
|
|
741
|
+
name: args.name ?? 'quality',
|
|
742
|
+
value: args.value,
|
|
743
|
+
dataType: 'NUMERIC',
|
|
744
|
+
comment: args.comment,
|
|
745
|
+
};
|
|
746
|
+
const res = await fetch(`${langfuseBaseUrl()}/api/public/scores`, {
|
|
747
|
+
method: 'POST',
|
|
748
|
+
headers: langfuseHeaders(),
|
|
749
|
+
body: JSON.stringify(body),
|
|
750
|
+
});
|
|
751
|
+
if (!res.ok) {
|
|
752
|
+
const text = await res.text().catch(() => '');
|
|
753
|
+
return { ok: false, error: `Langfuse API error ${res.status}: ${text}` };
|
|
754
|
+
}
|
|
755
|
+
return { ok: true, traceId };
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return { ok: false, error: err.message };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function sessionUsage(args) {
|
|
762
|
+
const cwd = args.cwd ? resolve(args.cwd) : process.cwd();
|
|
763
|
+
const homeDir = args.home_dir ? resolve(args.home_dir) : homedir();
|
|
764
|
+
const status = await buildStatus({ rootDir: ROOT_DIR, cwd, homeDir, env: process.env });
|
|
765
|
+
return {
|
|
766
|
+
cwd,
|
|
767
|
+
sessionUsage: status.sessionUsage,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Server setup
|
|
772
|
+
const server = new Server(
|
|
773
|
+
{ name: 'construct-mcp', version: '1.0.0' },
|
|
774
|
+
{ capabilities: { tools: {} } }
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
778
|
+
tools: [
|
|
779
|
+
{
|
|
780
|
+
name: 'agent_health',
|
|
781
|
+
description: 'Returns agent health summaries from the most recent performance review.',
|
|
782
|
+
inputSchema: {
|
|
783
|
+
type: 'object',
|
|
784
|
+
properties: {
|
|
785
|
+
agent_name: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
description: 'Specific agent name to filter, or omit for all agents.',
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: 'summarize_diff',
|
|
794
|
+
description: 'Summarizes the git diff between the current state and a base ref.',
|
|
795
|
+
inputSchema: {
|
|
796
|
+
type: 'object',
|
|
797
|
+
properties: {
|
|
798
|
+
base_ref: {
|
|
799
|
+
type: 'string',
|
|
800
|
+
description: 'Git ref to diff against (default: HEAD~1).',
|
|
801
|
+
},
|
|
802
|
+
cwd: {
|
|
803
|
+
type: 'string',
|
|
804
|
+
description: 'Working directory for the git command.',
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: 'scan_file',
|
|
811
|
+
description: 'Scans a file for secrets and code quality issues.',
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: 'object',
|
|
814
|
+
properties: {
|
|
815
|
+
file_path: {
|
|
816
|
+
type: 'string',
|
|
817
|
+
description: 'Absolute path to the file to scan.',
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
required: ['file_path'],
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'project_context',
|
|
825
|
+
description: 'Returns project context: .cx/context.md content, recent commits, and working tree status.',
|
|
826
|
+
inputSchema: {
|
|
827
|
+
type: 'object',
|
|
828
|
+
properties: {
|
|
829
|
+
cwd: {
|
|
830
|
+
type: 'string',
|
|
831
|
+
description: 'Project directory (default: process.cwd()).',
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: 'get_skill',
|
|
838
|
+
description: 'Reads a specific skill playbook from the Construct knowledge base (e.g. "security/security-arch", "web/design-quality").',
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: {
|
|
842
|
+
path: { type: 'string', description: 'Relative path to the skill (without .md extension)' },
|
|
843
|
+
},
|
|
844
|
+
required: ['path'],
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
name: 'orchestration_policy',
|
|
849
|
+
description: 'Classifies a request into intent, execution track, specialists, and approval boundaries.',
|
|
850
|
+
inputSchema: {
|
|
851
|
+
type: 'object',
|
|
852
|
+
properties: {
|
|
853
|
+
request: { type: 'string', description: 'User request or objective text.' },
|
|
854
|
+
fileCount: { type: 'number', description: 'Approximate number of files involved.' },
|
|
855
|
+
moduleCount: { type: 'number', description: 'Approximate number of modules involved.' },
|
|
856
|
+
introducesContract: { type: 'boolean', description: 'Whether the change introduces a new contract/dependency.' },
|
|
857
|
+
explicitDrive: { type: 'boolean', description: 'Whether drive/full-send mode is explicitly active.' },
|
|
858
|
+
approval: {
|
|
859
|
+
type: 'object',
|
|
860
|
+
description: 'Approval-boundary flags.',
|
|
861
|
+
properties: {
|
|
862
|
+
scopeChange: { type: 'boolean' },
|
|
863
|
+
productDecision: { type: 'boolean' },
|
|
864
|
+
riskAcceptance: { type: 'boolean' },
|
|
865
|
+
irreversibleAction: { type: 'boolean' },
|
|
866
|
+
blockedDependency: { type: 'boolean' },
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
name: 'list_skills',
|
|
874
|
+
description: 'Lists all available categories and playbooks in the Construct knowledge base.',
|
|
875
|
+
inputSchema: {
|
|
876
|
+
type: 'object',
|
|
877
|
+
properties: {},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: 'get_template',
|
|
882
|
+
description: 'Reads a doc template by name (e.g. "prd", "meta-prd", "prfaq", "evidence-brief", "adr", "runbook"). Resolves .cx/templates/docs/{name}.md first, then templates/docs/{name}.md.',
|
|
883
|
+
inputSchema: {
|
|
884
|
+
type: 'object',
|
|
885
|
+
properties: {
|
|
886
|
+
name: { type: 'string', description: 'Template name without .md extension' },
|
|
887
|
+
},
|
|
888
|
+
required: ['name'],
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
name: 'list_templates',
|
|
893
|
+
description: 'Lists shipped and project-override doc templates.',
|
|
894
|
+
inputSchema: { type: 'object', properties: {} },
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
name: 'search_skills',
|
|
898
|
+
description: 'Searches for a pattern within the Construct knowledge base skills.',
|
|
899
|
+
inputSchema: {
|
|
900
|
+
type: 'object',
|
|
901
|
+
properties: {
|
|
902
|
+
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
|
903
|
+
},
|
|
904
|
+
required: ['pattern'],
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: 'workflow_status',
|
|
909
|
+
description: 'Returns Construct .cx/workflow.json state and alignment findings.',
|
|
910
|
+
inputSchema: {
|
|
911
|
+
type: 'object',
|
|
912
|
+
properties: {
|
|
913
|
+
cwd: { type: 'string', description: 'Project directory (default: process.cwd()).' },
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: 'workflow_init',
|
|
919
|
+
description: 'Creates .cx/workflow.json if missing and returns the workflow state.',
|
|
920
|
+
inputSchema: {
|
|
921
|
+
type: 'object',
|
|
922
|
+
properties: {
|
|
923
|
+
cwd: { type: 'string', description: 'Project directory (default: process.cwd()).' },
|
|
924
|
+
title: { type: 'string', description: 'Workflow title.' },
|
|
925
|
+
spec_ref: { type: 'string', description: 'Path to the governing spec/plan document (e.g. .cx/plans/feature-x.md).' },
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
name: 'workflow_add_task',
|
|
931
|
+
description: 'Adds a Construct task packet to .cx/workflow.json.',
|
|
932
|
+
inputSchema: {
|
|
933
|
+
type: 'object',
|
|
934
|
+
properties: {
|
|
935
|
+
cwd: { type: 'string' },
|
|
936
|
+
key: { type: 'string' },
|
|
937
|
+
title: { type: 'string' },
|
|
938
|
+
phase: { type: 'string' },
|
|
939
|
+
owner: { type: 'string' },
|
|
940
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
941
|
+
readFirst: { type: 'array', items: { type: 'string' } },
|
|
942
|
+
doNotChange: { type: 'array', items: { type: 'string' } },
|
|
943
|
+
acceptanceCriteria: { type: 'array', items: { type: 'string' } },
|
|
944
|
+
verification: { type: 'array', items: { type: 'string' } },
|
|
945
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
946
|
+
overlays: { type: 'array', items: { type: 'string' } },
|
|
947
|
+
challengeRequired: { type: 'boolean' },
|
|
948
|
+
challengeStatus: { type: 'string' },
|
|
949
|
+
},
|
|
950
|
+
required: ['title', 'owner', 'acceptanceCriteria'],
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
name: 'workflow_update_task',
|
|
955
|
+
description: 'Updates a task status, owner, phase, note, or verification evidence in .cx/workflow.json.',
|
|
956
|
+
inputSchema: {
|
|
957
|
+
type: 'object',
|
|
958
|
+
properties: {
|
|
959
|
+
cwd: { type: 'string' },
|
|
960
|
+
key: { type: 'string' },
|
|
961
|
+
status: { type: 'string' },
|
|
962
|
+
owner: { type: 'string' },
|
|
963
|
+
phase: { type: 'string' },
|
|
964
|
+
note: { type: 'string' },
|
|
965
|
+
verification: { type: 'array', items: { type: 'string' } },
|
|
966
|
+
overlays: { type: 'array', items: { type: 'string' } },
|
|
967
|
+
challengeRequired: { type: 'boolean' },
|
|
968
|
+
challengeStatus: { type: 'string' },
|
|
969
|
+
},
|
|
970
|
+
required: ['key'],
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: 'workflow_needs_main_input',
|
|
975
|
+
description: 'Records a blocked_needs_user task and returns a NEEDS_MAIN_INPUT packet for the primary persona.',
|
|
976
|
+
inputSchema: {
|
|
977
|
+
type: 'object',
|
|
978
|
+
properties: {
|
|
979
|
+
cwd: { type: 'string' },
|
|
980
|
+
taskKey: { type: 'string' },
|
|
981
|
+
worker: { type: 'string' },
|
|
982
|
+
blocker: { type: 'string' },
|
|
983
|
+
question: { type: 'string' },
|
|
984
|
+
safeDefault: { type: 'string' },
|
|
985
|
+
context: { type: 'array', items: { type: 'string' } },
|
|
986
|
+
},
|
|
987
|
+
required: ['taskKey', 'worker', 'blocker', 'question'],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: 'workflow_validate',
|
|
992
|
+
description: 'Validates .cx/workflow.json schema and high-severity alignment findings.',
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: 'object',
|
|
995
|
+
properties: {
|
|
996
|
+
cwd: { type: 'string' },
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: 'workflow_import_plan',
|
|
1002
|
+
description: 'Parses a plan document (markdown) into structured workflow task packets and writes them to .cx/workflow.json. Supports rich ### T1 — Title sections with Owner/Phase/Files/Depends on/Read first/Do not change/Acceptance criteria fields.',
|
|
1003
|
+
inputSchema: {
|
|
1004
|
+
type: 'object',
|
|
1005
|
+
properties: {
|
|
1006
|
+
markdown: { type: 'string', description: 'The plan document markdown to parse into tasks' },
|
|
1007
|
+
cwd: { type: 'string', description: 'Project directory (default: cwd)' },
|
|
1008
|
+
title: { type: 'string', description: 'Workflow title if creating a new workflow' },
|
|
1009
|
+
spec_ref: { type: 'string', description: 'Path to the governing spec/plan document (e.g. .cx/plans/feature-x.md)' },
|
|
1010
|
+
phase: { type: 'string', description: 'Default phase for tasks without an explicit phase (default: implement)' },
|
|
1011
|
+
owner: { type: 'string', description: 'Default owner for tasks without an explicit owner (default: cx-engineer)' },
|
|
1012
|
+
readFirst: { type: 'array', items: { type: 'string' }, description: 'Default readFirst list applied to tasks without one' },
|
|
1013
|
+
doNotChange: { type: 'array', items: { type: 'string' }, description: 'Default doNotChange list applied to all tasks' },
|
|
1014
|
+
acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Extra acceptance criteria appended to each task' },
|
|
1015
|
+
},
|
|
1016
|
+
required: ['markdown'],
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: 'list_teams',
|
|
1021
|
+
description: 'Lists all available Construct team templates with members, focus, and promotion gates.',
|
|
1022
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: 'get_team',
|
|
1026
|
+
description: 'Returns the full definition of a named team template including members, skills, and promotion gates.',
|
|
1027
|
+
inputSchema: {
|
|
1028
|
+
type: 'object',
|
|
1029
|
+
properties: {
|
|
1030
|
+
name: { type: 'string', description: 'Team template name (e.g. feature, incident, architecture).' },
|
|
1031
|
+
},
|
|
1032
|
+
required: ['name'],
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: 'cx_trace',
|
|
1037
|
+
description: 'Records an agent trace in Langfuse for observability. Call at the start of every significant task with your agent name and the user goal.',
|
|
1038
|
+
inputSchema: {
|
|
1039
|
+
type: 'object',
|
|
1040
|
+
properties: {
|
|
1041
|
+
name: { type: 'string', description: 'Agent name (e.g. cx-engineer)' },
|
|
1042
|
+
id: { type: 'string', description: 'Optional trace UUID — auto-generated if omitted' },
|
|
1043
|
+
session_id: { type: 'string', description: 'Session ID to group related spans' },
|
|
1044
|
+
metadata: { type: 'object', description: 'Extra metadata (teamId, workflowId, etc.)' },
|
|
1045
|
+
input: { type: ['string', 'object'], description: 'Agent goal or user request' },
|
|
1046
|
+
output: { type: ['string', 'object'], description: 'Agent deliverable or response' },
|
|
1047
|
+
timestamp: { type: 'string', description: 'ISO start time (default: now)' },
|
|
1048
|
+
},
|
|
1049
|
+
required: ['name'],
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: 'cx_score',
|
|
1054
|
+
description: 'Attaches a quality score to a trace in Langfuse. Call after producing a significant deliverable.',
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
type: 'object',
|
|
1057
|
+
properties: {
|
|
1058
|
+
trace_id: { type: 'string', description: 'The trace ID returned by cx_trace' },
|
|
1059
|
+
name: { type: 'string', description: 'Score name — use "quality"' },
|
|
1060
|
+
value: { type: 'number', description: 'Score from 0.0 (poor) to 1.0 (excellent)' },
|
|
1061
|
+
comment: { type: 'string', description: 'Brief explanation of the score' },
|
|
1062
|
+
},
|
|
1063
|
+
required: ['trace_id', 'name', 'value'],
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: 'session_usage',
|
|
1068
|
+
description: 'Returns locally recorded interaction token and cost usage for the current Construct session.',
|
|
1069
|
+
inputSchema: {
|
|
1070
|
+
type: 'object',
|
|
1071
|
+
properties: {
|
|
1072
|
+
cwd: { type: 'string', description: 'Project directory (default: process.cwd()).' },
|
|
1073
|
+
home_dir: { type: 'string', description: 'Home directory override for reading ~/.cx session logs.' },
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
}));
|
|
1079
|
+
|
|
1080
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1081
|
+
const { name, arguments: args = {} } = request.params;
|
|
1082
|
+
|
|
1083
|
+
let result;
|
|
1084
|
+
try {
|
|
1085
|
+
if (name === 'agent_health') result = agentHealth(args);
|
|
1086
|
+
else if (name === 'summarize_diff') result = summarizeDiff(args);
|
|
1087
|
+
else if (name === 'scan_file') result = scanFile(args);
|
|
1088
|
+
else if (name === 'project_context') result = projectContext(args);
|
|
1089
|
+
else if (name === 'orchestration_policy') result = orchestrationPolicy(args);
|
|
1090
|
+
else if (name === 'list_skills') result = listSkills();
|
|
1091
|
+
else if (name === 'get_skill') result = getSkill(args);
|
|
1092
|
+
else if (name === 'search_skills') result = searchSkills(args);
|
|
1093
|
+
else if (name === 'get_template') result = getTemplate(args);
|
|
1094
|
+
else if (name === 'list_templates') result = listTemplates();
|
|
1095
|
+
else if (name === 'workflow_status') result = workflowStatus(args);
|
|
1096
|
+
else if (name === 'workflow_init') result = workflowInit(args);
|
|
1097
|
+
else if (name === 'workflow_add_task') result = workflowAddTask(args);
|
|
1098
|
+
else if (name === 'workflow_update_task') result = workflowUpdateTask(args);
|
|
1099
|
+
else if (name === 'workflow_needs_main_input') result = workflowNeedsMainInput(args);
|
|
1100
|
+
else if (name === 'workflow_validate') result = workflowValidate(args);
|
|
1101
|
+
else if (name === 'workflow_import_plan') result = workflowImportPlan(args);
|
|
1102
|
+
else if (name === 'list_teams') result = listTeams();
|
|
1103
|
+
else if (name === 'get_team') result = getTeam(args);
|
|
1104
|
+
else if (name === 'cx_trace') result = await cxTrace(args);
|
|
1105
|
+
else if (name === 'cx_score') result = await cxScore(args);
|
|
1106
|
+
else if (name === 'session_usage') result = await sessionUsage(args);
|
|
1107
|
+
else result = { error: `Unknown tool: ${name}` };
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
result = { error: err.message ?? String(err) };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1114
|
+
};
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
export { cxTrace, projectContext, workflowStatus };
|
|
1118
|
+
|
|
1119
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1120
|
+
console.error('[construct-mcp] server started');
|
|
1121
|
+
const transport = new StdioServerTransport();
|
|
1122
|
+
await server.connect(transport);
|
|
1123
|
+
}
|