@geraldmaron/construct 1.0.23 → 1.0.24
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 +15 -215
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/reconcile/index.mjs +0 -2
- package/lib/service-manager.mjs +38 -256
- package/lib/setup.mjs +26 -426
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- 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/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/platforms/claude/settings.template.json +0 -7
- package/scripts/sync-specialists.mjs +46 -12
- package/specialists/prompts/cx-qa.md +1 -1
- 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';
|
|
@@ -822,35 +820,10 @@ async function cmdDoctor() {
|
|
|
822
820
|
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
823
821
|
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
824
822
|
|
|
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'));
|
|
823
|
+
const { VectorClient } = await import('../lib/storage/vector-client.mjs');
|
|
824
|
+
const vectorClient = new VectorClient({ env: process.env });
|
|
825
|
+
const vectorOk = await vectorClient.isHealthy();
|
|
826
|
+
add('Vector backend (LanceDB) reachable', vectorOk);
|
|
854
827
|
|
|
855
828
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
856
829
|
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
@@ -979,35 +952,6 @@ async function cmdDoctor() {
|
|
|
979
952
|
add('Observation size', false, true);
|
|
980
953
|
}
|
|
981
954
|
|
|
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
955
|
try {
|
|
1012
956
|
const { detectBeadsDrift } = await import('../lib/beads/drift.mjs');
|
|
1013
957
|
const drift = detectBeadsDrift();
|
|
@@ -1037,24 +981,6 @@ async function cmdDoctor() {
|
|
|
1037
981
|
add('cm available', false, true);
|
|
1038
982
|
}
|
|
1039
983
|
|
|
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
984
|
try {
|
|
1059
985
|
const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
|
|
1060
986
|
if (fs.existsSync(failuresPath)) {
|
|
@@ -1569,49 +1495,11 @@ async function cmdStorage(args) {
|
|
|
1569
1495
|
println(`Reconciled observations: re-embedded ${result.reembedded} of ${result.checked} (model ${result.model}).`);
|
|
1570
1496
|
return;
|
|
1571
1497
|
}
|
|
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;
|
|
1498
|
+
if (action === 'migrations' || action === 'repair-migrations') {
|
|
1499
|
+
// Postgres schema migrations were removed with the SQL backend; LanceDB
|
|
1500
|
+
// tables are created on demand and need no migration runner.
|
|
1501
|
+
errorln('construct storage ' + action + ' is no longer supported: Postgres was removed.');
|
|
1502
|
+
process.exit(1);
|
|
1615
1503
|
}
|
|
1616
1504
|
if (action === 'sync') {
|
|
1617
1505
|
const result = await syncFileStateToSql(process.cwd(), { env: process.env, project: inferProjectName(process.cwd()) });
|
|
@@ -3543,6 +3431,7 @@ async function cmdOrchestrate(args) {
|
|
|
3543
3431
|
host: flag('--host'),
|
|
3544
3432
|
hostModel: flag('--host-model'),
|
|
3545
3433
|
hostProvider: flag('--host-provider'),
|
|
3434
|
+
workerBackend: flag('--worker-backend'),
|
|
3546
3435
|
fileCount: Number(flag('--file-count') || 0),
|
|
3547
3436
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3548
3437
|
};
|
|
@@ -4566,99 +4455,10 @@ async function cmdLintContracts() {
|
|
|
4566
4455
|
}
|
|
4567
4456
|
|
|
4568
4457
|
async function cmdBackup(args) {
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
const noPrune = args.includes('--no-prune');
|
|
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
|
-
}
|
|
4595
|
-
return;
|
|
4596
|
-
}
|
|
4597
|
-
|
|
4598
|
-
if (sub === 'prune') {
|
|
4599
|
-
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
4600
|
-
const keep = keepArg
|
|
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
|
-
}
|
|
4613
|
-
|
|
4614
|
-
if (sub === 'verify') {
|
|
4615
|
-
const archivePath = args[1];
|
|
4616
|
-
if (!archivePath) { errorln('Usage: construct backup verify <archive>'); process.exit(1); }
|
|
4617
|
-
const { verifyBackup } = await import('../lib/storage/backup.mjs');
|
|
4618
|
-
const result = await verifyBackup(archivePath);
|
|
4619
|
-
if (result.ok) {
|
|
4620
|
-
ok('Backup verified — all checksums match.');
|
|
4621
|
-
} else {
|
|
4622
|
-
errorln(`Backup verification failed:\n ${result.errors.join('\n ')}`);
|
|
4623
|
-
process.exit(1);
|
|
4624
|
-
}
|
|
4625
|
-
return;
|
|
4626
|
-
}
|
|
4627
|
-
|
|
4628
|
-
if (sub === 'restore') {
|
|
4629
|
-
const archivePath = args[1];
|
|
4630
|
-
const yes = args.includes('--yes');
|
|
4631
|
-
if (!archivePath) { errorln('Usage: construct backup restore <archive> [--yes]'); process.exit(1); }
|
|
4632
|
-
if (!yes) {
|
|
4633
|
-
println(`About to restore from: ${archivePath}`);
|
|
4634
|
-
println('This will overwrite observations, sessions, config.env, and registry.json.');
|
|
4635
|
-
println('Press Ctrl+C to cancel, or re-run with --yes to proceed.');
|
|
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);
|
|
4646
|
-
}
|
|
4647
|
-
return;
|
|
4648
|
-
}
|
|
4649
|
-
|
|
4650
|
-
if (sub === 'list') {
|
|
4651
|
-
const { listBackups } = await import('../lib/storage/backup.mjs');
|
|
4652
|
-
const backups = listBackups();
|
|
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
|
-
}
|
|
4660
|
-
|
|
4661
|
-
errorln(`Unknown backup subcommand: ${sub}. Available: create, verify, restore, list, prune`);
|
|
4458
|
+
// Postgres-era backups were removed with the SQL backend (lib/storage/backup.mjs).
|
|
4459
|
+
// State is now Git-backed (.cx/) plus the local LanceDB index under .cx/lancedb.
|
|
4460
|
+
void args;
|
|
4461
|
+
errorln('construct backup is no longer supported: Postgres was removed. Commit .cx/ for Git-backed state; the LanceDB index lives under .cx/lancedb.');
|
|
4662
4462
|
process.exit(1);
|
|
4663
4463
|
}
|
|
4664
4464
|
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -241,7 +241,7 @@ function inferFileClassification(filePath) {
|
|
|
241
241
|
* Summary = first non-empty line of extracted text (≤120 chars).
|
|
242
242
|
* Content = first 1500 chars of extracted text for RAG searchability.
|
|
243
243
|
*/
|
|
244
|
-
function recordInboxObservation(rootDir, { sourcePath, outputPath, characters, extractedText, category, knowledgeSubdir }) {
|
|
244
|
+
async function recordInboxObservation(rootDir, { sourcePath, outputPath, characters, extractedText, category, knowledgeSubdir }) {
|
|
245
245
|
const lines = (extractedText ?? '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
246
246
|
const firstLine = (lines[0] ?? '').slice(0, 120);
|
|
247
247
|
const filename = sourcePath.split('/').pop();
|
|
@@ -251,7 +251,10 @@ function recordInboxObservation(rootDir, { sourcePath, outputPath, characters, e
|
|
|
251
251
|
const content = (extractedText ?? '').slice(0, 1500);
|
|
252
252
|
const tags = ['inbox', 'ingested-doc', category, `knowledge:${knowledgeSubdir}`];
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
// Await the write so callers (poll) know ingestion is complete. Fire-and-
|
|
255
|
+
// forget here let the LanceDB write outlive poll() and race the caller's
|
|
256
|
+
// teardown, surfacing as "ENOTEMPTY: directory not empty" on .cx cleanup.
|
|
257
|
+
await addObservation(rootDir, {
|
|
255
258
|
role: 'construct',
|
|
256
259
|
category,
|
|
257
260
|
summary,
|
|
@@ -414,7 +417,7 @@ export class InboxWatcher {
|
|
|
414
417
|
const droppedInfo = fileResult.droppedInfo ?? [];
|
|
415
418
|
writeExtractionWarning({ sourcePath: candidate.path, droppedInfo });
|
|
416
419
|
|
|
417
|
-
recordInboxObservation(this.#rootDir, {
|
|
420
|
+
await recordInboxObservation(this.#rootDir, {
|
|
418
421
|
sourcePath: candidate.path,
|
|
419
422
|
outputPath: fileResult.outputPath,
|
|
420
423
|
characters: fileResult.characters,
|