@geraldmaron/construct 1.0.21 → 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 +23 -7
- package/bin/construct +206 -228
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +31 -10
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +39 -163
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +72 -10
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +169 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +59 -252
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +104 -480
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -43
- 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/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +7 -6
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -11
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +427 -107
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +3 -12
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +28 -36
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +15 -15
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- 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/server/static/index.html +0 -1
- 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/vector-store.mjs +0 -100
package/bin/construct
CHANGED
|
@@ -65,10 +65,9 @@ import { runTeamReviewCli } from '../lib/telemetry/team-rollup.mjs';
|
|
|
65
65
|
import { readDashboardState, startDashboard, startServices } from '../lib/service-manager.mjs';
|
|
66
66
|
import { readCostLog, summarizeCostData, formatCostReport, clearCostLog } from '../lib/cost.mjs';
|
|
67
67
|
import { readEfficiencyLog, summarizeEfficiencyData, formatEfficiencyReport } from '../lib/efficiency.mjs';
|
|
68
|
+
import { resolveColors } from '../lib/term-format.mjs';
|
|
68
69
|
import { buildHybridSearchResultsAsync } from '../lib/storage/hybrid-query.mjs';
|
|
69
|
-
import { createSqlClient, closeSqlClient
|
|
70
|
-
import { describeVectorStore } from '../lib/storage/vector-store.mjs';
|
|
71
|
-
import { describeSqlStore } from '../lib/storage/sql-store.mjs';
|
|
70
|
+
import { createSqlClient, closeSqlClient } from '../lib/storage/backend.mjs';
|
|
72
71
|
import { syncFileStateToSql } from '../lib/storage/sync.mjs';
|
|
73
72
|
import { deleteIngestedArtifacts, getStorageStatus, inferProjectName, resetStorage } from '../lib/storage/admin.mjs';
|
|
74
73
|
import { runPressureRelease } from '../lib/runtime-pressure.mjs';
|
|
@@ -80,12 +79,7 @@ for (const [key, value] of Object.entries(ENV)) {
|
|
|
80
79
|
if (!(key in process.env)) process.env[key] = value;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
const COLORS = (
|
|
84
|
-
? {
|
|
85
|
-
bold: '\u001b[1m', dim: '\u001b[2m', reset: '\u001b[0m',
|
|
86
|
-
red: '\u001b[31m', green: '\u001b[32m', yellow: '\u001b[33m', cyan: '\u001b[36m',
|
|
87
|
-
}
|
|
88
|
-
: { bold: '', dim: '', reset: '', red: '', green: '', yellow: '', cyan: '' };
|
|
82
|
+
const COLORS = resolveColors();
|
|
89
83
|
|
|
90
84
|
function println(message = '') { process.stdout.write(`${message}\n`); }
|
|
91
85
|
function errorln(message = '') { process.stderr.write(`${message}\n`); }
|
|
@@ -250,6 +244,45 @@ async function cmdSync(args) {
|
|
|
250
244
|
opLog.close('failed', { stage: 'sync-specialists', status: syncStatus });
|
|
251
245
|
process.exit(syncStatus);
|
|
252
246
|
}
|
|
247
|
+
|
|
248
|
+
// State reconciliation (ADR-0027): bring on-disk state in line with the
|
|
249
|
+
// current disposition contract. Auto-safety tasks run silently; ask-safety
|
|
250
|
+
// tasks surface via construct doctor instead. --reconcile=<id> runs one task
|
|
251
|
+
// on explicit consent (the path for ask-safety tasks). --no-reconcile
|
|
252
|
+
// bypasses the auto pass entirely for diagnostic runs.
|
|
253
|
+
|
|
254
|
+
const reconcileArg = args.find((a) => a.startsWith('--reconcile='));
|
|
255
|
+
if (reconcileArg) {
|
|
256
|
+
const id = reconcileArg.slice('--reconcile='.length);
|
|
257
|
+
try {
|
|
258
|
+
const { runReconciliation } = await import('../lib/reconcile/index.mjs');
|
|
259
|
+
const result = await runReconciliation(id, {});
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
errorln(`Reconciliation failed (${id}): ${result.reason}`);
|
|
262
|
+
if (result.available) errorln(`Available: ${result.available.join(', ')}`);
|
|
263
|
+
opLog.close('failed', { stage: 'reconcile', id, reason: result.reason });
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (result.ran) ok(`Reconciled (${id}): ${result.summary}`);
|
|
267
|
+
else println(`Reconciliation (${id}): ${result.summary}`);
|
|
268
|
+
opLog.event('reconcile-explicit', { id, ran: !!result.ran });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
errorln(`Reconciliation error (${id}): ${err.message}`);
|
|
271
|
+
opLog.close('failed', { stage: 'reconcile', id, reason: err.message });
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
} else if (!args.includes('--no-reconcile')) {
|
|
275
|
+
try {
|
|
276
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
277
|
+
const { runAutoReconciliations } = await import('../lib/reconcile/index.mjs');
|
|
278
|
+
const { applied } = await runAutoReconciliations({ verbose });
|
|
279
|
+
for (const entry of applied) ok(`Reconciled (${entry.id}): ${entry.summary}`);
|
|
280
|
+
opLog.event('reconcile', { applied: applied.length });
|
|
281
|
+
} catch (err) {
|
|
282
|
+
opLog.event('reconcile-error', { message: err.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
253
286
|
if (args.includes('--no-docs')) {
|
|
254
287
|
opLog.close('ok', { docs: 'skipped' });
|
|
255
288
|
return;
|
|
@@ -584,23 +617,24 @@ async function cmdDoctor() {
|
|
|
584
617
|
// Tier model selection. Construct ships with no default — at least
|
|
585
618
|
// one tier must be configured (registry.json primary OR CX_MODEL_*
|
|
586
619
|
// env override) before any LLM-backed workflow can run.
|
|
587
|
-
//
|
|
588
|
-
|
|
620
|
+
// A missing, partial, or unresolvable tier is advisory, not a repo defect:
|
|
621
|
+
// model tiers are user/machine-specific config, so doctor warns and never
|
|
622
|
+
// fails the gate on them.
|
|
589
623
|
try {
|
|
590
624
|
const { resolveModelTiers } = await import('../lib/model-registry.mjs');
|
|
591
625
|
const resolved = resolveModelTiers({ env: process.env });
|
|
592
626
|
if (resolved.complete && !resolved.errors) {
|
|
593
627
|
add('Models — all tiers configured', true);
|
|
594
628
|
} else if (resolved.configured === 0) {
|
|
595
|
-
add('Models — no tier configured (pick one in the dashboard or run `construct models --apply`)',
|
|
629
|
+
add('Models — no tier configured (pick one in the dashboard or run `construct models --apply`)', false, true);
|
|
596
630
|
} else {
|
|
597
631
|
const missing = Object.entries(resolved.models)
|
|
598
632
|
.filter(([, v]) => !v)
|
|
599
633
|
.map(([k]) => k);
|
|
600
|
-
add(`Models — only ${resolved.configured}/3 tiers configured (missing: ${missing.join(', ')})`,
|
|
634
|
+
add(`Models — only ${resolved.configured}/3 tiers configured (missing: ${missing.join(', ')})`, false, true);
|
|
601
635
|
}
|
|
602
636
|
} catch (err) {
|
|
603
|
-
add(`Models — check failed: ${err.message}`,
|
|
637
|
+
add(`Models — check failed: ${err.message}`, false, true);
|
|
604
638
|
}
|
|
605
639
|
|
|
606
640
|
add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
|
|
@@ -786,35 +820,10 @@ async function cmdDoctor() {
|
|
|
786
820
|
const researchLintResults = lintResearchRepo({ rootDir: process.cwd() });
|
|
787
821
|
add('Research artifacts meet minimum evidence structure', researchLintResults.every((entry) => entry.errors.length === 0), true);
|
|
788
822
|
|
|
789
|
-
const
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
let sqlReachable = false;
|
|
794
|
-
if (sqlConfig.configured && sqlClient) {
|
|
795
|
-
try {
|
|
796
|
-
await sqlClient`select 1 as ok`;
|
|
797
|
-
sqlReachable = true;
|
|
798
|
-
add('SQL backend reachable', true);
|
|
799
|
-
} catch {
|
|
800
|
-
// In solo mode the local vector index is the active storage path, so an
|
|
801
|
-
// unreachable Postgres is expected unless the user has started it. Emit a
|
|
802
|
-
// contextual message rather than a bare ⚠ so the fix is obvious.
|
|
803
|
-
const { getDeploymentMode } = await import('../lib/deployment-mode.mjs');
|
|
804
|
-
const mode = getDeploymentMode(process.env) ?? 'solo';
|
|
805
|
-
const hint = mode === 'solo'
|
|
806
|
-
? ' (solo mode — run `construct dev` to start Postgres)'
|
|
807
|
-
: ' — check DATABASE_URL and that the server is running';
|
|
808
|
-
add(`SQL backend unreachable${hint}`, false, true);
|
|
809
|
-
} finally {
|
|
810
|
-
await closeSqlClient(sqlClient);
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
add('SQL backend configured', sqlConfig.fallbackAvailable, true);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const vectorConfig = describeVectorStore(process.env);
|
|
817
|
-
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);
|
|
818
827
|
|
|
819
828
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
820
829
|
const { changed: stale } = await regenerateDocs({ rootDir: ROOT_DIR, check: true });
|
|
@@ -943,35 +952,6 @@ async function cmdDoctor() {
|
|
|
943
952
|
add('Observation size', false, true);
|
|
944
953
|
}
|
|
945
954
|
|
|
946
|
-
// Only run the schema migration check when SQL is both configured AND
|
|
947
|
-
// currently reachable. If the SQL check above already failed (sqlReachable
|
|
948
|
-
// is false) this check will also fail with the same ECONNREFUSED and would
|
|
949
|
-
// produce a redundant second warning about the same offline server.
|
|
950
|
-
if (sqlConfig.configured && sqlReachable) {
|
|
951
|
-
try {
|
|
952
|
-
const migClient = createSqlClient(process.env);
|
|
953
|
-
if (migClient) {
|
|
954
|
-
const { describeMigrations } = await import('../lib/storage/migrations.mjs');
|
|
955
|
-
const m = await describeMigrations(migClient);
|
|
956
|
-
await closeSqlClient(migClient);
|
|
957
|
-
const ok = !!m.ok && m.drift.length === 0;
|
|
958
|
-
let label = 'Schema migrations applied + no drift';
|
|
959
|
-
if (!ok && m.drift && m.drift.length > 0) {
|
|
960
|
-
const repairable = m.drift.filter((d) => d.idempotent).length;
|
|
961
|
-
const notRepairable = m.drift.length - repairable;
|
|
962
|
-
if (notRepairable === 0) {
|
|
963
|
-
label = `Schema migrations: ${m.drift.length} drifted (all idempotent — run \`construct storage repair-migrations --yes\`)`;
|
|
964
|
-
} else {
|
|
965
|
-
label = `Schema migrations: ${m.drift.length} drifted (${notRepairable} non-idempotent — write a new migration file)`;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
add(label, ok, true);
|
|
969
|
-
}
|
|
970
|
-
} catch {
|
|
971
|
-
add('Schema migrations applied + no drift', false, true);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
955
|
try {
|
|
976
956
|
const { detectBeadsDrift } = await import('../lib/beads/drift.mjs');
|
|
977
957
|
const drift = detectBeadsDrift();
|
|
@@ -1001,24 +981,6 @@ async function cmdDoctor() {
|
|
|
1001
981
|
add('cm available', false, true);
|
|
1002
982
|
}
|
|
1003
983
|
|
|
1004
|
-
try {
|
|
1005
|
-
const dockerCli = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['docker'], { stdio: 'pipe' });
|
|
1006
|
-
let dockerOk = false;
|
|
1007
|
-
let dockerNote = 'Docker not installed (optional — managed Postgres needs it)';
|
|
1008
|
-
if (dockerCli.status === 0) {
|
|
1009
|
-
const dockerInfo = spawnSync('docker', ['info'], { stdio: 'pipe' });
|
|
1010
|
-
if (dockerInfo.status === 0) {
|
|
1011
|
-
dockerOk = true;
|
|
1012
|
-
dockerNote = 'Docker daemon reachable';
|
|
1013
|
-
} else {
|
|
1014
|
-
dockerNote = 'Docker CLI installed but daemon not reachable — run `construct up` on macOS to auto-start, or start Docker manually';
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
add(dockerOk ? 'Docker daemon reachable' : `Docker daemon reachable (${dockerNote})`, dockerOk, true);
|
|
1018
|
-
} catch {
|
|
1019
|
-
add('Docker daemon reachable', false, true);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
984
|
try {
|
|
1023
985
|
const failuresPath = path.join(HOME, '.cx', 'hook-failures.jsonl');
|
|
1024
986
|
if (fs.existsSync(failuresPath)) {
|
|
@@ -1069,6 +1031,26 @@ async function cmdDoctor() {
|
|
|
1069
1031
|
add('Registry integrity check', false);
|
|
1070
1032
|
}
|
|
1071
1033
|
|
|
1034
|
+
// Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
|
|
1035
|
+
// with its summary and the exact `construct sync --reconcile=<id>` command.
|
|
1036
|
+
// Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
|
|
1037
|
+
// ask tasks both land here, since ask tasks never run from the auto pass.
|
|
1038
|
+
let reconcileDrift = [];
|
|
1039
|
+
try {
|
|
1040
|
+
const { listReconciliations } = await import('../lib/reconcile/index.mjs');
|
|
1041
|
+
const tasks = await listReconciliations();
|
|
1042
|
+
reconcileDrift = tasks.filter((t) => t.needsRepair);
|
|
1043
|
+
add(
|
|
1044
|
+
reconcileDrift.length === 0
|
|
1045
|
+
? 'Reconciliation drift: none'
|
|
1046
|
+
: `Reconciliation drift: ${reconcileDrift.length} task${reconcileDrift.length === 1 ? '' : 's'} need repair (see below)`,
|
|
1047
|
+
reconcileDrift.length === 0,
|
|
1048
|
+
true,
|
|
1049
|
+
);
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
add(`Reconciliation drift check failed: ${err.message}`, false, true);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1072
1054
|
println('Construct Health Check');
|
|
1073
1055
|
println('══════════════════════');
|
|
1074
1056
|
println('');
|
|
@@ -1080,6 +1062,17 @@ async function cmdDoctor() {
|
|
|
1080
1062
|
else if (check.optional) warnCount += 1;
|
|
1081
1063
|
else failCount += 1;
|
|
1082
1064
|
}
|
|
1065
|
+
|
|
1066
|
+
if (reconcileDrift.length > 0) {
|
|
1067
|
+
println('');
|
|
1068
|
+
println('Reconciliation drift:');
|
|
1069
|
+
for (const task of reconcileDrift) {
|
|
1070
|
+
println(` ⚠ ${task.id} (${task.safety})`);
|
|
1071
|
+
if (task.summary) println(` ${task.summary}`);
|
|
1072
|
+
println(` fix: construct sync --reconcile=${task.id}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1083
1076
|
println('');
|
|
1084
1077
|
println(`Results: ${okCount} passed, ${warnCount} warnings, ${failCount} failed`);
|
|
1085
1078
|
if (failCount > 0) process.exit(1);
|
|
@@ -1178,6 +1171,16 @@ async function cmdUp(args = []) {
|
|
|
1178
1171
|
else warn(`${svc.name}: ${svc.note}`);
|
|
1179
1172
|
}
|
|
1180
1173
|
|
|
1174
|
+
// Show Langfuse credentials guidance after first start
|
|
1175
|
+
const langfuseService = results.find(r => r.name === 'Langfuse');
|
|
1176
|
+
if (langfuseService && langfuseService.status === 'started' && langfuseService.note?.includes('auto-configured')) {
|
|
1177
|
+
println('');
|
|
1178
|
+
info('Langfuse credentials:');
|
|
1179
|
+
info(' Dashboard: http://localhost:54330 (login: admin@construct.local / construct-admin)');
|
|
1180
|
+
info(' API keys auto-saved to ~/.construct/config.env');
|
|
1181
|
+
info(' Run `construct status` anytime to view credentials');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1181
1184
|
if (recovery) {
|
|
1182
1185
|
println('');
|
|
1183
1186
|
info(`Recovery: ${recovery.message}`);
|
|
@@ -1410,6 +1413,44 @@ async function cmdIngest(args) {
|
|
|
1410
1413
|
}
|
|
1411
1414
|
}
|
|
1412
1415
|
|
|
1416
|
+
async function cmdExport(args) {
|
|
1417
|
+
// construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>]
|
|
1418
|
+
// Optional capability per ADR-0024: requires pandoc on PATH (and typst for PDF).
|
|
1419
|
+
// Absent tooling returns a structured "install X" error, not a crash.
|
|
1420
|
+
|
|
1421
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
1422
|
+
const inputPath = positional[0];
|
|
1423
|
+
const toFlag = args.find((a) => a.startsWith('--to='))?.split('=')[1];
|
|
1424
|
+
const outFlag = args.find((a) => a.startsWith('--output='))?.split('=')[1];
|
|
1425
|
+
const detectOnly = args.includes('--detect');
|
|
1426
|
+
|
|
1427
|
+
const { detect, exportMarkdown, EXPORT_FORMATS } = await import('../lib/document-export.mjs');
|
|
1428
|
+
|
|
1429
|
+
if (detectOnly) {
|
|
1430
|
+
const format = toFlag || 'pdf';
|
|
1431
|
+
println(JSON.stringify(detect(format), null, 2));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (!inputPath) {
|
|
1436
|
+
errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--detect]');
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
if (!toFlag) {
|
|
1440
|
+
errorln(`--to is required. Supported formats: ${EXPORT_FORMATS.join(', ')}`);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const resolvedIn = path.resolve(process.cwd(), inputPath);
|
|
1445
|
+
const resolvedOut = outFlag ? path.resolve(process.cwd(), outFlag) : null;
|
|
1446
|
+
const result = exportMarkdown({ inputPath: resolvedIn, outputPath: resolvedOut, format: toFlag });
|
|
1447
|
+
if (!result.ok) {
|
|
1448
|
+
errorln(result.message);
|
|
1449
|
+
process.exit(result.missing?.length ? 2 : 1);
|
|
1450
|
+
}
|
|
1451
|
+
ok(result.message);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1413
1454
|
async function cmdInfer(args) {
|
|
1414
1455
|
try {
|
|
1415
1456
|
const result = await runInferCli(args, { cwd: process.cwd() });
|
|
@@ -1454,49 +1495,11 @@ async function cmdStorage(args) {
|
|
|
1454
1495
|
println(`Reconciled observations: re-embedded ${result.reembedded} of ${result.checked} (model ${result.model}).`);
|
|
1455
1496
|
return;
|
|
1456
1497
|
}
|
|
1457
|
-
if (action === 'migrations') {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
const report = await describeMigrations(sqlClient);
|
|
1463
|
-
println(JSON.stringify(report, null, 2));
|
|
1464
|
-
if (report.drift?.length > 0) process.exit(2);
|
|
1465
|
-
} finally {
|
|
1466
|
-
await closeSqlClient(sqlClient);
|
|
1467
|
-
}
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
if (action === 'repair-migrations') {
|
|
1471
|
-
const confirm = rest.includes('--yes');
|
|
1472
|
-
if (!confirm) {
|
|
1473
|
-
errorln('This re-applies drifted migrations (idempotent only) and updates their recorded SHA.');
|
|
1474
|
-
errorln('Destructive migrations (DROP / TRUNCATE / ALTER…DROP / DELETE) are refused.');
|
|
1475
|
-
errorln('Re-run with --yes to proceed.');
|
|
1476
|
-
process.exit(1);
|
|
1477
|
-
}
|
|
1478
|
-
const { runMigrations } = await import('../lib/storage/migrations.mjs');
|
|
1479
|
-
const sqlClient = createSqlClient(process.env);
|
|
1480
|
-
if (!sqlClient) { errorln('No DATABASE_URL configured.'); process.exit(1); }
|
|
1481
|
-
try {
|
|
1482
|
-
const result = await runMigrations(sqlClient, { repair: true });
|
|
1483
|
-
println(JSON.stringify({
|
|
1484
|
-
applied: result.applied,
|
|
1485
|
-
repaired: result.repaired,
|
|
1486
|
-
skipped: result.skipped,
|
|
1487
|
-
drift: result.drift,
|
|
1488
|
-
}, null, 2));
|
|
1489
|
-
if (result.drift.length > 0) {
|
|
1490
|
-
errorln('');
|
|
1491
|
-
errorln('Refused to repair non-idempotent migrations:');
|
|
1492
|
-
for (const d of result.drift) errorln(` ${d.filename}`);
|
|
1493
|
-
process.exit(2);
|
|
1494
|
-
}
|
|
1495
|
-
info(`Repaired ${result.repaired.length} migration(s); applied ${result.applied.length} new.`);
|
|
1496
|
-
} finally {
|
|
1497
|
-
await closeSqlClient(sqlClient);
|
|
1498
|
-
}
|
|
1499
|
-
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);
|
|
1500
1503
|
}
|
|
1501
1504
|
if (action === 'sync') {
|
|
1502
1505
|
const result = await syncFileStateToSql(process.cwd(), { env: process.env, project: inferProjectName(process.cwd()) });
|
|
@@ -1976,7 +1979,17 @@ async function cmdEmbeddedTriage(args) {
|
|
|
1976
1979
|
input = fs.readFileSync(0, 'utf8');
|
|
1977
1980
|
}
|
|
1978
1981
|
if (!input.trim() && !ingestion?.error) {
|
|
1979
|
-
|
|
1982
|
+
// This is an embedded-contract verb: an embedder that pipes empty/no input
|
|
1983
|
+
// must still get a parseable, versioned envelope — not a bare exit. Emit a
|
|
1984
|
+
// typed error envelope on stdout so the contract holds, and exit non-zero so
|
|
1985
|
+
// shell callers still see failure.
|
|
1986
|
+
const { wrapResponse } = await import('../lib/embedded-contract/envelope.mjs');
|
|
1987
|
+
const envelope = wrapResponse({
|
|
1988
|
+
surface: 'cli',
|
|
1989
|
+
warnings: ['No artifact provided — pass --text "...", --file <path>, or pipe content on stdin.'],
|
|
1990
|
+
data: { error: { code: 'missing_input', message: 'intake classify requires an artifact to classify; received empty input.' } },
|
|
1991
|
+
});
|
|
1992
|
+
println(JSON.stringify(envelope, null, 2));
|
|
1980
1993
|
process.exit(1);
|
|
1981
1994
|
}
|
|
1982
1995
|
const request = {
|
|
@@ -3364,6 +3377,47 @@ async function cmdOrchestrate(args) {
|
|
|
3364
3377
|
const flag = (name) => { const i = rest.indexOf(name); return i !== -1 ? rest[i + 1] : undefined; };
|
|
3365
3378
|
const wantsJson = rest.includes('--json') || args.includes('--json');
|
|
3366
3379
|
|
|
3380
|
+
// Thin-client mode: drive the local orchestration daemon over HTTP instead of
|
|
3381
|
+
// running in-process, proving the engine-as-service contract editors/CI use.
|
|
3382
|
+
if (rest.includes('--remote')) {
|
|
3383
|
+
const remoteVal = flag('--remote');
|
|
3384
|
+
const base = (remoteVal && /^https?:\/\//.test(remoteVal)) ? remoteVal.replace(/\/$/, '') : `http://${process.env.BIND_HOST || '127.0.0.1'}:${process.env.PORT || '4242'}`;
|
|
3385
|
+
const { getDashboardToken } = await import('../lib/server/auth.mjs');
|
|
3386
|
+
const token = getDashboardToken();
|
|
3387
|
+
const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) };
|
|
3388
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
3389
|
+
try {
|
|
3390
|
+
if (sub === 'run') {
|
|
3391
|
+
const text = flag('--text') || positional.join(' ').trim();
|
|
3392
|
+
if (!text) { errorln('Usage: construct orchestrate run "<request>" --remote [--strategy S] [--worker-backend provider] [--json]'); process.exit(1); }
|
|
3393
|
+
const res = await fetch(`${base}/api/orchestration/runs`, { method: 'POST', headers, body: JSON.stringify({ request: text, workflowType: flag('--workflow-type'), requestedStrategy: flag('--strategy') || 'auto', host: flag('--host'), hostModel: flag('--host-model'), hostProvider: flag('--host-provider'), workerBackend: flag('--worker-backend'), fileCount: Number(flag('--file-count') || 0), moduleCount: Number(flag('--module-count') || 0) }) });
|
|
3394
|
+
const env = await res.json();
|
|
3395
|
+
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3396
|
+
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3397
|
+
println(`Run ${env.data.runId} started on ${base} — ${env.data.status} · executionMode=${env.data.execution?.executionMode}. Poll: construct orchestrate status ${env.data.runId} --remote`);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (sub === 'status') {
|
|
3401
|
+
const runId = positional[0];
|
|
3402
|
+
const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
|
|
3403
|
+
const res = await fetch(`${base}${path}`, { headers });
|
|
3404
|
+
const env = await res.json();
|
|
3405
|
+
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3406
|
+
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3407
|
+
if (runId) {
|
|
3408
|
+
println(`Run ${env.data.runId} — ${env.data.status} · executionMode=${env.data.execution?.executionMode}`);
|
|
3409
|
+
for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
|
|
3410
|
+
} else {
|
|
3411
|
+
for (const r of env.data.runs || []) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
|
|
3412
|
+
}
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
} catch (err) {
|
|
3416
|
+
errorln(`Could not reach the orchestration daemon at ${base}: ${err.message}. Start it with \`construct dashboard\`.`);
|
|
3417
|
+
process.exit(1);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3367
3421
|
if (sub === 'run') {
|
|
3368
3422
|
const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
3369
3423
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
@@ -3377,6 +3431,7 @@ async function cmdOrchestrate(args) {
|
|
|
3377
3431
|
host: flag('--host'),
|
|
3378
3432
|
hostModel: flag('--host-model'),
|
|
3379
3433
|
hostProvider: flag('--host-provider'),
|
|
3434
|
+
workerBackend: flag('--worker-backend'),
|
|
3380
3435
|
fileCount: Number(flag('--file-count') || 0),
|
|
3381
3436
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3382
3437
|
};
|
|
@@ -3386,6 +3441,9 @@ async function cmdOrchestrate(args) {
|
|
|
3386
3441
|
if (run.execution.degraded) println(` degraded: ${run.execution.degradationReason}`);
|
|
3387
3442
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
3388
3443
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
3444
|
+
for (const t of run.tasks) {
|
|
3445
|
+
if (t.reasoning) println(` ↳ ${t.role} reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
3446
|
+
}
|
|
3389
3447
|
return;
|
|
3390
3448
|
}
|
|
3391
3449
|
|
|
@@ -3397,7 +3455,10 @@ async function cmdOrchestrate(args) {
|
|
|
3397
3455
|
if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
|
|
3398
3456
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3399
3457
|
println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode}`);
|
|
3400
|
-
for (const t of run.tasks)
|
|
3458
|
+
for (const t of run.tasks) {
|
|
3459
|
+
println(` ${t.id} ${t.role} — ${t.status}`);
|
|
3460
|
+
if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
3461
|
+
}
|
|
3401
3462
|
return;
|
|
3402
3463
|
}
|
|
3403
3464
|
const runs = await getRuns(process.cwd());
|
|
@@ -4394,99 +4455,10 @@ async function cmdLintContracts() {
|
|
|
4394
4455
|
}
|
|
4395
4456
|
|
|
4396
4457
|
async function cmdBackup(args) {
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
const noPrune = args.includes('--no-prune');
|
|
4402
|
-
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
4403
|
-
const keep = keepArg
|
|
4404
|
-
? Number(keepArg.split('=')[1])
|
|
4405
|
-
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
4406
|
-
const { createBackup, pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
4407
|
-
println('Creating backup…');
|
|
4408
|
-
try {
|
|
4409
|
-
const result = await createBackup({ includeSecrets });
|
|
4410
|
-
ok(`Backup created: ${result.path}`);
|
|
4411
|
-
println(` Contents: ${result.manifest.contents.join(', ') || '(empty)'}`);
|
|
4412
|
-
if (!includeSecrets) println(' Secrets redacted. Use --include-secrets to include them.');
|
|
4413
|
-
if (!noPrune) {
|
|
4414
|
-
const pruned = pruneBackups({ keep });
|
|
4415
|
-
if (pruned.removed.length) {
|
|
4416
|
-
println(` Auto-pruned ${pruned.removed.length} old backup(s); kept ${pruned.kept}.`);
|
|
4417
|
-
}
|
|
4418
|
-
}
|
|
4419
|
-
} catch (err) {
|
|
4420
|
-
errorln(`Backup failed: ${err.message}`);
|
|
4421
|
-
process.exit(1);
|
|
4422
|
-
}
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4425
|
-
|
|
4426
|
-
if (sub === 'prune') {
|
|
4427
|
-
const keepArg = args.find((a) => a.startsWith('--keep='));
|
|
4428
|
-
const keep = keepArg
|
|
4429
|
-
? Number(keepArg.split('=')[1])
|
|
4430
|
-
: Number(process.env.CONSTRUCT_BACKUP_RETAIN || 10);
|
|
4431
|
-
const { pruneBackups } = await import('../lib/storage/backup.mjs');
|
|
4432
|
-
const result = pruneBackups({ keep });
|
|
4433
|
-
if (result.removed.length === 0) {
|
|
4434
|
-
println(`Nothing to prune (have ${result.kept}, keeping ${keep}).`);
|
|
4435
|
-
} else {
|
|
4436
|
-
ok(`Pruned ${result.removed.length} backup(s); kept ${result.kept}.`);
|
|
4437
|
-
for (const r of result.removed) println(` removed ${r}`);
|
|
4438
|
-
}
|
|
4439
|
-
return;
|
|
4440
|
-
}
|
|
4441
|
-
|
|
4442
|
-
if (sub === 'verify') {
|
|
4443
|
-
const archivePath = args[1];
|
|
4444
|
-
if (!archivePath) { errorln('Usage: construct backup verify <archive>'); process.exit(1); }
|
|
4445
|
-
const { verifyBackup } = await import('../lib/storage/backup.mjs');
|
|
4446
|
-
const result = await verifyBackup(archivePath);
|
|
4447
|
-
if (result.ok) {
|
|
4448
|
-
ok('Backup verified — all checksums match.');
|
|
4449
|
-
} else {
|
|
4450
|
-
errorln(`Backup verification failed:\n ${result.errors.join('\n ')}`);
|
|
4451
|
-
process.exit(1);
|
|
4452
|
-
}
|
|
4453
|
-
return;
|
|
4454
|
-
}
|
|
4455
|
-
|
|
4456
|
-
if (sub === 'restore') {
|
|
4457
|
-
const archivePath = args[1];
|
|
4458
|
-
const yes = args.includes('--yes');
|
|
4459
|
-
if (!archivePath) { errorln('Usage: construct backup restore <archive> [--yes]'); process.exit(1); }
|
|
4460
|
-
if (!yes) {
|
|
4461
|
-
println(`About to restore from: ${archivePath}`);
|
|
4462
|
-
println('This will overwrite observations, sessions, config.env, and registry.json.');
|
|
4463
|
-
println('Press Ctrl+C to cancel, or re-run with --yes to proceed.');
|
|
4464
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
4465
|
-
}
|
|
4466
|
-
const { restoreBackup } = await import('../lib/storage/backup.mjs');
|
|
4467
|
-
const result = await restoreBackup(archivePath, { yes });
|
|
4468
|
-
if (result.ok) {
|
|
4469
|
-
ok(`Restored: ${result.restored.join(', ')}`);
|
|
4470
|
-
} else {
|
|
4471
|
-
if (result.restored.length) println(`Partially restored: ${result.restored.join(', ')}`);
|
|
4472
|
-
errorln(`Errors:\n ${result.errors.join('\n ')}`);
|
|
4473
|
-
process.exit(1);
|
|
4474
|
-
}
|
|
4475
|
-
return;
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
if (sub === 'list') {
|
|
4479
|
-
const { listBackups } = await import('../lib/storage/backup.mjs');
|
|
4480
|
-
const backups = listBackups();
|
|
4481
|
-
if (!backups.length) { println('No backups found.'); return; }
|
|
4482
|
-
for (const b of backups) {
|
|
4483
|
-
const mb = (b.size / 1024 / 1024).toFixed(1);
|
|
4484
|
-
println(` ${b.name} (${mb} MB) ${b.mtime.toISOString().slice(0, 19)}`);
|
|
4485
|
-
}
|
|
4486
|
-
return;
|
|
4487
|
-
}
|
|
4488
|
-
|
|
4489
|
-
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.');
|
|
4490
4462
|
process.exit(1);
|
|
4491
4463
|
}
|
|
4492
4464
|
|
|
@@ -5059,6 +5031,7 @@ async function readStdin() {
|
|
|
5059
5031
|
const handlers = new Map([
|
|
5060
5032
|
// Core
|
|
5061
5033
|
['dev', cmdUp],
|
|
5034
|
+
['dashboard', cmdServe],
|
|
5062
5035
|
['stop', cmdDown],
|
|
5063
5036
|
['status', cmdStatus],
|
|
5064
5037
|
['install', cmdSetup],
|
|
@@ -5080,6 +5053,7 @@ const handlers = new Map([
|
|
|
5080
5053
|
['diff', cmdDiff],
|
|
5081
5054
|
['distill', cmdDistill],
|
|
5082
5055
|
['ingest', cmdIngest],
|
|
5056
|
+
['export', cmdExport],
|
|
5083
5057
|
['infer', cmdInfer],
|
|
5084
5058
|
['search', cmdSearch],
|
|
5085
5059
|
['storage', cmdStorage],
|
|
@@ -5119,6 +5093,10 @@ const handlers = new Map([
|
|
|
5119
5093
|
['models', cmdModels],
|
|
5120
5094
|
['execution', cmdExecution],
|
|
5121
5095
|
['orchestrate', cmdOrchestrate],
|
|
5096
|
+
['acp', async () => {
|
|
5097
|
+
const { runAcpServer } = await import('../lib/acp/server.mjs');
|
|
5098
|
+
runAcpServer({ input: process.stdin, output: process.stdout, env: process.env, defaultCwd: process.cwd() });
|
|
5099
|
+
}],
|
|
5122
5100
|
['beads:stats', async (args) => {
|
|
5123
5101
|
const { getContentionStats, getHumanStatus } = await import('../lib/beads-optimistic.mjs');
|
|
5124
5102
|
const jsonOutput = args.includes('--json');
|
|
@@ -24,6 +24,7 @@ import path from 'node:path';
|
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
|
|
26
26
|
import { stageProjectAdapters } from '../lib/install/stage-project.mjs';
|
|
27
|
+
import { missingIgnorePatterns } from '../lib/host-disposition.mjs';
|
|
27
28
|
|
|
28
29
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const PKG_ROOT = path.resolve(HERE, '..');
|
|
@@ -70,23 +71,17 @@ try {
|
|
|
70
71
|
}
|
|
71
72
|
} catch { /* fall through */ }
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// stay project-only and land when the user runs `construct init` in a repo.
|
|
74
|
+
// ADR-0029: machine-scope writes are opt-in. The postinstall hook for a global
|
|
75
|
+
// install prints scope guidance and exits; `~/.claude/CLAUDE.md`,
|
|
76
|
+
// `~/.claude/settings.json`, and `~/.construct/*` land only when the user runs
|
|
77
|
+
// `construct install --scope=user`, so the consent point is visible.
|
|
78
78
|
|
|
79
79
|
if (process.env.npm_config_global === 'true' || process.env.npm_config_global === true) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
if (result.status !== 0) {
|
|
87
|
-
fail(`Global front-door sync failed (exit ${result.status}).`, 'Run `construct sync --global` to complete it.');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
80
|
+
log('global install detected; machine-scope setup is opt-in (ADR-0029)');
|
|
81
|
+
log('to wire ~/.construct/* and the front-door agent, run:');
|
|
82
|
+
log(' construct install --scope=user');
|
|
83
|
+
log('to set up a project, cd into it and run:');
|
|
84
|
+
log(' construct init');
|
|
90
85
|
process.exit(0);
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -114,6 +109,21 @@ try {
|
|
|
114
109
|
pkgVersion: PKG_VERSION,
|
|
115
110
|
log,
|
|
116
111
|
});
|
|
112
|
+
|
|
113
|
+
// ADR-0027: Ensure .gitignore covers the newly staged adapters (construct-f6l6).
|
|
114
|
+
// Idempotent: missingIgnorePatterns returns only patterns not already present.
|
|
115
|
+
const giPath = path.join(initCwd, '.gitignore');
|
|
116
|
+
const existing = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
117
|
+
const missing = missingIgnorePatterns(existing);
|
|
118
|
+
if (missing.length > 0) {
|
|
119
|
+
const HEADER = '# Construct — generated adapters, launcher, and runtime state.';
|
|
120
|
+
const SUBHEADER = '# Machine-specific, recreated by `construct sync`; never source (ADR-0027).';
|
|
121
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
122
|
+
const block = `${prefix}\n${HEADER}\n${SUBHEADER}\n${missing.join('\n')}\n`;
|
|
123
|
+
const { appendFileSync } = await import('node:fs');
|
|
124
|
+
appendFileSync(giPath, block, 'utf8');
|
|
125
|
+
log(`appended ${missing.length} Construct ignore pattern(s) to .gitignore`);
|
|
126
|
+
}
|
|
117
127
|
} catch (err) {
|
|
118
128
|
fail(`Adapter staging failed: ${err.message}`, 'The package is installed; run `npx construct init` in this project to complete setup.');
|
|
119
129
|
// Intentionally exit 0: staging is best-effort completion, and a non-zero exit
|