@geraldmaron/construct 1.1.1 → 1.2.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 +30 -19
- package/bin/construct +487 -87
- 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 +258 -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/artifact-workflow.mjs +252 -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 +90 -19
- 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 +246 -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 +146 -15
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +86 -6
- package/lib/config/source-targets.mjs +311 -0
- 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/source-checkout.mjs +16 -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 +384 -38
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +4 -3
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +4 -3
- package/lib/embedded-contract/index.mjs +11 -0
- 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/export-branding.mjs +32 -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 +18 -16
- 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 +50 -119
- 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/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- 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 +103 -10
- 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/embedded-contract.mjs +14 -0
- 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 +254 -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 +259 -9
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- 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 +489 -0
- package/specialists/artifact-manifest.schema.json +83 -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-reference.docx +0 -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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/openrouter-fallback.mjs — OpenRouter failure parsing and turn-time fallback.
|
|
3
|
+
*
|
|
4
|
+
* When a free slug returns 404/429 at call time, walks the live poll for the next
|
|
5
|
+
* candidate. When an Ollama tag is pinned but not pulled, falls through to the
|
|
6
|
+
* next configured provider for the same turn (mirroring session launch resolution).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { classifyProviderFailure, resolveValidatedChatModel } from '../model-router.mjs';
|
|
10
|
+
|
|
11
|
+
const MAX_FALLBACK_ATTEMPTS = 3;
|
|
12
|
+
|
|
13
|
+
export function parseOpenRouterError(error) {
|
|
14
|
+
const text = typeof error === 'string' ? error : String(error?.message || error || '');
|
|
15
|
+
let raw = text;
|
|
16
|
+
try {
|
|
17
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
18
|
+
if (jsonMatch) {
|
|
19
|
+
const body = JSON.parse(jsonMatch[0]);
|
|
20
|
+
raw = body?.error?.metadata?.raw || body?.error?.message || text;
|
|
21
|
+
}
|
|
22
|
+
} catch { /* not JSON */ }
|
|
23
|
+
|
|
24
|
+
let summary = raw;
|
|
25
|
+
const ollamaMissing = raw.match(/model ['"]([^'"]+)['"] not found/i);
|
|
26
|
+
if (ollamaMissing) {
|
|
27
|
+
const native = ollamaMissing[1];
|
|
28
|
+
summary = `Ollama model '${native}' is not installed locally. Pull it with: ollama pull ${native} (or: construct ollama pull ${native})`;
|
|
29
|
+
} else if (/rate[- ]?limit|429|temporarily rate-limited/i.test(raw)) {
|
|
30
|
+
summary = raw.replace(/^Provider returned error\.?\s*/i, '').trim() || 'rate-limited upstream';
|
|
31
|
+
} else if (/unavailable for free|paid version is available/i.test(raw)) {
|
|
32
|
+
summary = 'free tier retired — use a different free model or paid slug';
|
|
33
|
+
} else if (/Failed after \d+ attempts/i.test(text)) {
|
|
34
|
+
summary = raw || 'provider error after retries';
|
|
35
|
+
} else if (/OLLAMA_MODEL_NOT_PULLED|not installed locally/i.test(raw)) {
|
|
36
|
+
summary = raw;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { raw, summary, text };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ensureFailedModels(session) {
|
|
43
|
+
if (!session.failedModels) session.failedModels = new Set();
|
|
44
|
+
return session.failedModels;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function recordFailedModel(session, modelId) {
|
|
48
|
+
if (!modelId) return;
|
|
49
|
+
ensureFailedModels(session).add(modelId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getExcludeList(session) {
|
|
53
|
+
return [...ensureFailedModels(session)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function shouldAttemptFreeFallback(session, modelId) {
|
|
57
|
+
if (session?.modelMode === 'free-router') return true;
|
|
58
|
+
return typeof modelId === 'string' && /:free$/i.test(modelId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isOllamaNotPulledError(error) {
|
|
62
|
+
const text = typeof error === 'string' ? error : String(error?.message || error || '');
|
|
63
|
+
return error?.code === 'OLLAMA_MODEL_NOT_PULLED' || /not installed locally/i.test(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function handleOllamaNotPulledFailure({ session, error, env = process.env, currentModel }) {
|
|
67
|
+
if (!isOllamaNotPulledError(error)) return null;
|
|
68
|
+
if (typeof currentModel !== 'string' || !currentModel.startsWith('ollama/')) return null;
|
|
69
|
+
|
|
70
|
+
recordFailedModel(session, currentModel);
|
|
71
|
+
const excludeFamilies = typeof currentModel === 'string' && currentModel.startsWith('ollama/')
|
|
72
|
+
? ['ollama']
|
|
73
|
+
: [];
|
|
74
|
+
const next = resolveValidatedChatModel({ env, requested: null, excludeFamilies });
|
|
75
|
+
if (!next?.id || next.id === currentModel) return null;
|
|
76
|
+
|
|
77
|
+
const parsed = parseOpenRouterError(error);
|
|
78
|
+
return {
|
|
79
|
+
modelId: next.id,
|
|
80
|
+
notice: `${parsed.summary} Switched to ${next.id} for this turn.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleModelFailure(opts) {
|
|
85
|
+
const openRouter = await handleOpenRouterFailure(opts);
|
|
86
|
+
if (openRouter) return openRouter;
|
|
87
|
+
return handleOllamaNotPulledFailure(opts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleOpenRouterFailure({ session, error, env = process.env, currentModel }) {
|
|
91
|
+
const parsed = parseOpenRouterError(error);
|
|
92
|
+
const classified = classifyProviderFailure({ error: { message: parsed.text || parsed.raw } });
|
|
93
|
+
if (!classified?.retryable) return null;
|
|
94
|
+
if (!shouldAttemptFreeFallback(session, currentModel)) return null;
|
|
95
|
+
|
|
96
|
+
recordFailedModel(session, currentModel);
|
|
97
|
+
const exclude = getExcludeList(session);
|
|
98
|
+
const { resolveFreeOpenRouterModel } = await import('../../apps/chat/engine/models.mjs');
|
|
99
|
+
const next = await resolveFreeOpenRouterModel({ env, tier: 'standard', exclude });
|
|
100
|
+
if (!next || next === currentModel) return null;
|
|
101
|
+
|
|
102
|
+
const short = (id) => id.replace(/^openrouter\//, '');
|
|
103
|
+
return {
|
|
104
|
+
modelId: next,
|
|
105
|
+
notice: `${short(currentModel)} failed (${parsed.summary}). Switched to ${short(next)}. Retry or /model to pick manually.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runTurnWithFallback({
|
|
110
|
+
driver,
|
|
111
|
+
text,
|
|
112
|
+
session,
|
|
113
|
+
layers,
|
|
114
|
+
env,
|
|
115
|
+
promptOptions = {},
|
|
116
|
+
runTurnInto,
|
|
117
|
+
onUpdate = () => {},
|
|
118
|
+
}) {
|
|
119
|
+
let model = session.model;
|
|
120
|
+
let lastState = null;
|
|
121
|
+
let lastNotice = null;
|
|
122
|
+
|
|
123
|
+
for (let attempt = 0; attempt < MAX_FALLBACK_ATTEMPTS; attempt++) {
|
|
124
|
+
lastState = await runTurnInto(
|
|
125
|
+
driver,
|
|
126
|
+
text,
|
|
127
|
+
{ ...promptOptions, model, turnOverlay: promptOptions.turnOverlay },
|
|
128
|
+
{ session, layers, onUpdate },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!lastState.error) {
|
|
132
|
+
return { state: lastState, model, notice: lastNotice };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fallback = await handleModelFailure({
|
|
136
|
+
session,
|
|
137
|
+
error: lastState.error,
|
|
138
|
+
env,
|
|
139
|
+
currentModel: model,
|
|
140
|
+
});
|
|
141
|
+
if (!fallback) break;
|
|
142
|
+
|
|
143
|
+
model = fallback.modelId;
|
|
144
|
+
session.model = model;
|
|
145
|
+
lastNotice = fallback.notice;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { state: lastState, model, notice: lastNotice };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { MAX_FALLBACK_ATTEMPTS };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/permission-prompt.mjs — interactive permission decisions for ask mode.
|
|
3
|
+
*
|
|
4
|
+
* Maps user keystrokes and readline answers to the host-agnostic decision
|
|
5
|
+
* vocabulary (allow | allow_always | reject) shared by the permission gate
|
|
6
|
+
* (apps/chat/engine/tools/permission.mjs). Both the Ink overlay and the linear
|
|
7
|
+
* readline prompt use the same parser so behaviour stays identical.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function formatPermissionQuestion({ tool = 'tool', input = null } = {}) {
|
|
11
|
+
const detail = input && typeof input === 'object'
|
|
12
|
+
? Object.keys(input).slice(0, 3).join(', ')
|
|
13
|
+
: '';
|
|
14
|
+
const suffix = detail ? ` (${detail})` : '';
|
|
15
|
+
return `Allow "${tool}"${suffix}? [y] once [a] always [n] reject`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parsePermissionDecision(raw) {
|
|
19
|
+
const s = String(raw ?? '').trim().toLowerCase();
|
|
20
|
+
if (['a', 'always', 'allow_always', 'all'].includes(s)) return 'allow_always';
|
|
21
|
+
if (['n', 'no', 'reject', 'r', 'deny', '0'].includes(s)) return 'reject';
|
|
22
|
+
if (['y', 'yes', 'allow', 'once', '1', ''].includes(s)) return 'allow';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parsePermissionKey(char) {
|
|
27
|
+
if (!char) return null;
|
|
28
|
+
const c = char.toLowerCase();
|
|
29
|
+
if (c === 'a') return 'allow_always';
|
|
30
|
+
if (c === 'n' || c === 'r') return 'reject';
|
|
31
|
+
if (c === 'y') return 'allow';
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/picker-catalog.mjs — predefined list-picker item sets for construct chat.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LAYER_KEYS, PERMISSION_MODES, SANDBOX_LEVELS, INSPECTOR_MODES } from './config.mjs';
|
|
6
|
+
|
|
7
|
+
export const PERMISSION_PICKER_ITEMS = Object.freeze([
|
|
8
|
+
{ id: 'allow', label: 'Allow once', detail: 'y' },
|
|
9
|
+
{ id: 'allow_always', label: 'Allow always', detail: 'a' },
|
|
10
|
+
{ id: 'reject', label: 'Reject', detail: 'n' },
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const BOOL_PICKER_ITEMS = Object.freeze([
|
|
14
|
+
{ id: 'on', label: 'on', detail: 'true' },
|
|
15
|
+
{ id: 'off', label: 'off', detail: 'false' },
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function settingKeyPickerItems() {
|
|
19
|
+
return [
|
|
20
|
+
{ id: 'thinking', label: 'thinking', tag: 'bool' },
|
|
21
|
+
...LAYER_KEYS.map((k) => ({ id: k, label: k, tag: 'layer' })),
|
|
22
|
+
{ id: 'permission', label: 'permission mode', tag: 'enum' },
|
|
23
|
+
{ id: 'sandbox', label: 'sandbox', tag: 'enum' },
|
|
24
|
+
{ id: 'inspector', label: 'inspector panel', tag: 'enum' },
|
|
25
|
+
{ id: 'ascii', label: 'ascii glyphs', tag: 'bool' },
|
|
26
|
+
{ id: 'theme', label: 'color theme', tag: 'enum' },
|
|
27
|
+
{ id: 'model', label: 'model', tag: 'model' },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function enumPickerItems(key) {
|
|
32
|
+
if (key === 'permission') return PERMISSION_MODES.map((v) => ({ id: v, label: v }));
|
|
33
|
+
if (key === 'sandbox') return SANDBOX_LEVELS.map((v) => ({ id: v, label: v }));
|
|
34
|
+
if (key === 'inspector') return INSPECTOR_MODES.map((v) => ({ id: v, label: v }));
|
|
35
|
+
if (key === 'theme') return ['auto', 'light', 'dark'].map((v) => ({ id: v, label: v }));
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isBoolSetting(key) {
|
|
40
|
+
return key === 'thinking' || key === 'ascii' || LAYER_KEYS.includes(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isEnumSetting(key) {
|
|
44
|
+
return key === 'permission' || key === 'sandbox' || key === 'inspector' || key === 'theme';
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/policy-telemetry.mjs — bounded sink for execution-policy degraded-mode
|
|
3
|
+
* events (construct-6zga.1.2 / construct-rv2x).
|
|
4
|
+
*
|
|
5
|
+
* When the owned loop compiles a turn's execution policy from a missing, unknown,
|
|
6
|
+
* or degraded capability profile, the policy collapses to the conservative
|
|
7
|
+
* envelope. The engine records that fact here instead of printing to the chat
|
|
8
|
+
* stream so the degradation is observable (dashboard, diagnostics) without
|
|
9
|
+
* touching the renderer event union or the live transcript. The buffer is
|
|
10
|
+
* in-process and bounded; it is a diagnostic signal, not durable state.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const RECENT = [];
|
|
14
|
+
const MAX_EVENTS = 50;
|
|
15
|
+
|
|
16
|
+
export function recordPolicyTelemetry(event = {}) {
|
|
17
|
+
const entry = {
|
|
18
|
+
kind: typeof event.kind === 'string' ? event.kind : 'execution-policy-degraded',
|
|
19
|
+
model: event.model ?? null,
|
|
20
|
+
capabilityClass: event.capabilityClass ?? 'unknown',
|
|
21
|
+
reasons: Array.isArray(event.reasons) ? [...event.reasons] : [],
|
|
22
|
+
};
|
|
23
|
+
RECENT.push(entry);
|
|
24
|
+
if (RECENT.length > MAX_EVENTS) RECENT.shift();
|
|
25
|
+
return entry;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function recentPolicyTelemetry() {
|
|
29
|
+
return RECENT.map((e) => ({ ...e, reasons: [...e.reasons] }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearPolicyTelemetry() {
|
|
33
|
+
RECENT.length = 0;
|
|
34
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/present.mjs — headless formatters for construct chat surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Shared by Ink TUI, linear renderer, and dashboard web cockpit. Keeps route
|
|
5
|
+
* rows, tool grouping, usage ledgers, and SSE overlay serialization identical
|
|
6
|
+
* across surfaces.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatTokens, formatUsageFooter } from './tui/usage.mjs';
|
|
10
|
+
|
|
11
|
+
export function summarizeToolCalls(tools = []) {
|
|
12
|
+
const groups = new Map();
|
|
13
|
+
for (const t of tools) {
|
|
14
|
+
const title = t.title || t.kind || 'tool';
|
|
15
|
+
if (!groups.has(title)) {
|
|
16
|
+
groups.set(title, { title, count: 0, status: 'completed', refs: [] });
|
|
17
|
+
}
|
|
18
|
+
const g = groups.get(title);
|
|
19
|
+
g.count += 1;
|
|
20
|
+
if (t.status === 'failed') g.status = 'failed';
|
|
21
|
+
else if (t.status === 'pending' && g.status !== 'failed') g.status = 'pending';
|
|
22
|
+
else if (t.status === 'in_progress' && g.status === 'completed') g.status = 'in_progress';
|
|
23
|
+
const ref = t.input?.path || t.input?.pattern || t.input?.glob || t.input?.name;
|
|
24
|
+
if (ref) {
|
|
25
|
+
const s = String(ref);
|
|
26
|
+
if (!g.refs.includes(s)) g.refs.push(s);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...groups.values()];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function summarizeSources(sources = []) {
|
|
33
|
+
if (!sources?.length) {
|
|
34
|
+
return { total: 0, byTool: {}, refs: [] };
|
|
35
|
+
}
|
|
36
|
+
const byTool = {};
|
|
37
|
+
const refs = [];
|
|
38
|
+
for (const s of sources) {
|
|
39
|
+
if (typeof s === 'string') {
|
|
40
|
+
if (!refs.includes(s)) refs.push(s);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
byTool[s.tool] = (byTool[s.tool] || 0) + 1;
|
|
44
|
+
if (s.ref && !refs.includes(s.ref)) refs.push(s.ref);
|
|
45
|
+
}
|
|
46
|
+
return { total: sources.length, byTool, refs };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatRefsInline(refs, { max = 3 } = {}) {
|
|
50
|
+
if (!refs?.length) return '';
|
|
51
|
+
if (refs.length <= max) return refs.join(', ');
|
|
52
|
+
const shown = refs.slice(0, max);
|
|
53
|
+
return `${shown.join(', ')} +${refs.length - max} more`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatSourceToolCounts(byTool) {
|
|
57
|
+
const entries = Object.entries(byTool || {});
|
|
58
|
+
if (!entries.length) return '';
|
|
59
|
+
return entries.map(([tool, n]) => `${tool} ${n}`).join(' ');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function contextRows(overlay, { layers = null } = {}) {
|
|
63
|
+
if (!overlay) return [];
|
|
64
|
+
const rows = [];
|
|
65
|
+
if (overlay.intent) rows.push({ label: 'intent', value: overlay.intent });
|
|
66
|
+
if (overlay.workCategory) rows.push({ label: 'category', value: overlay.workCategory });
|
|
67
|
+
if (overlay.track) rows.push({ label: 'track', value: overlay.track });
|
|
68
|
+
if (overlay.specialists?.length && layers?.specialists !== false) {
|
|
69
|
+
rows.push({ label: 'route', value: overlay.specialists.join(' → ') });
|
|
70
|
+
}
|
|
71
|
+
if (overlay.externalResearch?.required) {
|
|
72
|
+
const shape = overlay.externalResearch.shape ? ` (${overlay.externalResearch.shape})` : '';
|
|
73
|
+
rows.push({ label: 'research', value: `required${shape}` });
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function splitSourceLines(refs, { limit = 4 } = {}) {
|
|
79
|
+
if (!refs?.length) return { lines: ['none yet'], hidden: 0, total: 0 };
|
|
80
|
+
const shown = refs.slice(0, limit);
|
|
81
|
+
return {
|
|
82
|
+
lines: shown,
|
|
83
|
+
hidden: Math.max(0, refs.length - limit),
|
|
84
|
+
total: refs.length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function toolGroupLabel(group) {
|
|
89
|
+
const count = group.count > 1 ? ` ×${group.count}` : '';
|
|
90
|
+
const refs = formatRefsInline(group.refs, { max: 2 });
|
|
91
|
+
return refs ? `${group.title}${count} ${refs}` : `${group.title}${count}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function toolStatusGlyph(status) {
|
|
95
|
+
if (status === 'completed') return '✓';
|
|
96
|
+
if (status === 'failed') return '✗';
|
|
97
|
+
if (status === 'in_progress') return '›';
|
|
98
|
+
return '·';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {Record<string, unknown> | null | undefined} overlay
|
|
103
|
+
* @param {{ layers?: Record<string, boolean> | null }} [options]
|
|
104
|
+
* @returns {{
|
|
105
|
+
* chain: string[],
|
|
106
|
+
* intent: string | null,
|
|
107
|
+
* track: string | null,
|
|
108
|
+
* gates: Array<{ label: string, value: string }>,
|
|
109
|
+
* summary: string | null,
|
|
110
|
+
* chainLine: string | null,
|
|
111
|
+
* } | null}
|
|
112
|
+
*/
|
|
113
|
+
export function formatRouteStrip(overlay, { layers = null } = {}) {
|
|
114
|
+
if (!overlay) return null;
|
|
115
|
+
const showSpecialists = layers?.specialists !== false;
|
|
116
|
+
const chain = showSpecialists && Array.isArray(overlay.specialists)
|
|
117
|
+
? [...overlay.specialists]
|
|
118
|
+
: [];
|
|
119
|
+
const intent = overlay.intent || null;
|
|
120
|
+
const track = overlay.track || null;
|
|
121
|
+
const gates = formatGateRows(overlay);
|
|
122
|
+
const summary = overlay.dispatchSummary || null;
|
|
123
|
+
const chainLine = showSpecialists
|
|
124
|
+
? (chain.length ? chain.join(' → ') : 'direct')
|
|
125
|
+
: null;
|
|
126
|
+
return { chain, intent, track, gates, summary, chainLine };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Record<string, unknown> | null | undefined} overlay
|
|
131
|
+
* @param {{ layers?: Record<string, boolean> | null }} [options]
|
|
132
|
+
*/
|
|
133
|
+
export function formatRouteLogLine(overlay, { layers = null } = {}) {
|
|
134
|
+
const strip = formatRouteStrip(overlay, { layers });
|
|
135
|
+
if (!strip) return '';
|
|
136
|
+
const parts = [];
|
|
137
|
+
if (strip.intent) parts.push(`intent=${strip.intent}`);
|
|
138
|
+
if (strip.track) parts.push(`track=${strip.track}`);
|
|
139
|
+
if (strip.chainLine) parts.push(strip.chainLine);
|
|
140
|
+
return parts.join(' · ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatRiskFlags(riskFlags = {}) {
|
|
144
|
+
return Object.entries(riskFlags)
|
|
145
|
+
.filter(([, v]) => v)
|
|
146
|
+
.map(([k]) => k);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatContractChain(chain = []) {
|
|
150
|
+
return chain.map((edge) => `${edge.producer} → ${edge.consumer}${edge.stage ? ` (${edge.stage})` : ''}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function formatGateRows(overlay) {
|
|
154
|
+
if (!overlay) return [];
|
|
155
|
+
const rows = [];
|
|
156
|
+
if (overlay.externalResearch?.required) {
|
|
157
|
+
const detail = overlay.externalResearch.shape || overlay.externalResearch.reason || 'yes';
|
|
158
|
+
rows.push({ label: 'research', value: `required (${detail})` });
|
|
159
|
+
}
|
|
160
|
+
if (overlay.framingChallenge?.required) {
|
|
161
|
+
rows.push({ label: 'framing', value: 'challenge required' });
|
|
162
|
+
}
|
|
163
|
+
if (overlay.docAuthoring?.docType) {
|
|
164
|
+
rows.push({ label: 'doc', value: `${overlay.docAuthoring.docType} → ${overlay.docAuthoring.owner || 'unknown'}` });
|
|
165
|
+
}
|
|
166
|
+
if (overlay.artifactReview?.requiredReviewers?.length) {
|
|
167
|
+
rows.push({
|
|
168
|
+
label: 'reviewers',
|
|
169
|
+
value: overlay.artifactReview.requiredReviewers.join(', '),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function contextMeterBar(used, size, width = 18) {
|
|
176
|
+
const ratio = size > 0 ? Math.max(0, Math.min(1, used / size)) : 0;
|
|
177
|
+
const filled = Math.round(ratio * width);
|
|
178
|
+
return {
|
|
179
|
+
bar: '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled)),
|
|
180
|
+
ratio,
|
|
181
|
+
percent: `${Math.round(ratio * 100)}%`,
|
|
182
|
+
label: `${formatTokens(used) || '0'}/${formatTokens(size) || '?'}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function sessionUsageSummary(session) {
|
|
187
|
+
const t = session?.usage?.tokens || session?.tokens || {};
|
|
188
|
+
const parts = [];
|
|
189
|
+
if (t.total) parts.push(`${formatTokens(t.total)} tok`);
|
|
190
|
+
const cost = session?.usage?.cost?.amount ?? session?.cost?.amount;
|
|
191
|
+
if (cost > 0) parts.push(`~$${cost.toFixed(cost < 1 ? 3 : 2)}`);
|
|
192
|
+
const turns = session?.usage?.turns ?? session?.turns;
|
|
193
|
+
if (turns) parts.push(`${turns} turn${turns === 1 ? '' : 's'}`);
|
|
194
|
+
return parts.join(' · ') || 'no tokens yet';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function formatTurnUsageLine(usage) {
|
|
198
|
+
return formatUsageFooter(usage, {}).replace(/^\[usage\] /, '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function overlayToSsePayload(overlay) {
|
|
202
|
+
if (!overlay) return null;
|
|
203
|
+
return {
|
|
204
|
+
type: 'overlay',
|
|
205
|
+
intent: overlay.intent || null,
|
|
206
|
+
workCategory: overlay.workCategory || null,
|
|
207
|
+
track: overlay.track || null,
|
|
208
|
+
specialists: overlay.specialists || [],
|
|
209
|
+
externalResearch: overlay.externalResearch || null,
|
|
210
|
+
riskFlags: overlay.riskFlags || null,
|
|
211
|
+
contractChain: overlay.contractChain || [],
|
|
212
|
+
framingChallenge: overlay.framingChallenge || null,
|
|
213
|
+
dispatchSummary: overlay.dispatchSummary || null,
|
|
214
|
+
dispatchReasons: overlay.dispatchReasons || null,
|
|
215
|
+
triggers: overlay.triggers || [],
|
|
216
|
+
docAuthoring: overlay.docAuthoring || null,
|
|
217
|
+
artifactReview: overlay.artifactReview || null,
|
|
218
|
+
sessionTurnIndex: overlay.sessionTurnIndex ?? 0,
|
|
219
|
+
priorIntent: overlay.priorIntent ?? null,
|
|
220
|
+
workingBranch: overlay.workingBranch ?? null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function sessionMetaToSsePayload({ session, layers, workingBranch, oracle, ctx = null }) {
|
|
225
|
+
return {
|
|
226
|
+
type: 'session_meta',
|
|
227
|
+
model: session?.model || null,
|
|
228
|
+
demoLabel: session?.demoGuide ? (session.demoTitle || 'scripted') : null,
|
|
229
|
+
modelMode: session?.modelMode || 'pinned',
|
|
230
|
+
sandbox: session?.sandbox || null,
|
|
231
|
+
permissionMode: session?.permissionMode || null,
|
|
232
|
+
layers: layers || {},
|
|
233
|
+
workingBranch: workingBranch || null,
|
|
234
|
+
ctx,
|
|
235
|
+
oracle: oracle ? {
|
|
236
|
+
visible: oracle.visible,
|
|
237
|
+
summary: oracle.summary,
|
|
238
|
+
topGaps: (oracle.topGaps || []).map((g) => ({ id: g.id, detail: g.detail || g.message || '' })),
|
|
239
|
+
} : null,
|
|
240
|
+
usage: session?.usage ? {
|
|
241
|
+
turns: session.usage.turns,
|
|
242
|
+
tokens: { ...session.usage.tokens },
|
|
243
|
+
cost: session.usage.cost ? { ...session.usage.cost } : null,
|
|
244
|
+
} : null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/session-context.mjs — session-scoped context for planTurn routing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export function buildPlanContext({ session, cwd = process.cwd(), turnBlocks = [], text = '' } = {}) {
|
|
10
|
+
const turns = turnBlocks.filter((item) => item.kind === 'turn');
|
|
11
|
+
const lastTurn = turns.length ? turns[turns.length - 1].block : null;
|
|
12
|
+
|
|
13
|
+
let workingBranch = null;
|
|
14
|
+
try {
|
|
15
|
+
workingBranch = execSync('git branch --show-current', { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
16
|
+
} catch { /* not a git repo */ }
|
|
17
|
+
|
|
18
|
+
let projectSummary = null;
|
|
19
|
+
const contextPath = path.join(cwd, '.cx', 'context.md');
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(contextPath)) {
|
|
22
|
+
projectSummary = fs.readFileSync(contextPath, 'utf8').slice(0, 500);
|
|
23
|
+
}
|
|
24
|
+
} catch { /* unreadable */ }
|
|
25
|
+
|
|
26
|
+
const trimmed = String(text).trim();
|
|
27
|
+
const vagueFollowUp = /^(tell me more|what about|continue|go on|explain|elaborate|and\?)/i.test(trimmed);
|
|
28
|
+
const projectQuestion = /\b(what is this project|what('s| is) this (repo|project|codebase)|describe this project)\b/i.test(trimmed);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
turnIndex: session?.usage?.turns ?? 0,
|
|
32
|
+
priorIntent: lastTurn?.overlay?.intent ?? null,
|
|
33
|
+
priorWorkCategory: lastTurn?.overlay?.workCategory ?? null,
|
|
34
|
+
workingBranch,
|
|
35
|
+
projectSummary,
|
|
36
|
+
vagueFollowUp: vagueFollowUp && (session?.usage?.turns ?? 0) > 0,
|
|
37
|
+
projectQuestion,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/session-persist.mjs — JSONL transcript persistence for construct chat surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Shared by CLI linear mode and dashboard owned-loop SSE so web/desktop resume
|
|
5
|
+
* matches .cx/chat-sessions/ on disk.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
11
|
+
import { deserializeBlock, serializeBlock, snapshotTurn } from './tui/turn-block.mjs';
|
|
12
|
+
import { migrateEvidenceVerdict } from './evidence.mjs';
|
|
13
|
+
|
|
14
|
+
export function chatSessionFilePath({ cwd, convId, host = 'construct-web' }) {
|
|
15
|
+
const dir = resolveProjectScopedPath('chat-sessions', { cwd });
|
|
16
|
+
return path.join(dir, `${convId}-${host}.jsonl`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createChatPersister({ cwd, sessionId, resumePath = null, host = 'construct' }) {
|
|
20
|
+
try {
|
|
21
|
+
const dir = resolveProjectScopedPath('chat-sessions', { cwd });
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
const file = resumePath || path.join(dir, `${new Date().toISOString().replace(/[:.]/g, '-')}-${host}-${sessionId}.jsonl`);
|
|
24
|
+
if (!resumePath) {
|
|
25
|
+
fs.appendFileSync(file, `${JSON.stringify({ type: 'session_start', host, sessionId, ts: new Date().toISOString() })}\n`);
|
|
26
|
+
}
|
|
27
|
+
const append = (row) => {
|
|
28
|
+
try {
|
|
29
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...row, ts: new Date().toISOString() })}\n`);
|
|
30
|
+
} catch { /* log is best-effort */ }
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
filePath: file,
|
|
34
|
+
event: (event) => append(event),
|
|
35
|
+
transcript: (role, text) => append({ type: 'transcript', role, text }),
|
|
36
|
+
transcriptBlock: (turn) => append(serializeBlock(turn)),
|
|
37
|
+
};
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadPersistedTurns(filePath) {
|
|
44
|
+
if (!filePath || !fs.existsSync(filePath)) return { turns: [], turnBlocks: [] };
|
|
45
|
+
const turns = [];
|
|
46
|
+
const turnBlocks = [];
|
|
47
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
try {
|
|
50
|
+
const row = JSON.parse(line);
|
|
51
|
+
const block = deserializeBlock(row);
|
|
52
|
+
if (!block) continue;
|
|
53
|
+
turnBlocks.push({ kind: 'turn', block });
|
|
54
|
+
const snap = snapshotTurn(block);
|
|
55
|
+
const evidence = migrateEvidenceVerdict(snap);
|
|
56
|
+
turns.push({
|
|
57
|
+
id: snap.id || `turn-${turns.length + 1}`,
|
|
58
|
+
userText: snap.userText || '',
|
|
59
|
+
assistant: snap.assistant || '',
|
|
60
|
+
thinking: snap.thinking || '',
|
|
61
|
+
tools: snap.tools || [],
|
|
62
|
+
overlay: snap.overlay || null,
|
|
63
|
+
sources: snap.sources || [],
|
|
64
|
+
usage: snap.usage || null,
|
|
65
|
+
evidence,
|
|
66
|
+
unverified: ['insufficient_evidence', 'uncited_evidence', 'partially_verified'].includes(evidence.status),
|
|
67
|
+
working: false,
|
|
68
|
+
system: false,
|
|
69
|
+
});
|
|
70
|
+
} catch { /* skip malformed rows */ }
|
|
71
|
+
}
|
|
72
|
+
return { turns, turnBlocks };
|
|
73
|
+
}
|