@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 +15 -2
- package/bin/construct +144 -81
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/cli-commands.mjs +280 -146
- package/lib/contracts/validate.mjs +20 -10
- package/lib/contracts/violation-log.mjs +27 -12
- package/lib/hooks/stop-notify.mjs +7 -0
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/prepare.mjs +22 -4
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/roles/gateway.mjs +30 -1
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/insights.mjs +12 -0
- package/lib/setup.mjs +54 -9
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/package.json +1 -1
- package/personas/construct.md +2 -0
- package/platforms/claude/CLAUDE.md +43 -15
- package/scripts/sync-specialists.mjs +20 -2
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` |
|
|
119
|
-
| `construct install` | Machine setup:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
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
|
|
675
|
-
//
|
|
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
|
|
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
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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;
|