@geraldmaron/construct 1.0.0 → 1.0.1
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 +10 -7
- 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 +1116 -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 +525 -38
- 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,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/worker/entrypoint.mjs — long-running worker loop for team / enterprise mode.
|
|
4
|
+
*
|
|
5
|
+
* Claims pending intake items from the Postgres-backed queue and runs them
|
|
6
|
+
* through `runJob`, attaching evidence to the originating task graph node.
|
|
7
|
+
* Exits when the queue stays empty for `--idle-timeout-seconds` (default
|
|
8
|
+
* 300s) so a Kubernetes / nomad scheduler can scale workers to zero.
|
|
9
|
+
*
|
|
10
|
+
* The container's path policy is the workspace mount: `runJob` enforces
|
|
11
|
+
* `allowedPaths: [workspace]` so the worker can only touch the
|
|
12
|
+
* bind-mounted project tree even if a malicious command tries `..`.
|
|
13
|
+
*
|
|
14
|
+
* Invocation (inside the image):
|
|
15
|
+
* node lib/worker/entrypoint.mjs \
|
|
16
|
+
* --project=<project-name> \
|
|
17
|
+
* --workspace=/work \
|
|
18
|
+
* --idle-timeout-seconds=300 \
|
|
19
|
+
* --poll-interval-ms=2000
|
|
20
|
+
*
|
|
21
|
+
* Env: DATABASE_URL must be set. The container reads it from the platform
|
|
22
|
+
* (compose / k8s secret); the entrypoint does not source ~/.construct/config.env.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { createSqlClient, closeSqlClient } from '../storage/backend.mjs';
|
|
28
|
+
import { PostgresIntakeQueue } from '../intake/postgres-queue.mjs';
|
|
29
|
+
import { FilesystemTaskGraphStore } from '../task-graph/store.mjs';
|
|
30
|
+
import { runJob } from './run.mjs';
|
|
31
|
+
import { evidenceFromJobResult, recordEvidence, blockedPacket } from './evidence.mjs';
|
|
32
|
+
import { emitTraceEvent, newTraceId } from './trace.mjs';
|
|
33
|
+
|
|
34
|
+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
35
|
+
const DEFAULT_IDLE_TIMEOUT_SECONDS = 300;
|
|
36
|
+
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const arg of argv.slice(2)) {
|
|
41
|
+
const m = arg.match(/^--([a-z-]+)(?:=(.*))?$/);
|
|
42
|
+
if (!m) continue;
|
|
43
|
+
out[m[1]] = m[2] === undefined ? true : m[2];
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readWorkerName() {
|
|
49
|
+
return process.env.HOSTNAME || `worker-${process.pid}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logEvent(rootDir, eventType, fields = {}) {
|
|
53
|
+
try {
|
|
54
|
+
emitTraceEvent({
|
|
55
|
+
rootDir,
|
|
56
|
+
eventType,
|
|
57
|
+
...fields,
|
|
58
|
+
});
|
|
59
|
+
} catch { /* observability never breaks the worker loop */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sleep(ms) {
|
|
63
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Map an intake packet to a worker job. The packet's triage names the
|
|
68
|
+
* recommendedAction; we read the project's verification requirements
|
|
69
|
+
* from the task graph (if one was auto-generated) or fall back to a
|
|
70
|
+
* conservative `node --version` smoke command so the worker always
|
|
71
|
+
* has something to execute against and the queue drains.
|
|
72
|
+
*/
|
|
73
|
+
function jobFromPacket(packet, { workspace, defaultCommand }) {
|
|
74
|
+
const command = packet?.workerCommand || defaultCommand;
|
|
75
|
+
return {
|
|
76
|
+
jobId: `${packet.id}-${Date.now()}`,
|
|
77
|
+
taskId: packet.id,
|
|
78
|
+
project: packet.intake?.project || null,
|
|
79
|
+
workspaceRef: workspace,
|
|
80
|
+
command,
|
|
81
|
+
args: [],
|
|
82
|
+
timeoutSeconds: packet?.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS,
|
|
83
|
+
envPolicy: 'restricted',
|
|
84
|
+
allowedPaths: [workspace],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process one claimed packet end-to-end: run the command, record evidence
|
|
90
|
+
* if a graph node is named in the packet, mark the queue item processed
|
|
91
|
+
* or blocked. Returns true on success, false on failure (which surfaces
|
|
92
|
+
* a blocked packet for human review).
|
|
93
|
+
*/
|
|
94
|
+
async function processClaim({ packet, queue, store, rootDir, workspace, defaultCommand, workerName }) {
|
|
95
|
+
const traceId = packet?.traceId || newTraceId();
|
|
96
|
+
const job = jobFromPacket(packet, { workspace, defaultCommand });
|
|
97
|
+
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = await runJob({
|
|
101
|
+
rootDir,
|
|
102
|
+
job: { ...job, traceId },
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logEvent(rootDir, 'worker.completed', {
|
|
106
|
+
traceId,
|
|
107
|
+
project: job.project,
|
|
108
|
+
taskId: job.taskId,
|
|
109
|
+
metadata: { status: 'errored', error: err?.message || String(err) },
|
|
110
|
+
});
|
|
111
|
+
await queue.markSkipped(packet.id, {
|
|
112
|
+
skippedBy: workerName,
|
|
113
|
+
reason: `worker exec error: ${err?.message || String(err)}`,
|
|
114
|
+
});
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If the packet carries graphId + nodeId, attach evidence to that node.
|
|
119
|
+
if (packet?.graphId && packet?.nodeId) {
|
|
120
|
+
try {
|
|
121
|
+
const evidence = evidenceFromJobResult(result, {
|
|
122
|
+
evidenceType: packet.evidenceType || 'test-result',
|
|
123
|
+
});
|
|
124
|
+
recordEvidence({
|
|
125
|
+
store,
|
|
126
|
+
graphId: packet.graphId,
|
|
127
|
+
nodeId: packet.nodeId,
|
|
128
|
+
evidence,
|
|
129
|
+
rootDir,
|
|
130
|
+
});
|
|
131
|
+
} catch { /* evidence write best-effort */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (result.status === 'passed') {
|
|
135
|
+
await queue.markProcessed(packet.id, {
|
|
136
|
+
processedBy: workerName,
|
|
137
|
+
notes: `passed in ${result.durationMs}ms`,
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Failure path: surface as a blocked packet with attempted steps so the
|
|
143
|
+
// main agent can pick it up and decide next.
|
|
144
|
+
const reason = result.status === 'timeout'
|
|
145
|
+
? `command timed out after ${packet?.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS}s`
|
|
146
|
+
: `command failed with exit code ${result.exitCode}`;
|
|
147
|
+
const attempted = [job.command];
|
|
148
|
+
const blocked = blockedPacket({ taskId: packet.id, reason, attempted });
|
|
149
|
+
await queue.markSkipped(packet.id, {
|
|
150
|
+
skippedBy: workerName,
|
|
151
|
+
reason: `${reason}; stdout=${result.stdoutPath}; stderr=${result.stderrPath}`,
|
|
152
|
+
});
|
|
153
|
+
logEvent(rootDir, 'worker.completed', {
|
|
154
|
+
traceId,
|
|
155
|
+
project: job.project,
|
|
156
|
+
taskId: job.taskId,
|
|
157
|
+
metadata: { status: result.status, exitCode: result.exitCode, blocked, durationMs: result.durationMs },
|
|
158
|
+
});
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Long-running worker loop. Resolves with the run statistics when the
|
|
164
|
+
* idle timeout elapses or when `stopAfter` jobs have completed (test mode).
|
|
165
|
+
*/
|
|
166
|
+
export async function runWorkerLoop({
|
|
167
|
+
rootDir,
|
|
168
|
+
workspace,
|
|
169
|
+
project,
|
|
170
|
+
sql,
|
|
171
|
+
queue: injectedQueue = null,
|
|
172
|
+
store: injectedStore = null,
|
|
173
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
174
|
+
idleTimeoutSeconds = DEFAULT_IDLE_TIMEOUT_SECONDS,
|
|
175
|
+
defaultCommand = 'node --version',
|
|
176
|
+
stopAfter = null,
|
|
177
|
+
now = Date.now,
|
|
178
|
+
} = {}) {
|
|
179
|
+
if (!project) throw new Error('runWorkerLoop: project is required');
|
|
180
|
+
if (!workspace) throw new Error('runWorkerLoop: workspace is required');
|
|
181
|
+
if (!injectedQueue && !sql) throw new Error('runWorkerLoop: sql client (or injected queue) is required');
|
|
182
|
+
|
|
183
|
+
const workerName = readWorkerName();
|
|
184
|
+
const queue = injectedQueue || new PostgresIntakeQueue({ sql, project });
|
|
185
|
+
const store = injectedStore || new FilesystemTaskGraphStore(rootDir);
|
|
186
|
+
|
|
187
|
+
let processed = 0;
|
|
188
|
+
let skipped = 0;
|
|
189
|
+
let idleSince = now();
|
|
190
|
+
|
|
191
|
+
while (true) {
|
|
192
|
+
const claimed = await queue.claim({ claimedBy: workerName });
|
|
193
|
+
if (claimed) {
|
|
194
|
+
const ok = await processClaim({
|
|
195
|
+
packet: claimed,
|
|
196
|
+
queue,
|
|
197
|
+
store,
|
|
198
|
+
rootDir,
|
|
199
|
+
workspace,
|
|
200
|
+
defaultCommand,
|
|
201
|
+
workerName,
|
|
202
|
+
});
|
|
203
|
+
if (ok) processed += 1; else skipped += 1;
|
|
204
|
+
idleSince = now();
|
|
205
|
+
if (stopAfter && (processed + skipped) >= stopAfter) break;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ((now() - idleSince) >= idleTimeoutSeconds * 1000) break;
|
|
210
|
+
await sleep(pollIntervalMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { processed, skipped, workerName };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// CLI entrypoint when invoked directly (the Dockerfile.worker CMD).
|
|
217
|
+
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
|
|
218
|
+
if (invokedDirectly) {
|
|
219
|
+
const args = parseArgs(process.argv);
|
|
220
|
+
const workspace = args.workspace || process.env.CONSTRUCT_WORKSPACE || process.cwd();
|
|
221
|
+
const rootDir = args['root-dir'] || workspace;
|
|
222
|
+
const project = args.project || process.env.CONSTRUCT_PROJECT_NAME || path.basename(path.resolve(workspace));
|
|
223
|
+
const pollIntervalMs = Number(args['poll-interval-ms'] || DEFAULT_POLL_INTERVAL_MS);
|
|
224
|
+
const idleTimeoutSeconds = Number(args['idle-timeout-seconds'] || DEFAULT_IDLE_TIMEOUT_SECONDS);
|
|
225
|
+
|
|
226
|
+
const sql = createSqlClient(process.env);
|
|
227
|
+
if (!sql) {
|
|
228
|
+
process.stderr.write('[worker] DATABASE_URL is required for the worker entrypoint.\n');
|
|
229
|
+
process.exit(78); // EX_CONFIG
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const summary = await runWorkerLoop({
|
|
234
|
+
rootDir, workspace, project, sql, pollIntervalMs, idleTimeoutSeconds,
|
|
235
|
+
});
|
|
236
|
+
process.stdout.write(`[worker] exiting after idle timeout — processed=${summary.processed} skipped=${summary.skipped} worker=${summary.workerName}\n`);
|
|
237
|
+
process.exit(0);
|
|
238
|
+
} finally {
|
|
239
|
+
await closeSqlClient(sql);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/worker/evidence.mjs — record verified evidence onto task graph nodes.
|
|
3
|
+
*
|
|
4
|
+
* Evidence is the contract that lets a node move from in-progress → done.
|
|
5
|
+
* The worker plane emits one evidence record per executed job; the
|
|
6
|
+
* higher-level dispatcher refuses to mark a node done without at least
|
|
7
|
+
* one evidence entry. BLOCKED and NEEDS_MAIN_INPUT statuses carry their
|
|
8
|
+
* own dedicated schemas: BLOCKED requires `attempted` steps; NEEDS_MAIN_INPUT
|
|
9
|
+
* requires `question`, `safeDefault`, and `context` fields.
|
|
10
|
+
*
|
|
11
|
+
* Evidence record shape:
|
|
12
|
+
* {
|
|
13
|
+
* taskId, evidenceType, command?, status, summary,
|
|
14
|
+
* artifacts?, traceId?, createdAt
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { emitTraceEvent } from './trace.mjs';
|
|
19
|
+
|
|
20
|
+
export const EVIDENCE_TYPES = [
|
|
21
|
+
'test-result',
|
|
22
|
+
'lint-result',
|
|
23
|
+
'build-result',
|
|
24
|
+
'retrieval-eval',
|
|
25
|
+
'manual-verification',
|
|
26
|
+
'source-citation',
|
|
27
|
+
'trace-link',
|
|
28
|
+
'sample-output',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function buildSummary(jobResult) {
|
|
32
|
+
if (!jobResult) return '';
|
|
33
|
+
const head = `${jobResult.status} · exit ${jobResult.exitCode} · ${jobResult.durationMs}ms`;
|
|
34
|
+
return head;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function evidenceFromJobResult(jobResult, { evidenceType = 'test-result', summary } = {}) {
|
|
38
|
+
if (!jobResult?.taskId) throw new Error('evidenceFromJobResult: jobResult.taskId is required');
|
|
39
|
+
return {
|
|
40
|
+
taskId: jobResult.taskId,
|
|
41
|
+
evidenceType,
|
|
42
|
+
command: jobResult.command || null,
|
|
43
|
+
status: jobResult.status,
|
|
44
|
+
summary: summary || buildSummary(jobResult),
|
|
45
|
+
artifacts: jobResult.artifacts || [],
|
|
46
|
+
traceId: jobResult.traceId || null,
|
|
47
|
+
createdAt: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append an evidence record to a task graph node and emit the matching
|
|
53
|
+
* trace event. Returns the updated node.
|
|
54
|
+
*/
|
|
55
|
+
export function recordEvidence({ store, graphId, nodeId, evidence, rootDir, emitEvent = emitTraceEvent }) {
|
|
56
|
+
if (!store) throw new Error('recordEvidence: store is required');
|
|
57
|
+
if (!graphId || !nodeId) throw new Error('recordEvidence: graphId and nodeId are required');
|
|
58
|
+
if (!evidence) throw new Error('recordEvidence: evidence is required');
|
|
59
|
+
|
|
60
|
+
const node = store.updateNodeStatus(graphId, nodeId, undefined, { addEvidence: evidence });
|
|
61
|
+
|
|
62
|
+
if (rootDir) {
|
|
63
|
+
emitEvent({
|
|
64
|
+
rootDir,
|
|
65
|
+
eventType: 'evidence.recorded',
|
|
66
|
+
traceId: evidence.traceId || undefined,
|
|
67
|
+
project: node.project,
|
|
68
|
+
taskId: nodeId,
|
|
69
|
+
metadata: {
|
|
70
|
+
evidenceType: evidence.evidenceType,
|
|
71
|
+
status: evidence.status,
|
|
72
|
+
summary: evidence.summary,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function blockedPacket({ taskId, attempted = [], reason }) {
|
|
80
|
+
if (!taskId) throw new Error('blockedPacket: taskId is required');
|
|
81
|
+
if (!reason) throw new Error('blockedPacket: reason is required');
|
|
82
|
+
if (!Array.isArray(attempted) || attempted.length === 0) {
|
|
83
|
+
throw new Error('blockedPacket: at least one attempted step is required');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
taskId,
|
|
87
|
+
status: 'blocked',
|
|
88
|
+
reason,
|
|
89
|
+
attempted,
|
|
90
|
+
createdAt: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function needsInputPacket({ taskId, question, safeDefault, context }) {
|
|
95
|
+
if (!taskId) throw new Error('needsInputPacket: taskId is required');
|
|
96
|
+
if (!question) throw new Error('needsInputPacket: question is required');
|
|
97
|
+
if (safeDefault === undefined) throw new Error('needsInputPacket: safeDefault is required (use null if none)');
|
|
98
|
+
if (!context) throw new Error('needsInputPacket: context is required');
|
|
99
|
+
return {
|
|
100
|
+
taskId,
|
|
101
|
+
status: 'needs-input',
|
|
102
|
+
question,
|
|
103
|
+
safeDefault,
|
|
104
|
+
context,
|
|
105
|
+
createdAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/worker/run.mjs — bounded command execution for the worker plane.
|
|
3
|
+
*
|
|
4
|
+
* Runs a single shell command with a hard timeout and a path-policy
|
|
5
|
+
* check so workers cannot write outside their allowed workspace. The
|
|
6
|
+
* worker plane stays decoupled from the control plane: this module
|
|
7
|
+
* accepts a job descriptor and returns a job result; it does not pick
|
|
8
|
+
* what to run or interpret the result. Higher layers (intake claim,
|
|
9
|
+
* task graph dispatcher, evidence recorder) glue the pieces together.
|
|
10
|
+
*
|
|
11
|
+
* Job input:
|
|
12
|
+
* { jobId, taskId, project, workspaceRef, command, args?,
|
|
13
|
+
* timeoutSeconds, envPolicy, allowedPaths, network }
|
|
14
|
+
*
|
|
15
|
+
* Job output:
|
|
16
|
+
* { jobId, taskId, status, exitCode, stdoutPath, stderrPath,
|
|
17
|
+
* durationMs, artifacts, traceId, spanId }
|
|
18
|
+
*
|
|
19
|
+
* `envPolicy: 'restricted'` strips the env down to PATH, HOME, USER,
|
|
20
|
+
* TZ, LANG plus an explicit allowlist. Path policy is enforced by
|
|
21
|
+
* resolving every entry in allowedPaths and refusing to start when
|
|
22
|
+
* the resolved workspaceRef sits outside that allowlist.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
import { emitTraceEvent, newTraceId, newSpanId } from './trace.mjs';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
32
|
+
const ARTIFACTS_SUBDIR = '.cx/runtime/worker';
|
|
33
|
+
|
|
34
|
+
function artifactsDir(rootDir) {
|
|
35
|
+
return path.join(rootDir, ARTIFACTS_SUBDIR);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildRestrictedEnv(allowedKeys = []) {
|
|
39
|
+
const base = ['PATH', 'HOME', 'USER', 'TZ', 'LANG', 'LC_ALL', 'TMPDIR'];
|
|
40
|
+
const env = {};
|
|
41
|
+
for (const key of [...base, ...allowedKeys]) {
|
|
42
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
43
|
+
}
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isInsideAllowedPaths(workspaceRef, allowedPaths) {
|
|
48
|
+
if (!allowedPaths || allowedPaths.length === 0) return true;
|
|
49
|
+
const target = path.resolve(workspaceRef);
|
|
50
|
+
return allowedPaths
|
|
51
|
+
.map((p) => path.resolve(p))
|
|
52
|
+
.some((allowed) => target === allowed || target.startsWith(`${allowed}${path.sep}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run a worker job. Resolves with the result regardless of command outcome;
|
|
57
|
+
* rejects only on programmer errors (invalid job shape, path policy denial).
|
|
58
|
+
*/
|
|
59
|
+
export async function runJob({
|
|
60
|
+
rootDir,
|
|
61
|
+
job,
|
|
62
|
+
emitEvent = emitTraceEvent,
|
|
63
|
+
spawnFn = spawn,
|
|
64
|
+
} = {}) {
|
|
65
|
+
if (!rootDir) throw new Error('runJob: rootDir is required');
|
|
66
|
+
if (!job?.jobId) throw new Error('runJob: job.jobId is required');
|
|
67
|
+
if (!job?.command) throw new Error('runJob: job.command is required');
|
|
68
|
+
|
|
69
|
+
const traceId = job.traceId || newTraceId();
|
|
70
|
+
const spanId = newSpanId();
|
|
71
|
+
const timeoutSeconds = Number(job.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS);
|
|
72
|
+
const workspaceRef = path.resolve(job.workspaceRef || rootDir);
|
|
73
|
+
|
|
74
|
+
if (!isInsideAllowedPaths(workspaceRef, job.allowedPaths)) {
|
|
75
|
+
throw new Error(`runJob: workspaceRef ${workspaceRef} is outside allowedPaths`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const artifactsRoot = artifactsDir(rootDir);
|
|
79
|
+
if (!existsSync(artifactsRoot)) mkdirSync(artifactsRoot, { recursive: true });
|
|
80
|
+
const stdoutPath = path.join(artifactsRoot, `${job.jobId}.stdout.log`);
|
|
81
|
+
const stderrPath = path.join(artifactsRoot, `${job.jobId}.stderr.log`);
|
|
82
|
+
|
|
83
|
+
const env = job.envPolicy === 'restricted'
|
|
84
|
+
? buildRestrictedEnv(job.allowedEnvKeys)
|
|
85
|
+
: { ...process.env };
|
|
86
|
+
|
|
87
|
+
emitEvent({
|
|
88
|
+
rootDir,
|
|
89
|
+
eventType: 'worker.started',
|
|
90
|
+
traceId,
|
|
91
|
+
spanId,
|
|
92
|
+
project: job.project,
|
|
93
|
+
taskId: job.taskId,
|
|
94
|
+
metadata: { jobId: job.jobId, command: job.command, timeoutSeconds, workspaceRef },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const started = Date.now();
|
|
98
|
+
const child = spawnFn(job.command, job.args || [], {
|
|
99
|
+
cwd: workspaceRef,
|
|
100
|
+
env,
|
|
101
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
102
|
+
shell: typeof job.command === 'string' && (!job.args || job.args.length === 0),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let stdout = '';
|
|
106
|
+
let stderr = '';
|
|
107
|
+
let timedOut = false;
|
|
108
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
109
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
110
|
+
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
timedOut = true;
|
|
113
|
+
child.kill('SIGTERM');
|
|
114
|
+
setTimeout(() => child.kill('SIGKILL'), 2000).unref();
|
|
115
|
+
}, timeoutSeconds * 1000);
|
|
116
|
+
|
|
117
|
+
const exitCode = await new Promise((resolve) => {
|
|
118
|
+
child.once('close', (code) => resolve(code ?? -1));
|
|
119
|
+
child.once('error', () => resolve(-1));
|
|
120
|
+
});
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
|
|
123
|
+
writeFileSync(stdoutPath, stdout, 'utf8');
|
|
124
|
+
writeFileSync(stderrPath, stderr, 'utf8');
|
|
125
|
+
|
|
126
|
+
const durationMs = Date.now() - started;
|
|
127
|
+
const status = timedOut ? 'timeout' : exitCode === 0 ? 'passed' : 'failed';
|
|
128
|
+
|
|
129
|
+
const result = {
|
|
130
|
+
jobId: job.jobId,
|
|
131
|
+
taskId: job.taskId || null,
|
|
132
|
+
project: job.project || null,
|
|
133
|
+
status,
|
|
134
|
+
exitCode,
|
|
135
|
+
stdoutPath,
|
|
136
|
+
stderrPath,
|
|
137
|
+
durationMs,
|
|
138
|
+
artifacts: [{ path: stdoutPath, kind: 'stdout' }, { path: stderrPath, kind: 'stderr' }],
|
|
139
|
+
traceId,
|
|
140
|
+
spanId,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
emitEvent({
|
|
144
|
+
rootDir,
|
|
145
|
+
eventType: 'worker.completed',
|
|
146
|
+
traceId,
|
|
147
|
+
spanId,
|
|
148
|
+
project: job.project,
|
|
149
|
+
taskId: job.taskId,
|
|
150
|
+
metadata: { jobId: job.jobId, status, exitCode, durationMs, stdoutPath, stderrPath, timedOut },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/worker/trace.mjs — append-only trace event log for worker + intake flow.
|
|
3
|
+
*
|
|
4
|
+
* Trace events tie an intake packet → task graph → role dispatch → worker
|
|
5
|
+
* job → evidence record together via traceId + spanId. Events land in two
|
|
6
|
+
* places concurrently:
|
|
7
|
+
*
|
|
8
|
+
* 1. `.cx/traces/<YYYY-MM-DD>.jsonl` — append-only local JSONL. Always on,
|
|
9
|
+
* no credentials required. Tail live, archive, or replay.
|
|
10
|
+
* 2. Remote telemetry backend — fire-and-forget batched ingest when
|
|
11
|
+
* CONSTRUCT_TELEMETRY_PUBLIC_KEY + CONSTRUCT_TELEMETRY_SECRET_KEY are
|
|
12
|
+
* configured and CONSTRUCT_TRACE_BACKEND is `remote` (or unset).
|
|
13
|
+
* Each unique traceId triggers one trace-create plus an event-create
|
|
14
|
+
* per emitted event. Failures never throw — observability must not
|
|
15
|
+
* break the caller.
|
|
16
|
+
*
|
|
17
|
+
* Reads CONSTRUCT_TELEMETRY_PUBLIC_KEY / CONSTRUCT_TELEMETRY_SECRET_KEY /
|
|
18
|
+
* CONSTRUCT_TELEMETRY_URL from env.
|
|
19
|
+
*
|
|
20
|
+
* Event types in current use:
|
|
21
|
+
* intake.received — daemon ingested a file
|
|
22
|
+
* intake.triaged — classifyRdIntake produced a triage block
|
|
23
|
+
* task_graph.created — graph derived from triage
|
|
24
|
+
* role.dispatched — context router selected a persona
|
|
25
|
+
* tool.called — agent invoked a tool
|
|
26
|
+
* worker.started — worker began executing a command
|
|
27
|
+
* worker.completed — worker finished (status: passed | failed | timeout)
|
|
28
|
+
* evidence.recorded — evidence appended to a task graph node
|
|
29
|
+
* approval.requested — high-risk action blocked on approval
|
|
30
|
+
* approval.resolved — approval granted or denied
|
|
31
|
+
* memory.written — durable memory record persisted
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
35
|
+
import { randomBytes } from 'node:crypto';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import { createIngestClient } from '../telemetry/ingest.mjs';
|
|
39
|
+
|
|
40
|
+
const TRACE_SUBDIR = '.cx/traces';
|
|
41
|
+
|
|
42
|
+
export const TRACE_EVENT_TYPES = [
|
|
43
|
+
'daemon.started',
|
|
44
|
+
'daemon.heartbeat',
|
|
45
|
+
'daemon.stopped',
|
|
46
|
+
'lifecycle.completed',
|
|
47
|
+
'intake.received',
|
|
48
|
+
'intake.triaged',
|
|
49
|
+
'task_graph.created',
|
|
50
|
+
'role.dispatched',
|
|
51
|
+
'tool.called',
|
|
52
|
+
'worker.started',
|
|
53
|
+
'worker.completed',
|
|
54
|
+
'evidence.recorded',
|
|
55
|
+
'approval.requested',
|
|
56
|
+
'approval.resolved',
|
|
57
|
+
'memory.written',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function traceDir(rootDir) {
|
|
61
|
+
return path.join(rootDir, TRACE_SUBDIR);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function todayShard() {
|
|
65
|
+
return new Date().toISOString().slice(0, 10);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function newTraceId() {
|
|
69
|
+
return `trace-${randomBytes(8).toString('hex')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function newSpanId() {
|
|
73
|
+
return `span-${randomBytes(6).toString('hex')}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Reused per-process. The ingest client batches; recreating it would lose
|
|
77
|
+
// the queue. `available` flips false when keys aren't set, which is the
|
|
78
|
+
// graceful no-op path for solo-mode users without no telemetry backend configured.
|
|
79
|
+
let telemetryClient = null;
|
|
80
|
+
const seenTraces = new Set();
|
|
81
|
+
|
|
82
|
+
function getTelemetryClient(env = process.env) {
|
|
83
|
+
if (telemetryClient !== null) return telemetryClient;
|
|
84
|
+
const backend = (env.CONSTRUCT_TRACE_BACKEND || 'remote').toLowerCase();
|
|
85
|
+
if (backend === 'none' || backend === 'off' || backend === 'local') {
|
|
86
|
+
telemetryClient = { available: false, trace: () => {}, event: () => {} };
|
|
87
|
+
return telemetryClient;
|
|
88
|
+
}
|
|
89
|
+
telemetryClient = createIngestClient({
|
|
90
|
+
baseUrl: (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, ''),
|
|
91
|
+
publicKey: env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
92
|
+
secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
93
|
+
});
|
|
94
|
+
return telemetryClient;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Internal: reset the cached telemetry client. Test-only.
|
|
99
|
+
*/
|
|
100
|
+
export function _resetTelemetryClient() {
|
|
101
|
+
telemetryClient = null;
|
|
102
|
+
seenTraces.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function exportToRemote(event, env) {
|
|
107
|
+
const client = getTelemetryClient(env);
|
|
108
|
+
if (!client.available) return;
|
|
109
|
+
|
|
110
|
+
// First time we see a traceId, register the trace so subsequent events
|
|
111
|
+
// attach to a known parent.
|
|
112
|
+
if (event.traceId && !seenTraces.has(event.traceId)) {
|
|
113
|
+
seenTraces.add(event.traceId);
|
|
114
|
+
try {
|
|
115
|
+
client.trace({
|
|
116
|
+
id: event.traceId,
|
|
117
|
+
name: event.eventType,
|
|
118
|
+
metadata: {
|
|
119
|
+
project: event.project || undefined,
|
|
120
|
+
role: event.role || undefined,
|
|
121
|
+
taskId: event.taskId || undefined,
|
|
122
|
+
intakeId: event.intakeId || undefined,
|
|
123
|
+
},
|
|
124
|
+
timestamp: event.createdAt,
|
|
125
|
+
});
|
|
126
|
+
} catch { /* observability must not break the caller */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
client.event({
|
|
131
|
+
id: event.spanId,
|
|
132
|
+
traceId: event.traceId,
|
|
133
|
+
parentObservationId: event.parentSpanId || undefined,
|
|
134
|
+
name: event.eventType,
|
|
135
|
+
metadata: event.metadata,
|
|
136
|
+
input: undefined,
|
|
137
|
+
output: undefined,
|
|
138
|
+
startTime: event.createdAt,
|
|
139
|
+
});
|
|
140
|
+
} catch { /* swallow */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function emitTraceEvent({
|
|
144
|
+
rootDir,
|
|
145
|
+
eventType,
|
|
146
|
+
traceId,
|
|
147
|
+
spanId,
|
|
148
|
+
parentSpanId = null,
|
|
149
|
+
project = null,
|
|
150
|
+
role = null,
|
|
151
|
+
taskId = null,
|
|
152
|
+
intakeId = null,
|
|
153
|
+
metadata = {},
|
|
154
|
+
env = process.env,
|
|
155
|
+
}) {
|
|
156
|
+
if (!rootDir) throw new Error('emitTraceEvent: rootDir is required');
|
|
157
|
+
if (!eventType) throw new Error('emitTraceEvent: eventType is required');
|
|
158
|
+
if (!TRACE_EVENT_TYPES.includes(eventType)) {
|
|
159
|
+
throw new Error(`emitTraceEvent: unknown eventType ${eventType}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const dir = traceDir(rootDir);
|
|
163
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
164
|
+
|
|
165
|
+
const event = {
|
|
166
|
+
traceId: traceId || newTraceId(),
|
|
167
|
+
spanId: spanId || newSpanId(),
|
|
168
|
+
parentSpanId,
|
|
169
|
+
eventType,
|
|
170
|
+
project,
|
|
171
|
+
role,
|
|
172
|
+
taskId,
|
|
173
|
+
intakeId,
|
|
174
|
+
metadata,
|
|
175
|
+
createdAt: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
appendFileSync(path.join(dir, `${todayShard()}.jsonl`), `${JSON.stringify(event)}\n`, 'utf8');
|
|
178
|
+
|
|
179
|
+
// Fire-and-forget export to remote telemetry backend. Silent no-op when not configured.
|
|
180
|
+
exportToRemote(event, env);
|
|
181
|
+
return event;
|
|
182
|
+
}
|