@geraldmaron/construct 1.0.16 → 1.0.18

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.
Files changed (191) hide show
  1. package/README.md +4 -1
  2. package/bin/construct +236 -21
  3. package/bin/construct-postinstall.mjs +17 -2
  4. package/commands/design/flow.md +2 -0
  5. package/commands/design/ui.md +2 -0
  6. package/lib/cli-commands.mjs +65 -13
  7. package/lib/embed/semantic.mjs +5 -3
  8. package/lib/embedded-contract/audit.mjs +52 -0
  9. package/lib/embedded-contract/capability.mjs +179 -0
  10. package/lib/embedded-contract/contract-version.mjs +39 -0
  11. package/lib/embedded-contract/envelope.mjs +70 -0
  12. package/lib/embedded-contract/index.mjs +71 -0
  13. package/lib/embedded-contract/ingest.mjs +77 -0
  14. package/lib/embedded-contract/model-resolve.mjs +186 -0
  15. package/lib/embedded-contract/redaction.mjs +91 -0
  16. package/lib/embedded-contract/role-facts.mjs +66 -0
  17. package/lib/embedded-contract/triage.mjs +134 -0
  18. package/lib/embedded-contract/workflow-defs.mjs +125 -0
  19. package/lib/embedded-contract/workflow-invoke.mjs +218 -0
  20. package/lib/hooks/config-protection.mjs +12 -5
  21. package/lib/init-unified.mjs +36 -26
  22. package/lib/intake/classify.mjs +6 -0
  23. package/lib/intake/tables/rnd.mjs +33 -0
  24. package/lib/mcp/server.mjs +69 -0
  25. package/lib/mcp/tools/embedded-contract.mjs +77 -0
  26. package/lib/model-router.mjs +40 -0
  27. package/lib/op-log.mjs +61 -0
  28. package/lib/roles/catalog.mjs +26 -95
  29. package/lib/service-manager.mjs +33 -11
  30. package/lib/setup.mjs +21 -3
  31. package/lib/validators/skills.mjs +21 -0
  32. package/package.json +12 -7
  33. package/scripts/sync-specialists.mjs +12 -3
  34. package/skills/ai/agent-dev.md +2 -0
  35. package/skills/ai/llm-security.md +2 -0
  36. package/skills/ai/ml-ops.md +2 -0
  37. package/skills/ai/orchestration-workflow.md +2 -0
  38. package/skills/ai/prompt-and-eval.md +2 -0
  39. package/skills/ai/prompt-optimizer.md +2 -0
  40. package/skills/ai/rag-system.md +2 -0
  41. package/skills/ai/trace-triage.md +36 -0
  42. package/skills/architecture/api-design.md +2 -0
  43. package/skills/architecture/caching.md +2 -0
  44. package/skills/architecture/cloud-native.md +2 -0
  45. package/skills/architecture/message-queue.md +2 -0
  46. package/skills/architecture/security-arch.md +2 -0
  47. package/skills/compliance/ai-disclosure.md +2 -0
  48. package/skills/compliance/data-privacy.md +2 -0
  49. package/skills/compliance/license-audit.md +2 -0
  50. package/skills/compliance/regulatory-review.md +2 -0
  51. package/skills/development/cpp.md +2 -0
  52. package/skills/development/go.md +2 -0
  53. package/skills/development/java.md +2 -0
  54. package/skills/development/kotlin.md +2 -0
  55. package/skills/development/mobile-crossplatform.md +2 -0
  56. package/skills/development/python.md +2 -0
  57. package/skills/development/rust.md +2 -0
  58. package/skills/development/shell.md +2 -0
  59. package/skills/development/swift.md +2 -0
  60. package/skills/development/typescript.md +2 -0
  61. package/skills/devops/ci-cd.md +2 -0
  62. package/skills/devops/containerization.md +2 -0
  63. package/skills/devops/cost-optimization.md +2 -0
  64. package/skills/devops/data-engineering.md +2 -0
  65. package/skills/devops/database.md +2 -0
  66. package/skills/devops/dependency-management.md +2 -0
  67. package/skills/devops/devsecops.md +2 -0
  68. package/skills/devops/git-workflow.md +2 -0
  69. package/skills/devops/incident-response.md +2 -0
  70. package/skills/devops/monorepo.md +2 -0
  71. package/skills/devops/observability.md +2 -0
  72. package/skills/devops/performance.md +2 -0
  73. package/skills/devops/testing.md +2 -0
  74. package/skills/docs/adr-workflow.md +2 -0
  75. package/skills/docs/backlog-proposal-workflow.md +2 -0
  76. package/skills/docs/customer-profile-workflow.md +2 -0
  77. package/skills/docs/document-ingest-workflow.md +2 -0
  78. package/skills/docs/evidence-ingest-workflow.md +2 -0
  79. package/skills/docs/init-docs.md +2 -0
  80. package/skills/docs/init-project.md +2 -0
  81. package/skills/docs/memo-and-decision-capture.md +45 -0
  82. package/skills/docs/prd-workflow.md +2 -0
  83. package/skills/docs/prfaq-workflow.md +2 -0
  84. package/skills/docs/product-intelligence-review.md +2 -0
  85. package/skills/docs/product-intelligence-workflow.md +2 -0
  86. package/skills/docs/product-signal-workflow.md +2 -0
  87. package/skills/docs/research-workflow.md +2 -0
  88. package/skills/docs/runbook-workflow.md +2 -0
  89. package/skills/docs/strategy-workflow.md +2 -0
  90. package/skills/docs/transcript-synthesis.md +43 -0
  91. package/skills/exploration/dependency-graph-reading.md +2 -0
  92. package/skills/exploration/repo-map.md +2 -0
  93. package/skills/exploration/tracer-bullet-method.md +2 -0
  94. package/skills/exploration/unknown-codebase-onboarding.md +2 -0
  95. package/skills/frameworks/django.md +2 -0
  96. package/skills/frameworks/nextjs.md +2 -0
  97. package/skills/frameworks/react.md +2 -0
  98. package/skills/frameworks/spring-boot.md +2 -0
  99. package/skills/frontend-design/accessibility.md +2 -0
  100. package/skills/frontend-design/component-patterns.md +2 -0
  101. package/skills/frontend-design/engineering.md +2 -0
  102. package/skills/frontend-design/screen-reader-testing.md +34 -0
  103. package/skills/frontend-design/state-management.md +2 -0
  104. package/skills/frontend-design/ui-aesthetics.md +2 -0
  105. package/skills/frontend-design/ux-principles.md +2 -0
  106. package/skills/operating/change-management.md +2 -0
  107. package/skills/operating/incident-response.md +2 -0
  108. package/skills/operating/oncall-rotation.md +2 -0
  109. package/skills/operating/orchestration-reference.md +2 -0
  110. package/skills/operating/raw-data-structuring.md +44 -0
  111. package/skills/operating/unstructured-triage.md +45 -0
  112. package/skills/quality-gates/premortem.md +37 -0
  113. package/skills/quality-gates/review-work.md +2 -0
  114. package/skills/quality-gates/verify-change.md +2 -0
  115. package/skills/quality-gates/verify-module.md +2 -0
  116. package/skills/quality-gates/verify-quality.md +2 -0
  117. package/skills/quality-gates/verify-security.md +2 -0
  118. package/skills/roles/architect.ai-systems.md +2 -0
  119. package/skills/roles/architect.data.md +2 -0
  120. package/skills/roles/architect.enterprise.md +2 -0
  121. package/skills/roles/architect.integration.md +2 -0
  122. package/skills/roles/architect.md +2 -0
  123. package/skills/roles/architect.platform.md +2 -0
  124. package/skills/roles/data-analyst.experiment.md +2 -0
  125. package/skills/roles/data-analyst.md +2 -0
  126. package/skills/roles/data-analyst.product-intelligence.md +2 -0
  127. package/skills/roles/data-analyst.product.md +2 -0
  128. package/skills/roles/data-analyst.telemetry.md +2 -0
  129. package/skills/roles/data-engineer.pipeline.md +2 -0
  130. package/skills/roles/data-engineer.vector-retrieval.md +2 -0
  131. package/skills/roles/data-engineer.warehouse.md +2 -0
  132. package/skills/roles/debugger.md +2 -0
  133. package/skills/roles/designer.accessibility.md +2 -0
  134. package/skills/roles/designer.md +2 -0
  135. package/skills/roles/engineer.ai.md +2 -0
  136. package/skills/roles/engineer.data.md +2 -0
  137. package/skills/roles/engineer.md +2 -0
  138. package/skills/roles/engineer.platform.md +2 -0
  139. package/skills/roles/operator.docs.md +2 -0
  140. package/skills/roles/operator.md +2 -0
  141. package/skills/roles/operator.release.md +2 -0
  142. package/skills/roles/operator.sre.md +2 -0
  143. package/skills/roles/orchestrator.md +2 -0
  144. package/skills/roles/product-manager.ai-product.md +2 -0
  145. package/skills/roles/product-manager.business-strategy.md +2 -0
  146. package/skills/roles/product-manager.enterprise.md +2 -0
  147. package/skills/roles/product-manager.growth.md +2 -0
  148. package/skills/roles/product-manager.md +2 -0
  149. package/skills/roles/product-manager.platform.md +2 -0
  150. package/skills/roles/product-manager.product.md +2 -0
  151. package/skills/roles/qa.ai-eval.md +2 -0
  152. package/skills/roles/qa.api-contract.md +2 -0
  153. package/skills/roles/qa.data-pipeline.md +2 -0
  154. package/skills/roles/qa.md +2 -0
  155. package/skills/roles/qa.test-automation.md +2 -0
  156. package/skills/roles/qa.web-ui.md +2 -0
  157. package/skills/roles/researcher.explorer.md +2 -0
  158. package/skills/roles/researcher.md +2 -0
  159. package/skills/roles/researcher.ux.md +2 -0
  160. package/skills/roles/reviewer.devil-advocate.md +2 -0
  161. package/skills/roles/reviewer.evaluator.md +2 -0
  162. package/skills/roles/reviewer.md +2 -0
  163. package/skills/roles/reviewer.trace.md +2 -0
  164. package/skills/roles/security.ai.md +2 -0
  165. package/skills/roles/security.appsec.md +2 -0
  166. package/skills/roles/security.cloud.md +2 -0
  167. package/skills/roles/security.legal-compliance.md +2 -0
  168. package/skills/roles/security.md +2 -0
  169. package/skills/roles/security.privacy.md +2 -0
  170. package/skills/roles/security.supply-chain.md +2 -0
  171. package/skills/security/blue-team.md +2 -0
  172. package/skills/security/code-audit.md +2 -0
  173. package/skills/security/pentest.md +2 -0
  174. package/skills/security/red-team.md +2 -0
  175. package/skills/security/threat-intel.md +2 -0
  176. package/skills/security/vuln-research.md +2 -0
  177. package/skills/strategy/competitive-landscape.md +2 -0
  178. package/skills/strategy/jobs-to-be-done.md +38 -0
  179. package/skills/strategy/market-research-methods.md +2 -0
  180. package/skills/strategy/narrative-arc.md +2 -0
  181. package/skills/strategy/pricing-positioning.md +2 -0
  182. package/skills/utility/clean-code.md +2 -0
  183. package/specialists/prompts/cx-architect.md +1 -0
  184. package/specialists/prompts/cx-designer.md +1 -1
  185. package/specialists/prompts/cx-engineer.md +1 -1
  186. package/specialists/prompts/cx-operations.md +1 -0
  187. package/specialists/prompts/cx-rd-lead.md +1 -0
  188. package/specialists/prompts/cx-sre.md +1 -0
  189. package/specialists/registry.json +20 -11
  190. package/specialists/role-manifests.json +1 -1
  191. package/templates/docs/test-plan.md +96 -0
@@ -283,20 +283,26 @@ function parseWithDocs(value) {
283
283
  return LANE_ORDER;
284
284
  }
285
285
 
286
- const lanes = value.split(',').map(v => v.trim().toLowerCase());
287
- const validLanes = lanes.filter(lane => LANE_ORDER.includes(lane));
288
-
289
- // Map common aliases
290
- const mapped = validLanes.map(lane => {
291
- if (lane === 'adr') return 'adrs';
292
- if (lane === 'rfc') return 'rfcs';
293
- if (lane === 'runbook') return 'runbooks';
294
- if (lane === 'postmortem') return 'postmortems';
295
- if (lane === 'prd') return 'prds';
296
- return lane;
297
- });
298
-
299
- return [...new Set(mapped)]; // dedupe
286
+ // Map singular aliases to canonical lane keys BEFORE validating, so
287
+ // `--with-docs=adr,rfc` resolves rather than being silently filtered out.
288
+ const ALIASES = { adr: 'adrs', rfc: 'rfcs', runbook: 'runbooks', postmortem: 'postmortems', prd: 'prds', brief: 'briefs', memo: 'memos', changelog: 'changelogs', meeting: 'meetings', note: 'notes' };
289
+
290
+ const requested = value.split(',').map(v => v.trim().toLowerCase()).filter(Boolean);
291
+ const resolved = [];
292
+ const unknown = [];
293
+ for (const lane of requested) {
294
+ const canonical = ALIASES[lane] || lane;
295
+ if (LANE_ORDER.includes(canonical)) resolved.push(canonical);
296
+ else unknown.push(lane);
297
+ }
298
+
299
+ // Surface typo'd lanes loudly instead of dropping them silently; valid lanes
300
+ // still proceed so one bad token does not abort init.
301
+ if (unknown.length) {
302
+ console.warn(`Warning: ignoring unknown --with-docs lane(s): ${unknown.join(', ')}. Valid lanes: ${LANE_ORDER.join(', ')}`);
303
+ }
304
+
305
+ return [...new Set(resolved)]; // dedupe
300
306
  }
301
307
 
302
308
  function inferProjectName(targetPath) {
@@ -741,8 +747,6 @@ This project enforces documentation quality:
741
747
  }
742
748
 
743
749
  function preflight(target) {
744
- console.log('[TRACE init:pre-flight]');
745
-
746
750
  // Check git repo
747
751
  try {
748
752
  execSync('git rev-parse --show-toplevel', { cwd: target, stdio: 'ignore' });
@@ -797,6 +801,14 @@ async function main() {
797
801
 
798
802
  const { clean } = preflight(target);
799
803
 
804
+ // Interactive sessions check machine prerequisites up front and offer to run
805
+ // `construct install` before any scaffolding, so a missing dependency is
806
+ // surfaced at the start rather than midway. Non-interactive runs rely on
807
+ // preflight's git check and stay silent so CI/tests are unaffected.
808
+ if (interactive && process.stdin.isTTY) {
809
+ await checkPrerequisites();
810
+ }
811
+
800
812
  if (!quiet) {
801
813
  console.log(`Initializing Construct in ${path.relative(process.cwd(), target) || "."}`);
802
814
  }
@@ -1037,7 +1049,6 @@ async function main() {
1037
1049
  // so the user can inspect what's watched and edit the file directly. Empty
1038
1050
  // parentDirs is the correct default: the inbox watcher always scans
1039
1051
  // .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
1040
- console.log('[TRACE init:intake-ask]');
1041
1052
 
1042
1053
  const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
1043
1054
  // When a custom intake surface was detected, default includeProjectInbox
@@ -1063,8 +1074,6 @@ async function main() {
1063
1074
  }
1064
1075
 
1065
1076
  // Ask about documentation system
1066
- console.log('[TRACE init:docs-ask]');
1067
-
1068
1077
  const { lanes, withArchitecture, withReadme, docsPreset: userDocsPreset } = await askDocumentationQuestions();
1069
1078
 
1070
1079
  // Create README.md if requested or missing
@@ -1072,9 +1081,7 @@ async function main() {
1072
1081
  if (withReadme || !fs.existsSync(readmePath)) {
1073
1082
  writeIfMissing(readmePath, buildProjectReadme(projectName));
1074
1083
  }
1075
-
1076
- console.log('[TRACE init:docs-write]');
1077
-
1084
+
1078
1085
  // Create documentation system if lanes specified
1079
1086
  if (lanes.length > 0) {
1080
1087
  // Filter out lanes that the project already covers elsewhere
@@ -1195,19 +1202,22 @@ async function main() {
1195
1202
  console.log(' NEXT STEPS');
1196
1203
  console.log('═══════════════════════════════════════════════════════════');
1197
1204
  console.log('');
1198
- console.log('1. Review AGENTS.md');
1205
+ // Number steps from a running counter so the list stays sequential when
1206
+ // the optional intake step is absent (was: hard-coded 1,2,4 → a visible gap).
1207
+ let step = 0;
1208
+ console.log(`${++step}. Review AGENTS.md`);
1199
1209
  console.log(' Operating rules and guidelines for this project');
1200
1210
  console.log('');
1201
- console.log('2. Edit plan.md');
1211
+ console.log(`${++step}. Edit plan.md`);
1202
1212
  console.log(' Add your current work and tasks');
1203
1213
  console.log('');
1204
1214
  if (lanes.includes('intake')) {
1205
- console.log('3. Use Intake');
1215
+ console.log(`${++step}. Use Intake`);
1206
1216
  console.log(' Drop files in .cx/inbox/ for processing');
1207
1217
  console.log(' Run: construct intake');
1208
1218
  console.log('');
1209
1219
  }
1210
- console.log('4. Start working');
1220
+ console.log(`${++step}. Start working`);
1211
1221
  console.log(' Address @construct in your editor to begin');
1212
1222
  console.log('');
1213
1223
  }
@@ -90,6 +90,9 @@ const FILENAME_HINTS = [
90
90
  { re: /eval|metric|benchmark/i, intakeType: 'eval-finding' },
91
91
  { re: /-bug\b|\bbug-/i, intakeType: 'bug' },
92
92
  { re: /runbook/i, intakeType: 'ops' },
93
+ { re: /^memo[-_]|[-_]memo\b/i, intakeType: 'memo' },
94
+ { re: /transcript|meeting-notes|standup|stand-up/i, intakeType: 'transcript' },
95
+ { re: /dataset|raw-?data|data-?dump|\.csv$|\.tsv$/i, intakeType: 'raw-data' },
93
96
  ];
94
97
 
95
98
  // Title-level family terms by intakeType. When the doc's H1 contains a
@@ -105,6 +108,9 @@ const TITLE_FAMILY = {
105
108
  'eval-finding': /^#\s*(eval(uation)?(\s+finding)?\b|benchmark)/im,
106
109
  bug: /^#\s*(bug\s+report|defect)\b/im,
107
110
  ops: /^#\s*(runbook|operations\s+guide)\b/im,
111
+ memo: /^#\s*(memo|decision\s+memo|status\s+update)\b/im,
112
+ transcript: /^#\s*(transcript|meeting\s+(notes|minutes)|call\s+notes)\b/im,
113
+ 'raw-data': /^#\s*(dataset|raw\s+data|data\s+export)\b/im,
108
114
  };
109
115
 
110
116
  // Title-lock returns the highest confidence the classifier emits because
@@ -22,6 +22,9 @@ export const INTAKE_TYPES = [
22
22
  'ops',
23
23
  'security',
24
24
  'legal-compliance',
25
+ 'memo',
26
+ 'transcript',
27
+ 'raw-data',
25
28
  'unknown',
26
29
  ];
27
30
 
@@ -170,6 +173,36 @@ export const CLASSIFICATION_TABLE = [
170
173
  risk: 'low',
171
174
  requiresApproval: false,
172
175
  },
176
+ {
177
+ intakeType: 'memo',
178
+ keywords: ['memo', 'decision memo', 'for your information', 'fyi', 'action item', 'action items', 'status update', 'weekly update', 'announcement', 'heads up', 'team update', 'decided to', 'proposal to'],
179
+ rdStage: 'artifact',
180
+ primaryOwner: 'docs-keeper',
181
+ recommendedChain: ['docs-keeper', 'reviewer'],
182
+ recommendedAction: 'summarize',
183
+ risk: 'low',
184
+ requiresApproval: false,
185
+ },
186
+ {
187
+ intakeType: 'transcript',
188
+ keywords: ['transcript', 'webvtt', 'meeting notes', 'meeting minutes', 'minutes of', 'attendees', 'call notes', 'stand-up notes', 'standup notes', 'recording of', 'speaker 1', 'speaker 2'],
189
+ rdStage: 'signal',
190
+ primaryOwner: 'researcher',
191
+ recommendedChain: ['researcher', 'data-analyst'],
192
+ recommendedAction: 'summarize',
193
+ risk: 'low',
194
+ requiresApproval: false,
195
+ },
196
+ {
197
+ intakeType: 'raw-data',
198
+ keywords: ['dataset', 'raw data', 'data dump', 'csv export', 'data export', 'column names', 'field names', 'rows and columns', 'records export', 'telemetry export', 'json export'],
199
+ rdStage: 'research',
200
+ primaryOwner: 'data-analyst',
201
+ recommendedChain: ['data-analyst', 'data-engineer'],
202
+ recommendedAction: 'summarize',
203
+ risk: 'low',
204
+ requiresApproval: false,
205
+ },
173
206
  ];
174
207
 
175
208
  export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
@@ -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
+ }
@@ -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
+ }
@@ -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 a list of available roles with
5
- * optional department grouping and consolidated-role views. Consumed by the
6
- * `roles:list` CLI command.
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 a list of role descriptors.
31
+ * Return the list of role descriptors from the registry's specialists array.
26
32
  *
27
- * @param {object} opts
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({ departments = false, consolidated = false } = {}) {
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
- if (consolidated) {
36
- const c = registry.consolidatedRoles || {};
37
- return (c.roles || []).map((r) => ({
38
- id: r.id,
39
- absorbs: r.absorbs || [],
40
- whenToUse: r.whenToUse || '',
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({ departments = false, consolidated = false } = {}) {
98
- const roles = listRoles({ departments, consolidated });
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
  }