@geraldmaron/construct 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -19
- package/bin/construct +311 -81
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +129 -15
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +373 -92
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +228 -7
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +28 -14
- package/personas/construct.md +2 -2
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +41 -28
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +7 -2
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +8 -8
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/status.mjs — aggregate certification posture across surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Rolls up latest run verdicts, stale-impact markers, and catalog coverage for
|
|
5
|
+
* capabilities, specialists, skills, artifact types, document categories, and demos.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { loadCapabilityLedger } from '../capability-ledger.mjs';
|
|
12
|
+
import { artifactTypes } from '../artifact-manifest.mjs';
|
|
13
|
+
import { loadCertificationStatus } from './stale-impact.mjs';
|
|
14
|
+
import { listScenarios } from './scenarios.mjs';
|
|
15
|
+
import { listCertificationRunIds, readCertificationRun } from './store.mjs';
|
|
16
|
+
import { loadCanonicalScenarios } from './canonical-scenarios.mjs';
|
|
17
|
+
import { DOCUMENT_IO_CATEGORIES, validateDocumentIoFixtures } from './document-io-fixtures.mjs';
|
|
18
|
+
import { defaultSkillInventoryPath } from './skill-inventory.mjs';
|
|
19
|
+
|
|
20
|
+
function findConstructRoot(startPath = process.cwd()) {
|
|
21
|
+
let current = path.resolve(startPath);
|
|
22
|
+
while (true) {
|
|
23
|
+
if (fs.existsSync(path.join(current, 'package.json')) && fs.existsSync(path.join(current, 'tests'))) return current;
|
|
24
|
+
const parent = path.dirname(current);
|
|
25
|
+
if (parent === current) return path.resolve(startPath);
|
|
26
|
+
current = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readRegistry(rootDir) {
|
|
31
|
+
const file = path.join(rootDir, 'specialists', 'registry.json');
|
|
32
|
+
if (!fs.existsSync(file)) return { specialists: [] };
|
|
33
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function listSpecialists(registry) {
|
|
37
|
+
return registry.specialists ?? [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeStatus({ verdict = null, stale = false, neverRun = true }) {
|
|
41
|
+
if (stale) return 'stale';
|
|
42
|
+
if (!verdict || neverRun) return 'never-run';
|
|
43
|
+
return verdict.status ?? 'never-run';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scanLatestRuns({ rootDir }) {
|
|
47
|
+
const byScenario = new Map();
|
|
48
|
+
const byCapability = new Map();
|
|
49
|
+
for (const runId of listCertificationRunIds({ rootDir })) {
|
|
50
|
+
try {
|
|
51
|
+
const { run } = readCertificationRun(runId, { rootDir });
|
|
52
|
+
const prevScenario = byScenario.get(run.scenarioId);
|
|
53
|
+
if (!prevScenario || run.createdAt > prevScenario.createdAt) {
|
|
54
|
+
byScenario.set(run.scenarioId, run);
|
|
55
|
+
}
|
|
56
|
+
const prevCap = byCapability.get(run.capabilityId);
|
|
57
|
+
if (!prevCap || run.createdAt > prevCap.createdAt) {
|
|
58
|
+
byCapability.set(run.capabilityId, run);
|
|
59
|
+
}
|
|
60
|
+
} catch { /* skip corrupt run */ }
|
|
61
|
+
}
|
|
62
|
+
return { byScenario, byCapability };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function staleCapabilitySet({ rootDir }) {
|
|
66
|
+
const { status } = loadCertificationStatus({ rootDir });
|
|
67
|
+
if (!status?.capabilities) return new Set();
|
|
68
|
+
return new Set(
|
|
69
|
+
Object.values(status.capabilities)
|
|
70
|
+
.filter((entry) => entry.status === 'stale')
|
|
71
|
+
.map((entry) => entry.capabilityId),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildCapabilityRows({ rootDir, byCapability, staleSet }) {
|
|
76
|
+
const { ledger } = loadCapabilityLedger({ rootDir });
|
|
77
|
+
return (ledger.capabilities ?? []).map((cap) => {
|
|
78
|
+
const run = byCapability.get(cap.id);
|
|
79
|
+
const stale = staleSet.has(cap.id);
|
|
80
|
+
return {
|
|
81
|
+
id: cap.id,
|
|
82
|
+
criticality: cap.criticality ?? null,
|
|
83
|
+
status: normalizeStatus({ verdict: run?.verdict, stale, neverRun: !run }),
|
|
84
|
+
lastRunAt: run?.createdAt ?? null,
|
|
85
|
+
lastScenarioId: run?.scenarioId ?? null,
|
|
86
|
+
verdict: run?.verdict ?? null,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildSpecialistRows({ rootDir, registry, byScenario }) {
|
|
92
|
+
let specialistScenario = null;
|
|
93
|
+
let run = null;
|
|
94
|
+
try {
|
|
95
|
+
const scenarios = listScenarios({ repoRoot: rootDir });
|
|
96
|
+
specialistScenario = scenarios.find((s) => s.id === 'specialist.prompt.normal') ?? null;
|
|
97
|
+
run = specialistScenario ? byScenario.get(specialistScenario.id) : null;
|
|
98
|
+
} catch { /* catalog missing in minimal fixtures */ }
|
|
99
|
+
return listSpecialists(registry).map((agent) => ({
|
|
100
|
+
id: agent.name,
|
|
101
|
+
status: normalizeStatus({ verdict: run?.verdict, stale: false, neverRun: !run }),
|
|
102
|
+
lastRunAt: run?.createdAt ?? null,
|
|
103
|
+
scenarioId: specialistScenario?.id ?? null,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadSkillInventoryFile(rootDir) {
|
|
108
|
+
const file = defaultSkillInventoryPath(rootDir);
|
|
109
|
+
if (!fs.existsSync(file)) return { skills: [] };
|
|
110
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildSkillRows({ rootDir }) {
|
|
114
|
+
const inventory = loadSkillInventoryFile(rootDir);
|
|
115
|
+
const workflowSkills = (inventory.skills ?? []).filter((s) => s.workflowSkill === true);
|
|
116
|
+
return workflowSkills.map((skill) => ({
|
|
117
|
+
id: skill.id,
|
|
118
|
+
status: 'never-run',
|
|
119
|
+
lastRunAt: null,
|
|
120
|
+
scenarioId: null,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildArtifactRows({ rootDir, byScenario }) {
|
|
125
|
+
const types = artifactTypes({ rootDir });
|
|
126
|
+
const prdRun = byScenario.get('artifact.release-gate.prd');
|
|
127
|
+
return types.map((type) => ({
|
|
128
|
+
id: type,
|
|
129
|
+
status: type === 'prd' && prdRun
|
|
130
|
+
? normalizeStatus({ verdict: prdRun.verdict, stale: false, neverRun: false })
|
|
131
|
+
: 'never-run',
|
|
132
|
+
lastRunAt: type === 'prd' ? prdRun?.createdAt ?? null : null,
|
|
133
|
+
scenarioId: type === 'prd' ? 'artifact.release-gate.prd' : null,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildDocumentCategoryRows({ rootDir }) {
|
|
138
|
+
const validation = validateDocumentIoFixtures({ rootDir });
|
|
139
|
+
return DOCUMENT_IO_CATEGORIES.map((cat) => ({
|
|
140
|
+
id: cat.id,
|
|
141
|
+
label: cat.label,
|
|
142
|
+
status: 'never-run',
|
|
143
|
+
fixturePresent: validation.errors.every((err) => !err.includes(`/${cat.id}/`)),
|
|
144
|
+
lastRunAt: null,
|
|
145
|
+
scenarioId: null,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildDemoRows({ rootDir }) {
|
|
150
|
+
try {
|
|
151
|
+
const { catalog } = loadCanonicalScenarios({ rootDir });
|
|
152
|
+
return (catalog.demos ?? []).map((demo) => ({
|
|
153
|
+
id: demo.id,
|
|
154
|
+
surface: demo.surface ?? null,
|
|
155
|
+
status: 'never-run',
|
|
156
|
+
lastRunAt: null,
|
|
157
|
+
scenarioId: null,
|
|
158
|
+
tape: demo.tape ?? demo.tapePath ?? null,
|
|
159
|
+
}));
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {object} [opts]
|
|
167
|
+
* @param {string} [opts.rootDir]
|
|
168
|
+
* @param {string} [opts.capabilityId] — filter to one capability
|
|
169
|
+
*/
|
|
170
|
+
export function buildCertificationStatus({ rootDir = process.cwd(), capabilityId = null } = {}) {
|
|
171
|
+
const root = findConstructRoot(rootDir);
|
|
172
|
+
const registry = readRegistry(root);
|
|
173
|
+
const { byScenario, byCapability } = scanLatestRuns({ rootDir: root });
|
|
174
|
+
const staleSet = staleCapabilitySet({ rootDir: root });
|
|
175
|
+
|
|
176
|
+
const report = {
|
|
177
|
+
generatedAt: new Date().toISOString(),
|
|
178
|
+
capabilities: buildCapabilityRows({ rootDir: root, byCapability, staleSet }),
|
|
179
|
+
specialists: buildSpecialistRows({ rootDir: root, registry, byScenario }),
|
|
180
|
+
skills: buildSkillRows({ rootDir: root }),
|
|
181
|
+
artifactTypes: buildArtifactRows({ rootDir: root, byScenario }),
|
|
182
|
+
documentCategories: buildDocumentCategoryRows({ rootDir: root }),
|
|
183
|
+
demos: buildDemoRows({ rootDir: root }),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (capabilityId) {
|
|
187
|
+
const cap = report.capabilities.find((c) => c.id === capabilityId);
|
|
188
|
+
const relatedScenarios = listScenarios({ repoRoot: root })
|
|
189
|
+
.filter((s) => s.capabilityId === capabilityId)
|
|
190
|
+
.map((s) => ({
|
|
191
|
+
id: s.id,
|
|
192
|
+
status: normalizeStatus({
|
|
193
|
+
verdict: byScenario.get(s.id)?.verdict,
|
|
194
|
+
stale: staleSet.has(capabilityId),
|
|
195
|
+
neverRun: !byScenario.get(s.id),
|
|
196
|
+
}),
|
|
197
|
+
lastRunAt: byScenario.get(s.id)?.createdAt ?? null,
|
|
198
|
+
}));
|
|
199
|
+
return {
|
|
200
|
+
generatedAt: report.generatedAt,
|
|
201
|
+
capability: cap ?? { id: capabilityId, status: 'never-run' },
|
|
202
|
+
scenarios: relatedScenarios,
|
|
203
|
+
stale: staleSet.has(capabilityId),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return report;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function statusLabel(status) {
|
|
211
|
+
if (status === 'stale') return 'STALE';
|
|
212
|
+
if (status === 'never-run') return 'never-run';
|
|
213
|
+
if (status === 'pass') return 'pass';
|
|
214
|
+
if (status === 'fail') return 'FAIL';
|
|
215
|
+
if (status === 'inconclusive') return 'inconclusive';
|
|
216
|
+
return String(status);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printSection(title, rows, { idKey = 'id' } = {}) {
|
|
220
|
+
process.stdout.write(`\n${title}\n`);
|
|
221
|
+
if (!rows.length) {
|
|
222
|
+
process.stdout.write(' (none)\n');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
for (const row of rows) {
|
|
226
|
+
const label = statusLabel(row.status);
|
|
227
|
+
const staleMark = row.status === 'stale' ? ' ⚠' : '';
|
|
228
|
+
const neverMark = row.status === 'never-run' ? ' ·' : '';
|
|
229
|
+
const when = row.lastRunAt ? ` @ ${row.lastRunAt}` : '';
|
|
230
|
+
process.stdout.write(` ${String(row[idKey]).padEnd(40)} ${label}${staleMark}${neverMark}${when}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatCertificationStatus(report, { capabilityId = null } = {}) {
|
|
235
|
+
if (capabilityId) {
|
|
236
|
+
const cap = report.capability;
|
|
237
|
+
process.stdout.write(`Certification status · capability: ${capabilityId}\n`);
|
|
238
|
+
process.stdout.write(` status: ${statusLabel(cap?.status ?? 'never-run')}\n`);
|
|
239
|
+
if (report.stale) process.stdout.write(' stale: yes (ledger changePaths touched)\n');
|
|
240
|
+
printSection('Scenarios', report.scenarios ?? [], { idKey: 'id' });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
process.stdout.write(`Certification status · ${report.generatedAt}\n`);
|
|
245
|
+
process.stdout.write('Legend: STALE = evidence outdated · never-run = no recorded run\n');
|
|
246
|
+
printSection('Capabilities', report.capabilities);
|
|
247
|
+
printSection('Specialists', report.specialists);
|
|
248
|
+
printSection('Workflow skills', report.skills);
|
|
249
|
+
printSection('Artifact types', report.artifactTypes);
|
|
250
|
+
printSection('Document categories', report.documentCategories);
|
|
251
|
+
printSection('Demo surfaces', report.demos);
|
|
252
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/certification/store.mjs — durable certification run artifacts under .cx/certification/.
|
|
3
|
+
*
|
|
4
|
+
* Each run is stored as .cx/certification/runs/<run-id>/run.json plus optional
|
|
5
|
+
* redacted output siblings referenced by relative paths in the run record.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { assertCertificationRun, validateCertificationRun } from './run.mjs';
|
|
11
|
+
|
|
12
|
+
function findProjectRoot(startPath = process.cwd()) {
|
|
13
|
+
let current = path.resolve(startPath);
|
|
14
|
+
while (true) {
|
|
15
|
+
if (fs.existsSync(path.join(current, 'package.json'))) return current;
|
|
16
|
+
const parent = path.dirname(current);
|
|
17
|
+
if (parent === current) return path.resolve(startPath);
|
|
18
|
+
current = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function certificationRoot(rootDir = process.cwd()) {
|
|
23
|
+
return path.join(findProjectRoot(rootDir), '.cx', 'certification');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function certificationRunsRoot(rootDir = process.cwd()) {
|
|
27
|
+
return path.join(certificationRoot(rootDir), 'runs');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function certificationRunDir(runId, rootDir = process.cwd()) {
|
|
31
|
+
return path.join(certificationRunsRoot(rootDir), runId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function certificationRunPath(runId, rootDir = process.cwd()) {
|
|
35
|
+
return path.join(certificationRunDir(runId, rootDir), 'run.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function writeCertificationRun(run, { rootDir = process.cwd(), outputs = {} } = {}) {
|
|
39
|
+
const validation = validateCertificationRun(run);
|
|
40
|
+
if (!validation.valid) throw new Error(validation.errors.join('; '));
|
|
41
|
+
|
|
42
|
+
const dir = certificationRunDir(run.id, rootDir);
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
const artifacts = { ...(run.artifacts ?? {}) };
|
|
46
|
+
if (outputs.markdown != null) {
|
|
47
|
+
const rel = 'output.md';
|
|
48
|
+
fs.writeFileSync(path.join(dir, rel), String(outputs.markdown));
|
|
49
|
+
artifacts.outputMarkdown = rel;
|
|
50
|
+
}
|
|
51
|
+
if (outputs.json != null) {
|
|
52
|
+
const rel = 'output.json';
|
|
53
|
+
fs.writeFileSync(path.join(dir, rel), `${JSON.stringify(outputs.json, null, 2)}\n`);
|
|
54
|
+
artifacts.outputJson = rel;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const record = assertCertificationRun({ ...run, artifacts: Object.keys(artifacts).length ? artifacts : run.artifacts ?? null });
|
|
58
|
+
fs.writeFileSync(certificationRunPath(run.id, rootDir), `${JSON.stringify(record, null, 2)}\n`);
|
|
59
|
+
return { dir, path: certificationRunPath(run.id, rootDir), run: record };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function readCertificationRun(runId, { rootDir = process.cwd() } = {}) {
|
|
63
|
+
const filePath = certificationRunPath(runId, rootDir);
|
|
64
|
+
const run = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
65
|
+
const validation = validateCertificationRun(run);
|
|
66
|
+
if (!validation.valid) throw new Error(validation.errors.join('; '));
|
|
67
|
+
return { filePath, run };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function listCertificationRunIds({ rootDir = process.cwd() } = {}) {
|
|
71
|
+
const runsRoot = certificationRunsRoot(rootDir);
|
|
72
|
+
if (!fs.existsSync(runsRoot)) return [];
|
|
73
|
+
return fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory())
|
|
75
|
+
.map((entry) => entry.name)
|
|
76
|
+
.sort();
|
|
77
|
+
}
|
package/lib/chat/cli.mjs
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/cli.mjs — `construct chat` command handler (zero-dep launcher).
|
|
3
|
+
*
|
|
4
|
+
* ADR-0041: Construct owns the loop. This launcher stays in the zero-dep core and
|
|
5
|
+
* only orchestrates — it loads project settings, resolves the model, builds the
|
|
6
|
+
* owned-loop driver (apps/chat/engine, which lazy-imports the optional Vercel AI
|
|
7
|
+
* SDK and provider packages), persists the normalized event timeline under
|
|
8
|
+
* .cx/chat-sessions/, and then chooses a surface.
|
|
9
|
+
*
|
|
10
|
+
* Surface selection: desktop window (Tauri or Chromium app mode) is the default on
|
|
11
|
+
* machines with a display; `--web` opens a browser tab; `--plain`, `--accessible`,
|
|
12
|
+
* NO_COLOR, TERM=dumb, and non-TTY streams route to the linear renderer.
|
|
13
|
+
* `--window` is an alias for the desktop path. `--no-window` forces a browser tab.
|
|
14
|
+
*
|
|
15
|
+
* `--list` reports the model catalog and which providers are configured; `--model`
|
|
16
|
+
* pins the model for this launch; `--resume` restores the latest (or given) session
|
|
17
|
+
* transcript from `.cx/chat-sessions/`; the `--no-*` flags override saved transparency
|
|
18
|
+
* layers.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { resolveColors } from '../term-format.mjs';
|
|
24
|
+
import { resolveLayers } from './transparency.mjs';
|
|
25
|
+
import { runChatLoop } from './tui/render.mjs';
|
|
26
|
+
import { createSessionUsage } from './tui/usage.mjs';
|
|
27
|
+
import { loadChatConfig } from './config.mjs';
|
|
28
|
+
import { loadConstructEnv } from '../env-config.mjs';
|
|
29
|
+
import { createCommands } from './commands.mjs';
|
|
30
|
+
import { resolveResumePath, restoreFromSession } from './session-restore.mjs';
|
|
31
|
+
import { buildSystemPrompt } from './system-prompt.mjs';
|
|
32
|
+
import { resolveExecutionCapabilityProfile } from '../models/execution-capability-profile.mjs';
|
|
33
|
+
import { compileExecutionPolicy } from '../models/execution-policy.mjs';
|
|
34
|
+
import { createChatPersister } from './session-persist.mjs';
|
|
35
|
+
import { hasGuiDisplay } from './desktop-binary.mjs';
|
|
36
|
+
import { createOwnedLoopDriver } from '../../apps/chat/engine/loop-driver.mjs';
|
|
37
|
+
import { listChatModels, resolveChatModelSelectionAsync, resolveSessionModel } from '../../apps/chat/engine/models.mjs';
|
|
38
|
+
import { resolveDemoGuideForChat } from '../demo-surface.mjs';
|
|
39
|
+
|
|
40
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
42
|
+
|
|
43
|
+
function parseFlags(args) {
|
|
44
|
+
const flags = {
|
|
45
|
+
plain: false,
|
|
46
|
+
accessible: false,
|
|
47
|
+
web: false,
|
|
48
|
+
window: false,
|
|
49
|
+
noWindow: false,
|
|
50
|
+
list: false,
|
|
51
|
+
quiet: false,
|
|
52
|
+
free: false,
|
|
53
|
+
model: null,
|
|
54
|
+
resume: null,
|
|
55
|
+
ascii: null,
|
|
56
|
+
demo: null,
|
|
57
|
+
};
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
if (arg === '--list') flags.list = true;
|
|
61
|
+
else if (arg === '--plain') flags.plain = true;
|
|
62
|
+
else if (arg === '--accessible') flags.accessible = true;
|
|
63
|
+
else if (arg === '--web') flags.web = true;
|
|
64
|
+
else if (arg === '--window') flags.window = true;
|
|
65
|
+
else if (arg === '--no-window') flags.noWindow = true;
|
|
66
|
+
else if (arg === '--quiet') flags.quiet = true;
|
|
67
|
+
else if (arg === '--free') flags.free = true;
|
|
68
|
+
else if (arg === '--ascii') flags.ascii = true;
|
|
69
|
+
else if (arg === '--resume') flags.resume = true;
|
|
70
|
+
else if (arg.startsWith('--resume=')) flags.resume = arg.slice('--resume='.length);
|
|
71
|
+
else if (arg === '--no-thinking') flags.thinking = false;
|
|
72
|
+
else if (arg === '--no-path') flags.path = false;
|
|
73
|
+
else if (arg === '--no-specialists') flags.specialists = false;
|
|
74
|
+
else if (arg === '--no-tools') flags.tools = false;
|
|
75
|
+
else if (arg === '--no-observability') flags.observability = false;
|
|
76
|
+
else if (arg === '--model') flags.model = args[++i] || null;
|
|
77
|
+
else if (arg.startsWith('--model=')) flags.model = arg.slice('--model='.length);
|
|
78
|
+
else if (arg === '--demo') flags.demo = args[++i] || null;
|
|
79
|
+
else if (arg.startsWith('--demo=')) flags.demo = arg.slice('--demo='.length);
|
|
80
|
+
}
|
|
81
|
+
return flags;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printModels(output, env) {
|
|
85
|
+
const colors = resolveColors({ stream: output, env });
|
|
86
|
+
const models = listChatModels({ env });
|
|
87
|
+
const configured = models.filter((m) => m.configured);
|
|
88
|
+
output.write(`${colors.bold}Owned loop \u2014 model catalog${colors.reset}\n`);
|
|
89
|
+
if (!configured.length) {
|
|
90
|
+
output.write(`${colors.dim}No provider configured. Set a key (a value or an op:// reference) in ~/.construct/config.env, run \`construct creds login copilot\` for GitHub Copilot, or start Ollama locally.${colors.reset}\n`);
|
|
91
|
+
}
|
|
92
|
+
for (const m of models) {
|
|
93
|
+
const mark = m.configured ? `${colors.green}\u2713${colors.reset}` : `${colors.dim}\u00b7${colors.reset}`;
|
|
94
|
+
output.write(` ${mark} ${m.id}${m.local ? `${colors.dim} (local)${colors.reset}` : ''}\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createPersister({ cwd, sessionId, resumePath = null }) {
|
|
99
|
+
return createChatPersister({ cwd, sessionId, resumePath, host: 'construct' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wantsLinearSurface({ flags, env, output, input }) {
|
|
103
|
+
if (flags.plain || flags.accessible) return true;
|
|
104
|
+
if (env.NO_COLOR || env.TERM === 'dumb') return true;
|
|
105
|
+
if (env.CX_CHAT_FORCE_LINEAR === '1') return true;
|
|
106
|
+
if (!output.isTTY || !input.isTTY) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveChatSurface({ flags, env, output, input }) {
|
|
111
|
+
if (wantsLinearSurface({ flags, env, output, input })) return 'linear';
|
|
112
|
+
if (flags.web || flags.noWindow) return 'web';
|
|
113
|
+
if (flags.window || env.CONSTRUCT_CHAT_WINDOW === '1' || hasGuiDisplay(env)) return 'desktop';
|
|
114
|
+
return 'headless';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Desktop means the native construct-chat window: an existing binary, or Tauri
|
|
118
|
+
// source that runDesktopChat builds on demand. A browser does not qualify;
|
|
119
|
+
// `construct chat --web` is the explicit browser path.
|
|
120
|
+
|
|
121
|
+
async function canLaunchDesktop(env, cwd = process.cwd()) {
|
|
122
|
+
const { hasGuiDisplay, resolveDesktopBinary } = await import('./desktop-binary.mjs');
|
|
123
|
+
if (!hasGuiDisplay(env)) return false;
|
|
124
|
+
if (resolveDesktopBinary()) return true;
|
|
125
|
+
const { hasDesktopSource } = await import('./desktop-build.mjs');
|
|
126
|
+
return hasDesktopSource(cwd);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveAsciiMode({ flags, env, config }) {
|
|
130
|
+
if (flags.ascii === true) return true;
|
|
131
|
+
if (env.CX_CHAT_ASCII === '1') return true;
|
|
132
|
+
if (config.ui?.ascii) return true;
|
|
133
|
+
if (process.platform === 'win32' && !env.WT_SESSION && !env.WT_PROFILE_ID && env.TERM !== 'xterm-256color') {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createPermissionHandler({ session, bridge }) {
|
|
140
|
+
return async (req) => {
|
|
141
|
+
const mode = session.permissionMode || 'allow_once';
|
|
142
|
+
if (mode === 'reject') return 'reject';
|
|
143
|
+
if (mode === 'allow_always') return 'allow_always';
|
|
144
|
+
if (mode === 'allow_once') return 'allow';
|
|
145
|
+
if (mode === 'ask' && bridge?.prompt) return bridge.prompt(req);
|
|
146
|
+
return 'allow';
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runChat(args = [], { env = process.env, cwd = process.cwd(), input = process.stdin, output = process.stdout, errorOutput = process.stderr } = {}) {
|
|
151
|
+
const flags = parseFlags(args);
|
|
152
|
+
if (!flags.demo && env.CONSTRUCT_DEMO) flags.demo = env.CONSTRUCT_DEMO;
|
|
153
|
+
|
|
154
|
+
const effectiveEnv = loadConstructEnv({ rootDir: cwd, env: flags.plain ? { ...env, NO_COLOR: '1' } : env, warn: false });
|
|
155
|
+
|
|
156
|
+
if (flags.list) { printModels(output, effectiveEnv); return 0; }
|
|
157
|
+
|
|
158
|
+
const { config } = loadChatConfig({ cwd });
|
|
159
|
+
|
|
160
|
+
let modelMode = config.modelMode || 'pinned';
|
|
161
|
+
if (flags.free) modelMode = 'free-router';
|
|
162
|
+
|
|
163
|
+
const savedModel = flags.model || (modelMode === 'pinned' ? config.model : null) || null;
|
|
164
|
+
|
|
165
|
+
let resolvedModel = savedModel;
|
|
166
|
+
if (modelMode === 'free-router') {
|
|
167
|
+
const sessionStub = { modelMode: 'free-router', failedModels: new Set() };
|
|
168
|
+
resolvedModel = await resolveSessionModel(sessionStub, { env: effectiveEnv, tier: 'standard' });
|
|
169
|
+
if (!resolvedModel && !flags.list) {
|
|
170
|
+
errorOutput.write('OpenRouter free router needs OPENROUTER_API_KEY in ~/.construct/config.env, or no free models are available.\n');
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Saved layers seed the run; --no-* overrides win for this launch.
|
|
176
|
+
const layers = resolveLayers({ flags, env: effectiveEnv });
|
|
177
|
+
for (const key of Object.keys(config.layers)) {
|
|
178
|
+
if (flags[key] === undefined && config.layers[key] === false) layers[key] = false;
|
|
179
|
+
}
|
|
180
|
+
if (config.thinking === false && flags.thinking === undefined) layers.thinking = false;
|
|
181
|
+
|
|
182
|
+
const resumePath = resolveResumePath({ cwd, resume: flags.resume });
|
|
183
|
+
const restored = resumePath ? restoreFromSession(resumePath) : null;
|
|
184
|
+
|
|
185
|
+
const demoPack = flags.demo
|
|
186
|
+
? resolveDemoGuideForChat(flags.demo, { cwd, repoRoot: REPO_ROOT })
|
|
187
|
+
: null;
|
|
188
|
+
|
|
189
|
+
const modelResolution = modelMode === 'free-router' && resolvedModel
|
|
190
|
+
? { id: resolvedModel, source: 'free-router', notice: null, rejected: [] }
|
|
191
|
+
: await resolveChatModelSelectionAsync({
|
|
192
|
+
env: effectiveEnv,
|
|
193
|
+
requested: savedModel,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const session = {
|
|
197
|
+
model: modelResolution.id || resolvedModel,
|
|
198
|
+
savedModel: modelMode === 'pinned' ? (savedModel || modelResolution.id) : null,
|
|
199
|
+
modelMode,
|
|
200
|
+
modelNotice: modelResolution.notice || null,
|
|
201
|
+
failedModels: new Set(),
|
|
202
|
+
layers,
|
|
203
|
+
thinking: layers.thinking,
|
|
204
|
+
permissionMode: config.permissionMode || 'allow_once',
|
|
205
|
+
sandbox: config.sandbox || 'workspace-write',
|
|
206
|
+
ui: {
|
|
207
|
+
ascii: resolveAsciiMode({ flags, env: effectiveEnv, config }),
|
|
208
|
+
inspector: config.ui?.inspector || 'auto',
|
|
209
|
+
theme: config.ui?.theme || 'auto',
|
|
210
|
+
},
|
|
211
|
+
usage: restored?.usage || createSessionUsage(),
|
|
212
|
+
demoGuide: demoPack?.guide || null,
|
|
213
|
+
demoTitle: demoPack?.script?.title || null,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
let surface = resolveChatSurface({ flags, env: effectiveEnv, output, input });
|
|
217
|
+
|
|
218
|
+
if (surface === 'headless') {
|
|
219
|
+
errorOutput.write('No graphical display available. Use `construct chat --plain` for terminal mode.\n');
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (surface === 'desktop') {
|
|
224
|
+
if (!(await canLaunchDesktop(effectiveEnv, cwd))) {
|
|
225
|
+
errorOutput.write('No graphical display available. Use `construct chat --plain` for terminal mode, or `construct chat --web` for a browser tab.\n');
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const { runDesktopChat } = await import('./desktop-launcher.mjs');
|
|
230
|
+
return runDesktopChat({ cwd, env: effectiveEnv, output, errorOutput });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
errorOutput.write(`Desktop chat unavailable: ${err.message}\n`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (surface === 'web') {
|
|
238
|
+
try {
|
|
239
|
+
const { runWebChat } = await import('./web-launcher.mjs');
|
|
240
|
+
return runWebChat({ cwd, env: effectiveEnv, output, errorOutput });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
errorOutput.write(`Web chat unavailable: ${err.message}\n`);
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const permissionBridge = { prompt: null };
|
|
248
|
+
const handlers = {
|
|
249
|
+
getSandbox: () => session.sandbox,
|
|
250
|
+
getPermissionMode: () => session.permissionMode,
|
|
251
|
+
requestPermission: createPermissionHandler({ session, bridge: permissionBridge }),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const createAgent = async (opts) => {
|
|
255
|
+
const { createAiSdkAgent } = await import('../../apps/chat/engine/ai-sdk-agent.mjs');
|
|
256
|
+
return createAiSdkAgent(opts);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const driver = createOwnedLoopDriver({
|
|
260
|
+
env: effectiveEnv,
|
|
261
|
+
cwd,
|
|
262
|
+
model: session.model,
|
|
263
|
+
handlers,
|
|
264
|
+
systemPrompt: (() => {
|
|
265
|
+
const base = buildSystemPrompt({ capabilityTier: compileExecutionPolicy({ profile: resolveExecutionCapabilityProfile({ model: session.model }) }).prompt.systemPromptTier });
|
|
266
|
+
return demoPack?.systemOverlay ? `${base}\n\nDemo script:\n${demoPack.systemOverlay}` : base;
|
|
267
|
+
})(),
|
|
268
|
+
createAgent,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
let started;
|
|
272
|
+
try {
|
|
273
|
+
started = await driver.start();
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const hint = /Cannot find package|ERR_MODULE_NOT_FOUND|find module/i.test(err.message)
|
|
276
|
+
? '\nThe owned loop needs its optional dependencies. Install them with:\n npm install ai @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/openai-compatible zod'
|
|
277
|
+
: '';
|
|
278
|
+
errorOutput.write(`Failed to start the owned loop: ${err.message}${hint}\n`);
|
|
279
|
+
try { driver.stop?.(); } catch { /* nothing started */ }
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const notices = [];
|
|
284
|
+
if (modelResolution.notice) notices.push(modelResolution.notice);
|
|
285
|
+
if (demoPack?.welcome) notices.push(demoPack.welcome);
|
|
286
|
+
if (!session.model) {
|
|
287
|
+
notices.push('no model resolved \u2014 set a provider key or run `construct creds login copilot`, then use /model');
|
|
288
|
+
}
|
|
289
|
+
if (flags.resume && !restored?.sessionFile) {
|
|
290
|
+
notices.push('no prior session found to resume \u2014 starting fresh');
|
|
291
|
+
} else if (restored?.sessionFile && (restored.turnBlocks?.length || restored.transcript.length)) {
|
|
292
|
+
const count = restored.turnBlocks?.length || restored.transcript.length;
|
|
293
|
+
notices.push(`resumed ${count} turn(s) from ${path.basename(restored.sessionFile)}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const persist = createPersister({ cwd, sessionId: started.sessionId, resumePath: restored?.sessionFile || null });
|
|
297
|
+
const commands = createCommands({
|
|
298
|
+
driver,
|
|
299
|
+
host: 'construct',
|
|
300
|
+
hostId: 'construct',
|
|
301
|
+
version: null,
|
|
302
|
+
cwd,
|
|
303
|
+
env: effectiveEnv,
|
|
304
|
+
turnBlocksRef: () => [],
|
|
305
|
+
demoGuide: demoPack?.guide || null,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
await runChatLoop({
|
|
310
|
+
driver,
|
|
311
|
+
host: 'construct',
|
|
312
|
+
version: null,
|
|
313
|
+
layers,
|
|
314
|
+
input,
|
|
315
|
+
env: effectiveEnv,
|
|
316
|
+
output,
|
|
317
|
+
persist,
|
|
318
|
+
commands,
|
|
319
|
+
session,
|
|
320
|
+
notices,
|
|
321
|
+
permissionBridge,
|
|
322
|
+
initialTranscript: restored?.transcript || [],
|
|
323
|
+
initialTurnBlocks: restored?.turnBlocks || [],
|
|
324
|
+
cwd,
|
|
325
|
+
flags,
|
|
326
|
+
});
|
|
327
|
+
} finally {
|
|
328
|
+
try { driver.stop?.(); } catch { /* already stopped */ }
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { resolveChatSurface, wantsLinearSurface };
|