@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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/event-bus.mjs — append-only event log at ~/.cx/events.jsonl.
|
|
3
|
+
*
|
|
4
|
+
* Hook processes emit failure/signal events here. The router + gateway read
|
|
5
|
+
* recent events to decide escalation. Lines are rotated to keep the file
|
|
6
|
+
* bounded. Fingerprint = sha1(type + project + first summary line) so the
|
|
7
|
+
* same failure dedups across emits.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const MAX_LINES = 1000;
|
|
16
|
+
|
|
17
|
+
function rootDir() {
|
|
18
|
+
return process.env.CONSTRUCT_ROLES_ROOT || join(homedir(), '.cx');
|
|
19
|
+
}
|
|
20
|
+
function eventsPath() {
|
|
21
|
+
return join(rootDir(), 'events.jsonl');
|
|
22
|
+
}
|
|
23
|
+
function ensureDir() {
|
|
24
|
+
const r = rootDir();
|
|
25
|
+
if (!existsSync(r)) mkdirSync(r, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fingerprintOf(type, project, summary) {
|
|
29
|
+
const firstLine = String(summary || '').split('\n')[0].trim();
|
|
30
|
+
return createHash('sha1').update(`${type}|${project}|${firstLine}`).digest('hex').slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function emit(eventType, payload = {}) {
|
|
34
|
+
ensureDir();
|
|
35
|
+
const project = payload.project || '';
|
|
36
|
+
const summary = payload.summary || '';
|
|
37
|
+
const fingerprint = fingerprintOf(eventType, project, summary);
|
|
38
|
+
const line = {
|
|
39
|
+
ts: Date.now(),
|
|
40
|
+
type: eventType,
|
|
41
|
+
project,
|
|
42
|
+
branch: payload.branch || '',
|
|
43
|
+
cwd: payload.cwd || '',
|
|
44
|
+
summary,
|
|
45
|
+
context: payload.context ?? null,
|
|
46
|
+
fingerprint,
|
|
47
|
+
};
|
|
48
|
+
appendFileSync(eventsPath(), JSON.stringify(line) + '\n');
|
|
49
|
+
rotate();
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function recent({ since = 0, type, fingerprint, limit = 200 } = {}) {
|
|
54
|
+
const ep = eventsPath();
|
|
55
|
+
if (!existsSync(ep)) return [];
|
|
56
|
+
const raw = readFileSync(ep, 'utf8').split('\n').filter(Boolean);
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let i = raw.length - 1; i >= 0 && out.length < limit; i--) {
|
|
59
|
+
let entry;
|
|
60
|
+
try { entry = JSON.parse(raw[i]); } catch { continue; }
|
|
61
|
+
if (since && entry.ts < since) break;
|
|
62
|
+
if (type && entry.type !== type) continue;
|
|
63
|
+
if (fingerprint && entry.fingerprint !== fingerprint) continue;
|
|
64
|
+
out.push(entry);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rotate() {
|
|
70
|
+
try {
|
|
71
|
+
const ep = eventsPath();
|
|
72
|
+
const raw = readFileSync(ep, 'utf8').split('\n').filter(Boolean);
|
|
73
|
+
if (raw.length <= MAX_LINES) return;
|
|
74
|
+
const trimmed = raw.slice(raw.length - MAX_LINES).join('\n') + '\n';
|
|
75
|
+
writeFileSync(ep, trimmed);
|
|
76
|
+
} catch { /* rotate is best-effort */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const _paths = { rootDir, eventsPath };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/fence.mjs — pure action-vs-manifest fence check.
|
|
3
|
+
*
|
|
4
|
+
* Invoked from the policy-engine and edit-guard hooks when a session is
|
|
5
|
+
* tagged with a persona. Glob patterns use simple ** / * matching (no full
|
|
6
|
+
* minimatch).
|
|
7
|
+
*
|
|
8
|
+
* Decision semantics:
|
|
9
|
+
* - allowed: target is inside the persona's allowedPaths/Commands/Labels
|
|
10
|
+
* - approval: target matches an entry in approvalRequired (needs user yes)
|
|
11
|
+
* - denied: neither — default deny
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { loadManifest } from './manifest.mjs';
|
|
15
|
+
|
|
16
|
+
function globMatch(pattern, target) {
|
|
17
|
+
if (!pattern || !target) return false;
|
|
18
|
+
const re = pattern
|
|
19
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
20
|
+
.replace(/\*\*\//g, '__GLOBSTARSLASH__')
|
|
21
|
+
.replace(/\*\*/g, '__GLOBSTAR__')
|
|
22
|
+
.replace(/\*/g, '[^/]*')
|
|
23
|
+
.replace(/__GLOBSTARSLASH__/g, '(?:.*\\/)?')
|
|
24
|
+
.replace(/__GLOBSTAR__/g, '.*');
|
|
25
|
+
return new RegExp('^' + re + '$').test(target);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function matchAny(patterns, target) {
|
|
29
|
+
if (!Array.isArray(patterns)) return false;
|
|
30
|
+
return patterns.some((p) => globMatch(p, target));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function actionPrefix(action) {
|
|
34
|
+
const idx = action.indexOf(':');
|
|
35
|
+
return idx === -1 ? action : action.slice(0, idx);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function checkAction({ personaId, action, target = '' }) {
|
|
39
|
+
const manifest = loadManifest(personaId);
|
|
40
|
+
if (!manifest) return { allowed: false, reason: 'persona-not-onboarded' };
|
|
41
|
+
const fence = manifest.fence || {};
|
|
42
|
+
|
|
43
|
+
if (action === 'edit' || action === 'write') {
|
|
44
|
+
if (matchAny(fence.allowedPaths, target)) return { allowed: true };
|
|
45
|
+
const approvalForms = [`edit:${target}`, 'edit:**', `${action}:${target}`];
|
|
46
|
+
if (
|
|
47
|
+
(fence.approvalRequired || []).some((req) =>
|
|
48
|
+
req === 'edit' || req === action || approvalForms.includes(req) || globMatch(req.replace(/^edit:/, ''), target)
|
|
49
|
+
)
|
|
50
|
+
) {
|
|
51
|
+
return { allowed: false, reason: 'needs-approval', approval: true };
|
|
52
|
+
}
|
|
53
|
+
return { allowed: false, reason: 'outside-fence' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (action === 'bash') {
|
|
57
|
+
const cmd = String(target).trim();
|
|
58
|
+
const allowed = fence.allowedCommands || [];
|
|
59
|
+
const isPrefixMatch = allowed.some((c) => cmd === c || cmd.startsWith(c + ' '));
|
|
60
|
+
if (isPrefixMatch) return { allowed: true };
|
|
61
|
+
if ((fence.approvalRequired || []).includes('bash')) {
|
|
62
|
+
return { allowed: false, reason: 'needs-approval', approval: true };
|
|
63
|
+
}
|
|
64
|
+
return { allowed: false, reason: 'outside-fence' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (action === 'commit' || action === 'push') {
|
|
68
|
+
if ((fence.approvalRequired || []).includes(action)) {
|
|
69
|
+
return { allowed: false, reason: 'needs-approval', approval: true };
|
|
70
|
+
}
|
|
71
|
+
return { allowed: false, reason: 'outside-fence' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (action === 'bd-label') {
|
|
75
|
+
const labels = String(target).split(',').map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
const allowed = fence.allowedBdLabels || [];
|
|
77
|
+
if (labels.every((l) => allowed.includes(l) || l.startsWith('next:'))) return { allowed: true };
|
|
78
|
+
return { allowed: false, reason: 'label-outside-fence' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { allowed: false, reason: 'unknown-action' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { globMatch };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/gateway.mjs — single entry point hooks call to record events and
|
|
3
|
+
* (maybe) invoke a persona.
|
|
4
|
+
*
|
|
5
|
+
* Flow: kill-switch checks → emit to bus → route → threshold/cooldown/rate
|
|
6
|
+
* ceiling → create bd issue → queue pending invocation → SSE toast.
|
|
7
|
+
*
|
|
8
|
+
* Defensive: every step is best-effort so a partial failure (bd unavailable,
|
|
9
|
+
* SSE bus unloaded) never blocks the calling hook. Returns a structured
|
|
10
|
+
* decision so callers can log / chain.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { emit, _paths } from './event-bus.mjs';
|
|
18
|
+
import { route as routeEvent } from './router.mjs';
|
|
19
|
+
import { runBd } from '../beads-client.mjs';
|
|
20
|
+
|
|
21
|
+
function rootDir() {
|
|
22
|
+
return process.env.CONSTRUCT_ROLES_ROOT || join(homedir(), '.cx');
|
|
23
|
+
}
|
|
24
|
+
function pendingPath() {
|
|
25
|
+
return join(rootDir(), 'role-pending.jsonl');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULTS = {
|
|
29
|
+
thresholdHits: 2,
|
|
30
|
+
thresholdWindowMs: 10 * 60 * 1000,
|
|
31
|
+
cooldownMs: 30 * 60 * 1000,
|
|
32
|
+
rateCeilingPerHour: 3,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function ensureDir() {
|
|
36
|
+
const r = rootDir();
|
|
37
|
+
if (!existsSync(r)) mkdirSync(r, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readPending() {
|
|
41
|
+
const pp = pendingPath();
|
|
42
|
+
if (!existsSync(pp)) return [];
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(pp, 'utf8')
|
|
45
|
+
.split('\n')
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
} catch { return []; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function appendPending(entry) {
|
|
53
|
+
ensureDir();
|
|
54
|
+
appendFileSync(pendingPath(), JSON.stringify(entry) + '\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function shouldEscalate(event, manifest, now = Date.now()) {
|
|
58
|
+
const fp = event.fingerprint;
|
|
59
|
+
const pending = readPending();
|
|
60
|
+
|
|
61
|
+
// Cooldown: same fingerprint already escalated within window?
|
|
62
|
+
const recentSame = pending.find(
|
|
63
|
+
(p) => p.fingerprint === fp && now - p.ts < DEFAULTS.cooldownMs
|
|
64
|
+
);
|
|
65
|
+
if (recentSame) return { escalate: false, reason: 'cooldown' };
|
|
66
|
+
|
|
67
|
+
// Rate ceiling per persona per hour
|
|
68
|
+
const personaId = (manifest && manifest.killSwitchEnv) ? manifest.killSwitchEnv : '';
|
|
69
|
+
const hourAgo = now - 60 * 60 * 1000;
|
|
70
|
+
const recentForPersona = pending.filter(
|
|
71
|
+
(p) => p.ts > hourAgo && p.killSwitchEnv === personaId
|
|
72
|
+
);
|
|
73
|
+
if (recentForPersona.length >= DEFAULTS.rateCeilingPerHour) {
|
|
74
|
+
return { escalate: false, reason: 'rate-ceiling' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Severity short-circuit: immediate on first hit
|
|
78
|
+
const immediate = Array.isArray(manifest?.severityImmediate)
|
|
79
|
+
&& manifest.severityImmediate.includes(event.type);
|
|
80
|
+
if (immediate) return { escalate: true, reason: 'severity-immediate', count: 1 };
|
|
81
|
+
|
|
82
|
+
// Threshold: N hits within window from events.jsonl
|
|
83
|
+
const ep = _paths.eventsPath();
|
|
84
|
+
let hits = 0;
|
|
85
|
+
if (existsSync(ep)) {
|
|
86
|
+
const lines = readFileSync(ep, 'utf8').split('\n').filter(Boolean);
|
|
87
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
88
|
+
let e;
|
|
89
|
+
try { e = JSON.parse(lines[i]); } catch { continue; }
|
|
90
|
+
if (now - e.ts > DEFAULTS.thresholdWindowMs) break;
|
|
91
|
+
if (e.fingerprint === fp) hits++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (hits >= DEFAULTS.thresholdHits) {
|
|
95
|
+
return { escalate: true, reason: 'threshold', count: hits };
|
|
96
|
+
}
|
|
97
|
+
return { escalate: false, reason: 'below-threshold', count: hits };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function createBdIncident(event, routeResult) {
|
|
101
|
+
const labels = (routeResult.manifest.outputs?.bdLabels || []).join(',') || 'incident';
|
|
102
|
+
const firstLine = String(event.summary || '').split('\n')[0].slice(0, 120);
|
|
103
|
+
const title = `[${routeResult.personaId}] ${event.type} — ${firstLine}`;
|
|
104
|
+
const body = JSON.stringify(
|
|
105
|
+
{
|
|
106
|
+
eventType: event.type,
|
|
107
|
+
ts: event.ts,
|
|
108
|
+
project: event.project,
|
|
109
|
+
branch: event.branch,
|
|
110
|
+
cwd: event.cwd,
|
|
111
|
+
summary: event.summary,
|
|
112
|
+
context: event.context,
|
|
113
|
+
fingerprint: event.fingerprint,
|
|
114
|
+
fence: routeResult.manifest.fence || {},
|
|
115
|
+
handoffCandidates: routeResult.manifest.handoffCandidates || [],
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
2
|
|
119
|
+
);
|
|
120
|
+
const result = await runBd(
|
|
121
|
+
['create', title, '-t', 'bug', '-l', labels, '-d', body],
|
|
122
|
+
{ actor: `roles:${routeResult.personaId}`, silent: true, timeoutSeconds: 10, commandTimeoutSeconds: 30 }
|
|
123
|
+
);
|
|
124
|
+
if (!result.success) return { success: false, error: result.error || 'bd-failed' };
|
|
125
|
+
const match = String(result.output || '').match(/Created issue:\s*([\w-]+)/);
|
|
126
|
+
return { success: true, issueId: match ? match[1] : null, raw: result.output };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function emitToastBestEffort(payload) {
|
|
130
|
+
try {
|
|
131
|
+
const mod = await import('../embed/notifications.mjs');
|
|
132
|
+
if (typeof mod.emitEmbedNotification === 'function') {
|
|
133
|
+
mod.emitEmbedNotification(payload);
|
|
134
|
+
}
|
|
135
|
+
} catch { /* dashboard not loaded; toast is best-effort */ }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function tryEscalate(event) {
|
|
139
|
+
const r = routeEvent(event);
|
|
140
|
+
if (!r) return { escalated: false, event, reason: 'no-owner' };
|
|
141
|
+
|
|
142
|
+
if (r.manifest.killSwitchEnv && process.env[r.manifest.killSwitchEnv] === 'off') {
|
|
143
|
+
return { escalated: false, event, reason: 'persona-off' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const decision = shouldEscalate(event, r.manifest);
|
|
147
|
+
if (!decision.escalate) {
|
|
148
|
+
return { escalated: false, event, reason: decision.reason, decision };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const { checkBudget } = await import('../cost-ledger.mjs');
|
|
153
|
+
const budget = checkBudget({ personaId: r.personaId });
|
|
154
|
+
if (!budget.allowed) {
|
|
155
|
+
return { escalated: false, event, reason: 'budget-exhausted', budget };
|
|
156
|
+
}
|
|
157
|
+
} catch { /* cost ledger optional — never block on missing module */ }
|
|
158
|
+
|
|
159
|
+
const bd = await createBdIncident(event, r);
|
|
160
|
+
if (!bd.success) {
|
|
161
|
+
return { escalated: false, event, reason: 'bd-create-failed', error: bd.error };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pendingEntry = {
|
|
165
|
+
ts: Date.now(),
|
|
166
|
+
personaId: r.personaId,
|
|
167
|
+
cxId: r.cxId,
|
|
168
|
+
bdIssueId: bd.issueId,
|
|
169
|
+
fingerprint: event.fingerprint,
|
|
170
|
+
eventType: event.type,
|
|
171
|
+
summary: String(event.summary || '').split('\n')[0],
|
|
172
|
+
killSwitchEnv: r.manifest.killSwitchEnv || '',
|
|
173
|
+
};
|
|
174
|
+
appendPending(pendingEntry);
|
|
175
|
+
|
|
176
|
+
await emitToastBestEffort({
|
|
177
|
+
type: 'warning',
|
|
178
|
+
source: 'roles',
|
|
179
|
+
message: `${r.cxId}: ${pendingEntry.summary}`,
|
|
180
|
+
meta: {
|
|
181
|
+
personaId: r.personaId,
|
|
182
|
+
bdIssueId: bd.issueId,
|
|
183
|
+
fingerprint: event.fingerprint,
|
|
184
|
+
eventType: event.type,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { escalated: true, event, personaId: r.personaId, bdIssueId: bd.issueId, decision };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function recordAndMaybeInvoke(eventType, payload = {}) {
|
|
192
|
+
if (process.env.CONSTRUCT_ROLES === 'off') {
|
|
193
|
+
return { recorded: false, escalated: false, reason: 'global-off' };
|
|
194
|
+
}
|
|
195
|
+
const event = emit(eventType, payload);
|
|
196
|
+
const r = await tryEscalate(event);
|
|
197
|
+
return { recorded: true, ...r };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function listPending({ unresolved = true } = {}) {
|
|
201
|
+
const entries = readPending();
|
|
202
|
+
if (!unresolved) return entries;
|
|
203
|
+
return entries.filter((e) => !e.resolvedAt);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Drain events that accumulated outside a Claude session. Walks recent
|
|
207
|
+
// events.jsonl, runs each unescalated one through tryEscalate. Bounded by
|
|
208
|
+
// `sinceMs` and `maxProcess` so a long-quiet machine can't trigger a flood.
|
|
209
|
+
|
|
210
|
+
export async function processBacklog({ sinceMs = 60 * 60 * 1000, maxProcess = 10 } = {}) {
|
|
211
|
+
if (process.env.CONSTRUCT_ROLES === 'off') return { processed: 0, escalated: 0, reason: 'global-off' };
|
|
212
|
+
|
|
213
|
+
const ep = _paths.eventsPath();
|
|
214
|
+
if (!existsSync(ep)) return { processed: 0, escalated: 0 };
|
|
215
|
+
const cutoff = Date.now() - sinceMs;
|
|
216
|
+
const lines = readFileSync(ep, 'utf8').split('\n').filter(Boolean);
|
|
217
|
+
const pending = readPending();
|
|
218
|
+
const pendingFps = new Set(pending.map((p) => p.fingerprint));
|
|
219
|
+
|
|
220
|
+
const candidates = [];
|
|
221
|
+
for (let i = lines.length - 1; i >= 0 && candidates.length < maxProcess * 4; i--) {
|
|
222
|
+
let e;
|
|
223
|
+
try { e = JSON.parse(lines[i]); } catch { continue; }
|
|
224
|
+
if (e.ts < cutoff) break;
|
|
225
|
+
if (pendingFps.has(e.fingerprint)) continue;
|
|
226
|
+
candidates.push(e);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let escalated = 0;
|
|
230
|
+
let processed = 0;
|
|
231
|
+
for (const e of candidates.reverse()) {
|
|
232
|
+
if (processed >= maxProcess) break;
|
|
233
|
+
processed++;
|
|
234
|
+
const r = await tryEscalate(e);
|
|
235
|
+
if (r.escalated) escalated++;
|
|
236
|
+
}
|
|
237
|
+
return { processed, escalated };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function markResolved(fingerprint) {
|
|
241
|
+
const entries = readPending();
|
|
242
|
+
let changed = false;
|
|
243
|
+
const next = entries.map((e) => {
|
|
244
|
+
if (e.fingerprint === fingerprint && !e.resolvedAt) {
|
|
245
|
+
changed = true;
|
|
246
|
+
return { ...e, resolvedAt: Date.now() };
|
|
247
|
+
}
|
|
248
|
+
return e;
|
|
249
|
+
});
|
|
250
|
+
if (!changed) return false;
|
|
251
|
+
writeFileSync(pendingPath(), next.map((e) => JSON.stringify(e)).join('\n') + '\n');
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function resetPending() {
|
|
256
|
+
const pp = pendingPath();
|
|
257
|
+
if (existsSync(pp)) writeFileSync(pp, '');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const _gatewayPaths = { rootDir, pendingPath };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/hook-emit.mjs — synchronous one-liner hooks call to publish events.
|
|
3
|
+
*
|
|
4
|
+
* Wraps event-bus.emit with auto-detection of project + branch from the hook
|
|
5
|
+
* input. Never throws — hooks must stay on the hot path. Returns the event or
|
|
6
|
+
* null on failure. Use `emitRoleEvent({ type, summary, hookInput, context })`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { emit } from './event-bus.mjs';
|
|
11
|
+
|
|
12
|
+
function gitInfo(cwd) {
|
|
13
|
+
try {
|
|
14
|
+
const project = execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 1000 }).toString().trim();
|
|
15
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { cwd, timeout: 1000 }).toString().trim();
|
|
16
|
+
return { project, branch };
|
|
17
|
+
} catch {
|
|
18
|
+
return { project: cwd || '', branch: '' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function emitRoleEvent({ type, summary, hookInput = {}, context = null }) {
|
|
23
|
+
if (process.env.CONSTRUCT_ROLES === 'off') return null;
|
|
24
|
+
try {
|
|
25
|
+
const cwd = hookInput.cwd || process.cwd();
|
|
26
|
+
const { project, branch } = gitInfo(cwd);
|
|
27
|
+
return emit(type, {
|
|
28
|
+
project,
|
|
29
|
+
branch,
|
|
30
|
+
cwd,
|
|
31
|
+
summary: String(summary || '').slice(0, 4096),
|
|
32
|
+
context,
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/manifest.mjs — role manifest loader.
|
|
3
|
+
*
|
|
4
|
+
* Reads agents/role-manifests.json. A persona is "onboarded" when its
|
|
5
|
+
* `events` array is non-empty. Empty entries reserve the slot for future
|
|
6
|
+
* wiring without code change.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const MANIFEST_PATH = join(__dirname, '..', '..', 'agents', 'role-manifests.json');
|
|
15
|
+
|
|
16
|
+
let cached = null;
|
|
17
|
+
|
|
18
|
+
function load() {
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
const raw = readFileSync(MANIFEST_PATH, 'utf8');
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
cached = parsed.personas || {};
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadManifest(personaId) {
|
|
27
|
+
const id = String(personaId).replace(/^cx-/, '');
|
|
28
|
+
const all = load();
|
|
29
|
+
return all[id] || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isOnboarded(personaId) {
|
|
33
|
+
const m = loadManifest(personaId);
|
|
34
|
+
return !!(m && Array.isArray(m.events) && m.events.length > 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listOnboardedPersonas() {
|
|
38
|
+
const all = load();
|
|
39
|
+
return Object.keys(all).filter((id) => isOnboarded(id));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function listAllPersonas() {
|
|
43
|
+
return Object.keys(load());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function _resetCache() {
|
|
47
|
+
cached = null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/roles/router.mjs — pure event → persona resolver.
|
|
3
|
+
*
|
|
4
|
+
* Uses EVENT_OWNERSHIP from orchestration-policy and validates against
|
|
5
|
+
* the loaded role manifest. Returns null (no owner) silently if the
|
|
6
|
+
* event has no declared owner or the persona is not yet onboarded —
|
|
7
|
+
* letting us declare ownership ahead of full wiring.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EVENT_OWNERSHIP } from '../orchestration-policy.mjs';
|
|
11
|
+
import { loadManifest, isOnboarded } from './manifest.mjs';
|
|
12
|
+
|
|
13
|
+
export function route(event) {
|
|
14
|
+
const type = event?.type;
|
|
15
|
+
if (!type) return null;
|
|
16
|
+
const ownerCxId = EVENT_OWNERSHIP[type];
|
|
17
|
+
if (!ownerCxId) return null;
|
|
18
|
+
const personaId = ownerCxId.replace(/^cx-/, '');
|
|
19
|
+
if (!isOnboarded(personaId)) return null;
|
|
20
|
+
const manifest = loadManifest(personaId);
|
|
21
|
+
if (!manifest.events.includes(type)) return null;
|
|
22
|
+
return { personaId, cxId: ownerCxId, manifest };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ownerOf(eventType) {
|
|
26
|
+
return EVENT_OWNERSHIP[eventType] || null;
|
|
27
|
+
}
|