@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* readme-age-check.mjs — Stop hook (async)
|
|
4
|
+
*
|
|
5
|
+
* On session end, scans tracked READMEs whose last commit is older than 90
|
|
6
|
+
* days and emits `readme.stale` for cx-docs-keeper. Rate-limited via
|
|
7
|
+
* ~/.cx/readme-age-state.json so the same README isn't reported more than
|
|
8
|
+
* once per week.
|
|
9
|
+
*
|
|
10
|
+
* @p95ms 1500
|
|
11
|
+
* @maxBlockingScope none (async, non-blocking)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
19
|
+
|
|
20
|
+
const STATE_PATH = join(homedir(), '.cx', 'readme-age-state.json');
|
|
21
|
+
const STALE_DAYS = 90;
|
|
22
|
+
const SUPPRESS_DAYS = 7;
|
|
23
|
+
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
|
|
26
|
+
function listReadmes() {
|
|
27
|
+
try {
|
|
28
|
+
const out = execSync(
|
|
29
|
+
"git ls-files | grep -E '(^|/)README\\.md$' | head -50",
|
|
30
|
+
{ cwd, timeout: 2000, shell: '/bin/sh' }
|
|
31
|
+
).toString();
|
|
32
|
+
return out.split('\n').filter(Boolean);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lastCommitTs(file) {
|
|
39
|
+
try {
|
|
40
|
+
const out = execSync(`git log -1 --format=%at -- "${file}"`, { cwd, timeout: 2000 }).toString().trim();
|
|
41
|
+
return parseInt(out, 10) * 1000;
|
|
42
|
+
} catch {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadState() {
|
|
48
|
+
try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')); } catch { return {}; }
|
|
49
|
+
}
|
|
50
|
+
function saveState(state) {
|
|
51
|
+
try { writeFileSync(STATE_PATH, JSON.stringify(state)); } catch { /* best effort */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readmes = listReadmes();
|
|
55
|
+
if (readmes.length === 0) process.exit(0);
|
|
56
|
+
|
|
57
|
+
const state = loadState();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const staleThreshold = now - STALE_DAYS * 24 * 60 * 60 * 1000;
|
|
60
|
+
const suppressUntil = now - SUPPRESS_DAYS * 24 * 60 * 60 * 1000;
|
|
61
|
+
|
|
62
|
+
for (const file of readmes) {
|
|
63
|
+
const ts = lastCommitTs(file);
|
|
64
|
+
if (!ts || ts >= staleThreshold) continue;
|
|
65
|
+
const prev = state[file] || 0;
|
|
66
|
+
if (prev > suppressUntil) continue;
|
|
67
|
+
emitRoleEvent({
|
|
68
|
+
type: 'readme.stale',
|
|
69
|
+
summary: `${file} last updated ${Math.round((now - ts) / (24 * 60 * 60 * 1000))} days ago`,
|
|
70
|
+
hookInput: { cwd },
|
|
71
|
+
context: { file, lastCommitTs: ts },
|
|
72
|
+
});
|
|
73
|
+
state[file] = now;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
saveState(state);
|
|
77
|
+
process.exit(0);
|
|
@@ -3,21 +3,28 @@
|
|
|
3
3
|
* lib/hooks/registry-sync.mjs — Registry sync hook — reminds to run construct sync after registry changes.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PostToolUse after edits to agents/registry.json. Checks if the file was modified and emits a reminder to run construct sync to regenerate platform adapters.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 12000
|
|
8
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
9
|
*/
|
|
7
10
|
import { execSync } from 'child_process';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
13
|
+
import { constructDir } from '../paths.mjs';
|
|
8
14
|
|
|
9
15
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
10
|
-
if (!filePath.endsWith('/agents/registry.json')) process.exit(0);
|
|
16
|
+
if (!filePath.endsWith(path.join('agents', 'registry.json')) && !filePath.endsWith('/agents/registry.json')) process.exit(0);
|
|
11
17
|
|
|
12
|
-
const toolkitDir =
|
|
18
|
+
const toolkitDir = constructDir();
|
|
13
19
|
try {
|
|
14
|
-
execSync('node sync-agents.mjs', {
|
|
20
|
+
execSync('node scripts/sync-agents.mjs', {
|
|
15
21
|
cwd: toolkitDir,
|
|
16
22
|
stdio: 'pipe',
|
|
17
23
|
env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir },
|
|
18
24
|
timeout: 12_000,
|
|
19
25
|
});
|
|
20
|
-
} catch (
|
|
21
|
-
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logHookFailure({ hook: 'registry-sync', err, phase: 'execute', input: { toolkitDir } });
|
|
28
|
+
process.stderr.write(`[registry-sync] Sync failed: ${err.message}\n`);
|
|
22
29
|
}
|
|
23
30
|
process.exit(0);
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* lib/hooks/scan-secrets.mjs — Scan secrets hook — detects potential secrets in files before they are committed.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PostToolUse after Edit/Write. Scans the modified file content against known secret patterns (API keys, tokens, passwords) and blocks (exit 2) on matches.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 30
|
|
8
|
+
* @maxBlockingScope PostToolUse
|
|
6
9
|
*/
|
|
7
10
|
import { readFileSync } from 'fs';
|
|
8
11
|
import { extname } from 'path';
|
|
12
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
9
13
|
|
|
10
14
|
const SCAN_EXTENSIONS = new Set([
|
|
11
15
|
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
@@ -23,15 +27,42 @@ const PLACEHOLDER_PATTERNS = [
|
|
|
23
27
|
/__[A-Z_]+__/,
|
|
24
28
|
];
|
|
25
29
|
|
|
30
|
+
// Patterns are matched per-line so multiple findings in one file are
|
|
31
|
+
// surfaced together. Capture group 1 is included in the JSONL log if
|
|
32
|
+
// present; otherwise the full match is used.
|
|
26
33
|
const SECRET_PATTERNS = [
|
|
27
34
|
{ name: 'Anthropic API key', pattern: /ANTHROPIC_API_KEY\s*=\s*(sk-ant-[a-zA-Z0-9\-_]{20,})/i },
|
|
35
|
+
{ name: 'Anthropic admin key', pattern: /(sk-ant-admin-[a-zA-Z0-9\-_]{20,})/ },
|
|
28
36
|
{ name: 'OpenAI API key', pattern: /OPENAI_API_KEY\s*=\s*(sk-[a-zA-Z0-9]{40,})/i },
|
|
37
|
+
{ name: 'OpenAI project key', pattern: /(sk-proj-[a-zA-Z0-9_\-]{40,})/ },
|
|
38
|
+
{ name: 'OpenAI organization key', pattern: /(sk-svcacct-[a-zA-Z0-9_\-]{40,})/ },
|
|
29
39
|
{ name: 'OpenRouter key', pattern: /(sk-or-v1-[a-zA-Z0-9]{40,})/ },
|
|
30
40
|
{ name: 'AWS access key', pattern: /(AKIA[0-9A-Z]{16})/ },
|
|
31
|
-
{ name: '
|
|
32
|
-
{ name: '
|
|
41
|
+
{ name: 'AWS secret access key', pattern: /aws[_\-]?secret[_\-]?access[_\-]?key\s*[:=]\s*['"]?([A-Za-z0-9\/\+=]{40})/i },
|
|
42
|
+
{ name: 'AWS session token', pattern: /(ASIA[0-9A-Z]{16})/ },
|
|
43
|
+
{ name: 'GCP service account JSON key', pattern: /"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----/ },
|
|
44
|
+
{ name: 'GCP API key', pattern: /(AIza[0-9A-Za-z\-_]{35})/ },
|
|
45
|
+
{ name: 'Azure storage account key', pattern: /AccountKey\s*=\s*([A-Za-z0-9\+\/=]{88})/ },
|
|
46
|
+
{ name: 'Private key (PEM)', pattern: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
47
|
+
{ name: 'SSH private key', pattern: /-----BEGIN OPENSSH PRIVATE KEY-----/ },
|
|
48
|
+
{ name: 'GitHub personal access token (classic)', pattern: /(ghp_[a-zA-Z0-9]{36})/ },
|
|
49
|
+
{ name: 'GitHub fine-grained PAT', pattern: /(github_pat_[A-Z0-9]{22}_[A-Za-z0-9]{59})/ },
|
|
33
50
|
{ name: 'GitHub Actions token', pattern: /(ghs_[a-zA-Z0-9]{36})/ },
|
|
34
|
-
{ name: '
|
|
51
|
+
{ name: 'GitHub OAuth token', pattern: /(gho_[a-zA-Z0-9]{36})/ },
|
|
52
|
+
{ name: 'GitHub user-to-server token', pattern: /(ghu_[a-zA-Z0-9]{36})/ },
|
|
53
|
+
{ name: 'GitHub server-to-server token', pattern: /(ghr_[a-zA-Z0-9]{36})/ },
|
|
54
|
+
{ name: 'Slack bot token', pattern: /(xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,})/ },
|
|
55
|
+
{ name: 'Slack user token', pattern: /(xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-z0-9]{32,})/ },
|
|
56
|
+
{ name: 'Slack webhook URL', pattern: /(https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]{24})/ },
|
|
57
|
+
{ name: 'Stripe live secret', pattern: /(sk_live_[A-Za-z0-9]{20,})/ },
|
|
58
|
+
{ name: 'Stripe restricted key', pattern: /(rk_live_[A-Za-z0-9]{20,})/ },
|
|
59
|
+
{ name: 'Twilio account SID', pattern: /(AC[a-f0-9]{32})/ },
|
|
60
|
+
{ name: 'Twilio auth token', pattern: /TWILIO_AUTH_TOKEN\s*=\s*([a-f0-9]{32})/i },
|
|
61
|
+
{ name: 'SendGrid API key', pattern: /(SG\.[a-zA-Z0-9_\-]{22}\.[a-zA-Z0-9_\-]{43})/ },
|
|
62
|
+
{ name: 'Mailgun API key', pattern: /(key-[a-z0-9]{32})/ },
|
|
63
|
+
{ name: 'JWT (signed)', pattern: /(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_\-]{10,})/ },
|
|
64
|
+
{ name: 'Database URL with credentials', pattern: /(?:DATABASE|POSTGRES|MYSQL|MONGODB)_URL\s*=\s*([a-z]+:\/\/[^:\s]+:[^@\s]+@)/i },
|
|
65
|
+
{ name: 'Generic API key (env-style)', pattern: /(?:^|\s)([A-Z][A-Z0-9_]*(?:API|SECRET|TOKEN|PASSWORD|PRIVATE)_KEY)\s*=\s*['"]?([A-Za-z0-9_\-+/=]{30,})['"]?/, captureGroup: 2 },
|
|
35
66
|
];
|
|
36
67
|
|
|
37
68
|
function isPlaceholder(value) {
|
|
@@ -48,7 +79,8 @@ if (!SCAN_EXTENSIONS.has(ext)) process.exit(0);
|
|
|
48
79
|
let content;
|
|
49
80
|
try {
|
|
50
81
|
content = readFileSync(filePath, 'utf8');
|
|
51
|
-
} catch {
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logHookFailure({ hook: 'scan-secrets', err, phase: 'read', input: { filePath } });
|
|
52
84
|
process.exit(0);
|
|
53
85
|
}
|
|
54
86
|
|
|
@@ -57,12 +89,12 @@ const findings = [];
|
|
|
57
89
|
|
|
58
90
|
for (let i = 0; i < lines.length; i++) {
|
|
59
91
|
const line = lines[i];
|
|
60
|
-
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
92
|
+
for (const { name, pattern, captureGroup } of SECRET_PATTERNS) {
|
|
61
93
|
const match = pattern.exec(line);
|
|
62
94
|
if (!match) continue;
|
|
63
|
-
const captured = match[1] ?? match[0];
|
|
95
|
+
const captured = match[captureGroup ?? 1] ?? match[0];
|
|
64
96
|
if (isPlaceholder(captured)) continue;
|
|
65
|
-
findings.push({ name, line: i + 1, value: captured.slice(0, 20) + '...' });
|
|
97
|
+
findings.push({ name, line: i + 1, value: String(captured).slice(0, 20) + '...' });
|
|
66
98
|
}
|
|
67
99
|
}
|
|
68
100
|
|
|
@@ -73,4 +105,15 @@ for (const { name, line, value } of findings) {
|
|
|
73
105
|
process.stderr.write(` Line ${line}: ${name} — matched value starts with: ${value}\n`);
|
|
74
106
|
}
|
|
75
107
|
process.stderr.write('Remove or rotate the secret before writing this file.\n');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
|
|
111
|
+
emitRoleEvent({
|
|
112
|
+
type: 'secrets.detected',
|
|
113
|
+
summary: `Secret(s) detected in ${filePath}: ${findings.map((f) => f.name).join(', ')}`,
|
|
114
|
+
hookInput: {},
|
|
115
|
+
context: { filePath, findings },
|
|
116
|
+
});
|
|
117
|
+
} catch { /* best effort */ }
|
|
118
|
+
|
|
76
119
|
process.exit(2);
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/session-optimize.mjs — Session end optimization hook — triggers agent optimization for low-performers.
|
|
4
|
+
*
|
|
5
|
+
* Runs as a Stop hook at session end. Checks recent performance reviews for agents with avgScore < 0.7
|
|
6
|
+
* and invokes `construct optimize <agent>` to generate and apply improvement patches.
|
|
7
|
+
*
|
|
8
|
+
* @p95ms 300
|
|
9
|
+
* @maxBlockingScope Stop (non-blocking, async)
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const CONSTRUCT_BIN = join(__dirname, '..', '..', 'bin', 'construct');
|
|
19
|
+
|
|
20
|
+
const OPTIMIZATION_THRESHOLD = 0.7; // Score below this triggers optimization
|
|
21
|
+
const MIN_TRACES_PER_AGENT = 3; // Minimum traces needed for optimization
|
|
22
|
+
const OPTIMIZATION_DAYS = 7; // Look back 7 days for performance data
|
|
23
|
+
const OPTIMIZATION_LOG_DIR = join(homedir(), '.cx', 'optimization-logs');
|
|
24
|
+
|
|
25
|
+
// Ensure optimization log directory exists
|
|
26
|
+
mkdirSync(OPTIMIZATION_LOG_DIR, { recursive: true });
|
|
27
|
+
|
|
28
|
+
let input = {};
|
|
29
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
30
|
+
|
|
31
|
+
// Only run for Construct sessions
|
|
32
|
+
const cwd = input?.cwd || process.cwd();
|
|
33
|
+
const projectName = cwd.split('/').pop() || 'project';
|
|
34
|
+
|
|
35
|
+
// Skip if this is not a Construct project or in global mode
|
|
36
|
+
if (projectName !== 'construct' && !existsSync(join(cwd, '.cx'))) {
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Reviews older than this trigger an auto-refresh on the next session-stop.
|
|
41
|
+
// Six hours keeps optimization signal current without re-running review on
|
|
42
|
+
// every short session. Operators can still run `construct review` manually.
|
|
43
|
+
const REVIEW_REFRESH_MS = 6 * 60 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
function maybeRefreshReviews() {
|
|
46
|
+
const reviewsDir = join(homedir(), '.cx', 'performance-reviews');
|
|
47
|
+
let needsRefresh = false;
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(reviewsDir)) {
|
|
50
|
+
needsRefresh = true;
|
|
51
|
+
} else {
|
|
52
|
+
const files = spawnSync('find', [reviewsDir, '-name', '*.json', '-type', 'f'], {
|
|
53
|
+
encoding: 'utf8', timeout: 5000,
|
|
54
|
+
}).stdout.trim().split('\n').filter(Boolean);
|
|
55
|
+
if (files.length === 0) {
|
|
56
|
+
needsRefresh = true;
|
|
57
|
+
} else {
|
|
58
|
+
const newestMs = files.reduce((acc, f) => {
|
|
59
|
+
try {
|
|
60
|
+
const m = Number(spawnSync('stat', ['-f', '%m', f], { encoding: 'utf8' }).stdout.trim());
|
|
61
|
+
return Number.isFinite(m) ? Math.max(acc, m * 1000) : acc;
|
|
62
|
+
} catch { return acc; }
|
|
63
|
+
}, 0);
|
|
64
|
+
if (Date.now() - newestMs > REVIEW_REFRESH_MS) needsRefresh = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch { /* tolerate fs errors silently — refresh is best-effort */ }
|
|
68
|
+
|
|
69
|
+
if (!needsRefresh) return;
|
|
70
|
+
|
|
71
|
+
// Refresh detached so the Stop hook does not block on telemetry latency.
|
|
72
|
+
try {
|
|
73
|
+
spawnSync(process.execPath, [CONSTRUCT_BIN, 'review', '--quiet'], {
|
|
74
|
+
stdio: 'ignore',
|
|
75
|
+
timeout: 60_000,
|
|
76
|
+
detached: true,
|
|
77
|
+
});
|
|
78
|
+
} catch { /* refresh is best-effort; manual `construct review` is always available */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read the most recent performance review
|
|
82
|
+
function getLatestPerformanceReview() {
|
|
83
|
+
const reviewsDir = join(homedir(), '.cx', 'performance-reviews');
|
|
84
|
+
if (!existsSync(reviewsDir)) return null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const files = spawnSync('find', [reviewsDir, '-name', '*.json', '-type', 'f'], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
timeout: 5000
|
|
90
|
+
}).stdout.trim().split('\n').filter(Boolean);
|
|
91
|
+
|
|
92
|
+
if (files.length === 0) return null;
|
|
93
|
+
|
|
94
|
+
// Sort by modification time, newest first
|
|
95
|
+
files.sort((a, b) => {
|
|
96
|
+
try {
|
|
97
|
+
const statA = spawnSync('stat', ['-f', '%m', a], { encoding: 'utf8' }).stdout.trim();
|
|
98
|
+
const statB = spawnSync('stat', ['-f', '%m', b], { encoding: 'utf8' }).stdout.trim();
|
|
99
|
+
return parseInt(statB) - parseInt(statA);
|
|
100
|
+
} catch {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const latestFile = files[0];
|
|
106
|
+
const content = readFileSync(latestFile, 'utf8');
|
|
107
|
+
return JSON.parse(content);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Error reading performance reviews: ${error.message}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if agent needs optimization
|
|
115
|
+
function needsOptimization(agentStats) {
|
|
116
|
+
return (
|
|
117
|
+
agentStats.scoredInvocations >= MIN_TRACES_PER_AGENT &&
|
|
118
|
+
agentStats.avgScore < OPTIMIZATION_THRESHOLD &&
|
|
119
|
+
agentStats.lowScoreTraces.length > 0
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Run optimization for a specific agent
|
|
124
|
+
function optimizeAgent(agentName) {
|
|
125
|
+
console.log(`🔧 Optimizing ${agentName} (score < ${OPTIMIZATION_THRESHOLD})`);
|
|
126
|
+
|
|
127
|
+
const logFile = join(OPTIMIZATION_LOG_DIR, `${new Date().toISOString().split('T')[0]}-${agentName}.log`);
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Run construct optimize with auto-apply (no --dry-run)
|
|
132
|
+
const result = spawnSync(CONSTRUCT_BIN, ['optimize', agentName, `--days=${OPTIMIZATION_DAYS}`], {
|
|
133
|
+
cwd,
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
timeout: 300000, // 5 minutes timeout for optimization
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const duration = Date.now() - startTime;
|
|
140
|
+
|
|
141
|
+
// Log the optimization attempt
|
|
142
|
+
const logEntry = {
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
agent: agentName,
|
|
145
|
+
durationMs: duration,
|
|
146
|
+
success: result.status === 0,
|
|
147
|
+
stdout: result.stdout,
|
|
148
|
+
stderr: result.stderr,
|
|
149
|
+
exitCode: result.status,
|
|
150
|
+
signal: result.signal
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
writeFileSync(logFile, JSON.stringify(logEntry, null, 2) + '\n');
|
|
154
|
+
|
|
155
|
+
if (result.status === 0) {
|
|
156
|
+
console.log(`✅ Optimization for ${agentName} completed in ${duration}ms`);
|
|
157
|
+
|
|
158
|
+
// Record observation about the optimization
|
|
159
|
+
recordOptimizationObservation(agentName, true, duration);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`❌ Optimization for ${agentName} failed with code ${result.status}`);
|
|
162
|
+
console.log(`Stderr: ${result.stderr.slice(0, 500)}...`);
|
|
163
|
+
|
|
164
|
+
// Record observation about the failure
|
|
165
|
+
recordOptimizationObservation(agentName, false, duration, result.stderr);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result.status === 0;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(`❌ Optimization for ${agentName} threw error: ${error.message}`);
|
|
171
|
+
|
|
172
|
+
const logEntry = {
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
agent: agentName,
|
|
175
|
+
durationMs: Date.now() - startTime,
|
|
176
|
+
success: false,
|
|
177
|
+
error: error.message,
|
|
178
|
+
stack: error.stack
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
writeFileSync(logFile, JSON.stringify(logEntry, null, 2) + '\n');
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Record optimization observation for learning
|
|
187
|
+
function recordOptimizationObservation(agentName, success, durationMs, error = null) {
|
|
188
|
+
const observation = {
|
|
189
|
+
role: 'construct',
|
|
190
|
+
category: success ? 'pattern' : 'anti-pattern',
|
|
191
|
+
summary: success
|
|
192
|
+
? `Auto-optimized ${agentName} in ${durationMs}ms`
|
|
193
|
+
: `Auto-optimization failed for ${agentName}: ${error?.slice(0, 200) || 'unknown error'}`,
|
|
194
|
+
content: success
|
|
195
|
+
? `Automatic optimization triggered for ${agentName} after performance review showed avgScore < ${OPTIMIZATION_THRESHOLD}. Optimization completed successfully in ${durationMs}ms.`
|
|
196
|
+
: `Automatic optimization failed for ${agentName}. Error: ${error || 'Unknown'}. System should notify maintainer.`,
|
|
197
|
+
tags: ['auto-optimization', 'performance-review', agentName],
|
|
198
|
+
confidence: 0.9
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Use existing observation recording if available
|
|
203
|
+
const observationFile = join(homedir(), '.cx', 'observations', `${Date.now()}-${agentName}-optimization.json`);
|
|
204
|
+
mkdirSync(dirname(observationFile), { recursive: true });
|
|
205
|
+
writeFileSync(observationFile, JSON.stringify(observation, null, 2) + '\n');
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Non-critical - observation recording failed
|
|
208
|
+
console.error(`Failed to record optimization observation: ${error.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Main optimization logic
|
|
213
|
+
function main() {
|
|
214
|
+
// If the most recent review is missing or stale (>6h), kick off a refresh
|
|
215
|
+
// detached so the Stop hook returns promptly. The refresh runs on the next
|
|
216
|
+
// session's Stop if it's still in flight; nothing here blocks user work.
|
|
217
|
+
maybeRefreshReviews();
|
|
218
|
+
|
|
219
|
+
const review = getLatestPerformanceReview();
|
|
220
|
+
if (!review || !review.agentStats || review.agentStats.length === 0) {
|
|
221
|
+
console.log('📊 No performance review data found. Run `construct review` first.');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(`📊 Checking ${review.agentStats.length} agents from performance review ${review.generated}`);
|
|
226
|
+
|
|
227
|
+
const agentsNeedingOptimization = review.agentStats.filter(needsOptimization);
|
|
228
|
+
|
|
229
|
+
if (agentsNeedingOptimization.length === 0) {
|
|
230
|
+
console.log(`✅ All ${review.agentStats.length} agents have scores >= ${OPTIMIZATION_THRESHOLD} or insufficient data`);
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`🔧 Found ${agentsNeedingOptimization.length} agents needing optimization:`);
|
|
235
|
+
agentsNeedingOptimization.forEach(agent => {
|
|
236
|
+
console.log(` • ${agent.name}: avgScore=${agent.avgScore}, invocations=${agent.invocations}`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Run optimization for each agent (serial for now)
|
|
240
|
+
let optimizedCount = 0;
|
|
241
|
+
let failedCount = 0;
|
|
242
|
+
|
|
243
|
+
for (const agent of agentsNeedingOptimization) {
|
|
244
|
+
const success = optimizeAgent(agent.name);
|
|
245
|
+
if (success) {
|
|
246
|
+
optimizedCount++;
|
|
247
|
+
} else {
|
|
248
|
+
failedCount++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Small delay between optimizations to avoid rate limits
|
|
252
|
+
if (agentsNeedingOptimization.length > 1) {
|
|
253
|
+
spawnSync('sleep', ['10'], { encoding: 'utf8' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Summary
|
|
258
|
+
console.log(`\n📈 Optimization Summary:`);
|
|
259
|
+
console.log(` • Agents checked: ${review.agentStats.length}`);
|
|
260
|
+
console.log(` • Needing optimization: ${agentsNeedingOptimization.length}`);
|
|
261
|
+
console.log(` • Successfully optimized: ${optimizedCount}`);
|
|
262
|
+
console.log(` • Failed: ${failedCount}`);
|
|
263
|
+
|
|
264
|
+
if (optimizedCount > 0) {
|
|
265
|
+
console.log(`\n💡 Optimized agents will be evaluated in the next performance review.`);
|
|
266
|
+
console.log(` Run \`construct review\` after next session to see improvements.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Log summary to file
|
|
270
|
+
const summary = {
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
project: projectName,
|
|
273
|
+
reviewDate: review.generated,
|
|
274
|
+
agentsChecked: review.agentStats.length,
|
|
275
|
+
agentsNeedingOptimization: agentsNeedingOptimization.length,
|
|
276
|
+
optimizedCount,
|
|
277
|
+
failedCount,
|
|
278
|
+
agents: agentsNeedingOptimization.map(a => ({
|
|
279
|
+
name: a.name,
|
|
280
|
+
avgScore: a.avgScore,
|
|
281
|
+
invocations: a.invocations,
|
|
282
|
+
optimized: optimizeAgent.includes ? 'yes' : 'no'
|
|
283
|
+
}))
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const summaryFile = join(OPTIMIZATION_LOG_DIR, `${new Date().toISOString().split('T')[0]}-summary.json`);
|
|
287
|
+
writeFileSync(summaryFile, JSON.stringify(summary, null, 2) + '\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Run main logic
|
|
291
|
+
try {
|
|
292
|
+
main();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error(`❌ Session optimization hook crashed: ${error.message}`);
|
|
295
|
+
console.error(error.stack);
|
|
296
|
+
|
|
297
|
+
// Log the crash
|
|
298
|
+
const crashLog = join(OPTIMIZATION_LOG_DIR, 'crashes', `${Date.now()}-crash.json`);
|
|
299
|
+
mkdirSync(dirname(crashLog), { recursive: true });
|
|
300
|
+
writeFileSync(crashLog, JSON.stringify({
|
|
301
|
+
timestamp: new Date().toISOString(),
|
|
302
|
+
error: error.message,
|
|
303
|
+
stack: error.stack,
|
|
304
|
+
cwd,
|
|
305
|
+
projectName
|
|
306
|
+
}, null, 2) + '\n');
|
|
307
|
+
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
process.exit(0);
|