@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,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/mcp-health-check.mjs — MCP health check hook — verifies MCP servers are reachable before tool use.
|
|
4
|
+
*
|
|
5
|
+
* Runs as PreToolUse on MCP tool calls. Checks that the target MCP server is running and reachable. Emits a warning (does not block) if the server is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const CACHE_PATH = `${homedir()}/.cx/mcp-health.json`;
|
|
11
|
+
const CACHE_TTL_MS = 60_000;
|
|
12
|
+
const markFailure = process.argv.includes('--mark-failure');
|
|
13
|
+
|
|
14
|
+
let input = {};
|
|
15
|
+
try {
|
|
16
|
+
const raw = readFileSync(0, 'utf8').trim();
|
|
17
|
+
if (raw) input = JSON.parse(raw);
|
|
18
|
+
} catch { /* no stdin or not JSON */ }
|
|
19
|
+
|
|
20
|
+
const toolName = input?.tool_name || input?.tool_input?.tool_name || '';
|
|
21
|
+
|
|
22
|
+
if (!toolName.startsWith('mcp__')) {
|
|
23
|
+
if (!markFailure) { process.stdout.write(JSON.stringify(input) + '\n'); }
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const serverSlug = toolName.split('__')[1] || '';
|
|
28
|
+
|
|
29
|
+
let cache = {};
|
|
30
|
+
try { cache = JSON.parse(readFileSync(CACHE_PATH, 'utf8')); } catch { /* fresh */ }
|
|
31
|
+
|
|
32
|
+
if (markFailure) {
|
|
33
|
+
const entry = cache[serverSlug] || { status: 'healthy', since: 0, failures: 0 };
|
|
34
|
+
entry.failures = (entry.failures || 0) + 1;
|
|
35
|
+
entry.status = 'unhealthy';
|
|
36
|
+
entry.since = Date.now();
|
|
37
|
+
cache[serverSlug] = entry;
|
|
38
|
+
try { writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2)); } catch { /* best effort */ }
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entry = cache[serverSlug];
|
|
43
|
+
if (entry?.status === 'unhealthy' && (Date.now() - entry.since) < CACHE_TTL_MS) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`[mcp-health] The ${serverSlug} connection is unavailable (failed ${entry.failures} time${entry.failures !== 1 ? 's' : ''} recently). Skipping this step.\n`
|
|
46
|
+
);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.stdout.write(JSON.stringify(input) + '\n');
|
|
51
|
+
process.exit(0);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/mcp-task-scope.mjs — MCP task scope hook — constrains MCP tool use to the active workflow task scope.
|
|
4
|
+
*
|
|
5
|
+
* Runs as PreToolUse on MCP tool calls. Reads the active workflow task and warns when MCP tools are used outside the declared task scope.
|
|
6
|
+
*/
|
|
7
|
+
// PreToolUse(mcp__*) — task-aware MCP scope advisor.
|
|
8
|
+
// Reads the active workflow task's mcpScope field.
|
|
9
|
+
// Warns (non-blocking) if calling an MCP not declared for the active task.
|
|
10
|
+
// Exceeds OmO's per-task scoped MCPs: adds attribution, guidance, and audit trail
|
|
11
|
+
// without requiring runtime MCP lifecycle management.
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
let input = {};
|
|
16
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
17
|
+
|
|
18
|
+
const cwd = input?.cwd || process.cwd();
|
|
19
|
+
const toolName = input?.tool_name || '';
|
|
20
|
+
|
|
21
|
+
const match = toolName.match(/^mcp__([^_]+(?:__[^_]+)*)__/);
|
|
22
|
+
if (!match) process.exit(0);
|
|
23
|
+
const mcpServer = match[1].replace(/__/g, '-');
|
|
24
|
+
|
|
25
|
+
const workflowPath = join(cwd, '.cx', 'workflow.json');
|
|
26
|
+
if (!existsSync(workflowPath)) process.exit(0);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
|
|
30
|
+
const activeTaskKey = wf.currentTaskKey;
|
|
31
|
+
if (!activeTaskKey) process.exit(0);
|
|
32
|
+
|
|
33
|
+
const task = (wf.tasks || []).find(t => t.key === activeTaskKey);
|
|
34
|
+
if (!task?.mcpScope || task.mcpScope.length === 0) process.exit(0);
|
|
35
|
+
|
|
36
|
+
const scope = task.mcpScope;
|
|
37
|
+
const inScope = scope.some(s => mcpServer.includes(s) || s.includes(mcpServer));
|
|
38
|
+
|
|
39
|
+
if (!inScope) {
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`[mcp-scope] ${mcpServer} is not in mcpScope for task "${activeTaskKey}".\n` +
|
|
42
|
+
`[mcp-scope] Declared scope: ${scope.join(', ')}. Proceeding — verify this call is intentional.\n`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
} catch { /* best effort */ }
|
|
46
|
+
|
|
47
|
+
process.exit(0);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/model-fallback.mjs — Model fallback hook — reroutes requests when the primary model is unavailable.
|
|
4
|
+
*
|
|
5
|
+
* Runs as PostToolUse when model calls fail. Reads the error, selects a fallback provider from the model router, and retries the request with the alternate model.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { readOpenRouterApiKeyFromOpenCodeConfig } from '../model-router.mjs';
|
|
12
|
+
|
|
13
|
+
const RATE_LIMIT_PATTERNS = [
|
|
14
|
+
/\b429\b/i,
|
|
15
|
+
/rate limit/i,
|
|
16
|
+
/usage limits?/i,
|
|
17
|
+
/specified API usage limits?/i,
|
|
18
|
+
/regain access/i,
|
|
19
|
+
/weekly limit/i,
|
|
20
|
+
/monthly limit/i,
|
|
21
|
+
/daily limit/i,
|
|
22
|
+
/too many requests/i,
|
|
23
|
+
/quota exceeded/i,
|
|
24
|
+
/model unavailable/i,
|
|
25
|
+
/model.*overloaded/i,
|
|
26
|
+
/timeout/i,
|
|
27
|
+
/timed out/i,
|
|
28
|
+
/ETIMEDOUT/i,
|
|
29
|
+
/ECONNRESET/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const COOLDOWN_MS = 10 * 60 * 1000;
|
|
33
|
+
const statePath = join(homedir(), '.cx', 'construct-model-fallback.json');
|
|
34
|
+
|
|
35
|
+
function readInput() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(0, 'utf8');
|
|
38
|
+
return raw ? JSON.parse(raw) : {};
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function flatten(value) {
|
|
45
|
+
if (value === null || value === undefined) return '';
|
|
46
|
+
if (typeof value === 'string') return value;
|
|
47
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
48
|
+
if (Array.isArray(value)) return value.map(flatten).join('\n');
|
|
49
|
+
if (typeof value === 'object') return Object.values(value).map(flatten).join('\n');
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recentlyApplied(now) {
|
|
54
|
+
try {
|
|
55
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
56
|
+
return typeof state.lastAppliedAt === 'number' && now - state.lastAppliedAt < COOLDOWN_MS;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeState(payload) {
|
|
63
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
64
|
+
writeFileSync(statePath, JSON.stringify(payload, null, 2));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const input = readInput();
|
|
68
|
+
const haystack = flatten(input);
|
|
69
|
+
if (!RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(haystack))) process.exit(0);
|
|
70
|
+
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (recentlyApplied(now)) {
|
|
73
|
+
process.stderr.write('[model-fallback] Rate limit/timeout detected; fallback recently applied, skipping cooldown window.\n');
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const toolkitDir = process.env.CX_TOOLKIT_DIR || join(homedir(), '.construct');
|
|
78
|
+
const constructBin = join(toolkitDir, 'bin', 'construct');
|
|
79
|
+
|
|
80
|
+
if (!existsSync(constructBin)) {
|
|
81
|
+
process.stderr.write(`[model-fallback] construct binary not found at ${constructBin}\n`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const openRouterApiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
86
|
+
|
|
87
|
+
if (!openRouterApiKey) {
|
|
88
|
+
process.stderr.write('[model-fallback] Rate limit/timeout detected; no OpenRouter API key found in OPENROUTER_API_KEY or OpenCode config, so automatic fallback cannot poll models.\n');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
process.stderr.write('[model-fallback] Rate limit/timeout detected. Running construct models --apply.\n');
|
|
94
|
+
execFileSync(constructBin, ['models', '--apply'], {
|
|
95
|
+
cwd: toolkitDir,
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir, OPENROUTER_API_KEY: openRouterApiKey },
|
|
98
|
+
timeout: 120_000,
|
|
99
|
+
});
|
|
100
|
+
writeState({ lastAppliedAt: now, reason: 'rate-limit-or-timeout' });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
process.stderr.write(`[model-fallback] Fallback failed: ${error.message}\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.exit(0);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/pre-compact.mjs — Pre-compact hook — prepares context summary before compaction runs.
|
|
4
|
+
*
|
|
5
|
+
* Runs before context compaction. Writes a structured summary of active workflow, context state, and pending tasks to preserve continuity across the compaction boundary.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { writeContextState } from '../context-state.mjs';
|
|
11
|
+
|
|
12
|
+
let input = {};
|
|
13
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
14
|
+
|
|
15
|
+
const cwd = input?.cwd || process.cwd();
|
|
16
|
+
const transcriptPath = input?.transcript_path || '';
|
|
17
|
+
|
|
18
|
+
const filesChanged = [];
|
|
19
|
+
const decisions = [];
|
|
20
|
+
const pendingTodos = [];
|
|
21
|
+
const workflowTasks = [];
|
|
22
|
+
const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
|
|
23
|
+
const efficiencyPath = join(homedir(), '.cx', 'session-efficiency.json');
|
|
24
|
+
let lastSummary = '';
|
|
25
|
+
|
|
26
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const transcriptRaw = readFileSync(transcriptPath, 'utf8');
|
|
29
|
+
let transcript;
|
|
30
|
+
try {
|
|
31
|
+
transcript = JSON.parse(transcriptRaw);
|
|
32
|
+
} catch {
|
|
33
|
+
transcript = transcriptRaw
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((line) => {
|
|
37
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
38
|
+
})
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
const messages = Array.isArray(transcript) ? transcript : (transcript.messages || []);
|
|
42
|
+
const recent = messages.slice(-60);
|
|
43
|
+
|
|
44
|
+
// Track the latest TodoWrite call to get current todo state
|
|
45
|
+
let latestTodos = null;
|
|
46
|
+
|
|
47
|
+
for (const msg of recent) {
|
|
48
|
+
if (msg.role === 'tool' || msg.type === 'tool_result') continue;
|
|
49
|
+
|
|
50
|
+
// Handle both array-of-blocks and plain-string content formats
|
|
51
|
+
const blocks = Array.isArray(msg.content)
|
|
52
|
+
? msg.content
|
|
53
|
+
: (typeof msg.content === 'string' ? [{ type: 'text', text: msg.content }] : []);
|
|
54
|
+
|
|
55
|
+
for (const block of blocks) {
|
|
56
|
+
if (block.type === 'tool_use' && (block.name === 'Write' || block.name === 'Edit')) {
|
|
57
|
+
const fp = block.input?.file_path || block.input?.path || '';
|
|
58
|
+
if (fp && !filesChanged.includes(fp)) filesChanged.push(fp);
|
|
59
|
+
}
|
|
60
|
+
if (block.type === 'tool_use' && block.name === 'TodoWrite') {
|
|
61
|
+
const todos = block.input?.todos;
|
|
62
|
+
if (Array.isArray(todos)) latestTodos = todos;
|
|
63
|
+
}
|
|
64
|
+
if (block.type === 'text' && msg.role === 'assistant') {
|
|
65
|
+
const text = (block.text || '').trim();
|
|
66
|
+
// Prefer the last substantive response (>80 chars) over short one-liners
|
|
67
|
+
if (text.length > 80 || (!lastSummary && text.length > 0)) {
|
|
68
|
+
lastSummary = text.slice(0, 400);
|
|
69
|
+
}
|
|
70
|
+
const decisionMatches = text.match(/(?:^|\n)(?:DECISION:|Decision:|\*\*Decision\*\*:)\s*([^\n]+)/g) || [];
|
|
71
|
+
for (const match of decisionMatches) {
|
|
72
|
+
const cleaned = match.replace(/^\n?/, '').replace(/^(?:DECISION:|Decision:|\*\*Decision\*\*:)\s*/, '').trim();
|
|
73
|
+
if (cleaned && !decisions.includes(cleaned)) decisions.push(cleaned);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract pending/in_progress todos from latest TodoWrite
|
|
80
|
+
if (latestTodos) {
|
|
81
|
+
for (const todo of latestTodos) {
|
|
82
|
+
if (todo.status === 'pending' || todo.status === 'in_progress') {
|
|
83
|
+
pendingTodos.push(todo);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract active workflow tasks from .cx/workflow.json
|
|
89
|
+
const workflowPath = join(cwd, '.cx', 'workflow.json');
|
|
90
|
+
if (existsSync(workflowPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
|
|
93
|
+
const tasks = wf?.tasks || wf?.state?.tasks || [];
|
|
94
|
+
for (const task of tasks) {
|
|
95
|
+
if (task.status !== 'done' && task.status !== 'skipped') {
|
|
96
|
+
workflowTasks.push({ key: task.key || task.id, title: task.title || task.description || '' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch { /* best effort */ }
|
|
100
|
+
}
|
|
101
|
+
} catch { /* best effort */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const warnFlags = (() => {
|
|
105
|
+
try { return readFileSync(warnFlagsPath, 'utf8').trim(); } catch { return ''; }
|
|
106
|
+
})();
|
|
107
|
+
|
|
108
|
+
const efficiencySummary = (() => {
|
|
109
|
+
try {
|
|
110
|
+
const stats = JSON.parse(readFileSync(efficiencyPath, 'utf8'));
|
|
111
|
+
const parts = [
|
|
112
|
+
`${stats.readCount || 0} reads`,
|
|
113
|
+
`${stats.uniqueFileCount || 0} unique files`,
|
|
114
|
+
];
|
|
115
|
+
if (stats.repeatedReadCount) parts.push(`${stats.repeatedReadCount} repeated reads`);
|
|
116
|
+
if (stats.largeReadCount) parts.push(`${stats.largeReadCount} large reads`);
|
|
117
|
+
if (stats.totalBytesRead) parts.push(`${Math.round(stats.totalBytesRead / 1024)} KB read`);
|
|
118
|
+
return parts.join(' · ');
|
|
119
|
+
} catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
|
|
124
|
+
const projectName = cwd.split('/').pop() || 'project';
|
|
125
|
+
const date = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
126
|
+
|
|
127
|
+
const contextLines = [
|
|
128
|
+
`# Session Context`,
|
|
129
|
+
`Last saved: ${date}`,
|
|
130
|
+
``,
|
|
131
|
+
`## What was in progress`,
|
|
132
|
+
lastSummary || '(no summary available)',
|
|
133
|
+
``,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
if (filesChanged.length) {
|
|
137
|
+
contextLines.push('## Files changed this session');
|
|
138
|
+
for (const f of filesChanged.slice(0, 20)) contextLines.push(`- ${f}`);
|
|
139
|
+
contextLines.push('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (decisions.length) {
|
|
143
|
+
contextLines.push('## Decisions captured');
|
|
144
|
+
for (const decision of decisions.slice(-10)) contextLines.push(`- ${decision}`);
|
|
145
|
+
contextLines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (pendingTodos.length) {
|
|
149
|
+
contextLines.push('## Pending TODOs');
|
|
150
|
+
for (const todo of pendingTodos) {
|
|
151
|
+
const status = todo.status === 'in_progress' ? '[~]' : '[ ]';
|
|
152
|
+
contextLines.push(`${status} ${todo.content || todo.title || todo.text || JSON.stringify(todo)}`);
|
|
153
|
+
}
|
|
154
|
+
contextLines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (workflowTasks.length) {
|
|
158
|
+
contextLines.push('## Active workflow tasks');
|
|
159
|
+
for (const task of workflowTasks.slice(0, 10)) {
|
|
160
|
+
contextLines.push(`- ${task.key}: ${task.title}`);
|
|
161
|
+
}
|
|
162
|
+
contextLines.push('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (efficiencySummary) {
|
|
166
|
+
contextLines.push('## Session efficiency snapshot');
|
|
167
|
+
contextLines.push(efficiencySummary);
|
|
168
|
+
contextLines.push('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
contextLines.push('## Open issues');
|
|
172
|
+
contextLines.push(warnFlags || 'None');
|
|
173
|
+
|
|
174
|
+
const content = contextLines.join('\n') + '\n';
|
|
175
|
+
const contextJson = {
|
|
176
|
+
source: 'pre-compact',
|
|
177
|
+
projectName,
|
|
178
|
+
lastSummary,
|
|
179
|
+
filesChanged: filesChanged.slice(0, 20),
|
|
180
|
+
decisions: decisions.slice(-10),
|
|
181
|
+
pendingTodos: pendingTodos.slice(0, 20),
|
|
182
|
+
workflowTasks: workflowTasks.slice(0, 10),
|
|
183
|
+
efficiencySummary,
|
|
184
|
+
warnFlags: warnFlags || 'None',
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const projectCxDir = join(cwd, '.cx');
|
|
188
|
+
try { mkdirSync(projectCxDir, { recursive: true }); } catch { /* exists */ }
|
|
189
|
+
try { writeContextState(cwd, { ...contextJson, contextSummary: lastSummary }, { markdown: content }); } catch { /* best effort */ }
|
|
190
|
+
if (decisions.length) {
|
|
191
|
+
const decisionsDir = join(projectCxDir, 'decisions');
|
|
192
|
+
const slug = `${date.replace(/[^0-9]/g, '')}-session-decisions`;
|
|
193
|
+
const decisionDoc = [
|
|
194
|
+
'# Session Decisions',
|
|
195
|
+
`Captured: ${date}`,
|
|
196
|
+
'',
|
|
197
|
+
'## Decisions',
|
|
198
|
+
...decisions.slice(-10).map((decision) => `- ${decision}`),
|
|
199
|
+
'',
|
|
200
|
+
'## Files changed',
|
|
201
|
+
...(filesChanged.length ? filesChanged.slice(0, 20).map((file) => `- ${file}`) : ['- None captured']),
|
|
202
|
+
'',
|
|
203
|
+
].join('\n');
|
|
204
|
+
try { mkdirSync(decisionsDir, { recursive: true }); } catch { /* exists */ }
|
|
205
|
+
try { writeFileSync(join(decisionsDir, `${slug}.md`), decisionDoc); } catch { /* best effort */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const globalCxDir = join(homedir(), '.cx');
|
|
209
|
+
try { mkdirSync(globalCxDir, { recursive: true }); } catch { /* exists */ }
|
|
210
|
+
try { writeContextState(homedir(), { ...contextJson, contextSummary: lastSummary }, { markdown: content }); } catch { /* best effort */ }
|
|
211
|
+
|
|
212
|
+
process.exit(0);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-push-gate.mjs — PreToolUse / Bash
|
|
4
|
+
*
|
|
5
|
+
* Intercepts `git push` commands. Detects available test and build scripts
|
|
6
|
+
* in the project, runs them in parallel, and blocks the push if either fails.
|
|
7
|
+
* Silent and instant when there's nothing to run.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { execSync, spawnSync } from 'child_process';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
|
|
13
|
+
let input = {};
|
|
14
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { /* no stdin */ }
|
|
15
|
+
|
|
16
|
+
// Must always echo stdin for PreToolUse chaining
|
|
17
|
+
const echo = () => { process.stdout.write(JSON.stringify(input) + '\n'); process.exit(0); };
|
|
18
|
+
|
|
19
|
+
const command = input?.tool_input?.command || input?.command || '';
|
|
20
|
+
|
|
21
|
+
// Only act on git push
|
|
22
|
+
if (!/\bgit\s+push\b/.test(command)) { echo(); }
|
|
23
|
+
|
|
24
|
+
// Find nearest package.json / Cargo.toml / pyproject.toml from cwd
|
|
25
|
+
const cwd = input?.cwd || process.cwd();
|
|
26
|
+
|
|
27
|
+
function findUp(filename, from) {
|
|
28
|
+
let dir = from;
|
|
29
|
+
for (let i = 0; i < 8; i++) {
|
|
30
|
+
const candidate = join(dir, filename);
|
|
31
|
+
if (existsSync(candidate)) return { path: candidate, dir };
|
|
32
|
+
const parent = dirname(dir);
|
|
33
|
+
if (parent === dir) break;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Node/npm project ──────────────────────────────────────────────────────────
|
|
40
|
+
function runNpmGate(projectDir) {
|
|
41
|
+
let pkg;
|
|
42
|
+
try { pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')); } catch { return null; }
|
|
43
|
+
|
|
44
|
+
const scripts = pkg.scripts || {};
|
|
45
|
+
const hasTest = !!(scripts.test && !scripts.test.includes('no test specified'));
|
|
46
|
+
const hasBuild = !!(scripts.build);
|
|
47
|
+
const hasCi = !!(scripts['test:ci'] || scripts['test:run']);
|
|
48
|
+
|
|
49
|
+
const testScript = scripts['test:ci'] ? 'test:ci' : scripts['test:run'] ? 'test:run' : 'test';
|
|
50
|
+
const runner = existsSync(join(projectDir, 'pnpm-lock.yaml')) ? 'pnpm'
|
|
51
|
+
: existsSync(join(projectDir, 'yarn.lock')) ? 'yarn'
|
|
52
|
+
: 'npm';
|
|
53
|
+
|
|
54
|
+
const jobs = [];
|
|
55
|
+
if (hasTest || hasCi) jobs.push({ label: 'tests', cmd: runner, args: ['run', testScript], timeout: 90_000 });
|
|
56
|
+
if (hasBuild) jobs.push({ label: 'build', cmd: runner, args: ['run', 'build'], timeout: 120_000 });
|
|
57
|
+
|
|
58
|
+
return { projectDir, jobs };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Rust/Cargo project ────────────────────────────────────────────────────────
|
|
62
|
+
function runCargoGate(projectDir) {
|
|
63
|
+
const jobs = [
|
|
64
|
+
{ label: 'tests', cmd: 'cargo', args: ['test', '--quiet'], timeout: 120_000 },
|
|
65
|
+
{ label: 'build', cmd: 'cargo', args: ['build', '--quiet'], timeout: 120_000 },
|
|
66
|
+
];
|
|
67
|
+
return { projectDir, jobs };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Python project ────────────────────────────────────────────────────────────
|
|
71
|
+
function runPythonGate(projectDir) {
|
|
72
|
+
const hasPytest = (() => { try { execSync('pytest --version', { stdio: 'pipe', timeout: 3000 }); return true; } catch { return false; } })();
|
|
73
|
+
if (!hasPytest) return null;
|
|
74
|
+
const jobs = [{ label: 'tests', cmd: 'pytest', args: ['--tb=short', '-q'], timeout: 90_000 }];
|
|
75
|
+
return { projectDir, jobs };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Detect project type
|
|
79
|
+
let gate = null;
|
|
80
|
+
const npm = findUp('package.json', cwd);
|
|
81
|
+
const cargo = findUp('Cargo.toml', cwd);
|
|
82
|
+
const pyproj = findUp('pyproject.toml', cwd) || findUp('setup.py', cwd);
|
|
83
|
+
|
|
84
|
+
if (npm) gate = runNpmGate(npm.dir);
|
|
85
|
+
else if (cargo) gate = runCargoGate(cargo.dir);
|
|
86
|
+
else if (pyproj) gate = runPythonGate(pyproj.dir);
|
|
87
|
+
|
|
88
|
+
// Nothing to check
|
|
89
|
+
if (!gate || gate.jobs.length === 0) { echo(); }
|
|
90
|
+
|
|
91
|
+
// Run jobs in parallel via Promise.all simulation (spawnSync is synchronous — run sequentially)
|
|
92
|
+
// For true parallel, we use spawnSync with a short timeout and report errors
|
|
93
|
+
const failures = [];
|
|
94
|
+
|
|
95
|
+
for (const job of gate.jobs) {
|
|
96
|
+
const result = spawnSync(job.cmd, job.args, {
|
|
97
|
+
cwd: gate.projectDir,
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
timeout: job.timeout,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result.status !== 0 || result.error) {
|
|
105
|
+
const stderr = (result.stderr || '').trim();
|
|
106
|
+
const stdout = (result.stdout || '').trim();
|
|
107
|
+
const detail = stderr || stdout;
|
|
108
|
+
// Extract first meaningful error line (skip blank, generic lines)
|
|
109
|
+
const firstError = detail.split('\n')
|
|
110
|
+
.map(l => l.trim())
|
|
111
|
+
.filter(l => l && !/^>/.test(l) && !/^\s*at /.test(l))
|
|
112
|
+
.slice(0, 3)
|
|
113
|
+
.join(' · ');
|
|
114
|
+
|
|
115
|
+
failures.push({ label: job.label, detail: firstError || `exited ${result.status}` });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (failures.length === 0) { echo(); }
|
|
120
|
+
|
|
121
|
+
// Block the push
|
|
122
|
+
const summary = failures
|
|
123
|
+
.map(f => `${f.label} failed — ${f.detail}`)
|
|
124
|
+
.join('\n');
|
|
125
|
+
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
`[pre-push-gate] Push blocked — fix these before pushing:\n${summary}\n\nRun the failing checks locally, then push again.\n`
|
|
128
|
+
);
|
|
129
|
+
process.exit(2);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/read-tracker.mjs — Read tracker hook — tracks file reads for efficiency analysis.
|
|
4
|
+
*
|
|
5
|
+
* Runs as PostToolUse after Read tool calls. Logs each read to ~/.cx/read-log.json including file path, line count, and session timestamp for efficiency reporting.
|
|
6
|
+
*/
|
|
7
|
+
// PostToolUse(Read) — records file content hash after each Read call
|
|
8
|
+
// Powers hash-staleness check in edit-guard.mjs
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
const CX_DIR = join(homedir(), '.cx');
|
|
15
|
+
const HASH_STORE = join(CX_DIR, 'file-hashes.json');
|
|
16
|
+
const EFFICIENCY_STORE = join(CX_DIR, 'session-efficiency.json');
|
|
17
|
+
const WARN_FLAGS = join(CX_DIR, 'warn-flags.txt');
|
|
18
|
+
const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
|
|
19
|
+
const REPEATED_READ_WARNING_THRESHOLD = 5;
|
|
20
|
+
const LARGE_READ_WARNING_THRESHOLD = 3;
|
|
21
|
+
const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
|
|
22
|
+
|
|
23
|
+
function loadEfficiencyStats(nowIso) {
|
|
24
|
+
const fresh = {
|
|
25
|
+
sessionStartedAt: nowIso,
|
|
26
|
+
lastUpdatedAt: nowIso,
|
|
27
|
+
readCount: 0,
|
|
28
|
+
uniqueFileCount: 0,
|
|
29
|
+
repeatedReadCount: 0,
|
|
30
|
+
largeReadCount: 0,
|
|
31
|
+
totalBytesRead: 0,
|
|
32
|
+
warnings: {},
|
|
33
|
+
files: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const existing = JSON.parse(readFileSync(EFFICIENCY_STORE, 'utf8'));
|
|
38
|
+
const lastUpdated = new Date(existing.lastUpdatedAt || 0).getTime();
|
|
39
|
+
if (!lastUpdated || Date.now() - lastUpdated > SESSION_IDLE_RESET_MS) return fresh;
|
|
40
|
+
return { ...fresh, ...existing, warnings: existing.warnings || {}, files: existing.files || {} };
|
|
41
|
+
} catch {
|
|
42
|
+
return fresh;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendWarning(message) {
|
|
47
|
+
try { appendFileSync(WARN_FLAGS, `${message}\n`); } catch { /* best effort */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function topRepeatedPath(files) {
|
|
51
|
+
return Object.entries(files || {})
|
|
52
|
+
.map(([filePath, value]) => ({ filePath, count: Number(value?.count || 0) }))
|
|
53
|
+
.filter((entry) => entry.count > 1)
|
|
54
|
+
.sort((a, b) => b.count - a.count || a.filePath.localeCompare(b.filePath))[0];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let input = {};
|
|
58
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
59
|
+
|
|
60
|
+
if ((input?.tool_name || '') !== 'Read') process.exit(0);
|
|
61
|
+
|
|
62
|
+
const rawPath = input?.tool_input?.file_path || '';
|
|
63
|
+
if (!rawPath) process.exit(0);
|
|
64
|
+
|
|
65
|
+
const absPath = rawPath.startsWith('/') ? rawPath : resolve(input?.cwd || process.cwd(), rawPath);
|
|
66
|
+
if (!existsSync(absPath)) process.exit(0);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(absPath, 'utf8');
|
|
70
|
+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
71
|
+
const nowIso = new Date().toISOString();
|
|
72
|
+
const requestedLimit = Number(input?.tool_input?.limit || 0);
|
|
73
|
+
const effectiveLimit = requestedLimit > 0 ? requestedLimit : 2000;
|
|
74
|
+
const isLargeRead = effectiveLimit > 400;
|
|
75
|
+
|
|
76
|
+
mkdirSync(CX_DIR, { recursive: true });
|
|
77
|
+
let store = {};
|
|
78
|
+
try { store = JSON.parse(readFileSync(HASH_STORE, 'utf8')); } catch { /* fresh */ }
|
|
79
|
+
|
|
80
|
+
store[absPath] = { hash, ts: nowIso, size: content.length };
|
|
81
|
+
|
|
82
|
+
// Prune entries older than 2 hours
|
|
83
|
+
const cutoff = Date.now() - 2 * 60 * 60 * 1000;
|
|
84
|
+
for (const [k, v] of Object.entries(store)) {
|
|
85
|
+
if (new Date(v.ts).getTime() < cutoff) delete store[k];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
89
|
+
|
|
90
|
+
const stats = loadEfficiencyStats(nowIso);
|
|
91
|
+
const existingFile = stats.files[absPath];
|
|
92
|
+
stats.readCount += 1;
|
|
93
|
+
stats.totalBytesRead += content.length;
|
|
94
|
+
if (isLargeRead) stats.largeReadCount += 1;
|
|
95
|
+
if (existingFile) {
|
|
96
|
+
stats.repeatedReadCount += 1;
|
|
97
|
+
} else {
|
|
98
|
+
stats.uniqueFileCount += 1;
|
|
99
|
+
}
|
|
100
|
+
stats.files[absPath] = {
|
|
101
|
+
count: (existingFile?.count || 0) + 1,
|
|
102
|
+
size: content.length,
|
|
103
|
+
lastReadAt: nowIso,
|
|
104
|
+
lastRequestedLimit: effectiveLimit,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (stats.repeatedReadCount >= REPEATED_READ_WARNING_THRESHOLD && !stats.warnings.repeatedReads) {
|
|
108
|
+
const top = topRepeatedPath(stats.files);
|
|
109
|
+
const topNote = top ? ` Top repeat: ${top.filePath} (${top.count}x).` : '';
|
|
110
|
+
appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session.${topNote} Use rg or construct distill before re-reading more files.`);
|
|
111
|
+
stats.warnings.repeatedReads = nowIso;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (stats.largeReadCount >= LARGE_READ_WARNING_THRESHOLD && !stats.warnings.largeReads) {
|
|
115
|
+
appendWarning(`Efficiency: ${stats.largeReadCount} large reads this session — prefer rg/glob plus targeted reads under 400 lines.`);
|
|
116
|
+
stats.warnings.largeReads = nowIso;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stats.totalBytesRead >= TOTAL_BYTES_WARNING_THRESHOLD && !stats.warnings.totalBytes) {
|
|
120
|
+
appendWarning(`Efficiency: ${Math.round(stats.totalBytesRead / 1024)} KB read this session — consider distill/query-focused retrieval or compact context before continuing.`);
|
|
121
|
+
stats.warnings.totalBytes = nowIso;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stats.lastUpdatedAt = nowIso;
|
|
125
|
+
|
|
126
|
+
writeFileSync(EFFICIENCY_STORE, JSON.stringify(stats, null, 2));
|
|
127
|
+
} catch { /* best effort */ }
|
|
128
|
+
|
|
129
|
+
process.exit(0);
|