@geraldmaron/construct 1.0.15 → 1.0.17
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 +19 -3
- package/bin/construct +376 -98
- package/bin/construct-postinstall.mjs +17 -2
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/cli-commands.mjs +333 -147
- package/lib/contracts/validate.mjs +20 -10
- package/lib/contracts/violation-log.mjs +27 -12
- package/lib/embedded-contract/audit.mjs +52 -0
- package/lib/embedded-contract/capability.mjs +179 -0
- package/lib/embedded-contract/contract-version.mjs +39 -0
- package/lib/embedded-contract/envelope.mjs +70 -0
- package/lib/embedded-contract/index.mjs +71 -0
- package/lib/embedded-contract/ingest.mjs +77 -0
- package/lib/embedded-contract/model-resolve.mjs +186 -0
- package/lib/embedded-contract/redaction.mjs +91 -0
- package/lib/embedded-contract/role-facts.mjs +66 -0
- package/lib/embedded-contract/triage.mjs +134 -0
- package/lib/embedded-contract/workflow-defs.mjs +125 -0
- package/lib/embedded-contract/workflow-invoke.mjs +218 -0
- package/lib/hooks/config-protection.mjs +12 -5
- package/lib/hooks/stop-notify.mjs +7 -0
- package/lib/init-unified.mjs +36 -26
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/prepare.mjs +22 -4
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/roles/gateway.mjs +30 -1
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/insights.mjs +12 -0
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +73 -10
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/lib/validators/skills.mjs +21 -0
- package/package.json +9 -3
- package/personas/construct.md +2 -0
- package/platforms/claude/CLAUDE.md +43 -15
- package/scripts/sync-specialists.mjs +32 -5
- package/skills/ai/agent-dev.md +2 -0
- package/skills/ai/llm-security.md +2 -0
- package/skills/ai/ml-ops.md +2 -0
- package/skills/ai/orchestration-workflow.md +2 -0
- package/skills/ai/prompt-and-eval.md +2 -0
- package/skills/ai/prompt-optimizer.md +2 -0
- package/skills/ai/rag-system.md +2 -0
- package/skills/ai/trace-triage.md +36 -0
- package/skills/architecture/api-design.md +2 -0
- package/skills/architecture/caching.md +2 -0
- package/skills/architecture/cloud-native.md +2 -0
- package/skills/architecture/message-queue.md +2 -0
- package/skills/architecture/security-arch.md +2 -0
- package/skills/compliance/ai-disclosure.md +2 -0
- package/skills/compliance/data-privacy.md +2 -0
- package/skills/compliance/license-audit.md +2 -0
- package/skills/compliance/regulatory-review.md +2 -0
- package/skills/development/cpp.md +2 -0
- package/skills/development/go.md +2 -0
- package/skills/development/java.md +2 -0
- package/skills/development/kotlin.md +2 -0
- package/skills/development/mobile-crossplatform.md +2 -0
- package/skills/development/python.md +2 -0
- package/skills/development/rust.md +2 -0
- package/skills/development/shell.md +2 -0
- package/skills/development/swift.md +2 -0
- package/skills/development/typescript.md +2 -0
- package/skills/devops/ci-cd.md +2 -0
- package/skills/devops/containerization.md +2 -0
- package/skills/devops/cost-optimization.md +2 -0
- package/skills/devops/data-engineering.md +2 -0
- package/skills/devops/database.md +2 -0
- package/skills/devops/dependency-management.md +2 -0
- package/skills/devops/devsecops.md +2 -0
- package/skills/devops/git-workflow.md +2 -0
- package/skills/devops/incident-response.md +2 -0
- package/skills/devops/monorepo.md +2 -0
- package/skills/devops/observability.md +2 -0
- package/skills/devops/performance.md +2 -0
- package/skills/devops/testing.md +2 -0
- package/skills/docs/adr-workflow.md +2 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -0
- package/skills/docs/customer-profile-workflow.md +2 -0
- package/skills/docs/document-ingest-workflow.md +2 -0
- package/skills/docs/evidence-ingest-workflow.md +2 -0
- package/skills/docs/init-docs.md +2 -0
- package/skills/docs/init-project.md +2 -0
- package/skills/docs/memo-and-decision-capture.md +45 -0
- package/skills/docs/prd-workflow.md +2 -0
- package/skills/docs/prfaq-workflow.md +2 -0
- package/skills/docs/product-intelligence-review.md +2 -0
- package/skills/docs/product-intelligence-workflow.md +2 -0
- package/skills/docs/product-signal-workflow.md +2 -0
- package/skills/docs/research-workflow.md +2 -0
- package/skills/docs/runbook-workflow.md +2 -0
- package/skills/docs/strategy-workflow.md +2 -0
- package/skills/docs/transcript-synthesis.md +43 -0
- package/skills/exploration/dependency-graph-reading.md +2 -0
- package/skills/exploration/repo-map.md +2 -0
- package/skills/exploration/tracer-bullet-method.md +2 -0
- package/skills/exploration/unknown-codebase-onboarding.md +2 -0
- package/skills/frameworks/django.md +2 -0
- package/skills/frameworks/nextjs.md +2 -0
- package/skills/frameworks/react.md +2 -0
- package/skills/frameworks/spring-boot.md +2 -0
- package/skills/frontend-design/accessibility.md +2 -0
- package/skills/frontend-design/component-patterns.md +2 -0
- package/skills/frontend-design/engineering.md +2 -0
- package/skills/frontend-design/screen-reader-testing.md +34 -0
- package/skills/frontend-design/state-management.md +2 -0
- package/skills/frontend-design/ui-aesthetics.md +2 -0
- package/skills/frontend-design/ux-principles.md +2 -0
- package/skills/operating/change-management.md +2 -0
- package/skills/operating/incident-response.md +2 -0
- package/skills/operating/oncall-rotation.md +2 -0
- package/skills/operating/orchestration-reference.md +2 -0
- package/skills/operating/raw-data-structuring.md +44 -0
- package/skills/operating/unstructured-triage.md +45 -0
- package/skills/quality-gates/premortem.md +37 -0
- package/skills/quality-gates/review-work.md +2 -0
- package/skills/quality-gates/verify-change.md +2 -0
- package/skills/quality-gates/verify-module.md +2 -0
- package/skills/quality-gates/verify-quality.md +2 -0
- package/skills/quality-gates/verify-security.md +2 -0
- package/skills/roles/architect.ai-systems.md +2 -0
- package/skills/roles/architect.data.md +2 -0
- package/skills/roles/architect.enterprise.md +2 -0
- package/skills/roles/architect.integration.md +2 -0
- package/skills/roles/architect.md +2 -0
- package/skills/roles/architect.platform.md +2 -0
- package/skills/roles/data-analyst.experiment.md +2 -0
- package/skills/roles/data-analyst.md +2 -0
- package/skills/roles/data-analyst.product-intelligence.md +2 -0
- package/skills/roles/data-analyst.product.md +2 -0
- package/skills/roles/data-analyst.telemetry.md +2 -0
- package/skills/roles/data-engineer.pipeline.md +2 -0
- package/skills/roles/data-engineer.vector-retrieval.md +2 -0
- package/skills/roles/data-engineer.warehouse.md +2 -0
- package/skills/roles/debugger.md +2 -0
- package/skills/roles/designer.accessibility.md +2 -0
- package/skills/roles/designer.md +2 -0
- package/skills/roles/engineer.ai.md +2 -0
- package/skills/roles/engineer.data.md +2 -0
- package/skills/roles/engineer.md +2 -0
- package/skills/roles/engineer.platform.md +2 -0
- package/skills/roles/operator.docs.md +2 -0
- package/skills/roles/operator.md +2 -0
- package/skills/roles/operator.release.md +2 -0
- package/skills/roles/operator.sre.md +2 -0
- package/skills/roles/orchestrator.md +2 -0
- package/skills/roles/product-manager.ai-product.md +2 -0
- package/skills/roles/product-manager.business-strategy.md +2 -0
- package/skills/roles/product-manager.enterprise.md +2 -0
- package/skills/roles/product-manager.growth.md +2 -0
- package/skills/roles/product-manager.md +2 -0
- package/skills/roles/product-manager.platform.md +2 -0
- package/skills/roles/product-manager.product.md +2 -0
- package/skills/roles/qa.ai-eval.md +2 -0
- package/skills/roles/qa.api-contract.md +2 -0
- package/skills/roles/qa.data-pipeline.md +2 -0
- package/skills/roles/qa.md +2 -0
- package/skills/roles/qa.test-automation.md +2 -0
- package/skills/roles/qa.web-ui.md +2 -0
- package/skills/roles/researcher.explorer.md +2 -0
- package/skills/roles/researcher.md +2 -0
- package/skills/roles/researcher.ux.md +2 -0
- package/skills/roles/reviewer.devil-advocate.md +2 -0
- package/skills/roles/reviewer.evaluator.md +2 -0
- package/skills/roles/reviewer.md +2 -0
- package/skills/roles/reviewer.trace.md +2 -0
- package/skills/roles/security.ai.md +2 -0
- package/skills/roles/security.appsec.md +2 -0
- package/skills/roles/security.cloud.md +2 -0
- package/skills/roles/security.legal-compliance.md +2 -0
- package/skills/roles/security.md +2 -0
- package/skills/roles/security.privacy.md +2 -0
- package/skills/roles/security.supply-chain.md +2 -0
- package/skills/security/blue-team.md +2 -0
- package/skills/security/code-audit.md +2 -0
- package/skills/security/pentest.md +2 -0
- package/skills/security/red-team.md +2 -0
- package/skills/security/threat-intel.md +2 -0
- package/skills/security/vuln-research.md +2 -0
- package/skills/strategy/competitive-landscape.md +2 -0
- package/skills/strategy/jobs-to-be-done.md +38 -0
- package/skills/strategy/market-research-methods.md +2 -0
- package/skills/strategy/narrative-arc.md +2 -0
- package/skills/strategy/pricing-positioning.md +2 -0
- package/skills/utility/clean-code.md +2 -0
- package/specialists/prompts/cx-engineer.md +1 -1
- package/specialists/registry.json +18 -9
- package/specialists/role-manifests.json +1 -1
package/lib/service-manager.mjs
CHANGED
|
@@ -442,9 +442,22 @@ async function tryStartDockerDaemon({
|
|
|
442
442
|
return false;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
// The optional services a user can pick from with `construct dev --select` /
|
|
446
|
+
// `--only=`. Pressure Guard (cleanup) and Doctor (the L0 supervisor daemon)
|
|
447
|
+
// are infrastructure, always on — they are intentionally not selectable.
|
|
448
|
+
|
|
449
|
+
export const SELECTABLE_SERVICES = Object.freeze([
|
|
450
|
+
{ key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
|
|
451
|
+
{ key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
|
|
452
|
+
{ key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
|
|
453
|
+
{ key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
|
|
454
|
+
{ key: 'opencode', label: 'OpenCode', description: 'OpenCode bridge server.' },
|
|
455
|
+
]);
|
|
456
|
+
|
|
445
457
|
export async function startServices({
|
|
446
458
|
rootDir,
|
|
447
459
|
homeDir = os.homedir(),
|
|
460
|
+
selected = null,
|
|
448
461
|
describeRuntimeSupportFn = describeRuntimeSupport,
|
|
449
462
|
getRuntimePortsFn = getRuntimePorts,
|
|
450
463
|
startDashboardFn = startDashboard,
|
|
@@ -461,6 +474,11 @@ export async function startServices({
|
|
|
461
474
|
const ports = await getRuntimePortsFn(homeDir);
|
|
462
475
|
const envPath = getUserEnvPath(homeDir);
|
|
463
476
|
|
|
477
|
+
// A null selection means "start everything" (the default). A Set restricts
|
|
478
|
+
// startup to the chosen optional services; always-on infrastructure ignores it.
|
|
479
|
+
|
|
480
|
+
const wants = (key) => selected === null || selected.has(key);
|
|
481
|
+
|
|
464
482
|
// On macOS, auto-launch Docker Desktop when the CLI is present but the
|
|
465
483
|
// daemon is down. Polls up to 60s so downstream Postgres startup sees a working
|
|
466
484
|
// compose runner instead of "Docker not available".
|
|
@@ -487,7 +505,7 @@ export async function startServices({
|
|
|
487
505
|
const databaseUrl = liveEnv.DATABASE_URL || '';
|
|
488
506
|
const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
|
|
489
507
|
|
|
490
|
-
if (usesManagedPostgres) {
|
|
508
|
+
if (wants('postgres') && usesManagedPostgres) {
|
|
491
509
|
if (!isConstructPostgresRunning()) {
|
|
492
510
|
const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
|
|
493
511
|
if (pgStart.status === 'started') {
|
|
@@ -515,20 +533,23 @@ export async function startServices({
|
|
|
515
533
|
note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
|
|
516
534
|
});
|
|
517
535
|
}
|
|
518
|
-
} else if (databaseUrl) {
|
|
536
|
+
} else if (wants('postgres') && databaseUrl) {
|
|
519
537
|
results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
// Dashboard
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
541
|
+
if (wants('dashboard')) {
|
|
542
|
+
const dashboard = await startDashboardFn({ rootDir, homeDir, preferredPort: ports.dashboard });
|
|
543
|
+
results.push({
|
|
544
|
+
name: 'Dashboard',
|
|
545
|
+
url: dashboard.url,
|
|
546
|
+
status: dashboard.reused ? 'reused' : 'started',
|
|
547
|
+
});
|
|
548
|
+
}
|
|
529
549
|
|
|
530
550
|
const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
|
|
531
551
|
const traceBackend = resolveTraceBackend(liveEnv);
|
|
552
|
+
if (wants('telemetry')) {
|
|
532
553
|
if (traceBackend === 'local') {
|
|
533
554
|
results.push({
|
|
534
555
|
name: 'Telemetry',
|
|
@@ -561,21 +582,22 @@ export async function startServices({
|
|
|
561
582
|
} else {
|
|
562
583
|
results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
|
|
563
584
|
}
|
|
585
|
+
}
|
|
564
586
|
|
|
565
587
|
// Memory (cm)
|
|
566
|
-
if (support.cm) {
|
|
588
|
+
if (wants('memory') && support.cm) {
|
|
567
589
|
if (await memoryProbeFn(ports.memory)) {
|
|
568
590
|
results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'reused' });
|
|
569
591
|
} else {
|
|
570
592
|
spawnDetachedFn('cm', ['serve', '--port', String(ports.memory)], homeDir, 'cm.log');
|
|
571
593
|
results.push({ name: 'Memory (cm)', url: `http://127.0.0.1:${ports.memory}`, status: 'started' });
|
|
572
594
|
}
|
|
573
|
-
} else {
|
|
595
|
+
} else if (wants('memory')) {
|
|
574
596
|
results.push({ name: 'Memory (cm)', status: 'unavailable', note: 'cm not installed — run: construct init or brew install dicklesworthstone/tap/cm' });
|
|
575
597
|
}
|
|
576
598
|
|
|
577
599
|
// OpenCode (optional)
|
|
578
|
-
if (support.opencode) {
|
|
600
|
+
if (wants('opencode') && support.opencode) {
|
|
579
601
|
if (await openCodeProbeFn(ports.bridge)) {
|
|
580
602
|
results.push({ name: 'OpenCode', url: `http://127.0.0.1:${ports.bridge}`, status: 'reused' });
|
|
581
603
|
} else {
|
package/lib/setup.mjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/setup.mjs —
|
|
3
|
+
* lib/setup.mjs — Machine-scoped first-run setup wizard for Construct.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Installs cm/cass, configures managed defaults, starts local Postgres
|
|
6
|
+
* (consent-driven), pre-warms the embedding model, wires MCP integrations,
|
|
7
|
+
* and writes ~/.construct/config.env. Invoked by `construct install` and
|
|
8
|
+
* by lib/install/first-invocation.mjs when a TTY user is missing resources.
|
|
9
|
+
*
|
|
10
|
+
* Project-scoped scaffolding (`.cx/`, AGENTS.md, plan.md, adapters) lives
|
|
11
|
+
* in lib/init-unified.mjs — invoked by `construct init`. These two flows
|
|
12
|
+
* are intentionally separate: install runs once per machine, init runs
|
|
13
|
+
* once per repo.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import fs from 'node:fs';
|
|
@@ -36,10 +42,15 @@ const LOCAL_POSTGRES_DB = 'construct';
|
|
|
36
42
|
const LOCAL_DATABASE_URL = `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${LOCAL_POSTGRES_PORT}/${LOCAL_POSTGRES_DB}`;
|
|
37
43
|
|
|
38
44
|
function printHelp() {
|
|
39
|
-
console.log(`Construct setup
|
|
45
|
+
console.log(`Construct install — machine setup (once per machine)
|
|
40
46
|
|
|
41
47
|
Usage:
|
|
42
|
-
construct
|
|
48
|
+
construct install [--yes] [--no-docker] [--reconfigure]
|
|
49
|
+
|
|
50
|
+
Flags:
|
|
51
|
+
--yes accept detected defaults without prompting
|
|
52
|
+
--no-docker skip local Postgres / Docker service setup
|
|
53
|
+
--reconfigure re-prompt for service consent, ignoring cached answers
|
|
43
54
|
|
|
44
55
|
What it does:
|
|
45
56
|
- creates ~/.construct/config.env
|
|
@@ -52,7 +63,7 @@ What it does:
|
|
|
52
63
|
- runs construct doctor
|
|
53
64
|
- detects the project tech stack and writes .cx/project-profile.json
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
For project setup (once per repo): construct init`);
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
function runConstruct(argsList, { optional = false } = {}) {
|
|
@@ -465,24 +476,73 @@ export function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
|
|
|
465
476
|
};
|
|
466
477
|
}
|
|
467
478
|
|
|
479
|
+
// $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
|
|
480
|
+
// Stop-hook commands. Absent symlink → every Stop hook fails silently,
|
|
481
|
+
// including stop-notify (dashboard cost feed). Idempotent: matching
|
|
482
|
+
// symlink left alone, stale symlink replaced, real dir refused.
|
|
483
|
+
|
|
484
|
+
export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
485
|
+
const target = path.join(homeDir, '.construct', 'lib');
|
|
486
|
+
const source = path.join(rootDir, 'lib');
|
|
487
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
488
|
+
let stat = null;
|
|
489
|
+
try { stat = fs.lstatSync(target); } catch { stat = null; }
|
|
490
|
+
if (!stat) {
|
|
491
|
+
fs.symlinkSync(source, target, 'dir');
|
|
492
|
+
return { status: 'created', target, source };
|
|
493
|
+
}
|
|
494
|
+
if (stat.isSymbolicLink()) {
|
|
495
|
+
const current = fs.readlinkSync(target);
|
|
496
|
+
if (current === source) return { status: 'kept', target, source };
|
|
497
|
+
fs.unlinkSync(target);
|
|
498
|
+
fs.symlinkSync(source, target, 'dir');
|
|
499
|
+
return { status: 'replaced', target, source, previous: current };
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
status: 'conflict',
|
|
503
|
+
target,
|
|
504
|
+
message: `${target} exists and is not a symlink — leaving alone. Move or remove it, then re-run \`construct install\` to wire the hooks.`,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
468
508
|
export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
|
|
469
509
|
const argSet = new Set(args);
|
|
470
510
|
const isYes = argSet.has('--yes');
|
|
471
511
|
const skipDocker = argSet.has('--no-docker');
|
|
512
|
+
const reconfigure = argSet.has('--reconfigure');
|
|
472
513
|
|
|
473
514
|
if (argSet.has('--help') || argSet.has('-h')) {
|
|
474
515
|
printHelp();
|
|
475
516
|
return;
|
|
476
517
|
}
|
|
477
518
|
|
|
519
|
+
// Reject unknown flags loudly so a typo (e.g. --reconfig) fails fast with the
|
|
520
|
+
// help text instead of silently running defaults.
|
|
521
|
+
|
|
522
|
+
const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--reconfigure', '--help', '-h']);
|
|
523
|
+
const unknownFlags = args.filter((a) => a.startsWith('-') && !KNOWN_FLAGS.has(a));
|
|
524
|
+
if (unknownFlags.length) {
|
|
525
|
+
console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
|
|
526
|
+
printHelp();
|
|
527
|
+
throw new Error(`Unknown setup flag(s): ${unknownFlags.join(', ')}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
478
530
|
console.log('Construct setup');
|
|
479
531
|
console.log('────────────────');
|
|
480
532
|
|
|
481
533
|
const envPath = ensureUserConfig(homeDir);
|
|
482
534
|
const opencodePath = ensureOpenCodeConfig();
|
|
535
|
+
const libLink = ensureLibSymlink({ homeDir, rootDir });
|
|
483
536
|
|
|
484
537
|
console.log(`User config: ${envPath}`);
|
|
485
538
|
console.log(`OpenCode config: ${opencodePath}`);
|
|
539
|
+
if (libLink.status === 'created' || libLink.status === 'replaced') {
|
|
540
|
+
console.log(`Hook lib link: ${libLink.target} → ${libLink.source} (${libLink.status})`);
|
|
541
|
+
} else if (libLink.status === 'kept') {
|
|
542
|
+
console.log(`Hook lib link: ${libLink.target} already in place`);
|
|
543
|
+
} else if (libLink.status === 'conflict') {
|
|
544
|
+
console.log(`Hook lib link: ${libLink.message}`);
|
|
545
|
+
}
|
|
486
546
|
warnIfGlobalCommandIsUnavailable();
|
|
487
547
|
|
|
488
548
|
const cmInstall = ensureCmInstalled({ env: process.env });
|
|
@@ -521,6 +581,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
521
581
|
const pgConsent = await consentToInstall({
|
|
522
582
|
name: 'postgres',
|
|
523
583
|
isYes,
|
|
584
|
+
force: reconfigure,
|
|
524
585
|
alreadyConfigured: Boolean(process.env.DATABASE_URL),
|
|
525
586
|
alreadyConfiguredNote: 'DATABASE_URL already set — using external database.',
|
|
526
587
|
envPath,
|
|
@@ -576,12 +637,14 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
576
637
|
const cheapestConsent = await consentToInstall({
|
|
577
638
|
name: 'cheapest-provider',
|
|
578
639
|
isYes,
|
|
640
|
+
force: reconfigure,
|
|
579
641
|
alreadyConfigured: false,
|
|
580
642
|
envPath,
|
|
581
643
|
defaultYes: false,
|
|
582
644
|
});
|
|
583
645
|
if (cheapestConsent.decision) {
|
|
584
646
|
try {
|
|
647
|
+
const { applyToEnv } = await import('./model-router.mjs');
|
|
585
648
|
const selections = await selectCheapestForAllTiers({ env: process.env });
|
|
586
649
|
const applied = {};
|
|
587
650
|
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
@@ -637,7 +700,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
637
700
|
|
|
638
701
|
if (isYes) {
|
|
639
702
|
console.log('\nManaged setup:');
|
|
640
|
-
console.log(` Deployment mode: ${getDeploymentMode(env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
703
|
+
console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
641
704
|
console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
|
|
642
705
|
console.log(` Vector model: ${managedValues.CONSTRUCT_VECTOR_MODEL}`);
|
|
643
706
|
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}`);
|
|
@@ -723,12 +786,12 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
723
786
|
runConstruct(['mcp', 'add', 'github', '--auto'], { optional: true });
|
|
724
787
|
} else {
|
|
725
788
|
console.log('\nManaged defaults written:');
|
|
726
|
-
console.log(` Deployment mode: ${getDeploymentMode(env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
789
|
+
console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
727
790
|
console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
|
|
728
791
|
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
|
|
729
792
|
console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
|
|
730
793
|
console.log('\nFor unattended setup, including local Postgres when Docker is running:');
|
|
731
|
-
console.log(' construct
|
|
794
|
+
console.log(' construct install --yes');
|
|
732
795
|
}
|
|
733
796
|
|
|
734
797
|
runConstruct(['sync']);
|
package/lib/update.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
|
|
13
|
+
import { writeVersionStamp } from './maintenance/cleanup.mjs';
|
|
14
|
+
|
|
13
15
|
const PACKAGE_NAME = '@geraldmaron/construct';
|
|
14
16
|
|
|
15
17
|
function readPackageJson(dir) {
|
|
@@ -70,6 +72,15 @@ export function buildUpdatePlan({ cwd }) {
|
|
|
70
72
|
args: [binPath, 'sync', '--no-docs'],
|
|
71
73
|
cwd: sourceRoot,
|
|
72
74
|
},
|
|
75
|
+
// Sweep legacy cx-*.md/.toml/.prompt.md left over from older releases
|
|
76
|
+
// at user scope, so the next doctor doesn't surface the warning.
|
|
77
|
+
{
|
|
78
|
+
label: 'Sweep legacy agents at user scope',
|
|
79
|
+
command: process.execPath,
|
|
80
|
+
args: [binPath, 'doctor', '--fix-legacy-agents'],
|
|
81
|
+
cwd: sourceRoot,
|
|
82
|
+
optional: true,
|
|
83
|
+
},
|
|
73
84
|
{
|
|
74
85
|
label: 'Run health checks',
|
|
75
86
|
command: process.execPath,
|
|
@@ -84,14 +95,24 @@ function formatCommand(command, args) {
|
|
|
84
95
|
return [command, ...args].join(' ');
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
function runStep(step, { spawn, env }) {
|
|
98
|
+
function runStep(step, { spawn, env, stdout }) {
|
|
88
99
|
const result = spawn(step.command, step.args, {
|
|
89
100
|
cwd: step.cwd,
|
|
90
101
|
env,
|
|
91
102
|
stdio: 'inherit',
|
|
92
103
|
});
|
|
93
|
-
if (result?.error)
|
|
104
|
+
if (result?.error) {
|
|
105
|
+
if (step.optional) {
|
|
106
|
+
stdout?.write?.(` (skipped: ${result.error.message})\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw result.error;
|
|
110
|
+
}
|
|
94
111
|
if ((result?.status ?? 1) !== 0) {
|
|
112
|
+
if (step.optional) {
|
|
113
|
+
stdout?.write?.(` (skipped: exit ${result?.status ?? 1})\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
95
116
|
throw new Error(`${step.label} failed with exit code ${result.status ?? 1}`);
|
|
96
117
|
}
|
|
97
118
|
}
|
|
@@ -107,9 +128,16 @@ export function runUpdate({ cwd, env = process.env, spawn = spawnSync, stdout =
|
|
|
107
128
|
for (const step of plan.steps) {
|
|
108
129
|
stdout.write(`→ ${step.label}\n`);
|
|
109
130
|
stdout.write(` ${formatCommand(step.command, step.args)}\n`);
|
|
110
|
-
runStep(step, { spawn, env });
|
|
131
|
+
runStep(step, { spawn, env, stdout });
|
|
111
132
|
}
|
|
112
133
|
|
|
134
|
+
// Advance the cleanup stamp to the checkout version so the next CLI
|
|
135
|
+
// invocation doesn't run a full cleanup pass on the assumption that
|
|
136
|
+
// an upgrade just happened.
|
|
137
|
+
try {
|
|
138
|
+
writeVersionStamp({ version: plan.version, summary: { freedBytes: 0, durationMs: 0 } });
|
|
139
|
+
} catch { /* stamp write is best-effort */ }
|
|
140
|
+
|
|
113
141
|
stdout.write('\n✓ Construct updated from current checkout.\n');
|
|
114
142
|
return plan;
|
|
115
143
|
}
|
package/lib/upgrade.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { writeVersionStamp } from './maintenance/cleanup.mjs';
|
|
14
|
+
|
|
13
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
15
17
|
const PACKAGE_NAME = '@geraldmaron/construct';
|
|
@@ -97,7 +99,7 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
stdout.write('Upgrading...\n\n');
|
|
100
|
-
|
|
102
|
+
|
|
101
103
|
const steps = [
|
|
102
104
|
{
|
|
103
105
|
label: 'Fetching latest from npm',
|
|
@@ -109,6 +111,15 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
|
|
|
109
111
|
command: 'construct',
|
|
110
112
|
args: ['sync', '--no-docs'],
|
|
111
113
|
},
|
|
114
|
+
// Sweep legacy cx-*.md/.toml/.prompt.md left behind by older releases at
|
|
115
|
+
// user scope. Without this the user gets a doctor warning every session
|
|
116
|
+
// until they manually run `construct doctor --fix-legacy-agents`.
|
|
117
|
+
{
|
|
118
|
+
label: 'Sweeping legacy agents at user scope',
|
|
119
|
+
command: 'construct',
|
|
120
|
+
args: ['doctor', '--fix-legacy-agents'],
|
|
121
|
+
optional: true,
|
|
122
|
+
},
|
|
112
123
|
{
|
|
113
124
|
label: 'Running health checks',
|
|
114
125
|
command: 'construct',
|
|
@@ -123,18 +134,34 @@ export function runUpgrade({ cwd = process.cwd(), env = process.env, stdout = pr
|
|
|
123
134
|
env,
|
|
124
135
|
stdio: 'inherit',
|
|
125
136
|
});
|
|
126
|
-
|
|
137
|
+
|
|
127
138
|
if (result.error) {
|
|
139
|
+
if (step.optional) {
|
|
140
|
+
stdout.write(` (skipped: ${result.error.message})\n`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
128
143
|
stdout.write(`\n✗ ${step.label} failed: ${result.error.message}\n`);
|
|
129
144
|
return { success: false, reason: 'step_failed', step: step.label };
|
|
130
145
|
}
|
|
131
|
-
|
|
146
|
+
|
|
132
147
|
if (result.status !== 0) {
|
|
148
|
+
if (step.optional) {
|
|
149
|
+
stdout.write(` (skipped: exit ${result.status})\n`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
133
152
|
stdout.write(`\n✗ ${step.label} failed with exit code ${result.status}\n`);
|
|
134
153
|
return { success: false, reason: 'step_failed', step: step.label };
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
|
-
|
|
156
|
+
|
|
157
|
+
// Advance the cleanup stamp so the next CLI invocation doesn't re-run a
|
|
158
|
+
// full cleanup pass under the assumption that the user just upgraded.
|
|
159
|
+
// Without this, every command pays the cleanup cost until something
|
|
160
|
+
// else writes the stamp.
|
|
161
|
+
try {
|
|
162
|
+
writeVersionStamp({ version: latestVersion, summary: { freedBytes: 0, durationMs: 0 } });
|
|
163
|
+
} catch { /* stamp write is best-effort */ }
|
|
164
|
+
|
|
138
165
|
stdout.write('\n');
|
|
139
166
|
stdout.write(`✓ Upgraded to ${latestVersion}\n`);
|
|
140
167
|
return { success: true, upgraded: true, from: currentVersion, to: latestVersion };
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* ---
|
|
7
7
|
* name: <kebab-case>
|
|
8
8
|
* description: "<≤1024 chars, includes a 'use when' trigger>"
|
|
9
|
+
* [inputs: [<string>, …] — optional, surfaced by capability discovery]
|
|
10
|
+
* [artifactType: <string> — optional, surfaced by capability discovery]
|
|
9
11
|
* [role/applies_to/inherits/version/profiles/cap for role files]
|
|
10
12
|
* ---
|
|
11
13
|
* # <Title>
|
|
@@ -100,6 +102,23 @@ function validateDescription(description) {
|
|
|
100
102
|
return null;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// Optional embedded-contract metadata: surfaced by capability discovery when
|
|
106
|
+
// present. Validated only when present so the field stays opt-in and is
|
|
107
|
+
// populated incrementally — inputs is a list of strings, artifactType a string.
|
|
108
|
+
|
|
109
|
+
function validateOptionalMetadata(frontmatter) {
|
|
110
|
+
const issues = [];
|
|
111
|
+
if ('inputs' in frontmatter && frontmatter.inputs !== null) {
|
|
112
|
+
if (!Array.isArray(frontmatter.inputs) || frontmatter.inputs.some((i) => typeof i !== 'string')) {
|
|
113
|
+
issues.push('frontmatter.inputs must be a list of strings when present');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if ('artifactType' in frontmatter && frontmatter.artifactType !== null && typeof frontmatter.artifactType !== 'string') {
|
|
117
|
+
issues.push('frontmatter.artifactType must be a string when present');
|
|
118
|
+
}
|
|
119
|
+
return issues;
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
export function validateSkills(roots) {
|
|
104
123
|
const dirs = Array.isArray(roots) ? roots : [roots];
|
|
105
124
|
const errors = [];
|
|
@@ -150,6 +169,8 @@ export function validateSkills(roots) {
|
|
|
150
169
|
const descErr = validateDescription(frontmatter.description);
|
|
151
170
|
if (descErr) errors.push(`${rel}: ${descErr}`);
|
|
152
171
|
|
|
172
|
+
for (const issue of validateOptionalMetadata(frontmatter)) errors.push(`${rel}: ${issue}`);
|
|
173
|
+
|
|
153
174
|
const { title, opener } = extractTitleAndOpener(body);
|
|
154
175
|
|
|
155
176
|
if (!title) {
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geraldmaron/construct",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
|
|
6
6
|
"bin": {
|
|
7
7
|
"construct": "bin/construct"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/embedded-contract/index.mjs",
|
|
11
|
+
"./embedded-contract": "./lib/embedded-contract/index.mjs",
|
|
12
|
+
"./lib/*": "./lib/*",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
16
|
"bin/construct",
|
|
11
17
|
"bin/construct-postinstall.mjs",
|
|
@@ -78,8 +84,8 @@
|
|
|
78
84
|
},
|
|
79
85
|
"optionalDependencies": {
|
|
80
86
|
"@opentelemetry/api": "^1.9.0",
|
|
81
|
-
"@opentelemetry/core": "^
|
|
82
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
87
|
+
"@opentelemetry/core": "^2.7.1",
|
|
88
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
83
89
|
"@opentelemetry/resources": "^1.25.0",
|
|
84
90
|
"@opentelemetry/sdk-trace-node": "^1.25.0",
|
|
85
91
|
"@opentelemetry/semantic-conventions": "^1.25.0"
|
package/personas/construct.md
CHANGED
|
@@ -34,6 +34,8 @@ Execution model:
|
|
|
34
34
|
|
|
35
35
|
Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
|
|
36
36
|
|
|
37
|
+
Orchestrated dispatches emit a task-packet with `goal`, `intent`, `workCategory`, `riskFlags`, `acceptanceCriteria` before naming specialists (`specialists/contracts.json:construct-to-orchestrator`).
|
|
38
|
+
|
|
37
39
|
## Gates and contracts (org-in-a-box)
|
|
38
40
|
|
|
39
41
|
`routeRequest` returns three artifacts; honor all three:
|
|
@@ -1,30 +1,58 @@
|
|
|
1
1
|
# Construct: Claude Code Integration
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This file ships as a reference template alongside the Construct npm package. The canonical project-instructions file is `/CLAUDE.md` at your repo root — that's the file Claude Code actually reads at session start. The structure here mirrors that canonical file so users know what shape their own `/CLAUDE.md` should take after `construct init` scaffolds a project.
|
|
4
|
+
|
|
5
|
+
Personas and specialists are defined in `specialists/registry.json` and rendered into Claude Code agents on every `construct sync`. The single user-facing persona is `construct`; the 28 specialists (cx-engineer, cx-security, cx-devil-advocate, …) are routed internally — you address Construct, Construct dispatches.
|
|
6
|
+
|
|
7
|
+
## Critical rules (mirror in your project /CLAUDE.md)
|
|
8
|
+
|
|
9
|
+
- **Never fabricate.** Every load-bearing claim cites a verifiable source. When a fact isn't in the source, write `unknown` or `[unverified]`. See `rules/common/no-fabrication.md`.
|
|
10
|
+
- **Confirm the working branch every session.** Session-start surfaces `## Working branch: <name>` at the top of the injected context.
|
|
11
|
+
- **Never commit, push, or merge without asking first.** State the branch, state the action, wait for explicit yes.
|
|
12
|
+
- **Never edit running hook files** without testing them in isolation. A broken hook blocks all tool use.
|
|
13
|
+
- **Hooks fire unconditionally. No skip env vars on quality gates.** If a check fires wrong, repair the check — do not re-introduce `CONSTRUCT_SKIP_*` / `CONSTRUCT_ALLOW_*`.
|
|
14
|
+
- **Never commit directly to main.** Branch, test, then merge.
|
|
15
|
+
- **Run `construct doctor`** after structural changes.
|
|
4
16
|
|
|
5
17
|
## Workflow roles
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
| Role | What it covers |
|
|
20
|
+
|---|---|
|
|
21
|
+
| **Planning** | Requirements, strategy, architecture, framing challenges |
|
|
22
|
+
| **Implementation** | Builds features and fixes bugs |
|
|
23
|
+
| **Validation** | Quality gates, code review, security, accessibility |
|
|
24
|
+
| **Research** | Docs, debugging, codebase exploration, external research |
|
|
25
|
+
| **Operations** | Releases, dev servers, health checks, observability |
|
|
26
|
+
|
|
27
|
+
Construct routes complex work through the full pipeline (plan → implement → validate → operate). For simple tasks, Construct acts directly without exposing the internal routing.
|
|
28
|
+
|
|
29
|
+
## Beads issue tracker
|
|
30
|
+
|
|
31
|
+
This project uses **bd (beads)** for issue tracking. Run `bd prime` for the full workflow context.
|
|
8
32
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
```bash
|
|
34
|
+
bd ready # Find available work
|
|
35
|
+
bd show <id> # View issue details
|
|
36
|
+
bd update <id> --claim # Claim work
|
|
37
|
+
bd close <id> # Complete work
|
|
38
|
+
```
|
|
14
39
|
|
|
15
|
-
|
|
40
|
+
Use `bd` for all task tracking. Do NOT use TodoWrite, TaskCreate, or markdown TODO lists.
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
plan → implement → validate → operate
|
|
42
|
+
## Session completion
|
|
19
43
|
|
|
20
|
-
|
|
44
|
+
When ending a work session, **work is not complete until `git push` succeeds.**
|
|
21
45
|
|
|
22
|
-
|
|
46
|
+
```bash
|
|
47
|
+
git pull --rebase
|
|
48
|
+
git push
|
|
49
|
+
git status # MUST show "up to date with origin"
|
|
50
|
+
```
|
|
23
51
|
|
|
24
|
-
## Tool
|
|
52
|
+
## Tool calls
|
|
25
53
|
|
|
26
54
|
When using Bash, always provide both `command` and `description` string fields. Do not emit XML-style fallback tool calls.
|
|
27
55
|
|
|
28
|
-
## Cross-
|
|
56
|
+
## Cross-tool memory
|
|
29
57
|
|
|
30
|
-
Construct uses cass-memory for cross-tool memory. Start the local HTTP server with `cm serve`, then
|
|
58
|
+
Construct uses cass-memory for cross-tool memory. Start the local HTTP server with `cm serve`, then `memory_search` to find prior context. Memory and beads are complementary: memory holds knowledge, beads hold tasks.
|
|
@@ -172,8 +172,17 @@ function detectConstructProject(cwd) {
|
|
|
172
172
|
|
|
173
173
|
const detectedProject = (!PROJECT_FLAG && !GLOBAL_FLAG) ? detectConstructProject(process.cwd()) : null;
|
|
174
174
|
const projectDir = PROJECT_FLAG ? process.cwd() : detectedProject;
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
|
|
176
|
+
// Lock and staging are scoped to the tier we actually mutate: a project dir for
|
|
177
|
+
// project-tier writes, the user's HOME for global-tier writes (which land in
|
|
178
|
+
// ~/.claude, ~/.codex, …). Keying the global tier to HOME — not the repo root —
|
|
179
|
+
// means two --global syncs against different HOMEs (e.g. parallel test files in
|
|
180
|
+
// isolated sandboxes) never collide on a shared repo-root lock, and staging
|
|
181
|
+
// renames stay on the same filesystem as their destinations.
|
|
182
|
+
|
|
183
|
+
const stateBase = projectDir || home;
|
|
184
|
+
const lockPath = path.join(stateBase, ".cx", "sync.lock");
|
|
185
|
+
const stagingDir = path.join(stateBase, ".cx", "sync-staging");
|
|
177
186
|
|
|
178
187
|
// Project-tier writes carry every registry entry. Global-tier writes carry only
|
|
179
188
|
// the `construct` front-door agent — specialists live with the project, not the
|
|
@@ -243,7 +252,7 @@ function writeFile(file, content, { stamp = true } = {}) {
|
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
// Two-phase: write to staging, commit later.
|
|
246
|
-
const rel = path.relative(
|
|
255
|
+
const rel = path.relative(stateBase, file);
|
|
247
256
|
const stagingPath = path.join(stagingDir, rel);
|
|
248
257
|
mkdirp(path.dirname(stagingPath));
|
|
249
258
|
fs.writeFileSync(stagingPath, stamped);
|
|
@@ -950,7 +959,16 @@ function getVSCodeSettingsPaths() {
|
|
|
950
959
|
path.join(appData, "Code - Insiders", "User", "settings.json"),
|
|
951
960
|
);
|
|
952
961
|
}
|
|
953
|
-
|
|
962
|
+
// settings.json present → ready to merge. settings.json absent but the
|
|
963
|
+
// VS Code User dir exists → installed without ever opening Preferences,
|
|
964
|
+
// and global sync still needs to write the MCP block. The User dir is
|
|
965
|
+
// the installation signal; bootstrap a minimal settings.json there.
|
|
966
|
+
|
|
967
|
+
return candidates.filter((candidate) => {
|
|
968
|
+
if (fs.existsSync(candidate)) return true;
|
|
969
|
+
const userDir = path.dirname(candidate);
|
|
970
|
+
return fs.existsSync(userDir);
|
|
971
|
+
});
|
|
954
972
|
}
|
|
955
973
|
|
|
956
974
|
function syncVSCode(targetDir = null) {
|
|
@@ -991,7 +1009,16 @@ function syncVSCode(targetDir = null) {
|
|
|
991
1009
|
let synced = false;
|
|
992
1010
|
for (const settingsPath of settingsPaths) {
|
|
993
1011
|
try {
|
|
994
|
-
|
|
1012
|
+
let settings;
|
|
1013
|
+
if (fs.existsSync(settingsPath)) {
|
|
1014
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
1015
|
+
} else {
|
|
1016
|
+
// VS Code installed (User dir present) but never opened → seed an
|
|
1017
|
+
// empty settings.json. Single object literal, no other keys, so
|
|
1018
|
+
// a later launch of VS Code finds it valid and writes user prefs
|
|
1019
|
+
// alongside.
|
|
1020
|
+
settings = {};
|
|
1021
|
+
}
|
|
995
1022
|
if (!settings["github.copilot.mcpServers"]) settings["github.copilot.mcpServers"] = {};
|
|
996
1023
|
const mcpServers = settings["github.copilot.mcpServers"];
|
|
997
1024
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
package/skills/ai/agent-dev.md
CHANGED
package/skills/ai/ml-ops.md
CHANGED