@geraldmaron/construct 1.0.12 → 1.0.15
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 +14 -2
- package/bin/construct +262 -14
- package/bin/construct-postinstall.mjs +23 -2
- package/commands/build/feature.md +0 -6
- package/commands/build/fix.md +0 -6
- package/commands/design/access.md +0 -6
- package/commands/design/flow.md +0 -6
- package/commands/design/ui.md +0 -6
- package/commands/measure/experiment.md +0 -6
- package/commands/measure/metrics.md +0 -6
- package/commands/measure/results.md +0 -6
- package/commands/plan/api.md +0 -6
- package/commands/plan/challenge.md +0 -6
- package/commands/plan/decide.md +0 -6
- package/commands/plan/feature.md +0 -7
- package/commands/plan/requirements.md +0 -6
- package/commands/remember/context.md +0 -6
- package/commands/remember/handoff.md +0 -6
- package/commands/remember/runbook.md +0 -6
- package/commands/review/code.md +0 -6
- package/commands/review/quality.md +0 -6
- package/commands/review/security.md +0 -6
- package/commands/ship/ready.md +0 -6
- package/commands/ship/release.md +0 -6
- package/commands/ship/status.md +0 -6
- package/commands/understand/docs.md +0 -6
- package/commands/understand/research.md +0 -6
- package/commands/understand/this.md +0 -6
- package/commands/understand/why.md +0 -6
- package/commands/work/clean.md +0 -6
- package/commands/work/drive.md +0 -6
- package/commands/work/optimize-prompts.md +0 -6
- package/commands/work/parallel-review.md +0 -6
- package/lib/auto-docs.mjs +11 -7
- package/lib/beads-client.mjs +19 -1
- package/lib/comment-lint.mjs +5 -1
- package/lib/context-state.mjs +15 -3
- package/lib/contracts/validate.mjs +54 -10
- package/lib/contracts/violation-log.mjs +154 -0
- package/lib/dashboard-static.mjs +7 -4
- package/lib/doc-stamp.mjs +16 -0
- package/lib/docs-verify.mjs +1 -8
- package/lib/doctor/watchers/bd-watch.mjs +6 -2
- package/lib/document-extract/docling-client.mjs +114 -0
- package/lib/document-extract/docling-sidecar.py +122 -0
- package/lib/document-extract/whisper-client.mjs +79 -0
- package/lib/document-extract.mjs +73 -8
- package/lib/document-ingest.mjs +26 -4
- package/lib/embed/daemon.mjs +26 -1
- package/lib/embed/docs-lifecycle.mjs +19 -0
- package/lib/embed/inbox.mjs +85 -2
- package/lib/embed/recommendation-store.mjs +29 -0
- package/lib/flavors/loader.mjs +1 -1
- package/lib/gates-audit.mjs +18 -12
- package/lib/handoffs/contract.mjs +2 -2
- package/lib/hooks/_lib/input.mjs +52 -0
- package/lib/hooks/adaptive-lint.mjs +4 -0
- package/lib/hooks/agent-tracker.mjs +59 -15
- package/lib/hooks/audit-reads.mjs +83 -42
- package/lib/hooks/audit-trail.mjs +28 -18
- package/lib/hooks/bash-output-logger.mjs +8 -2
- package/lib/hooks/block-no-verify.mjs +4 -0
- package/lib/hooks/ci-status-check.mjs +4 -0
- package/lib/hooks/comment-lint.mjs +6 -4
- package/lib/hooks/config-protection.mjs +4 -0
- package/lib/hooks/context-watch.mjs +4 -0
- package/lib/hooks/context-window-recovery.mjs +4 -0
- package/lib/hooks/dep-audit.mjs +12 -5
- package/lib/hooks/doc-coupling-check.mjs +5 -1
- package/lib/hooks/edit-accumulator.mjs +25 -10
- package/lib/hooks/edit-error-recovery.mjs +4 -0
- package/lib/hooks/edit-guard.mjs +4 -0
- package/lib/hooks/guard-bash.mjs +4 -0
- package/lib/hooks/mcp-audit.mjs +4 -0
- package/lib/hooks/mcp-health-check.mjs +4 -0
- package/lib/hooks/model-fallback.mjs +7 -11
- package/lib/hooks/policy-engine.mjs +4 -0
- package/lib/hooks/post-merge-docs-check.mjs +4 -0
- package/lib/hooks/post-merge-tracking.mjs +82 -0
- package/lib/hooks/pre-compact.mjs +4 -0
- package/lib/hooks/pre-push-gate.mjs +84 -231
- package/lib/hooks/proactive-activation.mjs +5 -2
- package/lib/hooks/readme-age-check.mjs +4 -0
- package/lib/hooks/registry-sync.mjs +35 -20
- package/lib/hooks/rule-verifier.mjs +3 -0
- package/lib/hooks/scan-secrets.mjs +4 -0
- package/lib/hooks/session-optimize.mjs +4 -0
- package/lib/hooks/session-reflect.mjs +4 -0
- package/lib/hooks/session-start.mjs +49 -2
- package/lib/hooks/session-tracking-refresh.mjs +70 -0
- package/lib/hooks/stop-notify.mjs +13 -2
- package/lib/hooks/stop-typecheck.mjs +4 -0
- package/lib/hooks/test-watch.mjs +8 -4
- package/lib/ingest/chunker.mjs +94 -0
- package/lib/ingest/pipeline.mjs +53 -0
- package/lib/ingest/store.mjs +82 -0
- package/lib/init-unified.mjs +90 -23
- package/lib/intake/attribution.mjs +77 -0
- package/lib/intake/intake-config.mjs +3 -0
- package/lib/intake/manifest.mjs +107 -0
- package/lib/intake/poll-lock.mjs +136 -0
- package/lib/intake/prepare.mjs +42 -4
- package/lib/knowledge/search.mjs +12 -0
- package/lib/logging/rotate.mjs +394 -0
- package/lib/mcp/server.mjs +142 -1
- package/lib/mcp/tools/project.mjs +2 -2
- package/lib/mcp/tools/skills.mjs +6 -3
- package/lib/mcp/tools/telemetry.mjs +1 -1
- package/lib/mcp/tools/workflow.mjs +6 -2
- package/lib/observation-store.mjs +14 -5
- package/lib/ollama-manager.mjs +32 -28
- package/lib/opencode-config.mjs +10 -3
- package/lib/orchestration/routing-tables.mjs +176 -0
- package/lib/orchestration-policy.mjs +18 -106
- package/lib/parity.mjs +48 -7
- package/lib/profiles/lifecycle.mjs +19 -1
- package/lib/project-init-shared.mjs +11 -5
- package/lib/project-root.mjs +104 -0
- package/lib/prompt-composer.js +11 -1
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/event-bus.mjs +29 -9
- package/lib/roles/router.mjs +8 -7
- package/lib/runtime/uv-bootstrap.mjs +129 -0
- package/lib/runtime/whisper-bootstrap.mjs +102 -0
- package/lib/server/index.mjs +53 -10
- package/lib/server/static/index.html +1 -15
- package/lib/specialists/postconditions.mjs +1 -1
- package/lib/status.mjs +1 -1
- package/lib/storage/backup.mjs +2 -2
- package/lib/sync/skill-frontmatter.mjs +113 -21
- package/lib/telemetry/intent-verifications.mjs +16 -3
- package/lib/telemetry/skill-calls.mjs +12 -3
- package/lib/tracking-surfaces.mjs +377 -0
- package/lib/validators/skills.mjs +86 -54
- package/lib/worker/trace.mjs +19 -2
- package/package.json +10 -6
- package/personas/construct.md +4 -7
- package/platforms/claude/settings.template.json +29 -28
- package/rules/common/beads-hygiene.md +3 -9
- package/rules/common/code-review.md +3 -6
- package/rules/common/coding-style.md +3 -6
- package/rules/common/comments.md +3 -7
- package/rules/common/commit-approval.md +3 -6
- package/rules/common/cx-agent-routing.md +3 -7
- package/rules/common/cx-skill-routing.md +3 -3
- package/rules/common/doc-ownership.md +3 -8
- package/rules/common/efficiency.md +3 -8
- package/rules/common/framing.md +3 -8
- package/rules/common/git-workflow.md +3 -5
- package/rules/common/no-fabrication.md +4 -12
- package/rules/common/patterns.md +3 -5
- package/rules/common/release-gates.md +3 -8
- package/rules/common/research.md +3 -8
- package/rules/common/review-before-change.md +3 -9
- package/rules/common/security.md +3 -6
- package/rules/common/skill-composition.md +3 -7
- package/rules/common/testing.md +3 -6
- package/rules/golang/coding-style.md +1 -5
- package/rules/golang/hooks.md +1 -5
- package/rules/golang/patterns.md +1 -5
- package/rules/golang/security.md +1 -5
- package/rules/golang/testing.md +1 -5
- package/rules/python/coding-style.md +1 -5
- package/rules/python/hooks.md +1 -5
- package/rules/python/patterns.md +1 -5
- package/rules/python/security.md +1 -5
- package/rules/python/testing.md +1 -5
- package/rules/swift/coding-style.md +1 -5
- package/rules/swift/hooks.md +1 -5
- package/rules/swift/patterns.md +1 -5
- package/rules/swift/security.md +1 -5
- package/rules/swift/testing.md +1 -5
- package/rules/typescript/coding-style.md +1 -5
- package/rules/typescript/hooks.md +1 -5
- package/rules/typescript/patterns.md +1 -5
- package/rules/typescript/security.md +1 -5
- package/rules/typescript/testing.md +1 -5
- package/rules/web/coding-style.md +3 -5
- package/rules/web/design-quality.md +3 -5
- package/rules/web/hooks.md +3 -5
- package/rules/web/patterns.md +3 -5
- package/rules/web/performance.md +3 -5
- package/rules/web/security.md +3 -5
- package/rules/web/testing.md +3 -5
- package/scripts/sync-specialists.mjs +269 -75
- package/skills/ai/agent-dev.md +4 -5
- package/skills/ai/llm-security.md +4 -5
- package/skills/ai/ml-ops.md +4 -5
- package/skills/ai/orchestration-workflow.md +4 -5
- package/skills/ai/prompt-and-eval.md +4 -5
- package/skills/ai/prompt-optimizer.md +4 -6
- package/skills/ai/rag-system.md +4 -5
- package/skills/architecture/api-design.md +4 -5
- package/skills/architecture/caching.md +4 -5
- package/skills/architecture/cloud-native.md +4 -5
- package/skills/architecture/message-queue.md +4 -5
- package/skills/architecture/security-arch.md +4 -5
- package/skills/compliance/ai-disclosure.md +4 -5
- package/skills/compliance/data-privacy.md +4 -5
- package/skills/compliance/license-audit.md +4 -5
- package/skills/compliance/regulatory-review.md +4 -5
- package/skills/development/cpp.md +4 -5
- package/skills/development/go.md +4 -5
- package/skills/development/java.md +4 -5
- package/skills/development/kotlin.md +4 -5
- package/skills/development/mobile-crossplatform.md +4 -5
- package/skills/development/python.md +4 -5
- package/skills/development/rust.md +4 -5
- package/skills/development/shell.md +4 -5
- package/skills/development/swift.md +4 -5
- package/skills/development/typescript.md +4 -5
- package/skills/devops/ci-cd.md +4 -5
- package/skills/devops/containerization.md +4 -5
- package/skills/devops/cost-optimization.md +4 -5
- package/skills/devops/data-engineering.md +4 -5
- package/skills/devops/database.md +4 -5
- package/skills/devops/dependency-management.md +4 -5
- package/skills/devops/devsecops.md +4 -5
- package/skills/devops/git-workflow.md +4 -5
- package/skills/devops/incident-response.md +4 -5
- package/skills/devops/monorepo.md +4 -5
- package/skills/devops/observability.md +4 -5
- package/skills/devops/performance.md +4 -5
- package/skills/devops/testing.md +4 -5
- package/skills/docs/adr-workflow.md +4 -7
- package/skills/docs/backlog-proposal-workflow.md +4 -3
- package/skills/docs/customer-profile-workflow.md +4 -3
- package/skills/docs/document-ingest-workflow.md +4 -3
- package/skills/docs/evidence-ingest-workflow.md +4 -3
- package/skills/docs/init-docs.md +4 -5
- package/skills/docs/init-project.md +4 -5
- package/skills/docs/prd-workflow.md +4 -7
- package/skills/docs/prfaq-workflow.md +4 -3
- package/skills/docs/product-intelligence-review.md +4 -3
- package/skills/docs/product-intelligence-workflow.md +4 -6
- package/skills/docs/product-signal-workflow.md +4 -5
- package/skills/docs/research-workflow.md +4 -5
- package/skills/docs/runbook-workflow.md +4 -5
- package/skills/docs/strategy-workflow.md +4 -5
- package/skills/exploration/dependency-graph-reading.md +4 -7
- package/skills/exploration/repo-map.md +4 -5
- package/skills/exploration/tracer-bullet-method.md +4 -7
- package/skills/exploration/unknown-codebase-onboarding.md +4 -7
- package/skills/frameworks/django.md +4 -5
- package/skills/frameworks/nextjs.md +4 -5
- package/skills/frameworks/react.md +4 -5
- package/skills/frameworks/spring-boot.md +4 -5
- package/skills/frontend-design/accessibility.md +4 -5
- package/skills/frontend-design/component-patterns.md +4 -5
- package/skills/frontend-design/engineering.md +4 -5
- package/skills/frontend-design/state-management.md +4 -5
- package/skills/frontend-design/ui-aesthetics.md +4 -5
- package/skills/frontend-design/ux-principles.md +4 -5
- package/skills/operating/change-management.md +4 -8
- package/skills/operating/incident-response.md +4 -8
- package/skills/operating/oncall-rotation.md +4 -7
- package/skills/operating/orchestration-reference.md +4 -10
- package/skills/quality-gates/review-work.md +4 -5
- package/skills/quality-gates/verify-change.md +4 -5
- package/skills/quality-gates/verify-module.md +4 -5
- package/skills/quality-gates/verify-quality.md +4 -5
- package/skills/quality-gates/verify-security.md +4 -5
- package/skills/roles/architect.ai-systems.md +6 -9
- package/skills/roles/architect.data.md +6 -9
- package/skills/roles/architect.enterprise.md +6 -9
- package/skills/roles/architect.integration.md +6 -9
- package/skills/roles/architect.md +7 -14
- package/skills/roles/architect.platform.md +6 -9
- package/skills/roles/data-analyst.experiment.md +6 -9
- package/skills/roles/data-analyst.md +6 -9
- package/skills/roles/data-analyst.product-intelligence.md +7 -9
- package/skills/roles/data-analyst.product.md +6 -9
- package/skills/roles/data-analyst.telemetry.md +7 -9
- package/skills/roles/data-engineer.pipeline.md +6 -9
- package/skills/roles/data-engineer.vector-retrieval.md +7 -9
- package/skills/roles/data-engineer.warehouse.md +6 -9
- package/skills/roles/debugger.md +6 -9
- package/skills/roles/designer.accessibility.md +6 -9
- package/skills/roles/designer.md +7 -9
- package/skills/roles/engineer.ai.md +6 -9
- package/skills/roles/engineer.data.md +6 -9
- package/skills/roles/engineer.md +9 -9
- package/skills/roles/engineer.platform.md +6 -9
- package/skills/roles/operator.docs.md +6 -9
- package/skills/roles/operator.md +9 -9
- package/skills/roles/operator.release.md +6 -9
- package/skills/roles/operator.sre.md +6 -9
- package/skills/roles/orchestrator.md +6 -9
- package/skills/roles/product-manager.ai-product.md +6 -9
- package/skills/roles/product-manager.business-strategy.md +6 -9
- package/skills/roles/product-manager.enterprise.md +6 -9
- package/skills/roles/product-manager.growth.md +7 -9
- package/skills/roles/product-manager.md +7 -9
- package/skills/roles/product-manager.platform.md +6 -9
- package/skills/roles/product-manager.product.md +6 -9
- package/skills/roles/qa.ai-eval.md +8 -9
- package/skills/roles/qa.api-contract.md +7 -9
- package/skills/roles/qa.data-pipeline.md +7 -9
- package/skills/roles/qa.md +7 -9
- package/skills/roles/qa.test-automation.md +6 -9
- package/skills/roles/qa.web-ui.md +7 -9
- package/skills/roles/researcher.explorer.md +6 -9
- package/skills/roles/researcher.md +8 -9
- package/skills/roles/researcher.ux.md +6 -9
- package/skills/roles/reviewer.devil-advocate.md +6 -9
- package/skills/roles/reviewer.evaluator.md +6 -9
- package/skills/roles/reviewer.md +9 -9
- package/skills/roles/reviewer.trace.md +6 -9
- package/skills/roles/security.ai.md +7 -9
- package/skills/roles/security.appsec.md +6 -9
- package/skills/roles/security.cloud.md +6 -9
- package/skills/roles/security.legal-compliance.md +6 -9
- package/skills/roles/security.md +7 -9
- package/skills/roles/security.privacy.md +7 -9
- package/skills/roles/security.supply-chain.md +7 -9
- package/skills/security/blue-team.md +4 -5
- package/skills/security/code-audit.md +4 -5
- package/skills/security/pentest.md +4 -5
- package/skills/security/red-team.md +4 -5
- package/skills/security/threat-intel.md +4 -5
- package/skills/security/vuln-research.md +4 -5
- package/skills/strategy/competitive-landscape.md +4 -8
- package/skills/strategy/market-research-methods.md +4 -8
- package/skills/strategy/narrative-arc.md +4 -8
- package/skills/strategy/pricing-positioning.md +4 -8
- package/skills/utility/clean-code.md +4 -5
- package/specialists/policy-inventory.json +14 -0
- package/specialists/registry.json +158 -13
- package/lib/hooks/env-check.mjs +0 -83
- package/lib/hooks/read-tracker.mjs +0 -61
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/server/static/assets/index-ab25c707.js +0 -70
- package/lib/server/static/assets/index-f0c80a2b.css +0 -1
- package/lib/specialist-contracts-enforce.mjs +0 -158
- package/rules/common/agents.md +0 -28
- package/rules/common/development-workflow.md +0 -32
- package/rules/common/performance.md +0 -55
|
@@ -112,10 +112,12 @@ function logCapDrop(rootDir, kind, dropped, total) {
|
|
|
112
112
|
function writeIndex(rootDir, entries) {
|
|
113
113
|
ensureDir(obsDir(rootDir), rootDir);
|
|
114
114
|
const trimmed = entries.slice(0, MAX_INDEX);
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const dropped = Math.max(0, entries.length - MAX_INDEX);
|
|
116
|
+
if (dropped > 0) {
|
|
117
|
+
logCapDrop(rootDir, 'observation-index', dropped, entries.length);
|
|
117
118
|
}
|
|
118
119
|
fs.writeFileSync(indexPath(rootDir), JSON.stringify(trimmed, null, 2) + '\n');
|
|
120
|
+
return { dropped };
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
function readVectors(rootDir) {
|
|
@@ -130,10 +132,12 @@ function readVectors(rootDir) {
|
|
|
130
132
|
|
|
131
133
|
function writeVectors(rootDir, records) {
|
|
132
134
|
ensureDir(obsDir(rootDir), rootDir);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
const dropped = Math.max(0, records.length - MAX_INDEX);
|
|
136
|
+
if (dropped > 0) {
|
|
137
|
+
logCapDrop(rootDir, 'observation-vectors', dropped, records.length);
|
|
135
138
|
}
|
|
136
139
|
fs.writeFileSync(vectorsPath(rootDir), JSON.stringify(records, null, 2) + '\n');
|
|
140
|
+
return { dropped };
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
/**
|
|
@@ -188,6 +192,7 @@ export async function addObservation(rootDir, {
|
|
|
188
192
|
JSON.stringify(record, null, 2) + '\n',
|
|
189
193
|
);
|
|
190
194
|
|
|
195
|
+
let indexDropped = 0;
|
|
191
196
|
withFileLockSync(indexPath(rootDir), () => {
|
|
192
197
|
const index = readIndex(rootDir);
|
|
193
198
|
index.unshift({
|
|
@@ -198,8 +203,12 @@ export async function addObservation(rootDir, {
|
|
|
198
203
|
project: record.project,
|
|
199
204
|
createdAt: now,
|
|
200
205
|
});
|
|
201
|
-
writeIndex(rootDir, index);
|
|
206
|
+
indexDropped = writeIndex(rootDir, index).dropped;
|
|
202
207
|
});
|
|
208
|
+
if (indexDropped > 0) {
|
|
209
|
+
record.capDropped = indexDropped;
|
|
210
|
+
process.stderr.write(`[observation-store] observation cap reached: ${indexDropped} oldest entries evicted (cap=${MAX_INDEX})\n`);
|
|
211
|
+
}
|
|
203
212
|
|
|
204
213
|
// Try SQL storage with neural embeddings
|
|
205
214
|
try {
|
package/lib/ollama-manager.mjs
CHANGED
|
@@ -6,10 +6,26 @@
|
|
|
6
6
|
* pulling new models, and configuring Ollama as a provider.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
|
|
13
|
+
// argv-array form for every shell-out: no shell interpolation, model names and
|
|
14
|
+
// JSON bodies are passed as discrete arguments / stdin so user-supplied values
|
|
15
|
+
// cannot break out of the command.
|
|
16
|
+
|
|
17
|
+
function curlGet(url, { timeout } = {}) {
|
|
18
|
+
return execFileSync('curl', ['-s', url], { encoding: 'utf8', timeout });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function curlPost(url, body, { timeout } = {}) {
|
|
22
|
+
return execFileSync('curl', ['-s', url, '--data-binary', '@-'], {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
timeout,
|
|
25
|
+
input: typeof body === 'string' ? body : JSON.stringify(body),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
14
30
|
|
|
15
31
|
/**
|
|
@@ -17,14 +33,11 @@ const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST |
|
|
|
17
33
|
*/
|
|
18
34
|
export function checkOllamaStatus() {
|
|
19
35
|
try {
|
|
20
|
-
|
|
36
|
+
execFileSync('which', ['ollama'], { stdio: 'pipe' });
|
|
21
37
|
const installed = true;
|
|
22
|
-
|
|
38
|
+
|
|
23
39
|
try {
|
|
24
|
-
const response =
|
|
25
|
-
encoding: 'utf8',
|
|
26
|
-
timeout: 3000
|
|
27
|
-
});
|
|
40
|
+
const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 3000 });
|
|
28
41
|
const data = JSON.parse(response);
|
|
29
42
|
return {
|
|
30
43
|
installed,
|
|
@@ -54,10 +67,7 @@ export function checkOllamaStatus() {
|
|
|
54
67
|
*/
|
|
55
68
|
export function listModels() {
|
|
56
69
|
try {
|
|
57
|
-
const response =
|
|
58
|
-
encoding: 'utf8',
|
|
59
|
-
timeout: 5000
|
|
60
|
-
});
|
|
70
|
+
const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 5000 });
|
|
61
71
|
const data = JSON.parse(response);
|
|
62
72
|
return data.models || [];
|
|
63
73
|
} catch (e) {
|
|
@@ -71,7 +81,7 @@ export function listModels() {
|
|
|
71
81
|
export function pullModel(modelName, { verbose = false } = {}) {
|
|
72
82
|
console.log(`Pulling ${modelName}...`);
|
|
73
83
|
try {
|
|
74
|
-
const output =
|
|
84
|
+
const output = execFileSync('ollama', ['pull', modelName], {
|
|
75
85
|
encoding: 'utf8',
|
|
76
86
|
stdio: verbose ? 'inherit' : 'pipe'
|
|
77
87
|
});
|
|
@@ -86,7 +96,7 @@ export function pullModel(modelName, { verbose = false } = {}) {
|
|
|
86
96
|
*/
|
|
87
97
|
export function removeModel(modelName) {
|
|
88
98
|
try {
|
|
89
|
-
|
|
99
|
+
execFileSync('ollama', ['rm', modelName], { encoding: 'utf8' });
|
|
90
100
|
return { success: true };
|
|
91
101
|
} catch (e) {
|
|
92
102
|
return { success: false, error: e.message };
|
|
@@ -98,10 +108,7 @@ export function removeModel(modelName) {
|
|
|
98
108
|
*/
|
|
99
109
|
export function showModel(modelName) {
|
|
100
110
|
try {
|
|
101
|
-
const response =
|
|
102
|
-
encoding: 'utf8',
|
|
103
|
-
timeout: 5000
|
|
104
|
-
});
|
|
111
|
+
const response = curlPost(`${OLLAMA_BASE_URL}/api/show`, { name: modelName }, { timeout: 5000 });
|
|
105
112
|
return JSON.parse(response);
|
|
106
113
|
} catch (e) {
|
|
107
114
|
throw new Error(`Failed to fetch model details: ${e.message}`);
|
|
@@ -113,14 +120,11 @@ export function showModel(modelName) {
|
|
|
113
120
|
*/
|
|
114
121
|
export function testModel(modelName, prompt = 'Say hello in one sentence.') {
|
|
115
122
|
try {
|
|
116
|
-
const response =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
encoding: 'utf8',
|
|
122
|
-
timeout: 30000
|
|
123
|
-
});
|
|
123
|
+
const response = curlPost(
|
|
124
|
+
`${OLLAMA_BASE_URL}/api/generate`,
|
|
125
|
+
{ model: modelName, prompt, stream: false },
|
|
126
|
+
{ timeout: 30000 },
|
|
127
|
+
);
|
|
124
128
|
const data = JSON.parse(response);
|
|
125
129
|
return {
|
|
126
130
|
success: true,
|
|
@@ -371,7 +375,7 @@ async function cmdOllamaSetup(args) {
|
|
|
371
375
|
if (autoYes || process.platform === 'darwin') {
|
|
372
376
|
console.log('Installing via Homebrew...');
|
|
373
377
|
try {
|
|
374
|
-
|
|
378
|
+
execFileSync('brew', ['install', 'ollama'], { stdio: 'inherit' });
|
|
375
379
|
console.log('✅ Ollama installed');
|
|
376
380
|
} catch (e) {
|
|
377
381
|
console.error('❌ Installation failed. Please install manually: https://ollama.com');
|
|
@@ -390,11 +394,11 @@ async function cmdOllamaSetup(args) {
|
|
|
390
394
|
console.log('⚠️ Ollama is not running');
|
|
391
395
|
console.log('Starting Ollama service...');
|
|
392
396
|
try {
|
|
393
|
-
|
|
397
|
+
execFileSync('brew', ['services', 'start', 'ollama'], { stdio: 'pipe' });
|
|
394
398
|
console.log('✅ Ollama service started');
|
|
395
399
|
// Wait for it to be ready
|
|
396
400
|
console.log('Waiting for Ollama to be ready...');
|
|
397
|
-
|
|
401
|
+
execFileSync('sleep', ['3']);
|
|
398
402
|
} catch (e) {
|
|
399
403
|
console.log('You can start it manually with: ollama serve');
|
|
400
404
|
}
|
package/lib/opencode-config.mjs
CHANGED
|
@@ -47,8 +47,15 @@ export function sanitizeOpenCodeConfig(config) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
|
|
50
|
+
// Treat the canonical home config as the default target; any explicit path
|
|
51
|
+
// outside the canonical home dir (e.g. project-scoped .opencode/config.json)
|
|
52
|
+
// is honored as-is so per-project sync writes to the right tree.
|
|
53
|
+
|
|
50
54
|
const canonical = getCanonicalOpenCodeConfigPath();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const target = file && path.resolve(file) !== path.resolve(findOpenCodeConfigPath())
|
|
56
|
+
? file
|
|
57
|
+
: canonical;
|
|
58
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
|
+
fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
|
|
60
|
+
return target;
|
|
54
61
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/routing-tables.mjs — declarative routing resolver.
|
|
3
|
+
*
|
|
4
|
+
* Reads specialist subscriptions, doc-artifact ownership, and watch-condition
|
|
5
|
+
* references from specialists/registry.json (with optional .cx/specialists/
|
|
6
|
+
* overlays) and exposes the forward lookups the orchestration layer needs.
|
|
7
|
+
*
|
|
8
|
+
* The resolver is the single source of truth for event/doc/watch routing.
|
|
9
|
+
* Projects override routing without patching library code by dropping JSON
|
|
10
|
+
* files into .cx/specialists/ — each overlay's fields apply over the
|
|
11
|
+
* canonical registry entry for the same specialist.
|
|
12
|
+
*
|
|
13
|
+
* Watch conditions are referenced by name. The implementations live in
|
|
14
|
+
* WATCHERS below — small predicates over the requestSignals shape. The
|
|
15
|
+
* registry decides WHICH specialist a named watcher routes to; this file
|
|
16
|
+
* decides WHAT condition triggers it. Project overlays can re-bind a
|
|
17
|
+
* watcher's specialist; adding a brand-new watcher requires code.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { findProjectRoot } from '../project-root.mjs';
|
|
24
|
+
|
|
25
|
+
const REGISTRY_PATH = fileURLToPath(new URL('../../specialists/registry.json', import.meta.url));
|
|
26
|
+
|
|
27
|
+
// Watch-condition predicates. Each is a pure function from requestSignals
|
|
28
|
+
// (see orchestration-policy.requestSignals) to boolean. Registry entries
|
|
29
|
+
// reference these by key. Keep predicates small and side-effect free.
|
|
30
|
+
|
|
31
|
+
const WATCHERS = {
|
|
32
|
+
'high-ambiguity-deep-work': (s) =>
|
|
33
|
+
s.ambiguityScore > 0.5 && s.workCategory === 'deep',
|
|
34
|
+
'visual-or-ui-risk': (s) =>
|
|
35
|
+
Boolean(s.visualDeliverable) || Boolean(s.riskFlags?.ui),
|
|
36
|
+
'auth-payments-non-narrow': (s) =>
|
|
37
|
+
Boolean(s.authOrPayments) && s.blastRadius !== 'narrow',
|
|
38
|
+
'architecture-without-metric': (s) =>
|
|
39
|
+
Boolean(s.riskFlags?.architecture) && !s.hasSuccessMetric,
|
|
40
|
+
'wide-blast-radius': (s) => s.blastRadius === 'wide',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let cache = null;
|
|
44
|
+
|
|
45
|
+
function readJsonSafe(path) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadOverlays() {
|
|
54
|
+
const root = findProjectRoot();
|
|
55
|
+
if (!root) return [];
|
|
56
|
+
const overlayDir = join(root, '.cx', 'specialists');
|
|
57
|
+
if (!existsSync(overlayDir)) return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const name of readdirSync(overlayDir)) {
|
|
60
|
+
if (!name.endsWith('.json')) continue;
|
|
61
|
+
const data = readJsonSafe(join(overlayDir, name));
|
|
62
|
+
if (data && typeof data === 'object') out.push(data);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildTables() {
|
|
68
|
+
const registry = readJsonSafe(REGISTRY_PATH);
|
|
69
|
+
if (!registry || !Array.isArray(registry.specialists)) {
|
|
70
|
+
throw new Error('routing-tables: registry.json missing or malformed');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Forward maps. Last writer wins for overlays; the resolver validates
|
|
74
|
+
// duplicate ownership inside the canonical registry, so a project overlay
|
|
75
|
+
// is the only way to re-bind a single event/doc/watcher.
|
|
76
|
+
|
|
77
|
+
const eventToOwner = new Map();
|
|
78
|
+
const docToOwner = new Map();
|
|
79
|
+
const watcherToOwner = new Map();
|
|
80
|
+
const watcherToReason = new Map();
|
|
81
|
+
|
|
82
|
+
const errors = [];
|
|
83
|
+
|
|
84
|
+
function apply(entry, source) {
|
|
85
|
+
const cxId = entry.name?.startsWith('cx-') ? entry.name : `cx-${entry.name}`;
|
|
86
|
+
if (Array.isArray(entry.subscriptions)) {
|
|
87
|
+
for (const event of entry.subscriptions) {
|
|
88
|
+
if (eventToOwner.has(event) && eventToOwner.get(event) !== cxId && source === 'registry') {
|
|
89
|
+
errors.push(`duplicate event ownership: ${event} → ${eventToOwner.get(event)} vs ${cxId}`);
|
|
90
|
+
}
|
|
91
|
+
eventToOwner.set(event, cxId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(entry.docArtifacts)) {
|
|
95
|
+
for (const docType of entry.docArtifacts) {
|
|
96
|
+
if (docToOwner.has(docType) && docToOwner.get(docType) !== cxId && source === 'registry') {
|
|
97
|
+
errors.push(`duplicate doc ownership: ${docType} → ${docToOwner.get(docType)} vs ${cxId}`);
|
|
98
|
+
}
|
|
99
|
+
docToOwner.set(docType, cxId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(entry.watchConditions)) {
|
|
103
|
+
for (const watch of entry.watchConditions) {
|
|
104
|
+
const name = typeof watch === 'string' ? watch : watch.watcher;
|
|
105
|
+
const reason = typeof watch === 'string' ? null : watch.reason;
|
|
106
|
+
if (!WATCHERS[name]) {
|
|
107
|
+
errors.push(`unknown watchCondition: ${name} (referenced by ${cxId})`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
watcherToOwner.set(name, cxId);
|
|
111
|
+
if (reason) watcherToReason.set(name, reason);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const entry of registry.specialists) apply(entry, 'registry');
|
|
117
|
+
for (const overlay of loadOverlays()) apply(overlay, 'overlay');
|
|
118
|
+
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
throw new Error(`routing-tables: ${errors.join('; ')}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
eventToOwner,
|
|
125
|
+
docToOwner,
|
|
126
|
+
watcherToOwner,
|
|
127
|
+
watcherToReason,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function tables() {
|
|
132
|
+
if (!cache) cache = buildTables();
|
|
133
|
+
return cache;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ownerForEvent(eventType) {
|
|
137
|
+
if (!eventType) return null;
|
|
138
|
+
return tables().eventToOwner.get(eventType) ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function ownerForDoc(docType) {
|
|
142
|
+
if (!docType) return null;
|
|
143
|
+
return tables().docToOwner.get(docType) ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function evaluateWatchConditions(signals) {
|
|
147
|
+
const triggers = [];
|
|
148
|
+
const t = tables();
|
|
149
|
+
for (const [name, predicate] of Object.entries(WATCHERS)) {
|
|
150
|
+
const owner = t.watcherToOwner.get(name);
|
|
151
|
+
if (!owner) continue;
|
|
152
|
+
if (!predicate(signals)) continue;
|
|
153
|
+
triggers.push({
|
|
154
|
+
specialist: owner,
|
|
155
|
+
reason: t.watcherToReason.get(name) ?? name,
|
|
156
|
+
watcher: name,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return triggers;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function knownEventTypes() {
|
|
163
|
+
return Array.from(tables().eventToOwner.keys());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function knownDocTypes() {
|
|
167
|
+
return Array.from(tables().docToOwner.keys());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function knownWatchers() {
|
|
171
|
+
return Object.keys(WATCHERS);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function _resetCache() {
|
|
175
|
+
cache = null;
|
|
176
|
+
}
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
* 3. contract chain (resolveContractChain) (what the typed handoffs are)
|
|
8
8
|
*
|
|
9
9
|
* Agent-to-agent contracts are defined in specialists/contracts.json and loaded via
|
|
10
|
-
* lib/specialist-contracts.mjs. That file is the
|
|
11
|
-
* producer→consumer
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* lib/specialist-contracts.mjs. That file is the source of truth for
|
|
11
|
+
* producer→consumer typed handoffs.
|
|
12
|
+
*
|
|
13
|
+
* Event ownership, doc-artifact ownership, and watch-condition routing live
|
|
14
|
+
* declaratively on specialist entries in specialists/registry.json and are
|
|
15
|
+
* resolved by lib/orchestration/routing-tables.mjs. Hardcoded maps here
|
|
16
|
+
* would create a second source of truth.
|
|
15
17
|
*/
|
|
16
18
|
import { resolveContractChain } from './specialist-contracts.mjs';
|
|
17
19
|
import { verifyRoute } from './intent-classifier.mjs';
|
|
20
|
+
import { ownerForEvent, ownerForDoc, evaluateWatchConditions, knownDocTypes } from './orchestration/routing-tables.mjs';
|
|
18
21
|
|
|
19
22
|
export const EXECUTION_TRACKS = {
|
|
20
23
|
immediate: 'immediate',
|
|
@@ -40,86 +43,11 @@ export const WORK_CATEGORIES = {
|
|
|
40
43
|
|
|
41
44
|
export const TERMINAL_STATES = ['DONE', 'BLOCKED', 'NEEDS_MAIN_INPUT'];
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* authoring these directly — authoring is how the owner's domain checks
|
|
47
|
-
* (framing, research, trade-off analysis) actually fire.
|
|
48
|
-
*
|
|
49
|
-
* Keep in sync with rules/common/doc-ownership.md.
|
|
50
|
-
*/
|
|
51
|
-
export const DOC_OWNERSHIP = {
|
|
52
|
-
prd: 'cx-product-manager',
|
|
53
|
-
'meta-prd': 'cx-product-manager',
|
|
54
|
-
'prd-platform': 'cx-product-manager',
|
|
55
|
-
'prd-business': 'cx-product-manager',
|
|
56
|
-
prfaq: 'cx-product-manager',
|
|
57
|
-
'one-pager': 'cx-product-manager',
|
|
58
|
-
'backlog-proposal': 'cx-product-manager',
|
|
59
|
-
'customer-profile': 'cx-product-manager',
|
|
60
|
-
adr: 'cx-architect',
|
|
61
|
-
rfc: 'cx-architect',
|
|
62
|
-
'rfc-platform': 'cx-architect',
|
|
63
|
-
'architecture-overview': 'cx-architect',
|
|
64
|
-
'system-design': 'cx-architect',
|
|
65
|
-
'research-brief': 'cx-researcher',
|
|
66
|
-
'evidence-brief': 'cx-researcher',
|
|
67
|
-
'signal-brief': 'cx-researcher',
|
|
68
|
-
'product-intelligence-report': 'cx-researcher',
|
|
69
|
-
runbook: 'cx-sre',
|
|
70
|
-
'incident-report': 'cx-sre',
|
|
71
|
-
postmortem: 'cx-sre',
|
|
72
|
-
'test-plan': 'cx-qa',
|
|
73
|
-
'qa-strategy': 'cx-qa',
|
|
74
|
-
'security-review': 'cx-security',
|
|
75
|
-
'threat-model': 'cx-security',
|
|
76
|
-
memo: 'cx-docs-keeper',
|
|
77
|
-
changelog: 'cx-docs-keeper',
|
|
78
|
-
};
|
|
46
|
+
// Event and doc-artifact ownership are resolved from specialists/registry.json
|
|
47
|
+
// via routing-tables.mjs. Re-exported here so callers that historically
|
|
48
|
+
// imported from this module keep working.
|
|
79
49
|
|
|
80
|
-
|
|
81
|
-
// an event of one of these types, the gateway routes the invocation to the
|
|
82
|
-
// listed persona. Empty owners mean the event is recorded but no persona
|
|
83
|
-
// fires until onboarded. Keep in sync with specialists/role-manifests.json.
|
|
84
|
-
|
|
85
|
-
export const EVENT_OWNERSHIP = {
|
|
86
|
-
'push_gate.fail': 'cx-sre',
|
|
87
|
-
'service.down': 'cx-sre',
|
|
88
|
-
'mcp.unhealthy.persistent': 'cx-sre',
|
|
89
|
-
'edit_loop.stuck': 'cx-sre',
|
|
90
|
-
'test.fail': 'cx-qa',
|
|
91
|
-
'test.flake': 'cx-qa',
|
|
92
|
-
'coverage.drop': 'cx-qa',
|
|
93
|
-
'dep.cve': 'cx-security',
|
|
94
|
-
'secrets.detected': 'cx-security',
|
|
95
|
-
'config.protection.violation': 'cx-security',
|
|
96
|
-
'pr.merged.no-docs': 'cx-docs-keeper',
|
|
97
|
-
'changelog.missing': 'cx-docs-keeper',
|
|
98
|
-
'readme.stale': 'cx-docs-keeper',
|
|
99
|
-
'adr.requested': 'cx-architect',
|
|
100
|
-
'arch.boundary.violated': 'cx-architect',
|
|
101
|
-
'regression.detected': 'cx-debugger',
|
|
102
|
-
'hang.detected': 'cx-debugger',
|
|
103
|
-
'release.candidate': 'cx-release-manager',
|
|
104
|
-
'version.bump.needed': 'cx-release-manager',
|
|
105
|
-
'backlog.stale': 'cx-product-manager',
|
|
106
|
-
'prd.requested': 'cx-product-manager',
|
|
107
|
-
'pr.opened': 'cx-reviewer',
|
|
108
|
-
'pr.ready-for-review': 'cx-reviewer',
|
|
109
|
-
'infra.change.requested': 'cx-platform-engineer',
|
|
110
|
-
'service.scale.event': 'cx-platform-engineer',
|
|
111
|
-
'design.requested': 'cx-designer',
|
|
112
|
-
'a11y.violation': 'cx-accessibility',
|
|
113
|
-
'research.requested': 'cx-researcher',
|
|
114
|
-
'evidence.requested': 'cx-researcher',
|
|
115
|
-
'eval.regression': 'cx-evaluator',
|
|
116
|
-
'trace.anomaly': 'cx-trace-reviewer',
|
|
117
|
-
'dep.license': 'cx-legal-compliance',
|
|
118
|
-
'privacy-policy.review': 'cx-legal-compliance',
|
|
119
|
-
'strategy.required': 'cx-business-strategist',
|
|
120
|
-
'plan.requested': 'cx-operations',
|
|
121
|
-
'research.gate.required': 'cx-rd-lead',
|
|
122
|
-
};
|
|
50
|
+
export { ownerForEvent, ownerForDoc } from './orchestration/routing-tables.mjs';
|
|
123
51
|
|
|
124
52
|
const DOC_AUTHORING_PATTERNS = [
|
|
125
53
|
{ pattern: /\b(adr|architecture decision record)s?\b/i, docType: 'adr' },
|
|
@@ -160,7 +88,7 @@ export function detectDocAuthoringIntent(request = '') {
|
|
|
160
88
|
if (!AUTHORING_VERBS.test(text)) return null;
|
|
161
89
|
for (const { pattern, docType } of DOC_AUTHORING_PATTERNS) {
|
|
162
90
|
if (pattern.test(text)) {
|
|
163
|
-
return { docType, owner:
|
|
91
|
+
return { docType, owner: ownerForDoc(docType) };
|
|
164
92
|
}
|
|
165
93
|
}
|
|
166
94
|
return null;
|
|
@@ -617,29 +545,13 @@ export function requestSignals(request = '', context = {}) {
|
|
|
617
545
|
|
|
618
546
|
// Signal-driven proactive triggers. Returns a list of { specialist, reason }
|
|
619
547
|
// pairs for specialists that should engage PRE-DISPATCH based on signals —
|
|
620
|
-
// separate from the keyword-only paths in selectSpecialists.
|
|
621
|
-
//
|
|
548
|
+
// separate from the keyword-only paths in selectSpecialists. The watch
|
|
549
|
+
// predicates and their specialist owners are declared in
|
|
550
|
+
// specialists/registry.json (watchConditions) and evaluated by
|
|
551
|
+
// orchestration/routing-tables.mjs.
|
|
622
552
|
|
|
623
553
|
export function proactiveTriggers(signals) {
|
|
624
|
-
|
|
625
|
-
const { ambiguityScore, workCategory, authOrPayments, riskFlags, blastRadius, hasSuccessMetric, visualDeliverable } = signals;
|
|
626
|
-
|
|
627
|
-
if (ambiguityScore > 0.5 && workCategory === WORK_CATEGORIES.deep) {
|
|
628
|
-
triggers.push({ specialist: 'cx-product-manager', reason: 'clarify acceptance criteria (high ambiguity)' });
|
|
629
|
-
}
|
|
630
|
-
if (visualDeliverable || riskFlags.ui) {
|
|
631
|
-
triggers.push({ specialist: 'cx-designer', reason: 'visual deliverable / UI risk surface' });
|
|
632
|
-
}
|
|
633
|
-
if (authOrPayments && blastRadius !== 'narrow') {
|
|
634
|
-
triggers.push({ specialist: 'cx-security', reason: 'pre-dispatch threat model (auth/payments/PII + non-narrow blast radius)' });
|
|
635
|
-
}
|
|
636
|
-
if (riskFlags.architecture && !hasSuccessMetric) {
|
|
637
|
-
triggers.push({ specialist: 'cx-devil-advocate', reason: 'architecture change without stated success metric' });
|
|
638
|
-
}
|
|
639
|
-
if (blastRadius === 'wide') {
|
|
640
|
-
triggers.push({ specialist: 'cx-sre', reason: 'wide blast radius — operational readiness review' });
|
|
641
|
-
}
|
|
642
|
-
return triggers;
|
|
554
|
+
return evaluateWatchConditions(signals).map(({ specialist, reason }) => ({ specialist, reason }));
|
|
643
555
|
}
|
|
644
556
|
|
|
645
557
|
// User-visible dispatch summary: one line per specialist with the reason it
|
package/lib/parity.mjs
CHANGED
|
@@ -39,12 +39,22 @@ function adapterName(entry, prefix) {
|
|
|
39
39
|
return entry.isPersona ? entry.name : `${prefix}-${entry.name}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function entriesForSurface(registry, surface) {
|
|
42
|
+
function entriesForSurface(registry, surface, { scope = 'global' } = {}) {
|
|
43
43
|
const prefix = registry.prefix || 'cx';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
|
|
45
|
+
// User-scope expects only the `construct` front-door agent. Specialists
|
|
46
|
+
// live with each Construct-managed project (apps/dashboard, apps/docs,
|
|
47
|
+
// and every other repo that runs `construct init`) — see the two-tier
|
|
48
|
+
// sync contract in scripts/sync-specialists.mjs.
|
|
49
|
+
|
|
50
|
+
const entries = scope === 'project'
|
|
51
|
+
? [
|
|
52
|
+
...(registry.orchestrator ? [{ ...registry.orchestrator, isPersona: true }] : []),
|
|
53
|
+
...(registry.specialists || []).map((s) => ({ ...s, isPersona: false })),
|
|
54
|
+
]
|
|
55
|
+
: registry.orchestrator
|
|
56
|
+
? [{ ...registry.orchestrator, isPersona: true }]
|
|
57
|
+
: [];
|
|
48
58
|
|
|
49
59
|
return entries
|
|
50
60
|
.filter((e) => {
|
|
@@ -217,12 +227,31 @@ function checkCursor(registry, { homeDir = os.homedir() } = {}) {
|
|
|
217
227
|
};
|
|
218
228
|
}
|
|
219
229
|
|
|
230
|
+
// Names a v1.0.10 install would have populated at user scope (claude, codex,
|
|
231
|
+
// copilot) before the two-tier sync contract moved specialists to project
|
|
232
|
+
// scope. Pulled from the live registry so the roster auto-updates as new
|
|
233
|
+
// specialists land — anything in this set is "expected legacy state during
|
|
234
|
+
// an upgrade," not real drift.
|
|
235
|
+
function legacyUserScopeRoster(registry) {
|
|
236
|
+
const prefix = registry.prefix || 'cx';
|
|
237
|
+
const specialists = (registry.specialists || []).map((s) => `${prefix}-${s.name}`);
|
|
238
|
+
return new Set(specialists);
|
|
239
|
+
}
|
|
240
|
+
|
|
220
241
|
/**
|
|
221
242
|
* Run parity checks across every supported surface. Never throws — returns a
|
|
222
243
|
* structured report so callers can render it however they like.
|
|
244
|
+
*
|
|
245
|
+
* A surface that reports `drift` is reclassified to `legacy-install` when the
|
|
246
|
+
* only divergence is extras that all match the v1.0.10 user-scope roster — a
|
|
247
|
+
* dev box mid-upgrade from v1.0.10 (which populated cx-* specialists at user
|
|
248
|
+
* scope) to v1.0.13+ (project scope only). `legacy-install` rolls up to the
|
|
249
|
+
* same overall-ok bucket as `absent` so it doesn't hard-fail the gate; the
|
|
250
|
+
* summary still surfaces the `--fix-legacy-agents` hint.
|
|
223
251
|
*/
|
|
224
252
|
export function checkParity({ rootDir = ROOT_DIR, homeDir = os.homedir() } = {}) {
|
|
225
253
|
const registry = loadRegistry(rootDir);
|
|
254
|
+
const legacyRoster = legacyUserScopeRoster(registry);
|
|
226
255
|
const surfaces = [
|
|
227
256
|
checkClaude(registry, { homeDir }),
|
|
228
257
|
checkOpenCode(registry, { homeDir }),
|
|
@@ -230,13 +259,16 @@ export function checkParity({ rootDir = ROOT_DIR, homeDir = os.homedir() } = {})
|
|
|
230
259
|
checkCopilot(registry, { homeDir }),
|
|
231
260
|
checkVSCode(registry, { homeDir }),
|
|
232
261
|
checkCursor(registry, { homeDir }),
|
|
233
|
-
];
|
|
262
|
+
].map((s) => reclassifyLegacy(s, legacyRoster));
|
|
234
263
|
|
|
235
|
-
const ok = surfaces.every((s) => s.status === 'ok' || s.status === 'absent');
|
|
264
|
+
const ok = surfaces.every((s) => s.status === 'ok' || s.status === 'absent' || s.status === 'legacy-install');
|
|
236
265
|
const summary = surfaces.map((s) => {
|
|
237
266
|
if (s.status === 'absent') return `${s.surface}: not installed`;
|
|
238
267
|
if (s.status === 'unreadable') return `${s.surface}: unreadable (${s.error})`;
|
|
239
268
|
if (s.status === 'ok') return `${s.surface}: ok (${s.actualCount}/${s.expectedCount} ${s.kind})`;
|
|
269
|
+
if (s.status === 'legacy-install') {
|
|
270
|
+
return `${s.surface}: legacy v1.0.10 install — ${s.extra.length} stale ${s.kind} (run \`construct doctor --fix-legacy-agents\`)`;
|
|
271
|
+
}
|
|
240
272
|
const parts = [];
|
|
241
273
|
if (s.missing.length) parts.push(`missing: ${s.missing.join(', ')}`);
|
|
242
274
|
if (s.extra.length) parts.push(`extra: ${s.extra.join(', ')}`);
|
|
@@ -245,3 +277,12 @@ export function checkParity({ rootDir = ROOT_DIR, homeDir = os.homedir() } = {})
|
|
|
245
277
|
|
|
246
278
|
return { ok, surfaces, summary };
|
|
247
279
|
}
|
|
280
|
+
|
|
281
|
+
function reclassifyLegacy(surface, legacyRoster) {
|
|
282
|
+
if (surface.status !== 'drift') return surface;
|
|
283
|
+
if ((surface.missing?.length ?? 0) !== 0) return surface;
|
|
284
|
+
if (!surface.extra?.length) return surface;
|
|
285
|
+
const allLegacy = surface.extra.every((name) => legacyRoster.has(name));
|
|
286
|
+
if (!allLegacy) return surface;
|
|
287
|
+
return { ...surface, status: 'legacy-install' };
|
|
288
|
+
}
|