@geraldmaron/construct 1.0.15 → 1.0.16

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 CHANGED
@@ -115,8 +115,8 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
115
115
  | `construct dev` | Start services for development |
116
116
  | `construct docs` | Documentation commands |
117
117
  | `construct doctor` | Check installation health |
118
- | `construct init` | Initialize project and start services |
119
- | `construct install` | Machine setup: install Docker, cm, and bootstrap config |
118
+ | `construct init` | Project setup (once per repo): scaffold .cx/, AGENTS.md, plan.md, adapters |
119
+ | `construct install` | Machine setup (once per machine): Docker, cm/cass, config, embeddings |
120
120
  | `construct intake` | View and process the active profile's intake queue (queue label varies by profile) |
121
121
  | `construct profile` | Manage the active org profile and its lifecycle (draft, promote, archive, health) |
122
122
  | `construct recommendations` | View and manage artifact recommendations |
@@ -129,11 +129,13 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
129
129
 
130
130
  | Command | What it does |
131
131
  |---|---|
132
+ | `construct ask` | One-shot ask against the active knowledge index |
132
133
  | `construct bootstrap` | Import seed observation corpus into local memory store for cold-start acceleration |
133
134
  | `construct customer` | Manage customer profiles for product intelligence |
134
135
  | `construct distill` | Distill documents with query-focused chunking |
135
136
  | `construct drop` | Ingest file from Downloads/Desktop |
136
137
  | `construct graph` | Task graph management |
138
+ | `construct handoffs` | List and inspect session handoff files in .cx/handoffs/ |
137
139
  | `construct headhunt` | Create domain expertise overlays |
138
140
  | `construct infer` | Infer schema from documents |
139
141
  | `construct ingest` | Convert documents to indexed markdown |
@@ -174,11 +176,14 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
174
176
  | `construct efficiency` | Show read efficiency, repeated files, and context-budget guidance |
175
177
  | `construct eval-datasets` | Sync scored Langfuse traces into eval datasets for prompt regression testing |
176
178
  | `construct evals` | Show evaluator catalog for prompt and agent experiments |
179
+ | `construct feedback:history` | Show recorded outcome ratings |
180
+ | `construct feedback:record` | Record an outcome rating for a recent specialist invocation |
177
181
  | `construct llm-judge` | Run LLM-as-a-judge evaluations on unscored traces for continuous quality feedback |
178
182
  | `construct optimize` | Prompt optimization using Langfuse trace quality scores |
179
183
  | `construct review` | Generate agent performance review from Langfuse trace backend |
180
184
  | `construct telemetry` | Query telemetry traces and latency data |
181
185
  | `construct telemetry-backfill` | Backfill sparse traces with observations (trace backend) |
186
+ | `construct telemetry-setup` | Configure Langfuse credentials and trace export |
182
187
 
183
188
  ### Diagnostics
184
189
 
@@ -187,6 +192,11 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
187
192
  | `construct audit` | Audit Construct internals and review the mutation trail |
188
193
  | `construct cleanup` | Release dev-agent memory pressure by cleaning stale helper and bridge processes |
189
194
  | `construct doc` | Verify or inspect auditability stamps on Construct-generated markdown files |
195
+ | `construct docs:check` | Check for missing how-to guides (alias for `docs check`) |
196
+ | `construct docs:reconcile` | Reconcile docs against the registry |
197
+ | `construct docs:site` | Manage the docs static site build |
198
+ | `construct docs:update` | Regenerate AUTO-managed doc regions (alias for `docs update`) |
199
+ | `construct docs:verify` | Validate documentation quality (alias for `docs verify`) |
190
200
 
191
201
  ### Advanced
192
202
 
@@ -195,6 +205,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
195
205
  | `construct auth:status` | Check auth status |
196
206
  | `construct backup` | System backups |
197
207
  | `construct beads` | Task queue management |
208
+ | `construct beads:stats` | Show beads counters and drift summary |
198
209
  | `construct ci` | Local CI mirror: run CI jobs locally or view recent run status |
199
210
  | `construct completions` | Shell completion scripts |
200
211
  | `construct config` | Deployment mode configuration |
@@ -206,6 +217,8 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
206
217
  | `construct policy` | Show active policy gates with enforcement details |
207
218
  | `construct provider` | Provider management |
208
219
  | `construct role` | Role framework management |
220
+ | `construct roles:list` | List installed role contracts |
221
+ | `construct roles:set` | Activate a role contract |
209
222
  | `construct scheduler` | Manage scheduled background jobs (tag-mining, doc-hygiene, skill-rollup) |
210
223
  | `construct skills` | Skill relevance detection |
211
224
  | `construct uninstall` | Remove Construct state |
package/bin/construct CHANGED
@@ -12,7 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { spawnSync } from 'node:child_process';
14
14
 
15
- import { CLI_COMMANDS, CLI_COMMANDS_BY_CATEGORY, CATEGORY_ORDER } from '../lib/cli-commands.mjs';
15
+ import { CLI_COMMANDS, CLI_COMMANDS_BY_CATEGORY, CATEGORY_ORDER, formatCommandHelp, isInternalCommand } from '../lib/cli-commands.mjs';
16
16
  import { buildStatus, formatStatusReport } from '../lib/status.mjs';
17
17
  import { validateRegistry } from '../lib/validator.mjs';
18
18
  import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig, applyToEnv, resetEnv, setTierModel, setModelWithTierInference } from '../lib/model-router.mjs';
@@ -104,73 +104,63 @@ function iconColumn(emoji) {
104
104
  return emoji + trailingSpaces;
105
105
  }
106
106
 
107
- function usage() {
108
- const showAll = restArgsCache.includes('--all') || restArgsCache.includes('-a');
109
- const commands = showAll ? CLI_COMMANDS : CLI_COMMANDS.filter(c => c.core);
110
-
107
+ function usage(opts = {}) {
108
+ const showAll = opts.all ?? (restArgsCache.includes('--all') || restArgsCache.includes('-a'));
109
+ // Always filter out internal commands; users can still run them directly,
110
+ // but --all is for "everything humans use" not "every handler in the map".
111
+ const visible = CLI_COMMANDS.filter((c) => !c.internal);
112
+ const commands = showAll ? visible : visible.filter((c) => c.core);
113
+
111
114
  if (showAll) {
112
115
  println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness (all commands)`);
113
116
  } else {
114
117
  println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
115
- println(`${COLORS.dim}Tip: Run ${COLORS.reset}construct --all${COLORS.dim} to see all commands${COLORS.reset}`);
118
+ println(`${COLORS.dim}Tip: Run ${COLORS.reset}construct --all${COLORS.dim} to see all commands · ${COLORS.reset}construct <cmd> --help${COLORS.dim} for details${COLORS.reset}`);
116
119
  }
117
-
120
+
118
121
  println(`\n${COLORS.dim}Usage:${COLORS.reset} construct <command> [options]\n`);
119
-
122
+
120
123
  for (const category of CATEGORY_ORDER) {
121
- const categoryCommands = commands.filter(c => c.category === category);
124
+ const categoryCommands = commands.filter((c) => c.category === category);
122
125
  if (!categoryCommands.length) continue;
123
126
  println(`${COLORS.bold}${category}${COLORS.reset}`);
124
127
  for (const command of categoryCommands) {
125
- println(` ${iconColumn(command.emoji)}${command.name.padEnd(14)} ${command.description}`);
128
+ const emoji = command.emoji ?? ' ';
129
+ println(` ${iconColumn(emoji)}${command.name.padEnd(14)} ${command.description}`);
126
130
  }
127
131
  println('');
128
132
  }
129
133
  }
130
134
 
131
- /** Show interactive context-aware menu when construct is run without arguments. */
135
+ // Bare-command landing show the same core-command listing as `construct
136
+ // --help`, prefixed with a context line so users know where they are, and
137
+ // suffixed with the one next-step suggestion that applies to their state.
138
+
132
139
  async function showInteractiveMenu() {
133
- const { loadProjectConfig } = await import('../lib/config/project-config.mjs');
134
140
  const { existsSync } = await import('node:fs');
135
141
  const { join } = await import('node:path');
136
-
142
+
137
143
  const projectRoot = process.cwd();
138
144
  const isConstructProject = existsSync(join(projectRoot, 'construct.config.json')) ||
139
145
  existsSync(join(projectRoot, '.cx'));
140
-
141
- println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
142
- println('');
143
-
146
+
144
147
  if (isConstructProject) {
145
148
  const projectName = process.env.CX_PROJECT_NAME || path.basename(projectRoot);
146
149
  println(`${COLORS.dim}Project:${COLORS.reset} ${projectName}`);
147
150
  println('');
148
151
  }
149
-
150
- println(`${COLORS.bold}Core Commands:${COLORS.reset}`);
151
- println(` ${COLORS.green}🚀 dev${COLORS.reset} Start services for development`);
152
- println(` ${COLORS.green}⏹ stop${COLORS.reset} Stop all running services`);
153
- println(` ${COLORS.green}📡 status${COLORS.reset} Show system health and credentials`);
154
- println(` ${COLORS.green}🏗️ init${COLORS.reset} Initialize project and start services`);
155
- println(` ${COLORS.green}🔄 sync${COLORS.reset} Sync agent adapters to AI tools`);
156
- println('');
157
- println(`${COLORS.dim}Run 'construct <command> --help' for command details${COLORS.reset}`);
158
- println(`${COLORS.dim}Run 'construct --all' to see all commands${COLORS.reset}`);
159
- println('');
160
-
161
- // Show context-aware suggestions
152
+
153
+ usage({ all: false });
154
+
162
155
  if (isConstructProject) {
163
156
  const { readDashboardState } = await import('../lib/service-manager.mjs');
164
157
  const dashboard = readDashboardState(HOME);
165
-
166
158
  if (!dashboard) {
167
- println(`${COLORS.yellow}💡 Services not running. Start with:${COLORS.reset}`);
168
- println(` ${COLORS.green}construct dev${COLORS.reset}`);
159
+ println(`${COLORS.yellow}💡 Services not running. Start with:${COLORS.reset} ${COLORS.green}construct dev${COLORS.reset}`);
169
160
  println('');
170
161
  }
171
162
  } else {
172
- println(`${COLORS.yellow}💡 Not a Construct project. Initialize with:${COLORS.reset}`);
173
- println(` ${COLORS.green}construct init${COLORS.reset}`);
163
+ println(`${COLORS.yellow}💡 Not a Construct project. Initialize with:${COLORS.reset} ${COLORS.green}construct init${COLORS.reset}`);
174
164
  println('');
175
165
  }
176
166
  }
@@ -506,35 +496,12 @@ async function cmdDoctor() {
506
496
  );
507
497
  }
508
498
 
509
- // Pre-push bypass frequency. Every honored CONSTRUCT_SKIP_PREPUSH /
510
- // CONSTRUCT_ALLOW_CLAUDE_PUSH / CONSTRUCT_SKIP_PR_LINT writes a JSONL
511
- // entry to ~/.construct/audit/prepush-bypass.log. Surfacing frequent
512
- // usage here is the early warning that the gate is the wrong shape —
513
- // the alternative is letting the bypass quietly become the default.
514
-
515
- const bypassLogPath = path.join(userHome, '.construct', 'audit', 'prepush-bypass.log');
516
- if (fs.existsSync(bypassLogPath)) {
517
- try {
518
- const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
519
- const recent = fs.readFileSync(bypassLogPath, 'utf8')
520
- .split('\n')
521
- .filter(Boolean)
522
- .map((line) => { try { return JSON.parse(line); } catch { return null; } })
523
- .filter((e) => e && Date.parse(e.ts) >= cutoff);
524
- const BYPASS_WARN_THRESHOLD = Number(process.env.CONSTRUCT_DOCTOR_BYPASS_WARN) || 3;
525
- if (recent.length >= BYPASS_WARN_THRESHOLD) {
526
- add(
527
- `Pre-push bypasses in last 7 days: ${recent.length} (gate may be wrong-sized — see ${bypassLogPath})`,
528
- false,
529
- true,
530
- );
531
- } else {
532
- add(`Pre-push bypasses in last 7 days: ${recent.length}`, true);
533
- }
534
- } catch { /* audit log read is best-effort */ }
535
- } else {
536
- add('Pre-push bypass log not present (no bypasses honored yet)', true);
537
- }
499
+ // (Pre-push bypass doctor check removed in v1.0.16.) v1.0.15 deleted the
500
+ // 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
504
+ // is harmless — next cleanup tick may sweep it.
538
505
 
539
506
  // Embed daemon log size. The daemon's own scheduler rotates every
540
507
  // minute at 50MB, but a freshly-upgraded install with a 34GB legacy
@@ -671,16 +638,12 @@ async function cmdDoctor() {
671
638
  path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code - Insiders'),
672
639
  ];
673
640
  if (vscodeAppSupportRoots.some((r) => fs.existsSync(r))) {
674
- // VS Code is installed but has no settings.json the user needs to create
675
- // one manually or run `construct sync` after VS Code sync support lands.
641
+ // VS Code installed check settings.json presence as a green/info signal.
642
+ // Absence is not a doctor finding VS Code adapter support hasn't landed
643
+ // yet, so "create one or wait for support" isn't actionable. The line is
644
+ // suppressed entirely when missing; doctor surfaces facts, not roadmap.
676
645
  const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
677
- add(
678
- vscodeSettingsOk
679
- ? 'VS Code settings file'
680
- : 'VS Code settings missing — create settings.json or wait for `construct sync` VS Code support',
681
- vscodeSettingsOk,
682
- true,
683
- );
646
+ if (vscodeSettingsOk) add('VS Code settings file', true, true);
684
647
  }
685
648
  add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
686
649
  add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
@@ -917,11 +880,15 @@ async function cmdDoctor() {
917
880
  }
918
881
 
919
882
  try {
920
- const { recentViolations } = await import('../lib/contracts/violation-log.mjs');
883
+ const { recentViolations, violationLogPath } = await import('../lib/contracts/violation-log.mjs');
921
884
  const violations = recentViolations({ windowMs: 24 * 60 * 60 * 1000 });
885
+ // Path is project-scoped (.cx/contract-violations.jsonl in cwd when
886
+ // inside a project, ~/.cx/ otherwise). Print the resolved one so
887
+ // users find the real file, not a stale ~/.cx/ reference.
888
+ const violationPath = violationLogPath();
922
889
  const label = violations.length === 0
923
890
  ? 'Contract violations (none in last 24h)'
924
- : `Contract violations (${violations.length} in last 24h — see ~/.cx/contract-violations.jsonl)`;
891
+ : `Contract violations (${violations.length} in last 24h — see ${violationPath})`;
925
892
  add(label, violations.length === 0, true);
926
893
  } catch {
927
894
  add('Contract violations', false, true);
@@ -2456,9 +2423,49 @@ async function cmdSkillsUsage(sub, args) {
2456
2423
  }
2457
2424
 
2458
2425
  if (sub === 'correlate-quality') {
2459
- println('Quality correlation requires Postgres (CONSTRUCT_DB_URL).');
2460
- println('Local JSONL does not carry cx_score data.');
2461
- println('Run with Postgres configured for full correlation output.');
2426
+ // Queries the construct_skill_quality_correlation view (db/schema/010_cx_scores.sql)
2427
+ // which aggregates cx_score values by session_id × skill_id over the past 90 days.
2428
+ // Falls back to a clear "no data" message when the DB is unreachable or the
2429
+ // view is empty rather than printing an empty table.
2430
+
2431
+ const sqlClient = createSqlClient(process.env);
2432
+ if (!sqlClient) {
2433
+ println('Quality correlation requires Postgres (DATABASE_URL or CONSTRUCT_DB_URL).');
2434
+ println('Local JSONL does not carry cx_score data.');
2435
+ println('Run with Postgres configured for full correlation output.');
2436
+ return;
2437
+ }
2438
+ try {
2439
+ const rows = await sqlClient`
2440
+ select skill_id, sessions, score_count, mean_score, median_score, p10_score, p90_score, last_scored_at
2441
+ from construct_skill_quality_correlation
2442
+ order by sessions desc, mean_score desc nulls last
2443
+ limit 50
2444
+ `;
2445
+ if (!rows || rows.length === 0) {
2446
+ println('No correlation data yet. The view aggregates cx_score values from the past 90 days against');
2447
+ println('skill invocations from the same sessions. Once cxScore() runs in a session that also invoked');
2448
+ println('a skill, rows appear here. Verify scoring with: construct telemetry query latency --limit=5');
2449
+ return;
2450
+ }
2451
+ const w = Math.max(15, ...rows.map((r) => String(r.skill_id || '').length));
2452
+ println(`${'skill_id'.padEnd(w)} sessions scores mean median p10 p90 last_scored_at`);
2453
+ println('-'.repeat(w + 70));
2454
+ for (const r of rows) {
2455
+ const pad = (v, n) => String(v ?? '').padStart(n);
2456
+ println(
2457
+ `${String(r.skill_id || '').padEnd(w)} ${pad(r.sessions, 8)} ${pad(r.score_count, 6)} ` +
2458
+ `${pad(r.mean_score, 5)} ${pad(r.median_score, 6)} ${pad(r.p10_score, 5)} ${pad(r.p90_score, 5)} ` +
2459
+ `${r.last_scored_at ? new Date(r.last_scored_at).toISOString().slice(0, 19) : ''}`,
2460
+ );
2461
+ }
2462
+ } catch (err) {
2463
+ errorln(`Quality correlation query failed: ${err.message}`);
2464
+ errorln('If the construct_skill_quality_correlation view is missing, apply db/schema/010_cx_scores.sql.');
2465
+ process.exit(1);
2466
+ } finally {
2467
+ await closeSqlClient(sqlClient);
2468
+ }
2462
2469
  return;
2463
2470
  }
2464
2471
 
@@ -2594,14 +2601,18 @@ async function cmdIntakeMetrics() {
2594
2601
  }
2595
2602
 
2596
2603
  async function cmdIntakeIntegrate(id, args, cwd) {
2597
- if (!id) { errorln('Usage: construct intake integrate <id> <github|jira|confluence>'); process.exit(1); }
2604
+ if (!id) { errorln('Usage: construct intake integrate <id> <github|jira|confluence> [--publish-issues]'); process.exit(1); }
2598
2605
 
2599
2606
  const system = args[0];
2600
2607
  if (!system || !['github', 'jira', 'confluence'].includes(system)) {
2601
- errorln('Usage: construct intake integrate <id> <github|jira|confluence>');
2608
+ errorln('Usage: construct intake integrate <id> <github|jira|confluence> [--publish-issues]');
2602
2609
  errorln('Available systems: github, jira, confluence');
2603
2610
  process.exit(1);
2604
2611
  }
2612
+ // --publish-issues unlocks the demo-source gate (CONSTRUCT_DEMO env or
2613
+ // packet sourcePath under tests/fixtures/ or .cx/intake/demo/). Required
2614
+ // before any demo/fixture intake can publish a real external ticket.
2615
+ const publishDemo = args.includes('--publish-issues');
2605
2616
 
2606
2617
  const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
2607
2618
  const queue = createIntakeQueue(cwd, process.env);
@@ -2613,10 +2624,13 @@ async function cmdIntakeIntegrate(id, args, cwd) {
2613
2624
  if (system === 'github') {
2614
2625
  const { createGitHubIssue, tagIntakeWithExternalRef } = await import('../lib/integrations/intake-integrations.mjs');
2615
2626
  info(`Creating GitHub issue from intake ${id}...`);
2616
- result = await createGitHubIssue(entry);
2627
+ result = await createGitHubIssue(entry, { publishDemo });
2617
2628
  if (result.ok) {
2618
2629
  ok(`GitHub issue created: ${result.externalUrl}`);
2619
2630
  tagIntakeWithExternalRef(cwd, id, 'github', result.externalUrl, result.externalId);
2631
+ } else if (result.skipped === 'demo-source') {
2632
+ warn(`Skipped: ${result.error}`);
2633
+ process.exit(0);
2620
2634
  } else {
2621
2635
  errorln(`Failed: ${result.error}`);
2622
2636
  process.exit(1);
@@ -5850,6 +5864,13 @@ if (command === '--help' || command === '-h' || command === 'help') {
5850
5864
  process.exit(0);
5851
5865
  }
5852
5866
 
5867
+ // --all / -a as a standalone flag — equivalent to `help --all`. Without
5868
+ // this, `construct --all` (which the help footer advertises) 404s.
5869
+ if (command === '--all' || command === '-a') {
5870
+ usage({ all: true });
5871
+ process.exit(0);
5872
+ }
5873
+
5853
5874
  // Version flags → print version and exit
5854
5875
  if (command === '--version' || command === '-V') {
5855
5876
  const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
@@ -5860,11 +5881,53 @@ if (command === '--version' || command === '-V') {
5860
5881
  const handler = handlers.get(command);
5861
5882
  if (!handler) {
5862
5883
  errorln(`Unknown command: ${command}`);
5884
+ // Three-tier suggestion: prefix match → substring match → Levenshtein
5885
+ // distance ≤ 2. Catches single-letter typos, missing-letter typos, and
5886
+ // transpositions ('sycn' → 'sync') without dragging in a full fuzzy lib.
5887
+ const known = CLI_COMMANDS.filter((c) => !c.internal).map((c) => c.name);
5888
+ const editDistance = (a, b) => {
5889
+ if (a === b) return 0;
5890
+ if (!a.length || !b.length) return Math.max(a.length, b.length);
5891
+ const row = Array(b.length + 1).fill(0).map((_, i) => i);
5892
+ for (let i = 1; i <= a.length; i++) {
5893
+ let prev = i;
5894
+ for (let j = 1; j <= b.length; j++) {
5895
+ const cur = a[i - 1] === b[j - 1]
5896
+ ? row[j - 1]
5897
+ : 1 + Math.min(row[j - 1], row[j], prev);
5898
+ row[j - 1] = prev;
5899
+ prev = cur;
5900
+ }
5901
+ row[b.length] = prev;
5902
+ }
5903
+ return row[b.length];
5904
+ };
5905
+ let suggestion = known.find((n) => n.startsWith(command)) ||
5906
+ known.find((n) => n.includes(command));
5907
+ if (!suggestion) {
5908
+ let bestDist = 3;
5909
+ for (const name of known) {
5910
+ const d = editDistance(command, name);
5911
+ if (d < bestDist) { bestDist = d; suggestion = name; }
5912
+ }
5913
+ }
5914
+ if (suggestion) errorln(`Did you mean: construct ${suggestion}?`);
5863
5915
  println('');
5864
5916
  println(`${COLORS.dim}Run 'construct --help' for available commands${COLORS.reset}`);
5865
5917
  process.exit(1);
5866
5918
  }
5867
5919
 
5920
+ // Per-command --help / -h intercept. Critical safety: without this guard,
5921
+ // commands like `dev`, `stop`, `init`, `uninstall` silently ignore --help
5922
+ // and execute (running `construct stop --help` actually stops services).
5923
+ // Every command — including internal ones — gets the same shape via
5924
+ // formatCommandHelp(); per-command handlers no longer need to opt in.
5925
+
5926
+ if (rest.includes('--help') || rest.includes('-h')) {
5927
+ process.stdout.write(formatCommandHelp(command, { colors: true }));
5928
+ process.exit(0);
5929
+ }
5930
+
5868
5931
  // Self-maintenance on version change. Skipped for high-frequency hook calls
5869
5932
  // (the harness invokes `construct hook <name>` many times per session and
5870
5933
  // must stay fast). When the installed version differs from the stamp at
@@ -0,0 +1,51 @@
1
+ -- db/schema/010_cx_scores.sql — Per-trace quality score log + skill quality correlation view.
2
+ --
3
+ -- Backs `construct skills correlate-quality`. Producer: lib/mcp/tools/telemetry.mjs
4
+ -- cxScore() writes a row here when CONSTRUCT_DB_URL (or DATABASE_URL) is set.
5
+ -- Local JSONL/observation paths remain primary in solo mode; this table is
6
+ -- primary in team/enterprise mode where multiple agents share a session
7
+ -- timeline and the dashboard reads aggregate correlations.
8
+
9
+ create table if not exists construct_cx_scores (
10
+ id bigserial primary key,
11
+ ts timestamptz not null,
12
+ trace_id text not null,
13
+ session_id text,
14
+ agent_id text,
15
+ name text not null default 'quality',
16
+ value numeric not null,
17
+ comment text
18
+ );
19
+
20
+ create index if not exists construct_cx_scores_trace_idx
21
+ on construct_cx_scores (trace_id);
22
+
23
+ create index if not exists construct_cx_scores_session_ts_idx
24
+ on construct_cx_scores (session_id, ts desc);
25
+
26
+ create index if not exists construct_cx_scores_agent_ts_idx
27
+ on construct_cx_scores (agent_id, ts desc);
28
+
29
+ -- Skill quality correlation view: joins skill invocations and quality
30
+ -- scores by session_id so a reader can ask "for sessions where skill X was
31
+ -- invoked, what's the median / mean / p10 quality score?" The view is a
32
+ -- simple aggregate over the past 90 days; queries that need a longer
33
+ -- window can pull straight from the underlying tables. Materialised view
34
+ -- not used here because the correlation surface is small and the freshness
35
+ -- expectation is "current session" not "yesterday's batch."
36
+
37
+ create or replace view construct_skill_quality_correlation as
38
+ select
39
+ inv.skill_id,
40
+ count(distinct inv.session_id) as sessions,
41
+ count(distinct sc.id) as score_count,
42
+ round(avg(sc.value)::numeric, 3) as mean_score,
43
+ round((percentile_cont(0.5) within group (order by sc.value))::numeric, 3) as median_score,
44
+ round((percentile_cont(0.10) within group (order by sc.value))::numeric, 3) as p10_score,
45
+ round((percentile_cont(0.90) within group (order by sc.value))::numeric, 3) as p90_score,
46
+ max(sc.ts) as last_scored_at
47
+ from construct_skill_invocations inv
48
+ inner join construct_cx_scores sc on sc.session_id = inv.session_id
49
+ where inv.ts > now() - interval '90 days'
50
+ and sc.ts > now() - interval '90 days'
51
+ group by inv.skill_id;