@geraldmaron/construct 1.0.23 → 1.1.0
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 +0 -2
- package/bin/construct +189 -207
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +5 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +37 -10
- package/lib/document-ingest.mjs +90 -5
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +12 -8
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reconcile/index.mjs +0 -2
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/service-manager.mjs +79 -259
- package/lib/setup.mjs +44 -425
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/personas/construct.md +1 -1
- package/platforms/capabilities.json +76 -0
- package/platforms/claude/settings.template.json +0 -7
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +174 -40
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/prompts/cx-test-automation.md +12 -0
- package/specialists/registry.json +0 -8
- package/templates/docs/construct_guide.md +1 -1
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/reconcile/postgres-namespace.mjs +0 -102
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/unified-storage.mjs +0 -550
- package/lib/storage/vector-store.mjs +0 -100
package/README.md
CHANGED
|
@@ -269,7 +269,6 @@ construct/
|
|
|
269
269
|
├── commands Command prompt assets
|
|
270
270
|
├── config
|
|
271
271
|
├── dashboard
|
|
272
|
-
├── db
|
|
273
272
|
├── deploy
|
|
274
273
|
├── docs Architecture notes, runbooks, and documentation contract
|
|
275
274
|
├── examples
|
|
@@ -282,7 +281,6 @@ construct/
|
|
|
282
281
|
├── rules Coding and quality standards
|
|
283
282
|
├── schemas
|
|
284
283
|
├── scripts
|
|
285
|
-
├── services
|
|
286
284
|
├── skills Reusable domain knowledge files
|
|
287
285
|
├── specialists
|
|
288
286
|
├── templates
|
package/bin/construct
CHANGED
|
@@ -67,9 +67,7 @@ import { readCostLog, summarizeCostData, formatCostReport, clearCostLog } from '
|
|
|
67
67
|
import { readEfficiencyLog, summarizeEfficiencyData, formatEfficiencyReport } from '../lib/efficiency.mjs';
|
|
68
68
|
import { resolveColors } from '../lib/term-format.mjs';
|
|
69
69
|
import { buildHybridSearchResultsAsync } from '../lib/storage/hybrid-query.mjs';
|
|
70
|
-
import { createSqlClient, closeSqlClient
|
|
71
|
-
import { describeVectorStore } from '../lib/storage/vector-store.mjs';
|
|
72
|
-
import { describeSqlStore } from '../lib/storage/sql-store.mjs';
|
|
70
|
+
import { createSqlClient, closeSqlClient } from '../lib/storage/backend.mjs';
|
|
73
71
|
import { syncFileStateToSql } from '../lib/storage/sync.mjs';
|
|
74
72
|
import { deleteIngestedArtifacts, getStorageStatus, inferProjectName, resetStorage } from '../lib/storage/admin.mjs';
|
|
75
73
|
import { runPressureRelease } from '../lib/runtime-pressure.mjs';
|
|
@@ -171,6 +169,44 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}, { exitOnError = tru
|
|
|
171
169
|
return status;
|
|
172
170
|
}
|
|
173
171
|
|
|
172
|
+
async function cmdRegistryStatus(args = []) {
|
|
173
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
174
|
+
const { join } = await import('node:path');
|
|
175
|
+
const matrixPath = join(ROOT_DIR, 'tests', 'registry', 'capability-matrix.json');
|
|
176
|
+
|
|
177
|
+
if (!existsSync(matrixPath)) {
|
|
178
|
+
errorln('Capability matrix not found. Run "construct sync" or ensure tests/registry/capability-matrix.json exists.');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { capabilities } = JSON.parse(readFileSync(matrixPath, 'utf8'));
|
|
183
|
+
const jsonOutput = args.includes('--json');
|
|
184
|
+
|
|
185
|
+
if (jsonOutput) {
|
|
186
|
+
println(JSON.stringify(capabilities, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
println(`${COLORS.bold}Workflow & Surface Capability Registry${COLORS.reset}`);
|
|
191
|
+
println('='.repeat(40));
|
|
192
|
+
println('');
|
|
193
|
+
|
|
194
|
+
for (const cap of capabilities) {
|
|
195
|
+
const tierColor = cap.criticality === 'P0' ? COLORS.red : cap.criticality === 'P1' ? COLORS.yellow : COLORS.blue;
|
|
196
|
+
println(`${tierColor}[${cap.criticality}]${COLORS.reset} ${COLORS.bold}${cap.name}${COLORS.reset} (${cap.id})`);
|
|
197
|
+
println(` ${COLORS.dim}${cap.description}${COLORS.reset}`);
|
|
198
|
+
|
|
199
|
+
const surfaces = Object.entries(cap.surfaces);
|
|
200
|
+
for (const [name, status] of surfaces) {
|
|
201
|
+
const icon = status.quality_score === null ? '⚪' : status.quality_score >= 0.9 ? '🟢' : status.quality_score >= 0.7 ? '🟡' : '🔴';
|
|
202
|
+
const score = status.quality_score !== null ? `${(status.quality_score * 100).toFixed(0)}%` : 'N/A';
|
|
203
|
+
const date = status.last_validated ? new Date(status.last_validated).toLocaleDateString() : 'never';
|
|
204
|
+
println(` ${icon} ${name.padEnd(10)} | Score: ${score.padEnd(5)} | Validated: ${date}`);
|
|
205
|
+
}
|
|
206
|
+
println('');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
async function cmdStatus() {
|
|
175
211
|
const jsonOutput = restArgsCache.includes('--json');
|
|
176
212
|
const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
|
|
@@ -822,35 +858,10 @@ async function cmdDoctor() {
|
|
|
822
858
|
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
823
859
|
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
824
860
|
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
let sqlReachable = false;
|
|
830
|
-
if (sqlConfig.configured && sqlClient) {
|
|
831
|
-
try {
|
|
832
|
-
await sqlClient`select 1 as ok`;
|
|
833
|
-
sqlReachable = true;
|
|
834
|
-
add('SQL backend reachable', true);
|
|
835
|
-
} catch {
|
|
836
|
-
// In solo mode the local vector index is the active storage path, so an
|
|
837
|
-
// unreachable Postgres is expected unless the user has started it. Emit a
|
|
838
|
-
// contextual message rather than a bare ⚠ so the fix is obvious.
|
|
839
|
-
const { getDeploymentMode } = await import('../lib/deployment-mode.mjs');
|
|
840
|
-
const mode = getDeploymentMode(process.env) ?? 'solo';
|
|
841
|
-
const hint = mode === 'solo'
|
|
842
|
-
? ' (solo mode — run `construct dev` to start Postgres)'
|
|
843
|
-
: ' — check DATABASE_URL and that the server is running';
|
|
844
|
-
add(`SQL backend unreachable${hint}`, false, true);
|
|
845
|
-
} finally {
|
|
846
|
-
await closeSqlClient(sqlClient);
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
add('SQL backend configured', sqlConfig.fallbackAvailable, true);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const vectorConfig = describeVectorStore(process.env);
|
|
853
|
-
add('Vector backend configured', Boolean(vectorConfig.endpoint || vectorConfig.indexPath || vectorConfig.mode === 'file'));
|
|
861
|
+
const { VectorClient } = await import('../lib/storage/vector-client.mjs');
|
|
862
|
+
const vectorClient = new VectorClient({ env: process.env });
|
|
863
|
+
const vectorOk = await vectorClient.isHealthy();
|
|
864
|
+
add('Vector backend (LanceDB) reachable', vectorOk);
|
|
854
865
|
|
|
855
866
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
856
867
|
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
@@ -979,35 +990,6 @@ async function cmdDoctor() {
|
|
|
979
990
|
add('Observation size', false, true);
|
|
980
991
|
}
|
|
981
992
|
|
|
982
|
-
// Only run the schema migration check when SQL is both configured AND
|
|
983
|
-
// currently reachable. If the SQL check above already failed (sqlReachable
|
|
984
|
-
// is false) this check will also fail with the same ECONNREFUSED and would
|
|
985
|
-
// produce a redundant second warning about the same offline server.
|
|
986
|
-
if (sqlConfig.configured && sqlReachable) {
|
|
987
|
-
try {
|
|
988
|
-
const migClient = createSqlClient(process.env);
|
|
989
|
-
if (migClient) {
|
|
990
|
-
const { describeMigrations } = await import('../lib/storage/migrations.mjs');
|
|
991
|
-
const m = await describeMigrations(migClient);
|
|
992
|
-
await closeSqlClient(migClient);
|
|
993
|
-
const ok = !!m.ok && m.drift.length === 0;
|
|
994
|
-
let label = 'Schema migrations applied + no drift';
|
|
995
|
-
if (!ok && m.drift && m.drift.length > 0) {
|
|
996
|
-
const repairable = m.drift.filter((d) => d.idempotent).length;
|
|
997
|
-
const notRepairable = m.drift.length - repairable;
|
|
998
|
-
if (notRepairable === 0) {
|
|
999
|
-
label = `Schema migrations: ${m.drift.length} drifted (all idempotent — run \`construct storage repair-migrations --yes\`)`;
|
|
1000
|
-
} else {
|
|
1001
|
-
label = `Schema migrations: ${m.drift.length} drifted (${notRepairable} non-idempotent — write a new migration file)`;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
add(label, ok, true);
|
|
1005
|
-
}
|
|
1006
|
-
} catch {
|
|
1007
|
-
add('Schema migrations applied + no drift', false, true);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
993
|
try {
|
|
1012
994
|
const { detectBeadsDrift } = await import('../lib/beads/drift.mjs');
|
|
1013
995
|
const drift = detectBeadsDrift();
|
|
@@ -1037,24 +1019,6 @@ async function cmdDoctor() {
|
|
|
1037
1019
|
add('cm available', false, true);
|
|
1038
1020
|
}
|
|
1039
1021
|
|
|
1040
|
-
try {
|
|
1041
|
-
const dockerCli = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['docker'], { stdio: 'pipe' });
|
|
1042
|
-
let dockerOk = false;
|
|
1043
|
-
let dockerNote = 'Docker not installed (optional — managed Postgres needs it)';
|
|
1044
|
-
if (dockerCli.status === 0) {
|
|
1045
|
-
const dockerInfo = spawnSync('docker', ['info'], { stdio: 'pipe' });
|
|
1046
|
-
if (dockerInfo.status === 0) {
|
|
1047
|
-
dockerOk = true;
|
|
1048
|
-
dockerNote = 'Docker daemon reachable';
|
|
1049
|
-
} else {
|
|
1050
|
-
dockerNote = 'Docker CLI installed but daemon not reachable — run `construct up` on macOS to auto-start, or start Docker manually';
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
add(dockerOk ? 'Docker daemon reachable' : `Docker daemon reachable (${dockerNote})`, dockerOk, true);
|
|
1054
|
-
} catch {
|
|
1055
|
-
add('Docker daemon reachable', false, true);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
1022
|
try {
|
|
1059
1023
|
const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
|
|
1060
1024
|
if (fs.existsSync(failuresPath)) {
|
|
@@ -1125,6 +1089,69 @@ async function cmdDoctor() {
|
|
|
1125
1089
|
add(`Reconciliation drift check failed: ${err.message}`, false, true);
|
|
1126
1090
|
}
|
|
1127
1091
|
|
|
1092
|
+
// Docling document-extraction runtime (Python venv via uv). Provisioned lazily
|
|
1093
|
+
// on first document ingest, or eagerly via `construct install --with-docling`.
|
|
1094
|
+
// Advisory: absence is fine until a document is ingested.
|
|
1095
|
+
try {
|
|
1096
|
+
const { describeDoclingRuntime } = await import('../lib/runtime/uv-bootstrap.mjs');
|
|
1097
|
+
const docling = describeDoclingRuntime();
|
|
1098
|
+
if (docling.available) {
|
|
1099
|
+
add(`Docling runtime ready (docling ${docling.marker?.doclingVersion ?? '?'})`, true, true);
|
|
1100
|
+
} else {
|
|
1101
|
+
add('Docling runtime not provisioned (`construct install --with-docling`, or auto on first ingest)', false, true);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
add(`Docling runtime check failed: ${err.message}`, false, true);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Capability honesty without re-probing: if the OpenCode default model is one the
|
|
1108
|
+
// probe recorded as COLLAPSED (and its digest still matches), flag it at health
|
|
1109
|
+
// time. Reads the persisted store only — the slow probe stays behind --probe-local.
|
|
1110
|
+
try {
|
|
1111
|
+
const { readOpenCodeConfig } = await import('../lib/opencode-config.mjs');
|
|
1112
|
+
const { isKnownCollapsed } = await import('../lib/ollama/capability-store.mjs');
|
|
1113
|
+
const { modelDigest } = await import('../lib/ollama/provision-context.mjs');
|
|
1114
|
+
const { config } = readOpenCodeConfig();
|
|
1115
|
+
const defaultModel = (config?.model || config?.defaultModel || '').replace(/^ollama\//, '');
|
|
1116
|
+
if (defaultModel && isKnownCollapsed(defaultModel, modelDigest(defaultModel))) {
|
|
1117
|
+
add(`Default local model ${defaultModel} probed COLLAPSED — not agentic-capable (re-verify: construct doctor --probe-local)`, false, true);
|
|
1118
|
+
}
|
|
1119
|
+
} catch { /* advisory */ }
|
|
1120
|
+
|
|
1121
|
+
// Opt-in agentic-coherence probe for local models (`construct doctor --probe-local`).
|
|
1122
|
+
// Loads each registered Ollama model and measures repetition collapse on an agentic
|
|
1123
|
+
// prompt, so it is gated behind the flag. COLLAPSED models produce word salad no
|
|
1124
|
+
// matter how context and tools are tuned (capability is not predictable from size).
|
|
1125
|
+
if (args.includes('--probe-local')) {
|
|
1126
|
+
try {
|
|
1127
|
+
const { ollamaAvailable, listModels, probeAgenticCoherence, probeStreamingToolCallLeak, modelDigest } = await import('../lib/ollama/provision-context.mjs');
|
|
1128
|
+
const { recordProbeResult } = await import('../lib/ollama/capability-store.mjs');
|
|
1129
|
+
// --probe-stream additionally drives a streaming turn and flags a tool-call
|
|
1130
|
+
// leaked into the text channel (`<function=…>`) — the artifact the buffered
|
|
1131
|
+
// probe cannot reproduce. It reloads each model, so it stays opt-in.
|
|
1132
|
+
const probeStream = args.includes('--probe-stream');
|
|
1133
|
+
if (!ollamaAvailable()) {
|
|
1134
|
+
add('Local-model probe: Ollama not available', false, true);
|
|
1135
|
+
} else {
|
|
1136
|
+
const models = listModels().filter((m) => !/embed/i.test(m));
|
|
1137
|
+
for (const m of models) {
|
|
1138
|
+
const r = await probeAgenticCoherence(m);
|
|
1139
|
+
if (r.ok) recordProbeResult(m, r, modelDigest(m));
|
|
1140
|
+
const verdict = r.ok ? (r.coherent ? `COHERENT (repeat ${r.repeatRatio})` : `COLLAPSED (repeat ${r.repeatRatio})`) : `error: ${r.reason}`;
|
|
1141
|
+
add(`Local probe ${m}: ${verdict}`, r.ok && r.coherent, true);
|
|
1142
|
+
if (probeStream && r.ok && r.coherent) {
|
|
1143
|
+
const s = await probeStreamingToolCallLeak(m);
|
|
1144
|
+
if (s.ok) add(`Stream probe ${m}: ${s.leaked ? `TOOL-CALL LEAK (${s.marker})` : 'clean (native tool_calls)'}`, !s.leaked, true);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
add('Quantization: prefer Q4_K_M+ — low quants are a known word-salad source', true, true);
|
|
1148
|
+
add('Verified agentic-capable locals: qwen3-coder:32k, devstral:24b', true, true);
|
|
1149
|
+
}
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
add(`Local-model probe failed: ${err.message}`, false, true);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1128
1155
|
println('Construct Health Check');
|
|
1129
1156
|
println('══════════════════════');
|
|
1130
1157
|
println('');
|
|
@@ -1569,49 +1596,11 @@ async function cmdStorage(args) {
|
|
|
1569
1596
|
println(`Reconciled observations: re-embedded ${result.reembedded} of ${result.checked} (model ${result.model}).`);
|
|
1570
1597
|
return;
|
|
1571
1598
|
}
|
|
1572
|
-
if (action === 'migrations') {
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
const report = await describeMigrations(sqlClient);
|
|
1578
|
-
println(JSON.stringify(report, null, 2));
|
|
1579
|
-
if (report.drift?.length > 0) process.exit(2);
|
|
1580
|
-
} finally {
|
|
1581
|
-
await closeSqlClient(sqlClient);
|
|
1582
|
-
}
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
if (action === 'repair-migrations') {
|
|
1586
|
-
const confirm = rest.includes('--yes');
|
|
1587
|
-
if (!confirm) {
|
|
1588
|
-
errorln('This re-applies drifted migrations (idempotent only) and updates their recorded SHA.');
|
|
1589
|
-
errorln('Destructive migrations (DROP / TRUNCATE / ALTER…DROP / DELETE) are refused.');
|
|
1590
|
-
errorln('Re-run with --yes to proceed.');
|
|
1591
|
-
process.exit(1);
|
|
1592
|
-
}
|
|
1593
|
-
const { runMigrations } = await import('../lib/storage/migrations.mjs');
|
|
1594
|
-
const sqlClient = createSqlClient(process.env);
|
|
1595
|
-
if (!sqlClient) { errorln('No DATABASE_URL configured.'); process.exit(1); }
|
|
1596
|
-
try {
|
|
1597
|
-
const result = await runMigrations(sqlClient, { repair: true });
|
|
1598
|
-
println(JSON.stringify({
|
|
1599
|
-
applied: result.applied,
|
|
1600
|
-
repaired: result.repaired,
|
|
1601
|
-
skipped: result.skipped,
|
|
1602
|
-
drift: result.drift,
|
|
1603
|
-
}, null, 2));
|
|
1604
|
-
if (result.drift.length > 0) {
|
|
1605
|
-
errorln('');
|
|
1606
|
-
errorln('Refused to repair non-idempotent migrations:');
|
|
1607
|
-
for (const d of result.drift) errorln(` ${d.filename}`);
|
|
1608
|
-
process.exit(2);
|
|
1609
|
-
}
|
|
1610
|
-
info(`Repaired ${result.repaired.length} migration(s); applied ${result.applied.length} new.`);
|
|
1611
|
-
} finally {
|
|
1612
|
-
await closeSqlClient(sqlClient);
|
|
1613
|
-
}
|
|
1614
|
-
return;
|
|
1599
|
+
if (action === 'migrations' || action === 'repair-migrations') {
|
|
1600
|
+
// Postgres schema migrations were removed with the SQL backend; LanceDB
|
|
1601
|
+
// tables are created on demand and need no migration runner.
|
|
1602
|
+
errorln('construct storage ' + action + ' is no longer supported: Postgres was removed.');
|
|
1603
|
+
process.exit(1);
|
|
1615
1604
|
}
|
|
1616
1605
|
if (action === 'sync') {
|
|
1617
1606
|
const result = await syncFileStateToSql(process.cwd(), { env: process.env, project: inferProjectName(process.cwd()) });
|
|
@@ -3543,6 +3532,7 @@ async function cmdOrchestrate(args) {
|
|
|
3543
3532
|
host: flag('--host'),
|
|
3544
3533
|
hostModel: flag('--host-model'),
|
|
3545
3534
|
hostProvider: flag('--host-provider'),
|
|
3535
|
+
workerBackend: flag('--worker-backend'),
|
|
3546
3536
|
fileCount: Number(flag('--file-count') || 0),
|
|
3547
3537
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3548
3538
|
};
|
|
@@ -4242,15 +4232,29 @@ async function cmdMemory(args) {
|
|
|
4242
4232
|
const { getEngine } = await import('../lib/engine/index.mjs');
|
|
4243
4233
|
const engine = await getEngine({ rootDir: process.cwd() });
|
|
4244
4234
|
const summariser = engine.layers.compressor;
|
|
4235
|
+
|
|
4236
|
+
// The judge resolves the value-swap contradictions the heuristic abstains on
|
|
4237
|
+
// when a local model is available; null offline, so consolidation runs the
|
|
4238
|
+
// same with heuristic-only.
|
|
4239
|
+
const { createContradictionJudge } = await import('../lib/engine/contradiction-judge.mjs');
|
|
4240
|
+
const contradictionJudge = createContradictionJudge();
|
|
4245
4241
|
const thresholdArg = args.find((a) => a.startsWith('--threshold='));
|
|
4246
4242
|
const archiveDaysArg = args.find((a) => a.startsWith('--archive-days='));
|
|
4247
|
-
const opts = { summariser };
|
|
4243
|
+
const opts = { summariser, contradictionJudge };
|
|
4248
4244
|
if (thresholdArg) opts.similarityThreshold = Number(thresholdArg.split('=')[1]);
|
|
4249
4245
|
if (archiveDaysArg) opts.archiveAfterDays = Number(archiveDaysArg.split('=')[1]);
|
|
4246
|
+
if (args.includes('--no-supersede')) opts.supersedeDuplicates = false;
|
|
4247
|
+
if (args.includes('--no-contradictions')) opts.detectContradictions = false;
|
|
4250
4248
|
const result = await consolidate(process.cwd(), opts);
|
|
4249
|
+
const superseded = result.superseded ?? [];
|
|
4250
|
+
const contradictions = superseded.filter((s) => s.reason === 'contradiction').length;
|
|
4251
|
+
const restatements = superseded.length - contradictions;
|
|
4252
|
+
const agedOut = result.archived.length - superseded.length;
|
|
4251
4253
|
process.stdout.write(
|
|
4252
4254
|
`consolidate: ${result.clustersBefore} observations → ${result.clusters} insights, ` +
|
|
4253
|
-
`${
|
|
4255
|
+
`${restatements} restated, ${contradictions} contradicted, ${agedOut} aged-out, ` +
|
|
4256
|
+
`${result.archivePruned ?? 0} pruned from archive` +
|
|
4257
|
+
`${result.contradictionScanSkipped ? ' (contradiction scan skipped: store too large)' : ''}\n`
|
|
4254
4258
|
);
|
|
4255
4259
|
return;
|
|
4256
4260
|
}
|
|
@@ -4565,100 +4569,64 @@ async function cmdLintContracts() {
|
|
|
4565
4569
|
process.exit(1);
|
|
4566
4570
|
}
|
|
4567
4571
|
|
|
4568
|
-
async function
|
|
4569
|
-
const
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
4575
|
-
const keep = keepArg
|
|
4576
|
-
? Number(keepArg.split('=')[1])
|
|
4577
|
-
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
4578
|
-
const { createBackup, pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
4579
|
-
println('Creating backup…');
|
|
4580
|
-
try {
|
|
4581
|
-
const result = await createBackup({ includeSecrets });
|
|
4582
|
-
ok(`Backup created: ${result.path}`);
|
|
4583
|
-
println(` Contents: ${result.manifest.contents.join(', ') || '(empty)'}`);
|
|
4584
|
-
if (!includeSecrets) println(' Secrets redacted. Use --include-secrets to include them.');
|
|
4585
|
-
if (!noPrune) {
|
|
4586
|
-
const pruned = pruneBackups({ keep });
|
|
4587
|
-
if (pruned.removed.length) {
|
|
4588
|
-
println(` Auto-pruned ${pruned.removed.length} old backup(s); kept ${pruned.kept}.`);
|
|
4589
|
-
}
|
|
4590
|
-
}
|
|
4591
|
-
} catch (err) {
|
|
4592
|
-
errorln(`Backup failed: ${err.message}`);
|
|
4593
|
-
process.exit(1);
|
|
4594
|
-
}
|
|
4572
|
+
async function cmdLintPrompts() {
|
|
4573
|
+
const { validatePromptFiles } = await import('../lib/specialists/prompt-schema.mjs');
|
|
4574
|
+
const { errors, warnings, total, converted } = validatePromptFiles({ rootDir: ROOT_DIR });
|
|
4575
|
+
for (const w of warnings) console.warn(` warn ${w}`);
|
|
4576
|
+
if (errors.length === 0) {
|
|
4577
|
+
console.log(`specialist prompts: ${total} files (${converted} converted), 0 errors, ${warnings.length} warning(s)`);
|
|
4595
4578
|
return;
|
|
4596
4579
|
}
|
|
4580
|
+
console.error(`specialist prompts: ${errors.length} error(s) across ${total} files`);
|
|
4581
|
+
for (const err of errors) console.error(` ${err}`);
|
|
4582
|
+
process.exit(1);
|
|
4583
|
+
}
|
|
4597
4584
|
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
? Number(keepArg.split('=')[1])
|
|
4602
|
-
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
4603
|
-
const { pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
4604
|
-
const result = pruneBackups({ keep });
|
|
4605
|
-
if (result.removed.length === 0) {
|
|
4606
|
-
println(`Nothing to prune (have ${result.kept}, keeping ${keep}).`);
|
|
4607
|
-
} else {
|
|
4608
|
-
ok(`Pruned ${result.removed.length} backup(s); kept ${result.kept}.`);
|
|
4609
|
-
for (const r of result.removed) println(` removed ${r}`);
|
|
4610
|
-
}
|
|
4611
|
-
return;
|
|
4612
|
-
}
|
|
4585
|
+
async function cmdSpecialist(args) {
|
|
4586
|
+
const sub = args[0];
|
|
4587
|
+
if (sub === 'lint') return cmdLintPrompts();
|
|
4613
4588
|
|
|
4614
|
-
if (sub === '
|
|
4615
|
-
const
|
|
4616
|
-
if (!
|
|
4617
|
-
const {
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
}
|
|
4622
|
-
errorln(`Backup verification failed:\n ${result.errors.join('\n ')}`);
|
|
4623
|
-
process.exit(1);
|
|
4624
|
-
}
|
|
4589
|
+
if (sub === 'create') {
|
|
4590
|
+
const role = args[1];
|
|
4591
|
+
if (!role) { errorln('Usage: construct specialist create <role>'); process.exit(1); }
|
|
4592
|
+
const { createSpecialistDraft } = await import('../lib/specialists/scaffold.mjs');
|
|
4593
|
+
try {
|
|
4594
|
+
const { relPath } = createSpecialistDraft({ rootDir: ROOT_DIR, role });
|
|
4595
|
+
println(`Created ${relPath} — fill in the stubbed sections, add it to specialists/registry.json, then run \`construct specialist lint\`.`);
|
|
4596
|
+
} catch (e) { errorln(e.message); process.exit(1); }
|
|
4625
4597
|
return;
|
|
4626
4598
|
}
|
|
4627
4599
|
|
|
4628
|
-
if (sub === '
|
|
4629
|
-
const
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
4637
|
-
}
|
|
4638
|
-
const { restoreBackup } = await import('../lib/storage/backup.mjs');
|
|
4639
|
-
const result = await restoreBackup(archivePath, { yes });
|
|
4640
|
-
if (result.ok) {
|
|
4641
|
-
ok(`Restored: ${result.restored.join(', ')}`);
|
|
4642
|
-
} else {
|
|
4643
|
-
if (result.restored.length) println(`Partially restored: ${result.restored.join(', ')}`);
|
|
4644
|
-
errorln(`Errors:\n ${result.errors.join('\n ')}`);
|
|
4645
|
-
process.exit(1);
|
|
4600
|
+
if (sub === 'edit') {
|
|
4601
|
+
const role = args[1];
|
|
4602
|
+
if (!role) { errorln('Usage: construct specialist edit <role> [--set-bias=… --set-tension=… --add-overlay=… --bump-version]'); process.exit(1); }
|
|
4603
|
+
const { editSpecialistFrontmatter } = await import('../lib/specialists/scaffold.mjs');
|
|
4604
|
+
const setPerspective = {};
|
|
4605
|
+
for (const f of ['bias', 'tension', 'openingQuestion', 'failureMode']) {
|
|
4606
|
+
const a = args.find((x) => x.startsWith(`--set-${f}=`));
|
|
4607
|
+
if (a) setPerspective[f] = a.slice(`--set-${f}=`.length);
|
|
4646
4608
|
}
|
|
4609
|
+
const overlayArg = args.find((x) => x.startsWith('--add-overlay='));
|
|
4610
|
+
try {
|
|
4611
|
+
const { relPath } = editSpecialistFrontmatter({
|
|
4612
|
+
rootDir: ROOT_DIR, role, setPerspective,
|
|
4613
|
+
addOverlay: overlayArg ? overlayArg.slice('--add-overlay='.length) : undefined,
|
|
4614
|
+
bumpVersion: args.includes('--bump-version'),
|
|
4615
|
+
});
|
|
4616
|
+
println(`Updated ${relPath || `specialists/prompts/cx-${role}.md`} — run \`construct specialist lint\` to confirm.`);
|
|
4617
|
+
} catch (e) { errorln(e.message); process.exit(1); }
|
|
4647
4618
|
return;
|
|
4648
4619
|
}
|
|
4649
4620
|
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
if (!backups.length) { println('No backups found.'); return; }
|
|
4654
|
-
for (const b of backups) {
|
|
4655
|
-
const mb = (b.size / 1024 / 1024).toFixed(1);
|
|
4656
|
-
println(` ${b.name} (${mb} MB) ${b.mtime.toISOString().slice(0, 19)}`);
|
|
4657
|
-
}
|
|
4658
|
-
return;
|
|
4659
|
-
}
|
|
4621
|
+
errorln(`Unknown specialist subcommand: ${sub || '(none)'}. Available: create, edit, lint`);
|
|
4622
|
+
process.exit(1);
|
|
4623
|
+
}
|
|
4660
4624
|
|
|
4661
|
-
|
|
4625
|
+
async function cmdBackup(args) {
|
|
4626
|
+
// Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
|
|
4627
|
+
// State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
|
|
4628
|
+
void args;
|
|
4629
|
+
errorln('construct backup is no longer supported: Postgres was removed. Commit .cx/ for Git-backed state; the LanceDB index lives under .cx/lancedb.');
|
|
4662
4630
|
process.exit(1);
|
|
4663
4631
|
}
|
|
4664
4632
|
|
|
@@ -5200,8 +5168,19 @@ async function cmdHook(args) {
|
|
|
5200
5168
|
errorln(`Hook not found: ${hookPath}`);
|
|
5201
5169
|
process.exit(1);
|
|
5202
5170
|
}
|
|
5171
|
+
const started = Date.now();
|
|
5203
5172
|
const result = spawnSync(process.execPath, [hookPath, ...args.slice(1)], { stdio: 'inherit' });
|
|
5204
|
-
|
|
5173
|
+
const exitCode = result.status ?? 0;
|
|
5174
|
+
|
|
5175
|
+
// Record fire + outcome so hooks can be judged on whether they ever gate
|
|
5176
|
+
// anything, not just on latency. Dynamic import keeps non-hook commands from
|
|
5177
|
+
// paying the load; a telemetry failure must never change the hook's exit code.
|
|
5178
|
+
try {
|
|
5179
|
+
const { logHookCall } = await import('../lib/telemetry/hook-calls.mjs');
|
|
5180
|
+
logHookCall({ hookId: safe, exitCode, latencyMs: Date.now() - started });
|
|
5181
|
+
} catch { /* telemetry is best-effort */ }
|
|
5182
|
+
|
|
5183
|
+
process.exit(exitCode);
|
|
5205
5184
|
}
|
|
5206
5185
|
|
|
5207
5186
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -5257,6 +5236,7 @@ const handlers = new Map([
|
|
|
5257
5236
|
['infer', cmdInfer],
|
|
5258
5237
|
['search', cmdSearch],
|
|
5259
5238
|
['storage', cmdStorage],
|
|
5239
|
+
['registry:status', cmdRegistryStatus],
|
|
5260
5240
|
// Pricing / cost readouts are stubbed out: the ledger writes, model-pricing
|
|
5261
5241
|
// catalog, and per-turn accounting still run, but no CLI surface exposes
|
|
5262
5242
|
// them to the user. Handlers (cmdPricing, cmdCosts, cmdCost) remain in this
|
|
@@ -5440,6 +5420,8 @@ const handlers = new Map([
|
|
|
5440
5420
|
['lint:research', cmdLintResearch],
|
|
5441
5421
|
['lint:agents', cmdLintAgents],
|
|
5442
5422
|
['lint:contracts', cmdLintContracts],
|
|
5423
|
+
['lint:prompts', cmdLintPrompts],
|
|
5424
|
+
['specialist', cmdSpecialist],
|
|
5443
5425
|
['decisions', async (args) => {
|
|
5444
5426
|
const { runDecisionsCli } = await import('../lib/decisions/registry.mjs');
|
|
5445
5427
|
return runDecisionsCli(args);
|
|
@@ -22,7 +22,17 @@ import fs from 'node:fs';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { createHash } from 'node:crypto';
|
|
24
24
|
|
|
25
|
-
export const CONSTRUCT_INTEGRATION_VERSION =
|
|
25
|
+
export const CONSTRUCT_INTEGRATION_VERSION = 2;
|
|
26
|
+
|
|
27
|
+
// AGENTS.md is the cross-platform single source (the agents.md standard read by
|
|
28
|
+
// Codex/OpenCode/etc.); CLAUDE.md gets a thin pointer whose `@AGENTS.md` import
|
|
29
|
+
// makes Claude Code load the same content at session start — the pattern the
|
|
30
|
+
// Claude Code memory docs recommend for repos that carry both files. One mapping,
|
|
31
|
+
// shared by init and reconcile, so the two writers cannot disagree.
|
|
32
|
+
|
|
33
|
+
export function variantForFile(filePath) {
|
|
34
|
+
return path.basename(filePath) === 'CLAUDE.md' ? 'pointer' : 'full';
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
const BEGIN_PREFIX = '<!-- BEGIN CONSTRUCT INTEGRATION';
|
|
28
38
|
const END_MARKER = '<!-- END CONSTRUCT INTEGRATION -->';
|
|
@@ -37,7 +47,18 @@ function shortHash(body) {
|
|
|
37
47
|
// with Construct (≤60 lines). When a Beads Integration block is already present
|
|
38
48
|
// the tracker line defers to it rather than duplicating `bd` commands.
|
|
39
49
|
|
|
40
|
-
export function buildConstructIntegrationBody({ hasBeadsBlock = false } = {}) {
|
|
50
|
+
export function buildConstructIntegrationBody({ hasBeadsBlock = false, variant = 'full' } = {}) {
|
|
51
|
+
if (variant === 'pointer') {
|
|
52
|
+
return [
|
|
53
|
+
'## Construct integration',
|
|
54
|
+
'',
|
|
55
|
+
'@AGENTS.md',
|
|
56
|
+
'',
|
|
57
|
+
'The Construct integration guidance for this project lives in `AGENTS.md`, imported',
|
|
58
|
+
'above so Claude Code loads it at session start. Address `@construct` and ask for the',
|
|
59
|
+
'outcome — Construct routes to the right specialist chain.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
41
62
|
const tracker = hasBeadsBlock
|
|
42
63
|
? '- **Tracker**: see the Beads Integration block below for `bd` commands.'
|
|
43
64
|
: '- **Tracker**: use Beads (`bd`) for all task tracking — run `bd prime` for the workflow. Do not use ad-hoc TODO lists.';
|
|
@@ -78,11 +99,11 @@ export function injectConstructBlock(content, body, version = CONSTRUCT_INTEGRAT
|
|
|
78
99
|
// (with `header`) when missing; otherwise injects/updates in place, preserving
|
|
79
100
|
// all surrounding content. Writes only when the content changes.
|
|
80
101
|
|
|
81
|
-
export function injectIntoAgentFile(filePath, { version = CONSTRUCT_INTEGRATION_VERSION, header = '' } = {}) {
|
|
102
|
+
export function injectIntoAgentFile(filePath, { version = CONSTRUCT_INTEGRATION_VERSION, header = '', variant = null } = {}) {
|
|
82
103
|
const existed = fs.existsSync(filePath);
|
|
83
104
|
const current = existed ? fs.readFileSync(filePath, 'utf8') : '';
|
|
84
105
|
const hasBeadsBlock = BEADS_BLOCK_RE.test(current);
|
|
85
|
-
const body = buildConstructIntegrationBody({ hasBeadsBlock });
|
|
106
|
+
const body = buildConstructIntegrationBody({ hasBeadsBlock, variant: variant ?? variantForFile(filePath) });
|
|
86
107
|
const base = !existed && header ? (header.endsWith('\n') ? header : `${header}\n`) : current;
|
|
87
108
|
const { content, action } = injectConstructBlock(base, body, version);
|
|
88
109
|
const changed = !existed || action !== 'unchanged';
|