@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/handoffs/cleanup.mjs — automatic handoff hygiene.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle:
|
|
5
|
+
* 1. Open handoffs (referenced beads not all closed) stay where they are.
|
|
6
|
+
* 2. Resolved handoffs (referenced beads all closed) older than
|
|
7
|
+
* `handoffsMaxDays` get MOVED to `.cx/handoffs/archive/`.
|
|
8
|
+
* 3. Archived handoffs older than `2 * handoffsMaxDays` get DELETED.
|
|
9
|
+
* 4. Any open handoff past `handoffsMaxItems` (FIFO by mtime) is
|
|
10
|
+
* surfaced as a warning but never deleted — open work is sacred.
|
|
11
|
+
*
|
|
12
|
+
* The function is idempotent and runs from:
|
|
13
|
+
* - `construct down` (best effort, time-boxed)
|
|
14
|
+
* - doctor daemon tick (lib/doctor/watchers/handoffs.mjs)
|
|
15
|
+
* - `construct handoffs prune` (manual)
|
|
16
|
+
*
|
|
17
|
+
* Planning never recurses into archive/. The archive deletion threshold
|
|
18
|
+
* compares against archive-dir mtimes, reset on move.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
25
|
+
import { parseHandoffFile } from './contract.mjs';
|
|
26
|
+
|
|
27
|
+
const HANDOFFS_DIR = '.cx/handoffs';
|
|
28
|
+
const ARCHIVE_DIR = '.cx/handoffs/archive';
|
|
29
|
+
|
|
30
|
+
function bdRootCwd(projectRoot) {
|
|
31
|
+
try {
|
|
32
|
+
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8' });
|
|
33
|
+
if (r.status !== 0) return projectRoot;
|
|
34
|
+
const trimmed = r.stdout.trim();
|
|
35
|
+
const resolved = trimmed.startsWith('/') ? trimmed : path.join(projectRoot, trimmed);
|
|
36
|
+
const candidate = path.dirname(resolved);
|
|
37
|
+
if (fs.existsSync(path.join(candidate, '.beads'))) return candidate;
|
|
38
|
+
} catch { /* fall through */ }
|
|
39
|
+
return projectRoot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadClosedBeadIds(projectRoot) {
|
|
43
|
+
try {
|
|
44
|
+
const r = spawnSync('bd', ['list', '--status', 'closed', '--json'], {
|
|
45
|
+
cwd: bdRootCwd(projectRoot),
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
48
|
+
timeout: 5000,
|
|
49
|
+
});
|
|
50
|
+
if (r.status !== 0) return new Set();
|
|
51
|
+
const issues = JSON.parse(r.stdout || '[]');
|
|
52
|
+
return new Set(issues.map((it) => String(it.id).toLowerCase()));
|
|
53
|
+
} catch { return new Set(); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listHandoffFiles(dir) {
|
|
57
|
+
if (!fs.existsSync(dir)) return [];
|
|
58
|
+
return fs.readdirSync(dir)
|
|
59
|
+
.filter((f) => f.endsWith('.md'))
|
|
60
|
+
.map((f) => {
|
|
61
|
+
const full = path.join(dir, f);
|
|
62
|
+
const stat = fs.statSync(full);
|
|
63
|
+
return { filename: f, path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getRefBeads(filePath) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = parseHandoffFile(filePath);
|
|
70
|
+
if (Array.isArray(parsed.frontmatter?.beads) && parsed.frontmatter.beads.length > 0) {
|
|
71
|
+
return parsed.frontmatter.beads.map((s) => String(s).toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
// Legacy fallback: scrape `construct-xxx` references from the body.
|
|
74
|
+
const text = parsed.body || '';
|
|
75
|
+
const matches = text.match(/\bconstruct-[a-z0-9]+\b/gi) || [];
|
|
76
|
+
return Array.from(new Set(matches.map((s) => s.toLowerCase())));
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function planHandoffCleanup(projectRoot = process.cwd(), env = process.env, { now = Date.now() } = {}) {
|
|
83
|
+
const { config } = loadProjectConfig(projectRoot, env);
|
|
84
|
+
const disk = config?.resources?.disk || {};
|
|
85
|
+
const maxDays = disk.handoffsMaxDays ?? 30;
|
|
86
|
+
const maxItems = disk.handoffsMaxItems ?? 50;
|
|
87
|
+
|
|
88
|
+
const liveDir = path.join(projectRoot, HANDOFFS_DIR);
|
|
89
|
+
const archiveDir = path.join(projectRoot, ARCHIVE_DIR);
|
|
90
|
+
const liveFiles = listHandoffFiles(liveDir);
|
|
91
|
+
const archiveFiles = listHandoffFiles(archiveDir);
|
|
92
|
+
|
|
93
|
+
const archiveCutoff = now - maxDays * 24 * 60 * 60 * 1000;
|
|
94
|
+
const deleteCutoff = now - 2 * maxDays * 24 * 60 * 60 * 1000;
|
|
95
|
+
|
|
96
|
+
const closed = loadClosedBeadIds(projectRoot);
|
|
97
|
+
const actions = [];
|
|
98
|
+
const warnings = [];
|
|
99
|
+
|
|
100
|
+
for (const f of liveFiles) {
|
|
101
|
+
const beads = getRefBeads(f.path);
|
|
102
|
+
const allClosed = beads.length > 0 && beads.every((id) => closed.has(id));
|
|
103
|
+
if (allClosed && f.mtimeMs < archiveCutoff) {
|
|
104
|
+
actions.push({
|
|
105
|
+
kind: 'archive',
|
|
106
|
+
from: f.path,
|
|
107
|
+
to: path.join(archiveDir, f.filename),
|
|
108
|
+
reason: `referenced beads (${beads.join(', ')}) are closed and file is older than ${maxDays}d`,
|
|
109
|
+
bytes: f.bytes,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const f of archiveFiles) {
|
|
115
|
+
if (f.mtimeMs < deleteCutoff) {
|
|
116
|
+
actions.push({
|
|
117
|
+
kind: 'delete',
|
|
118
|
+
path: f.path,
|
|
119
|
+
reason: `archived handoff older than ${2 * maxDays}d`,
|
|
120
|
+
bytes: f.bytes,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Soft warning when too many live handoffs accumulate. Never delete
|
|
126
|
+
// live handoffs automatically — they may reference open work.
|
|
127
|
+
if (liveFiles.length > maxItems) {
|
|
128
|
+
warnings.push(`${liveFiles.length} live handoffs exceeds maxItems=${maxItems}. Close or archive resolved beads to bring it down.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { actions, warnings, maxDays, maxItems, archiveCutoff, deleteCutoff };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function executeHandoffCleanup(plan) {
|
|
135
|
+
const moved = [];
|
|
136
|
+
const deleted = [];
|
|
137
|
+
for (const a of plan.actions) {
|
|
138
|
+
try {
|
|
139
|
+
if (a.kind === 'archive') {
|
|
140
|
+
fs.mkdirSync(path.dirname(a.to), { recursive: true });
|
|
141
|
+
fs.renameSync(a.from, a.to);
|
|
142
|
+
moved.push(a);
|
|
143
|
+
} else if (a.kind === 'delete') {
|
|
144
|
+
fs.unlinkSync(a.path);
|
|
145
|
+
deleted.push(a);
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
a.error = err.message;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { moved, deleted, warnings: plan.warnings };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function autoCleanHandoffs(projectRoot = process.cwd(), env = process.env) {
|
|
155
|
+
const plan = planHandoffCleanup(projectRoot, env);
|
|
156
|
+
if (plan.actions.length === 0) return { plan, result: { moved: [], deleted: [], warnings: plan.warnings } };
|
|
157
|
+
const result = executeHandoffCleanup(plan);
|
|
158
|
+
return { plan, result };
|
|
159
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/handoffs/contract.mjs — handoff schema + parse/validate/format.
|
|
3
|
+
*
|
|
4
|
+
* A handoff is a markdown file in `.cx/handoffs/` written when a session
|
|
5
|
+
* bridges work to the next agent. Until now the shape was implicit, so
|
|
6
|
+
* handoffs varied in quality and there was no automated cleanup beyond
|
|
7
|
+
* raw mtime/count caps. This module defines a contract:
|
|
8
|
+
*
|
|
9
|
+
* ---
|
|
10
|
+
* schema: cx-handoff/v1
|
|
11
|
+
* id: 2026-05-18-something-something # filename stem
|
|
12
|
+
* created: 2026-05-18T14:00:00.000Z
|
|
13
|
+
* from_session: <session-uuid-or-alias>
|
|
14
|
+
* beads: [construct-xyz, construct-abc] # work this handoff is about
|
|
15
|
+
* status: open # open | resolved | archived
|
|
16
|
+
* title: One-line summary
|
|
17
|
+
* tags: [optional, free-form]
|
|
18
|
+
* ---
|
|
19
|
+
*
|
|
20
|
+
* ## What was done
|
|
21
|
+
* …
|
|
22
|
+
* ## What's left
|
|
23
|
+
* …
|
|
24
|
+
* ## Open questions
|
|
25
|
+
* …
|
|
26
|
+
* ## How to resume
|
|
27
|
+
* …
|
|
28
|
+
*
|
|
29
|
+
* Status semantics:
|
|
30
|
+
* - open — bead(s) still in progress or open; handoff is live context
|
|
31
|
+
* - resolved — every referenced bead is closed; handoff is informational
|
|
32
|
+
* - archived — moved to .cx/handoffs/archive/ during cleanup
|
|
33
|
+
*
|
|
34
|
+
* Cleanup uses these fields:
|
|
35
|
+
* - resolved handoffs older than retention → archived
|
|
36
|
+
* - archived handoffs older than 2× retention → deleted
|
|
37
|
+
* - any handoff past `handoffsMaxItems` (FIFO by mtime) → deleted
|
|
38
|
+
*
|
|
39
|
+
* `parseHandoff` is intentionally permissive — pre-contract handoffs with
|
|
40
|
+
* no frontmatter still parse with status='legacy' so they aren't lost.
|
|
41
|
+
* `validateHandoff` is the strict version, used when writing.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { readFileSync } from 'node:fs';
|
|
45
|
+
|
|
46
|
+
export const HANDOFF_SCHEMA_VERSION = 'cx-handoff/v1';
|
|
47
|
+
export const HANDOFF_STATUSES = Object.freeze(['open', 'resolved', 'archived']);
|
|
48
|
+
export const REQUIRED_SECTIONS = Object.freeze(['What was done', "What's left"]);
|
|
49
|
+
|
|
50
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
51
|
+
|
|
52
|
+
function parseFrontmatterBlock(text) {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const line of text.split('\n')) {
|
|
55
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
56
|
+
const idx = line.indexOf(':');
|
|
57
|
+
if (idx === -1) continue;
|
|
58
|
+
const key = line.slice(0, idx).trim();
|
|
59
|
+
const rawValue = line.slice(idx + 1).trim();
|
|
60
|
+
if (!key) continue;
|
|
61
|
+
if (rawValue.startsWith('[') && rawValue.endsWith(']')) {
|
|
62
|
+
out[key] = rawValue.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
|
|
63
|
+
} else if (rawValue === 'true' || rawValue === 'false') {
|
|
64
|
+
out[key] = rawValue === 'true';
|
|
65
|
+
} else if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
66
|
+
out[key] = Number(rawValue);
|
|
67
|
+
} else {
|
|
68
|
+
const unquoted = rawValue.replace(/^["']|["']$/g, '');
|
|
69
|
+
out[key] = unquoted;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseHandoff(text) {
|
|
76
|
+
if (typeof text !== 'string') {
|
|
77
|
+
return { frontmatter: {}, body: '', sections: {}, status: 'invalid', error: 'text required' };
|
|
78
|
+
}
|
|
79
|
+
const fmMatch = text.match(FRONTMATTER_RE);
|
|
80
|
+
let frontmatter = {};
|
|
81
|
+
let body = text;
|
|
82
|
+
if (fmMatch) {
|
|
83
|
+
frontmatter = parseFrontmatterBlock(fmMatch[1]);
|
|
84
|
+
body = text.slice(fmMatch[0].length);
|
|
85
|
+
}
|
|
86
|
+
const sections = {};
|
|
87
|
+
let currentHeading = null;
|
|
88
|
+
let currentLines = [];
|
|
89
|
+
for (const line of body.split('\n')) {
|
|
90
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
91
|
+
if (heading) {
|
|
92
|
+
if (currentHeading) sections[currentHeading] = currentLines.join('\n').trim();
|
|
93
|
+
currentHeading = heading[1];
|
|
94
|
+
currentLines = [];
|
|
95
|
+
} else if (currentHeading) {
|
|
96
|
+
currentLines.push(line);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (currentHeading) sections[currentHeading] = currentLines.join('\n').trim();
|
|
100
|
+
|
|
101
|
+
const status = frontmatter?.schema === HANDOFF_SCHEMA_VERSION
|
|
102
|
+
? (HANDOFF_STATUSES.includes(frontmatter.status) ? frontmatter.status : 'open')
|
|
103
|
+
: 'legacy';
|
|
104
|
+
return { frontmatter, body, sections, status };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseHandoffFile(filePath) {
|
|
108
|
+
const text = readFileSync(filePath, 'utf8');
|
|
109
|
+
return parseHandoff(text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function validateHandoff(parsed) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const fm = parsed?.frontmatter || {};
|
|
115
|
+
if (fm.schema !== HANDOFF_SCHEMA_VERSION) errors.push(`schema must be '${HANDOFF_SCHEMA_VERSION}'`);
|
|
116
|
+
if (typeof fm.id !== 'string' || !fm.id) errors.push('id required (typically the filename stem)');
|
|
117
|
+
if (typeof fm.created !== 'string' || !/\d{4}-\d{2}-\d{2}/.test(fm.created)) errors.push("created required (ISO-8601 string starting with YYYY-MM-DD)");
|
|
118
|
+
if (typeof fm.title !== 'string' || !fm.title.trim()) errors.push('title required');
|
|
119
|
+
if (fm.status && !HANDOFF_STATUSES.includes(fm.status)) errors.push(`status must be one of ${HANDOFF_STATUSES.join(', ')}`);
|
|
120
|
+
if (fm.beads !== undefined && !Array.isArray(fm.beads)) errors.push('beads must be an array of bd issue ids');
|
|
121
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
122
|
+
if (!parsed?.sections?.[section] || !parsed.sections[section].trim()) {
|
|
123
|
+
errors.push(`required section missing or empty: "${section}"`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { valid: errors.length === 0, errors };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatHandoff({ id, title, beads = [], status = 'open', from_session = null, tags = [], whatWasDone = '', whatsLeft = '', openQuestions = '', howToResume = '' } = {}) {
|
|
130
|
+
if (!id) throw new Error('formatHandoff: id required');
|
|
131
|
+
if (!title) throw new Error('formatHandoff: title required');
|
|
132
|
+
const lines = [
|
|
133
|
+
'---',
|
|
134
|
+
`schema: ${HANDOFF_SCHEMA_VERSION}`,
|
|
135
|
+
`id: ${id}`,
|
|
136
|
+
`created: ${new Date().toISOString()}`,
|
|
137
|
+
from_session ? `from_session: ${from_session}` : null,
|
|
138
|
+
`beads: [${beads.join(', ')}]`,
|
|
139
|
+
`status: ${status}`,
|
|
140
|
+
`title: ${JSON.stringify(title)}`,
|
|
141
|
+
tags.length ? `tags: [${tags.join(', ')}]` : null,
|
|
142
|
+
'---',
|
|
143
|
+
'',
|
|
144
|
+
'## What was done',
|
|
145
|
+
'',
|
|
146
|
+
whatWasDone.trim() || '_TBD_',
|
|
147
|
+
'',
|
|
148
|
+
"## What's left",
|
|
149
|
+
'',
|
|
150
|
+
whatsLeft.trim() || '_TBD_',
|
|
151
|
+
'',
|
|
152
|
+
'## Open questions',
|
|
153
|
+
'',
|
|
154
|
+
openQuestions.trim() || '_(none)_',
|
|
155
|
+
'',
|
|
156
|
+
'## How to resume',
|
|
157
|
+
'',
|
|
158
|
+
howToResume.trim() || '_TBD_',
|
|
159
|
+
'',
|
|
160
|
+
].filter((line) => line !== null);
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/handoffs/inventory.mjs — handoff hygiene primitives.
|
|
3
|
+
*
|
|
4
|
+
* Handoffs (markdown files in `.cx/handoffs/`) are written by
|
|
5
|
+
* `lib/beads-automation.mjs::create-handoff` whenever a session bridges
|
|
6
|
+
* to the next agent. The inventory helper surfaces:
|
|
7
|
+
*
|
|
8
|
+
* - total file count + total bytes
|
|
9
|
+
* - oldest mtime (age, in days)
|
|
10
|
+
* - count of files past `handoffsMaxDays` (eligible to prune)
|
|
11
|
+
* - count of files referencing a *closed* bead (stale-but-resolved)
|
|
12
|
+
* - per-file id, status, and path (from contract parser) for CLI display
|
|
13
|
+
*
|
|
14
|
+
* The dashboard insights card consumes this; doctor surfaces a warning
|
|
15
|
+
* when stale handoffs accumulate past the threshold.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
21
|
+
import { parseHandoff } from './contract.mjs';
|
|
22
|
+
|
|
23
|
+
const HANDOFFS_REL = '.cx/handoffs';
|
|
24
|
+
const BEAD_REF_RE = /\bconstruct-[a-z0-9]+\b/gi;
|
|
25
|
+
|
|
26
|
+
function readFiles(dir) {
|
|
27
|
+
if (!fs.existsSync(dir)) return [];
|
|
28
|
+
return fs.readdirSync(dir)
|
|
29
|
+
.filter((f) => f.endsWith('.md'))
|
|
30
|
+
.map((f) => {
|
|
31
|
+
const full = path.join(dir, f);
|
|
32
|
+
const stat = fs.statSync(full);
|
|
33
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
34
|
+
const beadRefs = Array.from(new Set((content.match(BEAD_REF_RE) || []).map((s) => s.toLowerCase())));
|
|
35
|
+
const parsed = parseHandoff(content);
|
|
36
|
+
const id = parsed.frontmatter?.id || f.replace(/\.md$/, '');
|
|
37
|
+
return { filename: f, id, path: full, mtimeMs: stat.mtimeMs, bytes: stat.size, beadRefs, status: parsed.status };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bdRootCwd(projectRoot) {
|
|
42
|
+
try {
|
|
43
|
+
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8' });
|
|
44
|
+
if (r.status !== 0) return projectRoot;
|
|
45
|
+
const trimmed = r.stdout.trim();
|
|
46
|
+
const resolved = trimmed.startsWith('/') ? trimmed : path.join(projectRoot, trimmed);
|
|
47
|
+
const candidate = path.dirname(resolved);
|
|
48
|
+
if (fs.existsSync(path.join(candidate, '.beads'))) return candidate;
|
|
49
|
+
} catch { /* fall through */ }
|
|
50
|
+
return projectRoot;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function closedBeadIds(projectRoot) {
|
|
54
|
+
try {
|
|
55
|
+
const r = spawnSync('bd', ['list', '--status', 'closed', '--json'], {
|
|
56
|
+
cwd: bdRootCwd(projectRoot),
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
59
|
+
timeout: 5000,
|
|
60
|
+
});
|
|
61
|
+
if (r.status !== 0) return new Set();
|
|
62
|
+
const issues = JSON.parse(r.stdout);
|
|
63
|
+
return new Set(issues.map((it) => String(it.id).toLowerCase()));
|
|
64
|
+
} catch { return new Set(); }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function summarizeHandoffs(projectRoot = process.cwd(), env = process.env, { now = Date.now() } = {}) {
|
|
68
|
+
const dir = path.join(projectRoot, HANDOFFS_REL);
|
|
69
|
+
const files = readFiles(dir);
|
|
70
|
+
if (files.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
state: 'empty',
|
|
73
|
+
total: 0,
|
|
74
|
+
bytes: 0,
|
|
75
|
+
oldestAgeDays: null,
|
|
76
|
+
pastRetentionCount: 0,
|
|
77
|
+
resolvedCount: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const { config } = loadProjectConfig(projectRoot, env);
|
|
81
|
+
const maxDays = config?.resources?.disk?.handoffsMaxDays ?? 30;
|
|
82
|
+
const maxItems = config?.resources?.disk?.handoffsMaxItems ?? 50;
|
|
83
|
+
const cutoff = now - maxDays * 24 * 60 * 60 * 1000;
|
|
84
|
+
const sorted = [...files].sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
85
|
+
const oldestAgeMs = now - sorted[0].mtimeMs;
|
|
86
|
+
const closed = closedBeadIds(projectRoot);
|
|
87
|
+
let pastRetentionCount = 0;
|
|
88
|
+
let resolvedCount = 0;
|
|
89
|
+
let resolvedFiles = [];
|
|
90
|
+
for (const f of files) {
|
|
91
|
+
if (f.mtimeMs < cutoff) pastRetentionCount += 1;
|
|
92
|
+
if (f.beadRefs.length > 0 && f.beadRefs.every((id) => closed.has(id))) {
|
|
93
|
+
resolvedCount += 1;
|
|
94
|
+
resolvedFiles.push(f.filename);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const bytes = files.reduce((s, f) => s + f.bytes, 0);
|
|
98
|
+
const enriched = files.map((f) => ({
|
|
99
|
+
...f,
|
|
100
|
+
ageDays: (now - f.mtimeMs) / (24 * 60 * 60 * 1000),
|
|
101
|
+
}));
|
|
102
|
+
return {
|
|
103
|
+
state: 'ok',
|
|
104
|
+
total: files.length,
|
|
105
|
+
bytes,
|
|
106
|
+
oldestAgeDays: oldestAgeMs / (24 * 60 * 60 * 1000),
|
|
107
|
+
maxDays,
|
|
108
|
+
maxItems,
|
|
109
|
+
pastRetentionCount,
|
|
110
|
+
resolvedCount,
|
|
111
|
+
resolvedFiles: resolvedFiles.slice(0, 10),
|
|
112
|
+
files: enriched,
|
|
113
|
+
recent: sorted.slice(-5).reverse().map((f) => ({
|
|
114
|
+
filename: f.filename,
|
|
115
|
+
ageDays: (now - f.mtimeMs) / (24 * 60 * 60 * 1000),
|
|
116
|
+
beadRefs: f.beadRefs,
|
|
117
|
+
bytes: f.bytes,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
package/lib/headhunt.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/headhunt.mjs —
|
|
3
|
+
* lib/headhunt.mjs — Domain overlay activation and promotion for Construct agents.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Creates temporary domain overlays that attach specialist knowledge to named
|
|
6
|
+
* agents, tracks overlay expiry, and manages promotion requests when a transient
|
|
7
|
+
* overlay should become a permanent skill. Backs the `construct headhunt` CLI
|
|
8
|
+
* and the MCP headhunt tool.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import fs from 'node:fs';
|
|
9
12
|
import os from 'node:os';
|
|
10
13
|
import path from 'node:path';
|
|
11
14
|
import { createInterface } from 'node:readline';
|
|
12
|
-
|
|
13
|
-
import { loadWorkflow, updateTask } from './workflow-state.mjs';
|
|
15
|
+
import { selectOption } from './tty-prompts.mjs';
|
|
14
16
|
|
|
15
17
|
const OVERLAY_DIRNAME = 'domain-overlays';
|
|
16
18
|
const PROMOTION_DIRNAME = 'promotion-requests';
|
|
@@ -181,45 +183,12 @@ function unique(values = []) {
|
|
|
181
183
|
return [...new Set((values || []).filter(Boolean))];
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
function findWorkflowTaskForOverlay(workflow, overlayId = null) {
|
|
185
|
-
const tasks = workflow?.tasks || [];
|
|
186
|
-
if (overlayId) {
|
|
187
|
-
const linkedTask = tasks.find((task) => Array.isArray(task.overlays) && task.overlays.includes(overlayId));
|
|
188
|
-
if (linkedTask) return linkedTask;
|
|
189
|
-
}
|
|
190
|
-
if (workflow?.currentTaskKey) {
|
|
191
|
-
const currentTask = tasks.find((task) => task.key === workflow.currentTaskKey);
|
|
192
|
-
if (currentTask) return currentTask;
|
|
193
|
-
}
|
|
194
|
-
return tasks.find((task) => task.status === 'in-progress' || task.status === 'blocked_needs_user') || null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
186
|
function attachOverlayToWorkflow(root, overlay, { challengeRequired = false, challengeStatus = null } = {}) {
|
|
198
|
-
|
|
199
|
-
const task = findWorkflowTaskForOverlay(workflow);
|
|
200
|
-
if (!task) return null;
|
|
201
|
-
|
|
202
|
-
updateTask(root, task.key, {
|
|
203
|
-
overlays: unique([...(task.overlays || []), overlay.id]),
|
|
204
|
-
challengeRequired: challengeRequired || task.challengeRequired,
|
|
205
|
-
challengeStatus: challengeStatus ?? task.challengeStatus,
|
|
206
|
-
note: `Attached domain overlay ${overlay.id} (${overlay.domain}).`,
|
|
207
|
-
});
|
|
208
|
-
return task.key;
|
|
187
|
+
return null;
|
|
209
188
|
}
|
|
210
189
|
|
|
211
190
|
function syncPromotionToWorkflow(root, request) {
|
|
212
|
-
|
|
213
|
-
const task = findWorkflowTaskForOverlay(workflow, request.id);
|
|
214
|
-
if (!task) return null;
|
|
215
|
-
|
|
216
|
-
updateTask(root, task.key, {
|
|
217
|
-
overlays: unique([...(task.overlays || []), request.id]),
|
|
218
|
-
challengeRequired: true,
|
|
219
|
-
challengeStatus: request.challenge?.status || task.challengeStatus || 'pending',
|
|
220
|
-
note: `Updated promotion request ${request.id} challenge status to ${request.challenge?.status || 'pending'}.`,
|
|
221
|
-
});
|
|
222
|
-
return task.key;
|
|
191
|
+
return null;
|
|
223
192
|
}
|
|
224
193
|
|
|
225
194
|
function isExpired(record) {
|
|
@@ -384,7 +353,6 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
|
|
|
384
353
|
const overlayId = `${slugify(templateName)}-team-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
385
354
|
const overlayDir = getOverlayDir(root);
|
|
386
355
|
const expiresAt = permanence === 'temporary' ? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() : null;
|
|
387
|
-
const workflow = loadWorkflow(root);
|
|
388
356
|
|
|
389
357
|
const overlay = {
|
|
390
358
|
id: overlayId,
|
|
@@ -401,8 +369,8 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
|
|
|
401
369
|
createdAt: now.toISOString(),
|
|
402
370
|
expiresAt,
|
|
403
371
|
project: path.basename(root),
|
|
404
|
-
workflowId:
|
|
405
|
-
taskKey:
|
|
372
|
+
workflowId: null,
|
|
373
|
+
taskKey: null,
|
|
406
374
|
status: 'active',
|
|
407
375
|
prompt: buildOverlayPrompt({ domain: templateName, objective, scope: typeof flags.scope === 'string' ? flags.scope : '', permanence, focus: tpl.focus, freshness }),
|
|
408
376
|
skills: tpl.skills ?? [],
|
|
@@ -450,8 +418,22 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
|
|
|
450
418
|
try {
|
|
451
419
|
let permanence = flags.save ? 'permanent_request' : 'temporary';
|
|
452
420
|
if (!flags.temp && !flags.save && interactive) {
|
|
453
|
-
|
|
454
|
-
|
|
421
|
+
permanence = await selectOption({
|
|
422
|
+
title: 'Choose overlay mode',
|
|
423
|
+
instructions: 'Pick whether this expertise should be temporary or saved as a promotion request.',
|
|
424
|
+
options: [
|
|
425
|
+
{
|
|
426
|
+
value: 'temporary',
|
|
427
|
+
label: 'Temporary overlay',
|
|
428
|
+
description: 'Use this expertise for the current task and let it expire automatically.',
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
value: 'permanent_request',
|
|
432
|
+
label: 'Promotion request',
|
|
433
|
+
description: 'Save a request to promote this expertise into a more permanent capability.',
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
});
|
|
455
437
|
}
|
|
456
438
|
|
|
457
439
|
let objective = typeof flags.for === 'string' ? flags.for.trim() : rest.slice(1).join(' ').trim();
|
|
@@ -469,7 +451,6 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
|
|
|
469
451
|
const classification = classifyOverlay(objective, typeof flags.team === 'string' ? flags.team : '');
|
|
470
452
|
const now = new Date();
|
|
471
453
|
const root = findConstructRoot(cwd);
|
|
472
|
-
const workflow = loadWorkflow(root);
|
|
473
454
|
const overlayId = `${slugify(domain)}-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
474
455
|
const overlayDir = getOverlayDir(root);
|
|
475
456
|
const promotionDir = getPromotionDir(root);
|
|
@@ -488,8 +469,8 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
|
|
|
488
469
|
createdAt: now.toISOString(),
|
|
489
470
|
expiresAt,
|
|
490
471
|
project: path.basename(root),
|
|
491
|
-
workflowId:
|
|
492
|
-
taskKey:
|
|
472
|
+
workflowId: null,
|
|
473
|
+
taskKey: null,
|
|
493
474
|
status: permanence === 'temporary' ? 'active' : 'promotion_requested',
|
|
494
475
|
teamId: overlayId,
|
|
495
476
|
sourceStrategy: [
|