@geraldmaron/construct 1.2.3 → 1.3.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/README.md +7 -10
- package/bin/construct +235 -209
- package/bin/construct-postinstall.mjs +0 -21
- package/commands/work/optimize-prompts.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/artifact-loop-core.mjs +412 -0
- package/lib/artifact-manifest.mjs +6 -0
- package/lib/artifact-release-gate.mjs +115 -49
- package/lib/audit-rules.mjs +2 -4
- package/lib/audit-skills.mjs +32 -20
- package/lib/audit-specialists.mjs +44 -26
- package/lib/auto-docs.mjs +6 -5
- package/lib/brand-prose.mjs +3 -3
- package/lib/brand-tokens.mjs +2 -2
- package/lib/certification/artifact-fixtures.mjs +4 -1
- package/lib/certification/demo-parity.mjs +6 -32
- package/lib/certification/real-llm-scenarios.mjs +58 -13
- package/lib/certification/role-cards.mjs +7 -6
- package/lib/certification/role-overlays.mjs +4 -4
- package/lib/certification/runner.mjs +9 -2
- package/lib/certification/skill-inventory.mjs +34 -22
- package/lib/certification/skill-scenarios.mjs +21 -8
- package/lib/certification/specialist-contracts.mjs +4 -3
- package/lib/certification/specialist-scenarios.mjs +7 -6
- package/lib/certification/status.mjs +5 -4
- package/lib/cli-commands.mjs +55 -54
- package/lib/comment-lint.mjs +1 -5
- package/lib/completions.mjs +20 -2
- package/lib/config/legacy-config-migration.mjs +90 -24
- package/lib/config/schema.mjs +5 -5
- package/lib/config/source-targets.mjs +80 -0
- package/lib/contracts/validate.mjs +119 -51
- package/lib/decisions/golden.mjs +3 -2
- package/lib/decisions/registry.mjs +11 -6
- package/lib/demo-project.mjs +188 -0
- package/lib/demo-recording.mjs +9 -34
- package/lib/demo-script.mjs +2 -2
- package/lib/demo-surface.mjs +13 -131
- package/lib/demo-tour-renderer.mjs +92 -0
- package/lib/demo.mjs +69 -54
- package/lib/diagram-export.mjs +63 -14
- package/lib/doctor/cli.mjs +12 -2
- package/lib/doctor/source-checkout.mjs +3 -4
- package/lib/doctor/watchers/consistency.mjs +148 -43
- package/lib/doctor/watchers/credential-parity.mjs +97 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +4 -32
- package/lib/document-export.mjs +73 -9
- package/lib/document-extract.mjs +10 -198
- package/lib/embed/artifact.mjs +2 -3
- package/lib/embed/cli.mjs +1 -1
- package/lib/embed/config.mjs +1 -2
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/daemon.mjs +5 -6
- package/lib/embed/demand-fetch.mjs +114 -2
- package/lib/embed/docs-lifecycle.mjs +9 -5
- package/lib/embed/intake-metrics.mjs +2 -2
- package/lib/embed/notifications.mjs +2 -3
- package/lib/embed/recommendation-store.mjs +25 -22
- package/lib/embed/role-framing.mjs +4 -9
- package/lib/embedded-contract/capability.mjs +4 -2
- package/lib/embedded-contract/contract-version.mjs +1 -1
- package/lib/env-config.mjs +2 -2
- package/lib/flavors/loader.mjs +21 -19
- package/lib/graph/build-from-registry.mjs +31 -7
- package/lib/graph/staleness.mjs +1 -1
- package/lib/headhunt.mjs +82 -35
- package/lib/hooks/agent-tracker.mjs +2 -2
- package/lib/hooks/config-protection.mjs +1 -1
- package/lib/hooks/registry-sync.mjs +3 -3
- package/lib/host-capabilities.mjs +5 -5
- package/lib/improvement/controller.mjs +2 -2
- package/lib/init-unified.mjs +19 -21
- package/lib/intake/classify.mjs +26 -0
- package/lib/intake/daemon.mjs +2 -3
- package/lib/intake/prepare.mjs +10 -3
- package/lib/intake/session-prelude.mjs +1 -1
- package/lib/intake/tables/creative.mjs +9 -9
- package/lib/intake/tables/operations.mjs +4 -4
- package/lib/intake/tables/research.mjs +2 -2
- package/lib/integrations/intake-integrations.mjs +9 -10
- package/lib/knowledge/research-store.mjs +2 -2
- package/lib/mcp/server.mjs +104 -61
- package/lib/mcp/tool-budget.mjs +1 -46
- package/lib/mcp/tool-recovery.mjs +44 -0
- package/lib/mcp/tools/artifact-author.mjs +36 -0
- package/lib/mcp/tools/find-tool.mjs +86 -0
- package/lib/mcp/tools/orchestration-run.mjs +107 -69
- package/lib/mcp/tools/project.mjs +4 -3
- package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
- package/lib/mcp/tools/skills.mjs +39 -22
- package/lib/mcp/tools/telemetry.mjs +2 -1
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/mcp-platform-config.mjs +7 -5
- package/lib/migrations/index.mjs +4 -2
- package/lib/migrations/v2-unified-registry.mjs +35 -0
- package/lib/model-router.mjs +203 -36
- package/lib/models/catalog.mjs +25 -9
- package/lib/models/execution-capability-profile.mjs +3 -3
- package/lib/models/execution-policy.mjs +7 -6
- package/lib/models/provider-poll.mjs +18 -4
- package/lib/opencode-config.mjs +56 -1
- package/lib/opencode-runtime-plugin.mjs +13 -10
- package/lib/oracle/artifact-gate.mjs +14 -4
- package/lib/oracle/cli.mjs +2 -0
- package/lib/oracle/dispatch.mjs +18 -1
- package/lib/oracle/execute.mjs +39 -7
- package/lib/oracle/index.mjs +1 -1
- package/lib/oracle/org-graph.mjs +10 -5
- package/lib/oracle/policy.mjs +6 -0
- package/lib/oracle/read-model.mjs +103 -6
- package/lib/oracle/reconcile.mjs +31 -4
- package/lib/oracle/remediation-dispatch.mjs +53 -0
- package/lib/oracle/routing.mjs +5 -0
- package/lib/oracle/synthesize.mjs +80 -3
- package/lib/orchestration/routing-tables.mjs +44 -10
- package/lib/orchestration/runtime.mjs +2 -0
- package/lib/orchestration/worker.mjs +1 -1
- package/lib/orchestration-policy.mjs +351 -62
- package/lib/overrides/resolver.mjs +1 -12
- package/lib/parity.mjs +37 -11
- package/lib/policy/engine.mjs +80 -15
- package/lib/prompt-composer.js +51 -15
- package/lib/prompt-metadata.mjs +3 -2
- package/lib/providers/connection-probe.mjs +58 -0
- package/lib/providers/copilot-auth.mjs +1 -1
- package/lib/providers/credential-bootstrap.mjs +1 -1
- package/lib/providers/op-run.mjs +3 -4
- package/lib/providers/secret-resolver.mjs +31 -0
- package/lib/publish-tooling.mjs +3 -11
- package/lib/publish.mjs +12 -29
- package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
- package/lib/reflect.mjs +2 -2
- package/lib/registry/agent-manifest.mjs +190 -0
- package/lib/registry/assemble.mjs +133 -0
- package/lib/registry/catalog.mjs +171 -0
- package/lib/registry/cli.mjs +590 -6
- package/lib/registry/consolidation.mjs +5 -4
- package/lib/registry/docs-sync.mjs +67 -0
- package/lib/registry/loader.mjs +147 -0
- package/lib/registry/org-io.mjs +76 -0
- package/lib/registry/retired-paths.mjs +71 -0
- package/lib/registry/surface-map.mjs +5 -7
- package/lib/registry/validator.mjs +438 -0
- package/lib/research-execution-policy.mjs +173 -0
- package/lib/roles/catalog.mjs +4 -9
- package/lib/roles/fence.mjs +107 -1
- package/lib/roles/flavor-bindings.mjs +68 -0
- package/lib/roles/gateway.mjs +140 -29
- package/lib/roles/manifest.mjs +22 -13
- package/lib/roles/router.mjs +1 -1
- package/lib/scopes/enrich.mjs +67 -0
- package/lib/scopes/hooks.mjs +12 -0
- package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
- package/lib/scopes/loader.mjs +104 -0
- package/lib/scopes/rebrand.mjs +32 -0
- package/lib/scopes/research-profile.mjs +16 -0
- package/lib/scopes/teams.mjs +98 -0
- package/lib/service-manager.mjs +1 -117
- package/lib/setup-prompts.mjs +1 -1
- package/lib/setup.mjs +6 -26
- package/lib/skills/composition-graph.mjs +98 -0
- package/lib/skills/router.mjs +80 -0
- package/lib/specialist-contracts.mjs +17 -18
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/specialists/prompt-schema.mjs +6 -5
- package/lib/specialists/roster.mjs +7 -12
- package/lib/specialists/schema.mjs +9 -6
- package/lib/status.mjs +25 -90
- package/lib/storage/file-lock.mjs +6 -3
- package/lib/telemetry/skill-calls.mjs +1 -1
- package/lib/templates/doc-presentation.mjs +63 -0
- package/lib/templates/visual-requirements.mjs +35 -2
- package/lib/term-format.mjs +107 -2
- package/lib/test-env-setup.mjs +21 -0
- package/lib/ui/components.mjs +42 -0
- package/lib/ui/glyphs.mjs +58 -0
- package/lib/ui/links.mjs +115 -0
- package/lib/ui/theme.mjs +108 -0
- package/lib/uninstall/uninstall.mjs +2 -1
- package/lib/validator.mjs +45 -8
- package/lib/validators/skill-effectiveness.mjs +174 -0
- package/lib/validators/skills.mjs +1 -1
- package/package.json +12 -8
- package/personas/construct.md +12 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/beads-hygiene.md +52 -0
- package/rules/common/neurodivergent-output.md +4 -7
- package/rules/common/research.md +2 -0
- package/scripts/sync-specialists.mjs +269 -51
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/brand/output-vibe.md +48 -0
- package/skills/docs/adr-workflow.md +3 -0
- package/skills/docs/backlog-proposal-workflow.md +3 -0
- package/skills/docs/codebase-research-workflow.md +5 -2
- package/skills/docs/customer-profile-workflow.md +3 -0
- package/skills/docs/document-ingest-workflow.md +3 -0
- package/skills/docs/evidence-ingest-workflow.md +4 -1
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +1 -1
- package/skills/docs/prfaq-workflow.md +3 -0
- package/skills/docs/product-intelligence-workflow.md +4 -1
- package/skills/docs/product-signal-workflow.md +3 -0
- package/skills/docs/research-workflow.md +13 -6
- package/skills/docs/runbook-workflow.md +3 -0
- package/skills/docs/strategy-workflow.md +3 -0
- package/skills/docs/user-research-workflow.md +6 -3
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
- package/skills/roles/architect.ai-systems.md +1 -1
- package/skills/roles/architect.data.md +1 -1
- package/skills/roles/architect.enterprise.md +1 -1
- package/skills/roles/architect.integration.md +1 -1
- package/skills/roles/architect.md +1 -1
- package/skills/roles/architect.platform.md +1 -1
- package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
- package/skills/roles/data-analyst.experiment.md +1 -1
- package/skills/roles/data-analyst.md +1 -1
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/data-analyst.product.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
- package/skills/roles/data-engineer.pipeline.md +2 -2
- package/skills/roles/data-engineer.vector-retrieval.md +2 -2
- package/skills/roles/data-engineer.warehouse.md +2 -2
- package/skills/roles/debugger.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +1 -1
- package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
- package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
- package/skills/roles/engineer.md +3 -7
- package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
- package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
- package/skills/roles/{operator.md → operations.md} +2 -5
- package/skills/roles/orchestrator.md +1 -1
- package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
- package/skills/roles/product-manager.ai-product.md +1 -1
- package/skills/roles/product-manager.enterprise.md +1 -1
- package/skills/roles/product-manager.growth.md +1 -2
- package/skills/roles/product-manager.md +1 -2
- package/skills/roles/product-manager.platform.md +1 -1
- package/skills/roles/product-manager.product.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -1
- package/skills/roles/qa.api-contract.md +1 -1
- package/skills/roles/qa.data-pipeline.md +1 -1
- package/skills/roles/qa.md +1 -1
- package/skills/roles/qa.web-ui.md +1 -1
- package/skills/roles/{operator.release.md → release-manager.md} +3 -3
- package/skills/roles/researcher.md +1 -1
- package/skills/roles/reviewer.md +1 -1
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.appsec.md +1 -1
- package/skills/roles/security.cloud.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +1 -1
- package/skills/roles/security.privacy.md +1 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/{operator.sre.md → sre.md} +3 -3
- package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
- package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
- package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
- package/skills/routing.json +18 -0
- package/skills/routing.md +1 -1
- package/specialists/artifact-manifest.json +2 -1
- package/specialists/audit-enrichments.json +14 -14
- package/specialists/org/contracts/accessibility-to-qa.json +37 -0
- package/specialists/org/contracts/any-to-business-strategist.json +57 -0
- package/specialists/org/contracts/any-to-debugger.json +38 -0
- package/specialists/org/contracts/any-to-designer.json +33 -0
- package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
- package/specialists/org/contracts/any-to-explorer.json +39 -0
- package/specialists/org/contracts/any-to-sre-incident.json +33 -0
- package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
- package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
- package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
- package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
- package/specialists/org/contracts/architect-to-engineer.json +34 -0
- package/specialists/org/contracts/architect-to-evaluator.json +59 -0
- package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
- package/specialists/org/contracts/architect-to-operations.json +45 -0
- package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
- package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
- package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
- package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
- package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
- package/specialists/org/contracts/designer-to-accessibility.json +36 -0
- package/specialists/org/contracts/engineer-to-qa.json +34 -0
- package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
- package/specialists/org/contracts/explorer-to-engineer.json +38 -0
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
- package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
- package/specialists/org/contracts/product-manager-to-architect.json +71 -0
- package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
- package/specialists/org/contracts/qa-to-release-manager.json +42 -0
- package/specialists/org/contracts/qa-to-test-automation.json +42 -0
- package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
- package/specialists/org/contracts/researcher-to-architect.json +40 -0
- package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
- package/specialists/org/contracts/reviewer-to-security.json +41 -0
- package/specialists/org/contracts/sre-to-release-manager.json +36 -0
- package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
- package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
- package/specialists/org/contracts/user-to-construct.json +30 -0
- package/specialists/org/groups/engineering-group.json +43 -0
- package/specialists/org/groups/governance-group.json +38 -0
- package/specialists/org/groups/operations-group.json +41 -0
- package/specialists/org/groups/product-group.json +46 -0
- package/specialists/org/groups/quality-group.json +42 -0
- package/specialists/org/groups/strategy-group.json +41 -0
- package/specialists/org/policies/action-approval.json +13 -0
- package/specialists/org/policies/agents-routing.json +13 -0
- package/specialists/org/policies/architecture.json +20 -0
- package/specialists/org/policies/bash-safety.json +13 -0
- package/specialists/org/policies/beads-hygiene.json +13 -0
- package/specialists/org/policies/bootstrap-state.json +13 -0
- package/specialists/org/policies/code-review.json +13 -0
- package/specialists/org/policies/coding-style.json +13 -0
- package/specialists/org/policies/comments.json +13 -0
- package/specialists/org/policies/commit-approval.json +13 -0
- package/specialists/org/policies/contract-preconditions.json +13 -0
- package/specialists/org/policies/deployment.json +20 -0
- package/specialists/org/policies/description.json +14 -0
- package/specialists/org/policies/design-approval.json +19 -0
- package/specialists/org/policies/doc-ownership.json +13 -0
- package/specialists/org/policies/file-path-fence.json +13 -0
- package/specialists/org/policies/framing.json +13 -0
- package/specialists/org/policies/git-workflow.json +13 -0
- package/specialists/org/policies/incident-response.json +15 -0
- package/specialists/org/policies/intake-triage.json +15 -0
- package/specialists/org/policies/neurodivergent-output.json +13 -0
- package/specialists/org/policies/no-fabrication.json +13 -0
- package/specialists/org/policies/patterns.json +13 -0
- package/specialists/org/policies/quality-gate-approval.json +17 -0
- package/specialists/org/policies/release-gates.json +13 -0
- package/specialists/org/policies/research-evidence.json +13 -0
- package/specialists/org/policies/review-before-change.json +13 -0
- package/specialists/org/policies/rollback.json +15 -0
- package/specialists/org/policies/scope-change.json +20 -0
- package/specialists/org/policies/secret-scan.json +13 -0
- package/specialists/org/policies/security-approval.json +17 -0
- package/specialists/org/policies/security.json +13 -0
- package/specialists/org/policies/session-efficiency.json +13 -0
- package/specialists/org/policies/skill-routing.json +13 -0
- package/specialists/org/policies/strategic-prioritization.json +17 -0
- package/specialists/org/policies/testing.json +13 -0
- package/specialists/org/policies/tool-invisibility.json +14 -0
- package/specialists/org/scopes/creative.json +54 -0
- package/specialists/org/scopes/operations.json +51 -0
- package/specialists/org/scopes/research.json +60 -0
- package/specialists/org/scopes/rnd.json +81 -0
- package/specialists/org/specialists/cx-accessibility.json +69 -0
- package/specialists/org/specialists/cx-ai-engineer.json +75 -0
- package/specialists/org/specialists/cx-architect.json +89 -0
- package/specialists/org/specialists/cx-business-strategist.json +72 -0
- package/specialists/org/specialists/cx-data-analyst.json +68 -0
- package/specialists/org/specialists/cx-data-engineer.json +71 -0
- package/specialists/org/specialists/cx-debugger.json +75 -0
- package/specialists/org/specialists/cx-designer.json +85 -0
- package/specialists/org/specialists/cx-devil-advocate.json +71 -0
- package/specialists/org/specialists/cx-docs-keeper.json +82 -0
- package/specialists/org/specialists/cx-engineer.json +106 -0
- package/specialists/org/specialists/cx-evaluator.json +69 -0
- package/specialists/org/specialists/cx-explorer.json +69 -0
- package/specialists/org/specialists/cx-legal-compliance.json +74 -0
- package/specialists/org/specialists/cx-operations.json +72 -0
- package/specialists/org/specialists/cx-oracle.json +46 -0
- package/specialists/org/specialists/cx-orchestrator.json +81 -0
- package/specialists/org/specialists/cx-platform-engineer.json +80 -0
- package/specialists/org/specialists/cx-product-manager.json +102 -0
- package/specialists/org/specialists/cx-qa.json +79 -0
- package/specialists/org/specialists/cx-rd-lead.json +73 -0
- package/specialists/org/specialists/cx-release-manager.json +77 -0
- package/specialists/org/specialists/cx-researcher.json +82 -0
- package/specialists/org/specialists/cx-reviewer.json +71 -0
- package/specialists/org/specialists/cx-security.json +91 -0
- package/specialists/org/specialists/cx-sre.json +94 -0
- package/specialists/org/specialists/cx-test-automation.json +69 -0
- package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
- package/specialists/org/specialists/cx-ux-researcher.json +68 -0
- package/specialists/org/teams/accessibility-team.json +39 -0
- package/specialists/org/teams/design-team.json +40 -0
- package/specialists/org/teams/engineering-team.json +50 -0
- package/specialists/org/teams/governance-team.json +41 -0
- package/specialists/org/teams/operations-team.json +46 -0
- package/specialists/org/teams/product-management-team.json +45 -0
- package/specialists/org/teams/quality-team.json +49 -0
- package/specialists/org/teams/research-team.json +39 -0
- package/specialists/org/teams/strategy-team.json +48 -0
- package/specialists/org/teams/ux-research-team.json +39 -0
- package/specialists/prompts/_shared/validation-contract.md +1 -1
- package/specialists/prompts/_team-template.md +10 -0
- package/specialists/prompts/cx-accessibility.md +1 -0
- package/specialists/prompts/cx-ai-engineer.md +1 -1
- package/specialists/prompts/cx-business-strategist.md +1 -1
- package/specialists/prompts/cx-data-engineer.md +1 -1
- package/specialists/prompts/cx-designer.md +1 -0
- package/specialists/prompts/cx-devil-advocate.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-evaluator.md +1 -1
- package/specialists/prompts/cx-explorer.md +1 -1
- package/specialists/prompts/cx-operations.md +1 -1
- package/specialists/prompts/cx-oracle.md +7 -3
- package/specialists/prompts/cx-orchestrator.md +17 -1
- package/specialists/prompts/cx-platform-engineer.md +1 -1
- package/specialists/prompts/cx-product-manager.md +2 -0
- package/specialists/prompts/cx-release-manager.md +1 -1
- package/specialists/prompts/cx-researcher.md +7 -4
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/prompts/cx-test-automation.md +2 -2
- package/specialists/prompts/cx-trace-reviewer.md +3 -3
- package/specialists/prompts/cx-ux-researcher.md +2 -1
- package/templates/demos/scripts/architecture-review-adr.json +30 -0
- package/templates/demos/scripts/capability-contract.json +25 -0
- package/templates/demos/scripts/intake-triage.json +25 -0
- package/templates/demos/scripts/profile-doctor-health.json +25 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
- package/templates/demos/tapes/architecture-review-adr.tape +44 -0
- package/templates/demos/tapes/capability-contract.tape +39 -0
- package/templates/demos/tapes/intake-triage.tape +39 -0
- package/templates/demos/tapes/profile-doctor-health.tape +39 -0
- package/templates/distribution/construct-brand.typ +23 -18
- package/templates/distribution/construct-decision.typ +1 -1
- package/templates/distribution/construct-pdf.typ +1 -1
- package/templates/distribution/construct-prd.typ +1 -1
- package/templates/distribution/construct-research.typ +1 -1
- package/templates/distribution/mermaid-puppeteer.json +8 -0
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prd.md +6 -0
- package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
- package/apps/chat/engine/loop-driver.mjs +0 -211
- package/apps/chat/engine/models.mjs +0 -122
- package/apps/chat/engine/provider-adapters.mjs +0 -171
- package/apps/chat/engine/tools/permission.mjs +0 -54
- package/apps/chat/engine/tools/primitives.mjs +0 -180
- package/apps/chat/engine/tools/registry.mjs +0 -122
- package/apps/chat/engine/turn-controls.mjs +0 -70
- package/lib/boundary.mjs +0 -127
- package/lib/certification/dashboard-api.mjs +0 -71
- package/lib/chat/cli.mjs +0 -333
- package/lib/chat/command-suggest.mjs +0 -161
- package/lib/chat/commands.mjs +0 -215
- package/lib/chat/config.mjs +0 -142
- package/lib/chat/context-compactor.mjs +0 -250
- package/lib/chat/context-continuation.mjs +0 -253
- package/lib/chat/continuation-source.mjs +0 -58
- package/lib/chat/demo-guide.mjs +0 -61
- package/lib/chat/design-tokens.mjs +0 -91
- package/lib/chat/desktop-binary.mjs +0 -81
- package/lib/chat/desktop-build.mjs +0 -130
- package/lib/chat/desktop-launcher.mjs +0 -133
- package/lib/chat/evidence.mjs +0 -145
- package/lib/chat/export.mjs +0 -74
- package/lib/chat/harness/driver.mjs +0 -91
- package/lib/chat/list-picker.mjs +0 -112
- package/lib/chat/model-picker.mjs +0 -356
- package/lib/chat/openrouter-fallback.mjs +0 -151
- package/lib/chat/permission-prompt.mjs +0 -33
- package/lib/chat/picker-catalog.mjs +0 -45
- package/lib/chat/policy-telemetry.mjs +0 -34
- package/lib/chat/present.mjs +0 -246
- package/lib/chat/session-context.mjs +0 -39
- package/lib/chat/session-persist.mjs +0 -73
- package/lib/chat/session-restore.mjs +0 -71
- package/lib/chat/session-settings.mjs +0 -53
- package/lib/chat/system-prompt.mjs +0 -52
- package/lib/chat/transparency.mjs +0 -93
- package/lib/chat/tui/color-scheme.mjs +0 -42
- package/lib/chat/tui/markdown.mjs +0 -123
- package/lib/chat/tui/presentation.mjs +0 -100
- package/lib/chat/tui/render.mjs +0 -500
- package/lib/chat/tui/turn-block.mjs +0 -284
- package/lib/chat/tui/turn-present.mjs +0 -18
- package/lib/chat/tui/turn-state.mjs +0 -88
- package/lib/chat/tui/usage.mjs +0 -122
- package/lib/chat/web-commands.mjs +0 -146
- package/lib/chat/web-launcher.mjs +0 -63
- package/lib/chat/web-picker-keys.mjs +0 -46
- package/lib/chat/web-session.mjs +0 -159
- package/lib/config/alias.mjs +0 -57
- package/lib/dashboard-demo.mjs +0 -71
- package/lib/dashboard-static.mjs +0 -175
- package/lib/install/desktop-binary-download.mjs +0 -88
- package/lib/profiles/loader.mjs +0 -123
- package/lib/profiles/rebrand.mjs +0 -46
- package/lib/server/auth.mjs +0 -169
- package/lib/server/chat-loop.mjs +0 -622
- package/lib/server/chat.mjs +0 -336
- package/lib/server/cors.mjs +0 -77
- package/lib/server/csrf.mjs +0 -103
- package/lib/server/demo-preview.mjs +0 -63
- package/lib/server/index.mjs +0 -2641
- package/lib/server/insights.mjs +0 -780
- package/lib/server/langfuse-login.mjs +0 -58
- package/lib/server/rate-limit.mjs +0 -93
- package/lib/server/telemetry-login.mjs +0 -100
- package/lib/server/webhook.mjs +0 -512
- package/specialists/contracts.json +0 -1032
- package/specialists/contracts.schema.json +0 -83
- package/specialists/policy-inventory.json +0 -188
- package/specialists/registry.json +0 -1412
- package/specialists/role-manifests.json +0 -217
- package/specialists/teams.json +0 -94
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apps/chat/engine/models.mjs — model and provider resolution for the owned loop.
|
|
3
|
-
*
|
|
4
|
-
* The owned loop is provider-agnostic (ADR-0041/ADR-0003): model choice is never
|
|
5
|
-
* hardcoded. This module reuses the core router (lib/model-router.mjs) so the chat
|
|
6
|
-
* surface, the orchestration worker, and the embedded contract all resolve models
|
|
7
|
-
* the same way. resolveChatModelSelection validates CX_MODEL pins against live
|
|
8
|
-
* credential detection; resolveChatModelSelectionAsync also probes Copilot session
|
|
9
|
-
* exchange so a bad token falls through before the first turn.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
getProviderModelCatalog,
|
|
14
|
-
describeModelFamily,
|
|
15
|
-
resolveValidatedChatModel,
|
|
16
|
-
isChatModelAvailable,
|
|
17
|
-
} from '../../../lib/model-router.mjs';
|
|
18
|
-
import { resolveFirstSecret } from '../../../lib/providers/secret-resolver.mjs';
|
|
19
|
-
|
|
20
|
-
export function listChatModels({ env = process.env, cwd = process.cwd(), activeModelId = null } = {}) {
|
|
21
|
-
const { providers } = getProviderModelCatalog({ env, cwd, activeModelId });
|
|
22
|
-
const models = [];
|
|
23
|
-
const seen = new Set();
|
|
24
|
-
for (const provider of providers) {
|
|
25
|
-
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
26
|
-
for (const id of provider.options?.[tier] || []) {
|
|
27
|
-
if (seen.has(id)) continue;
|
|
28
|
-
seen.add(id);
|
|
29
|
-
models.push({
|
|
30
|
-
id,
|
|
31
|
-
label: id,
|
|
32
|
-
provider: provider.id,
|
|
33
|
-
configured: provider.configured,
|
|
34
|
-
local: provider.local === true,
|
|
35
|
-
suitable: true,
|
|
36
|
-
tier,
|
|
37
|
-
available: isChatModelAvailable(id, { env }).ok,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return models.sort((a, b) => Number(b.configured) - Number(a.configured) || a.id.localeCompare(b.id));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { refreshLiveOpenRouterCatalog } from '../../../lib/models/catalog.mjs';
|
|
46
|
-
|
|
47
|
-
export function recommendChatModel({ env = process.env } = {}) {
|
|
48
|
-
const { providers } = getProviderModelCatalog({ env });
|
|
49
|
-
const configured = providers.find((p) => p.configured);
|
|
50
|
-
if (!configured) return null;
|
|
51
|
-
const id = configured.tiers?.standard || configured.tiers?.fast || null;
|
|
52
|
-
if (!id) return null;
|
|
53
|
-
const check = isChatModelAvailable(id, { env });
|
|
54
|
-
if (!check.ok) return null;
|
|
55
|
-
return { id, reason: `configured provider ${configured.label}` };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function resolveChatModelSelection({ env = process.env, requested = null, excludeFamilies = [] } = {}) {
|
|
59
|
-
return resolveValidatedChatModel({ env, requested, excludeFamilies });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function resolveFreeOpenRouterModel({ env = process.env, tier = 'standard', exclude = [] } = {}) {
|
|
63
|
-
const apiKey = resolveFirstSecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
|
|
64
|
-
if (!apiKey) return null;
|
|
65
|
-
const excludeSet = new Set(Array.isArray(exclude) ? exclude : []);
|
|
66
|
-
const { pollFreeModels, topForTier } = await import('../../../lib/model-free-selector.mjs');
|
|
67
|
-
const freeModels = await pollFreeModels(apiKey);
|
|
68
|
-
for (const candidate of topForTier(freeModels, tier, 20)) {
|
|
69
|
-
const modelId = candidate.id.startsWith('openrouter/') ? candidate.id : `openrouter/${candidate.id}`;
|
|
70
|
-
if (excludeSet.has(modelId)) continue;
|
|
71
|
-
if (isChatModelAvailable(modelId, { env }).ok) return modelId;
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function resolveSessionModel(session, { env = process.env, exclude = [], tier = 'standard' } = {}) {
|
|
77
|
-
if (session?.modelMode === 'free-router') {
|
|
78
|
-
const merged = [...new Set([...getExcludeFromSession(session), ...exclude])];
|
|
79
|
-
return resolveFreeOpenRouterModel({ env, tier, exclude: merged });
|
|
80
|
-
}
|
|
81
|
-
return session?.model || session?.savedModel || null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getExcludeFromSession(session) {
|
|
85
|
-
if (!session?.failedModels) return [];
|
|
86
|
-
return session.failedModels instanceof Set ? [...session.failedModels] : [];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function resolveChatModelSelectionAsync({
|
|
90
|
-
env = process.env,
|
|
91
|
-
requested = null,
|
|
92
|
-
fetchImpl = fetch,
|
|
93
|
-
} = {}) {
|
|
94
|
-
let resolution = resolveValidatedChatModel({ env, requested });
|
|
95
|
-
if (!resolution.id?.startsWith('github-copilot/')) return resolution;
|
|
96
|
-
|
|
97
|
-
const { preflightCopilotSession } = await import('../../../lib/providers/copilot-auth.mjs');
|
|
98
|
-
const probe = await preflightCopilotSession({ fetchImpl });
|
|
99
|
-
if (probe.ok) return resolution;
|
|
100
|
-
|
|
101
|
-
const fallback = resolveValidatedChatModel({ env, requested: null, excludeFamilies: ['github-copilot'] });
|
|
102
|
-
if (fallback.id) {
|
|
103
|
-
return {
|
|
104
|
-
...fallback,
|
|
105
|
-
notice: `GitHub Copilot session failed (${probe.message}). Using ${fallback.id}.`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
id: null,
|
|
110
|
-
source: null,
|
|
111
|
-
notice: `GitHub Copilot session failed: ${probe.message}`,
|
|
112
|
-
rejected: resolution.rejected,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function resolveChatModel(opts = {}) {
|
|
117
|
-
return resolveChatModelSelection(opts).id;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function describeChatModel(modelId, { env = process.env } = {}) {
|
|
121
|
-
return describeModelFamily(modelId, { env });
|
|
122
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apps/chat/engine/provider-adapters.mjs — centralized provider execution adapters
|
|
3
|
-
* (construct-6zga.1.3).
|
|
4
|
-
*
|
|
5
|
-
* One registry, keyed by provider group, owns provider execution. Each adapter
|
|
6
|
-
* owns its own credential resolution, base URL, native model-id translation, auth
|
|
7
|
-
* scheme, and AI SDK provider construction; the dispatcher does no
|
|
8
|
-
* provider/model-prefix behavior branch — it extracts the structural provider
|
|
9
|
-
* group (the model id's first segment, the same identity lib/models keys on) and
|
|
10
|
-
* looks the adapter up. Adding a compatible provider is a registry entry plus
|
|
11
|
-
* matrix fixtures, never a dispatch edit.
|
|
12
|
-
*
|
|
13
|
-
* Public model ids are preserved: the native id handed to the SDK is the model id
|
|
14
|
-
* with its group segment stripped, so anthropic/claude-x → claude-x and
|
|
15
|
-
* openrouter/anthropic/claude-x → anthropic/claude-x (OpenRouter's own id).
|
|
16
|
-
* Credentials resolve through the shared secret resolver (env, dotenv, shell rc,
|
|
17
|
-
* 1Password op:// refs); a missing key fails fast with a remediation hint.
|
|
18
|
-
*/
|
|
19
|
-
import { resolveFirstSecret } from '../../../lib/providers/secret-resolver.mjs';
|
|
20
|
-
import { providerGroupForModel } from '../../../lib/models/execution-capability-profile.mjs';
|
|
21
|
-
|
|
22
|
-
const OPENROUTER_BASE = 'https://openrouter.ai/api/v1';
|
|
23
|
-
|
|
24
|
-
function envKey(env, ...names) {
|
|
25
|
-
return resolveFirstSecret(names, { env });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function missingKey(provider, varName) {
|
|
29
|
-
const err = new Error(`No credentials for ${provider}: set ${varName} (or run \`construct creds\`) and retry.`);
|
|
30
|
-
err.code = 'PROVIDER_KEY_MISSING';
|
|
31
|
-
return err;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Adapter descriptors. `describe` is the serializable contract (auth scheme,
|
|
35
|
-
// protocol, credential env, base URL source) for tracing and conformance fixtures;
|
|
36
|
-
// `createModel` builds the AI SDK handle from the native id. The @ai-sdk/* imports
|
|
37
|
-
// stay lazy so the zero-dep core never loads them.
|
|
38
|
-
|
|
39
|
-
const ADAPTERS = {
|
|
40
|
-
anthropic: {
|
|
41
|
-
id: 'anthropic',
|
|
42
|
-
auth: 'api_key',
|
|
43
|
-
protocol: 'anthropic-messages',
|
|
44
|
-
credentialEnv: ['ANTHROPIC_API_KEY'],
|
|
45
|
-
baseURL: 'default',
|
|
46
|
-
async createModel({ nativeModelId, env }) {
|
|
47
|
-
const apiKey = envKey(env, 'ANTHROPIC_API_KEY');
|
|
48
|
-
if (!apiKey) throw missingKey('Anthropic', 'ANTHROPIC_API_KEY');
|
|
49
|
-
const { createAnthropic } = await import('@ai-sdk/anthropic');
|
|
50
|
-
return createAnthropic({ apiKey })(nativeModelId);
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
openai: {
|
|
54
|
-
id: 'openai',
|
|
55
|
-
auth: 'api_key',
|
|
56
|
-
protocol: 'openai-chat-completions',
|
|
57
|
-
credentialEnv: ['OPENAI_API_KEY'],
|
|
58
|
-
baseURL: 'default',
|
|
59
|
-
async createModel({ nativeModelId, env }) {
|
|
60
|
-
const apiKey = envKey(env, 'OPENAI_API_KEY');
|
|
61
|
-
if (!apiKey) throw missingKey('OpenAI', 'OPENAI_API_KEY');
|
|
62
|
-
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
63
|
-
return createOpenAI({ apiKey })(nativeModelId);
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
openrouter: {
|
|
67
|
-
id: 'openrouter',
|
|
68
|
-
auth: 'api_key',
|
|
69
|
-
protocol: 'openai-compatible',
|
|
70
|
-
credentialEnv: ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
71
|
-
baseURL: OPENROUTER_BASE,
|
|
72
|
-
async createModel({ nativeModelId, env }) {
|
|
73
|
-
const apiKey = envKey(env, 'OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY');
|
|
74
|
-
if (!apiKey) throw missingKey('OpenRouter', 'OPENROUTER_API_KEY');
|
|
75
|
-
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
76
|
-
return createOpenAICompatible({ name: 'openrouter', baseURL: OPENROUTER_BASE, apiKey })(nativeModelId);
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
ollama: {
|
|
80
|
-
id: 'ollama',
|
|
81
|
-
auth: 'none',
|
|
82
|
-
protocol: 'openai-compatible',
|
|
83
|
-
credentialEnv: [],
|
|
84
|
-
baseURL: 'env:OLLAMA_BASE_URL',
|
|
85
|
-
async createModel({ modelId, nativeModelId, env }) {
|
|
86
|
-
const { isOllamaModelInstalled, formatOllamaModelMissingMessage } = await import('../../../lib/ollama/installed-models.mjs');
|
|
87
|
-
if (isOllamaModelInstalled(modelId, { env }) === false) {
|
|
88
|
-
const err = new Error(formatOllamaModelMissingMessage(nativeModelId));
|
|
89
|
-
err.code = 'OLLAMA_MODEL_NOT_PULLED';
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
|
-
const baseURL = envKey(env, 'OLLAMA_BASE_URL') || 'http://localhost:11434/v1';
|
|
93
|
-
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
94
|
-
return createOpenAICompatible({ name: 'ollama', baseURL, apiKey: 'ollama' })(nativeModelId);
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
local: {
|
|
98
|
-
id: 'local',
|
|
99
|
-
auth: 'api_key',
|
|
100
|
-
protocol: 'openai-compatible',
|
|
101
|
-
credentialEnv: ['LOCAL_LLM_API_KEY'],
|
|
102
|
-
baseURL: 'env:LOCAL_LLM_BASE_URL',
|
|
103
|
-
async createModel({ nativeModelId, env }) {
|
|
104
|
-
const baseURL = envKey(env, 'LOCAL_LLM_BASE_URL');
|
|
105
|
-
if (!baseURL) throw missingKey('local OpenAI-compatible server', 'LOCAL_LLM_BASE_URL');
|
|
106
|
-
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
107
|
-
return createOpenAICompatible({ name: 'local', baseURL, apiKey: envKey(env, 'LOCAL_LLM_API_KEY') || 'local' })(nativeModelId);
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
'github-copilot': {
|
|
111
|
-
id: 'github-copilot',
|
|
112
|
-
auth: 'oauth',
|
|
113
|
-
protocol: 'openai-chat-completions',
|
|
114
|
-
credentialEnv: [],
|
|
115
|
-
baseURL: 'copilot',
|
|
116
|
-
async createModel({ nativeModelId }) {
|
|
117
|
-
const { getCopilotToken, copilotApiHeaders, COPILOT_API_BASE } = await import('../../../lib/providers/copilot-auth.mjs');
|
|
118
|
-
const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
|
|
119
|
-
const copilotFetch = async (url, init = {}) => {
|
|
120
|
-
const token = await getCopilotToken();
|
|
121
|
-
const headers = new Headers(init.headers);
|
|
122
|
-
for (const [key, value] of Object.entries(copilotApiHeaders())) headers.set(key, value);
|
|
123
|
-
headers.set('Authorization', `Bearer ${token}`);
|
|
124
|
-
return fetch(url, { ...init, headers });
|
|
125
|
-
};
|
|
126
|
-
return createOpenAICompatible({ name: 'github-copilot', baseURL: COPILOT_API_BASE, apiKey: 'via-fetch', fetch: copilotFetch })(nativeModelId);
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export function getProviderAdapter(group) {
|
|
132
|
-
return ADAPTERS[group] || null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Serializable adapter contract for tracing and conformance fixtures (no secrets,
|
|
136
|
-
// no SDK handles) — the metadata a new provider must register.
|
|
137
|
-
|
|
138
|
-
export function describeProviderAdapters() {
|
|
139
|
-
return Object.values(ADAPTERS).map((a) => ({
|
|
140
|
-
id: a.id,
|
|
141
|
-
auth: a.auth,
|
|
142
|
-
protocol: a.protocol,
|
|
143
|
-
credentialEnv: a.credentialEnv,
|
|
144
|
-
baseURL: a.baseURL,
|
|
145
|
-
}));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function nativeModelId(modelId, group = providerGroupForModel(modelId)) {
|
|
149
|
-
return String(modelId).slice(group.length + 1);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Map a Construct "provider/model" id onto an AI SDK language model via the
|
|
154
|
-
* adapter registry. No provider/model-prefix behavior branch: the group is a
|
|
155
|
-
* structural key, the adapter owns the behavior.
|
|
156
|
-
*/
|
|
157
|
-
export async function resolveLanguageModel(modelId, env) {
|
|
158
|
-
if (!modelId) {
|
|
159
|
-
const err = new Error('No model selected and no configured provider found. Run `construct models` or set CX_MODEL_STANDARD.');
|
|
160
|
-
err.code = 'PROVIDER_MODEL_UNRESOLVED';
|
|
161
|
-
throw err;
|
|
162
|
-
}
|
|
163
|
-
const group = providerGroupForModel(modelId);
|
|
164
|
-
const adapter = ADAPTERS[group];
|
|
165
|
-
if (!adapter) {
|
|
166
|
-
const err = new Error(`Provider for model '${modelId}' is not wired into the owned loop yet. Try an anthropic/, openai/, openrouter/, ollama/, local/, or github-copilot/ model.`);
|
|
167
|
-
err.code = 'PROVIDER_UNSUPPORTED';
|
|
168
|
-
throw err;
|
|
169
|
-
}
|
|
170
|
-
return adapter.createModel({ modelId, nativeModelId: nativeModelId(modelId, group), env });
|
|
171
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apps/chat/engine/tools/permission.mjs — the permission and sandbox gate for the
|
|
3
|
-
* owned loop's tools.
|
|
4
|
-
*
|
|
5
|
-
* Owning the loop means Construct, not a host, decides whether a tool runs. This
|
|
6
|
-
* gate reuses the host-agnostic decision vocabulary ADR-0040 defined
|
|
7
|
-
* (allow | allow_always | reject) and the sandbox levels from lib/chat/config.mjs
|
|
8
|
-
* (read-only, workspace-write, danger-full-access). Read-only tools (read, grep,
|
|
9
|
-
* glob) always pass; mutating tools (write, edit, shell) are gated by the sandbox
|
|
10
|
-
* level and the current permission mode, deferring to an interactive
|
|
11
|
-
* `requestPermission` handler only in `ask` mode. `allow_always` is sticky for the
|
|
12
|
-
* rest of the session so a user is not re-prompted per call.
|
|
13
|
-
*
|
|
14
|
-
* `allowOutside` (escape the workspace root) is granted only under
|
|
15
|
-
* danger-full-access; every other level keeps tools inside the workspace, which
|
|
16
|
-
* the primitives enforce independently.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export const READ_ONLY_TOOLS = new Set(['read', 'grep', 'glob']);
|
|
20
|
-
export const MUTATING_TOOLS = new Set(['write', 'edit', 'shell']);
|
|
21
|
-
|
|
22
|
-
export function createPermissionGate({
|
|
23
|
-
getSandbox = () => null,
|
|
24
|
-
getPermissionMode = () => 'allow_once',
|
|
25
|
-
requestPermission = null,
|
|
26
|
-
} = {}) {
|
|
27
|
-
let alwaysAllowed = false;
|
|
28
|
-
|
|
29
|
-
async function check(toolName, input = {}) {
|
|
30
|
-
const sandbox = getSandbox() || 'workspace-write';
|
|
31
|
-
const allowOutside = sandbox === 'danger-full-access';
|
|
32
|
-
|
|
33
|
-
if (READ_ONLY_TOOLS.has(toolName)) return { allowed: true, allowOutside };
|
|
34
|
-
|
|
35
|
-
if (sandbox === 'read-only') {
|
|
36
|
-
return { allowed: false, allowOutside, reason: 'sandbox is read-only; mutating tools are disabled (change with /set sandbox workspace-write)' };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (alwaysAllowed) return { allowed: true, allowOutside };
|
|
40
|
-
|
|
41
|
-
const mode = getPermissionMode() || 'allow_once';
|
|
42
|
-
if (mode === 'reject') return { allowed: false, allowOutside, reason: 'permission mode is reject' };
|
|
43
|
-
if (mode === 'allow_always') { alwaysAllowed = true; return { allowed: true, allowOutside }; }
|
|
44
|
-
if (mode === 'allow_once') return { allowed: true, allowOutside };
|
|
45
|
-
|
|
46
|
-
// ask mode: defer to the interactive handler, mapping its decision back.
|
|
47
|
-
const decision = requestPermission ? await requestPermission({ tool: toolName, input }) : 'allow';
|
|
48
|
-
if (decision === 'reject') return { allowed: false, allowOutside, reason: 'denied by user' };
|
|
49
|
-
if (decision === 'allow_always') { alwaysAllowed = true; return { allowed: true, allowOutside }; }
|
|
50
|
-
return { allowed: true, allowOutside };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { check, get alwaysAllowed() { return alwaysAllowed; } };
|
|
54
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apps/chat/engine/tools/primitives.mjs — zero-dep executors for the owned loop's
|
|
3
|
-
* agent tools (read, write, edit, glob, grep, shell).
|
|
4
|
-
*
|
|
5
|
-
* These are the side-effecting primitives the loop calls; they are pure Node so
|
|
6
|
-
* they are fully testable in a tmpdir with no model, network, or SDK. Path safety
|
|
7
|
-
* is enforced here: every path resolves inside the workspace root unless the caller
|
|
8
|
-
* passes `allowOutside` (set only by the danger-full-access sandbox). Search tools
|
|
9
|
-
* walk the tree with a hard result/size cap and skip the usual heavy/ignored dirs
|
|
10
|
-
* so a grep never hangs the loop. Shell reuses the bounded worker runner
|
|
11
|
-
* (lib/worker/run.mjs) for the timeout + path policy already proven there.
|
|
12
|
-
*
|
|
13
|
-
* Every function returns a structured, JSON-serializable result (never throws for
|
|
14
|
-
* an ordinary "not found"/"no match" outcome) so the result maps cleanly onto a
|
|
15
|
-
* tool_update event and back to the model.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import fs from 'node:fs';
|
|
19
|
-
import path from 'node:path';
|
|
20
|
-
import { randomUUID } from 'node:crypto';
|
|
21
|
-
import { runJob } from '../../../../lib/worker/run.mjs';
|
|
22
|
-
|
|
23
|
-
const IGNORED_DIRS = new Set(['node_modules', '.git', '.cx', 'dist', 'build', '.next', 'coverage', '.cache']);
|
|
24
|
-
const MAX_READ_BYTES = 256 * 1024;
|
|
25
|
-
const MAX_WALK_FILES = 20000;
|
|
26
|
-
|
|
27
|
-
function resolveInside(cwd, target, { allowOutside = false } = {}) {
|
|
28
|
-
const root = path.resolve(cwd);
|
|
29
|
-
const resolved = path.resolve(root, target || '.');
|
|
30
|
-
if (!allowOutside && resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
31
|
-
const err = new Error(`path '${target}' is outside the workspace; not permitted under the current sandbox`);
|
|
32
|
-
err.code = 'PATH_OUTSIDE_WORKSPACE';
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
return resolved;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function readFileTool({ cwd, path: target, maxBytes = MAX_READ_BYTES, allowOutside = false } = {}) {
|
|
39
|
-
const file = resolveInside(cwd, target, { allowOutside });
|
|
40
|
-
if (!fs.existsSync(file)) return { ok: false, error: `no such file: ${target}` };
|
|
41
|
-
const stat = fs.statSync(file);
|
|
42
|
-
if (stat.isDirectory()) return { ok: false, error: `${target} is a directory` };
|
|
43
|
-
const buf = fs.readFileSync(file);
|
|
44
|
-
const truncated = buf.length > maxBytes;
|
|
45
|
-
return { ok: true, path: target, bytes: stat.size, truncated, content: buf.slice(0, maxBytes).toString('utf8') };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function writeFileTool({ cwd, path: target, content = '', allowOutside = false } = {}) {
|
|
49
|
-
const file = resolveInside(cwd, target, { allowOutside });
|
|
50
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
51
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
52
|
-
return { ok: true, path: target, bytes: Buffer.byteLength(content, 'utf8') };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function editFileTool({ cwd, path: target, oldString, newString = '', replaceAll = false, allowOutside = false } = {}) {
|
|
56
|
-
const file = resolveInside(cwd, target, { allowOutside });
|
|
57
|
-
if (!fs.existsSync(file)) return { ok: false, error: `no such file: ${target}` };
|
|
58
|
-
if (typeof oldString !== 'string' || oldString.length === 0) return { ok: false, error: 'oldString is required and must be non-empty' };
|
|
59
|
-
const before = fs.readFileSync(file, 'utf8');
|
|
60
|
-
const occurrences = before.split(oldString).length - 1;
|
|
61
|
-
if (occurrences === 0) return { ok: false, error: 'oldString not found in file' };
|
|
62
|
-
if (occurrences > 1 && !replaceAll) return { ok: false, error: `oldString is not unique (${occurrences} matches); pass replaceAll or add more context` };
|
|
63
|
-
const after = replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
|
|
64
|
-
fs.writeFileSync(file, after, 'utf8');
|
|
65
|
-
return { ok: true, path: target, replacements: replaceAll ? occurrences : 1 };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Bounded depth-first walk yielding workspace-relative file paths, skipping the
|
|
69
|
-
// ignored dirs. Caps the number of files visited so a pathological tree cannot
|
|
70
|
-
// stall the loop.
|
|
71
|
-
|
|
72
|
-
function* walkFiles(root) {
|
|
73
|
-
const stack = [root];
|
|
74
|
-
let visited = 0;
|
|
75
|
-
while (stack.length) {
|
|
76
|
-
const dir = stack.pop();
|
|
77
|
-
let entries;
|
|
78
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
const full = path.join(dir, entry.name);
|
|
81
|
-
if (entry.isDirectory()) {
|
|
82
|
-
if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
83
|
-
stack.push(full);
|
|
84
|
-
} else if (entry.isFile()) {
|
|
85
|
-
if (++visited > MAX_WALK_FILES) return;
|
|
86
|
-
yield full;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Translate a simple glob ("**/*.mjs", "src/*.ts", "lib/**") into a RegExp. Only
|
|
93
|
-
// the common operators are supported (no brace expansion), which covers what the
|
|
94
|
-
// loop needs without pulling a glob dependency.
|
|
95
|
-
|
|
96
|
-
function globToRegExp(pattern) {
|
|
97
|
-
let re = '';
|
|
98
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
99
|
-
const c = pattern[i];
|
|
100
|
-
if (c === '*') {
|
|
101
|
-
if (pattern[i + 1] === '*') { re += '.*'; i++; if (pattern[i + 1] === '/') i++; }
|
|
102
|
-
else re += '[^/]*';
|
|
103
|
-
} else if (c === '?') re += '[^/]';
|
|
104
|
-
else if ('.+^${}()|[]\\'.includes(c)) re += `\\${c}`;
|
|
105
|
-
else re += c;
|
|
106
|
-
}
|
|
107
|
-
return new RegExp(`^${re}$`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function globTool({ cwd, pattern, limit = 200, allowOutside = false } = {}) {
|
|
111
|
-
const root = resolveInside(cwd, '.', { allowOutside });
|
|
112
|
-
if (!pattern) return { ok: false, error: 'pattern is required' };
|
|
113
|
-
const rx = globToRegExp(pattern);
|
|
114
|
-
const matches = [];
|
|
115
|
-
for (const file of walkFiles(root)) {
|
|
116
|
-
const rel = path.relative(root, file);
|
|
117
|
-
if (rx.test(rel)) {
|
|
118
|
-
matches.push(rel);
|
|
119
|
-
if (matches.length >= limit) break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return { ok: true, pattern, matches, truncated: matches.length >= limit };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function grepTool({ cwd, pattern, glob = null, limit = 200, caseInsensitive = false, allowOutside = false } = {}) {
|
|
126
|
-
const root = resolveInside(cwd, '.', { allowOutside });
|
|
127
|
-
if (!pattern) return { ok: false, error: 'pattern is required' };
|
|
128
|
-
let rx;
|
|
129
|
-
try { rx = new RegExp(pattern, caseInsensitive ? 'i' : ''); } catch (err) { return { ok: false, error: `invalid pattern: ${err.message}` }; }
|
|
130
|
-
const globRx = glob ? globToRegExp(glob) : null;
|
|
131
|
-
const matches = [];
|
|
132
|
-
for (const file of walkFiles(root)) {
|
|
133
|
-
const rel = path.relative(root, file);
|
|
134
|
-
if (globRx && !globRx.test(rel)) continue;
|
|
135
|
-
let text;
|
|
136
|
-
try {
|
|
137
|
-
const stat = fs.statSync(file);
|
|
138
|
-
if (stat.size > MAX_READ_BYTES) continue;
|
|
139
|
-
text = fs.readFileSync(file, 'utf8');
|
|
140
|
-
} catch { continue; }
|
|
141
|
-
if (text.includes('\u0000')) continue;
|
|
142
|
-
const lines = text.split('\n');
|
|
143
|
-
for (let i = 0; i < lines.length; i++) {
|
|
144
|
-
if (rx.test(lines[i])) {
|
|
145
|
-
matches.push({ file: rel, line: i + 1, text: lines[i].slice(0, 300) });
|
|
146
|
-
if (matches.length >= limit) return { ok: true, pattern, matches, truncated: true };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return { ok: true, pattern, matches, truncated: false };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export async function shellTool({ cwd, command, timeoutSeconds = 60, allowOutside = false } = {}) {
|
|
154
|
-
if (!command || typeof command !== 'string') return { ok: false, error: 'command is required' };
|
|
155
|
-
const root = path.resolve(cwd);
|
|
156
|
-
const result = await runJob({
|
|
157
|
-
rootDir: root,
|
|
158
|
-
job: {
|
|
159
|
-
jobId: `chat-${randomUUID()}`,
|
|
160
|
-
command,
|
|
161
|
-
timeoutSeconds,
|
|
162
|
-
workspaceRef: root,
|
|
163
|
-
allowedPaths: allowOutside ? [] : [root],
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
let stdout = '';
|
|
167
|
-
let stderr = '';
|
|
168
|
-
try { stdout = fs.readFileSync(result.stdoutPath, 'utf8'); } catch { /* no stdout */ }
|
|
169
|
-
try { stderr = fs.readFileSync(result.stderrPath, 'utf8'); } catch { /* no stderr */ }
|
|
170
|
-
return {
|
|
171
|
-
ok: result.status === 'passed',
|
|
172
|
-
status: result.status,
|
|
173
|
-
exitCode: result.exitCode,
|
|
174
|
-
durationMs: result.durationMs,
|
|
175
|
-
stdout: stdout.slice(0, MAX_READ_BYTES),
|
|
176
|
-
stderr: stderr.slice(0, MAX_READ_BYTES),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export const __test__ = { resolveInside, globToRegExp };
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* apps/chat/engine/tools/registry.mjs — builds the owned loop's tool set as Vercel
|
|
3
|
-
* AI SDK tools, gated by the permission/sandbox policy.
|
|
4
|
-
*
|
|
5
|
-
* Each tool wraps a zero-dep primitive (primitives.mjs) behind the permission gate
|
|
6
|
-
* (permission.mjs): the gate runs first, and a denial returns a structured result
|
|
7
|
-
* the model can read rather than throwing, so the loop degrades gracefully instead
|
|
8
|
-
* of crashing on a blocked action. A single `construct_tool` bridges the loop to
|
|
9
|
-
* Construct's existing MCP tool surface via dispatchToolByName (mirroring the MCP
|
|
10
|
-
* `construct_call` meta-tool), so the loop reaches knowledge search, skills, and
|
|
11
|
-
* orchestration policy without re-declaring 60+ schemas here.
|
|
12
|
-
*
|
|
13
|
-
* `ai` and `zod` are imported lazily and only here, so the rest of the engine and
|
|
14
|
-
* all of its tests stay free of the optional dependencies.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
readFileTool, writeFileTool, editFileTool, globTool, grepTool, shellTool,
|
|
19
|
-
} from './primitives.mjs';
|
|
20
|
-
import { createPermissionGate } from './permission.mjs';
|
|
21
|
-
|
|
22
|
-
function denied(reason) {
|
|
23
|
-
return { ok: false, denied: true, error: reason };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function buildAgentTools({ env = process.env, cwd = process.cwd(), handlers = {}, only = null } = {}) {
|
|
27
|
-
const { tool } = await import('ai');
|
|
28
|
-
const { z } = await import('zod');
|
|
29
|
-
|
|
30
|
-
const gate = createPermissionGate({
|
|
31
|
-
getSandbox: handlers.getSandbox || (() => null),
|
|
32
|
-
getPermissionMode: handlers.getPermissionMode || (() => 'allow_once'),
|
|
33
|
-
requestPermission: handlers.requestPermission || null,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const defs = {
|
|
37
|
-
read: tool({
|
|
38
|
-
description: 'Read a UTF-8 text file from the workspace.',
|
|
39
|
-
inputSchema: z.object({ path: z.string().describe('workspace-relative file path') }),
|
|
40
|
-
execute: async ({ path: p }) => {
|
|
41
|
-
const verdict = await gate.check('read', { path: p });
|
|
42
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
43
|
-
return readFileTool({ cwd, path: p, allowOutside: verdict.allowOutside });
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
glob: tool({
|
|
47
|
-
description: 'Find files by glob pattern (supports * ** ?).',
|
|
48
|
-
inputSchema: z.object({ pattern: z.string(), limit: z.number().int().positive().max(1000).optional() }),
|
|
49
|
-
execute: async ({ pattern, limit }) => {
|
|
50
|
-
const verdict = await gate.check('glob', { pattern });
|
|
51
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
52
|
-
return globTool({ cwd, pattern, limit, allowOutside: verdict.allowOutside });
|
|
53
|
-
},
|
|
54
|
-
}),
|
|
55
|
-
grep: tool({
|
|
56
|
-
description: 'Search file contents by regular expression, optionally filtered by a glob.',
|
|
57
|
-
inputSchema: z.object({
|
|
58
|
-
pattern: z.string(),
|
|
59
|
-
glob: z.string().optional(),
|
|
60
|
-
caseInsensitive: z.boolean().optional(),
|
|
61
|
-
limit: z.number().int().positive().max(1000).optional(),
|
|
62
|
-
}),
|
|
63
|
-
execute: async ({ pattern, glob, caseInsensitive, limit }) => {
|
|
64
|
-
const verdict = await gate.check('grep', { pattern });
|
|
65
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
66
|
-
return grepTool({ cwd, pattern, glob, caseInsensitive, limit, allowOutside: verdict.allowOutside });
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
|
-
write: tool({
|
|
70
|
-
description: 'Create or overwrite a workspace file with the given content.',
|
|
71
|
-
inputSchema: z.object({ path: z.string(), content: z.string() }),
|
|
72
|
-
execute: async ({ path: p, content }) => {
|
|
73
|
-
const verdict = await gate.check('write', { path: p });
|
|
74
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
75
|
-
return writeFileTool({ cwd, path: p, content, allowOutside: verdict.allowOutside });
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
edit: tool({
|
|
79
|
-
description: 'Replace an exact string in a workspace file. oldString must be unique unless replaceAll is set.',
|
|
80
|
-
inputSchema: z.object({
|
|
81
|
-
path: z.string(),
|
|
82
|
-
oldString: z.string(),
|
|
83
|
-
newString: z.string(),
|
|
84
|
-
replaceAll: z.boolean().optional(),
|
|
85
|
-
}),
|
|
86
|
-
execute: async ({ path: p, oldString, newString, replaceAll }) => {
|
|
87
|
-
const verdict = await gate.check('edit', { path: p });
|
|
88
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
89
|
-
return editFileTool({ cwd, path: p, oldString, newString, replaceAll, allowOutside: verdict.allowOutside });
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
shell: tool({
|
|
93
|
-
description: 'Run a bounded shell command in the workspace with a timeout.',
|
|
94
|
-
inputSchema: z.object({ command: z.string(), timeoutSeconds: z.number().int().positive().max(600).optional() }),
|
|
95
|
-
execute: async ({ command, timeoutSeconds }) => {
|
|
96
|
-
const verdict = await gate.check('shell', { command });
|
|
97
|
-
if (!verdict.allowed) return denied(verdict.reason);
|
|
98
|
-
return shellTool({ cwd, command, timeoutSeconds, allowOutside: verdict.allowOutside });
|
|
99
|
-
},
|
|
100
|
-
}),
|
|
101
|
-
construct_tool: tool({
|
|
102
|
-
description: 'Call a Construct MCP tool by name (e.g. knowledge_search, search_skills, orchestration_policy). Returns the tool result.',
|
|
103
|
-
inputSchema: z.object({ name: z.string(), args: z.record(z.string(), z.any()).optional() }),
|
|
104
|
-
execute: async ({ name, args }) => {
|
|
105
|
-
try {
|
|
106
|
-
const { dispatchToolByName } = await import('../../../../lib/mcp/server.mjs');
|
|
107
|
-
const result = await dispatchToolByName(name, args || {});
|
|
108
|
-
return { ok: true, name, result };
|
|
109
|
-
} catch (err) {
|
|
110
|
-
return { ok: false, name, error: err?.message || String(err) };
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
if (!Array.isArray(only) || only.length === 0) return defs;
|
|
117
|
-
const filtered = {};
|
|
118
|
-
for (const name of only) if (defs[name]) filtered[name] = defs[name];
|
|
119
|
-
return filtered;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const AGENT_TOOL_NAMES = ['read', 'glob', 'grep', 'write', 'edit', 'shell', 'construct_tool'];
|