@geraldmaron/construct 1.0.15 → 1.0.17
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 +19 -3
- package/bin/construct +376 -98
- package/bin/construct-postinstall.mjs +17 -2
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/cli-commands.mjs +333 -147
- package/lib/contracts/validate.mjs +20 -10
- package/lib/contracts/violation-log.mjs +27 -12
- package/lib/embedded-contract/audit.mjs +52 -0
- package/lib/embedded-contract/capability.mjs +179 -0
- package/lib/embedded-contract/contract-version.mjs +39 -0
- package/lib/embedded-contract/envelope.mjs +70 -0
- package/lib/embedded-contract/index.mjs +71 -0
- package/lib/embedded-contract/ingest.mjs +77 -0
- package/lib/embedded-contract/model-resolve.mjs +186 -0
- package/lib/embedded-contract/redaction.mjs +91 -0
- package/lib/embedded-contract/role-facts.mjs +66 -0
- package/lib/embedded-contract/triage.mjs +134 -0
- package/lib/embedded-contract/workflow-defs.mjs +125 -0
- package/lib/embedded-contract/workflow-invoke.mjs +218 -0
- package/lib/hooks/config-protection.mjs +12 -5
- package/lib/hooks/stop-notify.mjs +7 -0
- package/lib/init-unified.mjs +36 -26
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/prepare.mjs +22 -4
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/roles/gateway.mjs +30 -1
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/insights.mjs +12 -0
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +73 -10
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/lib/validators/skills.mjs +21 -0
- package/package.json +9 -3
- package/personas/construct.md +2 -0
- package/platforms/claude/CLAUDE.md +43 -15
- package/scripts/sync-specialists.mjs +32 -5
- package/skills/ai/agent-dev.md +2 -0
- package/skills/ai/llm-security.md +2 -0
- package/skills/ai/ml-ops.md +2 -0
- package/skills/ai/orchestration-workflow.md +2 -0
- package/skills/ai/prompt-and-eval.md +2 -0
- package/skills/ai/prompt-optimizer.md +2 -0
- package/skills/ai/rag-system.md +2 -0
- package/skills/ai/trace-triage.md +36 -0
- package/skills/architecture/api-design.md +2 -0
- package/skills/architecture/caching.md +2 -0
- package/skills/architecture/cloud-native.md +2 -0
- package/skills/architecture/message-queue.md +2 -0
- package/skills/architecture/security-arch.md +2 -0
- package/skills/compliance/ai-disclosure.md +2 -0
- package/skills/compliance/data-privacy.md +2 -0
- package/skills/compliance/license-audit.md +2 -0
- package/skills/compliance/regulatory-review.md +2 -0
- package/skills/development/cpp.md +2 -0
- package/skills/development/go.md +2 -0
- package/skills/development/java.md +2 -0
- package/skills/development/kotlin.md +2 -0
- package/skills/development/mobile-crossplatform.md +2 -0
- package/skills/development/python.md +2 -0
- package/skills/development/rust.md +2 -0
- package/skills/development/shell.md +2 -0
- package/skills/development/swift.md +2 -0
- package/skills/development/typescript.md +2 -0
- package/skills/devops/ci-cd.md +2 -0
- package/skills/devops/containerization.md +2 -0
- package/skills/devops/cost-optimization.md +2 -0
- package/skills/devops/data-engineering.md +2 -0
- package/skills/devops/database.md +2 -0
- package/skills/devops/dependency-management.md +2 -0
- package/skills/devops/devsecops.md +2 -0
- package/skills/devops/git-workflow.md +2 -0
- package/skills/devops/incident-response.md +2 -0
- package/skills/devops/monorepo.md +2 -0
- package/skills/devops/observability.md +2 -0
- package/skills/devops/performance.md +2 -0
- package/skills/devops/testing.md +2 -0
- package/skills/docs/adr-workflow.md +2 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -0
- package/skills/docs/customer-profile-workflow.md +2 -0
- package/skills/docs/document-ingest-workflow.md +2 -0
- package/skills/docs/evidence-ingest-workflow.md +2 -0
- package/skills/docs/init-docs.md +2 -0
- package/skills/docs/init-project.md +2 -0
- package/skills/docs/memo-and-decision-capture.md +45 -0
- package/skills/docs/prd-workflow.md +2 -0
- package/skills/docs/prfaq-workflow.md +2 -0
- package/skills/docs/product-intelligence-review.md +2 -0
- package/skills/docs/product-intelligence-workflow.md +2 -0
- package/skills/docs/product-signal-workflow.md +2 -0
- package/skills/docs/research-workflow.md +2 -0
- package/skills/docs/runbook-workflow.md +2 -0
- package/skills/docs/strategy-workflow.md +2 -0
- package/skills/docs/transcript-synthesis.md +43 -0
- package/skills/exploration/dependency-graph-reading.md +2 -0
- package/skills/exploration/repo-map.md +2 -0
- package/skills/exploration/tracer-bullet-method.md +2 -0
- package/skills/exploration/unknown-codebase-onboarding.md +2 -0
- package/skills/frameworks/django.md +2 -0
- package/skills/frameworks/nextjs.md +2 -0
- package/skills/frameworks/react.md +2 -0
- package/skills/frameworks/spring-boot.md +2 -0
- package/skills/frontend-design/accessibility.md +2 -0
- package/skills/frontend-design/component-patterns.md +2 -0
- package/skills/frontend-design/engineering.md +2 -0
- package/skills/frontend-design/screen-reader-testing.md +34 -0
- package/skills/frontend-design/state-management.md +2 -0
- package/skills/frontend-design/ui-aesthetics.md +2 -0
- package/skills/frontend-design/ux-principles.md +2 -0
- package/skills/operating/change-management.md +2 -0
- package/skills/operating/incident-response.md +2 -0
- package/skills/operating/oncall-rotation.md +2 -0
- package/skills/operating/orchestration-reference.md +2 -0
- package/skills/operating/raw-data-structuring.md +44 -0
- package/skills/operating/unstructured-triage.md +45 -0
- package/skills/quality-gates/premortem.md +37 -0
- package/skills/quality-gates/review-work.md +2 -0
- package/skills/quality-gates/verify-change.md +2 -0
- package/skills/quality-gates/verify-module.md +2 -0
- package/skills/quality-gates/verify-quality.md +2 -0
- package/skills/quality-gates/verify-security.md +2 -0
- package/skills/roles/architect.ai-systems.md +2 -0
- package/skills/roles/architect.data.md +2 -0
- package/skills/roles/architect.enterprise.md +2 -0
- package/skills/roles/architect.integration.md +2 -0
- package/skills/roles/architect.md +2 -0
- package/skills/roles/architect.platform.md +2 -0
- package/skills/roles/data-analyst.experiment.md +2 -0
- package/skills/roles/data-analyst.md +2 -0
- package/skills/roles/data-analyst.product-intelligence.md +2 -0
- package/skills/roles/data-analyst.product.md +2 -0
- package/skills/roles/data-analyst.telemetry.md +2 -0
- package/skills/roles/data-engineer.pipeline.md +2 -0
- package/skills/roles/data-engineer.vector-retrieval.md +2 -0
- package/skills/roles/data-engineer.warehouse.md +2 -0
- package/skills/roles/debugger.md +2 -0
- package/skills/roles/designer.accessibility.md +2 -0
- package/skills/roles/designer.md +2 -0
- package/skills/roles/engineer.ai.md +2 -0
- package/skills/roles/engineer.data.md +2 -0
- package/skills/roles/engineer.md +2 -0
- package/skills/roles/engineer.platform.md +2 -0
- package/skills/roles/operator.docs.md +2 -0
- package/skills/roles/operator.md +2 -0
- package/skills/roles/operator.release.md +2 -0
- package/skills/roles/operator.sre.md +2 -0
- package/skills/roles/orchestrator.md +2 -0
- package/skills/roles/product-manager.ai-product.md +2 -0
- package/skills/roles/product-manager.business-strategy.md +2 -0
- package/skills/roles/product-manager.enterprise.md +2 -0
- package/skills/roles/product-manager.growth.md +2 -0
- package/skills/roles/product-manager.md +2 -0
- package/skills/roles/product-manager.platform.md +2 -0
- package/skills/roles/product-manager.product.md +2 -0
- package/skills/roles/qa.ai-eval.md +2 -0
- package/skills/roles/qa.api-contract.md +2 -0
- package/skills/roles/qa.data-pipeline.md +2 -0
- package/skills/roles/qa.md +2 -0
- package/skills/roles/qa.test-automation.md +2 -0
- package/skills/roles/qa.web-ui.md +2 -0
- package/skills/roles/researcher.explorer.md +2 -0
- package/skills/roles/researcher.md +2 -0
- package/skills/roles/researcher.ux.md +2 -0
- package/skills/roles/reviewer.devil-advocate.md +2 -0
- package/skills/roles/reviewer.evaluator.md +2 -0
- package/skills/roles/reviewer.md +2 -0
- package/skills/roles/reviewer.trace.md +2 -0
- package/skills/roles/security.ai.md +2 -0
- package/skills/roles/security.appsec.md +2 -0
- package/skills/roles/security.cloud.md +2 -0
- package/skills/roles/security.legal-compliance.md +2 -0
- package/skills/roles/security.md +2 -0
- package/skills/roles/security.privacy.md +2 -0
- package/skills/roles/security.supply-chain.md +2 -0
- package/skills/security/blue-team.md +2 -0
- package/skills/security/code-audit.md +2 -0
- package/skills/security/pentest.md +2 -0
- package/skills/security/red-team.md +2 -0
- package/skills/security/threat-intel.md +2 -0
- package/skills/security/vuln-research.md +2 -0
- package/skills/strategy/competitive-landscape.md +2 -0
- package/skills/strategy/jobs-to-be-done.md +38 -0
- package/skills/strategy/market-research-methods.md +2 -0
- package/skills/strategy/narrative-arc.md +2 -0
- package/skills/strategy/pricing-positioning.md +2 -0
- package/skills/utility/clean-code.md +2 -0
- package/specialists/prompts/cx-engineer.md +1 -1
- package/specialists/registry.json +18 -9
- package/specialists/role-manifests.json +1 -1
package/lib/mcp/server.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
profileCreate, profileArchive, sandboxList, learningStatus,
|
|
57
57
|
knowledgeGraphAsk,
|
|
58
58
|
} from './tools/profile.mjs';
|
|
59
|
+
import { modelResolve, triageRecommend, workflowInvoke, capabilityDescribe } from './tools/embedded-contract.mjs';
|
|
59
60
|
|
|
60
61
|
const DEFAULT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
61
62
|
const ROOT_DIR = resolve(process.env.CX_TOOLKIT_DIR || DEFAULT_ROOT_DIR);
|
|
@@ -1044,6 +1045,70 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1044
1045
|
},
|
|
1045
1046
|
},
|
|
1046
1047
|
},
|
|
1048
|
+
{
|
|
1049
|
+
name: 'model_resolve',
|
|
1050
|
+
description: 'Resolve which model an embedded Construct workflow should use given the host/IDE provider context. Precedence: host model → same-provider-family fallback → Construct tier default → structured config error. Never reads or returns credential values (requiresCredential is a boolean) and never claims unverified provider health. Read-only; performs no writes.',
|
|
1051
|
+
inputSchema: {
|
|
1052
|
+
type: 'object',
|
|
1053
|
+
properties: {
|
|
1054
|
+
workflow_type: { type: 'string', description: 'Workflow type hint (e.g. evidence-ingest, prd-draft, architecture-review). Selects a tier when requested_tier is absent.' },
|
|
1055
|
+
requested_tier: { type: 'string', enum: ['reasoning', 'standard', 'fast'], description: 'Desired tier; overrides the workflow-type hint.' },
|
|
1056
|
+
host: { type: 'string', description: 'Host/IDE identifier (advisory).' },
|
|
1057
|
+
host_model: { type: 'string', description: 'Model the host is currently using (e.g. anthropic/claude-sonnet-4-6).' },
|
|
1058
|
+
host_provider: { type: 'string', description: 'Provider family the host uses, when no host_model is given.' },
|
|
1059
|
+
capabilities: { type: 'array', items: { type: 'string' }, description: 'Optional required capabilities; unverifiable ones are returned as warnings.' },
|
|
1060
|
+
allow_cross_provider_fallback: { type: 'boolean', description: 'Permit falling back outside the host provider family (default false).' },
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
name: 'triage_recommend',
|
|
1066
|
+
description: 'Classify an artifact and return a role-aware plan (primary owner, role chain with rationale, suggested skills, evidence requirements, expected outputs, approval requirements, risks, next steps, canExecute) WITHOUT enqueuing or executing. Classification confidence is reported distinctly from any generation confidence. Read-only; performs no durable write.',
|
|
1067
|
+
inputSchema: {
|
|
1068
|
+
type: 'object',
|
|
1069
|
+
properties: {
|
|
1070
|
+
input: { type: 'string', description: 'Artifact text to classify (meeting notes, bug report, proposal, etc.). Provide this OR file_path.' },
|
|
1071
|
+
file_path: { type: 'string', description: 'Path to a file to extract and classify (PDF/Office via docling, audio/video via whisper, transcripts, plain text). Used when input is absent.' },
|
|
1072
|
+
source_path: { type: 'string', description: 'Optional filename/source hint to aid classification.' },
|
|
1073
|
+
artifact_type: { type: 'string', description: 'Optional artifact-type hint (advisory).' },
|
|
1074
|
+
domain: { type: 'string', description: 'Optional broad domain hint.' },
|
|
1075
|
+
desired_outcome: { type: 'string', description: 'Optional desired outcome (advisory).' },
|
|
1076
|
+
constraints: { type: 'array', items: { type: 'string' }, description: 'Optional constraints (advisory).' },
|
|
1077
|
+
available_roles: { type: 'array', items: { type: 'string' }, description: 'Restrict the plan to these role ids; dropped roles are reported as warnings.' },
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: 'workflow_invoke',
|
|
1083
|
+
description: 'Invoke a named Construct workflow (roles/skills) non-interactively and return a provenanced execution plan: selected roles, rationale, applied skills, resolved model, evidence requirements, output contract, risks, and a traceId. Construct returns the orchestration plan; the host runtime performs specialist reasoning. Durable writes occur ONLY when approval_mode is allow-durable-write; proposal-only and requires-human-approval perform no durable writes.',
|
|
1084
|
+
inputSchema: {
|
|
1085
|
+
type: 'object',
|
|
1086
|
+
required: ['workflow_type'],
|
|
1087
|
+
properties: {
|
|
1088
|
+
workflow_type: { type: 'string', description: 'One of: evidence-ingest, proposal-review, prd-draft, architecture-review, risk-review, research-synthesis.' },
|
|
1089
|
+
input: { type: 'string', description: 'Artifact text the workflow operates on. Provide this OR file_path.' },
|
|
1090
|
+
file_path: { type: 'string', description: 'Path to a file to extract (docling/whisper/transcript) and operate on, used when input is absent.' },
|
|
1091
|
+
context: { type: 'object', description: 'Optional structured context; keys matching evidence requirements mark them satisfied.' },
|
|
1092
|
+
role_strategy: { type: 'string', enum: ['auto', 'explicit', 'constrained'], description: 'auto = default chain; explicit = use requested_roles; constrained = default chain intersected with requested_roles.' },
|
|
1093
|
+
requested_roles: { type: 'array', items: { type: 'string' }, description: 'Role ids for explicit/constrained strategies.' },
|
|
1094
|
+
approval_mode: { type: 'string', enum: ['proposal-only', 'requires-human-approval', 'allow-durable-write'], description: 'Gate for durable writes (default: the workflow type default).' },
|
|
1095
|
+
trace: { type: 'boolean', description: 'Emit a traceId for provenance correlation (default true).' },
|
|
1096
|
+
host: { type: 'string', description: 'Host/IDE identifier (advisory).' },
|
|
1097
|
+
host_model: { type: 'string', description: 'Model the host uses, for model resolution.' },
|
|
1098
|
+
host_provider: { type: 'string', description: 'Provider family the host uses, for model resolution.' },
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
name: 'capability_describe',
|
|
1104
|
+
description: 'Describe what this Construct install can do: versions, contract interfaces (CLI/MCP/SDK), roles, skills, workflows, schemas, models/providers, policies, telemetry posture, and plugins. Read-only and secret-free — provider entries carry env-key names and a configured boolean only, never credential values. Reads live registries so the published contract cannot drift from reality.',
|
|
1105
|
+
inputSchema: {
|
|
1106
|
+
type: 'object',
|
|
1107
|
+
properties: {
|
|
1108
|
+
root_dir: { type: 'string', description: 'Optional Construct install root (default: server toolkit dir).' },
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1047
1112
|
],
|
|
1048
1113
|
}));
|
|
1049
1114
|
|
|
@@ -1121,6 +1186,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1121
1186
|
else if (name === 'sandbox_list') result = sandboxList();
|
|
1122
1187
|
else if (name === 'learning_status') result = learningStatus(args);
|
|
1123
1188
|
else if (name === 'knowledge_graph_ask') result = knowledgeGraphAsk(args);
|
|
1189
|
+
else if (name === 'model_resolve') result = modelResolve(args);
|
|
1190
|
+
else if (name === 'triage_recommend') result = await triageRecommend(args);
|
|
1191
|
+
else if (name === 'workflow_invoke') result = await workflowInvoke(args);
|
|
1192
|
+
else if (name === 'capability_describe') result = capabilityDescribe(args);
|
|
1124
1193
|
else result = { error: `Unknown tool: ${name}` };
|
|
1125
1194
|
} catch (err) {
|
|
1126
1195
|
result = { error: err.message ?? String(err) };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/tools/embedded-contract.mjs — MCP surface for the Embedded Contract Layer.
|
|
3
|
+
*
|
|
4
|
+
* Thin adapters that translate MCP tool arguments into the embedded-contract
|
|
5
|
+
* core calls and return the same versioned envelope the CLI-JSON and SDK
|
|
6
|
+
* surfaces emit. No business logic lives here — it delegates to
|
|
7
|
+
* lib/embedded-contract/* so the three surfaces stay structurally identical.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { wrapContractResult } from '../../embedded-contract/envelope.mjs';
|
|
11
|
+
import { resolveEmbeddedModel as resolveModelCore } from '../../embedded-contract/model-resolve.mjs';
|
|
12
|
+
import { recommendPlan as recommendPlanCore } from '../../embedded-contract/triage.mjs';
|
|
13
|
+
import { invokeWorkflow as invokeWorkflowCore } from '../../embedded-contract/workflow-invoke.mjs';
|
|
14
|
+
import { buildCapabilityContract } from '../../embedded-contract/capability.mjs';
|
|
15
|
+
import { resolveInput } from '../../embedded-contract/ingest.mjs';
|
|
16
|
+
|
|
17
|
+
async function ingestArgs(args) {
|
|
18
|
+
if (args.input || !args.file_path) return { input: args.input, ingestion: undefined };
|
|
19
|
+
const resolved = await resolveInput({ filePath: args.file_path });
|
|
20
|
+
return { input: resolved.text, ingestion: resolved.error ? { ...(resolved.ingestion || {}), error: resolved.error } : resolved.ingestion };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function csv(value) {
|
|
24
|
+
if (Array.isArray(value)) return value;
|
|
25
|
+
if (typeof value === 'string' && value.trim()) return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function modelResolve(args = {}) {
|
|
30
|
+
const request = {
|
|
31
|
+
workflowType: args.workflow_type,
|
|
32
|
+
requestedTier: args.requested_tier,
|
|
33
|
+
host: args.host,
|
|
34
|
+
hostModel: args.host_model,
|
|
35
|
+
hostProvider: args.host_provider,
|
|
36
|
+
capabilities: csv(args.capabilities),
|
|
37
|
+
allowCrossProviderFallback: Boolean(args.allow_cross_provider_fallback),
|
|
38
|
+
};
|
|
39
|
+
return wrapContractResult(resolveModelCore(request), { surface: 'mcp' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function triageRecommend(args = {}) {
|
|
43
|
+
const { input, ingestion } = await ingestArgs(args);
|
|
44
|
+
const request = {
|
|
45
|
+
input,
|
|
46
|
+
ingestion,
|
|
47
|
+
sourcePath: args.source_path || args.file_path,
|
|
48
|
+
artifactType: args.artifact_type,
|
|
49
|
+
domain: args.domain,
|
|
50
|
+
desiredOutcome: args.desired_outcome,
|
|
51
|
+
constraints: csv(args.constraints),
|
|
52
|
+
availableRoles: csv(args.available_roles),
|
|
53
|
+
};
|
|
54
|
+
return wrapContractResult(recommendPlanCore(request), { surface: 'mcp' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function workflowInvoke(args = {}) {
|
|
58
|
+
const { input, ingestion } = await ingestArgs(args);
|
|
59
|
+
const request = {
|
|
60
|
+
workflowType: args.workflow_type,
|
|
61
|
+
input,
|
|
62
|
+
ingestion,
|
|
63
|
+
context: args.context,
|
|
64
|
+
roleStrategy: args.role_strategy,
|
|
65
|
+
requestedRoles: csv(args.requested_roles),
|
|
66
|
+
approvalMode: args.approval_mode,
|
|
67
|
+
trace: args.trace !== false,
|
|
68
|
+
host: args.host,
|
|
69
|
+
hostModel: args.host_model,
|
|
70
|
+
hostProvider: args.host_provider,
|
|
71
|
+
};
|
|
72
|
+
return wrapContractResult(await invokeWorkflowCore(request), { surface: 'mcp' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function capabilityDescribe(args = {}) {
|
|
76
|
+
return wrapContractResult(buildCapabilityContract({ rootDir: args.root_dir }), { surface: 'mcp' });
|
|
77
|
+
}
|
|
@@ -18,6 +18,7 @@ import { buildStatus } from '../../status.mjs';
|
|
|
18
18
|
import { readEfficiencyLog, buildCompactEfficiencyDigest } from '../../efficiency.mjs';
|
|
19
19
|
import { addObservation } from '../../observation-store.mjs';
|
|
20
20
|
import { loadConstructEnv } from '../../env-config.mjs';
|
|
21
|
+
import { createSqlClient, closeSqlClient } from '../../storage/backend.mjs';
|
|
21
22
|
|
|
22
23
|
// Load config.env once at module init so config.env values win over shell env
|
|
23
24
|
// (shell env may have stale/truncated credentials from earlier sessions)
|
|
@@ -243,6 +244,35 @@ export async function cxScore(args) {
|
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
// Postgres write-through (team/enterprise). Backs the
|
|
248
|
+
// construct_skill_quality_correlation view that `construct skills
|
|
249
|
+
// correlate-quality` reads — without this, the view stays empty and
|
|
250
|
+
// the CLI surface that promised correlation data delivered nothing.
|
|
251
|
+
// Best-effort: a missing DATABASE_URL or schema misalignment never
|
|
252
|
+
// blocks the score from reaching the remote telemetry or the local
|
|
253
|
+
// observation store above.
|
|
254
|
+
|
|
255
|
+
if (Number.isFinite(numericValue)) {
|
|
256
|
+
const sqlClient = createSqlClient(process.env);
|
|
257
|
+
if (sqlClient) {
|
|
258
|
+
try {
|
|
259
|
+
await sqlClient`
|
|
260
|
+
insert into construct_cx_scores (ts, trace_id, session_id, agent_id, name, value, comment)
|
|
261
|
+
values (
|
|
262
|
+
${new Date().toISOString()},
|
|
263
|
+
${traceId},
|
|
264
|
+
${args.session_id ?? null},
|
|
265
|
+
${args.agent_id ?? args.name ?? null},
|
|
266
|
+
${args.name ?? 'quality'},
|
|
267
|
+
${numericValue},
|
|
268
|
+
${args.comment ?? null}
|
|
269
|
+
)
|
|
270
|
+
`;
|
|
271
|
+
} catch { /* best-effort write — never block on DB issues */ }
|
|
272
|
+
finally { await closeSqlClient(sqlClient).catch(() => {}); }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
|
|
247
277
|
} catch (err) {
|
|
248
278
|
return { ok: false, error: err.message };
|
package/lib/model-router.mjs
CHANGED
|
@@ -418,6 +418,46 @@ export function resolveTiersForPrimary(primaryModelId) {
|
|
|
418
418
|
return family.resolve({ reasoning: undefined, standard: undefined, fast: undefined });
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
// Credential detection here is env-only — no network or process probe — so
|
|
422
|
+
// callers (embedded model resolution, capability discovery) stay synchronous
|
|
423
|
+
// and deterministic, and never expose a key value, only its presence.
|
|
424
|
+
|
|
425
|
+
function familyDescriptor(family, env) {
|
|
426
|
+
const requiresEnv = PROVIDER_ENV_MAP[family.id] || (Array.isArray(family.requiresEnv) ? family.requiresEnv : []);
|
|
427
|
+
const local = family.local === true;
|
|
428
|
+
const configured = local || requiresEnv.some((key) => {
|
|
429
|
+
const value = env?.[key];
|
|
430
|
+
return typeof value === "string" && value.length > 0;
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
id: family.id,
|
|
434
|
+
label: family.label,
|
|
435
|
+
local,
|
|
436
|
+
requiresEnv,
|
|
437
|
+
tiers: family.resolve({}),
|
|
438
|
+
configured,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Describe the provider family a model id belongs to, including which env keys
|
|
444
|
+
* confirm credentials and whether they are present. Returns null when no family
|
|
445
|
+
* matches.
|
|
446
|
+
*/
|
|
447
|
+
export function describeModelFamily(modelId, { env = process.env } = {}) {
|
|
448
|
+
const family = matchProviderFamily(modelId);
|
|
449
|
+
if (!family) return null;
|
|
450
|
+
return familyDescriptor(family, env);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Describe every provider family. Resolves a host-supplied provider id and
|
|
455
|
+
* enumerates model availability for the embedded contract layer.
|
|
456
|
+
*/
|
|
457
|
+
export function listModelFamilies({ env = process.env } = {}) {
|
|
458
|
+
return PROVIDER_FAMILY_TIERS.map((family) => familyDescriptor(family, env));
|
|
459
|
+
}
|
|
460
|
+
|
|
421
461
|
/**
|
|
422
462
|
* Read the OpenRouter API key from the OpenCode config file.
|
|
423
463
|
*/
|
package/lib/op-log.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/op-log.mjs — per-operation structured log file for long-running CLI ops.
|
|
3
|
+
*
|
|
4
|
+
* Commands like `dev`, `install`, `init`, and `sync` keep their human-facing
|
|
5
|
+
* output on stdout (println/ok/warn). This module adds a parallel machine log:
|
|
6
|
+
* one JSONL file per run at `<home>/.cx/<op>-<timestamp>.log`, every line
|
|
7
|
+
* stamped with a shared correlation id (`op_id`) so a failed run can be
|
|
8
|
+
* reconstructed after the fact and concurrent runs never interleave
|
|
9
|
+
* ambiguously. It reuses `makeLogger` from lib/logger.mjs for the line format.
|
|
10
|
+
*
|
|
11
|
+
* Writes are synchronous (an open fd + writeSync), not a buffered stream:
|
|
12
|
+
* CLI ops routinely end in process.exit(), and a buffered stream can drop its
|
|
13
|
+
* tail before the event loop drains it. Synchronous append loses nothing.
|
|
14
|
+
*
|
|
15
|
+
* Best-effort by contract: if the log dir can't be created or the file can't
|
|
16
|
+
* open, the returned handle silently no-ops. Logging must never break the
|
|
17
|
+
* operation it observes.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
|
|
24
|
+
import { makeLogger, newRequestId } from './logger.mjs';
|
|
25
|
+
|
|
26
|
+
const NOOP_LOGGER = Object.freeze({ debug() {}, info() {}, warn() {}, error() {} });
|
|
27
|
+
|
|
28
|
+
export function startOpLog(op, { homeDir = os.homedir(), env = process.env, now = new Date() } = {}) {
|
|
29
|
+
const id = newRequestId();
|
|
30
|
+
const dir = path.join(homeDir, '.cx');
|
|
31
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-');
|
|
32
|
+
const logPath = path.join(dir, `${op}-${stamp}.log`);
|
|
33
|
+
|
|
34
|
+
let fd = null;
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
fd = fs.openSync(logPath, 'a');
|
|
38
|
+
} catch {
|
|
39
|
+
fd = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Adapt the fd to the minimal stream surface makeLogger needs: a sync write.
|
|
43
|
+
const stream = fd === null ? null : { write: (line) => { try { fs.writeSync(fd, line); } catch { /* best-effort */ } } };
|
|
44
|
+
const log = stream
|
|
45
|
+
? makeLogger({ env, stream }).child({ op, op_id: id })
|
|
46
|
+
: NOOP_LOGGER;
|
|
47
|
+
|
|
48
|
+
log.info('op.start', { pid: process.pid });
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
logPath: fd === null ? null : logPath,
|
|
53
|
+
event: (name, fields = {}) => log.info(name, fields),
|
|
54
|
+
warn: (name, fields = {}) => log.warn(name, fields),
|
|
55
|
+
error: (name, fields = {}) => log.error(name, fields),
|
|
56
|
+
close: (status = 'ok', fields = {}) => {
|
|
57
|
+
log.info('op.end', { status, ...fields });
|
|
58
|
+
if (fd !== null) { try { fs.closeSync(fd); } catch { /* already closed */ } fd = null; }
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -352,6 +352,21 @@ export function classifyRoleFlavors(request = '') {
|
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// One line per non-null flavor: "cx-<role>: loaded <role>.<flavor>
|
|
356
|
+
// overlay". Verbose chat surface + cx_trace span attribute; lets a
|
|
357
|
+
// post-hoc reviewer see which overlays drove a given dispatch.
|
|
358
|
+
|
|
359
|
+
export function formatOverlaySelection(roleFlavors) {
|
|
360
|
+
if (!roleFlavors || typeof roleFlavors !== 'object') return [];
|
|
361
|
+
const lines = [];
|
|
362
|
+
for (const [role, flavor] of Object.entries(roleFlavors)) {
|
|
363
|
+
if (!flavor) continue;
|
|
364
|
+
const specialist = `cx-${role.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
365
|
+
lines.push(`${specialist}: loaded ${role}.${flavor} overlay`);
|
|
366
|
+
}
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
369
|
+
|
|
355
370
|
export function detectRiskFlags(request = '') {
|
|
356
371
|
const text = String(request).toLowerCase();
|
|
357
372
|
return {
|
package/lib/roles/catalog.mjs
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/roles/catalog.mjs — role listing and display helpers.
|
|
3
3
|
*
|
|
4
|
-
* Reads specialists/registry.json to produce
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Reads the flat `specialists` array in specialists/registry.json to produce the
|
|
5
|
+
* list of available roles. Each descriptor carries the registry-prefixed display
|
|
6
|
+
* name (cx-<name>), description, model tier, edit capability, and skill ids — the
|
|
7
|
+
* shape consumed by the `roles:list` CLI command and the embedded capability
|
|
8
|
+
* contract (lib/embed/capability.mjs).
|
|
9
|
+
*
|
|
10
|
+
* The registry once carried `departments` and `consolidatedRoles` groupings; the
|
|
11
|
+
* schema since collapsed to a single `specialists` array, so this reader exposes
|
|
12
|
+
* the flat list only and never invents grouping that has no source.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
import { readFileSync } from 'node:fs';
|
|
@@ -22,109 +28,34 @@ function loadRegistry() {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
* Return
|
|
31
|
+
* Return the list of role descriptors from the registry's specialists array.
|
|
26
32
|
*
|
|
27
|
-
* @
|
|
28
|
-
* @param {boolean} opts.departments Group by department when true.
|
|
29
|
-
* @param {boolean} opts.consolidated Return consolidated 12-role view when true.
|
|
30
|
-
* @returns {Array<object>}
|
|
33
|
+
* @returns {Array<{id:string,name:string,description:string,modelTier:string,internal:boolean,canEdit:boolean,skills:string[]}>}
|
|
31
34
|
*/
|
|
32
|
-
export function listRoles(
|
|
35
|
+
export function listRoles() {
|
|
33
36
|
const registry = loadRegistry();
|
|
37
|
+
const prefix = registry.prefix || 'cx';
|
|
38
|
+
const specialists = Array.isArray(registry.specialists) ? registry.specialists : [];
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const agents = Array.isArray(registry.agents)
|
|
45
|
-
? registry.agents
|
|
46
|
-
: Object.values(registry.agents || {});
|
|
47
|
-
|
|
48
|
-
if (!departments) {
|
|
49
|
-
return agents.map((a) => ({
|
|
50
|
-
id: a.name,
|
|
51
|
-
name: `cx-${a.name}`,
|
|
52
|
-
description: a.description || '',
|
|
53
|
-
internal: !!a.internal,
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Group by department based on subDepartment membership in registry.departments
|
|
58
|
-
const depts = registry.departments || {};
|
|
59
|
-
const grouped = {};
|
|
60
|
-
for (const [deptId, dept] of Object.entries(depts)) {
|
|
61
|
-
grouped[deptId] = {
|
|
62
|
-
name: dept.name || deptId,
|
|
63
|
-
roles: [],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
grouped._ungrouped = { name: 'Other', roles: [] };
|
|
67
|
-
|
|
68
|
-
for (const agent of agents) {
|
|
69
|
-
let placed = false;
|
|
70
|
-
for (const [deptId, dept] of Object.entries(depts)) {
|
|
71
|
-
const subs = dept.subDepartments || {};
|
|
72
|
-
for (const sub of Object.values(subs)) {
|
|
73
|
-
if ((sub.roles || []).includes(agent.name) || (sub.roles || []).includes(`cx-${agent.name}`)) {
|
|
74
|
-
grouped[deptId].roles.push({ id: agent.name, name: `cx-${agent.name}`, description: agent.description || '' });
|
|
75
|
-
placed = true;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (placed) break;
|
|
80
|
-
}
|
|
81
|
-
if (!placed) {
|
|
82
|
-
grouped._ungrouped.roles.push({ id: agent.name, name: `cx-${agent.name}`, description: agent.description || '' });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return Object.entries(grouped)
|
|
87
|
-
.filter(([, g]) => g.roles.length > 0)
|
|
88
|
-
.map(([id, g]) => ({ departmentId: id, departmentName: g.name, roles: g.roles }));
|
|
40
|
+
return specialists.map((s) => ({
|
|
41
|
+
id: s.name,
|
|
42
|
+
name: `${prefix}-${s.name}`,
|
|
43
|
+
description: s.description || '',
|
|
44
|
+
modelTier: s.modelTier || 'standard',
|
|
45
|
+
internal: !!s.internal,
|
|
46
|
+
canEdit: !!s.canEdit,
|
|
47
|
+
skills: Array.isArray(s.skills) ? s.skills : [],
|
|
48
|
+
}));
|
|
89
49
|
}
|
|
90
50
|
|
|
91
51
|
/**
|
|
92
52
|
* Format the role list as a human-readable string.
|
|
93
53
|
*
|
|
94
|
-
* @param {object} opts Same options as listRoles.
|
|
95
54
|
* @returns {string}
|
|
96
55
|
*/
|
|
97
|
-
export function formatRoleList(
|
|
98
|
-
const roles = listRoles(
|
|
99
|
-
const lines = [];
|
|
100
|
-
|
|
101
|
-
if (consolidated) {
|
|
102
|
-
lines.push('Consolidated Roles (12-role simplified structure)');
|
|
103
|
-
lines.push('=================================================');
|
|
104
|
-
for (const r of roles) {
|
|
105
|
-
lines.push(`\n${r.id}`);
|
|
106
|
-
lines.push(` Absorbs: ${r.absorbs.join(', ')}`);
|
|
107
|
-
lines.push(` Use when: ${r.whenToUse}`);
|
|
108
|
-
}
|
|
109
|
-
lines.push('');
|
|
110
|
-
return lines.join('\n');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (departments) {
|
|
114
|
-
lines.push('Available Roles by Department');
|
|
115
|
-
lines.push('=============================');
|
|
116
|
-
for (const group of roles) {
|
|
117
|
-
lines.push(`\n${group.departmentName}`);
|
|
118
|
-
for (const role of group.roles) {
|
|
119
|
-
lines.push(` ${role.name.padEnd(30)} ${role.description}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
lines.push('');
|
|
123
|
-
return lines.join('\n');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
lines.push('Available Roles');
|
|
127
|
-
lines.push('===============');
|
|
56
|
+
export function formatRoleList() {
|
|
57
|
+
const roles = listRoles();
|
|
58
|
+
const lines = ['Available Roles', '==============='];
|
|
128
59
|
for (const role of roles) {
|
|
129
60
|
lines.push(` ${role.name.padEnd(30)} ${role.description}`);
|
|
130
61
|
}
|
package/lib/roles/gateway.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
-
import { homedir } from 'node:os';
|
|
14
|
+
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
17
|
import { emit, _paths } from './event-bus.mjs';
|
|
@@ -54,7 +54,36 @@ function appendPending(entry) {
|
|
|
54
54
|
appendFileSync(pendingPath(), JSON.stringify(entry) + '\n');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Paths under the OS tmpdir are by definition test fixtures or sandbox
|
|
58
|
+
// runs, not real project state. Escalating from them spawns persistent
|
|
59
|
+
// beads for ephemeral files — pure tracker noise. macOS resolves
|
|
60
|
+
// $TMPDIR through a /private/var/folders/... symlink; the raw tmpdir
|
|
61
|
+
// and its /private prefix are both checked so either resolved form
|
|
62
|
+
// matches.
|
|
63
|
+
|
|
64
|
+
export function isTestFixturePath(p) {
|
|
65
|
+
if (!p || typeof p !== 'string') return false;
|
|
66
|
+
const tmp = tmpdir();
|
|
67
|
+
if (p.startsWith(tmp)) return true;
|
|
68
|
+
if (p.startsWith('/private' + tmp)) return true;
|
|
69
|
+
if (p.startsWith('/tmp/')) return true;
|
|
70
|
+
// macOS canonical user-tmp prefix when $TMPDIR is set elsewhere
|
|
71
|
+
if (/^\/private\/var\/folders\/[^/]+\/[^/]+\/T\//.test(p)) return true;
|
|
72
|
+
if (/^\/var\/folders\/[^/]+\/[^/]+\/T\//.test(p)) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function eventIsFromTestFixture(event) {
|
|
77
|
+
if (isTestFixturePath(event?.cwd)) return true;
|
|
78
|
+
if (isTestFixturePath(event?.project)) return true;
|
|
79
|
+
if (isTestFixturePath(event?.context?.filePath)) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
export function shouldEscalate(event, manifest, now = Date.now()) {
|
|
84
|
+
if (eventIsFromTestFixture(event)) {
|
|
85
|
+
return { escalate: false, reason: 'test-fixture-path' };
|
|
86
|
+
}
|
|
58
87
|
const fp = event.fingerprint;
|
|
59
88
|
const pending = readPending();
|
|
60
89
|
|
package/lib/scheduler/index.mjs
CHANGED
|
@@ -94,14 +94,34 @@ registerJob({
|
|
|
94
94
|
handler: async () => ({ status: 'noop', reason: 'not yet implemented' }),
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// Doc hygiene scan cadence is deployment-aware. Solo runs nightly because
|
|
98
|
+
// a single contributor's doc drift accumulates slowly. Team and enterprise
|
|
99
|
+
// runs hourly because many writers can shift the surface within a workday,
|
|
100
|
+
// and the higher limit (50 vs 25) gives the reconcile worker enough headroom
|
|
101
|
+
// to keep up. Mode/schedule resolved at module-load so a single registry
|
|
102
|
+
// entry covers both topologies.
|
|
103
|
+
|
|
104
|
+
const DOC_HYGIENE_SOLO_CRON = '0 2 * * *';
|
|
105
|
+
const DOC_HYGIENE_TEAM_CRON = '0 * * * *';
|
|
106
|
+
|
|
107
|
+
export function resolveDocHygieneSchedule(env = process.env) {
|
|
108
|
+
const mode = getDeploymentMode(env);
|
|
109
|
+
const isTeamish = mode === 'team' || mode === 'enterprise';
|
|
110
|
+
return {
|
|
111
|
+
mode: isTeamish ? 'team' : 'solo',
|
|
112
|
+
schedule: isTeamish ? DOC_HYGIENE_TEAM_CRON : DOC_HYGIENE_SOLO_CRON,
|
|
113
|
+
limit: isTeamish ? 50 : 25,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const docHygiene = resolveDocHygieneSchedule();
|
|
97
118
|
registerJob({
|
|
98
119
|
id: 'doc-hygiene-scan',
|
|
99
|
-
schedule:
|
|
100
|
-
mode:
|
|
120
|
+
schedule: docHygiene.schedule,
|
|
121
|
+
mode: docHygiene.mode,
|
|
101
122
|
handler: async ({ cwd, env }) => {
|
|
102
123
|
const { findHygieneCandidates } = await import('../hygiene/scan.mjs');
|
|
103
|
-
const
|
|
104
|
-
const limit = mode === 'solo' ? 25 : 50;
|
|
124
|
+
const { limit } = resolveDocHygieneSchedule(env);
|
|
105
125
|
const candidates = findHygieneCandidates({ cwd, limit });
|
|
106
126
|
return {
|
|
107
127
|
status: 'ok',
|
package/lib/server/insights.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import os from 'node:os';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
import { readCostLog, summarizeCostData } from '../cost.mjs';
|
|
17
|
+
import { getRebrand } from '../profiles/rebrand.mjs';
|
|
17
18
|
|
|
18
19
|
const TELEMETRY_TIMEOUT_MS = 3500;
|
|
19
20
|
|
|
@@ -309,12 +310,23 @@ function summarizeIntakeQueue(env, cwd = process.cwd()) {
|
|
|
309
310
|
}
|
|
310
311
|
} catch { /* skip */ }
|
|
311
312
|
|
|
313
|
+
// Profile-aware labels: the active profile's rebrand block (e.g.
|
|
314
|
+
// rnd → "Intake queue"/"signal"; sales-ops → "Lead inbox"/"lead")
|
|
315
|
+
// travels in the payload so the dashboard renders the right noun
|
|
316
|
+
// without hardcoding "intake" anywhere user-facing.
|
|
317
|
+
|
|
318
|
+
let rebrand;
|
|
319
|
+
try { rebrand = getRebrand(cwd); }
|
|
320
|
+
catch { rebrand = { intakeQueueLabel: 'Intake queue', signalNoun: 'signal' }; }
|
|
321
|
+
|
|
312
322
|
return {
|
|
313
323
|
state: pendingCount === 0 && processedCount === 0 && skippedCount === 0 ? 'empty' : 'ok',
|
|
314
324
|
pending: pendingCount,
|
|
315
325
|
processed: processedCount,
|
|
316
326
|
skipped: skippedCount,
|
|
317
327
|
customerLinked,
|
|
328
|
+
label: rebrand.intakeQueueLabel,
|
|
329
|
+
itemNoun: rebrand.signalNoun,
|
|
318
330
|
};
|
|
319
331
|
} catch (err) {
|
|
320
332
|
return { state: 'unreachable', message: err.message };
|