@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
package/README.md CHANGED
@@ -32,9 +32,11 @@ Initialize a project:
32
32
 
33
33
  ```bash
34
34
  cd ~/your-project
35
- construct init --auto-start
35
+ construct init --yes
36
36
  ```
37
37
 
38
+ `construct init` scaffolds the project (`.cx/`, `AGENTS.md`, `plan.md`, adapters) and starts the local services by default. Pass `--no-start` to skip service startup, or `--interactive` for the guided flow.
39
+
38
40
  Open your editor and talk to `@construct`. A walkthrough lives in `construct_guide.md` at your project root.
39
41
 
40
42
  No Node? Try `brew install geraldmaron/construct/construct`. Cloning a project that already uses Construct? `npx -y @geraldmaron/construct init` wires it up.
@@ -155,6 +157,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
155
157
 
156
158
  | Command | What it does |
157
159
  |---|---|
160
+ | `construct capability` | Describe what this Construct install can do (embedded contract; read-only, secret-free) |
158
161
  | `construct claude:allow` | Manage Claude Code `permissions.allow` from the outside (auto-classifier blocks the agent from editing it) |
159
162
  | `construct hosts` | Show host support for Construct orchestration |
160
163
  | `construct mcp` | Manage MCP integrations |
package/bin/construct CHANGED
@@ -165,12 +165,14 @@ async function showInteractiveMenu() {
165
165
  }
166
166
  }
167
167
 
168
- function runNodeScript(scriptPath, args = [], extraEnv = {}) {
168
+ function runNodeScript(scriptPath, args = [], extraEnv = {}, { exitOnError = true } = {}) {
169
169
  const result = spawnSync(process.execPath, [scriptPath, ...args], {
170
170
  stdio: 'inherit',
171
171
  env: { ...process.env, ...extraEnv },
172
172
  });
173
- if (result.status !== 0) process.exit(result.status ?? 1);
173
+ const status = result.status ?? 1;
174
+ if (status !== 0 && exitOnError) process.exit(status);
175
+ return status;
174
176
  }
175
177
 
176
178
  async function cmdStatus() {
@@ -238,12 +240,24 @@ async function cmdShow() {
238
240
  }
239
241
 
240
242
  async function cmdSync(args) {
243
+ const { startOpLog } = await import('../lib/op-log.mjs');
244
+ const opLog = startOpLog('sync', { homeDir: HOME });
245
+ opLog.event('args', { args });
246
+
241
247
  generateCompletions();
242
- runNodeScript(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs'), args);
243
- if (args.includes('--no-docs')) return;
248
+ const syncStatus = runNodeScript(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs'), args, {}, { exitOnError: false });
249
+ if (syncStatus !== 0) {
250
+ opLog.close('failed', { stage: 'sync-specialists', status: syncStatus });
251
+ process.exit(syncStatus);
252
+ }
253
+ if (args.includes('--no-docs')) {
254
+ opLog.close('ok', { docs: 'skipped' });
255
+ return;
256
+ }
244
257
  const { regenerateDocs } = await import('../lib/auto-docs.mjs');
245
258
  const { changed } = await regenerateDocs({ rootDir: ROOT_DIR });
246
259
  for (const f of changed) ok(`Updated ${path.relative(ROOT_DIR, f)}`);
260
+ opLog.close('ok', { docsChanged: changed.length });
247
261
  }
248
262
 
249
263
  async function cmdCompletions(args) {
@@ -498,9 +512,9 @@ async function cmdDoctor() {
498
512
 
499
513
  // (Pre-push bypass doctor check removed in v1.0.16.) v1.0.15 deleted the
500
514
  // entire skip-var infrastructure (CONSTRUCT_SKIP_PREPUSH, _COMMENT_LINT,
501
- // _PR_LINT, _DOCS, CONSTRUCT_ALLOW_CLAUDE_PUSH). There is no longer any
502
- // writer to ~/.construct/audit/prepush-bypass.log, so any historical
503
- // entries are stale and the warning surfaced forever. The file itself
515
+ // _PR_LINT, _DOCS, CONSTRUCT_ALLOW_CLAUDE_PUSH). Nothing writes to
516
+ // ~/.construct/audit/prepush-bypass.log, so any entries there are stale
517
+ // and would otherwise surface the warning indefinitely. The file itself
504
518
  // is harmless — next cleanup tick may sweep it.
505
519
 
506
520
  // Embed daemon log size. The daemon's own scheduler rotates every
@@ -1071,15 +1085,77 @@ async function cmdDoctor() {
1071
1085
  if (failCount > 0) process.exit(1);
1072
1086
  }
1073
1087
 
1074
- async function cmdUp() {
1088
+ // Resolve which services `construct dev` should start. Returns null (start
1089
+ // everything) unless --only= or --select narrows it. Unknown --only keys fail
1090
+ // loud rather than silently starting the wrong set; --select on a non-TTY is an
1091
+ // error pointing at --only so scripts get a deterministic path.
1092
+ async function resolveServiceSelection(args, selectable) {
1093
+ const validKeys = new Set(selectable.map((s) => s.key));
1094
+ const onlyArg = args.find((a) => a.startsWith('--only='));
1095
+
1096
+ if (onlyArg) {
1097
+ const keys = onlyArg.slice('--only='.length).split(',').map((k) => k.trim()).filter(Boolean);
1098
+ const unknown = keys.filter((k) => !validKeys.has(k));
1099
+ if (unknown.length) {
1100
+ errorln(`Unknown service(s) for --only: ${unknown.join(', ')}`);
1101
+ errorln(`Valid services: ${[...validKeys].join(', ')}`);
1102
+ process.exit(1);
1103
+ }
1104
+ if (keys.length === 0) {
1105
+ errorln('--only requires at least one service.');
1106
+ process.exit(1);
1107
+ }
1108
+ return new Set(keys);
1109
+ }
1110
+
1111
+ if (args.includes('--select')) {
1112
+ if (!process.stdin.isTTY) {
1113
+ errorln('--select needs an interactive terminal. Use --only=<a,b,c> in scripts.');
1114
+ errorln(`Valid services: ${[...validKeys].join(', ')}`);
1115
+ process.exit(1);
1116
+ }
1117
+ const { multiSelect } = await import('../lib/tty-prompts.mjs');
1118
+ try {
1119
+ const chosen = await multiSelect({
1120
+ title: 'Start which services?',
1121
+ instructions: 'Pressure Guard and Doctor always run. Space toggles, Enter confirms.',
1122
+ options: selectable.map((s) => ({ label: s.label, value: s.key, description: s.description, checked: true })),
1123
+ });
1124
+ if (!chosen.length) {
1125
+ errorln('No services selected — nothing to start.');
1126
+ process.exit(1);
1127
+ }
1128
+ return new Set(chosen);
1129
+ } catch {
1130
+ errorln('Service selection canceled.');
1131
+ process.exit(1);
1132
+ }
1133
+ }
1134
+
1135
+ return null;
1136
+ }
1137
+
1138
+ async function cmdUp(args = []) {
1139
+ const { SELECTABLE_SERVICES } = await import('../lib/service-manager.mjs');
1140
+
1141
+ // --only=a,b,c picks services non-interactively; --select opens a checklist.
1142
+ // Both resolve to a Set of service keys (null = start everything).
1143
+
1144
+ const selected = await resolveServiceSelection(args, SELECTABLE_SERVICES);
1145
+
1146
+ const { startOpLog } = await import('../lib/op-log.mjs');
1147
+ const opLog = startOpLog('dev', { homeDir: HOME });
1148
+ opLog.event('selection', { services: selected ? [...selected] : 'all' });
1149
+
1075
1150
  // Auto-magic health check — catch missing prerequisites before starting services
1076
1151
  const { quickHealthCheck, detectCredentials, resolveCredentials } = await import('../lib/health-check.mjs');
1077
-
1152
+
1078
1153
  // Resolve credentials from env, .env files, and 1Password before starting services
1079
1154
  const resolvedCreds = await resolveCredentials({ homeDir: HOME });
1080
-
1155
+
1081
1156
  const health = await quickHealthCheck({ homeDir: HOME, showCredentials: true });
1082
-
1157
+ opLog.event('prereq', { ok: health.ok, missing: health.missing ?? [] });
1158
+
1083
1159
  if (!health.ok) {
1084
1160
  errorln('Prerequisites missing for starting services:');
1085
1161
  for (const item of health.missing) {
@@ -1087,10 +1163,12 @@ async function cmdUp() {
1087
1163
  }
1088
1164
  println('');
1089
1165
  info('Run `construct install --yes` to install missing dependencies.');
1166
+ opLog.close('failed', { reason: 'prerequisites-missing' });
1090
1167
  process.exit(1);
1091
1168
  }
1092
-
1093
- const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME });
1169
+
1170
+ const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME, selected });
1171
+ opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
1094
1172
  for (const svc of results) {
1095
1173
  const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
1096
1174
  if (svc.status === 'started') ok(svc.note ? `${label} (${svc.note})` : label);
@@ -1128,8 +1206,10 @@ async function cmdUp() {
1128
1206
  try {
1129
1207
  const { runEmbedCli } = await import('../lib/embed/cli.mjs');
1130
1208
  await runEmbedCli(['start'], { rootDir: ROOT_DIR });
1209
+ opLog.event('embed', { status: 'started' });
1131
1210
  } catch (err) {
1132
1211
  warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
1212
+ opLog.warn('embed', { status: 'failed', error: err.message });
1133
1213
  }
1134
1214
  }
1135
1215
 
@@ -1176,6 +1256,9 @@ async function cmdUp() {
1176
1256
  println('Dashboard: http://127.0.0.1:4242');
1177
1257
  println('Run \x1b[32mconstruct\x1b[0m for commands');
1178
1258
  println('');
1259
+
1260
+ const failed = results.filter((r) => r.status === 'error' || r.status === 'failed');
1261
+ opLog.close(failed.length ? 'degraded' : 'ok', { failed: failed.map((r) => r.name) });
1179
1262
  }
1180
1263
 
1181
1264
  async function cmdDown() {
@@ -1304,7 +1387,14 @@ async function cmdReview(args) {
1304
1387
  process.exit(1);
1305
1388
  }
1306
1389
 
1307
- async function cmdInit(args) { runNodeScript(path.join(ROOT_DIR, 'lib', 'init-unified.mjs'), args); }
1390
+ async function cmdInit(args) {
1391
+ const { startOpLog } = await import('../lib/op-log.mjs');
1392
+ const opLog = startOpLog('init', { homeDir: HOME });
1393
+ opLog.event('args', { args });
1394
+ const status = runNodeScript(path.join(ROOT_DIR, 'lib', 'init-unified.mjs'), args, {}, { exitOnError: false });
1395
+ opLog.close(status === 0 ? 'ok' : 'failed', { status });
1396
+ if (status !== 0) process.exit(status);
1397
+ }
1308
1398
  async function cmdDocsVerify(args) { runNodeScript(path.join(ROOT_DIR, 'lib', 'docs-verify.mjs'), args); }
1309
1399
  async function cmdInitUpdate(args) {
1310
1400
  runNodeScript(path.join(ROOT_DIR, 'lib', 'init-update.mjs'), args);
@@ -1747,7 +1837,22 @@ async function cmdHosts() {
1747
1837
  printHostCapabilities();
1748
1838
  }
1749
1839
  }
1750
- async function cmdSetup(args) { await runSetup({ rootDir: ROOT_DIR, args, homeDir: HOME }); }
1840
+ async function cmdSetup(args) {
1841
+ const { startOpLog } = await import('../lib/op-log.mjs');
1842
+ const opLog = startOpLog('install', { homeDir: HOME });
1843
+ opLog.event('args', { args });
1844
+ try {
1845
+ await runSetup({ rootDir: ROOT_DIR, args, homeDir: HOME });
1846
+ opLog.close('ok');
1847
+ } catch (err) {
1848
+ opLog.error('setup', { error: err.message });
1849
+ opLog.close('failed');
1850
+ errorln(`\nSetup did not complete: ${err.message}`);
1851
+ errorln(' -> Diagnose with: construct doctor');
1852
+ errorln(' -> Re-run setup: construct install (add --reconfigure to re-prompt for services)');
1853
+ process.exit(1);
1854
+ }
1855
+ }
1751
1856
 
1752
1857
  function printConfigModeHelp() {
1753
1858
  println('Construct config — inspect and set deployment posture.');
@@ -1841,6 +1946,45 @@ function formatIntakeTable(rows, { intakeQueueLabel, signalNoun } = { intakeQueu
1841
1946
  for (const row of rows) println(fmt(row));
1842
1947
  }
1843
1948
 
1949
+ // Embedded triage/planning surface — classifies an artifact and returns a
1950
+ // role-aware plan with no enqueue and no durable write. Shared by
1951
+ // `intake classify` and `graph recommend`. Input comes from --text, --file, or
1952
+ // stdin; --no-enqueue is accepted for explicitness (this path never enqueues).
1953
+
1954
+ async function cmdEmbeddedTriage(args) {
1955
+ const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
1956
+ const fileArg = flag('--file');
1957
+ let input = flag('--text') || '';
1958
+ let ingestion = null;
1959
+ if (!input && fileArg) {
1960
+ const { resolveInput } = await import('../lib/embedded-contract/ingest.mjs');
1961
+ const resolved = await resolveInput({ filePath: fileArg });
1962
+ input = resolved.text;
1963
+ ingestion = resolved.error ? { ...(resolved.ingestion || {}), error: resolved.error } : resolved.ingestion;
1964
+ }
1965
+ if (!input && !process.stdin.isTTY) {
1966
+ input = fs.readFileSync(0, 'utf8');
1967
+ }
1968
+ if (!input.trim() && !ingestion?.error) {
1969
+ errorln('Usage: construct intake classify --json [--text "..." | --file <path> | <stdin>] [--source <path>] [--artifact-type <t>] [--available-roles a,b]');
1970
+ process.exit(1);
1971
+ }
1972
+ const request = {
1973
+ input,
1974
+ sourcePath: flag('--source') || (fileArg || ''),
1975
+ ingestion,
1976
+ artifactType: flag('--artifact-type'),
1977
+ domain: flag('--domain'),
1978
+ desiredOutcome: flag('--desired-outcome'),
1979
+ constraints: flag('--constraints')?.split(',').map((s) => s.trim()).filter(Boolean),
1980
+ availableRoles: flag('--available-roles')?.split(',').map((s) => s.trim()).filter(Boolean),
1981
+ };
1982
+ const { recommendPlan } = await import('../lib/embedded-contract/triage.mjs');
1983
+ const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
1984
+ const envelope = wrapContractResult(recommendPlan(request), { surface: 'cli' });
1985
+ println(JSON.stringify(envelope, null, 2));
1986
+ }
1987
+
1844
1988
  async function cmdIntake(args) {
1845
1989
  const sub = args[0];
1846
1990
 
@@ -1849,6 +1993,10 @@ async function cmdIntake(args) {
1849
1993
  return;
1850
1994
  }
1851
1995
 
1996
+ if (sub === 'classify') {
1997
+ return await cmdEmbeddedTriage(args.slice(1));
1998
+ }
1999
+
1852
2000
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
1853
2001
  const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
1854
2002
  const cwd = process.cwd();
@@ -2941,6 +3089,10 @@ async function cmdGraph(args) {
2941
3089
  const sub = args[0];
2942
3090
  if (!sub || sub === '--help' || sub === '-h') { printGraphHelp(); return; }
2943
3091
 
3092
+ if (sub === 'recommend') {
3093
+ return await cmdEmbeddedTriage(args.slice(1));
3094
+ }
3095
+
2944
3096
  const { FilesystemTaskGraphStore } = await import('../lib/task-graph/store.mjs');
2945
3097
  const { generateTaskGraphFromTriage } = await import('../lib/task-graph/generate.mjs');
2946
3098
  const cwd = process.cwd();
@@ -3174,6 +3326,23 @@ async function cmdValidate() {
3174
3326
  }
3175
3327
 
3176
3328
  async function cmdModels(args) {
3329
+ if (args[0] === 'resolve') {
3330
+ const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
3331
+ const { resolveEmbeddedModel } = await import('../lib/embedded-contract/model-resolve.mjs');
3332
+ const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
3333
+ const request = {
3334
+ workflowType: flag('--workflow-type'),
3335
+ requestedTier: flag('--tier'),
3336
+ host: flag('--host'),
3337
+ hostModel: flag('--host-model'),
3338
+ hostProvider: flag('--host-provider'),
3339
+ capabilities: flag('--capabilities')?.split(',').map((s) => s.trim()).filter(Boolean),
3340
+ allowCrossProviderFallback: args.includes('--allow-cross-provider'),
3341
+ };
3342
+ const envelope = wrapContractResult(resolveEmbeddedModel(request), { surface: 'cli' });
3343
+ println(JSON.stringify(envelope, null, 2));
3344
+ return;
3345
+ }
3177
3346
  const tier = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1] ?? '';
3178
3347
  const setModel = args.find((arg) => arg.startsWith('--set='))?.split('=')[1] ?? '';
3179
3348
  const envPath = getUserEnvPath(HOME);
@@ -4571,9 +4740,56 @@ async function cmdProviders(args) {
4571
4740
  process.exit(1);
4572
4741
  }
4573
4742
 
4743
+ // Capability discovery surface — a read-only, secret-free description of the
4744
+ // installed Construct's capabilities, for embedding applications.
4745
+
4746
+ async function cmdCapability(args) {
4747
+ const sub = args[0];
4748
+ if (sub && sub !== 'describe' && !sub.startsWith('--')) {
4749
+ errorln(`Unknown capability subcommand: ${sub}. Available: describe`);
4750
+ process.exit(1);
4751
+ }
4752
+ const { buildCapabilityContract } = await import('../lib/embedded-contract/capability.mjs');
4753
+ const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
4754
+ const envelope = wrapContractResult(buildCapabilityContract({ rootDir: ROOT_DIR }), { surface: 'cli' });
4755
+ println(JSON.stringify(envelope, null, 2));
4756
+ }
4757
+
4574
4758
  async function cmdWorkflow(args) {
4575
4759
  const sub = args[0] || 'list';
4576
4760
 
4761
+ if (sub === 'invoke') {
4762
+ const rest = args.slice(1);
4763
+ const flag = (name) => { const i = rest.indexOf(name); return i !== -1 ? rest[i + 1] : undefined; };
4764
+ let input = flag('--text') || '';
4765
+ const fileArg = flag('--file');
4766
+ let ingestion = null;
4767
+ if (!input && fileArg) {
4768
+ const { resolveInput } = await import('../lib/embedded-contract/ingest.mjs');
4769
+ const resolved = await resolveInput({ filePath: fileArg });
4770
+ input = resolved.text;
4771
+ ingestion = resolved.error ? { ...(resolved.ingestion || {}), error: resolved.error } : resolved.ingestion;
4772
+ }
4773
+ if (!input && !process.stdin.isTTY) input = fs.readFileSync(0, 'utf8');
4774
+ const request = {
4775
+ workflowType: flag('--workflow-type'),
4776
+ input,
4777
+ ingestion,
4778
+ roleStrategy: flag('--role-strategy'),
4779
+ requestedRoles: flag('--roles')?.split(',').map((s) => s.trim()).filter(Boolean),
4780
+ approvalMode: flag('--approval-mode'),
4781
+ trace: !rest.includes('--no-trace'),
4782
+ host: flag('--host'),
4783
+ hostModel: flag('--host-model'),
4784
+ hostProvider: flag('--host-provider'),
4785
+ };
4786
+ const { invokeWorkflow } = await import('../lib/embedded-contract/workflow-invoke.mjs');
4787
+ const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
4788
+ const envelope = wrapContractResult(await invokeWorkflow(request), { surface: 'cli' });
4789
+ println(JSON.stringify(envelope, null, 2));
4790
+ return;
4791
+ }
4792
+
4577
4793
  if (sub === 'list') {
4578
4794
  const { existsSync, readdirSync } = await import('node:fs');
4579
4795
  const { join } = await import('node:path');
@@ -4951,6 +5167,7 @@ const handlers = new Map([
4951
5167
  ['creds', cmdCreds],
4952
5168
  ['providers', cmdProviders],
4953
5169
  ['workflow', cmdWorkflow],
5170
+ ['capability', cmdCapability],
4954
5171
  ['tags', cmdTags],
4955
5172
  ['scheduler', cmdScheduler],
4956
5173
  ['lint:research', cmdLintResearch],
@@ -4994,13 +5211,11 @@ const handlers = new Map([
4994
5211
  ['roles:list', async (args) => {
4995
5212
  const { listRoles, formatRoleList } = await import('../lib/roles/catalog.mjs');
4996
5213
  const json = args.includes('--json');
4997
- const departments = args.includes('--departments');
4998
- const consolidated = args.includes('--consolidated');
4999
-
5214
+
5000
5215
  if (json) {
5001
- println(JSON.stringify(listRoles({ departments, consolidated }), null, 2));
5216
+ println(JSON.stringify(listRoles(), null, 2));
5002
5217
  } else {
5003
- process.stdout.write(formatRoleList({ departments, consolidated }));
5218
+ process.stdout.write(formatRoleList());
5004
5219
  }
5005
5220
  }],
5006
5221
  ['roles:set', async (args) => {
@@ -5921,7 +6136,7 @@ if (!handler) {
5921
6136
  // commands like `dev`, `stop`, `init`, `uninstall` silently ignore --help
5922
6137
  // and execute (running `construct stop --help` actually stops services).
5923
6138
  // Every command — including internal ones — gets the same shape via
5924
- // formatCommandHelp(); per-command handlers no longer need to opt in.
6139
+ // formatCommandHelp(); per-command handlers need no opt-in.
5925
6140
 
5926
6141
  if (rest.includes('--help') || rest.includes('-h')) {
5927
6142
  process.stdout.write(formatCommandHelp(command, { colors: true }));
@@ -34,6 +34,18 @@ const PKG_VERSION = (() => {
34
34
 
35
35
  const log = (msg) => process.stdout.write(`[construct-postinstall] ${msg}\n`);
36
36
 
37
+ // Failures go to stderr as a prominent, actionable block so they are visible in
38
+ // npm output instead of scrolling past as a benign-looking line. The script
39
+ // stays exit-0 by design (see the staging catch): best-effort adapter staging
40
+ // must not hard-fail a consumer's `npm install` — the package is installed and
41
+ // usable, and `construct init` completes staging on demand.
42
+
43
+ const fail = (msg, hint) => {
44
+ process.stderr.write(`\n[construct-postinstall] WARNING: ${msg}\n`);
45
+ if (hint) process.stderr.write(`[construct-postinstall] -> ${hint}\n`);
46
+ process.stderr.write('\n');
47
+ };
48
+
37
49
  if (process.env.CONSTRUCT_SKIP_POSTINSTALL === '1') {
38
50
  log('skipping (CONSTRUCT_SKIP_POSTINSTALL=1)');
39
51
  process.exit(0);
@@ -72,7 +84,7 @@ if (process.env.npm_config_global === 'true' || process.env.npm_config_global ==
72
84
  stdio: 'inherit',
73
85
  });
74
86
  if (result.status !== 0) {
75
- log(`global sync failed (exit ${result.status}); run \`construct sync --global\` manually`);
87
+ fail(`Global front-door sync failed (exit ${result.status}).`, 'Run `construct sync --global` to complete it.');
76
88
  }
77
89
  }
78
90
  process.exit(0);
@@ -103,6 +115,9 @@ try {
103
115
  log,
104
116
  });
105
117
  } catch (err) {
106
- log(`staging failed: ${err.message}; leaving project in a clean state`);
118
+ fail(`Adapter staging failed: ${err.message}`, 'The package is installed; run `npx construct init` in this project to complete setup.');
119
+ // Intentionally exit 0: staging is best-effort completion, and a non-zero exit
120
+ // here would fail the consumer's entire `npm install`. The failure is loud on
121
+ // stderr above and recoverable via `construct init`.
107
122
  process.exit(0);
108
123
  }
@@ -8,3 +8,5 @@ Produce:
8
8
  - JOBS-TO-BE-DONE: "When [situation], I want to [motivation], so I can [outcome]."
9
9
  - FRICTION MAP: 5 likely points where users get stuck, confused, or quit
10
10
  - DESIGN-DRIVING QUESTIONS: a small set of questions (typically 3-7) whose answers would change layout, flow, or interaction decisions
11
+
12
+ To capture the flow as a committed artifact, run `construct wireframe "<flow description>" --type=flow` (or `--type=user-journey` for a persona funnel) to emit a diffable Mermaid diagram under `.cx/wireframes/`. Stay text-first (no new diagramming dependency).
@@ -10,3 +10,5 @@ Every meaningful UI surface needs:
10
10
  - WCAG AA baseline: keyboard-navigable, 4.5:1 contrast, visible focus, ARIA labels
11
11
 
12
12
  Does it look intentional, or like a default template? Flag experience drift.
13
+
14
+ For a layout sketch, run `construct wireframe "<screen description>" --type=layout` to produce a committed low-fi HTML/Mermaid artifact under `.cx/wireframes/`, then refine it. Stay text-first (no new diagramming dependency). Valid `--type` values: layout, flow, state, sequence, er, user-journey.
@@ -20,7 +20,11 @@ export const CLI_COMMANDS = [
20
20
  category: 'Core',
21
21
  core: true,
22
22
  description: 'Start services for development',
23
- usage: 'construct dev',
23
+ usage: 'construct dev [--select] [--only=postgres,dashboard,...]',
24
+ options: [
25
+ { flag: '--select', desc: 'Pick which services to start from an interactive checklist' },
26
+ { flag: '--only=<a,b,c>', desc: 'Start only the named services (postgres, dashboard, telemetry, memory, opencode)' },
27
+ ],
24
28
  },
25
29
  {
26
30
  name: 'stop',
@@ -47,10 +51,11 @@ export const CLI_COMMANDS = [
47
51
  category: 'Core',
48
52
  core: true,
49
53
  description: 'Machine setup (once per machine): Docker, cm/cass, config, embeddings',
50
- usage: 'construct install [--yes] [--no-docker]',
54
+ usage: 'construct install [--yes] [--no-docker] [--reconfigure]',
51
55
  options: [
52
- { flag: '--yes', desc: 'Apply defaults without prompts' },
53
- { flag: '--no-docker', desc: 'Skip Docker-based service setup (local Postgres)' },
56
+ { flag: '--yes', desc: 'Apply defaults without prompts' },
57
+ { flag: '--no-docker', desc: 'Skip Docker-based service setup (local Postgres)' },
58
+ { flag: '--reconfigure', desc: 'Re-prompt for service consent, ignoring cached answers' },
54
59
  ],
55
60
  },
56
61
  {
@@ -95,7 +100,16 @@ export const CLI_COMMANDS = [
95
100
  category: 'Core',
96
101
  core: true,
97
102
  description: 'View and process the active profile\'s intake queue (queue label varies by profile)',
98
- usage: 'construct intake list|show|done|skip',
103
+ usage: 'construct intake list|show|done|skip|reopen|integrate|classify',
104
+ subcommands: [
105
+ { name: 'list', desc: 'List pending packets' },
106
+ { name: 'show <id>', desc: 'Show one packet (triage, related docs, excerpt, tag suggestions)' },
107
+ { name: 'done <id> [--output=<path>]', desc: 'Mark processed; optionally stamp the produced artifact' },
108
+ { name: 'skip <id> [--reason=…]', desc: 'Drop without action; preserves audit trail' },
109
+ { name: 'reopen <id>', desc: 'Move a processed or skipped packet back to pending' },
110
+ { name: 'integrate <id> <github|jira|confluence> [--publish-issues]', desc: 'Create an external ticket from a packet (--publish-issues unlocks the demo-source gate)' },
111
+ { name: 'classify --json [--text|--file|<stdin>]', desc: 'Classify an artifact and return a role-aware plan without enqueuing (embedded contract)' },
112
+ ],
99
113
  },
100
114
  {
101
115
  name: 'recommendations',
@@ -207,7 +221,17 @@ export const CLI_COMMANDS = [
207
221
  category: 'Core',
208
222
  core: true,
209
223
  description: 'Check installation health',
210
- usage: 'construct doctor [--fix-legacy-agents]',
224
+ usage: 'construct doctor [<status|logs|tick|report|consistency|watch|stop|credentials>] [--fix-legacy-agents]',
225
+ subcommands: [
226
+ { name: 'status', desc: 'Doctor daemon status' },
227
+ { name: 'logs', desc: 'Tail doctor daemon logs' },
228
+ { name: 'tick', desc: 'Run one doctor daemon check cycle now' },
229
+ { name: 'report', desc: 'Print the latest health report' },
230
+ { name: 'consistency', desc: 'Run cross-surface consistency checks' },
231
+ { name: 'watch', desc: 'Start the doctor daemon (continuous checks)' },
232
+ { name: 'stop', desc: 'Stop the doctor daemon' },
233
+ { name: 'credentials', desc: 'Diagnose provider credential resolution' },
234
+ ],
211
235
  options: [
212
236
  { flag: '--fix-legacy-agents', desc: 'Sweep legacy cx-*.md agents at user scope and re-sync' },
213
237
  ],
@@ -266,7 +290,10 @@ export const CLI_COMMANDS = [
266
290
  category: 'Work',
267
291
  core: false,
268
292
  description: 'Task graph management',
269
- usage: 'construct graph <show|update>',
293
+ usage: 'construct graph <list|show|from-intake|recommend>',
294
+ subcommands: [
295
+ { name: 'recommend --json [--text|--file|<stdin>]', desc: 'Return a role-aware plan for an artifact without enqueuing (embedded contract; alias of intake classify)' },
296
+ ],
270
297
  },
271
298
  {
272
299
  name: 'models',
@@ -274,7 +301,7 @@ export const CLI_COMMANDS = [
274
301
  category: 'Models & Integrations',
275
302
  core: false,
276
303
  description: 'Show or update model tier assignments',
277
- usage: 'construct models <list|set|free|reset|usage|cost>',
304
+ usage: 'construct models <list|set|free|reset|usage|cost|resolve>',
278
305
  subcommands: [
279
306
  { name: 'list', desc: 'Show current tier assignments' },
280
307
  { name: 'set --tier=<reasoning|standard|fast> --model=<model>', desc: 'Set a model for a tier' },
@@ -282,6 +309,18 @@ export const CLI_COMMANDS = [
282
309
  { name: 'reset', desc: 'Reset all tier assignments' },
283
310
  { name: 'usage', desc: 'Show token usage per tier' },
284
311
  { name: 'cost', desc: 'Show cost breakdown' },
312
+ { name: 'resolve --json', desc: 'Resolve the model for an embedded workflow given host context' },
313
+ ],
314
+ },
315
+ {
316
+ name: 'capability',
317
+ emoji: '🧩',
318
+ category: 'Models & Integrations',
319
+ core: false,
320
+ description: 'Describe what this Construct install can do (embedded contract; read-only, secret-free)',
321
+ usage: 'construct capability describe --json',
322
+ subcommands: [
323
+ { name: 'describe --json', desc: 'Emit versions, interfaces, roles, skills, workflows, schemas, models, policies, telemetry, plugins' },
285
324
  ],
286
325
  },
287
326
  {
@@ -304,7 +343,14 @@ export const CLI_COMMANDS = [
304
343
  category: 'Models & Integrations',
305
344
  core: false,
306
345
  description: 'Manage external Construct plugin manifests',
307
- usage: 'construct plugin <list|info|init>',
346
+ usage: 'construct plugin <list|info|init|validate|engine>',
347
+ subcommands: [
348
+ { name: 'list', desc: 'List discovered plugin manifests' },
349
+ { name: 'info <id>', desc: 'Show details for one plugin' },
350
+ { name: 'init', desc: 'Scaffold a new plugin manifest' },
351
+ { name: 'validate', desc: 'Validate a plugin manifest against the schema' },
352
+ { name: 'engine', desc: 'Plugin engine operations' },
353
+ ],
308
354
  },
309
355
  {
310
356
  name: 'hosts',
@@ -487,10 +533,13 @@ export const CLI_COMMANDS = [
487
533
  category: 'Advanced',
488
534
  core: false,
489
535
  description: 'Remove Construct state',
490
- usage: 'construct uninstall [--yes] [--all]',
536
+ usage: 'construct uninstall [--dry-run] [--yes] [--all] [--keep-state] [--scope=project|machine|all]',
491
537
  options: [
492
- { flag: '--yes', desc: 'Remove auto-risk categories without prompting' },
493
- { flag: '--all', desc: 'Combined with --yes: also remove ask-risk categories (project data, machine config)' },
538
+ { flag: '--dry-run', desc: 'Print the plan and exit; change nothing' },
539
+ { flag: '--yes', desc: 'Remove auto-risk (✓) categories without prompting' },
540
+ { flag: '--all', desc: 'Combined with --yes: also remove ask-risk (◐) categories (project data, machine config)' },
541
+ { flag: '--keep-state', desc: 'Only remove the launcher + adapters; preserve .cx/, ~/.construct, Postgres' },
542
+ { flag: '--scope=<...>', desc: 'Limit to project | machine | all (default: all)' },
494
543
  ],
495
544
  },
496
545
  {
@@ -675,7 +724,10 @@ export const CLI_COMMANDS = [
675
724
  category: 'Work',
676
725
  core: false,
677
726
  description: 'Instantiate workflow templates (PRD-to-review chains, onboarding, handoffs)',
678
- usage: 'construct workflow <list|show|new>',
727
+ usage: 'construct workflow <list|show|new|invoke>',
728
+ subcommands: [
729
+ { name: 'invoke --json --workflow-type <t> [--text|--file|<stdin>]', desc: 'Invoke a workflow (roles/skills) non-interactively with approval gating and provenance (embedded contract)' },
730
+ ],
679
731
  },
680
732
  {
681
733
  name: 'telemetry',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/embed/semantic.mjs — Semantic analysis for intake signals.
3
3
  *
4
- * Generates embeddings via @xenova/transformers, caches them to disk,
4
+ * Generates embeddings via @huggingface/transformers, caches them to disk,
5
5
  * computes cosine similarity, and clusters related signals by topic.
6
6
  * All local — no external API calls. Default model is all-MiniLM-L6-v2
7
7
  * (384-dimensional vectors, ~50MB on disk, fast inference).
@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
23
23
 
24
24
  const CACHE_DIR = join(homedir(), '.cx', 'cache', 'embeddings');
25
25
  const INDEX_FILE = join(CACHE_DIR, 'index.json');
26
- const MODEL_NAME = 'all-MiniLM-L6-v2';
26
+ const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
27
27
  const DIMENSIONS = 384;
28
28
  const LOG = process.env.CONSTRUCT_DEBUG_EMBED === '1';
29
29
 
@@ -35,7 +35,9 @@ let embedder = null;
35
35
  async function getEmbedder() {
36
36
  if (embedder !== null) return embedder;
37
37
  try {
38
- const { pipeline } = await import('@xenova/transformers');
38
+ const { pipeline, env: hfEnv } = await import('@huggingface/transformers');
39
+ hfEnv.allowLocalModels = true;
40
+ hfEnv.useBrowserCache = false;
39
41
  embedder = await pipeline('feature-extraction', MODEL_NAME, {
40
42
  quantized: true,
41
43
  });